fix(ws/voice): start-Frame-Optionen (Provider, audio_stream, language) ehren
Bug: /ws/voice nutzte bei explizitem {"type":"end"} die Optionen aus dem END-Frame.
voice_loop schickt Provider und audio_stream aber im START-Frame -> diese wurden
komplett ignoriert. Folge: --stt/--llm/--tts-provider und --stream-audio hatten
ueber /ws/voice KEINE Wirkung (Hybrid lief faktisch ueber die Cloud-Defaults,
audio_stream wurde nie aktiv).
Fix: start-Frame als start_options merken und beim Turn mit dem end-Frame mergen
({**start_options, **end}); VAD-Auto-Ende nutzt ebenfalls start_options.
Verifiziert: Provider-Override aus dem START-Frame greift (neuer Test); mit
audio_stream kommen jetzt mehrere Audio-Chunks ~6-7 s VOR dem fertigen Text
(satzweises Vorlesen startet frueher). 65 Tests gruen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ced63bac4c
commit
2d288be630
2 changed files with 21 additions and 4 deletions
|
|
@ -235,7 +235,7 @@ async def ws_voice(websocket: WebSocket, session_id: str | None = None, token: s
|
|||
fmt = "wav"
|
||||
active = None
|
||||
vad = None
|
||||
vad_options: dict = {}
|
||||
start_options: dict = {} # Konfig aus dem start-Frame (Provider, audio_stream, language)
|
||||
|
||||
async def _start_voice(audio: bytes, options: dict):
|
||||
nonlocal active
|
||||
|
|
@ -259,7 +259,7 @@ async def ws_voice(websocket: WebSocket, session_id: str | None = None, token: s
|
|||
audio = bytes(audio_buffer)
|
||||
audio_buffer.clear()
|
||||
vad.reset()
|
||||
await _start_voice(audio, vad_options)
|
||||
await _start_voice(audio, start_options)
|
||||
continue
|
||||
|
||||
raw = message.get("text")
|
||||
|
|
@ -278,6 +278,7 @@ async def ws_voice(websocket: WebSocket, session_id: str | None = None, token: s
|
|||
continue
|
||||
if ctype == "start":
|
||||
audio_buffer.clear()
|
||||
start_options = control # Provider/audio_stream/language fuer den Turn merken
|
||||
fmt = control.get("format", "wav")
|
||||
if control.get("vad"):
|
||||
vad = EnergyVAD(
|
||||
|
|
@ -285,7 +286,6 @@ async def ws_voice(websocket: WebSocket, session_id: str | None = None, token: s
|
|||
threshold=control.get("vad_threshold", 500.0),
|
||||
silence_ms=control.get("vad_silence_ms", 700.0),
|
||||
)
|
||||
vad_options = control
|
||||
else:
|
||||
vad = None
|
||||
continue
|
||||
|
|
@ -300,7 +300,8 @@ async def ws_voice(websocket: WebSocket, session_id: str | None = None, token: s
|
|||
audio_buffer.clear()
|
||||
if vad is not None:
|
||||
vad.reset()
|
||||
await _start_voice(audio, control)
|
||||
# start-Frame-Konfig + end-Frame zusammenfuehren (end kann ueberschreiben)
|
||||
await _start_voice(audio, {**start_options, **control})
|
||||
except WebSocketDisconnect:
|
||||
if active and not active.done():
|
||||
active.cancel()
|
||||
|
|
|
|||
|
|
@ -108,6 +108,22 @@ def test_ws_voice_transcribes_and_answers(monkeypatch):
|
|||
assert ws.receive_json()["type"] == "done"
|
||||
|
||||
|
||||
def test_ws_voice_start_frame_options_are_honored(monkeypatch):
|
||||
# Provider stehen im START-Frame, der end-Frame ist leer -> muessen trotzdem gelten.
|
||||
opts = _install_voice_stubs(monkeypatch)
|
||||
with client.websocket_connect("/ws/voice") as ws:
|
||||
ws.send_json({"type": "start", "format": "wav", **opts})
|
||||
ws.send_bytes(b"PCMDATA") # 7 Bytes
|
||||
ws.send_json({"type": "end"})
|
||||
transcript = ws.receive_json()
|
||||
# Stub-STT (aus dem START-Frame) liefert "erkannt(<bytes>)" -> beweist: Override greift
|
||||
assert transcript["type"] == "transcript" and transcript["text"] == "erkannt(7)"
|
||||
assert ws.receive_json()["type"] == "ack"
|
||||
assert ws.receive_json()["type"] == "semantic"
|
||||
ws.receive_bytes()
|
||||
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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue