TL;DR — Was du nach diesem Artikel weißt
- Wie der Visual-Orchestrator-Agent das Video-Skript an 6 Sub-Agents delegiert.
- Warum die christianohle-Pipeline CPU/API parallel und GPU seriell rendert.
- Den ThreadPoolExecutor-Code aus dem Repo, der das macht.
- Wie die Fallback-Kette Pipeline-Crashes verhindert (Fal → Comfy → Slide).
- Welche Cache-Strategie 4-Minuten-Recoveries nach Fehlern ermöglicht.
Das script.json aus dem Script-Generator-Agent liefert 14–20 Szenen mit klaren Visual-Types. Was jetzt passieren muss: jede Szene wird zu einem MP4-File. Manche brauchen mathematische Animationen, manche atmosphärische Hero-Bilder, manche typografisch saubere Slides, manche Image-to-Video mit Kling-AI.
Genau das ist die Aufgabe des Visual-Orchestrator-Agents. Er ist nicht selbst ein LLM-Call — er ist ein Python-Koordinator, der pro Szene den richtigen Sub-Agent aufruft, parallelisiert wo es geht, serialisiert wo es muss, und Fallback-Ketten aufbaut für den Fall, dass ein Sub-Agent crashed.
“Der Visual-Orchestrator ist der Agent, an dem ich die meiste Zeit gebaut habe. Beim ersten Versuch lief alles seriell — 14 Szenen, ~2 Min Render-Zeit pro Szene, ~28 Min total. Mit ThreadPoolExecutor und der CPU/GPU-Trennung bin ich runter auf ~6 Min für dieselben 14 Szenen. Faktor 4–5, nur durch saubere Architektur.”
Was der Visual-Orchestrator-Agent konkret tut
Der Code-Eingang im Pipeline-Orchestrator:
def run(script_path: Path, parallel_visuals: int = 2,
use_fal_video: bool = True, upload: bool = True) -> Path:
ensure_dirs()
script = VideoScript.model_validate_json(script_path.read_text(encoding="utf-8"))
topic_id = script.topic_id
raw = RAW_DIR / topic_id
raw.mkdir(parents=True, exist_ok=True)
Was der Visual-Orchestrator dann macht:
- Liest das validierte
VideoScriptein (Pydantic-Validierung erneut, defensive) - Entscheidet, welche Comfy-Szenen zu Fal-Video-Promotionen hochgestuft werden
- Trennt Szenen in GPU-Bound (ComfyUI) und CPU/API-Bound (alle anderen)
- Rendert die CPU/API-Szenen parallel mit ThreadPoolExecutor
- Rendert die GPU-Szenen danach seriell (ein ComfyUI-Job nach dem anderen)
- Reicht die Visual-Paths an den nächsten Agent (Voice-Agent) weiter
Hier sind die 6 Sub-Agents, die der Orchestrator unter sich hat:
| Sub-Agent | Tool | Ressource |
|---|---|---|
| Manim-Agent | Manim Community | CPU |
| Slide-Agent | Claude + Playwright | CPU |
| Comfy-Agent | Replicate Flux Schnell | API-bound |
| Fal-Video-Agent | fal.ai Kling 2.5 | API-bound |
| Stock-Agent | Pexels API | API-bound |
| Code-Agent | Playwright + Highlighting | CPU |
Jeder ist eigenständig — eigene Module, eigene Tools, eigene Cache-Verzeichnisse.
Die Fal-Promotion-Logik
Bevor parallel gerendert wird, läuft eine Vor-Stage: welche der comfy-Szenen werden zu Fal-Video-Promotionen hochgestuft? Kling 2.5 erzeugt aus einem Bild ein animiertes 5-Sekunden-Video — sehr beeindruckend, aber 0,80 € pro Szene. Bei 6 Comfy-Szenen wären das 4,80 €/Video, also musste eine Auswahl-Heuristik her:
FAL_MAX_PROMOTIONS = 3
FAL_ACTION_VERBS = {
"zoom", "pan", "rotate", "fly", "soar", "dive", "rush", "race",
"explode", "burst", "glow", "pulse", "shimmer", "flicker", "cascade",
...
}
def _fal_complexity_score(scene: Scene) -> int:
"""Heuristik fuer Prompt-Komplexitaet: Wortanzahl + Action-Verb-Bonus."""
text = (scene.visual_prompt or "")
if scene.motion_hint:
text += " " + scene.motion_hint
words = text.lower().split()
word_count = len(words)
action_hits = sum(1 for w in words if w.strip(".,;:!?") in FAL_ACTION_VERBS)
return word_count + 3 * action_hits
Die Logik: erste Comfy-Szene wird immer zu Fal hochgestuft (visueller Anker am Anfang), dann werden die nächsten 2 nach Komplexitäts-Score ausgewählt — Wortanzahl plus 3× Bonus pro Action-Verb. Hard-Cap bei 3 Promotionen pro Video.
Das hat einen schönen Effekt: nur die Szenen mit echtem Motion-Potenzial bekommen die teure Kling-Behandlung. Statische “person at desk”-Hero-Bilder bleiben auf günstigem Replicate Flux. Das spart pro Video durchschnittlich 1,60 € gegenüber “alle Comfy-Szenen via Fal”.
“Diese Action-Verb-Heuristik war eine 30-Min-Lösung, die mich seit 6 Wochen 1,60 € pro Video spart. Insgesamt also ~80 € seit ich sie gebaut habe. Nicht jede Architektur-Entscheidung muss elegant sein — manchmal ist eine simple Regex die richtige Antwort.”
Parallel CPU/API, seriell GPU
Hier ist die zentrale Architektur-Entscheidung:
gpu_scenes = [s for s in script.scenes if s.visual_type == "comfy" and s.scene_id not in fal_ids]
other_scenes = [s for s in script.scenes if s.visual_type != "comfy" or s.scene_id in fal_ids]
console.print(f"[cyan]→ Rendering {len(other_scenes)} CPU/API-Visuals parallel ({parallel_visuals} workers)…[/]")
with ThreadPoolExecutor(max_workers=parallel_visuals) as ex:
futures = {ex.submit(_render_scene, s, raw, fal_ids): s for s in other_scenes}
for fut in as_completed(futures):
s = futures[fut]
try:
visual_paths[s.scene_id] = fut.result()
except Exception as e:
...
if gpu_scenes:
console.print(f"[cyan]→ Rendering {len(gpu_scenes)} GPU-Visuals seriell (ComfyUI)…[/]")
for s in gpu_scenes:
visual_paths[s.scene_id] = _render_scene(s, raw, fal_ids)
Was hier passiert:
- Phase 1 (parallel): alle Manim-, Slide-, Stock-, Code- und Fal-Video-Szenen laufen gleichzeitig. Default 2 Worker — das ist genug, um Replicate- und Fal-Rate-Limits nicht zu treffen.
- Phase 2 (seriell): alle Comfy-Szenen, die NICHT zu Fal hochgestuft wurden, laufen nacheinander auf ComfyUI lokal.
Warum die Trennung? GPU ist ein Single-Resource. Mein AMD Radeon RX 7900 XTX hat 24 GB VRAM. ComfyUI mit Flux Schnell nutzt davon ~16 GB. Würde ich zwei ComfyUI-Jobs parallel starten, wäre OOM (Out-Of-Memory) garantiert — der Treiber lehnt das ab oder crashed gleich das ganze System.
CPU/API-Renderer haben dieses Problem nicht. Manim rendert auf CPU (langsam, aber parallel-tauglich), Replicate-Calls sind nur HTTP-Anfragen (skalieren über Workers).
“Ich hab am Anfang versucht, alles parallel zu jagen. Erstes Resultat: ComfyUI hat den Treiber gecrashed, und Windows wollte einen Reboot. Zweites Resultat: nach dem Reboot dieselbe Crash-Sequenz. Dritter Versuch: GPU-Szenen seriell. Seitdem keine OOM-Crashes mehr.”
Die Fallback-Kette als Robustheits-Pattern
Was eine produktionsreife Multi-Agent-Pipeline ausmacht: jeder Sub-Agent kann scheitern, ohne den Daily-Run zu killen. Im Pipeline-Code:
except Exception as e:
renderer_label = "fal_video" if s.scene_id in fal_ids else s.visual_type
console.print(f"[red]Scene {s.scene_id} ({renderer_label}) failed: {e}[/]")
# Fallback-Kette: fal.ai → comfy (Replicate) → slide
if s.scene_id in fal_ids:
console.print(f"[yellow] Fallback fal→comfy fuer Scene {s.scene_id}[/]")
try:
visual_paths[s.scene_id] = comfy_r.render(
s.visual_prompt, s.motion_hint, s.duration_sec,
raw / "comfy", s.scene_id,
)
continue
except Exception as e2:
console.print(f"[red] comfy-Fallback auch failed: {e2}[/]")
console.print(f"[yellow] Fallback auf Slide-Renderer fuer Scene {s.scene_id}[/]")
fallback_sub = raw / "slide"
visual_paths[s.scene_id] = slide_r.render(
s.visual_prompt, s.duration_sec, fallback_sub, s.scene_id
)
Drei Stufen:
- Erstwahl: Was das Skript vorschlägt (z.B. Fal-Video mit Kling)
- Zweitwahl: Wenn Erstwahl scheitert, versuche eine Stufe einfacher (Comfy via Replicate)
- Letztwahl: Wenn auch das scheitert, nutze Slide — der wird immer funktionieren
Slide ist die Reißleine, weil er nur HTML+Playwright nutzt. Keine externen APIs, keine GPU. Wenn Slide nicht funktioniert, ist die Maschine kaputt — dann ist Pipeline-Recovery sowieso unmöglich.
Cache-Strategie für 4-Minuten-Recoveries
Was den Visual-Orchestrator wirklich produktionsreif macht: Cache-Hits. Wenn ein Daily-Run mittendrin scheitert, soll der nächste Versuch nur die fehlenden Szenen neu rendern, nicht alle 14.
sub = raw_dir / scene.visual_type
cached = sub / f"scene_{scene.scene_id:03d}.mp4"
if cached.exists() and cached.stat().st_size > 1024:
console.print(f"[dim]Cache hit: scene_{scene.scene_id:03d} ({scene.visual_type})[/]")
return cached
Das Cache-Schema ist filesystem-basiert: data/raw/<topic_id>/<visual_type>/scene_NNN.mp4. Wenn die Datei existiert und größer als 1024 Bytes ist (eine 0-Byte-Datei wäre ein abgebrochener Render-Job), wird sie wiederverwendet.
In der Praxis bedeutet das: ein Pipeline-Run, der bei Szene 11 von 14 crashed (z.B. wegen ElevenLabs-Rate-Limit beim Voice-Agent), kann am nächsten Morgen mit python -m src.pipeline --script <pfad> neu gestartet werden. Die Szenen 1–10 sind gecached und in unter 1 Sekunde wiederverwendet, nur die letzten 4 werden neu gerendert. Recovery in 4 Min statt 28 Min.
“Diese Cache-Logik kostete mich am ersten Tag 3 Stunden Debugging — weil ich die 0-Byte-Schwelle vergessen hatte. Eine ComfyUI-Crash-Datei mit 0 Bytes wurde als ‘cached’ behandelt und nicht neu gerendert. Erst nach dem fünften Mal manuell deleten kam ich darauf, die
> 1024-Bedingung einzubauen. Heute: drei Zeilen Code, die mir jede Woche Stunden sparen.”
Wie der Visual-Orchestrator mit dem Voice-Agent verzahnt ist
Sobald alle Visuals gerendert sind, läuft der Voice-Agent (ElevenLabs Turbo v2.5) seriell über alle Szenen:
# 2. Voiceover (parallel zu Visuals theoretisch, hier seriell für Einfachheit)
console.print(f"[cyan]→ ElevenLabs TTS für {len(script.scenes)} Szenen…[/]")
audio_dir = raw / "voiceover"
audio_paths = tts.synthesize_scenes(script.scenes, audio_dir)
audio_map = {s.scene_id: p for s, p in zip(script.scenes, audio_paths)}
Beachte: Voice könnte parallel zu Visuals laufen — beides ist API-bound. Habe ich bewusst nicht gemacht, weil:
- Voice-Errors brauchen unmittelbares Logging (nicht in einem Future-Wrapper versteckt)
- ElevenLabs hat strikte Rate-Limits (~3 parallele Calls)
- Der Zeitgewinn wäre marginal (~30 Sek bei einem 6-Min-Pipeline-Run)
Architektur-Entscheidungen sind nicht immer “alles maximieren” — manchmal ist “klar lesbar bleiben” der bessere Trade-off.
Was als nächstes in der Serie kommt
Im nächsten Teil zeige ich den B-Roll-Agent — einen schmalen Mini-Agent, der nach dem Visual-Orchestrator läuft und in qualifizierende Szenen (Comfy/Manim, ≥15s) Pexels-Cutaways einfügt. Spoiler: das ist der einzige Agent in der Pipeline, der seine eigene Mini-Claude-Instanz für Cutaway-Detection nutzt — und er ist mein Beweis dafür, dass auch sehr fokussierte Agents Sinn machen.
Quellen
- [Python ThreadPoolExecutor Docs](https://docs.pyth
Hat dir der Artikel geholfen? Teile ihn.


