feat(1a): Flex-Sprachmodus vollständig entfernen
Sprache ist immer fest. Entfernt: language_mode aus Route/Schema/Config/
Runtime-Config, die Flex-Verzweigung in Orchestrator (effective_language =
language) und ws.py (immer normales transcribe), die "🔄 Flex"-Option im
Frontend samt zugehöriger app.js-Logik (langOverrides, loadMe, Turn-Sprache).
Die generische STT-Methode transcribe_detect bleibt als wiederverwendbare
Infrastruktur erhalten (nicht mehr aufgerufen). Test: Route trägt kein
language_mode mehr. 215 passed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
67a959c4b3
commit
be5206c7ed
10 changed files with 24 additions and 64 deletions
|
|
@ -50,7 +50,6 @@ async def chat(
|
|||
"input_endpoint": payload.input_endpoint,
|
||||
"output_endpoint": payload.output_endpoint,
|
||||
"language": payload.language,
|
||||
"language_mode": payload.language_mode,
|
||||
"stt_provider": payload.stt_provider,
|
||||
"llm_provider": payload.llm_provider,
|
||||
"tts_provider": payload.tts_provider,
|
||||
|
|
@ -106,7 +105,6 @@ async def chat(
|
|||
voice=voice,
|
||||
output=output,
|
||||
history=llm_context,
|
||||
language_mode=route.language_mode,
|
||||
text_only=bool(payload.text_only),
|
||||
)
|
||||
except Exception as exc:
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ _OVERRIDE_KEYS = (
|
|||
"input_endpoint",
|
||||
"output_endpoint",
|
||||
"language",
|
||||
"language_mode",
|
||||
"stt_provider",
|
||||
"llm_provider",
|
||||
"tts_provider",
|
||||
|
|
@ -67,7 +66,7 @@ async def _resolve(user, session_id, options):
|
|||
|
||||
async def _run_turn(
|
||||
websocket, store, user, session_id, route, orchestrator, output, text, options,
|
||||
detected_language: str | None = None, effective_voice: str | None = None,
|
||||
effective_voice: str | None = None,
|
||||
):
|
||||
"""Faehrt einen Antwort-Turn und streamt die Events an den Client."""
|
||||
conversation = (
|
||||
|
|
@ -158,8 +157,6 @@ async def _run_turn(
|
|||
history=llm_context,
|
||||
on_token=on_token,
|
||||
on_audio=on_audio,
|
||||
language_mode=route.language_mode,
|
||||
detected_language=detected_language,
|
||||
text_only=text_only,
|
||||
)
|
||||
else:
|
||||
|
|
@ -169,8 +166,6 @@ async def _run_turn(
|
|||
voice=voice,
|
||||
output=output,
|
||||
history=llm_context,
|
||||
language_mode=route.language_mode,
|
||||
detected_language=detected_language,
|
||||
text_only=text_only,
|
||||
)
|
||||
except Exception as exc:
|
||||
|
|
@ -228,21 +223,9 @@ async def _voice_turn(websocket, store, user, session_id, audio, fmt, options):
|
|||
return
|
||||
|
||||
try:
|
||||
if route.language_mode == "flex":
|
||||
# Flex: Sprache auto-erkennen, kein Übersetzen durch Whisper.
|
||||
transcript, detected_language = await orchestrator.stt.transcribe_detect(
|
||||
audio, fmt=fmt, language=None
|
||||
)
|
||||
# Stimme folgt der erkannten Sprache (Fallback: Routensprache).
|
||||
effective_voice = voice_for_route(
|
||||
route.tts_provider, detected_language or route.language, route.voice_gender
|
||||
)
|
||||
else:
|
||||
# Fix: konfigurierte Sprache an Whisper → Whisper übersetzt automatisch.
|
||||
transcript = await orchestrator.stt.transcribe(audio, fmt=fmt, language=route.language)
|
||||
detected_language = None
|
||||
# Stimme folgt der konfigurierten Sprache.
|
||||
effective_voice = voice_for_route(route.tts_provider, route.language, route.voice_gender)
|
||||
# Feste Sprache an Whisper → Whisper übersetzt automatisch.
|
||||
transcript = await orchestrator.stt.transcribe(audio, fmt=fmt, language=route.language)
|
||||
effective_voice = voice_for_route(route.tts_provider, route.language, route.voice_gender)
|
||||
except Exception as exc:
|
||||
await websocket.send_json({"type": "error", "status": 502, "detail": str(exc)})
|
||||
return
|
||||
|
|
@ -250,7 +233,6 @@ async def _voice_turn(websocket, store, user, session_id, audio, fmt, options):
|
|||
await websocket.send_json({
|
||||
"type": "transcript",
|
||||
"text": transcript,
|
||||
"detected_language": detected_language,
|
||||
})
|
||||
if not transcript or not transcript.strip():
|
||||
await websocket.send_json({
|
||||
|
|
@ -260,7 +242,7 @@ async def _voice_turn(websocket, store, user, session_id, audio, fmt, options):
|
|||
return
|
||||
await _run_turn(
|
||||
websocket, store, user, session_id, route, orchestrator, output, transcript, options,
|
||||
detected_language=detected_language, effective_voice=effective_voice,
|
||||
effective_voice=effective_voice,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ class Settings(BaseSettings):
|
|||
openrouter_tts_voice: str = "alloy"
|
||||
openrouter_llm_model: str = "openai/gpt-4.1-mini"
|
||||
default_language: str = "de"
|
||||
default_language_mode: str = "fix"
|
||||
default_input_endpoint: str = "local-default"
|
||||
default_output_endpoint: str = "local-default"
|
||||
default_stt_provider: str = "openrouter"
|
||||
|
|
|
|||
|
|
@ -89,8 +89,6 @@ class Orchestrator:
|
|||
voice: str | None = None,
|
||||
output=None,
|
||||
history: list[dict] | None = None,
|
||||
language_mode: str = "fix",
|
||||
detected_language: str | None = None,
|
||||
text_only: bool = False,
|
||||
):
|
||||
trace = PipelineTrace()
|
||||
|
|
@ -98,10 +96,8 @@ class Orchestrator:
|
|||
trace.raw_transcript = text
|
||||
trace.cleaned_transcript = await self.input_cleaner.run(text or "")
|
||||
|
||||
# Flex: die erkannte Sprache bestimmt die Ausgabe. Gibt es keine (z. B. Text-Chat
|
||||
# ohne Audio), bleibt sie None -> das LLM antwortet von selbst in der Eingabesprache.
|
||||
# Fix: immer die konfigurierte Sprache (Whisper hat die Eingabe übersetzt).
|
||||
effective_language = detected_language if language_mode == "flex" else language
|
||||
# Immer die konfigurierte (feste) Sprache (Whisper hat die Eingabe übersetzt).
|
||||
effective_language = language
|
||||
|
||||
with _stage("llm"):
|
||||
trace.semantic_response = await self.llm.complete(
|
||||
|
|
@ -144,8 +140,6 @@ class Orchestrator:
|
|||
history: list[dict] | None = None,
|
||||
on_token=None,
|
||||
on_audio=None,
|
||||
language_mode: str = "fix",
|
||||
detected_language: str | None = None,
|
||||
text_only: bool = False,
|
||||
):
|
||||
"""Wie chat_text, aber gestreamt.
|
||||
|
|
@ -158,9 +152,8 @@ class Orchestrator:
|
|||
trace.raw_transcript = text
|
||||
trace.cleaned_transcript = await self.input_cleaner.run(text or "")
|
||||
|
||||
# Siehe chat_text: Flex folgt der erkannten Sprache (None -> LLM spiegelt Eingabe),
|
||||
# Fix nutzt die konfigurierte Sprache.
|
||||
effective_language = detected_language if language_mode == "flex" else language
|
||||
# Immer die konfigurierte (feste) Sprache.
|
||||
effective_language = language
|
||||
|
||||
# text_only: kein Server-Audio (Geräte-TTS spricht selbst) -> kein Chunked-TTS.
|
||||
chunker = SentenceChunker() if (on_audio and not text_only) else None
|
||||
|
|
|
|||
|
|
@ -155,7 +155,6 @@ ROUTE_KEYS = (
|
|||
"llm_provider",
|
||||
"tts_provider",
|
||||
"language",
|
||||
"language_mode",
|
||||
"voice_gender",
|
||||
)
|
||||
|
||||
|
|
@ -235,7 +234,6 @@ class ResolvedRoute:
|
|||
llm_provider: str
|
||||
tts_provider: str
|
||||
language: str
|
||||
language_mode: str = "fix"
|
||||
voice_gender: str = "any"
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
|
|
@ -246,7 +244,6 @@ class ResolvedRoute:
|
|||
"llm_provider": self.llm_provider,
|
||||
"tts_provider": self.tts_provider,
|
||||
"language": self.language,
|
||||
"language_mode": self.language_mode,
|
||||
"voice_gender": self.voice_gender,
|
||||
}
|
||||
|
||||
|
|
@ -288,7 +285,6 @@ def resolve_route(
|
|||
"llm_provider": cfg.default_llm_provider,
|
||||
"tts_provider": cfg.default_tts_provider,
|
||||
"language": cfg.default_language,
|
||||
"language_mode": cfg.default_language_mode,
|
||||
}
|
||||
|
||||
user_prefs = user.prefs if user is not None else {}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ RUNTIME_SETTABLE: dict[str, tuple[str, str, str]] = {
|
|||
"default_llm_provider": ("LLM-Provider (Standard)", "str", "openrouter | local-openai-compatible"),
|
||||
"default_tts_provider": ("TTS-Provider (Standard)", "str", "openrouter | piper | chatterbox — Standard, pro Nutzer überschreibbar"),
|
||||
"default_language": ("Sprache (Standard)", "str", "de | en | … — Standard, pro Nutzer überschreibbar"),
|
||||
"default_language_mode": ("Sprachmodus (Standard)", "str", "fix | flex — Standard, pro Nutzer überschreibbar"),
|
||||
"openrouter_llm_model": ("LLM-Modell (OpenRouter)", "str", "z.B. google/gemini-3.1-flash-lite"),
|
||||
"openrouter_tts_model": ("TTS-Modell (OpenRouter)", "str", "z.B. google/gemini-3.1-flash-tts-preview"),
|
||||
"openrouter_tts_voice": ("TTS-Stimme (OpenRouter)", "str", "z.B. Zephyr, Puck, Kore"),
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ class ChatRequest(BaseModel):
|
|||
input_endpoint: str | None = None
|
||||
output_endpoint: str | None = None
|
||||
language: str | None = None
|
||||
language_mode: Literal["fix", "flex"] | None = None
|
||||
voice: str | None = None
|
||||
stt_provider: str | None = None
|
||||
llm_provider: str | None = None
|
||||
|
|
@ -93,7 +92,6 @@ class UserPrefs(BaseModel):
|
|||
llm_provider: str | None = None
|
||||
tts_provider: str | None = None
|
||||
language: str | None = None
|
||||
language_mode: Literal["fix", "flex"] | None = None
|
||||
|
||||
|
||||
class MemoryCreate(BaseModel):
|
||||
|
|
|
|||
|
|
@ -13,13 +13,9 @@ 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).
|
||||
// Beide internen Felder (language, language_mode) werden aus dieser einen Auswahl abgeleitet.
|
||||
// Die gewählte Sprache ist immer fest (Flex-Modus wurde entfernt).
|
||||
function langOverrides() {
|
||||
const sel = langSel && langSel.value;
|
||||
if (!sel || sel === "flex") return { language_mode: "flex" };
|
||||
return { language: sel, language_mode: "fix" };
|
||||
return { language: (langSel && langSel.value) || "de" };
|
||||
}
|
||||
|
||||
// Nutzer-Präferenzen als Request-Overrides für jeden Turn.
|
||||
|
|
@ -348,10 +344,7 @@ function loadMe() {
|
|||
// Gespeicherte Nutzer-Präferenzen in die Dropdowns laden.
|
||||
const prefs = me.prefs || {};
|
||||
if (langSel) {
|
||||
// Flex-Modus -> "flex", sonst die feste Sprache (Fallback: Flex).
|
||||
langSel.value = prefs.language_mode === "flex"
|
||||
? "flex"
|
||||
: (prefs.language || "de");
|
||||
langSel.value = prefs.language || "de";
|
||||
}
|
||||
applyPlaybackChoice(prefs.tts_provider);
|
||||
applyGenderChoice(prefs.voice_gender || 'f');
|
||||
|
|
@ -544,10 +537,9 @@ function runTurn(path, onopen) {
|
|||
const pcm = [];
|
||||
let answerEl = null;
|
||||
let sampleRate = 24000;
|
||||
// Sprache dieses Turns (für den Replay je Bubble): Flex folgt erkannter Sprache.
|
||||
let routeLang = null, routeMode = null, detectedLang = null;
|
||||
const turnLang = () =>
|
||||
(routeMode === "flex" ? (detectedLang || routeLang) : routeLang) || null;
|
||||
// Sprache dieses Turns (für den Replay je Bubble) = feste Routensprache.
|
||||
let routeLang = null;
|
||||
const turnLang = () => routeLang || null;
|
||||
|
||||
ws.onopen = () => onopen(ws);
|
||||
ws.onerror = () => { statusEl.textContent = "Verbindungsfehler"; resolve(); };
|
||||
|
|
@ -560,10 +552,8 @@ function runTurn(path, onopen) {
|
|||
switch (msg.type) {
|
||||
case "ack":
|
||||
routeLang = (msg.route && msg.route.language) || null;
|
||||
routeMode = (msg.route && msg.route.language_mode) || null;
|
||||
break;
|
||||
case "transcript":
|
||||
detectedLang = msg.detected_language || null;
|
||||
if (msg.text) addMessage("user", msg.text, { lang: turnLang() });
|
||||
break;
|
||||
case "token":
|
||||
|
|
@ -620,9 +610,8 @@ async function sendText(text) {
|
|||
busy = true;
|
||||
setMicBusy();
|
||||
await ensureSession();
|
||||
// Bei fester Sprache wird der eigene Text in dieser Sprache vorgelesen; bei Flex offen.
|
||||
const lo = langOverrides();
|
||||
addMessage("user", text, { lang: lo.language_mode === "fix" ? lo.language : null });
|
||||
addMessage("user", text, { lang: lo.language || null });
|
||||
statusEl.textContent = "denkt …";
|
||||
const audioStream = !isDeviceMode(); // Geräte-Modus: kein Server-Audio anfordern
|
||||
await runTurn("/ws/chat", (ws) => {
|
||||
|
|
|
|||
|
|
@ -219,9 +219,8 @@
|
|||
<header class="shrink-0 h-14 flex items-center gap-2 px-3 border-b border-slate-200 dark:border-slate-700 bg-white/80 dark:bg-slate-800/80 backdrop-blur">
|
||||
<img src="/favicon.svg?v=1" alt="" class="w-7 h-7 shrink-0" />
|
||||
<h1 class="flex-1 truncate font-semibold text-base">Voice Assistant</h1>
|
||||
<select id="lang-sel" title="Sprache — feste Sprache oder 🔄 Flex (folgt der gesprochenen Sprache)"
|
||||
<select id="lang-sel" title="Sprache"
|
||||
class="text-sm rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-2 py-1.5">
|
||||
<option value="flex">🔄 Flex</option>
|
||||
<option value="de">🇩🇪 DE</option>
|
||||
<option value="en">🇬🇧 EN</option>
|
||||
<option value="fr">🇫🇷 FR</option>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue