# 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 _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"))
# 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;
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;
}
/* === 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