# app.py """LTX 2.3 All-in-One — Gradio entry point.""" from __future__ import annotations import os import pathlib import random import sys import time from typing import Any import gradio as gr import backend as backend_module import modes import ui import workflow as wf_module # --------------------------------------------------------------------------- # Bootstrap — runs once on cold start. # --------------------------------------------------------------------------- def _on_spaces() -> bool: return bool(os.environ.get("SPACES_ZERO_GPU")) COMFYUI_REPO = "https://github.com/comfyanonymous/ComfyUI.git" COMFYUI_COMMIT = os.environ.get( "LTX23_AIO_COMFYUI_COMMIT", "eb0686bbb60c83e44c3a3e4f7defd0f589cfef10", ) CUSTOM_NODES_PINNED: list[tuple[str, str]] = [ ("https://github.com/Lightricks/ComfyUI-LTXVideo.git", "2acf7af8991f33b5cc06ec26753cb6e88e057d04"), ("https://github.com/kijai/ComfyUI-KJNodes.git", "01d9fa9c983273532cacdf9532c74a93c7dc86d2"), ("https://github.com/rgthree/rgthree-comfy.git", "683836c46e898668936c433502504cc0627482c5"), ("https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite.git", "2984ec4c4b93292421888f38db74a5e8802a8ff8"), ("https://github.com/pythongosssss/ComfyUI-Custom-Scripts.git", "609f3afaa74b2f88ef9ce8d939626065e3247469"), ("https://github.com/city96/ComfyUI-GGUF.git", "6ea2651e7df66d7585f6ffee804b20e92fb38b8a"), ("https://github.com/Fannovel16/comfyui_controlnet_aux.git", "e8b689a513c3e6b63edc44066560ca5919c0576e"), ("https://github.com/evanspearman/ComfyMath.git", "c01177221c31b8e5fbc062778fc8254aeb541638"), ("https://github.com/Smirnov75/ComfyUI-mxToolkit.git", "7f7a0e584f12078a1c589645d866ae96bad0cc35"), ("https://github.com/DoctorDiffusion/ComfyUI-MediaMixer.git", "2bae7b5ea8fc52d8a4d668d62fed76265f4eec2c"), ] def _git_clone(url: str, dst: pathlib.Path, ref: str) -> None: """Clone *url* at *ref* into *dst*. *ref* may be a branch, tag, or SHA. `git clone --branch` only accepts branch/tag names, so we use init+fetch which works for any object GitHub allows fetching (default: reachable commits in public repos). """ import subprocess dst = pathlib.Path(dst) dst.mkdir(parents=True, exist_ok=True) subprocess.check_call(["git", "-C", str(dst), "init", "-q"]) subprocess.check_call(["git", "-C", str(dst), "remote", "add", "origin", url]) subprocess.check_call(["git", "-C", str(dst), "fetch", "--depth", "1", "origin", ref]) subprocess.check_call(["git", "-C", str(dst), "checkout", "-q", "FETCH_HEAD"]) def _mirror_preload_hf_cache() -> None: """Mirror the build-populated HF cache into a writable runtime tree. HF Spaces' build pipeline runs `preload_from_hub` as a different user than the runtime container, so the populated `~/.cache/huggingface/` is read-only for us (uid 1000). Any subsequent `hf_hub_download` call that needs to write a NEW file (lazy-loaded LoRAs, GGUF, etc.) fails with "Permission denied" because the parent dir isn't writable. Fix: build a parallel tree at `~/hf-cache-rw/` that we own, with: - dirs: created fresh via mkdir - blob files (`blobs/`): hardlinked (shared inode, instant) - relative snapshot symlinks: preserved as symlinks - `refs/` files: byte-copied (HF lib overwrites these) - everything else: byte-copied (safest default) Then set HF_HOME / HF_HUB_CACHE so HF lib reads/writes through the mirror. Reads are zero-copy via hardlink/symlink; new downloads land in dirs we created. """ import shutil src_root = pathlib.Path.home() / ".cache" / "huggingface" dst_root = pathlib.Path.home() / "hf-cache-rw" dst_root.mkdir(parents=True, exist_ok=True) os.environ["HF_HOME"] = str(dst_root) os.environ["HF_HUB_CACHE"] = str(dst_root / "hub") if not src_root.exists(): return counts = {"dirs": 0, "hardlinks": 0, "symlinks": 0, "copies": 0, "errors": 0} def _treat_as_copy(rel_path: pathlib.PurePath) -> bool: # Anything under a refs/ dir, anywhere in the tree. return any(part == "refs" for part in rel_path.parts) def _walk(s: pathlib.Path, d: pathlib.Path) -> None: try: d.mkdir(parents=True, exist_ok=True) counts["dirs"] += 1 except OSError as exc: print(f"[bootstrap] mirror mkdir fail {d}: {exc}", flush=True) counts["errors"] += 1 return for entry in s.iterdir(): de = d / entry.name try: if entry.is_symlink(): if de.exists() or de.is_symlink(): continue target = os.readlink(str(entry)) de.symlink_to(target) counts["symlinks"] += 1 elif entry.is_dir(): _walk(entry, de) elif entry.is_file(): if de.exists(): continue rel = de.relative_to(dst_root) if _treat_as_copy(rel): shutil.copy2(entry, de) counts["copies"] += 1 else: try: os.link(str(entry), str(de)) counts["hardlinks"] += 1 except OSError: # Cross-device or other — fall back to symlink. de.symlink_to(entry) counts["symlinks"] += 1 except OSError as exc: print(f"[bootstrap] mirror skip {entry}: {exc}", flush=True) counts["errors"] += 1 _walk(src_root, dst_root) print( f"[bootstrap] hf cache mirrored to {dst_root}: " f"{counts['dirs']} dirs, {counts['hardlinks']} hardlinks, " f"{counts['symlinks']} symlinks, {counts['copies']} copies, " f"{counts['errors']} errors", flush=True, ) def _bootstrap() -> None: on_spaces = _on_spaces() # /data requires the paid persistent-storage add-on (separate from Pro). # Without it, /data is unwritable. $HOME is writable and — because ZeroGPU # containers freeze on sleep rather than tear down — the clone persists # across calls within a single deploy. comfy_dir = (pathlib.Path.home() / "comfyui") if on_spaces else pathlib.Path("comfyui") if on_spaces and not comfy_dir.exists(): print(f"[bootstrap] cold start on Spaces; cloning ComfyUI to {comfy_dir}", flush=True) comfy_dir.parent.mkdir(parents=True, exist_ok=True) _git_clone(COMFYUI_REPO, comfy_dir, ref=COMFYUI_COMMIT) for node_url, node_ref in CUSTOM_NODES_PINNED: name = node_url.rstrip(".git").rsplit("/", 1)[-1] _git_clone(node_url, comfy_dir / "custom_nodes" / name, ref=node_ref) import subprocess # ComfyUI core requirements + each custom node's requirements for req_path in [ comfy_dir / "requirements.txt", *(cn / "requirements.txt" for cn in (comfy_dir / "custom_nodes").iterdir()), ]: if req_path.exists(): print(f"[bootstrap] pip install -r {req_path}", flush=True) subprocess.check_call( [sys.executable, "-m", "pip", "install", "--quiet", "-r", str(req_path)] ) if str(comfy_dir) not in sys.path: sys.path.insert(0, str(comfy_dir)) os.environ.setdefault("COMFY_MODELS_DIR", str(comfy_dir / "models")) # Mirror the build-time HF cache (populated by preload_from_hub, owned by # build user → read-only for runtime user 1000) into a writable parallel # tree under $HOME, then point HF_HUB_CACHE / HF_HOME at it. After this: # - preloaded blobs are accessible via hardlink (no data copy, instant reads) # - relative snapshot symlinks resolve within the mirror # - refs/* are byte-copies so HF lib can overwrite when commits advance # - new lazy-downloaded files write to dirs we own → no permission errors if on_spaces: _mirror_preload_hf_cache() # Stage placeholder input files so the workflow's hard-referenced loaders # (LoadImage/VHS_Load*) don't error at runtime even when the active mode # doesn't actually use the file. Real user uploads are placed alongside via # `_stage_to_comfy_input` later. seed_dir = pathlib.Path(__file__).parent / "assets" / "seed_inputs" inputs_dir = comfy_dir / "input" inputs_dir.mkdir(parents=True, exist_ok=True) if seed_dir.exists(): import shutil for src in seed_dir.iterdir(): if not src.is_file(): continue dst = inputs_dir / src.name if not dst.exists(): try: shutil.copy2(src, dst) except OSError as exc: print(f"[bootstrap] could not seed {src.name}: {exc}", flush=True) _bootstrap() # --------------------------------------------------------------------------- # Styling: hide the default top tab strip (drawer nav drives selection), # add status-card styling, plus single responsive breakpoint at 1023 px # (drawer slides over body) / 1024 px+ (drawer pinned). # --------------------------------------------------------------------------- _CUSTOM_CSS = """ /* Hide Gradio's top tab strip — sidebar drives selection. */ .aio-tabs > .tab-nav, .aio-tabs > div:first-child[role="tablist"], .aio-tabs > div:first-child:has([role="tab"]) { position: absolute !important; left: -99999px !important; top: -99999px !important; height: 0 !important; overflow: hidden !important; visibility: visible !important; pointer-events: auto !important; } /* === Header === */ .aio-header { display: flex; align-items: center; gap: 12px; padding: 11px 18px; border-bottom: 1px solid #262C35; background: #12161B; position: relative; /* HF injects #huggingface-space-header at fixed z-index 20 (top-right like/share widget). Stay below it by default so we don't cover it. */ z-index: 15; } /* When drawer is open, lift header above scrim (z-45) and drawer (z-50) so the hamburger flips to × and remains clickable as a close affordance. Toggled in lockstep with .aio-shell.drawer-open via the inline JS below. */ .aio-header.drawer-elevated { z-index: 60; } .aio-ham-label { display: none; width: 32px; height: 32px; border: 1px solid #262C35; border-radius: 5px; color: #7C8693; cursor: pointer; align-items: center; justify-content: center; font-size: 18px; font-weight: 300; user-select: none; } .aio-ham-label:hover { color: #E0A458; border-color: #E0A458; } .aio-title { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; color: #E6E8EB; } .aio-title .accent { color: #E0A458; } .aio-mode-tag { margin-left: auto; padding: 4px 9px; font-family: 'IBM Plex Mono', ui-monospace, monospace; font-size: 11px; font-weight: 500; letter-spacing: 0.04em; color: #E0A458; border: 1px solid #E0A458; border-radius: 4px; } .aio-tipbar { margin: 0 0 6px 0; padding: 6px 14px; font-family: 'IBM Plex Sans', system-ui, sans-serif; font-size: 12px; color: #B5BCC6; background: #1A1F26; border-bottom: 1px solid #262C35; text-align: center; } .aio-tipbar strong { color: #E6E8EB; font-weight: 500; } .aio-tipbar .aio-heart { color: #E55B6E; } .aio-mode-warning { margin: 4px 0 10px 0 !important; padding: 10px 14px !important; font-family: 'IBM Plex Sans', system-ui, sans-serif !important; font-size: 12px !important; line-height: 1.55 !important; color: #D4C18B !important; background: rgba(224, 164, 88, 0.08) !important; border-left: 3px solid #E0A458 !important; border-radius: 4px !important; } .aio-mode-warning strong { color: #E0A458 !important; font-weight: 500 !important; } .aio-hf-tip { margin: 12px 0 8px 0 !important; padding: 9px 14px !important; font-family: 'IBM Plex Sans', system-ui, sans-serif !important; font-size: 11.5px !important; line-height: 1.5 !important; color: #9CA8B5 !important; background: rgba(124, 134, 147, 0.06) !important; border-left: 3px solid #5C6671 !important; border-radius: 4px !important; } .aio-hf-tip strong { color: #C8D0DA !important; font-weight: 500 !important; } /* === Drawer === */ .aio-shell { position: relative; } .aio-drawer { width: 220px; border-right: 1px solid #262C35; background: #12161B; padding: 14px 10px !important; flex-shrink: 0; transition: left 0.2s ease; } .aio-drawer-heading { font-family: 'IBM Plex Mono', ui-monospace, monospace; font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: #7C8693; padding: 6px 8px 4px !important; margin: 0 !important; } /* Mode buttons */ .aio-mode-btn { width: 100%; text-align: left; margin: 2px 0 !important; } .aio-mode-btn-active { background: #1A1F26 !important; color: #E0A458 !important; border-left: 3px solid #E0A458 !important; } /* Model status / settings panels */ .aio-model-badge { padding: 9px 11px; border-radius: 6px; background: #1A1F26; border: 1px solid #262C35; font-size: 11.5px; font-family: 'IBM Plex Mono', ui-monospace, monospace; color: #7C8693; } /* === Status banner === */ .status-card { padding: 12px 16px; border-radius: 6px; background: #1A1F26; border: 1px solid #262C35; } .status-row { display: flex; gap: 14px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; } .status-stage { font-weight: 600; color: #E0A458; } .status-meta { font-size: 12px; color: #7C8693; font-family: 'IBM Plex Mono', ui-monospace, monospace; } .status-bar { height: 4px; background: #262C35; border-radius: 99px; overflow: hidden; } .status-fill { height: 100%; background: #E0A458; transition: width .3s; } .status-mem { font-size: 11px; color: #7C8693; margin-top: 6px; font-family: 'IBM Plex Mono', ui-monospace, monospace; } .status-error { background: #3A1E20 !important; border-color: #F4A6A8 !important; color: #F4A6A8 !important; } .status-error .status-stage { color: #F4A6A8; } /* === Drawer toggle behavior at the desktop boundary === */ @media (max-width: 1023px) { .aio-ham-label { display: flex; } .aio-drawer { position: fixed; top: 0; bottom: 0; left: -100%; z-index: 50; box-shadow: 4px 0 24px rgba(0,0,0,0.6); max-width: 80vw; overflow-y: auto; overflow-x: hidden; padding-top: 80px !important; } /* `.aio-shell.drawer-open` is toggled by the hamburger's inline JS. `body:has(:checked)` would be cleaner but Gradio prefixes user CSS with `.gradio-container .contain `, breaking ancestor selectors. */ .aio-shell.drawer-open .aio-drawer { left: 0; } .aio-shell.drawer-open::before { content: ""; position: fixed; inset: 0; background: rgba(0,0,0,0.92); z-index: 45; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); } /* Mobile sub-tweaks */ .aio-mode-btn { font-size: 13px !important; padding: 7px 10px !important; } .aio-body [class*="row"] { flex-wrap: wrap !important; } .aio-body [class*="row"] > div { flex: 1 1 100% !important; min-width: 0 !important; } } @media (min-width: 1024px) { .aio-ham-label { display: none; } } """ # --------------------------------------------------------------------------- # UI # --------------------------------------------------------------------------- _TOPAZ_THEME = gr.themes.Base( primary_hue=gr.themes.Color( c50="#FBE5C7", c100="#F5D29C", c200="#EFC174", c300="#E9B05A", c400="#E5A75B", c500="#E0A458", c600="#C68D3F", c700="#A6722E", c800="#7E5722", c900="#583C18", c950="#3A2810", ), neutral_hue=gr.themes.Color( c50="#E6E8EB", c100="#C9CDD3", c200="#ACB1B9", c300="#9097A0", c400="#7C8693", c500="#626972", c600="#4A4F58", c700="#363B43", c800="#262C35", c900="#1A1F26", c950="#12161B", ), font=(gr.themes.GoogleFont("IBM Plex Sans"), "ui-sans-serif", "system-ui", "sans-serif"), font_mono=(gr.themes.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace"), ).set( body_background_fill="#12161B", background_fill_primary="#12161B", background_fill_secondary="#1A1F26", block_background_fill="#1A1F26", block_label_background_fill="transparent", body_text_color="#E6E8EB", body_text_color_subdued="#7C8693", border_color_primary="#262C35", border_color_accent="#E0A458", button_primary_background_fill="#E0A458", button_primary_background_fill_hover="#F0B870", button_primary_text_color="#12161B", button_secondary_background_fill="#1A1F26", button_secondary_background_fill_hover="#232930", button_secondary_text_color="#E6E8EB", button_secondary_border_color="#262C35", input_background_fill="#12161B", input_border_color="#262C35", input_border_color_focus="#E0A458", error_background_fill="#3A1E20", error_text_color="#F4A6A8", slider_color="#E0A458", ) _HEAD_HTML = """ """ def build_app() -> gr.Blocks: with gr.Blocks(theme=_TOPAZ_THEME, title="LTX 2.3 Studio", css=_CUSTOM_CSS, head=_HEAD_HTML) as app: # Header: hamburger button toggles `.drawer-open` on `.aio-shell`. # The click-outside dismisser is registered via gr.Blocks(head=...) # below — Gradio strips