Skip to content

Commit 72cb427

Browse files
authored
[feat]: add FastLTX-2.3 Gradio demo package (draft) (#1247)
1 parent 63030cf commit 72cb427

14 files changed

Lines changed: 3382 additions & 0 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# FastLTX-2.3 Gradio local demo
2+
3+
Local Gradio + FastAPI demo for FastLTX-2.3 text-to-video generation. This
4+
directory is a package (`gradio_local_demo_ltx2_3/`) split out from the
5+
original single-file version to make each concern independently reviewable.
6+
The folder name matches the sibling `gradio_local_demo*.py` demos in this
7+
directory so both flat and packaged demos read consistently.
8+
9+
> **Status: draft.** This package is structurally in place but will not run
10+
> against the current upstream `fastvideo` package. See *Blocking prereqs*
11+
> below.
12+
13+
## Layout
14+
15+
| File | Purpose |
16+
| --- | --- |
17+
| `app.py` | `main()` — CLI args, `VideoGenerator` / `SamplingParam` boot, FastAPI mount with logo/favicon/generated-clip routes, `uvicorn.run`. |
18+
| `config.py` | Constants, defaults, env-var resolution, Inductor tuning flags, `setup_model_environment`, `resolve_model_path`, `resolve_refine_upsampler_path`, `apply_ltx2_defaults`. |
19+
| `safety.py` | fastText NSFW + hate-speech classifiers, `PromptSafetyCheck`, `get_prompt_safety_check`. |
20+
| `prompt_rewrite.py` | Cerebras-backed prompt enhancer wrapper (`maybe_enhance_prompt`, `get_prompt_enhancer`). Curated prompts bypass enhancement. |
21+
| `prompt_enhancer.py` | Cerebras API client used by `prompt_rewrite.py`. |
22+
| `rendering.py` | HTML helpers: timing cards, error cards, completed-clip gallery, image-upload status. |
23+
| `examples.py` | `load_example_prompts` — reads `selected_ltx2_prompts.jsonl`. |
24+
| `ui.py` | `create_gradio_interface` — Gradio Blocks, CSS, event wiring, generation closure. |
25+
| `__main__.py` | Enables `python -m gradio_local_demo_ltx2_3`. |
26+
| `__init__.py` | Re-exports `main` from `app`. |
27+
| `selected_ltx2_prompts.jsonl` | Curated example prompts. |
28+
| `prompts/prompt_extension_system_prompt.md` | System prompt for the Cerebras enhancer. |
29+
| `download_fasttext_classifiers.py` | Helper to download NSFW/hate-speech classifier binaries from Hugging Face Hub. |
30+
31+
## How to run
32+
33+
```bash
34+
cd examples/inference/gradio/local
35+
python -m gradio_local_demo_ltx2_3 --port 7860
36+
```
37+
38+
GPU requirement: a single FP4-capable GPU (B200 or comparable) for the
39+
"real-time 1080p" speed claim. Lower tiers will still run but slower.
40+
41+
## Blocking prereqs (why this draft PR cannot be merged yet)
42+
43+
The upstream `fastvideo` package is missing three pieces that the demo
44+
currently depends on verbatim. Each needs its own upstreaming PR before this
45+
demo can actually boot:
46+
47+
1. **`fastvideo.layers.quantization.fp4_config.FP4Config`** — the demo sets
48+
`pipeline_config.dit_config.quant_config = FP4Config()` in `app.py`.
49+
Upstream only ships `absmax_fp8.py` and `base_config.py` under
50+
`fastvideo/layers/quantization/`.
51+
2. **LTX-2.3 refine / image-conditioning kwargs on `VideoGenerator`**
52+
`ltx2_refine_enabled`, `ltx2_refine_upsampler_path`, `ltx2_refine_lora_path`,
53+
`ltx2_refine_num_inference_steps`, `ltx2_refine_guidance_scale`,
54+
`ltx2_refine_add_noise`, `ltx2_images`, `ltx2_image_crf`. Upstream
55+
`fastvideo/fastvideo_args.py` currently wires only `ltx2_vae_tiling`.
56+
The backing stages (`ltx2_refine.py`, `ltx2_i2v_conditioning.py`) are
57+
also missing from `fastvideo/pipelines/stages/`.
58+
3. **`fastvideo.configs.sample.base.SamplingParam`** — the import path used
59+
by this demo. Upstream moved sampling params to
60+
`fastvideo.api.sampling_param`. A re-export shim at the old path, or an
61+
import update here once the other two prereqs land, will resolve it.
62+
63+
## Environment variables
64+
65+
| Var | Default | Purpose |
66+
| --- | --- | --- |
67+
| `LTX2_3_MODEL_PATH` | `FastVideo/LTX-2.3-Distilled-Diffusers` | Model ID or local snapshot. |
68+
| `LTX2_CLASSIFIER_DIR` | package dir | Where to look for fastText classifiers. |
69+
| `LTX2_NSFW_CLASSIFIER_PATH` || Explicit path to NSFW classifier `.bin`. |
70+
| `LTX2_HATESPEECH_CLASSIFIER_PATH` || Explicit path to hate-speech classifier `.bin`. |
71+
| `LTX2_REFINE_UPSAMPLER_PATH` || Explicit path to the spatial upsampler dir. |
72+
| `FASTVIDEO_PROMPT_API_KEY` / `CEREBRAS_API_KEY` || Cerebras API key for prompt enhancement. When missing, enhancer returns the raw prompt. |
73+
| `LTX2_PROMPT_MODEL` | `gpt-oss-120b` | Cerebras model name for the enhancer. |
74+
| `LTX2_PROMPT_TEMPERATURE` | `1.0` | Enhancer LLM temperature. |
75+
| `LTX2_PROMPT_EXTENSION_SYSTEM_PROMPT_PATH` | `prompts/prompt_extension_system_prompt.md` | System prompt for the enhancer. |
76+
77+
## Fetching the safety classifiers
78+
79+
```bash
80+
python examples/inference/gradio/local/gradio_local_demo_ltx2_3/download_fasttext_classifiers.py
81+
```
82+
83+
The classifiers come from `allenai/dolma-jigsaw-fasttext-bigrams-{nsfw,hatespeech}`
84+
on Hugging Face Hub.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""FastLTX-2.3 Gradio local demo package.
2+
3+
Split from the monolithic gradio_local_demo_ltx2_3.py for maintainability.
4+
Runtime entrypoint is `main` in .app.
5+
"""
6+
7+
from .app import main
8+
9+
__all__ = ["main"]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .app import main
2+
3+
if __name__ == "__main__":
4+
main()
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import argparse
2+
import os
3+
from pathlib import Path
4+
5+
import gradio as gr
6+
7+
from fastvideo.configs.pipelines.base import PipelineConfig
8+
from fastvideo.configs.sample.base import SamplingParam
9+
from fastvideo.entrypoints.video_generator import VideoGenerator
10+
from fastvideo.layers.quantization.fp4_config import FP4Config
11+
from fastvideo.utils import maybe_download_model
12+
13+
from .config import (
14+
GENERATED_CLIP_ROOT,
15+
MODEL_ID,
16+
apply_ltx2_defaults,
17+
resolve_model_path,
18+
resolve_refine_upsampler_path,
19+
setup_model_environment,
20+
)
21+
from .ui import create_gradio_interface
22+
23+
def main():
24+
parser = argparse.ArgumentParser(description="FastVideo Gradio Local Demo")
25+
parser.add_argument("--t2v_model_paths", type=str,
26+
default=MODEL_ID,
27+
help="Comma separated list of paths to the T2V model(s)")
28+
parser.add_argument("--host", type=str, default="0.0.0.0",
29+
help="Host to bind to")
30+
parser.add_argument("--port", type=int, default=7860,
31+
help="Port to bind to")
32+
args = parser.parse_args()
33+
gradio_temp_dir = os.path.abspath("outputs/gradio_tmp")
34+
os.makedirs(gradio_temp_dir, exist_ok=True)
35+
os.environ["GRADIO_TEMP_DIR"] = gradio_temp_dir
36+
generators = {}
37+
default_params = {}
38+
model_paths = args.t2v_model_paths.split(",")
39+
for model_path in model_paths:
40+
print(f"Loading model: {model_path}")
41+
setup_model_environment(model_path)
42+
resolved_model_input = str(resolve_model_path(model_path))
43+
model_root = maybe_download_model(resolved_model_input)
44+
resolved_model_path = Path(model_root)
45+
46+
pipeline_config = PipelineConfig.from_pretrained(str(resolved_model_path))
47+
pipeline_config.dit_config.quant_config = FP4Config()
48+
refine_upsampler_path = resolve_refine_upsampler_path(resolved_model_path)
49+
print(f"Using refine upsampler: {refine_upsampler_path}")
50+
51+
generators[model_path] = VideoGenerator.from_pretrained(
52+
str(resolved_model_path),
53+
num_gpus=1,
54+
ltx2_refine_enabled=True,
55+
ltx2_refine_upsampler_path=str(refine_upsampler_path),
56+
ltx2_refine_lora_path="", # disable refine LoRA for distilled model
57+
ltx2_refine_num_inference_steps=2,
58+
ltx2_refine_guidance_scale=1.0,
59+
ltx2_refine_add_noise=True,
60+
pipeline_config=pipeline_config,
61+
enable_torch_compile=True,
62+
enable_torch_compile_text_encoder=True,
63+
torch_compile_kwargs={
64+
"backend": "inductor",
65+
"fullgraph": True,
66+
"mode": "max-autotune-no-cudagraphs",
67+
"dynamic": False,
68+
},
69+
dit_cpu_offload=False,
70+
vae_cpu_offload=False,
71+
text_encoder_cpu_offload=False,
72+
ltx2_vae_tiling=False,
73+
)
74+
default_params[model_path] = apply_ltx2_defaults(
75+
SamplingParam.from_pretrained(str(resolved_model_path))
76+
)
77+
demo = create_gradio_interface(default_params, generators)
78+
print(f"Starting Gradio frontend at http://{args.host}:{args.port}")
79+
print(f"T2V Models: {args.t2v_model_paths}")
80+
81+
from fastapi import FastAPI, Request, HTTPException
82+
from fastapi.responses import HTMLResponse, FileResponse
83+
import uvicorn
84+
85+
app = FastAPI()
86+
87+
@app.get("/logo.png")
88+
def get_logo():
89+
return FileResponse(
90+
"assets/full.svg",
91+
media_type="image/svg+xml",
92+
headers={
93+
"Cache-Control": "public, max-age=3600",
94+
"Access-Control-Allow-Origin": "*"
95+
}
96+
)
97+
98+
@app.get("/nvidia.png")
99+
def get_nvidia_logo():
100+
return FileResponse(
101+
"assets/nv.png",
102+
media_type="image/png",
103+
headers={
104+
"Cache-Control": "public, max-age=3600",
105+
"Access-Control-Allow-Origin": "*"
106+
}
107+
)
108+
109+
@app.get("/favicon.ico")
110+
def get_favicon():
111+
favicon_path = "assets/icon-simple.svg"
112+
113+
if os.path.exists(favicon_path):
114+
return FileResponse(
115+
favicon_path,
116+
media_type="image/svg+xml",
117+
headers={
118+
"Cache-Control": "public, max-age=3600",
119+
"Access-Control-Allow-Origin": "*"
120+
}
121+
)
122+
else:
123+
raise HTTPException(status_code=404, detail="Favicon not found")
124+
125+
@app.get("/generated-clips/{clip_path:path}")
126+
def get_generated_clip(clip_path: str):
127+
root = GENERATED_CLIP_ROOT.resolve()
128+
resolved_path = (root / clip_path).resolve()
129+
130+
if root not in resolved_path.parents or not resolved_path.is_file():
131+
raise HTTPException(status_code=404, detail="Clip not found")
132+
133+
return FileResponse(
134+
resolved_path,
135+
media_type="video/mp4",
136+
headers={
137+
"Cache-Control": "no-store",
138+
"Access-Control-Allow-Origin": "*",
139+
},
140+
)
141+
142+
@app.get("/", response_class=HTMLResponse)
143+
def index(request: Request):
144+
base_url = str(request.base_url).rstrip('/')
145+
return f"""
146+
<!DOCTYPE html>
147+
<html lang="en">
148+
<head>
149+
<meta charset="UTF-8" />
150+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
151+
152+
<title>FastLTX-2.3</title>
153+
<meta name="title" content="FastLTX-2.3">
154+
<meta name="description" content="Make video generation go blurrrrrrr">
155+
<meta name="keywords" content="FastVideo, video generation, AI, machine learning, FastLTX-2.3">
156+
157+
<meta property="og:type" content="website">
158+
<meta property="og:url" content="{base_url}/">
159+
<meta property="og:title" content="FastLTX-2.3">
160+
<meta property="og:description" content="Make video generation go blurrrrrrr">
161+
<meta property="og:image" content="{base_url}/logo.png">
162+
<meta property="og:image:width" content="1200">
163+
<meta property="og:image:height" content="630">
164+
<meta property="og:site_name" content="FastLTX-2.3">
165+
166+
<meta property="twitter:card" content="summary_large_image">
167+
<meta property="twitter:url" content="{base_url}/">
168+
<meta property="twitter:title" content="FastLTX-2.3">
169+
<meta property="twitter:description" content="Make video generation go blurrrrrrr">
170+
<meta property="twitter:image" content="{base_url}/logo.png">
171+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.ico">
172+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon.ico">
173+
<link rel="apple-touch-icon" href="/favicon.ico">
174+
<style>
175+
body, html {{
176+
margin: 0;
177+
padding: 0;
178+
min-height: 100%;
179+
width: 100%;
180+
background: #000;
181+
background-color: #000;
182+
background-image: none;
183+
overscroll-behavior-y: auto;
184+
scroll-behavior: smooth;
185+
}}
186+
body {{
187+
position: relative;
188+
}}
189+
body::before {{
190+
content: "";
191+
position: fixed;
192+
inset: 0;
193+
background: #000;
194+
pointer-events: none;
195+
z-index: -1;
196+
}}
197+
iframe {{
198+
display: block;
199+
width: 100%;
200+
height: 100vh;
201+
background: #000;
202+
background-color: #000;
203+
background-image: none;
204+
border: none;
205+
}}
206+
</style>
207+
</head>
208+
<body>
209+
<iframe src="/gradio" width="100%" height="100%" style="border: none;"></iframe>
210+
</body>
211+
</html>
212+
"""
213+
214+
app = gr.mount_gradio_app(
215+
app,
216+
demo,
217+
path="/gradio",
218+
allowed_paths=[
219+
os.path.abspath("outputs"),
220+
os.path.abspath("outputs_video"),
221+
os.path.abspath("fastvideo-logos"),
222+
]
223+
)
224+
225+
uvicorn.run(app, host=args.host, port=args.port)

0 commit comments

Comments
 (0)