christianohle
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.

Quellen

Hat dir der Artikel geholfen? Teile ihn.

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.