feat: Audio-Eingang über WebSocket (/ws/voice) - Sprach-zu-Sprach (#4 Ausbau)
- /ws/voice: binaere Audio-Frames puffern, {"type":"end"} -> STT-Transkription
-> transcript-Event -> bestehende Antwort-Pipeline (ack/token/audio/semantic/done)
- ws.py refaktoriert: gemeinsame _resolve + _run_turn fuer /ws/chat und /ws/voice
- stream/audio_stream auch fuer Sprach-Turns nutzbar; mehrere Utterances pro Verbindung
- Tests: 47 gruen (+2: Transkript->Antwort, leerer Puffer)
- Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur)
Hinweis: STT pro Aeusserung (gepuffert); partielle Live-Transkripte (Streaming-STT
mit VAD) bleiben naechster Increment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b5913b0a44
commit
9340d3f998
5 changed files with 233 additions and 98 deletions
|
|
@ -66,3 +66,51 @@ def test_ws_requires_token_when_auth_enabled(monkeypatch):
|
|||
with pytest.raises(WebSocketDisconnect):
|
||||
with client.websocket_connect("/ws/chat"):
|
||||
pass
|
||||
|
||||
|
||||
def _install_voice_stubs(monkeypatch):
|
||||
class StubSTT:
|
||||
async def transcribe(self, audio_bytes, fmt, language=None):
|
||||
return f"erkannt({len(audio_bytes)})"
|
||||
|
||||
class StubLLM:
|
||||
async def complete(self, text, history=None, session_id=None):
|
||||
return f"Antwort zu {text}"
|
||||
|
||||
class StubTTS:
|
||||
async def synthesize(self, text, voice=None, audio_format="pcm"):
|
||||
return b"VOICEAUD"
|
||||
|
||||
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())
|
||||
return {
|
||||
"stt_provider": "ss",
|
||||
"llm_provider": "ll",
|
||||
"tts_provider": "tt",
|
||||
"output_endpoint": "loopback",
|
||||
}
|
||||
|
||||
|
||||
def test_ws_voice_transcribes_and_answers(monkeypatch):
|
||||
opts = _install_voice_stubs(monkeypatch)
|
||||
with client.websocket_connect("/ws/voice?session_id=v1") as ws:
|
||||
ws.send_bytes(b"PCMDATA") # 7 Bytes
|
||||
ws.send_bytes(b"MORE") # 4 Bytes -> insgesamt 11
|
||||
ws.send_json({"type": "end", **opts})
|
||||
|
||||
transcript = ws.receive_json()
|
||||
assert transcript["type"] == "transcript" and transcript["text"] == "erkannt(11)"
|
||||
assert ws.receive_json()["type"] == "ack"
|
||||
semantic = ws.receive_json()
|
||||
assert semantic["type"] == "semantic" and semantic["text"] == "Antwort zu erkannt(11)"
|
||||
assert ws.receive_bytes() == b"VOICEAUD"
|
||||
assert ws.receive_json()["type"] == "done"
|
||||
|
||||
|
||||
def test_ws_voice_empty_buffer_errors(monkeypatch):
|
||||
opts = _install_voice_stubs(monkeypatch)
|
||||
with client.websocket_connect("/ws/voice") as ws:
|
||||
ws.send_json({"type": "end", **opts}) # kein Audio gesendet
|
||||
err = ws.receive_json()
|
||||
assert err["type"] == "error" and "no audio" in err["detail"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue