feat: Audio-Streaming als Default (Admin abschaltbar via AUDIO_STREAM_DEFAULT)
- Config audio_stream_default=True; _run_turn nutzt es, wenn die Anfrage audio_stream nicht explizit setzt (explizite Anfrage gewinnt) - voice_loop: immer durchgehender Player (spielt 1 oder N Haeppchen luekenlos); --stream-audio / --no-stream-audio als Tri-State (sonst entscheidet der Server-Default) - conftest: audio_stream_default=False fuer deterministische Tests; neuer Test fuer Default-an-Streaming ueber /ws/voice; 67 Tests gruen - Doku + .env.example aktualisiert (AUDIO_STREAM_DEFAULT, --no-stream-audio) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5296459b07
commit
7c1f00662c
7 changed files with 52 additions and 28 deletions
|
|
@ -190,8 +190,9 @@ def _close_player(player) -> None:
|
|||
|
||||
def _start_frame(args) -> dict:
|
||||
frame = {"type": "start", "format": "wav"}
|
||||
if getattr(args, "stream_audio", False):
|
||||
frame["audio_stream"] = True
|
||||
# audio_stream ist Tri-State: True/False explizit, None -> Server-Default entscheidet.
|
||||
if getattr(args, "stream_audio", None) is not None:
|
||||
frame["audio_stream"] = args.stream_audio
|
||||
if getattr(args, "stream_text", False):
|
||||
frame["stream"] = True
|
||||
for key in ("stt_provider", "llm_provider", "tts_provider", "language"):
|
||||
|
|
@ -252,22 +253,21 @@ async def one_turn(ws, wav: bytes, start_frame: dict, player=None) -> bytes:
|
|||
return audio
|
||||
|
||||
|
||||
async def _send_and_play(url: str, wav: bytes, start_frame: dict, stream_audio: bool = False) -> None:
|
||||
async def _send_and_play(url: str, wav: bytes, start_frame: dict) -> None:
|
||||
"""Eine kurze Verbindung pro Runde (Gedaechtnis bleibt serverseitig via session_id).
|
||||
|
||||
Ohne Stream: komplettes Audio sammeln, NACH dem Schliessen abspielen.
|
||||
Mit Stream-Audio: durchgehenden Player oeffnen, Haeppchen sofort einspeisen; der
|
||||
Player wird nach dem Schliessen der Verbindung geleert (kein Keepalive-Timeout)."""
|
||||
player = open_stream_player() if stream_audio else None
|
||||
if stream_audio and player is None:
|
||||
print(" (kein Player fuer Stream-Audio gefunden – spiele am Ende komplett ab)")
|
||||
Es wird immer ein durchgehender Player benutzt: der spielt sowohl viele Haeppchen
|
||||
(Stream-Audio) als auch ein einzelnes Komplett-Audio luekenlos ab. Wird kein Player
|
||||
gefunden, wird das Audio gesammelt und als WAV abgespielt. Die Wiedergabe blockiert
|
||||
nie die offene Verbindung (Schreiben im Thread; Player wird nach Schliessen geleert)."""
|
||||
player = open_stream_player()
|
||||
try:
|
||||
async with websockets.connect(url, max_size=None) as ws:
|
||||
audio = await one_turn(ws, wav, start_frame, player)
|
||||
finally:
|
||||
if player is not None:
|
||||
await asyncio.to_thread(_close_player, player)
|
||||
if audio:
|
||||
if audio: # nur wenn kein Streaming-Player verfuegbar war
|
||||
play_pcm(audio)
|
||||
|
||||
|
||||
|
|
@ -281,12 +281,11 @@ async def run(args) -> None:
|
|||
with open(args.file, "rb") as fh:
|
||||
wav = fh.read()
|
||||
print(f"Sende Datei: {args.file}")
|
||||
await _send_and_play(url, wav, start_frame, args.stream_audio)
|
||||
await _send_and_play(url, wav, start_frame)
|
||||
return
|
||||
|
||||
recorder = resolve_recorder(args.recorder)
|
||||
print(f"Ziel: {args.url} (Session '{args.session}')"
|
||||
+ (" [Stream-Audio: satzweises Vorlesen]" if args.stream_audio else ""))
|
||||
print(f"Ziel: {args.url} (Session '{args.session}')")
|
||||
print(f"Aufnahme mit: {recorder}"
|
||||
+ (f" (Gerät: {args.device})" if args.device else " (Standardgerät)"))
|
||||
print("Sprich nach 'START', stoppe mit Enter. Strg+C beendet den Loop.")
|
||||
|
|
@ -304,7 +303,7 @@ async def run(args) -> None:
|
|||
print(" python scripts/voice_loop.py --recorder arecord --device plughw:2,0")
|
||||
continue
|
||||
try:
|
||||
await _send_and_play(url, wav, start_frame, args.stream_audio)
|
||||
await _send_and_play(url, wav, start_frame)
|
||||
except Exception as exc: # noqa: BLE001 - Verbindung pro Runde; Fehler nicht fatal
|
||||
print(f" Verbindungsfehler: {exc}")
|
||||
|
||||
|
|
@ -324,9 +323,12 @@ def main() -> None:
|
|||
p.add_argument("--llm-provider", dest="llm_provider", default=None)
|
||||
p.add_argument("--tts-provider", dest="tts_provider", default=None)
|
||||
p.add_argument("--language", default=None)
|
||||
p.add_argument("--stream-audio", dest="stream_audio", action="store_true",
|
||||
help="Antwort satzweise vorlesen, sobald der erste Satz fertig ist "
|
||||
"(geringere Zeit bis zum ersten Ton)")
|
||||
# Audio-Streaming: Default entscheidet der Server (AUDIO_STREAM_DEFAULT, i. d. R. an).
|
||||
audio_grp = p.add_mutually_exclusive_group()
|
||||
audio_grp.add_argument("--stream-audio", dest="stream_audio", action="store_const", const=True,
|
||||
default=None, help="satzweises Vorlesen erzwingen (frueher Ton)")
|
||||
audio_grp.add_argument("--no-stream-audio", dest="stream_audio", action="store_const", const=False,
|
||||
default=None, help="satzweises Vorlesen abschalten (komplett am Ende)")
|
||||
p.add_argument("--stream-text", dest="stream_text", action="store_true",
|
||||
help="Antworttext live am Monitor anzeigen, waehrend die KI ihn erzeugt")
|
||||
p.add_argument("--file", default=None, help="WAV statt Mikrofon senden (Test ohne Aufnahme)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue