Als der Agent 13 Minuten Sprache durch "Vielen Dank" ersetzte
Ein Whisper-Default kostete mich beim ersten echten E2E-Lauf 13 Minuten Transkript -- ersetzt durch einen Halluzinations-Loop. Die Diagnose, der Fix und vier übertragbare Lektionen.

TL;DR
Ich baue gerade einen lokalen, agentischen Video-Cutter: Claude im Terminal als Regisseur, lokale ASR und ffmpeg als Werkzeuge, ein EDL-JSON als Vertrag zwischen Entscheidung und Render. Beim ersten echten Durchlauf auf einem 58-Minuten-Live-Coding-Clip lief ein stiller Whisper-Default auf: condition_on_previous_text=True schickte das Modell in einen Halluzinations-Loop und ersetzte rund 13 Minuten echte Sprache durch ein wiederholtes "Vielen Dank". Im Demo-Material wäre mir das nie aufgefallen. Dieser Artikel ist die ehrliche Aufarbeitung: der Versuch, die Wand, die Diagnose, der Fix in vier Schichten und die Lektionen, die ich mitnehme.
Worum es geht
Seit Ende 2024 arbeite ich täglich mit KI-Coding-Agenten, und ein Teil davon ist, eigene Werkzeuge zu bauen, statt nur fremde zu konsumieren. Eines dieser Werkzeuge ist ein agentischer Video-Cutter: Ich werfe ein ungeschnittenes Talking-Head- oder Screencast-Video hinein, und am Ende kommt ein optimierter Schnitt heraus. Füllwörter raus, lange Pausen gekürzt, Versprecher entfernt. Orchestriert wird das nicht von einer starren Pipeline, sondern von Claude im Terminal als Regisseur. Die schwere Arbeit erledigen lokale Werkzeuge: ein Speech-to-Text-Modell für das Transkript, ffmpeg für den frame-genauen Schnitt.
Die Architektur dahinter ist bewusst entkoppelt. Zwischen der Entscheidung ("an dieser Stelle wird geschnitten") und dem Render ("ffmpeg setzt den Schnitt") liegt ein EDL-JSON als Vertrag. Das Transkript ist die Grundlage jeder Entscheidung. Wenn das Transkript falsch ist, ist alles danach falsch, egal wie sauber der Rest läuft. Genau hier ist mir der Boden weggebrochen.
Bis zu diesem Punkt hatte ich nur mit kurzen Test-Clips gearbeitet: ein, zwei Minuten, sauberes Audio, klare Sprache. Alles lief. Der Schnitt saß, die Captions stimmten. Ein Happy-Path-Demo, wie es im Buche steht. Der erste echte Stresstest war ein 58-Minuten-Mitschnitt einer Live-Coding-Session auf Deutsch.
Der Versuch
Der Standard-Ablauf ist eine Kette von Schritten, jeder mit einem klaren Output:
- Transkribieren: Audio rein, zeitgestempeltes Transkript-JSON raus.
- Transkript säubern: offensichtliche Artefakte entfernen.
- Schnitt-Entscheidung: aus Transkript und Stille-Erkennung das EDL-JSON ableiten.
- Pausen-Map reviewen, freigeben.
- Rendern und Captions exportieren.
Ich habe den 58-Minuten-Clip in Schritt 1 geschickt und nebenbei weitergearbeitet. Das lokale ASR-Modell brauchte ein paar Minuten, lieferte ein vollständiges JSON, kein Fehler, kein Crash, sauberer Exit-Code. Auf den ersten Blick: Erfolg. Genau das ist die Falle. Ein fehlerfreier Exit-Code sagt nur, dass der Prozess durchgelaufen ist, nicht dass das Ergebnis stimmt.
Die Wand
Aufgefallen ist es mir erst beim Review der Pausen-Map. Der Schnitt war an einer Stelle absurd: ein riesiger zusammenhängender Block, der gekürzt werden sollte, mitten in einem Abschnitt, in dem ich nachweislich durchgehend geredet hatte. Ich bin zurück ins Transkript-JSON und habe nachgesehen.
Dort stand, etwa ab Minute 30, über mehrere hundert Segmente hinweg, immer wieder dieselbe Zeile:
{ "start": 1812.4, "end": 1814.9, "text": " Vielen Dank." }
{ "start": 1814.9, "end": 1817.2, "text": " Vielen Dank." }
{ "start": 1817.2, "end": 1819.8, "text": " Vielen Dank." }
Rund 13 Minuten echte Sprache, in denen ich live Code erklärt habe, waren im Transkript komplett verschwunden und durch einen wiederholten "Vielen Dank"-Loop ersetzt. Das Audio war einwandfrei, ich konnte es abspielen und meine eigene Stimme klar hören. Das Transkript hingegen behauptete, ich hätte 13 Minuten lang nichts gesagt außer einer Höflichkeitsfloskel.
Ein fehlerfreier Exit-Code sagt nur, dass der Prozess durchgelaufen ist. Er sagt nichts darüber, ob das Ergebnis stimmt.
Das Tückische daran: Hätte der Schnitt-Algorithmus diesen Block einfach durchgewunken, wäre der Fehler vielleicht nie aufgefallen. 13 Minuten Inhalt wären lautlos aus dem fertigen Video verschwunden. Der absurde Schnitt war reines Glück, ein sichtbares Symptom eines stillen Datenschadens weiter oben in der Kette.
Die Diagnose
Das Muster war mir vertraut. Wer länger mit Whisper-basierten Modellen arbeitet, kennt den Halluzinations-Loop: Bei Stille, Musik, Hintergrundrauschen oder schlicht einer Passage, die das Modell nicht sauber dekodieren kann, fällt es in eine Endlos-Wiederholung. Im Deutschen ist "Vielen Dank" ein klassischer Kandidat, im Englischen oft "Thank you" oder "Thanks for watching", weil solche Phrasen im Trainingsmaterial (Untertitel von Videos) massiv überrepräsentiert sind. Das Modell rät nicht "nichts", es rät die wahrscheinlichste Floskel und klammert sich daran fest.
Die eigentliche Ursache ist ein Default-Parameter, der gut gemeint und für viele Fälle sogar sinnvoll ist:
# Whisper-Default -- der eigentliche Auslöser
result = model.transcribe(
audio,
condition_on_previous_text=True, # Default
)
condition_on_previous_text=True bedeutet: Das Modell bekommt für jedes neue Segment seinen eigenen, zuvor erzeugten Text als Kontext mit. Im Normalfall macht das die Transkription kohärenter, Eigennamen und Terminologie bleiben konsistent. Aber es koppelt das Modell auch an seine eigenen Fehler. Sobald es einmal "Vielen Dank" produziert hat, wird genau dieser Text zum Kontext für das nächste Segment. Und der wahrscheinlichste nächste Text nach "Vielen Dank" ist wieder "Vielen Dank". Ein Rückkopplungsschleife, die sich selbst verstärkt, bis das Modell nicht mehr herausfindet.
Drei Dinge musste ich ausschließen, um sicher zu sein, dass das die Ursache war:
- Defektes Audio? Nein. Abgespielt, klare Sprache, kein Rauschen, keine Stille an der Stelle.
- Falsches Modell oder Sprache? Nein. Die ersten 30 Minuten waren sauber transkribiert, derselbe Lauf, dasselbe Modell.
- Ein Bug in meinem Säuberungs-Schritt? Nein. Der Loop stand schon im rohen ASR-Output, bevor mein eigener Code ihn überhaupt anfasste.
Damit war klar: Der Schaden entstand direkt im Transkriptions-Schritt, ausgelöst durch den Kontext-Default. Vermutlich gab es an dieser Stelle im Audio eine kurze Passage mit leiserer oder undeutlicher Sprache, die den ersten Fehlgriff provoziert hat. Den Rest hat die Kopplung erledigt.
Der Fix in vier Schichten
Die naheliegende Antwort ist, condition_on_previous_text auf False zu setzen. Das habe ich getan, aber es allein reicht nicht, und es hat einen Preis. Die Lösung, die ich übernommen habe, stammt im Kern aus einem früheren Projekt von mir, einem macOS-Push-to-Talk-Werkzeug mit lokaler Spracherkennung. Dort hatte ich gegen exakt dasselbe Problem schon eine mehrschichtige Verteidigung gebaut. Vier Schichten, weil keine einzelne davon den ganzen Fehlerraum abdeckt.
Schicht 1: Die Kopplung kappen. Der Default wandert auf False. Damit kann ein einzelner Fehlgriff sich nicht mehr über den eigenen Kontext fortpflanzen. Jedes Segment wird unabhängig dekodiert.
# Fix Schicht 1 -- Kopplung an eigene Fehler kappen
result = model.transcribe(
audio,
condition_on_previous_text=False,
)
Schicht 2: Den Loop nachträglich erkennen und entfernen. cond=False verhindert die Ausbreitung, aber nicht jeden einzelnen Fehlgriff. Also läuft nach der Transkription ein deterministischer Cleaner über das JSON, der wiederholte identische Segmente als Loop erkennt und kollabiert. Kein Modell, reine Logik: Wenn dieselbe kurze Phrase N-mal hintereinander mit lückenlosen Zeitstempeln auftaucht, ist das keine Sprache, das ist ein Artefakt.
# Fix Schicht 2 -- den Loop deterministisch kollabieren (vereinfacht)
def collapse_loops(segments, min_repeats=4):
cleaned, run = [], []
def flush(run):
# erkannten Loop auf ein Vorkommen kollabieren, sonst voll uebernehmen
return run[:1] if len(run) >= min_repeats else run
for seg in segments:
text = seg["text"].strip()
if run and text == run[-1]["text"].strip():
run.append(seg)
continue
cleaned.extend(flush(run))
run = [seg]
cleaned.extend(flush(run)) # letzten Lauf nicht vergessen
return cleaned
Schicht 3: Stille gar nicht erst dekodieren. Ein optionales Voice-Activity-Gate erkennt vorab, wo überhaupt Sprache ist, und schickt nur diese Passagen ins Modell. Stille kann dann gar nicht erst zur Halluzination werden. Das ist die sauberste Vorbeugung, aber ich halte sie opt-in, weil ein zu aggressives Gate echte leise Sprache verwerfen kann.
Schicht 4: Confidence sichtbar machen. Das Modell liefert pro Wort Wahrscheinlichkeiten. Ein Halluzinations-Loop hat oft ein charakteristisches Confidence-Muster. Diese Werte mitzuführen, statt sie wegzuwerfen, gibt eine spätere Handhabe, verdächtige Passagen automatisch zu markieren, statt sie zu glauben.
Wichtig ist die Reihenfolge: keine einzelne Schicht ist die Lösung. Schicht 1 stoppt die Ausbreitung, Schicht 2 räumt die Reste weg, Schicht 3 verhindert den Auslöser, Schicht 4 macht das Problem messbar. Zusammen sind sie robust, einzeln sind sie alle lückenhaft.
Ein ehrlicher Tradeoff bleibt: cond=False macht das Transkript an manchen Stellen weniger glatt, und es wirft tatsächlich gesprochene Füllwörter wie "ähm" eher aus dem Text. Für einen Cutter, der Füllwörter ohnehin entfernen will, klingt das praktisch, aber es heißt, dass ich die Füllwort-Erkennung jetzt auf das Audio stützen muss statt auf das Transkript. Ein gelöstes Problem hat ein neues geöffnet. So ist das fast immer.
Was ich daraus mitnehme
Vier Lektionen, die ich für übertragbar halte, weit über diesen einen Bug hinaus.
1. Halluzination fällt erst im echten Material auf
Mein Demo-Clip war zwei Minuten sauberes Audio. Er hätte mir diesen Fehler nie gezeigt, weil er die Bedingungen, die ihn auslösen, gar nicht enthielt: keine längeren undeutlichen Passagen, keine Stelle, an der das Modell ins Raten kommt. Happy-Path-Demos testen, ob die Mechanik läuft, nicht ob das Ergebnis bei realen, unordentlichen Eingaben standhält. Der erste echte 58-Minuten-Lauf war kein netter Zusatztest, er war der eigentliche Test.
2. Ein sauberer Exit-Code ist kein Korrektheits-Beweis
Der Transkriptions-Schritt lief ohne Fehler durch und lieferte ein wohlgeformtes JSON. Auf Prozessebene war alles grün. Der Inhalt war trotzdem zu großen Teilen Müll. Bei generativen Komponenten reicht "ist nicht abgestürzt" nicht als Erfolgskriterium. Man braucht eine inhaltliche Prüfung: Plausibilitäts-Checks, Confidence-Schwellen, oder einen Menschen, der vor der Freigabe wirklich hinsieht.
3. Stille Defaults sind die gefährlichsten
condition_on_previous_text=True ist kein Bug, es ist eine bewusste, dokumentierte Voreinstellung, die in vielen Fällen das Richtige tut. Aber sie ist unsichtbar, solange man sie nicht kennt, und ihr Versagen ist lautlos. Die teuersten Fehler kommen selten von etwas, das offensichtlich kaputt ist. Sie kommen von einem vernünftigen Default, der in der eigenen, leicht abweichenden Situation die falsche Wahl ist.
4. Verteidigung gehört an den Vertrag, nicht ans Ende
Weil das Transkript der Vertrag für alles Nachfolgende ist, musste die Absicherung genau dort sitzen, nicht erst im fertigen Video. Den Loop am EDL-JSON abzufangen wäre zu spät gewesen, der Schaden wäre schon eingebaut. Wer mit Pipelines aus mehreren Schritten arbeitet, sollte die Prüfung dort einsetzen, wo die Daten entstehen, nicht dort, wo sie auffallen. Die gleiche Haltung beschreibe ich im Harness-Design für KI-Coding-Agenten: nicht dem Output blind vertrauen, sondern dem Werkzeug die Mittel geben, sich selbst zu prüfen.
Fazit
Der Reihe nach, was diesen Fehler ausgemacht hat:
- Der Auslöser: ein gut gemeinter Whisper-Default,
condition_on_previous_text=True, der das Modell an seine eigenen Fehler koppelt. - Der Schaden: rund 13 Minuten echte Sprache, ersetzt durch einen "Vielen Dank"-Loop, still und ohne jeden Fehler im Prozess.
- Die Sichtbarkeit: aufgefallen nur durch Zufall, ein absurder Schnitt als Symptom eines Datenschadens weiter oben.
- Der Fix: vier Schichten, von der gekappten Kopplung über den deterministischen Loop-Cleaner und das opt-in Voice-Gate bis zur mitgeführten Confidence.
- Der Preis: weniger Glättung, Füllwort-Erkennung wandert vom Text aufs Audio. Ein gelöstes Problem öffnet das nächste.
Lokale Spracherkennung ist kein Hexenwerk, aber sie ist auch nicht magisch. Sie ist ein Werkzeug mit klaren Schwächen, und wer es ernsthaft einsetzt, muss diese Schwächen kennen und absichern. Wer denselben Loop schon einmal gesehen hat, in welcher Sprache auch immer, oder eine andere Verteidigung gebaut hat: schreiben Sie mir, ich sammle solche Muster. Wen die Methodik hinter solchen Prüf-Loops interessiert, der findet in der offenen Ressourcen-Bibliothek auf agenticbuilders.at/ressourcen das Material dazu. Und wenn Sie an einem eigenen Aufbau dieser Art arbeiten und einen Sparringspartner suchen: ich arbeite bewusst nur mit ein bis zwei Kunden gleichzeitig, hier erreichen Sie mich.
Übrigens: Dass der Cutter ein lokales Modell als Werkzeug nutzt statt eines Cloud-Dienstes, hat denselben Hintergrund wie mein gescheiterter Versuch, ein Coding-Modell lokal zu betreiben. Lokal heißt Kontrolle, aber auch: Man trägt jede Schwäche des Modells selbst.