TL;DR — Was du nach diesem Artikel weißt
- Wie der Assembly-Agent alle vorigen Agent-Outputs zu einem Master-MP4 verbindet.
- Den Per-Szenen-Merge-Code mit
-stream_loop -1für Visual-Looping bei kürzeren Visuals.- Den ffmpeg-Concat-Demuxer und warum Format-Konsistenz vorausgesetzt ist.
- Wie Sidechain-Ducking als simple Volume-Reduktion fast identisch klingt.
- Warum FPS-Normalisierung der unterschätzte Bug-Killer der Video-Pipeline ist.
Im vorherigen Teil habe ich Voice- und Subtitle-Agent gezeigt — die letzten beiden Agents, die individuelle Daten produzieren. Heute der Assembly-Agent: der finale Konsolidator, der alle vorherigen Outputs einsammelt und zu einem fertigen 6-Min-MP4 zusammenfügt. Per-Szenen-Merge, Concat-Demuxer, BGM-Mix mit Sidechain-Ducking — alles klassisches ffmpeg-Handwerk, aber präzise konfiguriert.
Was diesen Agent besonders macht: er ruft kein LLM auf. Er ist rein deterministisch, baut auf Stream-Format-Konventionen, und ist trotzdem essenziell. Multi-Agent-Pipelines brauchen nicht jede Stage als LLM-Call — manche Stages sind einfach gut konfigurierte Tools, die Inputs zuverlässig in Outputs verwandeln.
“Der Assembly-Agent war der Agent, an dem ich am meisten ffmpeg-Lessons gelernt habe. Drei Tage Bugs für unsynchronisiertes Audio, doppelt so lange Videos, schwarze Frames in der Mitte. Heute läuft er stabil — aber jede einzelne Zeile des ffmpeg-Codes hat eine Bug-Geschichte hinter sich.”
Was der Assembly-Agent konkret tut
Eingang: für jede der 14–20 Szenen ein Visual-MP4 (vom Visual-Orchestrator, ggf. mit B-Roll-Overlay) plus eine Voiceover-MP3 (vom Voice-Agent). Ausgang: data/output/<topic_id>.mp4 — das fertige Video.
Im Assembly-Code:
"""Assembly: nimmt Visual-Clips + Voiceover-Chunks + BG-Music → finales MP4.
Workflow:
1. Pro Szene: Visual-Clip + Voiceover-MP3 → ein Clip mit eingebettetem Audio
2. Alle Szenen-Clips concat → master.mp4
3. BG-Music drüberlegen mit Sidechain-Ducking
4. Auto-Subtitles via Whisper
5. Burn-In via ffmpeg
"""
Vier Sub-Stages, jede mit eigenem ffmpeg-Aufruf. Lass mich sie nacheinander durchgehen.
Sub-Stage 1: Per-Szenen-Merge
Erste Aufgabe: jede Szene wird zu einem Clip mit eingebettetem Audio. Ein Visual-MP4 + ein Voice-MP3 → ein gemerged-MP4.
def merge_scene(visual: Path, audio: Path, output: Path) -> Path:
"""Visual-Clip + Voiceover → ein MP4. Visual wird ggf. auf Audio-Länge gestreckt/geloopt."""
audio_dur = _probe_duration(audio)
cmd = [
"ffmpeg", "-y",
"-stream_loop", "-1", "-i", str(visual), # Loop falls Visual kürzer
"-i", str(audio),
"-t", f"{audio_dur:.3f}",
"-c:v", "libx264", "-preset", "veryfast",
"-r", "30",
"-pix_fmt", "yuv420p",
"-vf", "fps=30",
"-c:a", "aac", "-b:a", "192k", "-ar", "48000",
"-map", "0:v:0", "-map", "1:a:0",
"-shortest",
str(output),
]
_run(cmd)
return output
Das Schlüssel-Detail hier: -stream_loop -1 auf dem Visual-Input. Wenn das Visual kürzer ist als das Audio (passiert bei Stock-Clips oder Manim-Animationen mit fester Länge), wird das Visual stumm geloopt, bis das Audio durch ist. -shortest auf dem Output-Side stellt sicher, dass am Ende dort gestoppt wird, wo das Audio endet — keine endlosen Visual-Loops nach Ende der Narration.
Zwei weitere harte Regeln:
-r 30und-vf fps=30: alle Clips werden auf 30 FPS normalisiert. Das ist die Voraussetzung für den späteren Concat-Demuxer.-pix_fmt yuv420p: standardisiertes Pixel-Format, kompatibel mit allen Browsern und YouTube-Encoder.-ar 48000: standardisierte Audio-Sample-Rate.
“FPS-Normalisierung kostete mich am ersten Tag drei Stunden Bug-Suche. Manim rendert mit 60 FPS, ComfyUI mit 30, Slide mit 25. Der Concat-Demuxer crashed mit kryptischer Fehlermeldung ‘concat: Different streams’. Lösung: alles auf 30 FPS via
-vf fps=30umrechnen, dann passt der Concat. Heute eine Zeile im Code, damals drei Stunden Frust.”
Sub-Stage 2: Concat-Demuxer
Sind alle Szenen-Clips gemergt, kommt der Concat. Hier wird der Concat-Demuxer genutzt — nicht der einfacher zu nutzende concat-Filter — weil er format-agnostisch ist und keine Re-Encodierung braucht (wenn die Stream-Formate konsistent sind).
def concat_clips(clips: list[Path], output: Path) -> Path:
"""ffmpeg concat-demuxer (sicher, keine Re-Encodierung wenn Format passt)."""
list_file = output.with_suffix(".txt")
list_file.write_text(
"\n".join(f"file '{c.absolute()}'" for c in clips),
encoding="utf-8",
)
cmd = [
"ffmpeg", "-y", "-f", "concat", "-safe", "0",
"-i", str(list_file),
"-c:v", "libx264", "-preset", "veryfast", "-crf", "20",
"-c:a", "aac", "-b:a", "192k",
str(output),
]
_run(cmd)
list_file.unlink(missing_ok=True)
return output
Wie der Concat-Demuxer arbeitet:
- Eine Text-Datei wird geschrieben, mit einer Zeile pro Clip:
file '/path/to/clip_001.mp4' - ffmpeg liest diese Datei mit
-f concat -i list.txt - Alle Clips werden in der Reihenfolge der Liste verkettet
Das ist deutlich robuster als der concat-Filter, weil:
- Der Demuxer toleriert leichte Format-Schwankungen (mit
-safe 0) - Er muss Streams nur dann re-encoden, wenn die Specs nicht passen
- Er kann auch ohne Re-Encoding laufen mit
-c copy— bei meinem Setup re-encode ich aber bewusst, weil meine Quell-Clips manchmal Bitrate-Outliers haben
“
-safe 0ist der Trick, der den Concat-Demuxer mit absoluten Pfaden funktionieren lässt. Ohne ihn lehnt ffmpeg jeden Pfad ab, der nicht relativ zur Listen-Datei ist. Drei Stunden Doku-Wühlen, bis ich das gefunden hatte.”
Sub-Stage 3: BGM-Mix mit Sidechain-Ducking
Jetzt kommt der finale Schritt: Background-Music unter den Voiceover legen. Echte Sidechain-Compression wäre die richtige Lösung — die BGM wird automatisch leiser, sobald die Voice-Spur Pegel hat. ffmpeg unterstützt das mit sidechaincompress-Filter, aber:
- Es ist langsamer (zwei Audio-Streams analysieren statt einer)
- Die Parameter (
threshold,ratio,attack,release) brauchen sorgfältiges Tuning - Der Effekt im YouTube-Kontext ist marginal hörbar
Stattdessen: Konstantes Volume-Reduktion der BGM um -22 dB. Klingt im Kontext fast identisch wie echtes Ducking.
def mix_bgm(video: Path, output: Path, bgm_path: Path | None = None,
bgm_db: int = -22) -> Path:
"""Mischt BG-Music dezent unter den Voiceover.
Sidechain-Compression simuliert via Volume-Reduktion."""
cmd = [
"ffmpeg", "-y",
"-i", str(video),
"-stream_loop", "-1", "-i", str(bgm_path),
"-filter_complex",
f"[1:a]volume={bgm_db}dB[bgm];"
f"[0:a][bgm]amix=inputs=2:duration=first:dropout_transition=2[a]",
"-map", "0:v", "-map", "[a]",
"-c:v", "copy",
"-c:a", "aac", "-b:a", "192k",
str(output),
]
_run(cmd)
return output
Das Filter-Komplex erklärt:
[1:a]volume=-22dB[bgm]: BGM (Stream 1) wird um 22 dB leiser[0:a][bgm]amix=inputs=2:duration=first:dropout_transition=2[a]: Voice (Stream 0) und gedämpfte BGM werden gemischt, Output endet wenn Voice endet (duration=first)-c:v copy: Video-Stream wird nicht re-encodet — spart ~30 Sekunden Render-Zeit
“-22 dB ist der Sweet-Spot. -18 dB ist zu laut, BGM überdeckt Voiceover bei leiseren Passagen. -25 dB ist zu leise, BGM wird unhörbar in Headphones. -22 dB hat YouTube-Standard, klingt ‘professionell’ ohne aufdringlich zu sein.”
Sub-Stage 4: Whisper-Transkription mit Re-Segmentierung
Sobald das Video mit BGM fertig ist, läuft Whisper über die Audio-Spur und erzeugt Untertitel. Das passiert am Master-MP4, nicht an den einzelnen Voice-Files — Whisper performt besser auf längeren zusammenhängenden Audio-Strecken.
def _generate_srt_only(video):
"""Whisper-Transkription, schreibt nur SRT (kein Burn-In)."""
import whisper
console.print("[cyan]Whisper-Transkription (nur SRT)...[/cyan]")
model = whisper.load_model("small")
result = model.transcribe(str(video), language="de",
task="transcribe", word_timestamps=True)
result["segments"] = _resegment_for_subtitles(
result.get("segments", []), max_words=7, max_duration=3.5)
srt_file = Path(video).with_suffix(".srt")
_write_srt(result["segments"], srt_file)
return srt_file
Beachte: Whisper “small” statt “large-v3” — bewusste Trade-off-Entscheidung. Für reine deutsche Untertitel-Generierung ist die Genauigkeit ausreichend, die Laufzeit halbiert sich. Bei englischen Skripten würde ich auf “medium” oder “large” hochskalieren.
Re-Segmentierung auf 7-Worte / 3,5 s ist im Voice+Subtitle-Artikel detailliert erklärt — der Code-Pfad ist derselbe, nur das Eingangs-Audio ist hier das Master-MP4.
Wie der Assembly-Agent Robust gegen Sub-Failures ist
Im Pipeline-Orchestrator:
# 4. Thumbnail (Replicate Hero + Brand-Template via Playwright)
console.print(f"[cyan]→ Thumbnail-Generation...[/]")
try:
thumb_r.render_thumbnail(script_path, thumb_dir)
except Exception as e:
console.print(f"[yellow]Thumbnail fehlgeschlagen (non-fatal): {e}[/]")
# 5. Metadata (3 Titel + Description + Kapitel)
console.print(f"[cyan]→ Metadata-Generation...[/]")
try:
meta_r.render_metadata(script_path, thumb_dir)
except Exception as e:
console.print(f"[yellow]Metadata fehlgeschlagen (non-fatal): {e}[/]")
Was nach dem Assembly-Agent läuft (Thumbnail + Metadata) ist non-fatal. Wenn Thumbnail-Generation crashed, läuft die Pipeline weiter — das fertige MP4 ist da, nur das Thumbnail muss manuell ergänzt werden. Das ist eine bewusste Architektur-Entscheidung: das Hauptprodukt (Video) darf nicht an Cherry-on-Top-Stages scheitern.
Der Assembly-Agent selbst ist hingegen kritisch — wenn er crashed, gibt es kein Master-MP4. Daher hier kein try/except um die ffmpeg-Aufrufe, sondern explizites RuntimeError, das den ganzen Run mit Exit-Code 2 beendet.
“Diese Trennung ‘kritisch vs. non-fatal’ ist eine Architektur-Lektion, die ich auch in Nicht-KI-Pipelines anwende. Welche Stage ist Pflicht, welche ist Bonus? Beim Bonus mit Try/Except umgehen, beim Pflicht crashen lassen. Das macht die Pipeline-Logik klarer.”
Was der Assembly-Agent kostet
- ffmpeg lokal: 0 € Cloud-Kosten, nur CPU-Zeit (~90 Sekunden für 6 Min Video)
- Whisper lokal: 0 €, ~90 Sekunden auf der GPU
Gesamt: 0 € pro Video. Der Assembly-Agent ist die billigste Stage der Pipeline. Cloud-Alternativen (z.B. AWS MediaConvert, Cloudflare Stream) wären 0,30–0,80 € pro Video — bei 12 Videos pro Monat 4–10 € extra für etwas, das mein Rechner sowieso kann.
Was als nächstes in der Serie kommt
Im letzten Teil der Build-in-Public-Serie kommt die Kosten-Analyse pro Agent — was kostet jede einzelne Stage, wo sind die Hebel zur Skalierung, was würde sich ändern bei 100 Videos pro Tag statt 3 pro Woche. Spoiler: einer der Agents kostet 60 % der Gesamt-Pipeline, und es ist nicht der, den die meisten erwarten würden.
Meine Einschätzung
Nicht jede Stage in einer KI-Pipeline braucht ein LLM. Der Assembly-Agent ist mein bestes Beispiel dafür — er ist rein deterministisch, ruft kein Sprachmodell auf und ist trotzdem einer der wertvollsten Bausteine der gesamten Kette. In meinen eigenen Tests war oft genau das der Unterschied: Nicht alles mit LLM-Calls erschlagen, wenn ein gut konfiguriertes ffmpeg-Skript stabiler, schneller und billiger ist. Die echte Kunst beim Pipeline-Bau ist zu wissen, wo KI den Unterschied macht und wo klassisches Engineering reicht.
Quellen
- [ffmpeg c


