diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 83475e3..f8e1c6b 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -103,6 +103,13 @@ async def get_config(): **Zweck:** Das Frontend kann damit nicht verfügbare TTS-Optionen automatisch ausgrauen, ohne dass der Nutzer erst auf einen Fehler stößt. +Das Feld `available.cartesia_unsupported_languages` (z. B. `["ar","it","ru","pl",…]`, +abgeleitet aus `_UNSUPPORTED_LANGS` in `app/providers/tts/cartesia.py`) speist im UI +einen Hinweis: Wählt der Nutzer Cartesia **und** eine feste Sprache, die Cartesia +nicht spricht, erscheint „Cartesia spricht nicht — es wird automatisch die +lokale Stimme (Piper) verwendet." Das macht den `tts_fallback = "piper"`-Mechanismus +sichtbar, statt lautlos eine andere Stimme zu liefern. + --- ### 1.2 `app/web/app.js` — Chatterbox/OpenRouter Verfügbarkeits-Refresh diff --git a/app/api/config.py b/app/api/config.py index c939955..1d3b430 100644 --- a/app/api/config.py +++ b/app/api/config.py @@ -2,6 +2,7 @@ import httpx from fastapi import APIRouter from app.config import settings, active_profile +from app.providers.tts.cartesia import _UNSUPPORTED_LANGS as _CARTESIA_UNSUPPORTED from app.dependencies import ( resolve_route, get_audio_router, @@ -80,6 +81,7 @@ async def get_config(): "chatterbox_reachable": chatterbox_ok, "openrouter_tts_available": openrouter_tts_ok, "cartesia_available": cartesia_ok, + "cartesia_unsupported_languages": sorted(_CARTESIA_UNSUPPORTED), }, "secrets": { "openrouter_api_key_set": bool(settings.openrouter_api_key.strip()), diff --git a/app/web/app.js b/app/web/app.js index 234caa9..7ed07cf 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -9,6 +9,9 @@ const formEl = $("#prompt-form"); const micBtn = $("#mic"); const ttsSel = $("#tts"); const langSel = $("#lang-sel"); +const cartesiaHint = $("#cartesia-lang-hint"); +let cartesiaUnsupportedLangs = []; // von /api/config: Sprachen, die Cartesia nicht spricht +const CARTESIA_LANG_LABELS = { ar: "Arabisch", it: "Italienisch", ru: "Russisch", pl: "Polnisch", nl: "Niederländisch", sv: "Schwedisch", tr: "Türkisch" }; // "Flex" ist im Sprachmenü nur ein weiterer Wert: keine feste Sprache, sondern // automatische Erkennung. Eine konkrete Sprache = Fix-Modus (Eingabe wird übersetzt). @@ -231,6 +234,7 @@ async function refreshCartesiaPreset() { try { const cfg = await fetch("/api/config", { cache: "no-store" }).then(r => r.json()); const available = cfg.available?.cartesia_available === true; + cartesiaUnsupportedLangs = cfg.available?.cartesia_unsupported_languages || []; btn.disabled = !available; btn.classList.toggle("opacity-50", !available); btn.classList.toggle("cursor-not-allowed", !available); @@ -242,9 +246,28 @@ async function refreshCartesiaPreset() { ttsSel.value = "piper"; syncPresetUI(); } + updateCartesiaLangHint(); } catch (e) { /* ignorieren */ } } +// Zeigt einen Hinweis, wenn Cartesia gewählt ist, die feste Sprache aber nicht +// unterstützt wird — dann übernimmt automatisch die lokale Piper-Stimme (tts_fallback). +function updateCartesiaLangHint() { + if (!cartesiaHint) return; + const lang = langSel && langSel.value; + const show = ttsSel && ttsSel.value === "cartesia" + && lang && lang !== "flex" + && cartesiaUnsupportedLangs.includes(lang); + if (show) { + const label = CARTESIA_LANG_LABELS[lang] || lang.toUpperCase(); + cartesiaHint.textContent = + `Hinweis: Cartesia spricht ${label} nicht — es wird automatisch die lokale Stimme (Piper) verwendet.`; + cartesiaHint.classList.remove("hidden"); + } else { + cartesiaHint.classList.add("hidden"); + } +} + // Graut "Cloud" (OpenRouter TTS) aus, wenn das TTS-Modell nicht verfügbar ist. async function refreshOpenrouterTtsPreset() { const btn = document.querySelector('.tts-preset[data-tts="openrouter"]'); @@ -759,7 +782,7 @@ window.addEventListener("resize", scrollToBottom); promptEl.addEventListener("focus", () => setTimeout(scrollToBottom, 300)); // Dropdown-Änderungen dauerhaft als Nutzer-Präferenz speichern. -if (langSel) langSel.addEventListener("change", () => savePrefs(langOverrides())); +if (langSel) langSel.addEventListener("change", () => { savePrefs(langOverrides()); updateCartesiaLangHint(); }); if (ttsSel) { ttsSel.addEventListener("change", () => { unlockTTS(); // Auswahl ist eine Nutzergeste -> Geräte-TTS gleich freischalten @@ -770,6 +793,7 @@ if (ttsSel) { localStorage.removeItem(PLAYBACK_KEY); savePrefs({ tts_provider: ttsSel.value || null }); // echter Provider -> Server-Pref } + updateCartesiaLangHint(); }); } diff --git a/app/web/index.html b/app/web/index.html index ea91fc4..ecf9519 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -298,6 +298,7 @@ + diff --git a/tests/test_endpoints_e2e.py b/tests/test_endpoints_e2e.py index 8593723..d45e174 100644 --- a/tests/test_endpoints_e2e.py +++ b/tests/test_endpoints_e2e.py @@ -110,3 +110,12 @@ def test_config_endpoint_exposes_no_secrets(): # Keine echten Secrets im Body. assert "sk-or-" not in json.dumps(body) assert set(body["secrets"].keys()) == {"openrouter_api_key_set"} + + +def test_config_endpoint_lists_cartesia_unsupported_languages(): + # Das Frontend nutzt diese Liste, um bei Cartesia + nicht unterstützter + # Sprache auf den lokalen Piper-Fallback hinzuweisen. + body = client.get("/api/config").json() + langs = body["available"]["cartesia_unsupported_languages"] + for lang in ("ar", "it", "ru", "pl"): + assert lang in langs