Wer Voice-Over für Home-Automation, Podcasts oder Tutorials sucht, landet oft bei Cloud-Anbietern. Doch mit Piper, einer schnellen, lokal laufenden TTS-Engine, und einer Prise Python lässt sich ein System bauen, das dank Wiktionary-Anbindung selbst schwierige Ortsnamen wie „Wermelskirchen“ perfekt ausspricht.
Hier ist der Weg von der Installation bis zum optimierten Workflow.
Installation: Piper & Stimmen
Piper ist in C++ geschrieben und extrem performant. Für die deutsche Sprache ist die Stimme von Thorsten (Thorsten-Voice) der Goldstandard.
Schritt 1: Piper installieren
Am einfachsten geht es via Python-Paketmanager (pipx wird empfohlen, um das System sauber zu halten):
pipx install piper-tts
Schritt 2: Stimmen-Modelle laden
Wir nutzen zwei Qualitätsstufen. Das medium-Modell ist schnell, das high-Modell bietet noch mehr Nuancen.
mkdir -p ~/piper-voices && cd ~/piper-voices
# Medium Modell
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/de/de_DE/thorsten/medium/de_DE-thorsten-medium.onnx
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/de/de_DE/thorsten/medium/de_DE-thorsten-medium.onnx.json
# High Modell (für finale Podcast-Qualität)
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/de/de_DE/thorsten/high/de_DE-thorsten-high.onnx
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/de/de_DE/thorsten/high/de_DE-thorsten-high.onnx.json
Als schneller Workflow: Kommandozeilen-Aliase
Um nicht jedes Mal lange Befehle tippen zu müssen legt man Kurzbefehle in der .bashrc (oder .zshrc) an:
# Schnelltest (Ausgabe direkt über die Lautsprecher)
alias say='piper --model ~/piper-voices/de_DE-thorsten-medium.onnx --output_raw | aplay -r 22050 -f S16_LE -t raw'
# In Datei schreiben (High Quality)
alias render_hq='piper --model ~/piper-voices/de_DE-thorsten-high.onnx --output_file'
Anwendung: echo "Hallo Welt" | say oder echo "Text" | render_hq test.wav.
Weitere Idee: Ein Phonetik-Skript
Standard-TTS scheitert oft an Eigennamen oder Fachbegriffen. Die Lösung: Wir nutzen die IPA-Lautschrift (International Phonetic Alphabet) von Wiktionary.
Die Entscheidung: API vs. Scraping
In der Theorie bietet Wiktionary eine API. In der Praxis liefert diese oft nur unstrukturierten Wikitext oder extrem verschachteltes JSON zurück.
Die Lösung: Man nutzt gezieltes HTML-Scraping. Über den CSS-Selektor span.ipa lässt sich die exakte Aussprache eines Wortes viel zuverlässiger extrahieren.
Das Python-Skript (audio_gen.py)
Dieses Skript liest einen Text, prüft für jedes Wort, ob eine Lautschrift im lokalen Cache oder auf Wiktionary existiert, reichert den Text mit IPA-Tags ([[ ˈapfl̩ ]]) an und übergibt das Ergebnis an Piper.
#!/usr/bin/env python3
import requests
import re
import json
import os
import argparse
import subprocess
# --- KONFIGURATION ---
CACHE_FILE = "pronunciation_cache.json"
# Pfad zu deinem Thorsten-Voice Modell
MODEL_PATH = os.path.expanduser("~/piper-voices/de_DE-thorsten-high.onnx")
def get_ipa(word):
"""Holt die IPA-Lautschrift von Wiktionary via Scraping."""
word_clean = word.strip().capitalize()
url = f"https://de.wiktionary.org/wiki/{word_clean}"
headers = {'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64) TTS-Project'}
try:
r = requests.get(url, headers=headers, timeout=5)
if r.status_code == 200:
# Flexibler Regex für Parsoid-HTML (handhabt style/about Attribute)
match = re.search(r'class="ipa"[^>]*>(.+?)</span>', r.text)
if match:
ipa = match.group(1).strip()
# HTML-Tags entfernen
ipa = re.sub(r'<[^>]+>', '', ipa)
# Piper-Cleanup: Zeichen entfernen, die Warnungen auslösen
# / [] sind Begrenzer, ͡ und ̯ sind Verbindungsbögen
for char in ["/", "[", "]", "͡", "̯"]:
ipa = ipa.replace(char, "")
# Nur die erste Aussprachevariante nehmen
return ipa.split(",")[0].split(";")[0].strip()
except Exception:
pass
return None
def process_text(text, cache):
"""Reichert den Text mit IPA-Tags für Piper an."""
words = text.split()
processed = []
modifications = []
for word in words:
# Nur Satzzeichen am Rand entfernen, Umlaute behalten
clean = word.strip(".,!?;:()\"' ")
if len(clean) >= 2:
if clean in cache:
ipa = cache[clean]
processed.append(f"[[ {ipa} ]]")
modifications.append((clean, ipa, "Cache"))
else:
ipa = get_ipa(clean)
if ipa:
cache[clean] = ipa
processed.append(f"[[ {ipa} ]]")
modifications.append((clean, ipa, "Wiktionary"))
else:
processed.append(word)
else:
processed.append(word)
return " ".join(processed), modifications
def main():
parser = argparse.ArgumentParser(description="TTS mit Wiktionary-Support")
parser.add_argument("input", help="Input Textdatei (.txt oder .md)")
parser.add_argument("-o", "--output", default="output.wav", help="Name der Output-Datei")
args = parser.parse_args()
if not os.path.exists(args.input):
print(f"Fehler: Datei {args.input} nicht gefunden.")
return
# UTF-8 ist Pflicht für Umlaute!
with open(args.input, "r", encoding="utf-8") as f:
content = f.read()
# Cache laden (falls vorhanden)
cache = {}
if os.path.exists(CACHE_FILE):
with open(CACHE_FILE, "r", encoding="utf-8") as f:
cache = json.load(f)
enriched_text, mods = process_text(content, cache)
# Status-Ausgabe für das Blog-Feeling
print(f"\n--- Analyse: {len(words)} Wörter verarbeitet ---")
if mods:
for m in mods:
print(f"Anpassung: {m[0]} -> {m[1]} ({m[2]})")
# Cache speichern (ensure_ascii=False macht die Datei lesbar!)
with open(CACHE_FILE, "w", encoding="utf-8") as f:
json.dump(cache, f, indent=4, ensure_ascii=False)
# Piper Aufruf via Subprocess
print("\n--- Generiere Audio ---")
try:
subprocess.run([
"piper", "--model", MODEL_PATH, "--output_file", args.output
], input=enriched_text.encode('utf-8'), check=True)
print(f"Erfolg! Datei gespeichert: {args.output}")
except Exception as e:
print(f"Fehler bei Piper: {e}")
if __name__ == "__main__":
main()
Was dieses Skript macht:
- UTF-8 Handling: Durch
encoding="utf-8"beim Lesen und Schreiben gibt es keine Probleme mit Umlauten auf Linux-Servern. - Transparenter Cache: Dank
ensure_ascii=Falsewird die JSON-Datei in echtem Text gespeichert und man kann sie einfach mit einem Editor öffnen und Wörter korrigieren, ohne kryptische Unicode-Codes (\u02c8) entziffern zu müssen. - Piper-Optimierung: Die automatische Entfernung der Ligaturbögen sorgt dafür, dass die Piper-Konsole während des Renderns sauber bleibt und keine
Missing phoneme-Warnungen ausgibt. - Regex-Robustheit: Da Wiktionary oft zusätzliche Attribute in den HTML-Tags hat (z. B.
styleoderabout), muss der Parser flexibel sein:re.search(r'class="ipa"[^>]*>(.+?)</span>', r.text).
Den Cache manuell tunen
Nach dem ersten Lauf wird eine pronunciation_cache.json angelegt. Da wir ensure_ascii=False nutzen, sieht sie in dieser Art aus:
{
"Wermelskirchen": "ˈvɛʁml̩sˌkɪʁçn̩",
"Nextcloud": "nɛkstklaʊd"
}
Hier kann man die Aussprache manuell verfeinern. Wenn „Thorsten“ ein Wort zu schnell spricht, fügt man einfach ein paar Zeichen ein oder ändert die Betonung.
Mit diesem Setup hat man ein interessantes Werkzeug an der Hand. Durch die Kombination aus lokaler High-Quality Stimme und dynamischer Phonetik-Abfrage klingen automatisierte Texte besser, sondern nach professionellem Sprecher – und das sogar offline (sobald der Cache gefüllt ist).
Tipp: Besonders bei Ortsnamen lohnt sich der Blick in den Cache, um die typische Betonung exakt zu treffen!
