tts-voice-creator-clone-and.../static/sections/s-llms.html
mARTin-B78 ea50267c30 Add unified Studio casting workflow and fix voice/casting pipeline bugs
Introduces the new Studio section (Source -> Characters -> Voices ->
Perform & Export) that reuses the existing Read Aloud/Library/Script
Rehearsal code via DOM reparenting instead of duplicating it, and rolls up
a long tail of bugs found while producing a real audiobook through it:
umlaut-eating name sanitizers, a voice picker that mispositioned itself and
capped results at 60, PDF pagination silently breaking on trimmed \f
markers, a race letting stale audio keep playing after a new line was
clicked, an alias-overlap bug that could silently redirect a voice/image
save onto the wrong character, voice design failing outright during brief
TTS backend restarts instead of retrying, sparse cast entries defaulting to
English/wrong gender, and a reassigned voice never reaching an already-open
Stage session or invalidating its cached audio. Also adds a persistent
per-line audio cache, audiobook export browsing/download, and an inline
voice-design prompt editor. Full details in CHANGELOG.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 02:03:56 +02:00

832 lines
47 KiB
HTML

<style>
.header-cat-block { display: none !important; }
.header-cat-block.is-active { display: flex !important; }
</style>
<div class="section-head engines-page-head">
<div class="engines-head-main">
<span class="section-icon"><span class="mdi mdi-brain"></span></span>
<div class="section-title">
<h2>Engines</h2>
<p>Local and cloud services for language generation, speech recognition, and synthesis.</p>
</div>
</div>
<div class="engines-head-context header-cat-block" data-engines-cat="llm">
<div class="engines-context-copy">
<h3 class="llm-sec-title">Language Models</h3>
<p class="llm-sec-note">Pick the <strong>Active Language Model</strong> below. Cards connect engines and apply a URL with <strong>Use as LLM</strong>.</p>
</div>
</div>
<div class="engines-head-context header-cat-block" data-engines-cat="stt">
<div class="engines-context-copy">
<h3 class="llm-sec-title">Speech Recognition</h3>
<p class="llm-sec-note">Docker stack containers appear first, with local runners and cloud APIs below.</p>
</div>
</div>
<div class="engines-head-context header-cat-block" data-engines-cat="tts">
<div class="engines-context-copy">
<h3 class="llm-sec-title">Text to Speech</h3>
<p class="llm-sec-note">Docker-detected engines and local presets are listed together. Apply a URL with <strong>Use as TTS</strong>.</p>
</div>
<div class="engines-context-actions">
<button class="btn-secondary" type="button" onclick="openAddEngineDialog('tts')"><span class="mdi mdi-plus"></span> Add Custom</button>
<button class="btn-secondary dc-refresh-btn" type="button"><span class="mdi mdi-refresh"></span> Refresh</button>
<span class="llm-free-badge llm-free-local">100% Local</span>
</div>
</div>
<div class="engines-head-context header-cat-block" data-engines-cat="image">
<div class="engines-context-copy">
<h3 class="llm-sec-title">Image Generation</h3>
<p class="llm-sec-note">Used to generate character profile pictures. Pick the active provider below and add its API key.</p>
</div>
</div>
</div>
<!-- ── Language Models sub-page ──────────────────────────── -->
<div class="s-engines-page" data-page="llm">
<!-- ── Active Language Model (global picker) ──────────────── -->
<div class="llm-active-panel card">
<div class="llm-active-head">
<span class="llm-active-icon"><span class="mdi mdi-star-four-points"></span></span>
<div>
<h4 class="llm-active-title">Active Language Model</h4>
<p class="llm-active-sub">Default endpoint &amp; model for all LLM tasks. Per-task dropdowns can still override it.</p>
</div>
</div>
<div class="llm-active-controls">
<label class="llm-active-lbl"><span class="mdi mdi-server-network"></span> Endpoint</label>
<select id="llm-active-url" class="llm-active-url-inp" style="appearance: auto; cursor: pointer; padding-right: 25px;">
<option value="">— Select an endpoint —</option>
</select>
<label class="llm-active-lbl"><span class="mdi mdi-brain"></span> Model</label>
<select id="llm-active-model" aria-label="Active language model"><option value="">— fetch models —</option></select>
<button id="llm-active-refresh" class="btn-secondary" type="button" title="Fetch models"><span class="mdi mdi-refresh"></span></button>
<span id="llm-active-status" class="llm-active-status"></span>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:20px;margin-bottom:12px">
<h4 class="llm-local-cat" style="margin:0;text-transform:uppercase">Local</h4>
<button class="btn-primary" type="button" onclick="openAddEngineDialog('llm')"><span class="mdi mdi-plus"></span> Add Custom</button>
</div>
<div id="dc-grid-llm" class="dc-grid" style="margin-bottom:14px"></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:28px;margin-bottom:12px">
<h4 class="llm-local-cat" style="margin:0;text-transform:uppercase"><span class="mdi mdi-earth"></span> Cloud APIs <span class="llm-free-badge" style="margin-left:8px;vertical-align:middle;text-transform:none">Free tiers</span></h4>
<button class="btn-primary" type="button" onclick="openAddEngineDialog('llm')"><span class="mdi mdi-plus"></span> Add Custom</button>
</div>
<div class="llm-service-grid">
<div class="llm-card llm-card-highlight">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-lightning-bolt"></span></span>
<div>
<div class="llm-card-name">Groq</div>
<div class="llm-card-sub">Ultra-fast inference &middot; OpenAI-compatible</div>
</div>
<span class="llm-tier-badge llm-tier-free">Free</span>
</div>
<div class="llm-card-stats">
<span>30 000 tokens / min</span>
<span>14 400 req / day</span>
<span>Lowest latency</span>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="gsk_&hellip;" data-llm-key="groq_llm">
<a class="llm-link" href="https://console.groq.com" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>https://api.groq.com/openai/v1</code>
</div>
<div class="llm-models">
<span class="llm-model-tag">llama-3.3-70b-versatile</span>
<span class="llm-model-tag">qwen-qwq-32b</span>
<span class="llm-model-tag">deepseek-r1-distill-llama-70b</span>
</div>
<div class="dc-controls-row" style="margin-top:10px">
<button class="llm-local-ping llm-cloud-connect-btn" data-llm-key="groq_llm" data-llm-endpoint="https://api.groq.com/openai/v1"><span class="mdi mdi-lan-connect"></span> Connect</button>
<button class="llm-use-btn dc-use-btn llm-cloud-use-btn" data-llm-key="groq_llm" data-llm-endpoint="https://api.groq.com/openai/v1" data-llm-model="llama-3.3-70b-versatile" style="margin-left:auto"><span class="mdi mdi-arrow-right-circle-outline"></span> Use as LLM</button>
</div>
</div>
<div class="llm-card">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-earth"></span></span>
<div>
<div class="llm-card-name">OpenRouter</div>
<div class="llm-card-sub">50+ free models &middot; OpenAI-compatible</div>
</div>
<span class="llm-tier-badge llm-tier-free">Free</span>
</div>
<div class="llm-card-stats">
<span>20 req / min</span>
<span>200 req / day (free models)</span>
<span>Single API for all models</span>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="sk-or-&hellip;" data-llm-key="openrouter">
<a class="llm-link" href="https://openrouter.ai" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>https://openrouter.ai/api/v1</code>
</div>
<div class="llm-models">
<span class="llm-model-tag">qwen/qwen3-235b-a22b:free</span>
<span class="llm-model-tag">deepseek/deepseek-r1-0528:free</span>
<span class="llm-model-tag">mistralai/mistral-7b-instruct:free</span>
</div>
<div class="dc-controls-row" style="margin-top:10px">
<button class="llm-local-ping llm-cloud-connect-btn" data-llm-key="openrouter" data-llm-endpoint="https://openrouter.ai/api/v1"><span class="mdi mdi-lan-connect"></span> Connect</button>
<button class="llm-use-btn dc-use-btn llm-cloud-use-btn" data-llm-key="openrouter" data-llm-endpoint="https://openrouter.ai/api/v1" data-llm-model="qwen/qwen3-235b-a22b:free" style="margin-left:auto"><span class="mdi mdi-arrow-right-circle-outline"></span> Use as LLM</button>
</div>
</div>
<div class="llm-card">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-diamond-outline"></span></span>
<div>
<div class="llm-card-name">Google Gemini</div>
<div class="llm-card-sub">Gemini 2.5 Flash &middot; Generous free tier</div>
</div>
<span class="llm-tier-badge llm-tier-free">Free</span>
</div>
<div class="llm-card-stats">
<span>250 000 tokens / min</span>
<span>15 req / min free</span>
<span>1M context window</span>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="AIza&hellip;" data-llm-key="gemini">
<a class="llm-link" href="https://ai.google.dev" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>https://generativelanguage.googleapis.com/v1beta/openai/</code>
</div>
<div class="llm-info-note">Uses OpenAI-compat wrapper &mdash; use model <code>gemini-2.5-flash</code></div>
<div class="dc-controls-row" style="margin-top:10px">
<button class="llm-local-ping llm-cloud-connect-btn" data-llm-key="gemini" data-llm-endpoint="https://generativelanguage.googleapis.com/v1beta/openai/"><span class="mdi mdi-lan-connect"></span> Connect</button>
<button class="llm-use-btn dc-use-btn llm-cloud-use-btn" data-llm-key="gemini" data-llm-endpoint="https://generativelanguage.googleapis.com/v1beta/openai/" data-llm-model="gemini-2.5-flash" style="margin-left:auto"><span class="mdi mdi-arrow-right-circle-outline"></span> Use as LLM</button>
</div>
</div>
<div class="llm-card">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-alpha-m-circle-outline"></span></span>
<div>
<div class="llm-card-name">Mistral AI</div>
<div class="llm-card-sub">OpenAI-compatible &middot; EU-based</div>
</div>
<span class="llm-tier-badge llm-tier-free">Free</span>
</div>
<div class="llm-card-stats">
<span>1B tokens / month</span>
<span>2 req / min free</span>
<span>GDPR-compliant</span>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="Mistral key&hellip;" data-llm-key="mistral">
<a class="llm-link" href="https://mistral.ai" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>https://api.mistral.ai/v1</code>
</div>
<div class="llm-models">
<span class="llm-model-tag">mistral-small-latest</span>
<span class="llm-model-tag">mistral-large-latest</span>
</div>
<div class="dc-controls-row" style="margin-top:10px">
<button class="llm-local-ping llm-cloud-connect-btn" data-llm-key="mistral" data-llm-endpoint="https://api.mistral.ai/v1"><span class="mdi mdi-lan-connect"></span> Connect</button>
<button class="llm-use-btn dc-use-btn llm-cloud-use-btn" data-llm-key="mistral" data-llm-endpoint="https://api.mistral.ai/v1" data-llm-model="mistral-small-latest" style="margin-left:auto"><span class="mdi mdi-arrow-right-circle-outline"></span> Use as LLM</button>
</div>
</div>
<div class="llm-card">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-robot-outline"></span></span>
<div>
<div class="llm-card-name">OpenAI</div>
<div class="llm-card-sub">GPT models &middot; pay-as-you-go</div>
</div>
<span class="llm-tier-badge llm-tier-paid">Paid</span>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="sk-&hellip;" data-llm-key="openai_llm">
<a class="llm-link" href="https://platform.openai.com/api-keys" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>https://api.openai.com/v1</code>
</div>
<div class="llm-models">
<span class="llm-model-tag">gpt-4o</span>
<span class="llm-model-tag">gpt-4o-mini</span>
<span class="llm-model-tag">o3-mini</span>
</div>
<div class="dc-controls-row" style="margin-top:10px">
<button class="llm-local-ping llm-cloud-connect-btn" data-llm-key="openai_llm" data-llm-endpoint="https://api.openai.com/v1"><span class="mdi mdi-lan-connect"></span> Connect</button>
<button class="llm-use-btn dc-use-btn llm-cloud-use-btn" data-llm-key="openai_llm" data-llm-endpoint="https://api.openai.com/v1" data-llm-model="gpt-4o-mini" style="margin-left:auto"><span class="mdi mdi-arrow-right-circle-outline"></span> Use as LLM</button>
</div>
</div>
<div class="llm-card">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-triangle-outline"></span></span>
<div>
<div class="llm-card-name">Anthropic</div>
<div class="llm-card-sub">Claude models &middot; pay-as-you-go</div>
</div>
<span class="llm-tier-badge llm-tier-paid">Paid</span>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="sk-ant-&hellip;" data-llm-key="anthropic_llm">
<a class="llm-link" href="https://console.anthropic.com/settings/keys" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>https://api.anthropic.com/v1</code>
</div>
<div class="llm-info-note">Not OpenAI-compatible &mdash; needs the Anthropic Messages API, not <code>/chat/completions</code></div>
<div class="dc-controls-row" style="margin-top:10px">
<button class="llm-local-ping llm-cloud-connect-btn" data-llm-key="anthropic_llm" data-llm-endpoint="https://api.anthropic.com/v1" data-llm-probe-type="anthropic"><span class="mdi mdi-lan-connect"></span> Connect</button>
<button class="llm-use-btn dc-use-btn llm-cloud-use-btn" data-llm-key="anthropic_llm" data-llm-endpoint="https://api.anthropic.com/v1" data-llm-model="claude-3-5-haiku-latest" data-llm-incompatible="1" style="margin-left:auto"><span class="mdi mdi-arrow-right-circle-outline"></span> Use as LLM</button>
</div>
</div>
</div>
<h4 class="llm-local-cat" style="margin-top: 40px; text-transform: uppercase;">Additional Settings</h4>
<div class="card" style="margin-bottom:24px;">
<div class="s-page-head">
<h3>LLM Refinement Defaults</h3>
<p>Automatic text cleanup after transcription</p>
</div>
<div style="display: flex; flex-direction: column; gap: 16px; margin-bottom: 24px;">
<div class="s-field">
<label>Auto-refine after transcription</label>
<select id="s-auto-refine">
<option value="off">Off — show raw transcription</option>
<option value="on">On — refine automatically</option>
</select>
<span class="s-hint">When on, the LLM refinement runs immediately after each capture.</span>
</div>
<div class="s-field">
<label>Refinement model</label>
<input type="text" id="s-refine-model" placeholder="e.g. llama3.2, qwen3:8b, gpt-4o-mini">
<span class="s-hint">Model name sent to the active LLM URL. Leave empty to use the app's current default.</span>
</div>
</div>
<div class="s-toggle-grid">
<label class="s-toggle-row"><input type="checkbox" id="s-refine-fillers"><div><span class="s-toggle-title">Remove filler words</span><span class="s-hint">Strips um, uh, like, you know, sort of.</span></div></label>
<label class="s-toggle-row"><input type="checkbox" id="s-refine-repetitions"><div><span class="s-toggle-title">Remove repetitions</span><span class="s-hint">Collapses repeated words and phrases.</span></div></label>
<label class="s-toggle-row"><input type="checkbox" id="s-refine-corrections"><div><span class="s-toggle-title">Fix grammar &amp; spelling</span><span class="s-hint">Light corrections without changing meaning.</span></div></label>
<label class="s-toggle-row"><input type="checkbox" id="s-refine-punctuation"><div><span class="s-toggle-title">Fix punctuation</span><span class="s-hint">Adds missing commas, periods, and sentence casing.</span></div></label>
</div>
<div class="s-page-actions" style="margin-top: 15px;">
<button class="btn-primary s-save-btn">Save settings</button>
</div>
</div>
</div><!-- /llm -->
<!-- ── Speech to Text sub-page ───────────────────────────── -->
<div class="s-engines-page" data-page="stt">
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:20px;margin-bottom:12px">
<h4 class="llm-local-cat" style="margin:0;text-transform:uppercase">Local</h4>
<button class="btn-primary" type="button" onclick="openAddEngineDialog('stt')"><span class="mdi mdi-plus"></span> Add Custom</button>
</div>
<div id="dc-grid-stt" class="dc-grid" style="margin-bottom:14px"></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:28px;margin-bottom:12px">
<div style="display:flex;gap:8px;align-items:center">
<span class="mdi mdi-cloud-outline"></span>
<h4 class="llm-local-cat" style="margin:0;text-transform:uppercase">Cloud APIs</h4>
<span class="llm-free-badge">Free tiers</span>
</div>
<button class="btn-primary" type="button" onclick="openAddEngineDialog('stt')"><span class="mdi mdi-plus"></span> Add Custom</button>
</div>
<div class="llm-service-grid">
<div class="llm-card llm-card-highlight">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-lightning-bolt"></span></span>
<div>
<div class="llm-card-name">Groq Whisper</div>
<div class="llm-card-sub">whisper-large-v3-turbo &middot; OpenAI-compatible</div>
</div>
<span class="llm-tier-badge llm-tier-free">Free</span>
</div>
<div class="llm-card-stats">
<span>2 000 req / day</span>
<span><span class="mdi mdi-lightning-bolt"></span> Fastest cloud STT</span>
<span><span class="mdi mdi-memory"></span> Cloud &middot; 0 VRAM</span>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="gsk_&hellip;" data-llm-key="groq_stt">
<a class="llm-link" href="https://console.groq.com" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>https://api.groq.com/openai/v1</code>
</div>
<div class="llm-models">
<span class="llm-model-tag">whisper-large-v3-turbo</span>
<span class="llm-model-tag">whisper-large-v3</span>
<span class="llm-model-tag">distil-whisper-large-v3-en</span>
</div>
<div class="llm-info-note">Save key in <strong>Settings &rarr; Groq API key</strong> to enable <em>Groq Whisper</em> in the STT dropdown.</div>
</div>
<div class="llm-card">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-emoticon-happy-outline"></span></span>
<div>
<div class="llm-card-name">HuggingFace Inference</div>
<div class="llm-card-sub">Serverless Whisper models</div>
</div>
<span class="llm-tier-badge llm-tier-free">Free</span>
</div>
<div class="llm-card-stats">
<span>~1 000 req / day</span>
<span>Slower cold starts</span>
<span>Many model variants</span>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="hf_&hellip;" data-llm-key="hf_stt">
<a class="llm-link" href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>https://api-inference.huggingface.co/models/openai/whisper-large-v3</code>
</div>
</div>
<div class="llm-card">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-content-copy"></span></span>
<div>
<div class="llm-card-name">AssemblyAI</div>
<div class="llm-card-sub">High-accuracy transcription + speaker diarization</div>
</div>
<span class="llm-tier-badge llm-tier-free">Free</span>
</div>
<div class="llm-card-stats">
<span>100 h lifetime</span>
<span>Speaker labels</span>
<span>Auto-chapters</span>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="AssemblyAI key&hellip;" data-llm-key="assemblyai">
<a class="llm-link" href="https://www.assemblyai.com" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>https://api.assemblyai.com/v2/transcript</code>
</div>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:28px;margin-bottom:12px">
<h4 class="llm-local-cat" style="margin:0;text-transform:uppercase">Additional Settings</h4>
</div>
<div class="card" style="margin-bottom: 24px;">
<div class="s-page-head">
<h3>STT Connections</h3>
<p>Set up endpoints for Speech-to-Text backends.</p>
</div>
<div class="settings-grid stt-settings-grid">
<div class="s-field stt-url-field">
<label>Active STT URL</label>
<input type="text" id="s-whisper-url" placeholder="http://host.docker.internal:8010">
<span class="s-hint">Default recognition endpoint. Expected: <code>POST /v1/audio/transcriptions</code>.</span>
<div class="btn-row settings-mini-actions">
<button class="btn-secondary" id="s-use-parakeet-asr" type="button">Use Parakeet</button>
<button class="btn-secondary" id="s-use-nvidia-router" type="button">Use router</button>
<button class="btn-secondary" id="s-use-faster-whisper" type="button">Use faster-whisper</button>
<button class="btn-secondary" id="s-use-whisper-cpp" type="button">Use whisper.cpp</button>
</div>
</div>
<div class="s-field">
<label>faster-whisper-server URL <span class="s-label-note">(CTranslate2 GPU)</span></label>
<input type="text" id="s-faster-whisper-url" placeholder="http://host.docker.internal:8000">
<span class="s-hint">GPU-accelerated Whisper via CTranslate2. ~70&times; RT &middot; 1.5 GB VRAM.</span>
</div>
<div class="s-field">
<label>whisper.cpp URL <span class="s-label-note">(CPU/CUDA)</span></label>
<input type="text" id="s-whisper-cpp-url" placeholder="http://host.docker.internal:8080">
<span class="s-hint">Lightweight C++ Whisper server. ~8&ndash;15&times; RT CPU &middot; ~1 GB RAM.</span>
</div>
<div class="s-field">
<label>Groq API key <span class="s-label-note">(Groq Whisper &amp; LLM)</span></label>
<div class="s-key-row">
<input type="password" id="s-groq-api-key" placeholder="gsk_&hellip;" autocomplete="off">
<button type="button" class="s-eye-btn" data-target="s-groq-api-key" aria-label="Toggle API key visibility"><span class="mdi mdi-eye-outline"></span></button>
</div>
<span class="s-hint">Used for Groq Whisper STT and LLM. Free: 2 000 req/day.</span>
</div>
</div>
<div class="s-page-actions" style="margin-top: 15px;">
<button class="btn-primary s-save-btn">Save connections</button>
</div>
</div>
<!-- Inline STT test panel -->
<div class="stt-test-panel" id="stt-test-panel">
<div class="stt-test-row">
<span class="stt-test-label">Quick test</span>
<select id="stt-test-backend" class="stt-test-select"></select>
<button id="stt-test-mic-btn" class="stt-test-mic" title="Hold to record, click again to stop">
<span class="mdi mdi-microphone"></span>
</button>
<span id="stt-test-status" class="stt-test-status">Ready</span>
</div>
<div id="stt-test-result" class="stt-test-result" style="display:none"></div>
</div>
<div class="card" style="margin-top: 24px;">
<div class="s-page-head">
<h3>STT Transcription Defaults</h3>
<p>Default language and preferred STT backend for captures.</p>
</div>
<div class="settings-grid settings-behavior-grid">
<div class="s-field">
<label>Default language</label>
<select id="s-stt-language">
<option value="">Auto-detect</option>
<option value="en">English</option>
<option value="de">German</option>
<option value="fr">French</option>
<option value="es">Spanish</option>
<option value="it">Italian</option>
<option value="pt">Portuguese</option>
<option value="nl">Dutch</option>
<option value="ja">Japanese</option>
<option value="zh">Chinese</option>
</select>
</div>
<div class="s-field">
<label>Preferred STT backend</label>
<select id="s-stt-preferred-backend">
<option value="">Use active STT URL (from Connections)</option>
<option value="groq">Groq Whisper — cloud, fastest</option>
<option value="faster-whisper">faster-whisper-server — GPU local</option>
<option value="whisper-cpp">whisper.cpp — CPU/CUDA local</option>
<option value="parakeet">NVIDIA Parakeet — GPU local</option>
</select>
<span class="s-hint">Overrides the active STT URL for STT-TTS panel captures.</span>
</div>
</div>
<div class="s-page-actions" style="margin-top: 15px;">
<button class="btn-primary s-save-btn">Save settings</button>
</div>
</div>
</div><!-- /stt -->
<!-- ── Text to Speech sub-page ───────────────────────────── -->
<div class="s-engines-page" data-page="tts">
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:20px;margin-bottom:12px">
<h4 class="llm-local-cat" style="margin:0;text-transform:uppercase">Local</h4>
<button class="btn-primary" type="button" onclick="openAddEngineDialog('tts')"><span class="mdi mdi-plus"></span> Add Custom</button>
</div>
<div id="dc-grid-tts" class="dc-grid" style="margin-bottom:14px"></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:28px;margin-bottom:12px">
<div style="display:flex;gap:8px;align-items:center">
<span class="mdi mdi-cloud-outline"></span>
<h4 class="llm-local-cat" style="margin:0;text-transform:uppercase">Cloud APIs</h4>
<span class="llm-free-badge">Free tiers</span>
</div>
<button class="btn-primary" type="button" onclick="openAddEngineDialog('tts')"><span class="mdi mdi-plus"></span> Add Custom</button>
</div>
<div class="llm-service-grid">
<div class="llm-card llm-card-highlight">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-waveform"></span></span>
<div>
<div class="llm-card-name">ElevenLabs</div>
<div class="llm-card-sub">High-quality voice cloning &amp; synthesis</div>
</div>
<span class="llm-tier-badge llm-tier-free">Free</span>
</div>
<div class="llm-card-stats">
<span>10 000 chars / month</span>
<span>2 500 char / request max</span>
<span>Voice cloning supported</span>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="xi-api-key&hellip;" data-llm-key="elevenlabs">
<a class="llm-link" href="https://elevenlabs.io" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>https://api.elevenlabs.io/v1/text-to-speech</code>
</div>
</div>
<div class="llm-card">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-fish"></span></span>
<div>
<div class="llm-card-name">Fish Audio</div>
<div class="llm-card-sub">Voice cloning &amp; multilingual TTS</div>
</div>
<span class="llm-tier-badge llm-tier-free">Free</span>
</div>
<div class="llm-card-stats">
<span>1 h audio / month</span>
<span>100 req / min</span>
<span>30+ languages</span>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="Fish Audio key&hellip;" data-llm-key="fish_audio">
<a class="llm-link" href="https://fish.audio" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>https://api.fish.audio/v1/tts</code>
</div>
</div>
<div class="llm-card">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-music-note"></span></span>
<div>
<div class="llm-card-name">Kokoro TTS</div>
<div class="llm-card-sub">82M model &middot; HuggingFace Spaces demo</div>
</div>
<span class="llm-tier-badge llm-tier-demo">Demo</span>
</div>
<div class="llm-card-stats">
<span>Free web demo</span>
<span>&lt;$1 per 1M chars (paid)</span>
<span>High naturalness</span>
</div>
<div class="llm-info-box">
Use the HF Spaces web demo for quick tests, or run Kokoro locally via Docker for production use.
<br>
<a class="llm-link-inline" href="https://huggingface.co/spaces/hexgrad/Kokoro-TTS" target="_blank" rel="noopener"><span class="mdi mdi-arrow-right"></span> Open Kokoro HF Space</a>
</div>
</div>
</div> <!-- CLOSE llm-service-grid -->
<h4 class="llm-local-cat" style="margin-top: 40px; text-transform: uppercase;">Additional Settings</h4>
<div class="card" style="margin-bottom: 24px;">
<div class="s-page-head">
<h3>TTS Playback & API Behavior</h3>
<p>Controls how previews play and how OpenAI-compatible requests are shaped.</p>
</div>
<div class="settings-grid settings-behavior-grid">
<div class="s-field">
<label>TTS preview playback</label>
<select id="s-tts-stream-mode">
<option value="auto">Stream when available, then fall back</option>
<option value="streaming">Streaming only</option>
<option value="buffered">Buffered WAV only</option>
</select>
<span class="s-hint">Buffered keeps Save WAV available. Streaming starts sooner.</span>
</div>
<div class="s-field">
<label>OpenAI-compatible request style</label>
<select id="s-tts-backend">
<option value="openai">Qwen3-TTS / OpenAI</option>
<option value="localai">LocalAI (/tts)</option>
<option value="pocket">Pocket-TTS</option>
<option value="nvidia_magpie">NVIDIA Magpie / OpenAI</option>
</select>
<span class="s-hint">Controls payload shape for the normal TTS API URL.</span>
</div>
</div>
</div>
<div class="card" style="margin-bottom: 24px;">
<div class="s-page-head">
<h3>Default Playback Voice</h3>
<p>Pre-selected voice in the STT-TTS panel</p>
</div>
<div class="settings-grid settings-behavior-grid">
<div class="s-field">
<label>Default voice</label>
<select id="s-captures-default-voice">
<option value="">None — select manually</option>
</select>
</div>
</div>
</div>
<div class="s-page-actions" style="margin-top: 15px;">
<button class="btn-primary s-save-btn">Save settings</button>
<button class="btn-secondary s-reload-btn">Reload settings</button>
</div>
</div><!-- /tts -->
<!-- ── Image Generation sub-page ─────────────────────────── -->
<div class="s-engines-page" data-page="image">
<div class="llm-active-panel card">
<div class="llm-active-head">
<span class="llm-active-icon"><span class="mdi mdi-star-four-points"></span></span>
<div>
<h4 class="llm-active-title">Active Image Generation Provider</h4>
<p class="llm-active-sub">Used for the "Generate Image" action on a character's profile picture.</p>
</div>
</div>
<div class="llm-active-controls">
<label class="llm-active-lbl"><span class="mdi mdi-cloud-outline"></span> Provider</label>
<select id="image-gen-provider">
<option value="">— None selected —</option>
<option value="openai">OpenAI (gpt-image-1)</option>
<option value="google">Google (Gemini / Imagen)</option>
<option value="openrouter">OpenRouter (image-capable models)</option>
<option value="pollinations">Pollinations.ai (free, no key)</option>
<option value="comfyui">Local ComfyUI</option>
</select>
<label class="llm-active-lbl"><span class="mdi mdi-image-outline"></span> Model</label>
<select id="image-gen-model-select" style="max-width:240px"></select>
<input type="text" id="image-gen-model" placeholder="Custom model ID" style="max-width:200px;display:none">
<span id="image-gen-status" class="llm-active-status"></span>
</div>
<p class="llm-active-sub" style="margin-top:8px">OpenRouter reuses the same API key as its LLM card below &mdash; no separate key needed here.</p>
</div>
<div class="card" id="comfyui-config-panel" hidden style="margin-top:16px">
<div class="s-page-head">
<h3><span class="mdi mdi-graph-outline"></span> Local ComfyUI Workflow</h3>
<p>Runs a workflow on your own GPU instead of a paid API. Export it from ComfyUI's own UI first &mdash; Workflow menu &rarr; Export (API Format) &mdash; then paste the result below. The same workflow is used for both single portraits and character concept sheets; only the prompt text sent to it differs.</p>
</div>
<div style="display:flex; flex-direction:column; gap:14px">
<div class="s-field">
<label>ComfyUI URL</label>
<input type="text" id="comfyui-url" placeholder="http://host.docker.internal:8188">
<span class="s-hint">Base URL of your running ComfyUI instance (container port 8188 by default).</span>
</div>
<div class="s-field">
<label>Workflow JSON (API format)</label>
<textarea id="comfyui-workflow" rows="6" spellcheck="false" placeholder='{"3": {"class_type": "KSampler", "inputs": {...}}, "6": {"class_type": "CLIPTextEncode", "inputs": {"text": "", ...}}, ...}' style="font-family:monospace; font-size:12px"></textarea>
<span class="s-hint">Paste the raw JSON exported from ComfyUI's "Export (API Format)" action — not a regular saved workflow (that format has "nodes"/"links" keys and won't work here).</span>
</div>
<div style="display:flex; gap:14px; flex-wrap:wrap">
<div class="s-field" style="flex:1; min-width:160px">
<label>Prompt node ID</label>
<input type="text" id="comfyui-prompt-node-id" placeholder="e.g. 6">
<span class="s-hint">The node whose text input gets replaced with the character prompt each run.</span>
</div>
<div class="s-field" style="flex:1; min-width:160px">
<label>Prompt input field name</label>
<input type="text" id="comfyui-prompt-field" placeholder="text">
<span class="s-hint">Usually "text" (CLIPTextEncode) — check the node's "inputs" key in the JSON above.</span>
</div>
<div class="s-field" style="flex:1; min-width:160px">
<label>Output node ID</label>
<input type="text" id="comfyui-output-node-id" placeholder="e.g. 22">
<span class="s-hint">The SaveImage/PreviewImage node whose result to fetch.</span>
</div>
</div>
<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap">
<button type="button" class="btn-secondary" id="comfyui-test-btn"><span class="mdi mdi-lan-connect"></span> Test connection</button>
<button type="button" class="btn-secondary" id="comfyui-test-gen-btn"><span class="mdi mdi-image-outline"></span> Test generate</button>
<span id="comfyui-test-status" class="llm-active-status"></span>
</div>
<div id="comfyui-test-preview" hidden><img id="comfyui-test-img" style="max-width:220px; border-radius:8px; border:1px solid var(--border)"></div>
</div>
</div>
<div style="display:flex;gap:8px;align-items:center;margin-top:28px;margin-bottom:12px">
<span class="mdi mdi-cloud-outline"></span>
<h4 class="llm-local-cat" style="margin:0;text-transform:uppercase">Cloud APIs</h4>
</div>
<div class="llm-service-grid">
<div class="llm-card llm-card-highlight">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-robot-outline"></span></span>
<div>
<div class="llm-card-name">OpenAI</div>
<div class="llm-card-sub">gpt-image-1 &middot; Images API</div>
</div>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="sk-&hellip;" data-llm-key="openai_image">
<a class="llm-link" href="https://platform.openai.com/api-keys" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>https://api.openai.com/v1/images/generations</code>
</div>
</div>
<div class="llm-card">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-google"></span></span>
<div>
<div class="llm-card-name">Google</div>
<div class="llm-card-sub">Gemini 2.5 Flash Image / Imagen</div>
</div>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="AIza&hellip;" data-llm-key="google_image">
<a class="llm-link" href="https://aistudio.google.com/apikey" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>generativelanguage.googleapis.com</code>
</div>
</div>
<div class="llm-card">
<div class="llm-card-head">
<span class="llm-card-icon"><span class="mdi mdi-flower-outline"></span></span>
<div>
<div class="llm-card-name">Pollinations.ai</div>
<div class="llm-card-sub">Free &middot; no API key &middot; no account</div>
</div>
<span class="llm-tier-badge llm-tier-free">Free</span>
</div>
<div class="llm-card-stats">
<span>No signup</span>
<span>Shared free queue</span>
<span>Third-party public service</span>
</div>
<div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
<code>image.pollinations.ai</code>
</div>
<div class="llm-models">
<span class="llm-model-tag">flux</span>
</div>
<div class="llm-info-note">No key or setup needed &mdash; just pick it as the Active Provider above. No SLA/rate-limit guarantee since it's a shared free public queue; retries automatically on "queue full" errors.</div>
</div>
</div>
<div class="llm-info-note" style="margin-top:16px;">Local ComfyUI (no API key, runs on your own GPU) is configured separately above, in the Active Image Generation Provider panel.</div>
<div class="s-page-actions" style="margin-top: 15px;">
<button class="btn-primary s-save-btn">Save settings</button>
<button class="btn-secondary s-reload-btn">Reload settings</button>
</div>
</div><!-- /image -->
<!-- ── Add Custom Engine dialog ─────────────────────────────── -->
<dialog id="add-engine-dlg" class="add-engine-dlg">
<div class="add-engine-form">
<h3 class="add-engine-title"><span class="mdi mdi-plus-circle-outline"></span> Add Custom Engine</h3>
<div class="add-engine-field">
<label for="aed-name">Name</label>
<input id="aed-name" type="text" placeholder="My TTS Server" spellcheck="false">
</div>
<div class="add-engine-field">
<label for="aed-role">Role</label>
<select id="aed-role">
<option value="tts">Text to Speech (TTS)</option>
<option value="stt">Speech to Text (STT)</option>
<option value="llm">Language Model (LLM)</option>
</select>
</div>
<div class="add-engine-field">
<label for="aed-url">URL</label>
<input id="aed-url" type="text" placeholder="http://192.168.1.100:8080" spellcheck="false">
</div>
<div class="add-engine-field">
<label for="aed-container">Docker container name <span class="add-engine-opt">(optional — enables Stop / Start / Restart)</span></label>
<input id="aed-container" type="text" placeholder="my-tts-container" spellcheck="false">
</div>
<div class="add-engine-field">
<label for="aed-desc">Description <span class="add-engine-opt">(optional)</span></label>
<input id="aed-desc" type="text" placeholder="Short description">
</div>
<div class="add-engine-actions">
<button type="button" class="btn-secondary" id="aed-cancel">Cancel</button>
<button type="button" class="btn-primary" id="aed-save">Add Card</button>
</div>
</div>
</dialog>