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"
|
fmt = "wav"
|
||||||
active = None
|
active = None
|
||||||
vad = 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):
|
async def _start_voice(audio: bytes, options: dict):
|
||||||
nonlocal active
|
nonlocal active
|
||||||
|
|
@ -259,7 +259,7 @@ async def ws_voice(websocket: WebSocket, session_id: str | None = None, token: s
|
||||||
audio = bytes(audio_buffer)
|
audio = bytes(audio_buffer)
|
||||||
audio_buffer.clear()
|
audio_buffer.clear()
|
||||||
vad.reset()
|
vad.reset()
|
||||||
await _start_voice(audio, vad_options)
|
await _start_voice(audio, start_options)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raw = message.get("text")
|
raw = message.get("text")
|
||||||
|
|
@ -278,6 +278,7 @@ async def ws_voice(websocket: WebSocket, session_id: str | None = None, token: s
|
||||||
continue
|
continue
|
||||||
if ctype == "start":
|
if ctype == "start":
|
||||||
audio_buffer.clear()
|
audio_buffer.clear()
|
||||||
|
start_options = control # Provider/audio_stream/language fuer den Turn merken
|
||||||
fmt = control.get("format", "wav")
|
fmt = control.get("format", "wav")
|
||||||
if control.get("vad"):
|
if control.get("vad"):
|
||||||
vad = EnergyVAD(
|
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),
|
threshold=control.get("vad_threshold", 500.0),
|
||||||
silence_ms=control.get("vad_silence_ms", 700.0),
|
silence_ms=control.get("vad_silence_ms", 700.0),
|
||||||
)
|
)
|
||||||
vad_options = control
|
|
||||||
else:
|
else:
|
||||||
vad = None
|
vad = None
|
||||||
continue
|
continue
|
||||||
|
|
@ -300,7 +300,8 @@ async def ws_voice(websocket: WebSocket, session_id: str | None = None, token: s
|
||||||
audio_buffer.clear()
|
audio_buffer.clear()
|
||||||
if vad is not None:
|
if vad is not None:
|
||||||
vad.reset()
|
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:
|
except WebSocketDisconnect:
|
||||||
if active and not active.done():
|
if active and not active.done():
|
||||||
active.cancel()
|
active.cancel()
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,22 @@ def test_ws_voice_transcribes_and_answers(monkeypatch):
|
||||||
assert ws.receive_json()["type"] == "done"
|
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):
|
def test_ws_voice_empty_buffer_errors(monkeypatch):
|
||||||
opts = _install_voice_stubs(monkeypatch)
|
opts = _install_voice_stubs(monkeypatch)
|
||||||
with client.websocket_connect("/ws/voice") as ws:
|
with client.websocket_connect("/ws/voice") as ws:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue