From 08a63ef2d9df471d25b29ae3d4d5a5bc2fcaeb28 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Wed, 27 May 2026 00:16:43 +0200 Subject: [PATCH] Add Kokoro TTS + faster-whisper/whisper.cpp/Groq STT backends with metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New TTS backend: Kokoro FastAPI (82M) — OpenAI-compatible, 11 built-in voices, only shows when server is reachable (~300 MB CPU, ~0.1× RTF). New STT backends in Transcribe dropdown: faster-whisper (CTranslate2 GPU, ~70× RT, 1.5 GB VRAM), whisper.cpp (CPU/CUDA, ~8–15× RT, ~1 GB RAM), Groq Whisper (fastest cloud, free 2 000 req/day, key shared with Groq LLM). Backend help panels now show ⚡ speed · ⏰ latency · ⭐ quality · 💾 RAM metric chips for all TTS and STT backends. Active Docker Stack cards also get per-container metric chips. AI Backends section: "Use as STT" / "Use as TTS" one-click buttons on faster-whisper, whisper.cpp, and Kokoro cards apply URLs to Settings without leaving the page. Groq Whisper card notes the shared key path. Settings: Kokoro URL in TTS cluster; faster-whisper URL, whisper.cpp URL, Groq API key in STT cluster; quick-fill buttons for all local STT engines. Co-Authored-By: Claude Sonnet 4.6 --- server.py | 173 ++++++++++++++++++++++++-------- static/app.js | 94 +++++++++++++++-- static/sections/s-llms.html | 50 ++++++++- static/sections/s-settings.html | 38 ++++++- static/sections/s-tryout.html | 2 +- static/style.css | 29 +++++- 6 files changed, 329 insertions(+), 57 deletions(-) diff --git a/server.py b/server.py index 1300d54..bce8663 100644 --- a/server.py +++ b/server.py @@ -46,6 +46,10 @@ _NVIDIA_ASR_DEFAULT = os.environ.get("NVIDIA_PARAKEET_ASR_URL", "http://host.doc _NVIDIA_CLONE_DEFAULT = os.environ.get("NVIDIA_TTS_CLONE_URL", "http://host.docker.internal:8093") _NVIDIA_ZEROSHOT_DEFAULT = os.environ.get("NVIDIA_ZEROSHOT_TTS_URL", _NVIDIA_CLONE_DEFAULT) _NVIDIA_FLOW_DEFAULT = os.environ.get("NVIDIA_FLOW_TTS_URL", "http://host.docker.internal:8094") +_FASTER_WHISPER_DEFAULT = os.environ.get("FASTER_WHISPER_URL", "http://host.docker.internal:8000") +_WHISPER_CPP_DEFAULT = os.environ.get("WHISPER_CPP_URL", "http://host.docker.internal:8080") +_GROQ_STT_ENDPOINT = "https://api.groq.com/openai/v1" +_KOKORO_DEFAULT = os.environ.get("KOKORO_URL", "http://host.docker.internal:8880/v1") _TTS_CONTAINER = os.environ.get("TTS_CONTAINER_NAME", "faster-qwen3-tts") _TTS_CONTAINERS_RAW = os.environ.get("TTS_CONTAINER_NAMES", "") # comma-separated override _VOICE_DESIGN_MODEL = os.environ.get("VOICE_DESIGN_MODEL", "Qwen3-TTS-12Hz-1.7B-VoiceDesign") @@ -286,6 +290,7 @@ _SETTINGS_KEYS = { "output_dir", "voices_scan_dir", "voice_design_url", "customvoice_url", "nvidia_router_url", "nvidia_tts_url", "nvidia_asr_url", "nvidia_clone_url", "nvidia_zeroshot_url", "nvidia_flow_url", + "faster_whisper_url", "whisper_cpp_url", "groq_api_key", "kokoro_url", "whisper_api_key", "tts_api_key", "voice_design_api_key", "elevenlabs_api_key", "tts_stability_enabled", "tts_extra_params", "tts_extra_params_by_backend", } @@ -301,6 +306,7 @@ _TTS_STABILITY_BY_BACKEND_DEFAULT = { "nvidia_magpie": {}, "nvidia_zeroshot": {}, "nvidia_flow": {}, + "kokoro": {}, } _TTS_PAYLOAD_CORE_KEYS = {"model", "input", "voice", "response_format", "instruct", "language"} @@ -400,9 +406,12 @@ def _clean_preview_backend(value: str) -> str: "nvidia_flow_tts": "nvidia_flow", "magpie_flow": "nvidia_flow", "flow": "nvidia_flow", + "kokoro_fastapi": "kokoro", + "kokoro_tts": "kokoro", + "kokoro_local": "kokoro", } key = aliases.get(key, key) - return key if key in {"voice_clone", "streaming", "customvoice", "voice_design", "nvidia_magpie", "nvidia_zeroshot", "nvidia_flow"} else "voice_clone" + return key if key in {"voice_clone", "streaming", "customvoice", "voice_design", "nvidia_magpie", "nvidia_zeroshot", "nvidia_flow", "kokoro"} else "voice_clone" def _preview_backend_base_url(settings: dict, backend: str) -> str: @@ -419,6 +428,8 @@ def _preview_backend_base_url(settings: dict, backend: str) -> str: return settings.get("nvidia_zeroshot_url") or settings.get("nvidia_clone_url") or settings.get("nvidia_router_url") or _NVIDIA_ZEROSHOT_DEFAULT if backend == "nvidia_flow": return settings.get("nvidia_flow_url") or settings.get("nvidia_clone_url") or settings.get("nvidia_router_url") or _NVIDIA_FLOW_DEFAULT + if backend == "kokoro": + return settings.get("kokoro_url") or _KOKORO_DEFAULT return settings.get("tts_url") or _TTS_DEFAULT @@ -453,6 +464,10 @@ def _load_settings() -> dict: "nvidia_clone_url": _NVIDIA_CLONE_DEFAULT, "nvidia_zeroshot_url": _NVIDIA_ZEROSHOT_DEFAULT, "nvidia_flow_url": _NVIDIA_FLOW_DEFAULT, + "faster_whisper_url": _FASTER_WHISPER_DEFAULT, + "whisper_cpp_url": _WHISPER_CPP_DEFAULT, + "groq_api_key": "", + "kokoro_url": _KOKORO_DEFAULT, "whisper_api_key": "", "tts_api_key": "", "voice_design_api_key": "", @@ -1718,13 +1733,34 @@ _STT_BACKEND_ALIASES = { "router": "nvidia_router", "speech_router": "nvidia_router", "nvidia_speech_router": "nvidia_router", + "faster_whisper_server": "faster_whisper", + "faster-whisper": "faster_whisper", + "ctranslate2": "faster_whisper", + "faster_w": "faster_whisper", + "whisper-cpp": "whisper_cpp", + "whisper_cpp_server": "whisper_cpp", + "cpp": "whisper_cpp", + "groq": "groq_whisper", + "groq_stt": "groq_whisper", + "groq-whisper": "groq_whisper", } +_STT_BACKEND_METRICS: dict[str, dict] = { + "configured": {"speed": "GPU / CPU", "latency": "1–5 s", "quality": "large-v3", "ram": "3 GB VRAM"}, + "faster_whisper": {"speed": "~70× RT · GPU", "latency": "0.5–2 s", "quality": "large-v3", "ram": "1.5 GB VRAM"}, + "whisper_cpp": {"speed": "~8–15× RT · CPU", "latency": "1–5 s", "quality": "large-v3 Q5", "ram": "~1 GB RAM"}, + "groq_whisper": {"speed": "fastest cloud", "latency": "0.5–1 s", "quality": "Whisper Turbo", "ram": "cloud · 0"}, + "nvidia_parakeet":{"speed": "~200× RT · GPU", "latency": "<0.3 s", "quality": "Parakeet-TDT", "ram": "2 GB VRAM"}, + "nvidia_router": {"speed": "GPU routed", "latency": "~0.5 s", "quality": "varies", "ram": "varies"}, +} + + +_STT_VALID_BACKENDS = {"configured", "nvidia_parakeet", "nvidia_router", "faster_whisper", "whisper_cpp", "groq_whisper"} def _clean_stt_backend(value: str) -> str: key = re.sub(r"[^a-z0-9]+", "_", str(value or "configured").lower()).strip("_") key = _STT_BACKEND_ALIASES.get(key, key) - return key if key in {"configured", "nvidia_parakeet", "nvidia_router"} else "configured" + return key if key in _STT_VALID_BACKENDS else "configured" def _stt_backend_url(settings: dict, backend: str) -> str: @@ -1733,22 +1769,44 @@ def _stt_backend_url(settings: dict, backend: str) -> str: return settings.get("nvidia_asr_url") or _NVIDIA_ASR_DEFAULT if backend == "nvidia_router": return settings.get("nvidia_router_url") or _NVIDIA_ROUTER_DEFAULT + if backend == "faster_whisper": + return settings.get("faster_whisper_url") or _FASTER_WHISPER_DEFAULT + if backend == "whisper_cpp": + return settings.get("whisper_cpp_url") or _WHISPER_CPP_DEFAULT + if backend == "groq_whisper": + return _GROQ_STT_ENDPOINT return settings.get("whisper_url") or _WHISPER_DEFAULT def _stt_backend_model(backend: str) -> str: backend = _clean_stt_backend(backend) - return "whisper-1" if backend in {"nvidia_parakeet", "nvidia_router"} else "large-v3" + if backend in {"nvidia_parakeet", "nvidia_router"}: + return "whisper-1" + if backend == "whisper_cpp": + return "whisper-1" + if backend == "groq_whisper": + return "whisper-large-v3-turbo" + return "large-v3" def _stt_backend_label(backend: str, url: str) -> str: labels = { - "configured": "Configured Whisper/STT", + "configured": "Configured Whisper/STT", "nvidia_parakeet": "NVIDIA Parakeet ASR", - "nvidia_router": "NVIDIA Speech Router", + "nvidia_router": "NVIDIA Speech Router", + "faster_whisper": "faster-whisper (CTranslate2 GPU)", + "whisper_cpp": "whisper.cpp (CPU/CUDA)", + "groq_whisper": "Groq Whisper (cloud · free)", } port = _backend_port_label(url) - return f"{port} {labels.get(backend, backend)}" if port else labels.get(backend, backend) + label = labels.get(backend, backend) + return f"{port} {label}" if port else label + + +def _stt_backend_api_key(settings: dict, backend: str) -> str: + if backend == "groq_whisper": + return settings.get("groq_api_key", "").strip() + return settings.get("whisper_api_key", "").strip() def _stt_backend_health(url: str) -> tuple[bool, list[str]]: @@ -1781,13 +1839,20 @@ async def stt_backends(): settings = _load_settings() items = [] seen_urls: set[tuple[str, str]] = set() - for backend in ("configured", "nvidia_parakeet", "nvidia_router"): - url = _validate_http_url(_stt_backend_url(settings, backend), allow_private=True).rstrip("/") + ordered = ("configured", "faster_whisper", "whisper_cpp", "groq_whisper", "nvidia_parakeet", "nvidia_router") + for backend in ordered: + raw_url = _stt_backend_url(settings, backend) + url = _validate_http_url(raw_url, allow_private=True).rstrip("/") key = (backend, url) if key in seen_urls: continue seen_urls.add(key) - ok, models = _stt_backend_health(url) + api_key = _stt_backend_api_key(settings, backend) + if backend == "groq_whisper": + ok = bool(api_key) + models: list[str] = ["whisper-large-v3-turbo", "whisper-large-v3", "distil-whisper-large-v3-en"] + else: + ok, models = _stt_backend_health(url) items.append({ "id": backend, "label": _stt_backend_label(backend, url), @@ -1796,6 +1861,7 @@ async def stt_backends(): "available": ok, "model": _stt_backend_model(backend), "models": models, + "metrics": _STT_BACKEND_METRICS.get(backend, {}), }) return {"backends": items} @@ -1817,7 +1883,7 @@ def _transcription_text_from_response(resp: requests.Response) -> str: def _transcribe_audio(src: Path, settings: dict, backend: str = "configured") -> tuple[str, str]: backend = _clean_stt_backend(backend) stt_url = _validate_http_url(_stt_backend_url(settings, backend), allow_private=True).rstrip("/") - stt_key = settings.get("whisper_api_key", "").strip() + stt_key = _stt_backend_api_key(settings, backend) hdrs = {"Authorization": f"Bearer {stt_key}"} if stt_key else {} model = _stt_backend_model(backend) with src.open("rb") as f: @@ -2928,6 +2994,21 @@ def _voice_ids_from_payload(payload) -> list: return grouped +_KOKORO_BUILTIN_VOICES = [ + "af", # Default American Female + "af_bella", # Bella — American Female (warm) + "af_nicole", # Nicole — American Female (clear) + "af_sarah", # Sarah — American Female (expressive) + "af_sky", # Sky — American Female (bright) + "bf_emma", # Emma — British Female (refined) + "bf_isabella",# Isabella — British Female (elegant) + "am_adam", # Adam — American Male (deep) + "am_michael", # Michael — American Male (smooth) + "bm_george", # George — British Male (authoritative) + "bm_lewis", # Lewis — British Male (natural) +] + + def _fetch_backend_voices(settings: dict, backend: str) -> list: backend = _clean_preview_backend(backend) if backend in {"nvidia_zeroshot", "nvidia_flow"}: @@ -2944,6 +3025,8 @@ def _fetch_backend_voices(settings: dict, backend: str) -> list: return voices except Exception: continue + if backend == "kokoro": + return _KOKORO_BUILTIN_VOICES return [] @@ -3005,13 +3088,14 @@ def _backend_port_label(url: str) -> str: def _backend_display_name(backend: str, url: str) -> str: names = { - "voice_clone": "Voice Clone/Base (WAV File)", - "voice_design": "Voice Design", - "customvoice": "CustomVoice", - "streaming": "Streaming (WAV File)", - "nvidia_magpie": "NVIDIA Magpie TTS", - "nvidia_zeroshot": "NVIDIA Magpie Zeroshot Clone", - "nvidia_flow": "NVIDIA Magpie Flow Clone", + "voice_clone": "Voice Clone/Base (WAV File)", + "voice_design": "Voice Design", + "customvoice": "CustomVoice", + "streaming": "Streaming (WAV File)", + "nvidia_magpie": "NVIDIA Magpie TTS", + "nvidia_zeroshot":"NVIDIA Magpie Zeroshot Clone", + "nvidia_flow": "NVIDIA Magpie Flow Clone", + "kokoro": "Kokoro FastAPI (82M)", } port = _backend_port_label(url) return f"{port} {names.get(backend, backend)}" if port else names.get(backend, backend) @@ -3024,63 +3108,64 @@ def _backend_capabilities(backend: str) -> dict: "identity": "Strongest match to saved WAV voices.", "style": "Weak per-request style; instruct may be ignored.", "best_for": "Known voices, multilingual cloning, benchmarks, and reliable speaker identity.", - "uses_wav": True, - "style_aware": False, - "true_streaming": False, + "uses_wav": True, "style_aware": False, "true_streaming": False, + "speed": "~0.3× GPU", "latency": "1–3 s", "quality": "Premium clone", "ram": "6–8 GB VRAM", }, "voice_design": { "purpose": "Create or reuse prompt-designed voices from natural-language descriptions.", "identity": "Prompt persona, not the selected WAV speaker unless you first export/clone it.", "style": "Strong style and emotion control through instruct text.", "best_for": "New characters, personas, dialogue, and designing reference WAVs to clone later.", - "uses_wav": False, - "style_aware": True, - "true_streaming": False, + "uses_wav": False, "style_aware": True, "true_streaming": False, + "speed": "~0.4× GPU", "latency": "1–3 s", "quality": "Premium", "ram": "6–8 GB VRAM", }, "customvoice": { "purpose": "Generate speech with the CustomVoice model voices.", "identity": "Uses CustomVoice speakers, not arbitrary active WAV voices unless trained/configured there.", "style": "Good per-request style and emotion control.", "best_for": "Controlled style with configured CustomVoice speakers.", - "uses_wav": False, - "style_aware": True, - "true_streaming": False, + "uses_wav": False, "style_aware": True, "true_streaming": False, + "speed": "~0.3× GPU", "latency": "1–3 s", "quality": "Premium", "ram": "6–8 GB VRAM", }, "streaming": { "purpose": "Low-latency playback from saved WAV/reference voices.", "identity": "Same WAV voice identity path as Base.", "style": "Weak per-request style in the current streaming server.", "best_for": "Long text, assistants, Open WebUI/SillyTavern playback that can start before completion.", - "uses_wav": True, - "style_aware": False, - "true_streaming": True, + "uses_wav": True, "style_aware": False, "true_streaming": True, + "speed": "~0.1× GPU", "latency": "0.5–1 s", "quality": "Premium", "ram": "6–8 GB VRAM", }, "nvidia_magpie": { "purpose": "Generate speech with NVIDIA Magpie fixed speaker voices.", "identity": "Uses Magpie speaker aliases such as sofia, aria, jason, leo, and john; it is not a WAV voice-cloning model.", "style": "Language and speaker are controlled by the backend voice config; per-request style text is usually ignored.", "best_for": "Fast local NVIDIA TTS voices and OpenAI-compatible assistant playback.", - "uses_wav": False, - "style_aware": False, - "true_streaming": False, + "uses_wav": False, "style_aware": False, "true_streaming": False, + "speed": "~0.05× GPU", "latency": "0.3–0.8 s", "quality": "High", "ram": "4–6 GB VRAM", }, "nvidia_zeroshot": { "purpose": "Clone a saved library voice through NVIDIA Magpie TTS Zeroshot NIM.", "identity": "Sends the selected WAV as audio_prompt; no prompt transcript is required.", "style": "Best with a clear 3-10 second prompt. Optional quality params can be configured in Settings.", "best_for": "Fast NVIDIA reference-audio cloning, streaming-class use cases, live agents, and games.", - "uses_wav": True, - "style_aware": False, - "true_streaming": False, + "uses_wav": True, "style_aware": False, "true_streaming": False, + "speed": "~0.1× GPU", "latency": "0.5–1 s", "quality": "High clone", "ram": "4–6 GB VRAM", }, "nvidia_flow": { "purpose": "Clone a saved library voice through NVIDIA Magpie TTS Flow NIM.", "identity": "Sends the selected WAV plus its exact saved reference transcript.", "style": "Offline high-fidelity clone path; prompt transcript must match the reference audio.", "best_for": "Studio-style dubbing, narration, and podcast-quality offline generation.", - "uses_wav": True, - "style_aware": False, - "true_streaming": False, + "uses_wav": True, "style_aware": False, "true_streaming": False, + "speed": "~0.2× GPU", "latency": "1–2 s", "quality": "Studio", "ram": "4–6 GB VRAM", + }, + "kokoro": { + "purpose": "High-quality English TTS with Kokoro 82M model. OpenAI-compatible endpoint.", + "identity": "Uses Kokoro built-in voices (af_bella, bf_emma, am_adam, …); no WAV cloning.", + "style": "Voice selection via voice ID. Style instruction is not supported.", + "best_for": "Fast, high-quality CPU TTS. Low RAM footprint. Easy local Docker setup.", + "uses_wav": False, "style_aware": False, "true_streaming": False, + "speed": "~0.1× CPU", "latency": "0.2–0.5 s", "quality": "High (82M)", "ram": "300 MB CPU", }, } return caps.get(_clean_preview_backend(backend), {}) @@ -3099,7 +3184,7 @@ def _backend_health(url: str) -> bool: def _backend_available(backend: str, voices: list, health: bool) -> bool: - if _clean_preview_backend(backend) in {"nvidia_zeroshot", "nvidia_flow"}: + if _clean_preview_backend(backend) in {"nvidia_zeroshot", "nvidia_flow", "kokoro"}: return health return bool(voices) or health @@ -3108,7 +3193,7 @@ def _backend_available(backend: str, voices: list, health: bool) -> bool: async def tts_backends(): settings = _load_settings() items = [] - for backend in ("voice_clone", "voice_design", "customvoice", "streaming", "nvidia_magpie", "nvidia_zeroshot", "nvidia_flow"): + for backend in ("voice_clone", "voice_design", "customvoice", "streaming", "kokoro", "nvidia_magpie", "nvidia_zeroshot", "nvidia_flow"): url = _validate_http_url(_preview_backend_base_url(settings, backend), allow_private=True).rstrip("/") voices = _fetch_backend_voices(settings, backend) health = _backend_health(url) @@ -3576,6 +3661,14 @@ def _preview_request_audio(text: str, voice: str, settings: dict, instruct: str return _nvidia_clone_request_audio(text, voice, settings, "zeroshot") if backend == "nvidia_flow": return _nvidia_clone_request_audio(text, voice, settings, "flow") + if backend == "kokoro": + return _tts_request_audio( + text, voice, settings, instruct, + url_override=_preview_backend_base_url(settings, "kokoro"), + api_key_override=settings.get("tts_api_key", ""), + backend_override="openai", + extra_backend="kokoro", + ) return _tts_request_audio(text, voice, settings, instruct) diff --git a/static/app.js b/static/app.js index 9cb9efb..5b87175 100644 --- a/static/app.js +++ b/static/app.js @@ -1757,8 +1757,28 @@ function backendHelpHtml(b, compact = false) { b.style_aware ? ['good', 'style-aware'] : ['warn', 'weak style'], b.true_streaming ? ['good', 'true streaming'] : ['', 'buffered/normal'], ].map(([cls, text]) => `${escHtml(text)}`).join(''); + const metricParts = []; + if (b.speed) metricParts.push(`⚡ ${escHtml(b.speed)}`); + if (b.latency) metricParts.push(`⏰ ${escHtml(b.latency)}`); + if (b.quality) metricParts.push(`⭐ ${escHtml(b.quality)}`); + if (b.ram) metricParts.push(`💾 ${escHtml(b.ram)}`); + const metrics = metricParts.length ? `
${metricParts.join('')}
` : ''; const detail = compact ? escHtml(b.best_for || '') : `${escHtml(b.purpose || '')}
Identity: ${escHtml(b.identity || '')}
Style: ${escHtml(b.style || '')}
Best for: ${escHtml(b.best_for || '')}`; - return `${escHtml(b.label)}
${tags}
${detail}
`; + return `${escHtml(b.label)}
${tags}
${metrics}
${detail}
`; +} + +function sttBackendHelpHtml(b) { + if (!b) return 'No STT engine selected.'; + const m = b.metrics || {}; + const metricParts = []; + if (m.speed) metricParts.push(`⚡ ${escHtml(m.speed)}`); + if (m.latency) metricParts.push(`⏰ ${escHtml(m.latency)}`); + if (m.quality) metricParts.push(`⭐ ${escHtml(m.quality)}`); + if (m.ram) metricParts.push(`💾 ${escHtml(m.ram)}`); + const metrics = metricParts.length ? `
${metricParts.join('')}
` : ''; + const modelList = Array.isArray(b.models) && b.models.length ? ' Models: ' + b.models.slice(0, 4).join(', ') + '.' : ''; + const avail = b.available ? `ready` : `unavailable`; + return `${escHtml(b.label)} ${avail}${metrics}
${escHtml(b.url)}${escHtml(modelList)}
`; } function updateBackendHelp() { @@ -1861,9 +1881,13 @@ async function refreshTtsBackendAvailability(selected = '') { async function loadSettings() { const s = await fetch('/api/settings').then(r => r.json()); - $('s-whisper-url').value = s.whisper_url || ''; - $('s-whisper-key').value = s.whisper_api_key || ''; - $('s-tts-url').value = s.tts_url || ''; + $('s-whisper-url').value = s.whisper_url || ''; + $('s-whisper-key').value = s.whisper_api_key || ''; + $('s-tts-url').value = s.tts_url || ''; + $('s-faster-whisper-url').value = s.faster_whisper_url || ''; + $('s-whisper-cpp-url').value = s.whisper_cpp_url || ''; + $('s-groq-api-key').value = s.groq_api_key || ''; + $('s-kokoro-url').value = s.kokoro_url || ''; _appSettings = s; $('s-tts-stream-url').value = s.tts_stream_url || ''; $('s-customvoice-url').value = s.customvoice_url || 'http://host.docker.internal:8022'; @@ -1884,6 +1908,7 @@ async function loadSettings() { $('s-tts-extra-nvidia-magpie').value = JSON.stringify(byBackend.nvidia_magpie || {}, null, 2); $('s-tts-extra-nvidia-zeroshot').value = JSON.stringify(byBackend.nvidia_zeroshot || {}, null, 2); $('s-tts-extra-nvidia-flow').value = JSON.stringify(byBackend.nvidia_flow || {}, null, 2); + $('s-tts-extra-kokoro').value = JSON.stringify(byBackend.kokoro || {}, null, 2); $('s-voice-design-url').value = s.voice_design_url || 'http://host.docker.internal:8021'; $('s-vd-key').value = s.voice_design_api_key || ''; $('s-voices-scan-dir').value = s.voices_scan_dir || ''; @@ -1913,6 +1938,8 @@ $('settings-btn').addEventListener('click', async () => { await loadSettings(); $('s-close-btn').addEventListener('click', async () => { await loadSettings(); toast('Settings reloaded', 'success'); }); $('s-use-parakeet-asr')?.addEventListener('click', () => { $('s-whisper-url').value = $('s-nvidia-asr-url').value || 'http://host.docker.internal:8092'; }); $('s-use-nvidia-router')?.addEventListener('click', () => { const url = $('s-nvidia-router-url').value || 'http://host.docker.internal:8090'; $('s-whisper-url').value = url; $('s-nvidia-tts-url').value = url; }); +$('s-use-faster-whisper')?.addEventListener('click', () => { $('s-whisper-url').value = $('s-faster-whisper-url').value || 'http://host.docker.internal:8000'; }); +$('s-use-whisper-cpp')?.addEventListener('click', () => { $('s-whisper-url').value = $('s-whisper-cpp-url').value || 'http://host.docker.internal:8080'; }); $('s-save-btn').addEventListener('click', async () => { let ttsExtraParamsByBackend = {}; const paramFields = [ @@ -1923,6 +1950,7 @@ $('s-save-btn').addEventListener('click', async () => { ['nvidia_magpie', 's-tts-extra-nvidia-magpie', 'NVIDIA Magpie'], ['nvidia_zeroshot', 's-tts-extra-nvidia-zeroshot', 'NVIDIA Zeroshot'], ['nvidia_flow', 's-tts-extra-nvidia-flow', 'NVIDIA Flow'], + ['kokoro', 's-tts-extra-kokoro', 'Kokoro'], ]; try { for (const [key, id, label] of paramFields) { @@ -1936,7 +1964,11 @@ $('s-save-btn').addEventListener('click', async () => { body: JSON.stringify({ whisper_url: $('s-whisper-url').value, whisper_api_key: $('s-whisper-key').value, + faster_whisper_url: $('s-faster-whisper-url').value, + whisper_cpp_url: $('s-whisper-cpp-url').value, + groq_api_key: $('s-groq-api-key').value, tts_url: $('s-tts-url').value, + kokoro_url: $('s-kokoro-url').value, tts_stream_url: $('s-tts-stream-url').value, customvoice_url: $('s-customvoice-url').value, nvidia_router_url: $('s-nvidia-router-url').value, @@ -5805,8 +5837,7 @@ function updateSttBackendHelp() { const help = $('stt-tts-stt-help'); if (!help) return; if (!b) { help.textContent = 'No STT engine status loaded yet.'; return; } - const models = Array.isArray(b.models) && b.models.length ? ' Models: ' + b.models.slice(0, 4).join(', ') + '.' : ''; - help.textContent = `${b.available ? 'Ready' : 'Unavailable'} at ${b.url}.${models}`; + help.innerHTML = sttBackendHelpHtml(b); } async function refreshSttBackends(selected = '') { @@ -6346,6 +6377,15 @@ function renderLocalContainers(containers) { 'magpie-tts': '🐦', 'parakeet-rnnt-nim': '🦜', }; + const DC_METRICS = { + 'faster-qwen3-tts-voiceclone': [['⚡','~0.3× GPU'],['⏰','1–3 s'],['⭐','Premium clone'],['💾','6–8 GB VRAM']], + 'faster-qwen3-tts-voicedesign': [['⚡','~0.4× GPU'],['⏰','1–3 s'],['⭐','Premium'], ['💾','6–8 GB VRAM']], + 'faster-qwen3-tts-customvoice': [['⚡','~0.3× GPU'],['⏰','1–3 s'],['⭐','Premium'], ['💾','6–8 GB VRAM']], + 'faster-qwen3-tts-streaming': [['⚡','~0.1× GPU'],['⏰','0.5–1 s'],['⭐','Premium'], ['💾','6–8 GB VRAM']], + 'magpie-tts': [['⚡','~0.05× GPU'],['⏰','0.3–0.8 s'],['⭐','High'], ['💾','4–6 GB VRAM']], + 'parakeet-asr': [['⚡','~200× RT GPU'],['⏰','<0.3 s'],['⭐','Parakeet-TDT'],['💾','2 GB VRAM']], + 'parakeet-rnnt-nim': [['⚡','~200× RT GPU'],['⏰','<0.3 s'],['⭐','Parakeet-1B'], ['💾','2 GB VRAM']], + }; const roleIcon = { tts: '🔊', stt: '🎙️', 'stt+tts': '🔄', llm: '🤖' }; grid.innerHTML = containers.map(c => { @@ -6362,6 +6402,9 @@ function renderLocalContainers(containers) { const portBadge = c.port ? `:${c.port}` : ''; const installed = st !== 'not_found'; const icon = DC_ICONS[c.name] || roleIcon[c.role] || '📦'; + const metricChips = (DC_METRICS[c.name] || []) + .map(([em, txt]) => `${em} ${escHtml(txt)}`).join(''); + const metricsHtml = metricChips ? `
${metricChips}
` : ''; const n = escHtml(c.name); const actions = installed @@ -6385,6 +6428,7 @@ function renderLocalContainers(containers) {
${escHtml(stLabel)}
+ ${metricsHtml} ${c.description ? `

${escHtml(c.description)}

` : ''}
${actions}
`; @@ -6524,6 +6568,44 @@ $('dc-refresh-btn')?.addEventListener('click', loadLocalContainers); }); }); + // ── "Use as STT / TTS" quick-apply buttons in AI Backends section ────────── + async function applyAndSaveSettings(patch) { + try { + const resp = await fetch('/api/settings').then(r => r.json()); + const updated = { ...resp, ...patch }; + await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch) }); + Object.assign(_appSettings || {}, patch); + // refresh visible inputs in Settings if open + for (const [id, val] of Object.entries(patch)) { + const inp = $('s-' + id.replace(/_/g, '-')); + if (inp && inp.value !== undefined) inp.value = val; + } + await refreshTtsBackendAvailability(); + await refreshSttBackends(); + } catch (e) { toast('Apply failed: ' + e.message, 'error'); } + } + + $('llm-use-faster-whisper-stt')?.addEventListener('click', () => { + const url = document.querySelector('[data-llm-local-key="faster-whisper"]')?.value.trim() + || 'http://host.docker.internal:8000'; + applyAndSaveSettings({ faster_whisper_url: url }); + toast('faster-whisper-server URL saved → Settings. Use the "faster-whisper" engine in the STT dropdown.', 'success'); + }); + + $('llm-use-whisper-cpp-stt')?.addEventListener('click', () => { + const url = document.querySelector('[data-llm-local-key="whisper-cpp"]')?.value.trim() + || 'http://host.docker.internal:8080'; + applyAndSaveSettings({ whisper_cpp_url: url }); + toast('whisper.cpp URL saved → Settings. Use the "whisper.cpp" engine in the STT dropdown.', 'success'); + }); + + $('llm-use-kokoro-tts')?.addEventListener('click', () => { + const url = document.querySelector('[data-llm-local-key="kokoro"]')?.value.trim() + || 'http://host.docker.internal:8880/v1'; + applyAndSaveSettings({ kokoro_url: url }); + toast('Kokoro FastAPI URL saved → Settings. It now appears as "Kokoro FastAPI (82M)" in the TTS backend dropdown.', 'success'); + }); + })(); // ── Collapsible cards ────────────────────────────────────────────────────── diff --git a/static/sections/s-llms.html b/static/sections/s-llms.html index 106d802..eabc6f3 100644 --- a/static/sections/s-llms.html +++ b/static/sections/s-llms.html @@ -140,12 +140,21 @@ Check "Enable CORS" for browser access faster-whisper-server OpenAI-compat +
+ ⚡ ~70× RT GPU + ⏰ 0.5–2 s + ⭐ large-v3 + 💾 1.5 GB VRAM +

Drop-in local replacement for the Whisper API. GPU-accelerated via CTranslate2. OpenAI-compatible endpoint.

URL - +
+
+ +
docker-compose snippet @@ -171,12 +180,21 @@ Check "Enable CORS" for browser access whisper.cpp HTTP server
+
+ ⚡ ~8–15× RT CPU + ⏰ 1–5 s + ⭐ large-v3 Q5 + 💾 ~1 GB RAM +

Minimal C++ Whisper with a built-in HTTP server. CPU or Metal/CUDA. Low memory, fast on consumer hardware.

URL
+
+ +
Build & run @@ -202,7 +220,13 @@ cd whisper.cpp && cmake -B build && cmake --build build -j Piper TTS Fast · offline
-

Lightning-fast offline TTS. Runs on CPU in real time. 50+ language voices available. Ideal for low-latency pipelines.

+
+ ⚡ ~1× CPU realtime + ⏰ <50 ms + ⭐ Good (VITS) + 💾 ~50 MB RAM +
+

Lightning-fast offline TTS. Runs on CPU in real time. 50+ language voices available. Uses Wyoming protocol (port 10200) — not directly OpenAI-compatible.

URL @@ -226,12 +250,21 @@ cd whisper.cpp && cmake -B build && cmake --build build -j Kokoro FastAPI OpenAI-compat TTS
-

Kokoro-82M running behind an OpenAI-compatible TTS endpoint. Drop-in replacement for OpenAI’s TTS API.

+
+ ⚡ ~0.1× CPU RTF + ⏰ ~200 ms + ⭐ High (82M) + 💾 300 MB CPU +
+

Kokoro-82M running behind an OpenAI-compatible TTS endpoint. 11 built-in voices (af_bella, bf_emma, am_adam…). Drop-in replacement for OpenAI’s TTS API.

URL
+
+ +
Docker @@ -252,6 +285,12 @@ docker run -p 8880:8880 --gpus all \ XTTS v2 Voice cloning
+
+ ⚡ ~0.5× GPU RTF + ⏰ 1–3 s + ⭐ High (WAV clone) + 💾 3–4 GB VRAM +

Coqui XTTS — multilingual voice cloning from a 6-second sample. 17 languages. Compatible with this app’s voice library.

URL @@ -307,8 +346,8 @@ docker run -p 8880:8880 --gpus all \
2 000 req / day - Fastest inference - OpenAI API format + ⚡ Fastest cloud STT + 💾 Cloud · 0 VRAM
@@ -324,6 +363,7 @@ docker run -p 8880:8880 --gpus all \ whisper-large-v3 distil-whisper-large-v3-en
+
Save key in Settings → Groq API key to enable Groq Whisper in the STT dropdown → Try It Out and Clone a Voice.
diff --git a/static/sections/s-settings.html b/static/sections/s-settings.html index 6de2d30..0964cac 100644 --- a/static/sections/s-settings.html +++ b/static/sections/s-settings.html @@ -34,7 +34,7 @@
TTS Text to Speech - Qwen3 engines: clone, design, custom, streaming + Qwen3 engines: clone, design, custom, streaming · Kokoro FastAPI
@@ -57,6 +57,11 @@ Progressive low-latency WAV playback.
+
+ + + OpenAI-compatible TTS. Built-in voices: af_bella, bf_emma, am_adam… ~300 MB RAM · ~0.1× CPU RTF. +
@@ -97,18 +102,38 @@
STT Speech to Text - Whisper, Parakeet, or NVIDIA router + Whisper, faster-whisper, whisper.cpp, Groq, Parakeet, NVIDIA router
- + - Reference text recognition. Expected: POST /v1/audio/transcriptions. + Default recognition endpoint. Expected: POST /v1/audio/transcriptions.
+ +
+
+ + + GPU-accelerated Whisper via CTranslate2. ~70× RT · 1.5 GB VRAM · POST /v1/audio/transcriptions. +
+
+ + + Lightweight C++ Whisper server. ~8–15× RT CPU · ~1 GB RAM · POST /v1/audio/transcriptions. +
+
+ +
+ + +
+ Used for Groq Whisper STT (whisper-large-v3-turbo) and Groq LLM. Free: 2 000 req/day. Fastest cloud transcription. +
@@ -185,6 +210,11 @@ Optional multipart fields. The app also sends the saved reference transcript.
+
+ + + Extra JSON fields for the Kokoro backend. Usually empty — voice is selected from the voice dropdown. +
diff --git a/static/sections/s-tryout.html b/static/sections/s-tryout.html index def5c50..34e38cf 100644 --- a/static/sections/s-tryout.html +++ b/static/sections/s-tryout.html @@ -91,7 +91,7 @@ - Uses Settings → Whisper/STT URL by default. +
Uses Settings → Whisper/STT URL by default.
diff --git a/static/style.css b/static/style.css index 23cc0f9..dd55fa4 100644 --- a/static/style.css +++ b/static/style.css @@ -542,6 +542,8 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .backend-tag { border: 1px solid var(--border); border-radius: 4px; padding: 2px 6px; background: var(--panel); color: var(--subtext); font-size: 12px; } .backend-tag.good { color: var(--green); border-color: rgba(22,163,74,.35); } .backend-tag.warn { color: var(--yellow); border-color: rgba(217,119,6,.35); } +.backend-metrics-row { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 2px; } +.backend-metric-tag { border: 1px solid rgba(99,102,241,.3); border-radius: 4px; padding: 1px 6px; background: rgba(99,102,241,.07); color: var(--accent); font-size: 11px; white-space: nowrap; } .style-backend-warn { margin-top: 6px; padding: 7px 10px; border-radius: 6px; background: rgba(220,38,38,.07); border: 1px solid rgba(220,38,38,.25); @@ -882,7 +884,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .vl-avatar-img { width: 100%; height: 100%; object-fit: cover; border-radius: var(--radius); } .vl-avatar-flag { background: transparent; } .vl-avatar-flag .fi { display: block; width: 100%; height: 100%; background-size: cover; background-position: center; border-radius: var(--radius); } -.vl-avatar-initial { } +.vl-avatar-initial { background: transparent; } .vl-avatar-letter { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; font-size: 17px; font-weight: 800; border-radius: var(--radius); } /* Voice info block (name + type badge + gender chip) */ @@ -1479,6 +1481,25 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami } .llm-local-link:hover { text-decoration: underline; } +/* Metric chips inside AI Backend cards */ +.llm-local-metrics { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 4px; } +.llm-metric-chip { + font-size: 10px; padding: 1px 6px; border-radius: 4px; + border: 1px solid rgba(99,102,241,.3); background: rgba(99,102,241,.07); + color: var(--accent); white-space: nowrap; +} + +/* Use-as-STT/TTS action button inside AI Backend cards */ +.llm-local-actions { display: flex; gap: 6px; flex-wrap: wrap; } +.llm-use-btn { + font-size: 11px; padding: 3px 9px; border-radius: 5px; + border: 1px solid rgba(13,148,136,.4); background: rgba(13,148,136,.07); + color: var(--teal); cursor: pointer; font-family: inherit; font-weight: 600; + transition: background .15s, border-color .15s; +} +.llm-use-btn:hover { background: rgba(13,148,136,.15); border-color: var(--teal); } +.llm-use-btn:active { background: rgba(13,148,136,.25); } + /* Password show/hide toggle */ .llm-eye-btn { background: none; border: 1px solid var(--border); border-radius: 5px; @@ -1622,6 +1643,12 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami } .dc-status-label { font-size: 11px; color: var(--subtext); margin-bottom: 2px; } .dc-desc { font-size: 12px; color: var(--subtext); line-height: 1.45; margin: 0; flex: 1; } +.dc-metrics { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 4px; } +.dc-metric-chip { + font-size: 10px; padding: 1px 6px; border-radius: 4px; + border: 1px solid rgba(99,102,241,.3); background: rgba(99,102,241,.07); + color: var(--accent); white-space: nowrap; +} .dc-actions { margin-top: 6px; display: flex; gap: 6px; flex-wrap: wrap; } .dc-btn { font-size: 12px; padding: 4px 12px; }