InternScience's 35B agentic MoE (Qwen3-Next MoE arch; the card declares its
OWN base -> own model entry, not a qwen fine-tune slug), served from the
official FP8-dynamic compressed-tensors checkpoint on stock vLLM v0.24.0,
dual 3090 TP=2, full 262K. The agentic thinking-ON specialist.
First model onboarded END-TO-END through the Bring & Validate lane (T2
producer-zero): pull.sh route-C sibling swap (post deriver fix) ->
generate-compose -> documented swap -> full gate -> ④ vs-bar -> ⑤ scaffold.
Full gate PASS (rebench tag agents-a1-fp8-dual, 2026-07-03):
- bench 153.9/154.0 decode TPS (n=5, CV 0.1%, TTFT ~130ms), ~22.0 GB/card
- verify-stress 8/8 — staggered NIAH exact-recall to 240K (91%), VRAM Δ0
- soak PASS (0 growth, 0/100 silent-empty, 99.8% retention)
- 8-pack --full OFF 105/150 · ON 110/150 (post benchlocal #79+#81 harness):
toolcall 15/15 OFF · IF 15/15 ON · cli-40 thinking-ON 23/40 (the stack's
highest; base 17/40) · hermes 12/20 OFF, REGRESSES to 9/20 thinking-ON
(verified model behavior — disclosed as caveat 1)
vs qwen3.6-35b-a3b: general capability TIES (ON 110=110), decode −13% —
NOT a general upgrade; reach for it on tool/CLI-agent work thinking-ON.
Catalog wiring: compose dual/fp8-dynamic/fp8.yml (non-default KV named per
convention; OWN port 8072 — sibling-shared ports masquerade the slug in
estate detection); model profile (geometry verified == 35B-A3B from
config.json; mtp_num_hidden_layers=0 — safetensors header shows 0 mtp
tensors, config's 1 is an inherited default); registry entry status=caveats;
kv-calc agents-a1 spec + alias (fit-all prices, calibration 100%);
weights.py aliases (hf auto-fetch); LiteLLM route :8072; BENCHMARKS section;
count bumps (registry 57, disk 58, models 11).
Full catalog suite 60/60 (1 = known worktree-fixture submit-bench).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
The shared served-model-name `qwen3.6-27b-autoround` (from #490) mislabels the
non-autoround 27b scenes (fp8 dual-max, lmcache): /v1/models advertises
"autoround" while `root` points at qwen3.6-27b-fp8. Same class on gemma-4-31b —
after the v0.24.0 consolidation the default is cyankiwi qat-AWQ-INT4 (bf16 KV),
yet the LiteLLM route still targeted `gemma-4-31b-autoround` (a latent #482 drift).
Fix WITHOUT breaking anything, via vLLM multi-served-name:
- Every 27b scene now serves `qwen3.6-27b <its-quant-name>`; every gemma-31b
scene serves `gemma-4-31b <its-quant-name>`. The neutral name is PRIMARY
(honest /v1/models id); the quant-specific name is retained as a live ALIAS.
- LiteLLM: add `qwen3.6-27b` / `gemma-4-31b` canonical public routes; keep the
`-autoround` routes as back-compat aliases (same upstream). Repairs the gemma drift.
- Migrate our own MODEL= defaults + docs (bench/verify/quality/launch/setup, c3,
tui-core, EXAMPLES, ...) to the neutral name. Weights slugs (`-autoround-int4`)
untouched; CHANGELOG + results/ history left as-is.
Retiring the `-autoround` alias entirely is a deliberate later step once nothing
still asks for it.
Live-verified on-rig (single/minimal, stock v0.24.0): /v1/models lists BOTH names
(root=...-autoround-int4); chat to `qwen3.6-27b` AND `qwen3.6-27b-autoround` both
return 200; `qwen3.6-27b-fp8` correctly 404s. Full shell gate 59/59 (1 = known
worktree-fixture); c3 pytest 41 passed; served-name arg-order + YAML validated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
#531 pinned COMFYUI_ROOT for ComfyUI's input/models mount, but the 5 output
consumers (gallery :8189, orchestrator, tts, step-voice, production) mount a
separate COMFYUI_OUTPUT_DIR that was never derived from COMFYUI_ROOT — it fell
back to /mnt/models/comfyui/output. On any non-/mnt rig, ComfyUI wrote renders
to $COMFYUI_ROOT/output while the gallery served the empty /mnt default -> 404
on generated media (the '_output_images_will_be_put_here' placeholder was the tell).
- comfyui-paths.sh: derive + export COMFYUI_OUTPUT_DIR=$COMFYUI_ROOT/output
- c3_persist_comfy_root: pin it to .env PER-VAR (adds OUTPUT_DIR even when ROOT
was already pinned by #531 -> existing users self-heal on next studio start)
- test: +7 assertions incl. the ROOT-already-pinned migration case
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>
Root cause of "model dropdowns empty" on any rig whose MODEL_DIR isn't the /mnt
layout: the comfyui compose mounts `${COMFYUI_ROOT:-/mnt/models/comfyui}/models`
via `sudo docker compose --env-file .env`, but COMFYUI_ROOT is only derived
in-shell by comfyui-paths.sh — sudo strips the exported var and .env carries only
MODEL_DIR, so the compose falls back to the /mnt default and mounts an EMPTY tree.
Downloads (which key off the MODEL_DIR-derived COMFYUI_ROOT, e.g. $HOME/comfyui)
land elsewhere → ComfyUI sees no models → loaders 400 with "not in []" and the
HiDream node reports "not installed". Invisible on the maintainer rig only because
its /mnt layout makes the two paths coincide.
Fix: persist the resolved COMFYUI_ROOT into repo-root .env (write-if-absent, never
clobbers a hand-set value; no-op under C3_PATHS_NO_ENV / unwritable .env) — same
mechanism already used for LANIP. Called from c3_ensure_comfy_models_dir (every
downloader) and gpu-mode start_comfyui (the launch), so the --env-file'd compose
mounts the SAME tree the downloads went into.
- services/comfyui/comfyui-paths.sh: add c3_persist_comfy_root(); call it from
c3_ensure_comfy_models_dir. C3_ENV_FILE override for testability.
- scripts/gpu-mode.sh: start_comfyui pins COMFYUI_ROOT before the compose up.
- scripts/tests/test-comfyui-paths.sh: + cases H (writes when absent) / I (respects
existing). 13/13 green; test-studio-derig green; real .env not polluted.
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>
Kokoro TTS now world-readable-marks its /tts WAV + /narrate MP4 outputs (mirroring ComfyUI save nodes) so host-side consumers can read them directly. Re-scoped from the original #501 — dropped that PR's README production-agent section (superseded by docs/ai-studio/agents-architecture.md; it referenced a private path). Note: tts.py is COPY'd into studio-tts — rebuild the container to deploy.
Both lanes were fully wired (workflow + custom node + OWUI lane) but their
weights were never fetched by setup-ai-studio.sh -> download_studio_models.sh,
so selecting either returned a cryptic "ComfyUI generation error" — the log
shows FileNotFoundError: <model> is not installed (download_if_missing: false).
- HiDream: download_hidream_o1.sh existed and is correct, but the roster never
invoked it. Wire it in.
- Chroma: had no downloader at all. Its UNET (Chroma1-HD-fp8mixed), the shared
t5xxl_fp16 encoder, and the flux ae VAE relied on the legacy (now-unwired)
download_models.sh already being on disk. Add download_chroma.sh (mirrors
download_zimage.sh's split_files staging; ungated sources) and wire it in.
- studio-models.tsv: add the chroma + hidream rows so c3's missing-model check,
gpu-mode preflight, and the download modal know about them.
Repo file paths verified against the live HF API. The downloads themselves are
not run-verified here (no 120 GB pull); the script mirrors the proven sibling
downloaders. test-studio-derig.sh green.
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>
`hostname -I` is net-tools only; CachyOS (and any GNU inetutils host) lacks it, so c3_lan_ip
returned empty → the studio URLs fell back to `localhost` (and the reporter hit a setup abort
they attributed to it).
- c3_lan_ip: try `hostname -I`, then fall back to the portable `ip -4 -o addr show scope global`.
Always returns 0 (callers no longer need a `|| true` guard).
- .env is now the source of truth for LANIP, like MODEL_DIR: comfyui-paths.sh reads LANIP from
repo-root .env at source time (env still wins), and a new c3_resolve_lanip() PERSISTS an
auto-detected IP back to .env (user-editable) — or, if nothing detects, falls back to localhost
and tells the user to set LANIP in .env. Precedence: shell-env > .env > auto-detect > localhost.
- setup-ai-studio.sh calls c3_resolve_lanip (the persister); gpu-mode keeps reading (it now picks
up the .env value + the portable detect, no write on a mode switch).
- derig guard: assert the `ip` fallback exists (no hostname-only regression) + accept the
c3_resolve_lanip wrapper.
Verified: net-tools host, faithful CachyOS host (hostname rejects -I → ip fallback), pinned
.env/env (reused, not overwritten), and no-detect (localhost + instruction). Real repo .env
untouched by the tests.
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>
A "dig history of pakistan? … ok do it" chat built a film literally titled "ok do it":
the LLM controller returned brief='' (it read "dig history of pakistan?" as a research
request, not a film), so the brittle keyword floor took over and grabbed the confirm
phrase as the brief. We'd been patching the floor's keyword lists to cover for the LLM —
treating the symptom.
Fix it where it belongs — in the LLM's prompt — and stop leaning on keyword intent detection:
- CONTROLLER PROMPT (build_controller_system): brief extraction now INFERS the subject from
indirect phrasing — a topic asked to be researched/dug/searched ("dig history of pakistan"
-> "the history of pakistan"), a bare topic, or a changed subject. A live A/B: this one
prompt change flips the failing transcript from brief='' to brief='the history of pakistan'
with no false positives (greetings/capability-questions still -> ''). Its reply guidance is
also truthful about web research (no more "I can search the web!" fantasy).
- TRUST THE LLM: the pipe no longer ORs the LLM brief with the keyword floor. When the
controller answers, its brief/intent/reply are authoritative; the keyword floor is consulted
ONLY when the controller is unreachable (and even then never guesses a brief from a confirm).
- CONFIRM stays a tiny CLOSED vocabulary (is_confirm now catches compound bare confirms like
"ok do it" / "yes go ahead") — affirmation is a closed set where keywords are reliable; the
irreversible render is still gated by a real go-word (safety latch). Brief (open-ended) is
the LLM's job; confirm (closed) keeps a generic latch.
- _classify runs at temperature 0 so the brief is stable across turns.
Live end-to-end (controller -> merge -> decide_action), 5/5 correct: the failing transcript
builds "the history of pakistan", mid-chat revision swaps the brief, questions/greetings chat.
147 offline unittests green; pipe rebuilt + py_compile-clean.
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>
The director claimed it could "search the web" (the 4B hallucinated the capability — AGENTS.md
said nothing about it) but no search was wired, so a "history of Pakistan" documentary planned
from the 4B's frozen ~2024 knowledge — vague or invented facts.
Wire the rig's SearXNG (:8088, the same instance OWUI uses) as an OPT-IN documentary research
step:
- research.py — web_search() + research_notes(): query SearXNG, dedupe the top snippets into a
RESEARCH NOTES block. FAILS OPEN (down / no results / bad JSON / timeout → '' → plan from own
knowledge); research is an enhancement, never a hard dependency.
- planner.plan_from_brief(use_research=...): for a DOCUMENTARY brief, fetch the notes and ground
BOTH stages (treatment + plan) in real names/dates/events; provenance → prompts/research.json.
Narrative briefs never research (fiction needs no facts).
- server /produce reads a `research` flag; the OWUI pipe surfaces it two ways: the user can say
"research" / "search" / "dig", AND a documentary plan card OFFERS it ("reply research to ground
it in real web facts, or go to use what I know") — opt-in, never silent.
- AGENTS.md persona is now truthful: it researches a documentary you're making, but cannot browse
arbitrary pages or fetch live info to chat about (stops the false "I can search the web!" claim).
- director_intent.looks_documentary (injected, drift-guarded vs detect_format) gates the offer.
Live-validated: a researched "history of Pakistan" plan cites real recent events (1998 nuclear
tests, 2022 floods) and the actual first PM (Liaquat Ali Khan, straight from a SearXNG result),
vs a vaguer un-researched plan. 144 offline unittests green (network mocked); pipe rebuilt +
py_compile-clean.
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>
'Valid JSON' is not 'good film plan' — the schema can't tell that a documentary wandered
off-topic, that shots repeat, or that the narration won't fit. Add a critic that runs after
schema validation and BEFORE the (minutes-long) render, feeding any blocking issues back into
the repair loop (capped, fail-open).
critic.py — two layers:
- DETERMINISTIC (no LLM, always on, reliable): narration that won't fit its shot window,
near-duplicate shots, a documentary that smuggled in fictional characters.
- LLM per-shot RELEVANCE critique: "for each numbered shot, is it about the brief's subject?"
— the general form of the "documentary became a drama about Arif" bug. Conservative,
FAILS OPEN: any LLM error/empty/garbage → deterministic-only, never blocks a render.
Empirically tuned on the live 4B (the dry-run is the point):
- thinking-ON returns EMPTY output even at a 16k budget and takes ~100s on this uncensored 4B
→ critic runs thinking-OFF. extract_json/parse_critique still strip <think> defensively (F7,
via the shared util.strip_reasoning) so a future thinking stage can't pollute parsing.
- a HOLISTIC "is this good?" verdict got rubber-stamped (missed obvious drift); a PER-SHOT
off_topic framing catches it.
- temperature 0.2 was noisy (missed drift AND false-flagged a clean plan ~1/3) → temperature 0
(greedy) is reliable: drift → every off-topic shot flagged, clean plan → no issues (3/3 each).
Integration: planner gains a use_critic flag (server passes True; default False keeps offline
tests model-free). One critic-repair round (MAX_CRITIC_ROUNDS=1) then ship the valid plan;
director_call gains enable_thinking; provenance written to prompts/critic.json.
Live-validated: deterministic catches the original Arif character leak; the LLM critique flags
the drifted Arif shots [1-6] and passes a clean Pakistan-history plan; a clean plan_from_brief
run critiques without a spurious repair. 134 offline unittests green.
Note: a 4B is a borderline critic (Codex foresaw this) — the `call` is injectable, so routing
the critique to the 27B/35B for stronger judgement is a clean future upgrade.
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>
The OWUI Production lane was a keyword state machine — the 4B only wrote reply prose
after Python had already decided intent, so it only felt natural when phrasing matched
the regexes (Codex Q1). It also latched the brief to the first turn (couldn't change the
film mid-chat, F2) and couldn't parse a compound "go with ltx" (F3).
Add a structured intent controller: the 4B reads the WHOLE conversation and returns ONE
JSON decision {intent, brief, stack_patch(lanes), confirm, reply}. The keyword heuristics
from Batch 1 stay as the deterministic FLOOR and the fallback:
- brief = LLM's current desired film (handles a mid-chat subject change) or the floor brief.
- stack_patch = explicit MODEL/continuity picks only; the LLM does NOT own music/narration/
seconds (it over-reached — stripped music from a "bookstore promo" nobody asked to silence),
those stay keyword-driven. Unknown lanes dropped, never coerced; floor wins on conflict.
- confirmed = (LLM.confirm OR floor.confirm) AND has_confirm_word(last) — a render NEVER fires
on the LLM's say-so alone; the latest turn must carry a real confirm word. So "go with ltx"
builds (F3) but "make it 30 seconds" never does.
- reply = the LLM's contextual answer (reused for chat — no second call).
- decide_action(brief, confirmed, intent) -> build|proposal|chat|need_brief (pure, tested).
On any LLM failure (timeout/bad JSON) it falls back to the Batch-1 floor → strictly >= before.
All new logic is stdlib-only in director_intent.py (injected into the pipe, table-tested).
Validated end-to-end against the live 4B (classifier + merge + decide_action): 8/8 actions
correct — F1 (creation-question→proposal), F2 (mid-chat change→new brief), F3 (go with ltx→
build+ltx), the real failing transcript, question→chat, greeting→chat, go→build, confirm-
without-brief→need_brief. 119 offline unittests green; pipe rebuilt + py_compile-clean.
Out of scope (Codex Batch 3): semantic critic before render, selective thinking-on,
cross-lane Studio router.
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>
Quick-win batch from the Codex director-lane review (all confirmed against code):
F1 — the OWUI Production lane misread creation asks as questions: "can you make a
30s noir short?" / "do a 1-min documentary on Pakistan" start with can/do and end
with "?", so the old keyword gate excluded them from brief detection and dove into
chit-chat with no brief. A GENERATION REQUEST now takes precedence over question-
shape, so a creation ask phrased as a question is captured as the brief.
F13 — extracted the conversation-intent classifiers out of pipe()'s untestable
locals into services/studio/director_intent.py (stdlib-only, table-tested). The pipe
bake injects its source verbatim (same pattern as AGENTS.md/workflows), so the
deployed pipe and the tests can't drift. +11 intent tests incl. the real failing
transcript.
F8 — the planner's capability registry (capabilities.yaml) described ONLY Wan, so
the 4B planned against Wan's 16fps/silent physics even when LTX (24fps, native
audio) was pinned. Added ltx/sulphur/10eros contracts; prompt_slice now filters to
the PINNED lane and describes audio per-lane (no more hardcoded "SILENT"). Drift
guard: test_capabilities_match_wired_video_lanes.
F4 — pipe sized shots with round(secs/5) while the server uses ceil; aligned to ceil
(must match planner.derive_shots so the proposal == what /produce builds).
F11 — the production video-lane valve still called ltx/sulphur/10eros "roadmap (not
yet wired)" — they render + were live-validated. Fixed the text + a derig guard.
Out of scope (Codex Batch 2/3): structured LLM intent controller, semantic critic,
long-plan decomposition, selective thinking-on, cross-lane router.
Tests: 103 production+intent unittests + studio derig guard all green; pipe
rebuilt + py_compile-clean + baked intent block exec-verified.
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>
A "documentary on the history of Pakistan" became a fictional character drama
about an invented young architect named "Arif" — the 4B latched onto the
dominant CHARACTER BIBLE instruction and fictionalized a factual brief, drifting
off-topic by shot 7 (a guy laughing in a park) with poetic mood narration.
The brief reached the planner intact; the failure was entirely in the plan the
4B wrote. Make FORMAT a first-class, deterministic decision:
- prompts.detect_format(brief) classifies documentary (non-fiction) vs narrative
(fiction) from tight signals (document*, "history of", explainer, guide to, …).
- build_treatment_system(fmt) / build_plan_system(reg, …, fmt): documentary mode
front-loads a NON-FICTION banner, sets characters=[] in the shape, drops the
Detective-Marlowe Character Bible, and asks for FACTUAL narration + on-topic
archival/observational shots. Narrative mode keeps the existing Character Bible.
- normalize(…, fmt) FORCES project.format and, for documentary, strips any
characters the 4B emitted anyway — a hard guarantee the "Arif" failure can't
recur even if a future model ignores the prompt.
- schema.Project gains a `format` field so the plan/manifest records the genre.
Validated live on the 4B with the exact brief: title "The Partition of the
Subcontinent", characters [], factual narration ("The Radcliffe Line divides the
subcontinent"), every shot on-topic. Narrative noir brief still yields Detective
Marlowe. +5 offline tests; full production suite 90/90.
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>
A mixed cut/dissolve timeline took the xfade path where a cut was faked sub-frame (0.033s < 1 frame @24fps), collapsing the film to one clip (28s sulphur noir → 4.99s). Cut blend now 2/fps. + LTX clips skip no_audio_expected. Verified: re-assembles to 27.5s; 85 tests.
Fixed max_tokens=900 truncated longer plans (30s/6-shot → unbalanced JSON). plan_tokens=min(4096,700+n_shots*180) for plan + repair calls. Verified live on the failing sulphur brief.
Shared production/ltx_workflows.py (byte-identical to the inline builder) used by both the interactive pipe and the executor; render_video LTX branch (native res, 24fps, seed, i2v, neg); stack.py all 4 video lanes wired. Graph-validated live; suite 84.
Greetings + questions get a natural 4B reply (was: canned wall / proposal re-dump). Plan card shows only for the brief or a settings change; only 'go' renders. Director behavioral spec externalized to services/studio/director/AGENTS.md, baked into the pipe at build time.
Derive shot count from the brief's stated duration (1 minute → 12 shots, not a fixed 3); propose the full plan (models · length · est. time) and only build on 'go', adjustable in chat. Service + CLI auto-size; poll timeout scales with shots. +3 tests (80).
The 🎬 Studio · Production lane POSTed any message to /produce; a greeting made the 4B invent + render a random film. Add the director CHAT: gate (+ exact-match greeting fast path) like every other lane. Validated live.
comfyui-paths.sh: default MODEL_DIR to $HOME/models (user-owned) instead of the rig /mnt path when unconfigured; c3_ensure_comfy_models_dir() fail-fast; shared c3_lan_ip() used by gpu-mode + setup. Follow-up to #505/#503/#504. Full suite 60/60.
Operator-chosen stack (no silent Wan), 🎬 Studio·Production OWUI lane (plan-then-execute), 4 live-validated tiered keyframe lanes (+2 HiDream fixes), and the Character Bible (Tier A: semantic identity, default-on; live noir acceptance PASS — silhouette/wardrobe/role/language hold, face not locked). Tier B reference-image lock deferred to a separate gated spike. 77 offline tests.
Lands the rest of the production-agent arc on top of v0a (#496). The 3-deep PR
stack couldn't be merged per-PR (squash of #496 diverged the stacked branches +
--delete-branch auto-closed #497), so #497 + #499 land together here.
- v0b-core (#497): the qwen3.5-4b-uncensored director plans a brief into a valid
ProductionPlanV1 (capability registry + prompt pack + validator-repair loop);
llm_prompt provenance.
- v0b-images (#499): continuity — asset-DAG + Wan i2v + chroma keyframes; four
modes (none/chain/hero/storyboard), storyboard the default per the live A/B.
Isolated under services/studio/production/, stdlib + PyYAML, 36/36 offline, all
stages live-validated. Continuity path validated (not "solved" — residual is Wan
i2v keyframe-adherence, a video-model lever).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
* Add Studio Production Agent v0a executor scaffold
The v0a slice from docs/ai-studio-production-agent-design.md: one static
ProductionPlanV1 -> a finished MP4 by driving the existing AI-Studio lanes
serially (Wan t2v video, Kokoro narration, ACE-Step bed -> ffmpeg mix).
Scope (tight, per design): CLI/admin only, single-flight (host file-lock),
one pinned video lane (wan/t2v), ffprobe validators, final ffmpeg mix at the
delivery profile, typed/extensible manifest with run-level provenance
(delivery, workflow sha256s, seeds, exact ffmpeg argv). No 4B planner, no
OWUI lane, no image/asset-DAG, no Qdrant/SearXNG, no durable queue -> v0b/v1.
Isolated under services/studio/production/, stdlib-only (host has no
pydantic/pytest): dataclasses + validate(), unittest. A `synthetic` ffmpeg-
lavfi backend exercises the whole render->validate->assemble->manifest path
offline; the `live` backend drives ComfyUI (:8188) + Kokoro TTS (:8192) and
replicates the studio_pipe call patterns (does not import the OWUI pipe).
Verified offline: 12/12 unittests green incl. a full synthetic end-to-end
producing a real MP4 with an audio track; CLI run prints the exit-criteria
summary. Live render needs the ai-studio scene up on the rig (not run here).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
* Make LiveBackend artifact pull resilient to root-only service files
Live 3-shot run surfaced it: Kokoro TTS writes its WAV mode 0600 root:root,
so the host CLI (uid 1000) can't read it to copy (ComfyUI writes 0666, so
video/music copied fine). _pull() now falls back to `docker cp` from the
owning container — the docker daemon reads as root — when a host copy hits
PermissionError, then normalises to 0644. Stays isolated under production/;
the cleaner systemic fix (TTS service writing 0644) is a v0b follow-up.
Re-run: live 3-shot PASS rc=0, final MP4 832x480 h264+aac 13.4s, all
validators pass, zero babysitting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
* v0a assembler polish: dissolve transitions + gentler duck (defaults)
Deterministic post-production defaults — NOT v0b "brain" work. The bare
hard-cut + aggressive-duck output felt abrupt and under-sold the foundation
(observed on the live render), so smooth it by default:
- timeline `transition_in`: "dissolve" (default) | "cut", per seam
- assemble builds an xfade dissolve chain with crossfade-aware narration
timing; the concat path is kept for all-cut plans (true hard cuts)
- `assembly` knobs: transition_seconds (0.6) + duck_db (6 -> gentle sidechain
ratio 4, attack 20 / release 600) so the bed glides instead of pumping
- sample plan defaults to dissolve; hard cut stays available
- tests +3 (dissolve graph, all-cut concat, transition schema): 15/15
Continuity / i2v-chain / planner deliberately NOT here -> v0b.
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>
setup-ai-studio.sh's director download honored MODEL_DIR, but the ComfyUI
assets (image/video/audio) defaulted to a hardcoded /mnt/models/comfyui — so
any rig whose models don't live under /mnt failed mid-download with
"mkdir: Permission denied" (sumo Discord report, 2026-06-27). Two more walls
behind it: the disk-space preflight checked the hardcoded /mnt path and had no
override (an idempotent resume under-threshold was blocked), and gpu-mode.sh —
which setup calls at step 3 — hardcoded CLUB3090_DIR=/opt/ai/github/club-3090,
breaking on any other clone.
- New services/comfyui/comfyui-paths.sh: derive COMFYUI_ROOT / COMFYUI_MODELS_DIR
as a "comfyui" sibling of MODEL_DIR (read from repo-root .env when unset).
Backward-compatible on the rig (/mnt/models/huggingface -> /mnt/models/comfyui);
explicit overrides respected. Sourced by setup-ai-studio.sh,
download_studio_models.sh, and gpu-mode.sh so the download target, the disk
check, and the container mounts all agree.
- setup-ai-studio.sh: disk check uses the derived path; add SKIP_DISK_CHECK=1 to
bypass the free-space preflight independently of SKIP_DOWNLOAD.
- gpu-mode.sh: auto-detect CLUB3090_DIR from the script's real location (resolving
the /usr/local/bin symlink), overridable via env — removes the hardcoded path.
- The 3 legacy hardcoded download scripts (flux2/hunyuan_llava/models) honor
COMFYUI_MODELS_DIR.
- New test-comfyui-paths.sh guards the derivation; docs/ai-studio config note.
Full suite green (59/59).
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>
The LiteLLM gateway routes qwen3.6-27b-autoround → :8010, but the fp8 /
awq / lmcache 27b scenes served scene-specific names (qwen3.6-27b-fp8,
qwen3.6-27b-awq-bf16-int4). Bring one of those up as the :8010 primary
(e.g. via gpu-mode PORT override) and the gateway 404s on a served-name
mismatch (#482).
Standardize every 27b serving scene's --served-model-name to the
canonical qwen3.6-27b-autoround so the route matches whichever scene is
on :8010. The quant still differs by compose path/port — only the served
name is unified. Weights --model paths are untouched. Document the
invariant in services/litellm/config.yaml.
Full test suite green (58/58).
Closes#482
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>
* 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>
* Wire SearXNG web search + Qdrant vector DB into Open WebUI
Two integrations OWUI wasn't actually using despite both services running:
SearXNG (web search) — the compose used the pre-v0.9 env names
(ENABLE_RAG_WEB_SEARCH / RAG_WEB_SEARCH_*), which v0.9.6 ignores, so web
search was silently off. Renamed to the current names (ENABLE_WEB_SEARCH,
WEB_SEARCH_ENGINE, WEB_SEARCH_RESULT_COUNT, WEB_SEARCH_CONCURRENT_REQUESTS)
→ config path rag.web.search.*, seeded from env on a fresh volume.
SearXNG already serves JSON at :8088.
Qdrant (vector DB) — added VECTOR_DB=qdrant + QDRANT_URI (:6333) +
QDRANT_ON_DISK so document/web-search RAG vectors go to the qdrant service
instead of the embedded Chroma default. VECTOR_DB is a plain startup env
(applies every boot). Switching backends doesn't migrate existing Chroma
embeddings — re-index any knowledge.
Existing-volume note (this rig): web search also needed a one-time DB edit
(rag.web.search.enable/engine) because OWUI's PersistentConfig DB value
overrides the env once persisted; fresh installs get it from the env above.
Live-validated: ENABLE_WEB_SEARCH/engine=searxng at runtime; a web-search
round-trip through OWUI returned results AND created the qdrant collection
'open-webui_web-search' (3 points) — proving SearXNG + Qdrant work together
(search → embed → Qdrant, not Chroma).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
* OWUI web search: bypass embed/retrieve + disable hybrid (reliable results)
Web search ran (SearXNG → embed → Qdrant) but the model never saw the
results (sources: 0): the embed→retrieve path needs a reranking model for
hybrid search, and with RAG_RERANKING_MODEL unset the rerank step returned
0 chunks, so nothing reached the prompt.
- BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL=true — inject SearXNG results
straight into context (no vector round-trip).
- BYPASS_WEB_SEARCH_WEB_LOADER=true — use the search snippets, not full-page
fetches → small, fast context.
- ENABLE_RAG_HYBRID_SEARCH=false — pure vector retrieval for document RAG too
(hybrid needs a reranker; set RAG_RERANKING_MODEL + flip true for recall).
Live-validated: a web-search chat against gemma-4-12b-int8 now returns a
cited answer ("Anthropic recently announced ... [1]"), sources attached.
(Applied to the live rig via the PersistentConfig DB since env doesn't
override an existing volume; these env vars seed fresh installs.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
* OWUI: enable hybrid RAG with bge-reranker-base (in-process, CPU)
Re-enable hybrid (BM25 + vector + rerank) document-RAG retrieval now that a
cross-encoder reranker is configured. bge-reranker-base (~280 MB) loads
IN-PROCESS in OWUI on CPU — no GPU contention, no separate service —
auto-downloaded from HF on first use. Document-RAG only (web search bypasses
retrieval). Swap to bge-reranker-v2-m3 (~2.3 GB) for higher recall.
Applied to the live rig via the PersistentConfig DB (rag.reranking_model +
rag.enable_hybrid_search); these env vars seed fresh installs.
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>
* Lazy-load the step-voice service + scene-wire it into ai-studio
Make the premium-voice lane "just work" in the studio without pre-pinning
GPU1: the step-voice container is now part of the ai-studio scene, but the
~14 GB Step-Audio-EditX model loads on first use instead of at boot, and is
evicted before a video render so it never OOMs against the LTX/Wan DiT
(both want GPU1, the DisTorch donor is ~21.9 GB).
server.py — lazy lifecycle (STEP_VOICE_LAZY=1, default):
- model loads on the FIRST /clone or /edit (lock-guarded), not at import →
the container boots at ~0 GB GPU, so the OWUI voice lane appears as soon
as ai-studio starts it (the pipe gates the lane on :8193 reachability).
- new POST /unload frees the model from GPU; an idle-unload timer
(STEP_VOICE_IDLE_UNLOAD_S, default 300 s) reclaims GPU1 automatically.
- /health reports {ready, loaded, lazy}; transcribe+clone moved off the
event loop into the worker. STEP_VOICE_LAZY=0 restores eager boot.
build_studio_pipe.py / studio_pipe.py — deterministic voice⊕video mutex:
- video lanes (ltx/sulphur/10eros/wan) POST /unload before submitting the
render, so a freshly-used voice model can't collide with the video DiT.
gpu-mode.sh — scene wiring:
- start_step_voice in mode_ai_studio (lazy → cheap); stop_step_voice
wherever stop_comfyui is called (every LLM scene + mode_off) so GPU1 is
freed for a dual-card model. ai-studio --list-modes row + helpers added.
docker-compose.yml — STEP_VOICE_LAZY + STEP_VOICE_IDLE_UNLOAD_S env,
restart policy note (scene-managed), header rewritten for lazy behaviour.
Live-validated via the real compose (--build bakes server.py): lazy boot
loaded:false @ ~4 MiB GPU1, first /clone loads (~13 GB) + clean audio,
POST /unload frees back to ~0, reload clean; gemma12b on GPU0 untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
* ai-studio scene: start qdrant (OWUI's vector DB) + list it
Every model scene starts qdrant (start_service qdrant) but mode_ai_studio
didn't — it predates OWUI's Qdrant vector-DB wiring. So in ai-studio OWUI
ran without its vector store started by the scene (document RAG / web-search
embeddings would have no backend on a clean ai-studio launch from mode_off).
Add start_service qdrant to mode_ai_studio and qdrant:6333 to its
--list-modes service/port row, so it actually starts AND shows up in c3's
ai-studio scene preview alongside studio-step-voice.
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>
The OWUI picker listed models that weren't actually serving: the studio
image/video/audio lanes (always shown via the studio pipe) and every
LLM catalog model (always shown via the always-up LiteLLM :4000
gateway). Both error until you switch to the matching gpu-mode scene.
Now the picker reflects what's live.
Studio lanes — gate the pipe's pipes() on backend liveness:
- build_studio_pipe.py (source template) + regenerated studio_pipe.py:
add a _LANES catalog tagged by backend (ComfyUI :8188 for all media
lanes, the on-demand voice service :8193 for the voice lane), a cheap
reachability probe (_alive, short timeout) with an 8s TTL cache, and
filter pipes() to only the lanes whose backend answers. New valve
hide_unavailable_lanes (default true) toggles it off. When the
ai-studio scene is down, the Studio group drops out of the picker
instead of listing dead lanes.
LLMs — point OWUI at each backend directly instead of the gateway:
- services/openwebui/docker-compose.yml: seed one connection per backend
(director :8090 + :8010/:8051/:8032/:8038/:8199), drop :4000. OWUI
hides models from an unreachable connection, so each model shows only
while its scene serves. Served names match the catalog (IDs unchanged).
LiteLLM stays up for other clients; it just leaves OWUI's picker.
- scripts/lib/owui-unregister.sh: new symmetric inverse of
owui-register.sh (remove a connection by port; forged-JWT config API,
idempotent, no-op if OWUI down / not present).
- scripts/setup-ai-studio.sh: idempotent step that registers the 6
per-backend connections + drops a stale :4000, so existing installs
converge (fresh installs get it from the compose env seed).
Live-validated on the rig (gemma12b scene): picker LLMs = only
gemma-4-12b-int8 (the four down models hidden, :4000 gone), Studio group
hidden (ComfyUI + voice down). Probe verified both directions (live
endpoint -> shown even on a 404 path; dead port -> hidden).
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>
New `gpu-mode gemma12b` scene (alias gemma-12b) serves
vllm/gemma-12b-single-int8-mtp on :8038 — Gemma 4 12B (gemma4_unified
arch) AutoRound INT8 + bf16 KV + MTP n=2, single card (frees the other
GPU). Stops all dual models + ai-studio on entry; the single-card stop
is folded into the shared stop_all_gemma helper so every other model
scene auto-evicts it, with an explicit line in mode_gemma_int8 (the one
mode that doesn't call stop_all_gemma).
Wires it end-to-end: --list-modes TSV + JSON row (c3 Operate picks it
up), :8038 status probe, all-off detection chain, usage help, and a
litellm gateway route gemma-4-12b-int8 -> :8038.
Live-validated: boots and serves gemma-4-12b-int8 on :8038, coherent
completion direct and through the litellm gateway (:4000).
Note: the gemma4_unified image tag is ephemeral (arch-preview) — pin a
digest before treating this scene as durable.
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>
Switching to gemma stopped 27b but never started gemma. Root cause: `set -e`
+ mode_gemma_int8 called three functions deleted when the dflash/awq gemma
scenes were pruned (stop_gemma_dflash / _dflash_int8 / _awq) — the first
undefined call (`stop_gemma_dflash: command not found`) aborted the function
*after* the 27b teardown but *before* start_gemma_int8. Exactly the reported
"27b unloads, gemma fails to load".
- mode_gemma_int8: drop the 3 undefined calls; add the DEFINED stop_gemma_int8
(clean re-switch). Live-validated: `gpu-mode gemma` now starts
vllm-gemma-4-31b-mtp-int8 on :8032 and serves.
- Finish the partial prune: remove the 3 dead/unreachable gemma modes
(mode_gemma_dflash / _dflash_int8 / _awq — 0 dispatch refs) that referenced
the same deleted functions, + the orphaned GEMMA_DUAL_AWQ_DIR. No dangling
refs remain; bash -n clean.
- litellm: gemma-4-31b-autoround route :8030 → :8032 (the :8030 gemma-mtp scene
was pruned; the live scene serves :8032). End-to-end OWUI→litellm→gemma = 200.
OWUI only reached :8090 (studio director) + had a dead :8069 and a redundant
direct :8199 (deckard) — so `gpu-mode 27b` / `gemma` served fine but were
invisible in OWUI (only deckard worked, by luck of the direct :8199).
Bootstrap BOTH the director (:8090) AND the LiteLLM gateway (:4000) via the
plural OPENAI_API_BASE_URLS/_KEYS, so a fresh volume gets the full LLM catalog
(qwen3.6-27b / 35b-a3b / gemma-4-31b / gemma-4-12b / deckard-40b). Live DB on
this rig already updated to match (dead/redundant conns dropped).
Adds Krea 2 as the 5th image lane (🎨 Studio · Image · aesthetic), mirroring
the zimage lane: prose prompt, 8-step cfg=1, ~18GB GPU0 (coexists with the
director like the Ideogram lane).
- workflows/krea2.json — API graph from the official image_krea2_turbo_t2i
template: UNETLoader(krea2_turbo_fp8) → CLIPLoader(qwen3vl_4b, type=krea2)
→ CLIPTextEncode → KSampler(8/cfg1/euler/simple) → VAEDecode (qwen_image_vae).
- build_studio_pipe.py — KREA_TEMPLATE, WF["krea"], pipes() picker entry,
_comfy_krea(), lane detect/dispatch, director prose sys-prompt, krea_steps
valve. Regenerated studio_pipe.py (image x4 → x5).
Live-validated end to end: ComfyUI v0.26.0 renders the graph (8-step, 1024²,
36s cold-load, real 1.1MB RGB output); pipe regenerates + compiles; pushed to
OWUI (function row updated, pipe live). Needs the v0.26.0 pin (this branch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
The ComfyUI repo dir is bind-mounted from the host, so it's owned by the
host UID. git run as the container user then refused it ("fatal: detected
dubious ownership") → every fetch/checkout in checkout_comfy_ref() failed
silently and the bootstrap fell back to "current tree", misreporting it as
"(offline?)". Net effect: NO pin change ever applied — the container was
frozen at whatever it first cloned (0.24.0) regardless of COMFYUI_REF.
Add `git config --global --add safe.directory "$r"` before the git ops.
Validated live: container now checks out v0.26.0 (was stuck 0.24.0);
Krea2 renders (8-step, 1024², 36s) and the z_image lane still renders;
all custom nodes (HiDream-O1 / GGUF / DisTorch / Wan) load clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
v0.26.0 (comfyanonymous/ComfyUI #14589) adds native LOCAL Krea2 support —
the earlier "cloud-only, dropped" verdict was pin-specific (cb9f6394
predated it; only the cloud Krea2ImageNode existed → local load failed
"Could not detect model type").
- entrypoint.sh: COMFYUI_REF default cb9f6394 -> f6c162dd (v0.26.0); reword
the pin comment (Krea2 + Qwen3-VL text-gen; re-validate ALL lanes on bump).
- docker-compose.yml: pin comment v0.26.0 + Krea2.
- download_krea.sh (new): fetches the 3 Comfy-Org/Krea-2 assets (turbo fp8
DiT + Qwen3-VL-4B encoder + Qwen-Image VAE) into the ComfyUI models tree.
- download_studio_models.sh + studio-models.tsv: add Krea as an image-lane
model (roster <-> manifest kept mirrored).
NOT yet merge-ready: the v0.26.0 pin is a load-bearing bump — the custom
HiDream-O1 node, ComfyUI-GGUF, and DisTorch multi-GPU all ride it. On-rig
re-validation of every studio lane (Krea renders + the existing 9 still
work) is the merge gate. The OWUI lane graph (krea2.json workflow +
studio_pipe entry) is authored at that validation step (the Krea sampler /
Qwen3-VL encode graph differs from Z-Image — not clonable blind).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
Make a fresh clone go straight to generating, with the single-command UX the Image
Studio beta had — but for the consolidated image+video+audio studio.
- scripts/setup-ai-studio.sh (NEW, canonical): preflight (docker/gpu/~120 GB disk) →
build ComfyUI image → download_studio_models.sh (full roster) → gpu-mode ai-studio →
install the OWUI Studio pipe → onboarding. Flags: --yes / SKIP_BUILD / SKIP_DOWNLOAD / SKIP_PIPE.
- push-pipe-to-owui.sh now **UPSERTs**: installs the OWUI `function` row if absent (the
one previously-manual step — paste into Admin → Functions), else updates it. First-time
install just needs an OWUI admin account to exist. Validated: INSERT against a DB copy
(clean row + valid meta JSON), UPDATE live on the running OWUI.
- Retire the now-misnamed setup-image-studio.sh / setup-video-studio.sh → thin redirects to
setup-ai-studio.sh (they only pulled one modality post-consolidation and still echoed the
removed `gpu-mode image-studio`/`video-studio` scenes).
- Docs: services/studio/README.md (install is automated) + docs/ai-studio/README.md "Bring it up"
now leads with the one-command path.
Gate: scripts/tests 57/57; setup scripts syntax-clean; no test references the old script bodies.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
- requirements.md (new): hardware / software / disk / RAM to self-host AI Studio, expressed
generically (2× 24 GB Ampere+, PCIe ok / no NVLink; ~120 GB disk; 32 GB+ RAM), single-vs-dual-card,
and the director-placement VRAM lever. Linked from README's deep-dive index.
- video.md: a "Wan2.2 — tuning & limits" section (recipe + the sampler finding, 480p default /
720p-DisTorch valve, the corrected length-ceiling table, i2v-seeded chaining) + Wan model files in
the manifest. The ceiling table now shows the director as the single-card swing factor (121↔161)
and adds the DisTorch both-card column.
- Director placement documented as a lever (default GPU0; STUDIO_DIRECTOR_GPU / -ngl 0 relocate it),
with the caveat that GPU1 is unsafe for the LTX lanes (their 22 GB donor) — CPU is universal.
- The director carries its model name (Qwen3.5-4B-Uncensored) in the services manifest, matching the
lane-naming convention.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
Bring the Wan2.2 lane up to the LTX/Sulphur tuning bar (all live-validated on the rig):
- Sampler recipe: euler/simple → **euler_ancestral/beta** (the v10 Mega card's recommendation);
a same-seed sweep confirmed it's visibly sharper at the same ~145 s / same VRAM.
- **wan_hi_res** valve: 1280×720 via UnetLoaderGGUFDisTorch2MultiGPU (compute GPU0 / weights
donated to GPU1) — the plain single-card GGUF loader OOMs at 720p. ~3.5× slower, opt-in.
- **i2v**: attach an image → WanImageToVideo (start_image) animates it (wan22_rapid_i2v.json).
- **Long-clip chaining** (_comfy_wan_chain): requests >~5 s chain i2v-seeded ~5 s segments
(ImageFromBatch last frame → next start_image → ImageBatch concat), linear cost, no OOM wall;
segments = ceil(seconds/5) capped by wan_max_seconds (default 20 s). Wan-native, in-graph —
not the LTX orchestrator.
- The wan branch now routes image→i2v, length→chain, else t2v.
Single-card 480p ceiling is VRAM-bound: 81 (default) / 121 fit; 161 OOMs *with the 4.5 GB director
on GPU0* but renders (402 s) once it's relocated; DisTorch (both cards) fits 161/201 (402/576 s).
Director default stays GPU0 (safe with every lane; GPU1 collides with the LTX 22 GB donor).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
- README.md: 9→11 lanes, +Wan2.2 / +Z-Image rows, video labels aligned to the unified
`Studio · Video (Model)` naming.
- image.md: +Z-Image lane (table + section) + the "Krea2 dropped — cloud-only, no local
detection" note.
- video.md: four video lanes now (LTX / Sulphur / 10Eros / Wan2.2); Wan caveats (text→video,
no synced audio, 832×480) in the capabilities table; 8→11-lane matrix ref.
- services/studio/README.md: 9→11 picker entries + the two new workflow-graph rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
Wire the two new lanes into the install/preflight surfaces so a fresh rig fetches them
and c3 / gpu-mode know they're expected:
- download_zimage.sh — Z-Image-Turbo fp8 + Qwen3-4B encoder + flux ae VAE (~12 GB).
- download_wan.sh — Wan2.2-Rapid Mega NSFW Q8 GGUF + umt5 encoder + Wan 2.1 VAE (~25 GB).
- download_studio_models.sh ("grab all") calls both (idempotent — skips what's present).
- studio-models.tsv: +image/Z-Image, +video/Wan2.2 rows (the shared manifest read by both
c3's StudioModel loader and gpu-mode preflight). Roster note records Krea2 as dropped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
Bake-off result — two new validated OWUI lanes (live-rendered coherent on the rig):
- 🔓 Studio · Image (Z-Image) — Z-Image-Turbo fp8 (Alibaba 6B, Apache). Lumina2 encoder,
8-step cfg=1 turbo → ~25 s/1024² still. The FAST uncensored image lane. Native ComfyUI nodes.
- 🔓 Studio · Video (Wan2.2) — Wan2.2-Rapid-AllInOne Mega NSFW v10 Q8 GGUF (14B). umt5 encoder,
the AllInOne merge bakes a 4-step distill in → single 4-step cfg=1 sampler, 832×480×81 @16fps
(~3 min/clip). Text→video only, no synced audio (its own engine, not the LTX shared path).
Both graphs are authored from ComfyUI's official bundled templates and validated end-to-end.
Krea 2 was evaluated and DROPPED: its bespoke DiT isn't recognised by ComfyUI's local model
detection ("Could not detect model type") — only the cloud Krea2ImageNode works, which breaks
the "no cloud APIs" rule. Weights stay on disk, no lane.
Also unifies the OWUI picker naming to one format — `Studio · <Modality> (<Model> · <descriptor>)`
— so the video lanes match the image lanes and read as uncensored *video*:
🎬 Studio · Video (LTX-2.3) · 🔓 Studio · Video (Sulphur / 10Eros / Wan2.2)
🔓 Studio · Image (Chroma / Z-Image) · etc.
build_studio_pipe.py is the source of truth; studio_pipe.py regenerated + pushed to OWUI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
Sweep the studio docs to match the consolidation: `image-studio` /
`video-studio` / `comfyui` scene refs → the single `ai-studio` scene, drop the
gemma-12b chat-on-the-other-card framing, and document the new pieces.
- README: 8→9 lanes (+10Eros); "Lanes vs modes" → "One scene, lanes inside it".
- video.md: model manifest LoRA `-384`→`-384-1.1`, +10Eros unet row, lane matrix
+🔓 10Eros, VRAM note → `ai-studio`.
- audio.md: step-voice ⊕-video mutex bullet (c3 guards it) + start-from-c3 note.
- image.md / FAQ.md / services/studio/README.md: scene-name + bring-up fixups.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
Wire a second uncensored video lane — 10Eros (LTX-2.3-native dev fine-tune) —
alongside Sulphur, plus the updated distill LoRA (`-384-1.1`). The pipe builder
is the source of truth; `studio_pipe.py` is regenerated from it (push to OWUI
via `push-pipe-to-owui.sh`, since OWUI stores the pipe code in its DB).
`🔓 Studio · 10Eros` appears in the OWUI model picker for a direct A/B against
Sulphur on the same single-stage 8-step recipe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
Replace the separate `image-studio` / `video-studio` / `comfyui` scenes with
a single `ai-studio` scene: ComfyUI holds both GPUs always, and image / video /
audio are picked as *lanes* in OWUI rather than gpu-mode switches. Removes the
gemma-12b chat brain from the studio bundle (kept on disk, just unwired) and
points OWUI's DEFAULT_MODELS at the qwen director.
- gpu-mode.sh: `mode_ai_studio` replaces the two studio modes; drop the gemma
start/stop + the GPU0-pin split; preflight now reads the shared manifest.
- scripts/lib/studio-models.tsv: single source-of-truth model manifest (modality,
label, root, rel_path, size, installer) consumed by both gpu-mode preflight and
the c3 cockpit, so the two can't drift.
- download_{director,video,audio,ace_step,stable_audio,studio}_models.sh: idempotent
fetchers; download_studio_models.sh orchestrates "grab everything".
- setup-video-studio.sh + repointed setup-image-studio.sh → `gpu-mode ai-studio`.
- litellm: drop the gemma-4-12b :8069 route; comfyui compose/entrypoint GPU-pin
comments corrected (no scene sets the pin now).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
The studio sidecar images build on scene start, but their MODELS were never
downloaded by any script — so a fresh user's studio-director (the 🖼️ prompt
crafter, in both image- and video-studio), studio-tts, and studio-step-voice
booted with no weights. Add the downloads, all repo-verified:
- setup-image-studio.sh now also fetches the director GGUF + mmproj
(HauhauCS/Qwen3.5-4B-Uncensored-HauhauCS-Aggressive — commit matched the
on-disk metadata) into MODEL_DIR/qwen3.5-4b-gguf/…, closing the image-studio
🖼️ button gap.
- services/comfyui/download_kokoro.sh — kokoro-v1.0.onnx + voices-v1.0.bin from
the kokoro-onnx release assets (the format studio-tts loads).
- services/comfyui/download_step_audio.sh — stepfun-ai/Step-Audio-EditX +
Step-Audio-Tokenizer for studio-step-voice.
Paths mirror each sidecar compose's ${…_DIR} default. bash gates 56/0.
NOT included: the LTX-2.3 / Sulphur video-model set (video-studio) — ~12+ files
across several uploaders; needs a per-file source audit, tracked separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PoXV1FuKmTiDPB15qQvLjB
The comfyui compose hard-coded absolute bind mounts (/mnt/models/comfyui/* and
/mnt/models/huggingface), so a fresh clone on any other machine got empty mounts
→ ComfyUI with no models/custom-nodes. Now the host roots are parameterized:
- COMFYUI_ROOT (default /mnt/models/comfyui) for ComfyUI's own tree — it's a
SIBLING of the HF cache, not under MODEL_DIR, so it gets its own var;
- MODEL_DIR (default /mnt/models/huggingface) for the shared HF cache, matching
what the model composes mount.
Both default to the current rig paths (verified `compose config` is byte-identical
here with and without .env), so nothing changes on this rig; a different host
overrides them in the repo .env (passed via --env-file).
Note: setup-image-studio.sh still hard-codes /mnt/models/comfyui in its disk
preflight — a fully-portable comfyui would parameterize that too (out of scope here).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PoXV1FuKmTiDPB15qQvLjB
Ollama was dropped from serving 2026-05-10 (Qwen/Gemma route through LiteLLM
directly), but its services/ollama/ compose dir was kept "for manual spin-up" —
so the cockpit Containers tab (which lists every services/*/docker-compose.yml)
surfaced it as a stopped service that nothing in the stack starts. Remove it.
- Delete services/ollama/.
- gpu-mode.sh: drop the 9 defensive `stop_service ollama` calls, the :11434
status readout, and fix the stale `chat` usage-help line (it listed Ollama +
omitted SearXNG; the authoritative scene TSV was already correct).
- services/openwebui: drop the dead OLLAMA_BASE_URL env; set ENABLE_OLLAMA_API
=false so Open WebUI doesn't probe a now-dead :11434 on boot.
- cockpit services.py: drop ollama from the supporting-service docstring/comment
examples (it's no longer a known service).
The SERVICES=() array already excluded ollama, so no scene behaviour changes.
bash gates 56/0; cockpit container/known-service tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PoXV1FuKmTiDPB15qQvLjB
OWUI runs title/tags/follow-up/autocomplete tasks against the selected
model; when that's a Studio generation pipe it rendered the task prompts
as images. Pin TASK_MODEL/TASK_MODEL_EXTERNAL to qwen3.5-4b-uncensored
(:8090, always-on) so tasks run on a chat model — follow-ups stay ON, no
junk renders. (Set live in the OWUI DB 2026-06-12; this seeds fresh volumes.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OWUI runs its title/tags/follow-up/autocomplete TASK prompts against the
currently-selected model = the Studio generation pipe, so each chat turn
the pipe was rendering '### Task: Suggest follow-up questions...' as an
(invariably blocked) image — the mysterious extra image. Guard: if the
last user message is an OWUI task prompt (### Task:/### Chat History:/
autocompletion/etc.), return '' without generating. v0.13.2 -> 0.13.3.
(Also recommend setting OWUI's Task Model to a chat model.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>