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:
Dieter Schlüter 2026-06-17 01:48:56 +02:00
commit 293ed257db
72 changed files with 2612 additions and 0 deletions

0
app/pipeline/__init__.py Normal file
View file

View file

@ -0,0 +1,4 @@
class InputCleaner:
async def run(self, text: str) -> str:
cleaned = " ".join(text.strip().split())
return cleaned.replace(" äh ", " ").replace(" hm ", " ")

View file

@ -0,0 +1,37 @@
import re
class SpokenResponseAdapter:
async def run(self, text: str, language: str = "de") -> str:
if not text:
return ""
text = text.strip()
# Markdown / Formatierung entfernen
text = re.sub(r"```[\s\S]*?```", " ", text) # code blocks
text = re.sub(r"`([^`]*)`", r"\1", text) # inline code
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) # markdown links
text = re.sub(r"[*_~#>]+", " ", text) # markdown symbols
# Listen entschärfen
text = re.sub(r"(?m)^\s*[-•]\s+", "", text)
text = re.sub(r"(?m)^\s*\d+\.\s+", "", text)
# Mehrfache Leerzeichen / Zeilenumbrüche glätten
text = re.sub(r"\s+", " ", text).strip()
# Für Voice natürlicher machen: Doppelpunkte/Semikolons etwas beruhigen,
# aber Uhrzeiten/Verhältnisse (10:30) nicht zerstören -> nur am Wortende ersetzen.
text = re.sub(r"[:;](?=\s|$)", ",", text)
# Klammern meist nicht gut für TTS
text = text.replace("(", ", ")
text = text.replace(")", " ")
# Abschlusspunktion sicherstellen
if text and not text.endswith((".", "!", "?")):
text += "."
return text

View file

@ -0,0 +1,59 @@
import re
class TTSNormalizer:
async def run(self, text: str, language: str = "de") -> str:
if not text:
return ""
normalized = text
if language == "de":
replacements = {
"24/7": "vierundzwanzig sieben",
"&": " und ",
"%": " Prozent",
"": " Euro",
"$": " Dollar",
"km/h": " Kilometer pro Stunde",
"z.B.": "zum Beispiel",
"bzw.": "beziehungsweise",
"u.a.": "unter anderem",
"ca.": "circa",
}
else:
replacements = {
"24/7": "twenty four seven",
"&": " and ",
"%": " percent",
"": " euros",
"$": " dollars",
"km/h": " kilometers per hour",
"e.g.": "for example",
"i.e.": "that is",
}
for old, new in replacements.items():
normalized = normalized.replace(old, new)
# Slashes zwischen Wörtern/Zahlen sprachfreundlicher machen
normalized = re.sub(r"(\w)/(\w)", r"\1 oder \2", normalized)
# Datums-/Versions-/Bereichsstriche etwas entschärfen
normalized = normalized.replace("", " bis ")
normalized = normalized.replace("", ", ")
normalized = normalized.replace(" - ", ", ")
# URLs und E-Mails nicht roh vorlesen
normalized = re.sub(r"https?://\S+", "Link", normalized)
normalized = re.sub(r"\b[\w\.-]+@[\w\.-]+\.\w+\b", "E-Mail-Adresse", normalized)
# Mehrfache Leerzeichen glätten
normalized = re.sub(r"\s+", " ", normalized).strip()
if normalized and not normalized.endswith((".", "!", "?")):
normalized += "."
return normalized