Blog/LLMOps & Evals

Lokales Text-to-Speech im großen Maßstab: 95 Artikel mit offenen Modellen vertont

Drei offene TTS-Modelle auf einem Laptop: wie 95 Artikel zu 153 Minuten Sprachausgabe wurden — von SQLite-Zeilen über chunkierte Synthese und 192-kbit/s-AAC bis zum Player, der mit der Seite mitläuft.

··11 Min. Lesezeit

  • Text-to-speech
  • Audio
  • Kokoro
  • FFmpeg
  • Vue
Wellendiagramm der Sprachpipeline: Datenbankzeilen werden in Sätze zerlegt, von einem lokalen Modell synthetisiert, zu AAC kodiert und über einen klebenden Tab am Bildschirmrand abgespielt.

Das Wichtigste in Kürze

  • Lokales TTS kehrt die Ökonomie des Vertonens um: 95 Artikel und 153 Minuten Audio kosten etwa 35 Minuten Rechenzeit und null API-Gebühren — die Grenzkosten eines zusätzlichen Artikels sind Strom.
  • Die Modellwahl ist in dieser Größenordnung eine Tempo-Qualitäts-Pareto-Front, kein Benchmark-Kampf: Piper war am schnellsten, Bark kam nie fertig, und Kokoro v1.0 (82M, ONNX) gewann bei der Prosodie mit etwa dem Vierfachen der Echtzeit.
  • An Satzgrenzen chunken, bis 400 Zeichen, mit 80 ms Stille zwischen den Stücken; cleverere Schnittpunkte bringen fast nichts, was Hörer bemerken würden.
  • WAV ist ein Arbeitsformat, kein Auslieferungsformat: AAC mit 192 kbit/s und faststart reduzierte 406 MB auf 181 MB und macht Dauer und Spulen funktionierbar, bevor die Datei vollständig geladen ist.
  • Im prerenderten Player schlagen Medienereignisse Annahmen: loadedmetadata kann vor der Hydration feuern, deshalb muss die Dauer bei durationchange, canplay und mount neu gelesen werden — Zustand abonnieren, nicht Benachrichtigungen.

Diesen Artikel anhören

0:000:00

Jeder Artikel auf dieser Seite hat jetzt eine Vertonung: 95 Beiträge, 153 Minuten Audio, erzeugt auf einem einzigen Laptop ohne einen einzigen API-Aufruf. Dieser Post ist der vollständige Bericht — wie drei offene Text-to-Speech-Modelle verglichen wurden, wie die Pipeline SQLite-Zeilen in satzgroße Synthese-Chunks verwandelt, warum das ausgelieferte Format AAC mit 192 kbit/s ist und wie der Player gebaut ist, damit die Sprache mit der Seite mitläuft.

Zuerst die Zahlen, denn sie rahmen jede Entscheidung unten: 95 Artikel (45 Blogposts und 50 Tool-Reviews), 153 Minuten fertige Vertonung, etwa 35 Minuten Rechenzeit, 406 MB Zwischen-WAV reduziert auf 181 MB ausgeliefertes AAC und null Euro API-Gebühren. Alles lief auf einem Apple-Silicon-Laptop mit 16 GB Arbeitsspeicher.

Warum lokales Text-to-Speech

Gehostetes Text-to-Speech ist hervorragend und wird jedes Quartal besser — aber es wird verrechnet. In diesem Umfang wächst die Rechnung mit jedem Wort, jedem Neuaufbau nach einer Änderung, jedem Experiment mit Stimme oder Tempo. Eine lokale Pipeline kehrt die Ökonomie um: Die Grenzkosten eines zusätzlichen Artikels sind ein paar Cent Strom und zwei Minuten Warten. Außerdem ist sie reproduzierbar — derselbe Text, dieselbe Modellversion und dieselbe Stimme erzeugen nächstes Jahr dieselbe Datei, und das zählt, wenn ein geänderter Artikel neu vertont werden soll, ohne dass die Stimme driftet.

Es gibt einen zweiten Grund: Der Text verlässt die Maschine nicht. Der Entwurf eines noch nicht veröffentlichten Artikels wird von demselben Prozess gelesen, der ihn auch veröffentlichen wird, auf derselben Maschine — und wenn sich ein Absatz ändert, erzeugt ein Lauf über einen Slug eine Datei, nicht fünfundneunzig.

Drei Modelle, ein Laptop

Piper war der Ausgangspunkt, weil es auf die beste Art langweilig ist: ein kleines ONNX-Modell (die LibriTTS-high-Stimme wiegt etwa 137 MB), espeak-ng-Phonemisierung, 22,05 kHz Ausgabe, Erzeugung mit etwa dem Fünffachen der Echtzeit. Das Ergebnis ist sauber und konsistent — ein kompetenter Nachrichtensprecher —, aber auch gleichförmig. Lange Artikel klingen leicht flach.

Das erste Hindernis betraf das Packaging, nicht die Qualität: Die vorbuildete macOS-Binary gibt es nur für x86_64, und auf Apple Silicon verweigert sie die Verlinkung gegen die arm64-espeak-ng-Bibliothek mit einem Architekturkonflikt. Das Python-Paket umgeht die Binary komplett und lief nach Minuten. Eine nützliche Erinnerung: „läuft nicht" ist oft ein Toolchain-Problem, kein Modellproblem.

Bark war das verlockendste der drei: expressiv, zu Lachen und Seufzen fähig, mit veröffentlichten Samples, die jedes andere Modell wie ein Metronome klingen lassen. Es war auch das, das nie einen brauchbaren Satz produzierte. Das aktuelle PyTorch hat das Standardverhalten von torch.load auf weights_only umgestellt, weshalb das Entpacken des Checkpoints scheiterte, bis gepatcht wurde; als das Laden lief, war die Inferenz auf der CPU so langsam, dass 95 Artikel den größten Teil eines Tages gebraucht hätten. Expressivität war diesen Preis nicht wert — Bark bleibt für diese Arbeit ein Forschungsspielzeug.

Kokoro v1.0 über die kokoro-onnx-Bindings war der Fund: 82 Millionen Parameter, etwa 325 MB ONNX, 24 kHz Ausgabe, 54 Stimmen und Erzeugung mit etwa dem Vierfachen der Echtzeit bei deutlich besserer Prosodie als Piper — Sätze setzen ihre Betonung, und Zahlen und Abkürzungen klingen nicht nach dem stockenden Ton kleiner Modelle. So klingt jetzt jeder Artikel auf dieser Seite.

ModellGewichteAusgabeTempo auf diesem RechnerUrteil
Piper, LibriTTS highetwa 137 MB, ONNX22,05 kHz WAVetwa 5x EchtzeitSchnell und konsistent, leicht flach
Bark von Sunoetwa 2 GB, PyTorch24 kHz WAVim Budget nie fertig gewordenExpressiv, hier unpraktisch
Kokoro v1.082M, etwa 325 MB, ONNX24 kHz WAVetwa 4x EchtzeitNatürliche Prosodie; die ausgelieferte Stimme

Die Erzeugungspipeline

Die Inhaltsdatenbank ist die einzige Wahrheitsquelle, deshalb liest die Pipeline direkt aus ihr: Titel, Beschreibung, Kernaussagen und FAQ auf der einen, die Blockliste des Artikels auf der anderen Seite. Nichts wird aus der gerenderten Seite gescrapt. Wenn ein Absatz im Post steht, wird er vertont — und wenn es eine Tabelle, ein Callout oder ein Codeblock ist, wird er so zerlegt, dass ein Hörer folgen kann: Tabellenzeilen werden zu Komma-Zeilen, Callout-Titel werden wie Überschriften gesprochen, Code wird so gelesen, wie er geschrieben ist.

Zwei Randbedingungen prägen die Mitte der Pipeline. Modelle schneiden langen Input ab, deshalb müssen Artikel geteilt werden; und die Prosodie darf nicht halbiert werden, deshalb müssen die Schnitte an Satzgrenzen liegen. Der Chunker ist bewusst schlicht: Sätze bis 400 Zeichen ansammeln, an der Grenze leeren, die Stücke mit 80 Millisekunden Stille verbinden. Schlauere Schnittpunkte — Absatzende, Überschriften als Intonationsreset — brachten fast nichts; der Satz ist die Einheit, die Hörer tatsächlich bemerken.

Der Chunker

def chunks(text, limit=400):
    """Accumulate sentences up to limit characters; never split mid-sentence."""
    out, buf = [], ""
    for sentence in text.split(". "):
        cand = (buf + " " + sentence).strip()
        if len(cand) > limit and buf:
            out.append(buf + ".")
            buf = sentence
        else:
            buf = cand
    if buf:
        out.append(buf)
    return out

pieces = []
for part in chunks(article_text):
    audio, _ = model.create(part, voice="af_bella")
    pieces.append(audio)
    pieces.append(np.zeros(int(24000 * 0.08)))   # 80 ms between chunks
sf.write(tmp_wav, np.concatenate(pieces), 24000)

Eine Stimme liest alle 95 Artikel. Es ist af_bella, ausgewählt, nachdem derselbe Absatz in mehreren der 54 Stimmen erzeugt wurde. Konsistenz ist der Punkt: Der Klang des Archivs soll wie eine Publikation klingen, nicht wie eine Lotterie. Die Vertonung liest den englischen Text jedes Artikels — auch von den deutschen und ungarischen Seiten — und ist als englische Vertonung des Stücks gekennzeichnet, nicht als Übersetzung.

WAV ist ein Arbeitsformat, kein Auslieferungsformat

Der Syntheseschritt schreibt 24 kHz, 16-Bit-PCM: korrekt, spulbar, unkomprimiert — und etwa 406 MB für 153 Minuten Audio. Auf der Festplatte ist das harmlos, über das Netz ein Fehler, und auf einer Seite, die alles prerendert, wäre es die mit Abstand größte Asset-Klasse.

Das ausgelieferte Format ist AAC in einem M4A-Container mit 192 kbit/s, mono. Diese Bitrate ist für Sprache großzügig — 96 bis 128 kbit/s klingen bei 24 kHz bereits transparent —, aber 192 lässt Reserve und kostet etwa ein Megabyte pro Artikel. Der Container zählt ebenso wie der Codec: faststart verschiebt das moov-Atom an den Anfang der Datei, sodass ein Browser die Dauer kennt und spulen kann, bevor der Download endet. Ohne ihn stehen Player auf 0:00 und weigern sich zu spulen, bis das letzte Byte da ist.

ffmpeg -i article.wav -c:a aac -b:a 192k -ac 1 -movflags +faststart article.m4a

406 MB wurden zu 181 MB — 55 Prozent weniger, jede Datei unter 1,5 MB, ausgeliefert über ganz normale Range-Anfragen und Cache-Header. Die WAV-Dateien erreichen das Build-Output nie: Sie werden gelöscht, sobald der Encoder erfolgreich war.

Der Player

Audio, das zu spät kommt, hört niemand, deshalb lädt der Player nur Metadaten: der Header geholt, die Dauer gelernt, und keine eigentlichen Samples heruntergeladen, bis der Leser auf Play drückt. Nichts startet automatisch — eine Seite, die einen anspricht, ist eine Seite, die man schließt.

Jeder Artikel bekommt eine inline Leiste unter dem Inhaltsverzeichnis: Play und Pause, eine spulbare Fortschrittslinie mit aktueller und gesamter Zeit und einen Tempo-Schalter zwischen 1x, 1,25x, 1,5x und 2x. Das Spul-Element ist ein natives range-Input im Look der Seite — Tastaturnavigation und Screenreader-Semantik werden geerbt, nicht neu erfunden.

Die inline Leiste ist nutzlos, sobald man darunter gescrollt hat, deshalb übernimmt ein klebender Tab am rechten Bildschirmrand. Ein IntersectionObserver beobachtet die Leiste: In dem Moment, in dem sie den Bildschirm verlässt, erscheint der Tab; zurückgescrollt, tritt er zurück. Der Tab füllt sich von unten mit dem Fortschritt — Fortschritt ist auf einen Blick lesbar —, und er respektiert prefers-reduced-motion wie der Rest der Oberfläche.

Was unterwegs brach

  • Packaging schlug zweimal Modelle. Piper lieferte eine nur-x86_64-Binary für eine arm64-Maschine, und Barks Checkpoint-Laden scheiterte, nachdem PyTorch das weights_only-Standardverhalten umstellte. Keiner dieser Fehler hatte etwas mit Sprache zu tun.
  • Versionsdrift ist real. Das kokoro-onnx-Paket übergab den speed-Parameter als int32, während das ausgelieferte Modell float erwartet — ein Einzeiler-Patch, gefunden durch Lesen eines zweizeiligen Stacktraces statt durch Raten.
  • Prüfe den Ton, nicht nur die Datei. ffprobe auf jeder Ausgabe fing Dauer-Probleme, bevor sie einen Browser erreichten; eine Datei, die existiert, ist keine Datei, die spielt.
  • Alte Formate sterben schwer. Der erste Durchlauf ließ WAVs im Build-Output zurück, gefunden nur, weil eine Anfrage eine falsche Bytezahl zurückgab.

Die Zahlen

KennzahlWert
Vertonte Artikel95 (45 Blogposts, 50 Tool-Reviews)
Fertiges Audio153 Minuten
Rechenzeit gesamtetwa 35 Minuten, 4,5x Echtzeit
Spitzenauslastungunter 2 GB
Zwischen-WAV406 MB
Ausgeliefertes AAC181 MB, etwa 1 MB pro Artikel
API-Kosten0 EUR

Zusammengelesen sagen die Zahlen, dass die Kosten dieser Pipeline Geduld sind, nicht Geld. Ein vollständiger Neuaufbau — nach einer Stimmenänderung oder einer Änderung, die alle Artikel betrifft — braucht deutlich unter einer Stunde auf einer Maschine, die währenddessen weiter nutzbar bleibt. Das ist das Argument für lokales Text-to-Speech in dieser Größenordnung: nicht, dass es eine gehostete Frontier-Stimme an Expressivität schlägt, sondern dass es Vertonung zur Standardwahl macht statt zur Budgetposition.

Was ich anders machen würde

Mit Kokoro anfangen. Der Shootout war nicht umsonst — Modelle vergleichen ist, was die Wahl verteidbar macht —, aber die ausgelieferte Pipeline wäre mit Kokoro als einzigem Kandidaten identisch gewesen. Zweitens: direkt von der Synthese nach AAC kodieren und keine WAVs persistieren; das Zwischenformat fügte einen Aufräumschritt und 406 MB Dateien hinzu, die nie hätten existieren müssen. Drittens: die Medienereignisse des Players als zu abonnierenden Zustand behandeln, nicht als einzufangende Benachrichtigungen — und der Dauer-Fehler passiert nie.

Offen bleibt der Umfang. Die Vertonung ist vorerst englischsprachig — eine Stimme, eine Sprache, 95 Artikel —, und Stimmen pro Sprache sind der naheliegende nächste Schritt, wenn es die deutsche und ungarische Leserschaft verlangt. Bis dahin ist die englische Vertonung auch die Aussprachehilfe für jeden Produktnamen auf der Seite — ein eigener stiller Nutzen.

Quellen

  1. Kokoro onnx: Runtime-Bindings
  2. Kokoro-82M Modellkarte
  3. Piper Text-to-Speech
  4. Bark von Suno
  5. FFmpeg AAC-Encoder-Dokumentation
  6. torch.load weights_only Dokumentation
  7. ESpeak NG Phonemisierer

Häufige Fragen

Warum keine gehostete Text-to-Speech-API?

Weil bei 95 Artikeln die verrechneten Kosten bei jeder Änderung und jedem Neuaufbau zählen und weil Reproduzierbarkeit wichtiger ist: derselbe Text mit derselben Modellversion erzeugt nächstes Jahr dieselbe Datei. Gehostete Stimmen gewinnen weiterhin bei expressiver Spitze, aber nicht bei der Ökonomie eines ganzen Archivs.

Welches Modell klingt am besten?

Kokoro v1.0 — und es ist das ausgelieferte: 82 Millionen Parameter, natürliche Betonung, saubere Behandlung von Zahlen und Produktnamen. Piper mit der LibriTTS-Stimme ist knapp dahinter und deutlich schneller. Bark ist am expressivsten der drei, war aber für dieses Volumen zu langsam.

Wie lange dauert der komplette Lauf?

Etwa 35 Minuten für alle 95 Artikel auf einem Apple-Silicon-Laptop — rund 4,5-fache Echtzeit — mit weniger als 2 GB Spitzenauslastung. Ein einzelner Artikel wird in etwa 20 Sekunden neu erzeugt.

Warum AAC (M4A) statt MP3 oder Opus?

Bei 192 kbit/s sind die Codec-Unterschiede für Sprache unhörbar, deshalb entscheidet das Format: AAC läuft nativ überall, auch in Safari, faststart macht die Dauer sichtbar, bevor der Download endet, und Opus wäre kleiner, hat aber außerhalb von WebM ungleichmäßige Browserunterstützung.

Gibt es die Vertonung auch auf Deutsch und Ungarisch?

Noch nicht. Die Vertonung liest das englische Original jedes Artikels; die Player-Oberfläche selbst ist vollständig auf Deutsch, Englisch und Ungarisch lokalisiert.

Klingt nach dem, was du suchst?

Erzähl mir von deinem Projekt oder deiner Stelle – ich freue mich, von dir zu hören.