feat(P3): Diktat in Fremdsprache → Übersetzung in die Zielsprache
Spricht der Nutzer in einer anderen als der eingestellten Sprache, erkennt das System die gesprochene Sprache automatisch (transcribe_detect) und übersetzt die Anfrage per LLM in die Zielsprache — sie wird dann auch in der Zielsprache angezeigt und verarbeitet. Hilft beim Sprachenlernen und in der Kommunikation über Sprachgrenzen. - Orchestrator.translate() (LLM-basiert, nur Übersetzung). - ws.py Voice-STT: transcribe_detect + Übersetzung bei abweichender Sprache. - Test-Stubs um transcribe_detect ergänzt; neuer Übersetzungstest. 209 passed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
40291d318b
commit
a7f718192f
4 changed files with 70 additions and 2 deletions
|
|
@ -203,8 +203,15 @@ async def _voice_turn(websocket, store, user, session_id, audio, fmt, options):
|
|||
return
|
||||
|
||||
try:
|
||||
# Feste Sprache an Whisper → Whisper übersetzt automatisch.
|
||||
transcript = await orchestrator.stt.transcribe(audio, fmt=fmt, language=route.language)
|
||||
# Gesprochene Sprache automatisch erkennen (egal welche). Weicht sie von der
|
||||
# Zielsprache ab, wird die Anfrage in die Zielsprache übersetzt — so erscheint
|
||||
# und verarbeitet das System sie immer in der eingestellten Sprache.
|
||||
transcript, detected = await orchestrator.stt.transcribe_detect(
|
||||
audio, fmt=fmt, language=None
|
||||
)
|
||||
if (transcript and transcript.strip() and detected
|
||||
and detected.lower() != (route.language or "").lower()):
|
||||
transcript = await orchestrator.translate(transcript, 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)})
|
||||
|
|
|
|||
|
|
@ -12,6 +12,14 @@ TTS_SAMPLE_RATE = 24000
|
|||
TTS_CHANNELS = 1
|
||||
|
||||
|
||||
# Lesbare Sprachnamen für den Übersetzungs-Prompt (Code -> Name).
|
||||
_LANG_NAMES = {
|
||||
"de": "Deutsch", "en": "English", "fr": "français", "es": "español",
|
||||
"it": "italiano", "pt": "português", "pl": "polski", "ar": "العربية",
|
||||
"ru": "русский", "zh": "中文",
|
||||
}
|
||||
|
||||
|
||||
class Orchestrator:
|
||||
def __init__(self, stt, llm, tts, input_cleaner, spoken_adapter, tts_normalizer,
|
||||
normalize_level: str = "full"):
|
||||
|
|
@ -131,6 +139,23 @@ class Orchestrator:
|
|||
await self._emit_to_output(audio, output)
|
||||
return trace, audio
|
||||
|
||||
async def translate(self, text: str, target_language: str) -> str:
|
||||
"""Übersetzt Text per LLM in die Zielsprache. Liefert nur die Übersetzung.
|
||||
|
||||
Wird genutzt, wenn der Nutzer in einer anderen als der eingestellten
|
||||
Sprache diktiert: die Anfrage wird in die Zielsprache übersetzt.
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return text
|
||||
target = _LANG_NAMES.get((target_language or "").lower(), target_language)
|
||||
prompt = (
|
||||
f"Übersetze den folgenden Text nach {target}. Gib AUSSCHLIESSLICH die "
|
||||
f"Übersetzung zurück — ohne Anführungszeichen, ohne Erklärung.\n\n{text}"
|
||||
)
|
||||
with _stage("translate"):
|
||||
result = await self.llm.complete(prompt)
|
||||
return (result or "").strip() or text
|
||||
|
||||
async def chat_stream(
|
||||
self,
|
||||
text: str,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,9 @@ def _install_voice_stubs(monkeypatch):
|
|||
async def transcribe(self, audio_bytes, fmt, language=None):
|
||||
return "ok"
|
||||
|
||||
async def transcribe_detect(self, audio_bytes, fmt, language=None):
|
||||
return "ok", None
|
||||
|
||||
class LLM:
|
||||
async def complete(self, text, history=None, session_id=None, language=None):
|
||||
return "antwort"
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@ def _install_voice_stubs(monkeypatch):
|
|||
async def transcribe(self, audio_bytes, fmt, language=None):
|
||||
return f"erkannt({len(audio_bytes)})"
|
||||
|
||||
async def transcribe_detect(self, audio_bytes, fmt, language=None):
|
||||
return f"erkannt({len(audio_bytes)})", None
|
||||
|
||||
class StubLLM:
|
||||
async def complete(self, text, history=None, session_id=None, language=None):
|
||||
return f"Antwort zu {text}"
|
||||
|
|
@ -155,3 +158,33 @@ def test_ws_voice_empty_buffer_errors(monkeypatch):
|
|||
ws.send_json({"type": "end", **opts}) # kein Audio gesendet
|
||||
err = ws.receive_json()
|
||||
assert err["type"] == "error" and "no audio" in err["detail"]
|
||||
|
||||
|
||||
def test_ws_voice_translates_foreign_language_to_target(monkeypatch):
|
||||
# Diktat in einer anderen Sprache (detected=fr) bei Zielsprache de ->
|
||||
# die Anfrage wird übersetzt und in der Zielsprache angezeigt.
|
||||
class StubSTT:
|
||||
async def transcribe_detect(self, audio_bytes, fmt, language=None):
|
||||
return "bonjour, comment ça va", "fr"
|
||||
|
||||
class StubLLM:
|
||||
async def complete(self, text, history=None, session_id=None, language=None):
|
||||
# Übersetzungs- UND Antwort-Aufruf liefern denselben Stub-Text.
|
||||
return "GUTEN TAG"
|
||||
|
||||
class StubTTS:
|
||||
async def synthesize(self, text, voice=None, audio_format="pcm", language=None):
|
||||
return b"A"
|
||||
|
||||
monkeypatch.setitem(deps.STT_REGISTRY, "ss", lambda s: StubSTT())
|
||||
monkeypatch.setitem(deps.LLM_REGISTRY, "ll", lambda s: StubLLM())
|
||||
monkeypatch.setitem(deps.TTS_REGISTRY, "tt", lambda s: StubTTS())
|
||||
opts = {"stt_provider": "ss", "llm_provider": "ll", "tts_provider": "tt",
|
||||
"output_endpoint": "loopback", "language": "de"}
|
||||
with client.websocket_connect("/ws/voice") as ws:
|
||||
ws.send_json({"type": "start", "format": "wav", **opts})
|
||||
ws.send_bytes(b"PCMDATA")
|
||||
ws.send_json({"type": "end"})
|
||||
transcript = ws.receive_json()
|
||||
# Übersetzte (deutsche) Fassung wird angezeigt, nicht das französische Original.
|
||||
assert transcript["type"] == "transcript" and transcript["text"] == "GUTEN TAG"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue