# Project Guidelines — ltx2.3-AIO-generator Working notes for AI assistants and subagents implementing this project. > Companion: see `SKILLS.md` for process rules — how to investigate, verify, > commit, and ship changes here. This file is the *what* and *why*; SKILLS.md > is the *how*. --- ## ⚠ Git authorship — sole author rule **Mayank Gupta is the sole author on every commit in this repo.** No exceptions. When committing: - Do **NOT** append `Co-Authored-By: Claude ...` (or any other agent name). - Do **NOT** add "Generated with Claude Code" / "🤖 Generated with..." footers. - Do **NOT** pass `--author=...` — let git use the user's existing config. - Do **NOT** include attribution in PR descriptions. If asked to amend, re-commit, or rebase, strip any prior agent attribution from the commit message. Treat any tooling that suggests adding a Claude trailer as a bug to ignore. --- ## Project overview Gradio app wrapping the existing ComfyUI LTX 2.3 All-In-One workflow into mode-specific UIs. Same code runs locally (Apple Silicon MPS / NVIDIA CUDA) and on Hugging Face Spaces (ZeroGPU, Pro tier). **Spec:** `docs/superpowers/specs/2026-04-30-ltx23-aio-generator-design.md` **Plan:** `docs/superpowers/plans/2026-04-30-ltx23-aio-generator.md` **Future-improvements backlog:** `docs/future_improvements.md` If you're a subagent picking up a task, the plan file is your assignment. --- ## Modes (six) `t2v` text→video · `i2v` image→video · `a2v` audio→video · `lipsync` (image+audio) · `keyframe` (first+last frame→video) · `style` (preprocessor + IC-LoRA → restyle). Each is a separate API-format JSON in `workflows/`. Per-mode parameter patches live in `modes.py` `parameterize_fn`. --- ## Architectural facts (locked — do not relitigate) 1. **Backend is ComfyUI in library mode.** We call `comfy.execution.PromptExecutor` directly with workflow JSONs we parameterize. We do NOT run ComfyUI as a subprocess. 2. **Six mode-specific workflow JSON files** in `workflows/` are user-exported "API format" from the master workflow. Do not hand-edit. Editor-format (with `nodes` array) does NOT work — `walk_workflow_for_models` and `PromptExecutor` both expect API format. 3. **Models live in HF cache.** Local: `~/.cache/huggingface/hub` symlinked into `comfyui/models//`. Spaces: same hub cache mirrored into `~/hf-cache-rw/` (see "Spaces deployment" below). Never commit `*.safetensors`, `*.gguf`, `*.bin`, `*.pt`. The `assets/seed_inputs/` exception in `.gitignore` covers the small placeholder files. 4. **One backend, one process.** The `@spaces.GPU` decorator is the only divergence between local and Spaces runtimes. 5. **VRAM is ComfyUI's job.** The only `empty_cache()` calls live in `backend.py`'s `try/finally`. Don't sprinkle them elsewhere. 6. **Bundled ComfyUI, never user's existing.** Local: git submodule. Spaces: runtime clone via `_git_clone()` in `app.py:_bootstrap()`. 7. **comfy_dir resolves per-platform.** `~/comfyui` on Spaces (writable HOME), `/comfyui` locally. Both `app.py` and `backend.py` have `_comfy_dir()`-style helpers that MUST stay in sync. 8. **Custom nodes are pinned to SHAs**, not branches. See `CUSTOM_NODES_PINNED` in `app.py`. `--branch ` doesn't work in `git clone`; we use init+fetch+checkout via `_git_clone()`. --- ## Spaces deployment specifics (where the gotchas live) ### Model loading: `preload_from_hub` + runtime cache mirror HF Spaces' `preload_from_hub` directive in README YAML downloads listed files at build time into `~/.cache/huggingface/hub`. **Limitation: those files are owned by the build user** (root-ish). At runtime we run as uid 1000 and can't write there — any `hf_hub_download` for a non-preloaded file fails with `Permission denied (os error 13)`. **Fix:** `_mirror_preload_hf_cache()` in `app.py` walks the read-only preload tree once at bootstrap and builds a parallel writable tree at `~/hf-cache-rw/`: - `blobs/` files → **hardlinked** (zero-copy, shared inode, instant reads) - `snapshots//...` symlinks → **preserved** (relative paths resolve within the mirror) - `refs/` → **byte-copied** (HF lib overwrites these on etag check; hardlinks would fail) - All dirs → mkdir (we own them) - Falls back to symlink if `os.link()` returns EXDEV (cross-device) Then sets `HF_HOME=~/hf-cache-rw` and `HF_HUB_CACHE=~/hf-cache-rw/hub`. After this, preloaded reads are instant cache hits AND lazy downloads write to dirs we own. The 10-entry cap on `preload_from_hub` is a hard HF limit. Total preload size cap is 150 GB (Spaces ephemeral storage). Current list is ~111 GB; see `docs/future_improvements.md` for what got dropped (84 GB of unused Lightricks transformers, 39 GB GGUF — both lazy-load when actually referenced). ### Per-call ZeroGPU duration: dynamic estimator + auto-retry `@spaces.GPU(duration=N)` is a per-call timeout, not a billing cap. Shorter declared duration = faster queue priority on the shared pool. Setting a one-size-fits-all 600s caps everything in the slow lane. **`_duration_for(executor, workflow, output_ids, mode, preset, multiplier=1.0)`** in `backend.py` estimates from: - `_BASE_DURATION_S[mode]` — t2v 90s, lipsync 240s, style 360s, etc. - `_PRESET_MULT[preset]` — fast 1×, balanced 1.5×, quality 3× - `_frames_from_workflow(workflow)` — read from `EmptyLTXVLatentVideo` `length` - +60s cold-cache buffer, +0.3s/frame VAE decode - Clamped to `[60s, 900s]` `@spaces.GPU(duration=_duration_for)` decorates `_execute_workflow` — ZeroGPU calls the estimator with the same args. **Auto-retry on timeout** in `_on_generate` (app.py): if first attempt raises `gradio.exceptions.Error('GPU task aborted')`, classified as `category='gpu_timeout'`, the handler shows a "Retrying with extended GPU budget" banner and re-submits with `duration_multiplier=2.0`. The estimator clamps the retry at 900s anyway. One retry only. ### Returning the video path through ZeroGPU's subprocess boundary `executor.history_result` was unreliable across the `@spaces.GPU` boundary — sometimes the parent process saw an empty dict even when the file was on disk. Fix: `_execute_workflow` reads `history_result["outputs"]` INSIDE the GPU context and returns the path string directly (picklable). Plus a filesystem fallback `_newest_recent_video()` that scans `comfyui/output/` for the newest mp4 modified in the last 60s. ### `allowed_paths` for video output Gradio 5 refuses to expose files outside cwd / temp / `allowed_paths`. ComfyUI writes to `~/comfyui/output/...` which is outside our app's cwd `/home/user/app` on Spaces. `app.launch(..., allowed_paths=[str(_output_dir)])` whitelists the entire ComfyUI output tree. Without this, video generates fine but `gr.Video` shows blank. ### HF Spaces' header widget z-index (DOM-injected) When a Space is loaded via the bare embed URL (`https://*.hf.space`), HF injects `#huggingface-space-header` at fixed `z-index: 20` in the top-right (the heart/share widget). Our header z-index has to coexist: - Default: header `z-index: 15` (below HF widget — visible) - Drawer open: `.drawer-elevated` class bumps to `z-index: 60` (above scrim 45 / drawer 50, hamburger × clickable as close) JS toggles `.drawer-elevated` on `.aio-header` in lockstep with `.drawer-open` on `.aio-shell`. Three call sites: hamburger onclick, click-outside dismisser (in `gr.Blocks(head=...)` because `