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 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-17 23:42:23 +02:00
commit 9d0019274a
5 changed files with 22 additions and 7 deletions

View file

@ -323,8 +323,16 @@ curl -s -X POST "$URL/api/speak" -H 'Content-Type: application/json' \
# anderes TTS-Modell (andere Stimmenfamilie): # anderes TTS-Modell (andere Stimmenfamilie):
echo 'OPENROUTER_TTS_MODEL=openai/gpt-4o-mini-tts' >> .env echo 'OPENROUTER_TTS_MODEL=openai/gpt-4o-mini-tts' >> .env
``` ```
Das Feld `voice` gibt es im Body von `/api/speak` und `/api/chat`. Hinweis: Das Feld `voice` gibt es im Body von `/api/speak` und `/api/chat`. Im Sprech-Loop
`scripts/voice_loop.py` hat **keine** `--voice`-Option — dort gilt der Server-Default. 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) ## B2. Soundquelle & Ausgabe-Gerät wechseln (Mikrofon, Lautsprecher, Bluetooth, Handy)

View file

@ -51,7 +51,8 @@ async def chat(
"llm_provider": payload.llm_provider, "llm_provider": payload.llm_provider,
"tts_provider": payload.tts_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() store = get_store()

View file

@ -3,7 +3,6 @@ from io import BytesIO
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from app.config import settings
from app.errors import RoutingError from app.errors import RoutingError
from app.auth import require_user from app.auth import require_user
from app.store import User, SessionOwnershipError from app.store import User, SessionOwnershipError
@ -33,7 +32,8 @@ async def speak(
"language": payload.language, "language": payload.language,
"tts_provider": payload.tts_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
try: try:
route = resolve_route(user, session_id, overrides) route = resolve_route(user, session_id, overrides)

View file

@ -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()}) 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")) stream = bool(options.get("stream"))
# audio_stream: explizite Anfrage gewinnt, sonst der serverseitige Default (Admin). # audio_stream: explizite Anfrage gewinnt, sonst der serverseitige Default (Admin).
audio_stream = bool(options["audio_stream"]) if "audio_stream" in options \ audio_stream = bool(options["audio_stream"]) if "audio_stream" in options \

View file

@ -10,6 +10,7 @@ Beispiele:
python scripts/voice_loop.py --url ws://localhost:8003/ws/voice --session oma-anna 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 --device hw:1,0 # bestimmtes Mikrofon (arecord -L)
python scripts/voice_loop.py --llm-provider openrouter --tts-provider openrouter 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) python scripts/voice_loop.py --file frage.wav # ohne Mikrofon (Test)
Voraussetzungen: laufendes Gateway, `arecord` (Aufnahme), ein Player Voraussetzungen: laufendes Gateway, `arecord` (Aufnahme), ein Player
@ -211,7 +212,7 @@ def _start_frame(args) -> dict:
frame["audio_stream"] = args.stream_audio frame["audio_stream"] = args.stream_audio
if getattr(args, "stream_text", False): if getattr(args, "stream_text", False):
frame["stream"] = True 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) value = getattr(args, key, None)
if value: if value:
frame[key] = value frame[key] = value
@ -346,6 +347,9 @@ def main() -> None:
p.add_argument("--stt-provider", dest="stt_provider", default=None) p.add_argument("--stt-provider", dest="stt_provider", default=None)
p.add_argument("--llm-provider", dest="llm_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("--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) p.add_argument("--language", default=None)
# Audio-Streaming: Default entscheidet der Server (AUDIO_STREAM_DEFAULT, i. d. R. an). # Audio-Streaming: Default entscheidet der Server (AUDIO_STREAM_DEFAULT, i. d. R. an).
audio_grp = p.add_mutually_exclusive_group() audio_grp = p.add_mutually_exclusive_group()