From 9d0019274a6420312c57c950b96b3080d37027ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Schl=C3=BCter?= Date: Wed, 17 Jun 2026 23:42:23 +0200 Subject: [PATCH] feat(voice_loop): --voice-Option + saubere Default-Stimme je TTS-Provider - voice_loop.py: --voice reicht die Stimme im Start-Frame durch (nur wenn gesetzt), sonst greift der Provider-Default. Beispiele in Docstring/Help. - ws.py/speak.py/chat.py: ohne explizite Stimme None durchreichen statt die OpenRouter-Stimme zu erzwingen -> jeder Provider nimmt SEINEN Default (OPENROUTER_TTS_VOICE bzw. PIPER_VOICE). Ungenutzten settings-Import in speak.py entfernt. - Doku: BEDIENUNGSANLEITUNG (--voice + Default-Stimme je Provider). Co-Authored-By: Claude Opus 4.8 --- BEDIENUNGSANLEITUNG.md | 12 ++++++++++-- app/api/chat.py | 3 ++- app/api/speak.py | 4 ++-- app/api/ws.py | 4 +++- scripts/voice_loop.py | 6 +++++- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 7042101..dec938f 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -323,8 +323,16 @@ curl -s -X POST "$URL/api/speak" -H 'Content-Type: application/json' \ # anderes TTS-Modell (andere Stimmenfamilie): echo 'OPENROUTER_TTS_MODEL=openai/gpt-4o-mini-tts' >> .env ``` -Das Feld `voice` gibt es im Body von `/api/speak` und `/api/chat`. Hinweis: -`scripts/voice_loop.py` hat **keine** `--voice`-Option — dort gilt der Server-Default. +Das Feld `voice` gibt es im Body von `/api/speak` und `/api/chat`. Im Sprech-Loop +direkt durchprobieren mit `--voice` (ohne Angabe gilt der Provider-Default): +```bash +python scripts/voice_loop.py --tts-provider openrouter --voice Puck # Cloud-Stimme +python scripts/voice_loop.py --tts-provider piper --voice de_DE-kerstin-low # lokale Stimme +``` +**Default-Stimme je Provider:** Wird keine Stimme angefragt, nimmt jeder TTS-Provider +seinen eigenen Default — OpenRouter `OPENROUTER_TTS_VOICE`, piper `PIPER_VOICE`. `--voice` +ist provider-spezifisch: ein Gemini-/OpenAI-Stimmenname für `openrouter`, ein Modellname +für `piper` (ein unpassender Name fällt bei piper auf `PIPER_VOICE` zurück). ## B2. Soundquelle & Ausgabe-Gerät wechseln (Mikrofon, Lautsprecher, Bluetooth, Handy) diff --git a/app/api/chat.py b/app/api/chat.py index 79572b8..df8ffbd 100644 --- a/app/api/chat.py +++ b/app/api/chat.py @@ -51,7 +51,8 @@ async def chat( "llm_provider": payload.llm_provider, "tts_provider": payload.tts_provider, } - voice = payload.voice or settings.openrouter_tts_voice + # None -> der gewaehlte TTS-Provider nimmt seinen eigenen Default. + voice = payload.voice store = get_store() diff --git a/app/api/speak.py b/app/api/speak.py index c4c7cfb..7092a23 100644 --- a/app/api/speak.py +++ b/app/api/speak.py @@ -3,7 +3,6 @@ from io import BytesIO from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import StreamingResponse -from app.config import settings from app.errors import RoutingError from app.auth import require_user from app.store import User, SessionOwnershipError @@ -33,7 +32,8 @@ async def speak( "language": payload.language, "tts_provider": payload.tts_provider, } - voice = payload.voice or settings.openrouter_tts_voice + # None -> der gewaehlte TTS-Provider nimmt seinen eigenen Default. + voice = payload.voice try: route = resolve_route(user, session_id, overrides) diff --git a/app/api/ws.py b/app/api/ws.py index 4227131..86c6b22 100644 --- a/app/api/ws.py +++ b/app/api/ws.py @@ -90,7 +90,9 @@ async def _run_turn(websocket, store, user, session_id, route, orchestrator, out await websocket.send_json({"type": "ack", "route": route.as_dict()}) - voice = options.get("voice") or settings.openrouter_tts_voice + # Ohne explizite Stimme None lassen -> jeder TTS-Provider nimmt SEINEN Default + # (OpenRouter: OPENROUTER_TTS_VOICE, piper: PIPER_VOICE). + voice = options.get("voice") stream = bool(options.get("stream")) # audio_stream: explizite Anfrage gewinnt, sonst der serverseitige Default (Admin). audio_stream = bool(options["audio_stream"]) if "audio_stream" in options \ diff --git a/scripts/voice_loop.py b/scripts/voice_loop.py index f03e08a..46331d9 100644 --- a/scripts/voice_loop.py +++ b/scripts/voice_loop.py @@ -10,6 +10,7 @@ Beispiele: python scripts/voice_loop.py --url ws://localhost:8003/ws/voice --session oma-anna python scripts/voice_loop.py --device hw:1,0 # bestimmtes Mikrofon (arecord -L) python scripts/voice_loop.py --llm-provider openrouter --tts-provider openrouter + python scripts/voice_loop.py --tts-provider openrouter --voice Puck # Cloud-Stimme testen python scripts/voice_loop.py --file frage.wav # ohne Mikrofon (Test) Voraussetzungen: laufendes Gateway, `arecord` (Aufnahme), ein Player @@ -211,7 +212,7 @@ def _start_frame(args) -> dict: 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"): + for key in ("stt_provider", "llm_provider", "tts_provider", "language", "voice"): value = getattr(args, key, None) if value: frame[key] = value @@ -346,6 +347,9 @@ def main() -> None: p.add_argument("--stt-provider", dest="stt_provider", default=None) p.add_argument("--llm-provider", dest="llm_provider", default=None) p.add_argument("--tts-provider", dest="tts_provider", default=None) + p.add_argument("--voice", default=None, + help="TTS-Stimme (provider-spezifisch; ohne Angabe gilt der Provider-Default, " + "z. B. OPENROUTER_TTS_VOICE bzw. PIPER_VOICE)") p.add_argument("--language", default=None) # Audio-Streaming: Default entscheidet der Server (AUDIO_STREAM_DEFAULT, i. d. R. an). audio_grp = p.add_mutually_exclusive_group()