Add Kokoro TTS + faster-whisper/whisper.cpp/Groq STT backends with metrics
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 <noreply@anthropic.com>
This commit is contained in:
parent
90cc7b7eb1
commit
08a63ef2d9
153
server.py
153
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,12 +1769,24 @@ 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:
|
||||
@ -1746,9 +1794,19 @@ def _stt_backend_label(backend: str, url: str) -> str:
|
||||
"configured": "Configured Whisper/STT",
|
||||
"nvidia_parakeet": "NVIDIA Parakeet ASR",
|
||||
"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,12 +1839,19 @@ 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)
|
||||
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,
|
||||
@ -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 []
|
||||
|
||||
|
||||
@ -3012,6 +3095,7 @@ def _backend_display_name(backend: str, url: str) -> str:
|
||||
"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)
|
||||
|
||||
|
||||
|
||||
@ -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]) => `<span class="backend-tag ${cls}">${escHtml(text)}</span>`).join('');
|
||||
const metricParts = [];
|
||||
if (b.speed) metricParts.push(`<span class="backend-metric-tag">⚡ ${escHtml(b.speed)}</span>`);
|
||||
if (b.latency) metricParts.push(`<span class="backend-metric-tag">⏰ ${escHtml(b.latency)}</span>`);
|
||||
if (b.quality) metricParts.push(`<span class="backend-metric-tag">⭐ ${escHtml(b.quality)}</span>`);
|
||||
if (b.ram) metricParts.push(`<span class="backend-metric-tag">💾 ${escHtml(b.ram)}</span>`);
|
||||
const metrics = metricParts.length ? `<div class="backend-metrics-row">${metricParts.join('')}</div>` : '';
|
||||
const detail = compact ? escHtml(b.best_for || '') : `${escHtml(b.purpose || '')}<br><strong>Identity:</strong> ${escHtml(b.identity || '')}<br><strong>Style:</strong> ${escHtml(b.style || '')}<br><strong>Best for:</strong> ${escHtml(b.best_for || '')}`;
|
||||
return `<strong>${escHtml(b.label)}</strong><div class="backend-help-tags">${tags}</div><div>${detail}</div>`;
|
||||
return `<strong>${escHtml(b.label)}</strong><div class="backend-help-tags">${tags}</div>${metrics}<div>${detail}</div>`;
|
||||
}
|
||||
|
||||
function sttBackendHelpHtml(b) {
|
||||
if (!b) return 'No STT engine selected.';
|
||||
const m = b.metrics || {};
|
||||
const metricParts = [];
|
||||
if (m.speed) metricParts.push(`<span class="backend-metric-tag">⚡ ${escHtml(m.speed)}</span>`);
|
||||
if (m.latency) metricParts.push(`<span class="backend-metric-tag">⏰ ${escHtml(m.latency)}</span>`);
|
||||
if (m.quality) metricParts.push(`<span class="backend-metric-tag">⭐ ${escHtml(m.quality)}</span>`);
|
||||
if (m.ram) metricParts.push(`<span class="backend-metric-tag">💾 ${escHtml(m.ram)}</span>`);
|
||||
const metrics = metricParts.length ? `<div class="backend-metrics-row">${metricParts.join('')}</div>` : '';
|
||||
const modelList = Array.isArray(b.models) && b.models.length ? ' Models: ' + b.models.slice(0, 4).join(', ') + '.' : '';
|
||||
const avail = b.available ? `<span class="backend-tag good">ready</span>` : `<span class="backend-tag warn">unavailable</span>`;
|
||||
return `<strong>${escHtml(b.label)}</strong> ${avail}${metrics}<div style="margin-top:4px;font-size:0.85em;opacity:.8">${escHtml(b.url)}${escHtml(modelList)}</div>`;
|
||||
}
|
||||
|
||||
function updateBackendHelp() {
|
||||
@ -1864,6 +1884,10 @@ async function loadSettings() {
|
||||
$('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 ? `<span class="dc-port">:${c.port}</span>` : '';
|
||||
const installed = st !== 'not_found';
|
||||
const icon = DC_ICONS[c.name] || roleIcon[c.role] || '📦';
|
||||
const metricChips = (DC_METRICS[c.name] || [])
|
||||
.map(([em, txt]) => `<span class="dc-metric-chip">${em} ${escHtml(txt)}</span>`).join('');
|
||||
const metricsHtml = metricChips ? `<div class="dc-metrics">${metricChips}</div>` : '';
|
||||
|
||||
const n = escHtml(c.name);
|
||||
const actions = installed
|
||||
@ -6385,6 +6428,7 @@ function renderLocalContainers(containers) {
|
||||
</span>
|
||||
</div>
|
||||
<div class="dc-status-label">${escHtml(stLabel)}</div>
|
||||
${metricsHtml}
|
||||
${c.description ? `<p class="dc-desc">${escHtml(c.description)}</p>` : ''}
|
||||
<div class="dc-actions">${actions}</div>
|
||||
</div>`;
|
||||
@ -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 ──────────────────────────────────────────────────────
|
||||
|
||||
@ -140,12 +140,21 @@ Check "Enable CORS" for browser access</pre>
|
||||
<span class="llm-local-name">faster-whisper-server</span>
|
||||
<span class="llm-local-compat">OpenAI-compat</span>
|
||||
</div>
|
||||
<div class="llm-local-metrics">
|
||||
<span class="llm-metric-chip">⚡ ~70× RT GPU</span>
|
||||
<span class="llm-metric-chip">⏰ 0.5–2 s</span>
|
||||
<span class="llm-metric-chip">⭐ large-v3</span>
|
||||
<span class="llm-metric-chip">💾 1.5 GB VRAM</span>
|
||||
</div>
|
||||
<p class="llm-local-desc">Drop-in local replacement for the Whisper API. GPU-accelerated via CTranslate2. OpenAI-compatible endpoint.</p>
|
||||
<div class="llm-local-url">
|
||||
<span class="llm-local-url-label">URL</span>
|
||||
<input class="llm-local-url-inp" type="text" placeholder="http://localhost:8000/v1" data-llm-local-key="faster-whisper" data-llm-local-default="http://localhost:8000/v1" spellcheck="false">
|
||||
<input class="llm-local-url-inp" type="text" placeholder="http://localhost:8000" data-llm-local-key="faster-whisper" data-llm-local-default="http://localhost:8000" spellcheck="false">
|
||||
<button class="llm-local-ping" data-ping-key="faster-whisper" title="Test connection">Connect</button>
|
||||
</div>
|
||||
<div class="llm-local-actions">
|
||||
<button class="llm-use-btn" id="llm-use-faster-whisper-stt" type="button" title="Copy URL to Settings → faster-whisper-server URL and set as active STT">📋 Use as STT</button>
|
||||
</div>
|
||||
<div class="llm-local-snippet">
|
||||
<div class="llm-snippet-bar">
|
||||
<span>docker-compose snippet</span>
|
||||
@ -171,12 +180,21 @@ Check "Enable CORS" for browser access</pre>
|
||||
<span class="llm-local-name">whisper.cpp</span>
|
||||
<span class="llm-local-compat">HTTP server</span>
|
||||
</div>
|
||||
<div class="llm-local-metrics">
|
||||
<span class="llm-metric-chip">⚡ ~8–15× RT CPU</span>
|
||||
<span class="llm-metric-chip">⏰ 1–5 s</span>
|
||||
<span class="llm-metric-chip">⭐ large-v3 Q5</span>
|
||||
<span class="llm-metric-chip">💾 ~1 GB RAM</span>
|
||||
</div>
|
||||
<p class="llm-local-desc">Minimal C++ Whisper with a built-in HTTP server. CPU or Metal/CUDA. Low memory, fast on consumer hardware.</p>
|
||||
<div class="llm-local-url">
|
||||
<span class="llm-local-url-label">URL</span>
|
||||
<input class="llm-local-url-inp" type="text" placeholder="http://localhost:8080" data-llm-local-key="whisper-cpp" data-llm-local-default="http://localhost:8080" spellcheck="false">
|
||||
<button class="llm-local-ping" data-ping-key="whisper-cpp" title="Test connection">Connect</button>
|
||||
</div>
|
||||
<div class="llm-local-actions">
|
||||
<button class="llm-use-btn" id="llm-use-whisper-cpp-stt" type="button" title="Copy URL to Settings → whisper.cpp URL and set as active STT">📋 Use as STT</button>
|
||||
</div>
|
||||
<div class="llm-local-snippet">
|
||||
<div class="llm-snippet-bar">
|
||||
<span>Build & run</span>
|
||||
@ -202,7 +220,13 @@ cd whisper.cpp && cmake -B build && cmake --build build -j
|
||||
<span class="llm-local-name">Piper TTS</span>
|
||||
<span class="llm-local-compat">Fast · offline</span>
|
||||
</div>
|
||||
<p class="llm-local-desc">Lightning-fast offline TTS. Runs on CPU in real time. 50+ language voices available. Ideal for low-latency pipelines.</p>
|
||||
<div class="llm-local-metrics">
|
||||
<span class="llm-metric-chip">⚡ ~1× CPU realtime</span>
|
||||
<span class="llm-metric-chip">⏰ <50 ms</span>
|
||||
<span class="llm-metric-chip">⭐ Good (VITS)</span>
|
||||
<span class="llm-metric-chip">💾 ~50 MB RAM</span>
|
||||
</div>
|
||||
<p class="llm-local-desc">Lightning-fast offline TTS. Runs on CPU in real time. 50+ language voices available. Uses Wyoming protocol (port 10200) — not directly OpenAI-compatible.</p>
|
||||
<div class="llm-local-url">
|
||||
<span class="llm-local-url-label">URL</span>
|
||||
<input class="llm-local-url-inp" type="text" placeholder="localhost:10200" data-llm-local-key="piper" data-llm-local-default="localhost:10200" spellcheck="false">
|
||||
@ -226,12 +250,21 @@ cd whisper.cpp && cmake -B build && cmake --build build -j
|
||||
<span class="llm-local-name">Kokoro FastAPI</span>
|
||||
<span class="llm-local-compat">OpenAI-compat TTS</span>
|
||||
</div>
|
||||
<p class="llm-local-desc">Kokoro-82M running behind an OpenAI-compatible TTS endpoint. Drop-in replacement for OpenAI’s TTS API.</p>
|
||||
<div class="llm-local-metrics">
|
||||
<span class="llm-metric-chip">⚡ ~0.1× CPU RTF</span>
|
||||
<span class="llm-metric-chip">⏰ ~200 ms</span>
|
||||
<span class="llm-metric-chip">⭐ High (82M)</span>
|
||||
<span class="llm-metric-chip">💾 300 MB CPU</span>
|
||||
</div>
|
||||
<p class="llm-local-desc">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.</p>
|
||||
<div class="llm-local-url">
|
||||
<span class="llm-local-url-label">URL</span>
|
||||
<input class="llm-local-url-inp" type="text" placeholder="http://localhost:8880/v1" data-llm-local-key="kokoro" data-llm-local-default="http://localhost:8880/v1" spellcheck="false">
|
||||
<button class="llm-local-ping" data-ping-key="kokoro" title="Test connection">Connect</button>
|
||||
</div>
|
||||
<div class="llm-local-actions">
|
||||
<button class="llm-use-btn" id="llm-use-kokoro-tts" type="button" title="Copy URL to Settings → Kokoro FastAPI URL and enable Kokoro in Try It Out">📋 Use as TTS</button>
|
||||
</div>
|
||||
<div class="llm-local-snippet">
|
||||
<div class="llm-snippet-bar">
|
||||
<span>Docker</span>
|
||||
@ -252,6 +285,12 @@ docker run -p 8880:8880 --gpus all \
|
||||
<span class="llm-local-name">XTTS v2</span>
|
||||
<span class="llm-local-compat">Voice cloning</span>
|
||||
</div>
|
||||
<div class="llm-local-metrics">
|
||||
<span class="llm-metric-chip">⚡ ~0.5× GPU RTF</span>
|
||||
<span class="llm-metric-chip">⏰ 1–3 s</span>
|
||||
<span class="llm-metric-chip">⭐ High (WAV clone)</span>
|
||||
<span class="llm-metric-chip">💾 3–4 GB VRAM</span>
|
||||
</div>
|
||||
<p class="llm-local-desc">Coqui XTTS — multilingual voice cloning from a 6-second sample. 17 languages. Compatible with this app’s voice library.</p>
|
||||
<div class="llm-local-url">
|
||||
<span class="llm-local-url-label">URL</span>
|
||||
@ -307,8 +346,8 @@ docker run -p 8880:8880 --gpus all \
|
||||
</div>
|
||||
<div class="llm-card-stats">
|
||||
<span>2 000 req / day</span>
|
||||
<span>Fastest inference</span>
|
||||
<span>OpenAI API format</span>
|
||||
<span>⚡ Fastest cloud STT</span>
|
||||
<span>💾 Cloud · 0 VRAM</span>
|
||||
</div>
|
||||
<div class="llm-field-row">
|
||||
<label class="llm-label">API key</label>
|
||||
@ -324,6 +363,7 @@ docker run -p 8880:8880 --gpus all \
|
||||
<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 → Groq API key</strong> to enable <em>Groq Whisper</em> in the STT dropdown → <em>Try It Out</em> and <em>Clone a Voice</em>.</div>
|
||||
</div>
|
||||
|
||||
<div class="llm-card">
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
<div class="settings-cluster">
|
||||
<div class="settings-cluster-head">
|
||||
<strong>TTS Text to Speech</strong>
|
||||
<span>Qwen3 engines: clone, design, custom, streaming</span>
|
||||
<span>Qwen3 engines: clone, design, custom, streaming · Kokoro FastAPI</span>
|
||||
</div>
|
||||
<div class="settings-grid compact">
|
||||
<div class="s-field">
|
||||
@ -57,6 +57,11 @@
|
||||
<input type="text" id="s-tts-stream-url" placeholder="http://host.docker.internal:8023">
|
||||
<span class="s-hint">Progressive low-latency WAV playback.</span>
|
||||
</div>
|
||||
<div class="s-field">
|
||||
<label>Kokoro FastAPI URL <span style="font-weight:400">(82M · CPU-friendly)</span></label>
|
||||
<input type="text" id="s-kokoro-url" placeholder="http://host.docker.internal:8880/v1">
|
||||
<span class="s-hint">OpenAI-compatible TTS. Built-in voices: af_bella, bf_emma, am_adam… ~300 MB RAM · ~0.1× CPU RTF.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -97,18 +102,38 @@
|
||||
<div class="settings-cluster settings-cluster-stt">
|
||||
<div class="settings-cluster-head">
|
||||
<strong>STT Speech to Text</strong>
|
||||
<span>Whisper, Parakeet, or NVIDIA router</span>
|
||||
<span>Whisper, faster-whisper, whisper.cpp, Groq, Parakeet, NVIDIA router</span>
|
||||
</div>
|
||||
<div class="settings-grid compact stt-settings-grid">
|
||||
<div class="s-field stt-url-field">
|
||||
<label>Whisper/STT URL</label>
|
||||
<label>Configured Whisper/STT URL</label>
|
||||
<input type="text" id="s-whisper-url" placeholder="http://host.docker.internal:8010">
|
||||
<span class="s-hint">Reference text recognition. Expected: <code>POST /v1/audio/transcriptions</code>.</span>
|
||||
<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 style="font-weight:400">(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× RT · 1.5 GB VRAM · <code>POST /v1/audio/transcriptions</code>.</span>
|
||||
</div>
|
||||
<div class="s-field">
|
||||
<label>whisper.cpp URL <span style="font-weight:400">(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–15× RT CPU · ~1 GB RAM · <code>POST /v1/audio/transcriptions</code>.</span>
|
||||
</div>
|
||||
<div class="s-field">
|
||||
<label>Groq API key <span style="font-weight:400">(Groq Whisper & LLM)</span></label>
|
||||
<div class="s-key-row">
|
||||
<input type="password" id="s-groq-api-key" placeholder="gsk_…" autocomplete="off">
|
||||
<button type="button" class="s-eye-btn" data-target="s-groq-api-key">👁</button>
|
||||
</div>
|
||||
<span class="s-hint">Used for Groq Whisper STT (whisper-large-v3-turbo) and Groq LLM. Free: 2 000 req/day. Fastest cloud transcription.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -185,6 +210,11 @@
|
||||
<textarea id="s-tts-extra-nvidia-flow" spellcheck="false" placeholder="{}"></textarea>
|
||||
<span class="s-hint">Optional multipart fields. The app also sends the saved reference transcript.</span>
|
||||
</div>
|
||||
<div class="s-field">
|
||||
<label>Kokoro FastAPI params</label>
|
||||
<textarea id="s-tts-extra-kokoro" spellcheck="false" placeholder="{}"></textarea>
|
||||
<span class="s-hint">Extra JSON fields for the Kokoro backend. Usually empty — voice is selected from the voice dropdown.</span>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
@ -91,7 +91,7 @@
|
||||
<select id="stt-tts-stt-backend"><option value="configured">Configured Whisper/STT</option></select>
|
||||
<button class="btn-secondary" id="stt-tts-refresh-stt-btn" type="button">Refresh</button>
|
||||
</div>
|
||||
<span class="s-hint" id="stt-tts-stt-help">Uses Settings → Whisper/STT URL by default.</span>
|
||||
<div class="s-hint" id="stt-tts-stt-help">Uses Settings → Whisper/STT URL by default.</div>
|
||||
</div>
|
||||
<div class="s-field">
|
||||
<label>Speech audio</label>
|
||||
|
||||
@ -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; }
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user