feat: Audio-Streaming (chunked TTS) über WebSocket (#4 Ausbau)

- SentenceChunker (pipeline/sentence_chunker.py): inkrementelle Satzsegmentierung
- Orchestrator.chat_stream(on_audio): satzweise TTS, Audio-Chunk pro fertigem Satz;
  Gesamtaudio zusaetzlich an den Output-Endpunkt
- WS /ws/chat {"audio_stream":true}: audio-Events (json seq + binaerer Frame) live,
  kein finales Vollaudio; mit stream kombinierbar
- Tests: 45 gruen (+2: Sentence-Chunker, Audio-Streaming)
- Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-17 04:43:50 +02:00
commit b5913b0a44
7 changed files with 157 additions and 19 deletions

View file

@ -96,11 +96,25 @@ async def ws_chat(
voice = msg.get("voice") or settings.openrouter_tts_voice
stream = bool(msg.get("stream"))
try:
if stream:
async def on_token(delta):
await websocket.send_json({"type": "token", "text": delta})
audio_stream = bool(msg.get("audio_stream"))
on_token = None
if stream:
async def on_token(delta):
await websocket.send_json({"type": "token", "text": delta})
on_audio = None
if audio_stream:
audio_seq = 0
async def on_audio(chunk):
nonlocal audio_seq
await websocket.send_json({"type": "audio", "seq": audio_seq})
audio_seq += 1
await websocket.send_bytes(chunk)
try:
if stream or audio_stream:
trace, audio = await orchestrator.chat_stream(
text,
language=route.language,
@ -108,6 +122,7 @@ async def ws_chat(
output=output,
history=llm_context,
on_token=on_token,
on_audio=on_audio,
)
else:
trace, audio = await orchestrator.chat_text(
@ -132,7 +147,9 @@ async def ws_chat(
"spoken": trace.spoken_response,
}
)
await websocket.send_bytes(audio)
# Bei audio_stream wurden die Audio-Chunks bereits live gesendet.
if not audio_stream:
await websocket.send_bytes(audio)
await websocket.send_json(
{"type": "done", "audio_format": "pcm", "sample_rate": 24000}
)