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 ? `
${escHtml(c.description)}
` : ''}Drop-in local replacement for the Whisper API. GPU-accelerated via CTranslate2. OpenAI-compatible endpoint.
Minimal C++ Whisper with a built-in HTTP server. CPU or Metal/CUDA. Low memory, fast on consumer hardware.
Lightning-fast offline TTS. Runs on CPU in real time. 50+ language voices available. Ideal for low-latency pipelines.
+Lightning-fast offline TTS. Runs on CPU in real time. 50+ language voices available. Uses Wyoming protocol (port 10200) — not directly OpenAI-compatible.
Kokoro-82M running behind an OpenAI-compatible TTS endpoint. Drop-in replacement for OpenAI’s TTS API.
+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.
Coqui XTTS — multilingual voice cloning from a 6-second sample. 17 languages. Compatible with this app’s voice library.
POST /v1/audio/transcriptions.
+ Default recognition endpoint. Expected: POST /v1/audio/transcriptions.
POST /v1/audio/transcriptions.
+ POST /v1/audio/transcriptions.
+