Files
Thorsten/tts_module.py
T
ersterDetektiv 6f198c3d51 first commit
2026-09-25 06:47:14 +02:00

110 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()