tts-voice-creator-clone-and.../static/js/integrations.js
mARTin-B78 d99395480a Add Script Rehearser; clean Connect Apps; About changelog; rework Try It Out
## Script Rehearser (new feature)
- New section s-rehearser.html + rehearser.js + nav/loader wiring
- Phase 1: paste/upload script (.txt), auto-detect characters from
  'CHARACTER: dialog' or ALL-CAPS screenplay format
- Phase 2: assign a TTS voice per character, or mark 'I play this'
- Phase 3: step-through rehearsal — synthesizes other characters via TTS,
  shows level-meter + oscilloscope for your own lines, records them from mic
- Phase 4: session summary with per-line audio playback + download

## Connect Apps
- Removed duplicate standalone MCP/speak/hotkey full-width cards
- Kept the integration-grid cards (they use the real server URL from JS)
- Added Global Hotkey Daemon as a proper integration card with snippet-hotkey
  populated by integrations.js (uses proxyBase URL dynamically)

## About page
- GET /api/changelog endpoint reads CHANGELOG.md and returns it as text
- Collapsible 'Changelog' <details> card fetches and displays it lazily

## Try It Out
- Reorganised into three cards: Voice & backend / Text to synthesize / Generate
- Backend help panel moved below the voice row (not in the same flex row)
- Style instruction field gains a dynamic badge ('style-aware ✓' / 'weak style')
  and a yellow warning when a non-style-aware backend is selected while the
  field is filled — wired to both backend-select change and input events

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 20:09:59 +02:00

216 lines
7.5 KiB
JavaScript

// ── 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-hotkey')) $('snippet-hotkey').textContent =
`pip install pynput sounddevice soundfile pyperclip requests
python hotkey_daemon.py --server ${proxyBase}`;
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');
});