// ── Integration samples ────────────────────────────────────────────────── function cleanBaseUrl(url) { return String(url || '').trim().replace(/\/+$/, ''); } function getTtsBaseUrl() { return cleanBaseUrl($('s-tts-url')?.value) || 'http://localhost:8020'; } function getTtsV1Url() { const base = getTtsBaseUrl(); return base.endsWith('/v1') ? base : base + '/v1'; } function getTtsStreamBaseUrl() { return cleanBaseUrl($('s-tts-stream-url')?.value || _appSettings.tts_stream_url) || 'http://localhost:8023'; } function getTtsStreamV1Url() { const base = getTtsStreamBaseUrl(); return base.endsWith('/v1') ? base : base + '/v1'; } function getCreatorV1Url() { const loc = window.location; const protocol = loc.protocol || 'http:'; const port = loc.port ? ':' + loc.port : ''; const host = loc.hostname === '0.0.0.0' ? 'localhost' : loc.hostname; return `${protocol}//${host}${port}/v1`; } function updateCreatorUrlHints() { const warning = $('routing-url-warning'); const badBindHost = window.location.hostname === '0.0.0.0'; if (warning) warning.classList.toggle('show', badBindHost); } function activeVoiceIds() { return (_voices || []) .filter(v => v.enabled !== false) .slice() .sort((a, b) => a.id.localeCompare(b.id)) .map(v => v.id); } function integrationVoiceExample() { return activeVoiceIds()[0] || 'EN_F_ExampleVoice'; } function integrationVoiceList() { const ids = activeVoiceIds(); return ids.length ? ids.join(', ') : 'EN_F_ExampleVoice, DE_M_ExampleVoice'; } function virtualDesignVoiceIds() { return Object.keys(loadDesignPresets ? loadDesignPresets() : {}) .sort((a,b)=>a.localeCompare(b)) .map(name => 'vd_' + name.replace(/[^A-Za-z0-9_.-]+/g, '_').replace(/^_+|_+$/g, '')); } function renderIntegrationSnippets() { if (!$('snippet-sillytavern')) return; const base = getTtsBaseUrl(); const v1 = getTtsV1Url(); const streamV1 = getTtsStreamV1Url(); const proxyV1 = getCreatorV1Url(); const proxyBase = proxyV1.replace(/\/v1$/, ''); updateCreatorUrlHints(); const voice = integrationVoiceExample(); const voices = integrationVoiceList(); const vdVoices = virtualDesignVoiceIds(); const vdVoice = vdVoices[0] || 'vd_EN_F_Warm_Narrator'; $('integration-url-label').textContent = 'TTS backend: ' + base; $('snippet-sillytavern').textContent = `Provider: OpenAI compatible TTS API base URL: ${v1} API key: dummy Model: qwen3-tts Voice: ${voice} Custom voices: ${voices} Streaming backend, if your SillyTavern TTS extension supports progressive playback: API base URL: ${streamV1} Endpoint: /audio/speech Format: wav`; $('snippet-streaming-howto').textContent = `Direct streaming backend, no creator routing: API base URL: ${streamV1} Endpoint: /audio/speech Model: tts-1 Voice: ${voice} Response format: wav Requirement: the app must start playback while the HTTP response is still arriving. Current 8023 streaming service ignores per-request instruct/style text. Streaming through TTS Voice Creator routing: API base URL: ${proxyV1} Voice: default or another incoming route voice Routing tab: set Backend = Streaming for the matching rule Response format: wav Avoid before/after sounds for true streaming; route sounds and MP3 require buffering. SillyTavern note: Use OpenAI-compatible TTS if your extension supports progressive audio responses. If it waits for the whole file before playing, streaming works technically but will feel like buffered TTS.`; $('snippet-open-webui').textContent = `Admin settings -> Audio -> Text-to-Speech Engine/provider: OpenAI compatible API base URL: ${proxyV1} API key: dummy Model: tts-1 Voice: default Routing tab example: default + EN -> ${voice} default + DE -> DE_M_YourGermanVoice`; $('snippet-home-assistant').textContent = `Home Assistant OpenAI TTS agent: Base URL: ${proxyV1} API key: dummy Model: tts-1 Voice: default Extra JSON payload: {"app":"Home Assistant"} Audio format: mp3 or wav REST command example using routing: rest_command: routed_tts: url: "${proxyV1}/audio/speech" method: POST content_type: "application/json" headers: Authorization: "Bearer dummy" payload: > {"model":"tts-1","voice":"default","input":"{{ text }}","response_format":"mp3","app":"Home Assistant"} Important: use this Creator proxy URL, not the direct Qwen3 backend URL ${v1}. Routing only runs through the Creator proxy.`; $('snippet-curl').textContent = `curl -s "${v1}/models" curl -s "${v1}/audio/speech" \\ -H "Authorization: Bearer dummy" \\ -H "Content-Type: application/json" \\ -d '{"model":"qwen3-tts","voice":"${voice}","input":"Hello from Qwen3 TTS","response_format":"wav"}' \\ --output qwen3-tts-test.wav`; $('snippet-voice-design-proxy').textContent = `Virtual VoiceDesign mode - no WAV export needed Use this app as the OpenAI-compatible TTS endpoint: API base URL: ${proxyV1} API key: dummy Model: tts-1 Voice: ${vdVoice} Available virtual voices: ${vdVoices.length ? vdVoices.join(', ') : 'Save a Voice Design prompt preset first.'} curl -s "${proxyV1}/models" curl -s "${proxyV1}/audio/speech" \\ -H "Authorization: Bearer dummy" \\ -H "Content-Type: application/json" \\ -d '{"model":"tts-1","voice":"${vdVoice}","input":"This line is generated through the VoiceDesign container.","response_format":"wav"}' \\ --output voicedesign-virtual.wav`; if ($('snippet-mcp-claude-cmd')) $('snippet-mcp-claude-cmd').textContent = `claude mcp add voice-creator \\ --transport http \\ --url ${proxyBase}/mcp \\ --header "X-Voice-Creator-Client-Id: claude-code"`; if ($('snippet-mcp-claude-config')) $('snippet-mcp-claude-config').textContent = `{ "mcpServers": { "voice-creator": { "url": "${proxyBase}/mcp", "headers": { "X-Voice-Creator-Client-Id": "my-agent" } } } }`; if ($('snippet-speak')) $('snippet-speak').textContent = `# Bind your client ID to a voice once (persisted in Settings) curl -X PUT ${proxyBase}/speak/bindings/my-script \\ -H "Content-Type: application/json" \\ -d '{"voice":"${voice}"}' # Generate speech — voice from binding, persona rewrite optional curl -X POST ${proxyBase}/speak \\ -H "Content-Type: application/json" \\ -H "X-Voice-Creator-Client-Id: my-script" \\ -d '{"text":"Hello world","apply_persona":true}' \\ --output speech.wav # Or specify voice directly curl -X POST ${proxyBase}/speak \\ -H "Content-Type: application/json" \\ -d '{"text":"Hello world","voice":"${voice}"}' \\ --output speech.wav`; } document.querySelectorAll('.copy-snippet').forEach(btn => btn.addEventListener('click', async () => { const el = $(btn.dataset.snippet); if (!el) return; await copyText(el.textContent); toast('Snippet copied', 'success'); })); $('show-api-btn')?.addEventListener('click', () => { window.open('/docs', '_blank', 'noopener'); }); $('integration-refresh-btn')?.addEventListener('click', () => { renderIntegrationSnippets(); toast('Integration examples refreshed', 'success'); }); $('copy-active-voices-btn-integrations')?.addEventListener('click', async () => { const active = activeVoiceIds(); if (!active.length) { toast('No active voices to copy', 'error'); return; } await copyText(active.join(', ')); toast('Copied ' + active.length + ' active voices', 'success'); status('Copied active voices to clipboard'); });