Initial commit: Voice Assistant Gateway mit Konfig-/Routing-Fundament
- FastAPI-Gateway mit REST-Endpunkten (chat/speak/transcribe/devices/sessions/config) - Geschichtete Konfiguration mit Profilen (local-dev/hybrid/cloud) via TOML + ENV - Registry-Pattern + einheitliche Route-Aufloesung (Default->Profil->ENV->Session->Request) - Device Router (strikt, Singleton) und Output-Lifecycle im Orchestrator - OpenRouter-Adapter (STT multipart/LLM/TTS) + lokale Provider-Stubs - Regelbasierte Pipeline (Cleaner/Spoken-Adapter/TTS-Normalizer) - 22 automatisierte Tests; Doku: README, BEDIENUNGSANLEITUNG, Architektur - Secrets ausschliesslich ueber Umgebung; .env und lokale config gitignored Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
293ed257db
72 changed files with 2612 additions and 0 deletions
76
chat_client.py
Normal file
76
chat_client.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import io
|
||||
import wave
|
||||
import requests
|
||||
import soundfile as sf
|
||||
import numpy as np
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
GATEWAY_URL = "http://localhost:8003"
|
||||
CHAT_ENDPOINT = f"{GATEWAY_URL}/api/chat"
|
||||
|
||||
# feste Annahmen für Gemini 3.1 Flash TTS über OpenRouter
|
||||
SAMPLE_RATE = 24000
|
||||
CHANNELS = 1
|
||||
SAMPLE_WIDTH = 2 # 16-bit PCM
|
||||
|
||||
|
||||
def pcm_to_wav(pcm_bytes: bytes, wav_path: str) -> None:
|
||||
"""Rohes s16le-PCM in eine WAV-Datei schreiben."""
|
||||
with wave.open(wav_path, "wb") as wf:
|
||||
wf.setnchannels(CHANNELS)
|
||||
wf.setsampwidth(SAMPLE_WIDTH)
|
||||
wf.setframerate(SAMPLE_RATE)
|
||||
wf.writeframes(pcm_bytes)
|
||||
|
||||
|
||||
def play_wav(wav_path: str) -> None:
|
||||
"""WAV-Datei abspielen (ffplay oder aplay/mpv, je nach System)."""
|
||||
for cmd in (
|
||||
["ffplay", "-nodisp", "-autoexit", wav_path],
|
||||
["aplay", wav_path],
|
||||
["mpv", wav_path],
|
||||
):
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
return
|
||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||
continue
|
||||
print(f"Konnte keine geeignete Player-CLI finden für {wav_path}", file=sys.stderr)
|
||||
|
||||
|
||||
def chat_and_play(text: str, language: str = "de") -> None:
|
||||
payload = {"text": text, "language": language}
|
||||
|
||||
resp = requests.post(
|
||||
CHAT_ENDPOINT,
|
||||
json=payload,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
if not resp.ok:
|
||||
print("HTTP", resp.status_code)
|
||||
print(resp.text)
|
||||
return
|
||||
|
||||
pcm_bytes = b"".join(resp.iter_content(chunk_size=8192))
|
||||
|
||||
# Hinweis: Den Text-Trace (Transkript/Antwort) liefert /api/chat nur im JSON,
|
||||
# wenn man ?debug=true anhängt - nicht als Header im Audio-Stream.
|
||||
print("Audio-Format:", resp.headers.get("X-Audio-Format"))
|
||||
print("Sample-Rate:", resp.headers.get("X-Audio-Sample-Rate"))
|
||||
|
||||
wav_path = "chat_reply.wav"
|
||||
pcm_to_wav(pcm_bytes, wav_path)
|
||||
print(f"WAV gespeichert unter {wav_path}")
|
||||
play_wav(wav_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
user_text = " ".join(sys.argv[1:])
|
||||
else:
|
||||
user_text = "Wie wird das Wetter morgen in Bünde?"
|
||||
|
||||
chat_and_play(user_text, language="de")
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue