#!/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()