first commit

This commit is contained in:
ersterDetektiv
2026-09-25 06:47:14 +02:00
commit 6f198c3d51
9 changed files with 1100 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""
TTS-Modul: Liest JSON-Input und gibt Audio aus (Thorsten Emotional Voice).
JSON-Input Format:
{
"text": "Der zu sprechende Text",
"emotion": "neutral", // optional: amused|angry|disgusted|drunk|neutral|sleepy|surprised|whisper
"speaker_id": 4, // optional: 0-7 (überschreibt emotion)
"output_file": "output.wav" // optional: Ausgabedatei
}
Aufruf:
python tts_module.py # liest data/tts_input.json
python tts_module.py input.json # liest angegebene JSON-Datei
echo '{"text":"Hallo"}' | python tts_module.py # liest von stdin
"""
import sys
import json
import wave
import os
from pathlib import Path
# Pfade relativ zu diesem Script
SCRIPT_DIR = Path(__file__).parent
DEFAULT_INPUT = SCRIPT_DIR / "data" / "tts_input.json"
DEFAULT_OUTPUT = SCRIPT_DIR / "data" / "output.wav"
MODEL_PATH = SCRIPT_DIR / "de_DE-thorsten_emotional-medium.onnx"
CONFIG_PATH = SCRIPT_DIR / "de_DE-thorsten_emotional-medium.onnx.json"
# Emotion-Name → Speaker-ID
EMOTION_MAP = {
"amused": 0,
"angry": 1,
"disgusted": 2,
"drunk": 3,
"neutral": 4,
"sleepy": 5,
"surprised": 6,
"whisper": 7,
}
def load_input(source: str | None = None) -> dict:
"""Lädt JSON-Input aus Datei, stdin oder Standardpfad."""
if source:
with open(source, "r", encoding="utf-8") as f:
return json.load(f)
# Prüfe ob stdin Daten hat (z.B. Pipe)
if not sys.stdin.isatty():
return json.load(sys.stdin)
# Standard: data/tts_input.json
with open(DEFAULT_INPUT, "r", encoding="utf-8") as f:
return json.load(f)
def synthesize(data: dict) -> str:
"""Synthetisiert Text und gibt den Pfad zur Ausgabedatei zurück."""
from piper import PiperVoice
from piper.voice import SynthesisConfig
text = data.get("text", "")
if not text:
raise ValueError("Kein 'text' im JSON angegeben.")
# Speaker-ID bestimmen: direkt oder über Emotion-Name
if "speaker_id" in data:
speaker_id = int(data["speaker_id"])
else:
emotion = data.get("emotion", "neutral").lower()
speaker_id = EMOTION_MAP.get(emotion, EMOTION_MAP["neutral"])
output_file = str(data.get("output_file", DEFAULT_OUTPUT))
os.makedirs(os.path.dirname(output_file) or ".", exist_ok=True)
print(f"[TTS] Lade Modell...", flush=True)
voice = PiperVoice.load(str(MODEL_PATH), str(CONFIG_PATH))
syn_config = SynthesisConfig(speaker_id=speaker_id)
emotion_name = next((k for k, v in EMOTION_MAP.items() if v == speaker_id), str(speaker_id))
print(f"[TTS] Synthesisiere: '{text[:60]}...' | Emotion: {emotion_name} (ID {speaker_id})", flush=True)
with wave.open(output_file, "wb") as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(voice.config.sample_rate)
for chunk in voice.synthesize(text, syn_config):
wav_file.writeframes(chunk.audio_int16_bytes)
print(f"[TTS] Audio gespeichert: {output_file}", flush=True)
return output_file
def main():
source = sys.argv[1] if len(sys.argv) > 1 else None
try:
data = load_input(source)
output_path = synthesize(data)
# Optional: Audio automatisch abspielen
if data.get("autoplay", False):
os.system(f"aplay '{output_path}' 2>/dev/null || paplay '{output_path}' 2>/dev/null")
except FileNotFoundError as e:
print(f"[TTS] Fehler: Datei nicht gefunden – {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"[TTS] Fehler: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()