Files
club-3090/docs/ai-studio/video.md
noonghunna 47ac38385a Director placement lever: CPU / GPU0 / GPU1 (c3 Settings) (#473)
* Director placement lever: env-driven CPU / GPU0 / GPU1 (backend)

The studio director's GPU/CPU placement is now a single lever,
STUDIO_DIRECTOR_DEVICE (gpu0 | gpu1 | cpu, default gpu0), read from the rig
.env by gpu-mode's start_studio_director and translated into the compose
env (-ngl + CUDA_VISIBLE_DEVICES + device_ids):

- gpu0 (default): -ngl 99, GPU0 — fast craft (~50-100 tok/s), ~4.6 GB,
  coexists with the image lanes. Unchanged from before.
- gpu1: -ngl 99, GPU1 — only when GPU1 has room (NOT during a video render;
  GPU1 is the DisTorch DiT donor).
- cpu: -ngl 0, CUDA_VISIBLE_DEVICES="" — frees ~4.6 GB off GPU0 (lifts the
  single-card Wan window 121→161 frames) at ~single-digit tok/s craft.

Compose now reads ${DIRECTOR_NGL:-99} + ${STUDIO_DIRECTOR_CUDA-0} (no-colon
so an explicit empty value = CPU survives). Default (no override) preserves
current GPU0 behaviour exactly.

Live-validated: CPU mode starts with GPU0 full (gemma12b), adds 0 MiB VRAM
to GPU0, serves on :8090, generates (~5 tok/s CPU). The c3 Settings field
that writes STUDIO_DIRECTOR_DEVICE follows in the next commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm

* c3 Settings: Director placement (CPU / GPU0 / GPU1) + CPU thread cap

Exposes the director-placement lever in the c3 Settings modal so users pick
GPU0 (fast, default) / GPU1 / CPU without hand-editing the .env.

- services.py: director_device() reads STUDIO_DIRECTOR_DEVICE from the repo
  .env (default gpu0, validates the value); set_repo_env_var() upserts a key
  in place (preserves other lines, no duplicates, creates the file if absent).
- app.py: SettingsScreen gains a "Director placement" Select; apply_settings
  persists the choice to the repo .env (the SHARED config gpu-mode reads —
  distinct from c3-settings.json for MODEL_DIR/HF_TOKEN). Applies on the next
  ai-studio start.
- compose: CPU thread cap — -t ${DIRECTOR_THREADS:-8} bounds CPU use so the
  director doesn't starve OWUI's embedder/reranker (also CPU). The ~2.6 GB
  GGUF loads into system RAM (mmap'd; resident in page cache, not run from SSD).
- tests: +6 data-layer (TestDirectorPlacement) + 1 headless apply-settings
  round-trip (persists STUDIO_DIRECTOR_DEVICE, idempotent re-apply). Full
  suite green (728), settings/director subset 13/13.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm

* tests: fix stale scene names in gpu-mode-list EXPECT

The scene catalog renamed its dispatch keywords to qwen27b / gemma-31b,
but the test's EXPECT spot-check map still referenced the old 27b / gemma
short names — so the JSON-shape assertion had been red on master. Point
EXPECT at the canonical names the catalog now emits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm

* studio: run director in chat scene as the catalog-support layer

Bring the uncensored director up in the `chat` scene (honoring the
STUDIO_DIRECTOR_DEVICE placement knob), framing chat as the supporting-
infra home — OWUI + LiteLLM + Qdrant + SearXNG + director — for ad-hoc
Catalog models launched via `switch.sh --owui`.

A CPU-placed director uses no GPU, so it's the always-on path: it survives
scene switches and stays live in OWUI. New _director_evict_if_gpu helper
frees only a GPU-resident director when a dual-card LLM scene claims the
cards; mode_off stops it outright. Also brings mode_gemma_int8 in line with
its dual-card siblings (it was missing the studio teardown entirely).

Docs: requirements.md gains a "Chat scene — the Catalog-support layer"
section + reframes director placement around the unified knob / c3 Setting;
video.md note synced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm

* studio: disable director thinking on CPU only (latency)

The uncensored director is an "Aggressive" reasoning fine-tune that emits
a full <think> trace before every answer. On GPU that's cheap and the
trace lands in reasoning_content (content stays clean), so leave it on.
On CPU (~14 tok/s) the trace dominates latency, so gpu-mode now passes
`--jinja --reasoning off` for the cpu placement only — forcing the
template's enable_thinking=false (this fine-tune ignores /no_think and
--reasoning-budget 0, but honors --reasoning off).

Wired via a new DIRECTOR_THINK_ARGS compose param (empty on GPU). Live:
CPU director now answers in one pass, no reasoning trace, craft quality
intact (full cinematic spec, finish=stop).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm

* c3: Containers-pane director start honors placement + enumerate sidecars

Two consistency fixes for the Containers tab:

1. Starting studio-director from Containers now honors STUDIO_DIRECTOR_DEVICE
   instead of falling back to the GPU0/thinking-on compose default. New
   director_compose_env() mirrors gpu-mode's start_studio_director translation
   (NGL/CUDA/GPU/THINK_ARGS), injected as an `env K=V …` prefix on the compose
   up cmd (process env wins over --env-file). cpu → -ngl 0 + --reasoning off.

2. The nested studio sidecars (director/gallery/orchestrator/image-shim/
   step-voice/tts) now enumerate when STOPPED, so they're startable rows — not
   only visible while running. New STUDIO_SIDECARS map is the single SoT for
   resolving the container-name → services/studio/<sub>/ project (fixing the
   director↔enhancer name mismatch that previously returned None → docker
   restart, which fails on a fresh install).

+8 tests (director_compose_env cpu/gpu, director resolves to enhancer with the
env prefix, sidecar enumeration). Live: c3 service_start plan starts the director
CPU + no-think (argv -ngl 0 --reasoning off, GPU0 free, clean generation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm

---------

Co-authored-by: noonghunna <10742901+noonghunna@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:01:46 +05:00

19 KiB
Raw Blame History

Video Studio — chat-driven text/image → video on 2× 3090

The video side of Club 3090 AI Studio. Type a rough idea in Open WebUI; a "director" LLM crafts it into a professional prompt; ComfyUI renders it. Four video lanes share the pipe + the director: LTX-2.3 (video+audio) and its uncensored fine-tunes Sulphur / 10Eros, plus Wan2.2-Rapid (a separate uncensored text→video engine). Video is GPU-mutually-exclusive with the dual-card LLMs (it wants both 3090s).

Sibling docs: image.md (HiDream-O1 / Ideogram-4 / Chroma / Z-Image stills) · audio.md (voices / music / SFX) · README.md (the overview + the full 11-lane matrix + shared services).


Architecture

                          Browser
                             │  "a 40-second drone shot over a coastline"
                             ▼
              ┌──────────────────────────────────────────────┐
              │  Open WebUI   :8080   (the front-end)         │
              │  video lanes: 🎬 LTX-2.3 · 🔓 Sulphur          │
              └───────┬────────────────────────────┬──────────┘
                  (1) │ craft the prompt        (2) │ render
                      ▼                             ▼
        ┌──────────────────────────┐   ┌───────────────────────────┐
        │ Director   :8090         │   │ ComfyUI        :8188       │
        │ qwen3.5-4b · llama.cpp   │   │ LTX-2.3 / Sulphur 22B GGUF │
        │ GPU0 · ~4.5 GB           │   │ DisTorch · BOTH 3090s      │
        │ casual idea → pro prompt │   │ → .mp4 (video + audio)     │
        └──────────────────────────┘   └─────────────┬─────────────┘
                                       long clip >15s │ (else straight to gallery)
                                                      ▼
                                        ┌───────────────────────────┐
                                        │ Orchestrator   :8190       │
                                        │ chain ~10s segments →      │
                                        │ one combined clip · no GPU │
                                        └─────────────┬─────────────┘
                                                      ▼
                                        ┌───────────────────────────┐
                                        │ Gallery   :8189            │
                                        │ nginx over /output —       │
                                        │ links survive ComfyUI down │
                                        └─────────────┬─────────────┘
                                                      ▼  ▶️ link back in chat
                                                   Browser   (reply "make it night" to refine)
  • Studio pipe (services/studio/build_studio_pipe.pystudio_pipe.py): the OWUI Function. Video lanes (LTX, Sulphur) × two modes (text→video, image→video, auto-detected from whether you attach an image). One director + one gallery across all lanes (image/audio too — see README).
  • Director (services/studio/enhancer/): a small uncensored LLM that turns a casual line into a cinematic spec. Optional — falls back to your raw prompt if it's down.
  • ComfyUI (services/comfyui/): the renderer. The 22B DiT is split across both cards by UnetLoaderGGUFDisTorch2MultiGPU (compute on GPU0, weights donated from GPU1).
  • Gallery (services/studio/gallery/): always-on nginx serving ComfyUI's output dir, so media
    • links stay alive even when ComfyUI is stopped.

Quickstart

No one-shot installer yet (the models are large + sourced separately) — three steps:

1. Get the models. Diffusion weights → /mnt/models/comfyui/models/... (see the Models manifest below); the director GGUF → /mnt/models/huggingface/qwen3.5-4b-gguf/....

2. Bring the stack up:

bash scripts/gpu-mode.sh ai-studio

Stops the GPU LLMs and starts ComfyUI (both cards) + director (:8090) + gallery (:8189) + orchestrator (:8190) + Open WebUI. gpu-mode off (or any LLM mode) tears the video model down again — it's GPU-mutex with the dual-card LLMs.

3. Install the pipe into Open WebUI (once):

python3 services/studio/build_studio_pipe.py     # writes services/studio/studio_pipe.py

In Open WebUI → Admin → Functions → +, paste services/studio/studio_pipe.py, save, enable. The Studio lanes appear in the model picker (see README.md for the full set). Set the pipe's browser_base valve to your host's LAN IP (http://<your-host>:8189) so the returned links open from your browser. Then open Open WebUIhttp://<your-host>:8080.

First run

  1. Create your account — the first signup becomes admin (no hardcoded secret; Open WebUI generates its own per deployment).
  2. Pick a video lane in the model selector — 🎬 LTX-2.3 (video + audio) or 🔓 Sulphur.
  3. Type a scene"a fox padding through a neon alley at night" — and send. The director crafts a cinematic prompt and it renders; you get a ▶️ link to the clip.
  4. Refine by replying ("more moody", "make it night"); for a long clip, include a duration ("a 40-second…") and it auto-chains segments into one combined video.

First render after a cold ComfyUI takes a few minutes (loads the 22B DiT + first-boot node deps). A 10 s clip is ~2.5 min warm; longer clips scale ~linearly per segment.

The UX: craft-and-go, refine anytime (no approval gate)

  1. You type something light — "a fox in the city".
  2. The director rewrites it into a full cinematographer's prompt (subject + action, camera/lens/movement, lighting + time of day, palette + mood, ambient sound) and it renders immediately — no "confirm?" step. The crafted prompt is shown above the video.
  3. Refine by just replying with the change — "more moody", "make it night", "slower camera". The pipe carries the previous prompt forward and the director evolves it. Or type a brand-new idea and it starts fresh — the director decides which.

Attach an image instead of (or with) text → it auto-routes to the image→video lane (animates your still).

How to prompt

You don't write the cinematic prompt — the director does. Give it the intent in a line or two; it fills in camera, lens, lighting, palette, mood, and ambient sound. If you do want specific control, just name it and the director keeps it — e.g. "…top-down drone shot, golden hour, melancholic".

  • Length — put a duration in the message: "a 30-second timelapse…", "make it 1 minute". No duration → ~10 s. Over 15 s auto-chains ~10 s segments into one clip (capped ~120 s); each segment adds ~2.5 min of render time.
  • Lane — pick the model: 🎬 LTX-2.3 (video + audio) or 🔓 Sulphur (uncensored).
  • Image → video — attach an image (optionally with a motion note like "slow zoom in, leaves drifting"); it animates your still.
  • Voiceover — add "voiceover: …" / "narration: '…'" / "say: …" and a Kokoro voice is mixed over the clip (ducked under the ambient, normalized). Details in audio.md.
  • Refine — just reply with the change. It evolves the last prompt; a brand-new idea starts fresh.

Works best: one clear subject + one continuous camera move or action; slow / cinematic / ambient scenes; a defined mood or time of day.

Weaker / avoid: fast or chaotic action (especially across long-clip segment joins — a cut has no motion carry-over); lots of on-screen text or logos; many distinct subjects or hard scene-cuts inside one segment; exact object counts.

Examples

  • "a hummingbird at a red flower, macro, soft morning light" → a clean ~10 s macro shot.
  • "a 40-second drone flight over a foggy coastline at dawn, slow push forward" → 4 chained segments → one combined ~40 s clip.
  • then "make it stormy, darker" → re-crafts from that and regenerates.

What it can generate

Modes text→video, image→video (attach an image) — LTX lanes; the Wan lane is text→video only
Audio yes — LTX-2.3 generates synced ambient audio; optional Kokoro voiceover (audio.md). Wan has no synced audio (add a Kokoro voiceover if you want sound).
Resolution Sulphur / 10Eros 1280×720 · LTX 768×512 · Wan 832×480 (set in the workflow)
Length default ~10 s (LTX lanes); Wan ~5 s (81 frames @16fps); see the ceiling below
Video lanes 🎬 Studio · Video (LTX-2.3) (video+audio) · 🔓 Studio · Video (Sulphur) · 🔓 Studio · Video (10Eros) · 🔓 Studio · Video (Wan2.2) (all uncensored except LTX) — image/audio lanes in image.md / audio.md, full matrix in README.md

Length ceiling (measured on 2× 3090, 1280×720, frames = 24·seconds + 1)

A frame sweep on the single-stage Sulphur lane:

Frames Length Result
121 ~5 s crisp
241 ~10 s crisp — the default
361 ~15 s coherent end-to-end, but visibly lower-energy/softer
481 ~20 s collapses — near-uniform/garbage frames the whole clip

So the pipe defaults to 241 (10 s) and is hard-capped at 361 (15 s): a 20 s single-pass silently corrupts (returns with no error, just unusable frames), so the cap prevents hitting it by accident. VRAM is not the limiter — the weights sit on GPU1 (~22 GB, fixed); longer clips only grow GPU0's latent (peaks ~14 GB). The wall is model coherence, not memory. Wall time scales ~linearly (~2.5 min at 10 s, ~6.5 min at 15 s).

Past ~15 s you extend/chunk: render segments ≤15 s, condition each on the previous segment's last frame, concatenate into one clip — see Longer videos below.

Longer videos (60 s+)

Past the ~15 s single-pass ceiling, the studio chains segments: segment 1 is text→video; each later segment is image→video conditioned on the previous segment's last frame; all are ffmpeg-concatenated into one clip. Validated on 2× 3090 — the joins are visually seamless (the last-frame conditioning carries the scene across each cut). Caveat: a single frame has no velocity, so fast action can show a brief motion reset at a cut; slow/ambient scenes are clean (native LTX temporal-extend would smooth fast cuts — future).

In chat (default): just ask for a length — "a 40-second drone shot over a coastline". The pipe parses the duration, the director crafts the prompt, and the orchestrator (services/studio/orchestrator/, :8190) chains ceil(seconds/10) ~10 s segments and returns one combined video (with live "segment k/N" progress). Capped at max_seconds (default 120 s = 12 segments; each ~2.5 min to render). If the orchestrator is down, it falls back to a single capped clip.

CLI (host): the same chain is also a standalone tool — python3 services/studio/extend_chain.py "<prompt>" <n_segments> <frames_per_seg>.

Why a separate orchestrator: the OWUI pipe can't run ffmpeg or read the output dir, so the segment chaining + concat live in a tiny host-side service (ffmpeg + output access, no GPU).

The single-stage rule

Sulphur is a fine-tune of LTX-2.3-dev. The "official" dev recipe is 2-stage (a spatial upscaler + a refine pass). On this hardware that 2-stage path renders a diamond-lattice mesh over every frame. The fix — and what the pipe ships — is single-stage: splice the distilled LoRA onto the base sampler, 8 steps, cfg 1, no upscaler. Clean output. The workflow (workflows/ltx_distilled_distorch.json) already encodes this.

Wan2.2 — tuning & limits (the separate uncensored T2V/I2V lane)

🔓 Studio · Video (Wan2.2) is a different engine from the LTX family — Wan2.2-Rapid-AllInOne Mega NSFW v10 (14B, Q8 GGUF, Apache). The "AllInOne" merge bakes a 4-step distill LoRA in, so it's single 4-step cfg=1 (no LoRA-splice, no 2-stage path). It does not produce synced audio, and it does not share LTX's i2v node or orchestrator — it has its own Wan-native i2v + chaining. Workflows: workflows/wan22_rapid.json (t2v) · workflows/wan22_rapid_i2v.json (i2v).

Sampler recipe (measured). The model card's recommendation for v10 is euler_ancestral / beta — and a same-prompt/same-seed sweep confirmed it: it's visibly sharper than the generic euler / simple (legible signage, defined reflections, more texture) at the same ~145 s and no extra VRAM. Shift 5 (ModelSamplingSD3), cfg 1, 4 steps. Raising steps doesn't help (distilled).

Resolution — 480p default, 720p valve. 832×480 is the default (~2.5 min/clip, single card). 1280×720 OOMs on the plain GGUF loader (the 18 GB model + 720p compute overflows one 3090) — so the wan_hi_res valve swaps in UnetLoaderGGUFDisTorch2MultiGPU (compute GPU0 / weights donated from GPU1, exactly like the LTX lanes) to fit it, at ~3.5× the time (~9 min/clip). 720p is visibly more detailed; it's opt-in because of the cost.

Length ceiling (measured on 2× 3090, 832×480, single window). The single-card ceiling is set by how much of GPU0 the diffusion gets — and the ~4.5 GB director sharing GPU0 is the swing factor (see "VRAM / GPU split" below). Quality holds at every length that fits; the wall is VRAM, not coherence:

Frames Length Single-card, director on GPU0 Single-card, director relocated DisTorch (both cards)
81 5.1 s 150 s (default) 156 s
121 7.6 s 408 s
161 10.1 s OOM 402 s 402 s
201 12.6 s OOM (untested) 576 s

Two reads of this: (1) the "161 OOM" is not a model limit — freeing the director's 4.5 GB off GPU0 lifts the single-card ceiling 121 → 161 frames; with the director resident it's ~121. (2) Render cost scales super-linearly (81→121 nearly tripled the time — attention is quadratic in sequence length). So the pipe defaults to 81 frames and never stretches the single window to go long — even where VRAM would allow it, it's the wrong cost curve.

Going long — i2v-seeded chaining (not a bigger window). Ask for >~5 s and the lane chains fixed-cost ~5 s segments: each later segment is i2v-seeded from the previous segment's last frame (ImageFromBatchWanImageToVideo start_image), its duplicate seam frame is dropped, and the segments are concatenated (ImageBatch) into one clip. Cost is linear in length (no OOM wall), and the seam is continuous (same subject/scene carry through). segments = ceil(seconds / 5), capped by wan_max_seconds (default 20 s = 4 segments). This is Wan-native and in-graph — it does not use LTX's host-side orchestrator.

i2v. Attach an image and the lane animates it via WanImageToVideo (start_image); the director crafts motion (how it moves), not a re-description of the still — same pattern as the LTX i2v mode.

VRAM / GPU split

Video and the dual-card LLMs are mutually exclusive (both want the GPUs). In video mode: GPU1 holds the 22B DiT weights (~22 GB, DisTorch donor); GPU0 does compute (~714 GB) and hosts the ~4.5 GB director — they coexist on one card. Because ComfyUI holds both cards in ai-studio, you can also run a ≤1024² image lane in the same scene with no switch (it fits on GPU0 beside the director). Full per-lane VRAM in image.md / audio.md / README.md.

Director placement is a VRAM lever (default: GPU0; one knob — STUDIO_DIRECTOR_DEVICE=gpu0|gpu1|cpu, set via c3 → Settings → Director placement — relocates it; see requirements.md). Its 4.5 GB on GPU0 is exactly what caps the single-card Wan window at ~121 frames; freeing it lifts that to 161. GPU0 is the safe default — it coexists with every shipped default lane. GPU1 is not a blanket-safe target: the LTX/Sulphur/10Eros lanes already use GPU1 as their ~22 GB donor, so a director there OOMs them. GPU1 is fine only for the image lanes and the Wan lane (18 GB donor + 4.5 GB = 22.5 GB fits); CPU (-ngl 0) is the universally-safe relocation, at a craft-latency cost. Use the lever to unlock edge cases (Ideogram 2048², single-window Wan >121 frames), not as a default flip.

On the uncensored models

The Sulphur DiT and the director are uncensored fine-tunes — chosen so the lane doesn't refuse or sanitize creative prompts. That capability lives in the model weights; the infrastructure here is content-neutral. To craft prompts through an aligned model instead, point the pipe's chat_model/chat_url valves at e.g. gemma-4-12b — the Sulphur DiT still renders uncensored, only the prompt-writing changes. (The text encoder is the stock aligned gemma; for LTX it's not a meaningful censorship lever, so it's not abliterated.)

Models (video lanes — obtain separately → /mnt/models/comfyui/models/...)

File ComfyUI dir Lane
ltx-2.3-22b-distilled-1.1-Q8_0.gguf unet/ltx2.3/distilled-1.1/ LTX
sulphur-2/sulphur_dev-Q8_0.gguf unet/ Sulphur
10eros/10Eros_v1-Q8_0.gguf unet/ 10Eros
ltx-2.3-22b-distilled-lora-384-1.1.safetensors loras/ all dev lanes (single-stage splice)
ltx-2.3-22b-{distilled,dev}_{audio,video}_vae.safetensors vae/ LTX / Sulphur / 10Eros
ltx-2.3-22b-{distilled,dev}_embeddings_connectors.safetensors text_encoders/ LTX / Sulphur / 10Eros
wan-rapid/Mega-v10/wan2.2-rapid-mega-aio-nsfw-v10-Q8_0.gguf unet/ Wan2.2
umt5_xxl_fp8_e4m3fn_scaled.safetensors text_encoders/ Wan2.2 (encoder)
wan_2.1_vae.safetensors vae/ Wan2.2 (VAE)

Director GGUF (Qwen3.5-4B-Uncensored-…) → /mnt/models/huggingface/qwen3.5-4b-gguf/…. Image + audio model manifests are in image.md / audio.md.

Follow-ups (not yet built)

  • Native temporal-extend for smoother joins on fast-motion scenes (vs last-frame I2V).
  • Image→video long clips: chaining currently starts from text (seg 1 = t2v); extending an attached image past 15 s is future.