first commit
This commit is contained in:
+109
@@ -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()
|
||||
Reference in New Issue
Block a user