linoyts HF Staff commited on
Commit
1bf61b0
·
verified ·
1 Parent(s): 9792fcc

initial backend

Browse files
Files changed (4) hide show
  1. README.md +16 -6
  2. __pycache__/app.cpython-310.pyc +0 -0
  3. app.py +294 -0
  4. requirements.txt +9 -0
README.md CHANGED
@@ -1,13 +1,23 @@
1
  ---
2
- title: Ltx23 Dev Api
3
- emoji: 👁
4
  colorFrom: pink
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.19.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
1
  ---
2
+ title: LTX-2.3 Dev API
3
+ emoji: 🎬
4
  colorFrom: pink
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 6.13.0
8
+ python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
+ hardware: zero-a10g
12
+ short_description: Hero backend for the LTX-2.3 pre-production playground
13
+ models:
14
+ - Lightricks/LTX-2.3
15
  ---
16
 
17
+ # 🎬 LTX-2.3 dev two-stage — hero backend
18
+
19
+ Private API backend (native `TI2VidTwoStagesPipeline`: dev checkpoint + CFG/STG stage 1,
20
+ distilled-LoRA stage 2 + x2 upsampler) for the LTX-2.3 Pre-Production
21
+ Playground frontend. Set `HF_TOKEN` (gated `Lightricks/LTX-2.3`).
22
+
23
+ Text encoding (positive + negative for CFG) offloaded to the Gemma encoder Space (`TEXT_ENCODER_SPACE`, default `linoyts/ltx23-gemma-encoder-api`); `encode_prompts` is monkeypatched with precomputed `[ctx_p, ctx_n]`. With Gemma off-board, VRAM is ~2 fp8 transformers + VAEs — comfortable on 96 GB.
__pycache__/app.cpython-310.pyc ADDED
Binary file (10.8 kB). View file
 
app.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LTX-2.3 dev two-stage T2V backend (API Space for the Pre-Production Playground frontend).
2
+
3
+ TI2VidTwoStagesPipeline: dev checkpoint stage 1 with CFG/STG guidance, distilled-LoRA
4
+ stage 2 + x2 spatial upsampler. Text encoding (positive AND negative, for CFG) is
5
+ offloaded to the Gemma encoder Space (TEXT_ENCODER_SPACE) — encode_prompts is
6
+ monkeypatched with the precomputed [ctx_p, ctx_n]. Same ZeroGPU setup as the distilled
7
+ backend.
8
+ """
9
+
10
+ import os
11
+ import subprocess
12
+ import sys
13
+
14
+ os.environ["TORCH_COMPILE_DISABLE"] = "1"
15
+ os.environ["TORCHDYNAMO_DISABLE"] = "1"
16
+
17
+ subprocess.run([sys.executable, "-m", "pip", "install", "xformers==0.0.32.post2",
18
+ "--no-build-isolation"], check=False)
19
+
20
+ LTX_REPO_URL = "https://github.com/Lightricks/LTX-2.git"
21
+ LTX_REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LTX-2")
22
+ LTX_COMMIT_SHA = os.environ.get("LTX_COMMIT_SHA", "ae855f8538843825f9015a419cf4ba5edaf5eec2")
23
+
24
+ if not os.path.exists(LTX_REPO_DIR):
25
+ os.makedirs(LTX_REPO_DIR)
26
+ subprocess.run(["git", "init", LTX_REPO_DIR], check=True)
27
+ subprocess.run(["git", "remote", "add", "origin", LTX_REPO_URL], cwd=LTX_REPO_DIR, check=True)
28
+ subprocess.run(["git", "fetch", "--depth", "1", "origin", LTX_COMMIT_SHA], cwd=LTX_REPO_DIR, check=True)
29
+ subprocess.run(["git", "checkout", LTX_COMMIT_SHA], cwd=LTX_REPO_DIR, check=True)
30
+
31
+ subprocess.run(
32
+ [sys.executable, "-m", "pip", "install", "--force-reinstall", "--no-deps", "-e",
33
+ os.path.join(LTX_REPO_DIR, "packages", "ltx-core"),
34
+ "-e", os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines")],
35
+ check=True,
36
+ )
37
+ sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines", "src"))
38
+ sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-core", "src"))
39
+
40
+ import json
41
+ import logging
42
+ import random
43
+ import struct
44
+ import tempfile
45
+
46
+ import torch
47
+ torch._dynamo.config.suppress_errors = True
48
+ torch._dynamo.config.disable = True
49
+
50
+ import gradio as gr
51
+ import numpy as np
52
+ import spaces
53
+ from gradio_client import Client
54
+ from huggingface_hub import hf_hub_download
55
+
56
+ import ltx_pipelines.ti2vid_two_stages as ti2vid_module
57
+ from ltx_core.components.guiders import MultiModalGuiderParams
58
+ from ltx_core.loader import LoraPathStrengthAndSDOps
59
+ from ltx_core.model.video_vae import TilingConfig, get_video_chunks_number
60
+ from ltx_core.quantization import QuantizationPolicy
61
+ from ltx_core.text_encoders.gemma.embeddings_processor import EmbeddingsProcessorOutput
62
+ from ltx_pipelines.ti2vid_two_stages import TI2VidTwoStagesPipeline
63
+ from ltx_pipelines.utils.media_io import encode_video
64
+
65
+ # xformers attention: force-patch + disable Hopper-only FA3 dispatch (Blackwell ZeroGPU).
66
+ from ltx_core.model.transformer import attention as _attn_mod
67
+ try:
68
+ from xformers.ops import memory_efficient_attention as _mea
69
+ _attn_mod.memory_efficient_attention = _mea
70
+ from xformers.ops.fmha import _set_use_fa3
71
+ _set_use_fa3(False)
72
+ print("[ATTN] xformers patched, FA3 disabled")
73
+ except Exception as e:
74
+ print(f"[ATTN] xformers patch failed: {type(e).__name__}: {e}")
75
+
76
+ # Chunked-read safetensors loader: safe_open mmap deadlocks on FUSE-backed storage.
77
+ from ltx_core.loader.primitives import StateDict
78
+ from ltx_core.loader.sft_loader import SafetensorsStateDictLoader
79
+
80
+ _SAFETENSORS_DTYPE_MAP = {
81
+ "F64": torch.float64, "F32": torch.float32, "F16": torch.float16,
82
+ "BF16": torch.bfloat16, "F8_E5M2": torch.float8_e5m2, "F8_E4M3": torch.float8_e4m3fn,
83
+ "I64": torch.int64, "I32": torch.int32, "I16": torch.int16, "I8": torch.int8,
84
+ "U8": torch.uint8, "BOOL": torch.bool,
85
+ }
86
+
87
+
88
+ def _patched_load(self, path, sd_ops, device=None):
89
+ sd, size, dtype = {}, 0, set()
90
+ device = device or torch.device("cpu")
91
+ for shard_path in (path if isinstance(path, list) else [path]):
92
+ with open(shard_path, "rb") as f:
93
+ header_len = struct.unpack("<Q", f.read(8))[0]
94
+ header = json.loads(f.read(header_len).decode("utf-8"))
95
+ data_base = 8 + header_len
96
+ for name, meta in header.items():
97
+ if name == "__metadata__":
98
+ continue
99
+ expected_name = name if sd_ops is None else sd_ops.apply_to_key(name)
100
+ if expected_name is None:
101
+ continue
102
+ start, end = meta["data_offsets"]
103
+ f.seek(data_base + start)
104
+ buf = f.read(end - start)
105
+ t = torch.frombuffer(bytearray(buf), dtype=_SAFETENSORS_DTYPE_MAP[meta["dtype"]]
106
+ ).reshape(meta["shape"])
107
+ t = t.to(device=device, non_blocking=True, copy=False)
108
+ kvs = (((expected_name, t),) if sd_ops is None
109
+ else sd_ops.apply_to_key_value(expected_name, t))
110
+ for key, v in kvs:
111
+ size += v.nbytes
112
+ dtype.add(v.dtype)
113
+ sd[key] = v
114
+ return StateDict(sd=sd, device=device, size=size, dtype=dtype)
115
+
116
+
117
+ SafetensorsStateDictLoader.load = _patched_load
118
+
119
+ logging.getLogger().setLevel(logging.INFO)
120
+ MAX_SEED = np.iinfo(np.int32).max
121
+ FRAME_RATE = 24.0
122
+
123
+ DEFAULT_NEGATIVE = (
124
+ "shaky, glitchy, low quality, worst quality, deformed, distorted, disfigured, "
125
+ "motion smear, motion artifacts, fused fingers, bad anatomy, weird hand, ugly, "
126
+ "transition, static"
127
+ )
128
+
129
+ # LTX-2.3 guider defaults
130
+ VIDEO_STG, VIDEO_RESCALE, A2V_SCALE, STG_BLOCKS = 1.0, 0.7, 3.0, [28]
131
+ AUDIO_CFG, AUDIO_STG, AUDIO_RESCALE, V2A_SCALE = 7.0, 1.0, 0.7, 3.0
132
+
133
+ LTX_REPO = os.environ.get("LTX_REPO", "Lightricks/LTX-2.3")
134
+ DEV_FILE = "ltx-2.3-22b-dev.safetensors"
135
+ DISTILLED_LORA_FILE = "ltx-2.3-22b-distilled-lora-384-1.1.safetensors"
136
+ UPSCALER_FILE = "ltx-2.3-spatial-upscaler-x2-1.1.safetensors"
137
+ TOKEN = os.environ.get("HF_TOKEN")
138
+ TEXT_ENCODER_SPACE = os.environ.get("TEXT_ENCODER_SPACE", "linoyts/ltx23-gemma-encoder-api")
139
+
140
+ print("Downloading checkpoints…")
141
+ checkpoint_path = hf_hub_download(LTX_REPO, DEV_FILE, token=TOKEN)
142
+ distilled_lora_path = hf_hub_download(LTX_REPO, DISTILLED_LORA_FILE, token=TOKEN)
143
+ upsampler_path = hf_hub_download(LTX_REPO, UPSCALER_FILE, token=TOKEN)
144
+
145
+ pipeline = TI2VidTwoStagesPipeline(
146
+ checkpoint_path=checkpoint_path,
147
+ distilled_lora=[LoraPathStrengthAndSDOps(path=distilled_lora_path, strength=1.0, sd_ops=None)],
148
+ spatial_upsampler_path=upsampler_path,
149
+ gemma_root=None, # text encoding happens on TEXT_ENCODER_SPACE
150
+ loras=[],
151
+ quantization=QuantizationPolicy.fp8_cast(),
152
+ )
153
+
154
+ print("Preloading models (ZeroGPU tensor packing)…")
155
+ s1, s2 = pipeline.stage_1_model_ledger, pipeline.stage_2_model_ledger
156
+ _cached_s1 = {n: getattr(s1, n)() for n in ("transformer", "video_encoder")}
157
+ _cached_s2 = {n: getattr(s2, n)() for n in (
158
+ "transformer", "video_decoder", "audio_decoder", "vocoder", "spatial_upsampler")}
159
+ for name, model in _cached_s1.items():
160
+ setattr(s1, name, (lambda m: (lambda: m))(model))
161
+ for name, model in _cached_s2.items():
162
+ setattr(s2, name, (lambda m: (lambda: m))(model))
163
+ print("Pipeline ready.")
164
+
165
+
166
+ # ---- remote text encoding (positive + negative, needed for CFG) ----
167
+
168
+ _emb_cache = {}
169
+
170
+
171
+ def fetch_embeddings(prompt, negative_prompt, enhance_prompt, seed):
172
+ """Call the Gemma encoder Space; returns {'positive', 'negative', 'final_prompt'}."""
173
+ key = (prompt, negative_prompt, bool(enhance_prompt), int(seed) if enhance_prompt else 0)
174
+ if key in _emb_cache:
175
+ return _emb_cache[key]
176
+ client = Client(TEXT_ENCODER_SPACE, hf_token=TOKEN)
177
+ emb_file, final_prompt, status = client.predict(
178
+ prompt=prompt, negative_prompt=negative_prompt or "", encode_negative=True,
179
+ enhance_prompt=enhance_prompt, seed=int(seed), api_name="/encode",
180
+ )
181
+ print(f"[encoder] {status} | final prompt: {final_prompt[:80]}…")
182
+ data = torch.load(emb_file, map_location="cpu", weights_only=True)
183
+ if len(_emb_cache) > 32:
184
+ _emb_cache.clear()
185
+ _emb_cache[key] = data
186
+ return data
187
+
188
+
189
+ def _to_output(pack, device):
190
+ return EmbeddingsProcessorOutput(
191
+ video_encoding=pack["video"].to(device),
192
+ audio_encoding=pack["audio"].to(device) if pack["audio"] is not None else None,
193
+ attention_mask=pack["mask"].to(device),
194
+ )
195
+
196
+
197
+ def gpu_duration(embeddings, prompt, negative_prompt="", duration=3.0, *args, **kwargs):
198
+ return min(420, 120 + int(duration) * 30)
199
+
200
+
201
+ @spaces.GPU(duration=gpu_duration)
202
+ @torch.inference_mode()
203
+ def _generate_gpu(embeddings, prompt, negative_prompt, duration, used_seed,
204
+ num_inference_steps, video_cfg_scale, width, height,
205
+ progress=gr.Progress(track_tqdm=True)):
206
+ num_frames = ((int(duration * FRAME_RATE) + 1 - 1 + 7) // 8) * 8 + 1
207
+ tiling_config = TilingConfig.default()
208
+ video_chunks_number = get_video_chunks_number(num_frames, tiling_config)
209
+
210
+ precomputed = [_to_output(embeddings["positive"], "cuda"),
211
+ _to_output(embeddings["negative"], "cuda")]
212
+ original = ti2vid_module.encode_prompts
213
+ ti2vid_module.encode_prompts = lambda *a, **kw: precomputed
214
+ try:
215
+ video, audio = pipeline(
216
+ prompt=prompt,
217
+ negative_prompt=negative_prompt,
218
+ seed=used_seed,
219
+ height=int(height),
220
+ width=int(width),
221
+ num_frames=num_frames,
222
+ frame_rate=FRAME_RATE,
223
+ num_inference_steps=int(num_inference_steps),
224
+ video_guider_params=MultiModalGuiderParams(
225
+ cfg_scale=video_cfg_scale, stg_scale=VIDEO_STG, rescale_scale=VIDEO_RESCALE,
226
+ modality_scale=A2V_SCALE, skip_step=0, stg_blocks=STG_BLOCKS),
227
+ audio_guider_params=MultiModalGuiderParams(
228
+ cfg_scale=AUDIO_CFG, stg_scale=AUDIO_STG, rescale_scale=AUDIO_RESCALE,
229
+ modality_scale=V2A_SCALE, skip_step=0, stg_blocks=STG_BLOCKS),
230
+ images=[],
231
+ tiling_config=tiling_config,
232
+ enhance_prompt=False, # already enhanced on the encoder Space
233
+ )
234
+ finally:
235
+ ti2vid_module.encode_prompts = original
236
+
237
+ output_path = tempfile.mktemp(suffix=".mp4")
238
+ encode_video(video=video, fps=FRAME_RATE, audio=audio, output_path=output_path,
239
+ video_chunks_number=video_chunks_number)
240
+ return output_path
241
+
242
+
243
+ def generate(
244
+ prompt: str,
245
+ negative_prompt: str = DEFAULT_NEGATIVE,
246
+ duration: float = 5.0,
247
+ seed: int = 42,
248
+ randomize_seed: bool = False,
249
+ num_inference_steps: int = 30,
250
+ video_cfg_scale: float = 3.0,
251
+ width: int = 1536,
252
+ height: int = 864,
253
+ enhance_prompt: bool = True,
254
+ progress=gr.Progress(track_tqdm=True),
255
+ ):
256
+ """Dev two-stage T2V: returns (video_path, used_seed). Encoder call runs outside the GPU slot."""
257
+ used_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
258
+ progress(0.05, desc="encoding prompts (Gemma Space)…")
259
+ embeddings = fetch_embeddings(prompt, negative_prompt, enhance_prompt, used_seed)
260
+ output_path = _generate_gpu(embeddings, prompt, negative_prompt, duration, used_seed,
261
+ num_inference_steps, video_cfg_scale, width, height)
262
+ return output_path, used_seed
263
+
264
+
265
+ with gr.Blocks(title="LTX-2.3 dev API") as demo:
266
+ gr.Markdown("# 🎬 LTX-2.3 dev two-stage — hero backend\n"
267
+ "API backend for the LTX-2.3 Pre-Production Playground.")
268
+ with gr.Row():
269
+ with gr.Column():
270
+ prompt = gr.Textbox(label="Prompt", lines=3)
271
+ negative_prompt = gr.Textbox(label="Negative prompt", value=DEFAULT_NEGATIVE, lines=2)
272
+ duration = gr.Slider(1, 10, value=5, step=0.5, label="Duration (s)")
273
+ with gr.Row():
274
+ seed = gr.Number(label="Seed", value=42, precision=0)
275
+ randomize_seed = gr.Checkbox(label="Randomize seed", value=False)
276
+ with gr.Row():
277
+ num_inference_steps = gr.Slider(10, 50, value=30, step=1, label="Steps")
278
+ video_cfg_scale = gr.Slider(1, 8, value=3.0, step=0.1, label="CFG")
279
+ with gr.Row():
280
+ width = gr.Number(label="Width", value=1536, precision=0)
281
+ height = gr.Number(label="Height", value=864, precision=0)
282
+ enhance_prompt = gr.Checkbox(label="Enhance prompt", value=True)
283
+ btn = gr.Button("Generate", variant="primary")
284
+ with gr.Column():
285
+ video = gr.Video(label="Video", autoplay=True)
286
+ used_seed = gr.Number(label="Used seed", precision=0)
287
+
288
+ btn.click(generate,
289
+ [prompt, negative_prompt, duration, seed, randomize_seed, num_inference_steps,
290
+ video_cfg_scale, width, height, enhance_prompt],
291
+ [video, used_seed], api_name="generate")
292
+
293
+ if __name__ == "__main__":
294
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ transformers==4.57.6
2
+ accelerate
3
+ torch==2.8.0
4
+ einops
5
+ scipy
6
+ av
7
+ scikit-image>=0.25.2
8
+ flashpack==0.1.2
9
+ torchaudio==2.8.0