CHRISTIAN OHLE
Zurück zu Bauen

Bauen

Visual-Orchestrator: 6 Sub-Agents rendern 14 Szenen parallel

Build-in-Public Teil 4: Wie der Visual-Orchestrator-Agent mit ThreadPoolExecutor 6 Sub-Agents koordiniert — parallel CPU/API, seriell GPU, mit Fallback-Ketten.

7 Min Lesezeit visual orchestrator agent · multi-agent thread pool · ki video parallel rendering · fal video agent · comfy renderer · multi-agent pipeline
Hero-Image: Visual-Orchestrator: 6 Sub-Agents rendern 14 Szenen parallel

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:

  1. Liest das validierte VideoScript ein (Pydantic-Validierung erneut, defensive)
  2. Entscheidet, welche Comfy-Szenen zu Fal-Video-Promotionen hochgestuft werden
  3. Trennt Szenen in GPU-Bound (ComfyUI) und CPU/API-Bound (alle anderen)
  4. Rendert die CPU/API-Szenen parallel mit ThreadPoolExecutor
  5. Rendert die GPU-Szenen danach seriell (ein ComfyUI-Job nach dem anderen)
  6. 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-AgentToolRessource
Manim-AgentManim CommunityCPU
Slide-AgentClaude + PlaywrightCPU
Comfy-AgentReplicate Flux SchnellAPI-bound
Fal-Video-Agentfal.ai Kling 2.5API-bound
Stock-AgentPexels APIAPI-bound
Code-AgentPlaywright + HighlightingCPU

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:

  1. Erstwahl: Was das Skript vorschlägt (z.B. Fal-Video mit Kling)
  2. Zweitwahl: Wenn Erstwahl scheitert, versuche eine Stufe einfacher (Comfy via Replicate)
  3. 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.

Meine Einschätzung

Ich halte den Visual-Orchestrator für den architektonisch interessantesten Agent der ganzen Pipeline — nicht weil er ein LLM nutzt (tut er nicht), sondern weil er zeigt, wie man Parallelisierung und Fallback-Ketten als Robustheits-Pattern einsetzt. In meinen eigenen Tests sind Pipelines ohne Fallbacks oft beim ersten API-Timeout komplett abgebrochen. Die Fallback-Kette Fal-zu-Comfy-zu-Slide hat mir in sechs Wochen Production-Betrieb genau null manuelle Eingriffe erspart — weil sie jeden Fehler selbst auffängt. Wer eine Pipeline baut, die unattended laufen soll, braucht dieses Pattern. Wer es nicht hat, steht morgens um sieben auf und drückt manuell auf Retry.

Quellen

Porträt von Christian Ohle

Geschrieben von

Christian Ohle

Builder · Schmied der christianohle

Seit 2005 mit dem Web. Online-Marketing, Coding, lokale KI. Schreibt auf christianohle über Agents, MCP, lokale LLMs und Workflow-Automation — alles selbst getestet. Wöchentlicher Newsletter mit aktuellen News & Tutorials.