Exosfeer commited on
Commit
97d0511
·
1 Parent(s): ce6e449

Initial turbo Space: ZeroGPU-compatible LTX-2.3 with FP8 quantization and two-phase GPU leasing

Browse files
Files changed (3) hide show
  1. README.md +63 -6
  2. app.py +612 -0
  3. requirements.txt +18 -0
README.md CHANGED
@@ -1,13 +1,70 @@
1
  ---
2
- title: LTX 2.3 Turbo
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.8.0
8
  python_version: '3.12'
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 Turbo
3
+ emoji:
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 5.23.0
8
  python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
+ license: other
12
+ license_name: ltx-2-community-license-agreement
13
+ license_link: https://github.com/Lightricks/LTX-2/blob/main/LICENSE
14
+ short_description: LTX-2.3 video+audio generation on free ZeroGPU
15
  ---
16
 
17
+ # LTX-2.3 Turbo (ZeroGPU)
18
+
19
+ Generate synchronized **video + audio** from text or images using
20
+ [Lightricks/LTX-2.3](https://huggingface.co/Lightricks/LTX-2.3) — a 22B
21
+ parameter DiT-based audio-video foundation model — running on **free ZeroGPU**
22
+ hardware.
23
+
24
+ ## How it works
25
+
26
+ This Space uses a **two-phase GPU leasing** strategy to run the massive 22B
27
+ model on ZeroGPU's limited hardware:
28
+
29
+ 1. **Phase 1 — Text Encoding**: A short GPU lease loads the Gemma-3 12B text
30
+ encoder, encodes the prompt into context tensors, then frees the encoder.
31
+ 2. **Phase 2 — Video Generation**: A longer GPU lease loads the FP8-quantized
32
+ transformer (~22GB vs ~44GB in bf16), runs two-stage distilled denoising
33
+ (8 steps low-res + 4 steps high-res with 2x spatial upscaling), then
34
+ decodes video and audio.
35
+
36
+ ### Key optimizations for ZeroGPU
37
+
38
+ - **FP8 quantization**: Transformer weights are cast to `float8_e4m3fn`,
39
+ halving VRAM usage with minimal quality impact.
40
+ - **Two-phase GPU leasing**: Text encoding and video generation use separate
41
+ `@spaces.GPU()` calls, so the text encoder and transformer never coexist
42
+ in VRAM simultaneously.
43
+ - **Distilled pipeline**: Only 8+4 denoising steps (vs 30+ for the full model),
44
+ dramatically reducing inference time.
45
+
46
+ ## Parameters
47
+
48
+ | Parameter | Range | Default | Notes |
49
+ |-----------|-------|---------|-------|
50
+ | Mode | Text to Video / Image to Video | Text to Video | |
51
+ | Prompt | Free text | — | Describe scene, motion, and audio |
52
+ | Resolution | 768x512, 512x512, 512x768 | 768x512 | Upscaled 2x by spatial upscaler |
53
+ | Duration | 1–5 seconds | 2s | Shorter = more reliable on ZeroGPU |
54
+ | Enhance prompt | On/Off | On | Uses Gemma to enhance the prompt |
55
+ | Seed | 0–2^31 | Random | For reproducibility |
56
+
57
+ ## Limitations
58
+
59
+ - **ZeroGPU time limits**: Longer videos may exceed the GPU lease duration.
60
+ Keep duration at 3 seconds or less for best reliability.
61
+ - **VRAM constraints**: Even with FP8 quantization, very high resolutions
62
+ are not possible. The preset resolutions are tuned for ZeroGPU.
63
+ - **No audio conditioning**: This simplified interface doesn't support
64
+ custom audio input (the full model does).
65
+
66
+ ## Credits
67
+
68
+ - Model: [Lightricks/LTX-2.3](https://huggingface.co/Lightricks/LTX-2.3)
69
+ - Codebase: [Lightricks/LTX-2](https://github.com/Lightricks/LTX-2)
70
+ - ZeroGPU architecture inspired by [alexnasa/ltx-2-TURBO](https://huggingface.co/spaces/alexnasa/ltx-2-TURBO)
app.py ADDED
@@ -0,0 +1,612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LTX-2.3 Turbo — ZeroGPU Edition
3
+ Generates synchronized audio-video content using Lightricks/LTX-2.3 on
4
+ free ZeroGPU hardware via Hugging Face Spaces.
5
+
6
+ Architecture (ZeroGPU-compatible, two-phase GPU leasing):
7
+ 1. Model files are downloaded at module startup (CPU, no GPU lease).
8
+ 2. ModelLedger is constructed once, with gemma_root for text encoding.
9
+ 3. Phase 1 (@spaces.GPU): encode_prompt — loads Gemma text encoder,
10
+ encodes prompt, frees encoder. Returns pre-encoded context tensors.
11
+ 4. Phase 2 (@spaces.GPU): generate_video — loads transformer, video
12
+ encoder, spatial upsampler. Runs two-stage distilled denoising with
13
+ pre-encoded contexts. Decodes video+audio. Returns output file.
14
+ 5. FP8 quantization is used for the transformer to fit ~22GB (from ~44GB
15
+ in bf16), enabling inference on A100-40GB ZeroGPU allocations.
16
+
17
+ Based on the official LTX-2 codebase: https://github.com/Lightricks/LTX-2
18
+ Inspired by alexnasa/ltx-2-TURBO's ZeroGPU architecture.
19
+ """
20
+
21
+ import gc
22
+ import logging
23
+ import os
24
+ import random
25
+ import tempfile
26
+ import time
27
+ import traceback
28
+ from pathlib import Path
29
+
30
+ import gradio as gr
31
+ import numpy as np
32
+ import spaces
33
+ import torch
34
+ from huggingface_hub import hf_hub_download, snapshot_download
35
+
36
+ logging.basicConfig(level=logging.INFO)
37
+ logger = logging.getLogger(__name__)
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Constants
41
+ # ---------------------------------------------------------------------------
42
+ MAX_SEED = np.iinfo(np.int32).max
43
+ LTX_REPO = "Lightricks/LTX-2.3"
44
+ GEMMA_REPO = "google/gemma-3-12b-it-qat-q4_0-unquantized"
45
+ CKPT_DISTILLED = "ltx-2.3-22b-distilled.safetensors"
46
+ CKPT_UPSCALER = "ltx-2.3-spatial-upscaler-x2-1.0.safetensors"
47
+
48
+ # Distilled pipeline sigma schedules (from official LTX-2 constants)
49
+ DISTILLED_SIGMA_VALUES = [
50
+ 1.0,
51
+ 0.99375,
52
+ 0.9875,
53
+ 0.98125,
54
+ 0.975,
55
+ 0.909375,
56
+ 0.725,
57
+ 0.421875,
58
+ 0.0,
59
+ ]
60
+ STAGE_2_DISTILLED_SIGMA_VALUES = [0.909375, 0.725, 0.421875, 0.0]
61
+
62
+ # Resolution presets: (label, width, height)
63
+ RESOLUTION_PRESETS = {
64
+ "16:9 (768x512)": (768, 512),
65
+ "1:1 (512x512)": (512, 512),
66
+ "9:16 (512x768)": (512, 768),
67
+ }
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # 1) Download model files at module startup (CPU, no GPU lease)
71
+ # ---------------------------------------------------------------------------
72
+ logger.info("Downloading LTX model files...")
73
+ DISTILLED_PATH = hf_hub_download(repo_id=LTX_REPO, filename=CKPT_DISTILLED)
74
+ logger.info(f" Distilled checkpoint: {DISTILLED_PATH}")
75
+ UPSCALER_PATH = hf_hub_download(repo_id=LTX_REPO, filename=CKPT_UPSCALER)
76
+ logger.info(f" Upscaler: {UPSCALER_PATH}")
77
+
78
+ logger.info("Downloading Gemma text encoder...")
79
+ HF_TOKEN = os.environ.get("HF_TOKEN")
80
+ GEMMA_ROOT = snapshot_download(repo_id=GEMMA_REPO, token=HF_TOKEN)
81
+ logger.info(f" Gemma root: {GEMMA_ROOT}")
82
+ logger.info("All model files ready on disk.")
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # 2) Build ModelLedger (CPU — no model weights loaded to GPU yet)
86
+ # ---------------------------------------------------------------------------
87
+ logger.info("Constructing ModelLedger...")
88
+ from ltx_core.components.diffusion_steps import EulerDiffusionStep
89
+ from ltx_core.components.noisers import GaussianNoiser
90
+ from ltx_core.components.protocols import DiffusionStepProtocol
91
+ from ltx_core.model.audio_vae import decode_audio as vae_decode_audio
92
+ from ltx_core.model.upsampler import upsample_video
93
+ from ltx_core.model.video_vae import TilingConfig, get_video_chunks_number
94
+ from ltx_core.model.video_vae import decode_video as vae_decode_video
95
+ from ltx_core.quantization import QuantizationPolicy
96
+ from ltx_core.types import LatentState, VideoPixelShape
97
+ from ltx_pipelines.utils import ModelLedger, euler_denoising_loop
98
+ from ltx_pipelines.utils.args import ImageConditioningInput
99
+ from ltx_pipelines.utils.helpers import (
100
+ assert_resolution,
101
+ cleanup_memory,
102
+ combined_image_conditionings,
103
+ denoise_audio_video,
104
+ encode_prompts,
105
+ simple_denoising_func,
106
+ )
107
+ from ltx_pipelines.utils.media_io import encode_video
108
+ from ltx_pipelines.utils.types import PipelineComponents
109
+
110
+ # Use FP8 quantization to reduce transformer VRAM from ~44GB to ~22GB
111
+ fp8_quantization = QuantizationPolicy.fp8_cast()
112
+
113
+ model_ledger = ModelLedger(
114
+ dtype=torch.bfloat16,
115
+ device=torch.device("cuda"),
116
+ checkpoint_path=DISTILLED_PATH,
117
+ spatial_upsampler_path=UPSCALER_PATH,
118
+ gemma_root_path=GEMMA_ROOT,
119
+ loras=(),
120
+ quantization=fp8_quantization,
121
+ )
122
+
123
+ pipeline_components = PipelineComponents(
124
+ dtype=torch.bfloat16,
125
+ device=torch.device("cuda"),
126
+ )
127
+
128
+ logger.info("ModelLedger constructed (no GPU memory used yet).")
129
+
130
+
131
+ # ---------------------------------------------------------------------------
132
+ # Helpers
133
+ # ---------------------------------------------------------------------------
134
+ def align64(v: int) -> int:
135
+ """Round to nearest multiple of 64 (min 64) for two-stage pipeline."""
136
+ return max(64, int(round(int(v) / 64)) * 64)
137
+
138
+
139
+ def calc_frames(duration: float, fps: float) -> int:
140
+ """Compute num_frames satisfying: frames = 8k + 1, frames >= 9."""
141
+ raw = int(duration * fps) + 1
142
+ raw = max(raw, 9)
143
+ k = (raw - 1 + 7) // 8
144
+ return k * 8 + 1
145
+
146
+
147
+ def gpu_memory_info() -> str:
148
+ """Return a brief GPU memory summary."""
149
+ if not torch.cuda.is_available():
150
+ return "No GPU"
151
+ alloc = torch.cuda.memory_allocated() / 1024**3
152
+ total = torch.cuda.get_device_properties(0).total_mem / 1024**3
153
+ return f"{alloc:.1f} / {total:.1f} GB"
154
+
155
+
156
+ def get_gpu_duration(duration_seconds: float, has_image: bool) -> int:
157
+ """Estimate GPU lease duration in seconds based on video parameters."""
158
+ # Base time: ~60s for short videos, scales up with duration
159
+ base = 90
160
+ per_second = 30 # ~30s GPU time per second of video
161
+ extra = 30 if has_image else 0
162
+ return min(int(base + duration_seconds * per_second + extra), 300)
163
+
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # Phase 1: Text Encoding (separate GPU lease)
167
+ # ---------------------------------------------------------------------------
168
+ @spaces.GPU(duration=120)
169
+ @torch.inference_mode()
170
+ def encode_prompt_gpu(prompt: str, enhance_prompt: bool, image_path: str | None = None):
171
+ """
172
+ Load Gemma text encoder, encode prompt, free encoder.
173
+ Returns (video_context, audio_context) tensors.
174
+ """
175
+ logger.info(f"[Phase 1] Encoding prompt on GPU: {gpu_memory_info()}")
176
+ logger.info(f" prompt='{prompt[:80]}...', enhance={enhance_prompt}")
177
+
178
+ (ctx,) = encode_prompts(
179
+ [prompt],
180
+ model_ledger,
181
+ enhance_first_prompt=enhance_prompt,
182
+ enhance_prompt_image=image_path,
183
+ )
184
+ video_context = ctx.video_encoding
185
+ audio_context = ctx.audio_encoding
186
+
187
+ logger.info(
188
+ f"[Phase 1] Encoding complete. "
189
+ f"video_context: {video_context.shape}, audio_context: {audio_context.shape}, "
190
+ f"GPU: {gpu_memory_info()}"
191
+ )
192
+ return video_context, audio_context
193
+
194
+
195
+ # ---------------------------------------------------------------------------
196
+ # Phase 2: Video Generation (separate GPU lease, dynamic duration)
197
+ # ---------------------------------------------------------------------------
198
+ @spaces.GPU(duration=240)
199
+ @torch.inference_mode()
200
+ def generate_video_gpu(
201
+ video_context: torch.Tensor,
202
+ audio_context: torch.Tensor,
203
+ seed: int,
204
+ height: int,
205
+ width: int,
206
+ num_frames: int,
207
+ frame_rate: float,
208
+ images: list,
209
+ output_path: str,
210
+ ):
211
+ """
212
+ Run two-stage distilled denoising with pre-encoded contexts.
213
+ Replicates DistilledPipeline.__call__ but uses pre-encoded video_context
214
+ and audio_context instead of calling encode_prompts internally.
215
+ """
216
+ logger.info(f"[Phase 2] Starting generation on GPU: {gpu_memory_info()}")
217
+
218
+ device = torch.device("cuda")
219
+ dtype = torch.bfloat16
220
+ generator = torch.Generator(device=device).manual_seed(seed)
221
+ noiser = GaussianNoiser(generator=generator)
222
+ stepper = EulerDiffusionStep()
223
+
224
+ # ------ Stage 1: Low-res generation (half resolution) ------
225
+ logger.info("[Phase 2] Stage 1: Low-res generation...")
226
+ video_encoder = model_ledger.video_encoder()
227
+ transformer = model_ledger.transformer()
228
+ stage_1_sigmas = torch.Tensor(DISTILLED_SIGMA_VALUES).to(device)
229
+
230
+ def denoising_loop(
231
+ sigmas: torch.Tensor,
232
+ video_state: LatentState,
233
+ audio_state: LatentState,
234
+ stepper_arg: DiffusionStepProtocol,
235
+ ) -> tuple[LatentState, LatentState]:
236
+ return euler_denoising_loop(
237
+ sigmas=sigmas,
238
+ video_state=video_state,
239
+ audio_state=audio_state,
240
+ stepper=stepper_arg,
241
+ denoise_fn=simple_denoising_func(
242
+ video_context=video_context,
243
+ audio_context=audio_context,
244
+ transformer=transformer,
245
+ ),
246
+ )
247
+
248
+ stage_1_output_shape = VideoPixelShape(
249
+ batch=1,
250
+ frames=num_frames,
251
+ width=width // 2,
252
+ height=height // 2,
253
+ fps=frame_rate,
254
+ )
255
+
256
+ # Convert image paths back to ImageConditioningInput objects
257
+ image_conditionings = []
258
+ for img_data in images:
259
+ img_input = ImageConditioningInput(
260
+ path=img_data["path"],
261
+ frame_idx=img_data["frame_idx"],
262
+ strength=img_data["strength"],
263
+ )
264
+ image_conditionings.append(img_input)
265
+
266
+ stage_1_conditionings = combined_image_conditionings(
267
+ images=image_conditionings,
268
+ height=stage_1_output_shape.height,
269
+ width=stage_1_output_shape.width,
270
+ video_encoder=video_encoder,
271
+ dtype=dtype,
272
+ device=device,
273
+ )
274
+
275
+ video_state, audio_state = denoise_audio_video(
276
+ output_shape=stage_1_output_shape,
277
+ conditionings=stage_1_conditionings,
278
+ noiser=noiser,
279
+ sigmas=stage_1_sigmas,
280
+ stepper=stepper,
281
+ denoising_loop_fn=denoising_loop,
282
+ components=pipeline_components,
283
+ dtype=dtype,
284
+ device=device,
285
+ )
286
+
287
+ logger.info(f"[Phase 2] Stage 1 complete. GPU: {gpu_memory_info()}")
288
+
289
+ # ------ Stage 2: Upsample + refine at full resolution ------
290
+ logger.info("[Phase 2] Stage 2: Upsampling + refinement...")
291
+ upscaled_video_latent = upsample_video(
292
+ latent=video_state.latent[:1],
293
+ video_encoder=video_encoder,
294
+ upsampler=model_ledger.spatial_upsampler(),
295
+ )
296
+
297
+ torch.cuda.synchronize()
298
+ cleanup_memory()
299
+
300
+ stage_2_sigmas = torch.Tensor(STAGE_2_DISTILLED_SIGMA_VALUES).to(device)
301
+ stage_2_output_shape = VideoPixelShape(
302
+ batch=1, frames=num_frames, width=width, height=height, fps=frame_rate
303
+ )
304
+ stage_2_conditionings = combined_image_conditionings(
305
+ images=image_conditionings,
306
+ height=stage_2_output_shape.height,
307
+ width=stage_2_output_shape.width,
308
+ video_encoder=video_encoder,
309
+ dtype=dtype,
310
+ device=device,
311
+ )
312
+
313
+ video_state, audio_state = denoise_audio_video(
314
+ output_shape=stage_2_output_shape,
315
+ conditionings=stage_2_conditionings,
316
+ noiser=noiser,
317
+ sigmas=stage_2_sigmas,
318
+ stepper=stepper,
319
+ denoising_loop_fn=denoising_loop,
320
+ components=pipeline_components,
321
+ dtype=dtype,
322
+ device=device,
323
+ noise_scale=stage_2_sigmas[0],
324
+ initial_video_latent=upscaled_video_latent,
325
+ initial_audio_latent=audio_state.latent,
326
+ )
327
+
328
+ logger.info(f"[Phase 2] Stage 2 complete. GPU: {gpu_memory_info()}")
329
+
330
+ # ------ Decode video + audio ------
331
+ logger.info("[Phase 2] Decoding video and audio...")
332
+ torch.cuda.synchronize()
333
+ del transformer
334
+ del video_encoder
335
+ cleanup_memory()
336
+
337
+ tiling_config = TilingConfig.default()
338
+ decoded_video = vae_decode_video(
339
+ video_state.latent, model_ledger.video_decoder(), tiling_config, generator
340
+ )
341
+ decoded_audio = vae_decode_audio(
342
+ audio_state.latent, model_ledger.audio_decoder(), model_ledger.vocoder()
343
+ )
344
+
345
+ video_chunks_number = get_video_chunks_number(num_frames, tiling_config)
346
+ encode_video(
347
+ video=decoded_video,
348
+ fps=int(frame_rate),
349
+ audio=decoded_audio,
350
+ output_path=output_path,
351
+ video_chunks_number=video_chunks_number,
352
+ )
353
+
354
+ logger.info(f"[Phase 2] Video saved to {output_path}. GPU: {gpu_memory_info()}")
355
+ return output_path
356
+
357
+
358
+ # ---------------------------------------------------------------------------
359
+ # Main generate function (orchestrates Phase 1 + Phase 2)
360
+ # ---------------------------------------------------------------------------
361
+ def generate(
362
+ mode: str,
363
+ input_image,
364
+ prompt: str,
365
+ duration: float,
366
+ enhance_prompt: bool,
367
+ seed: int,
368
+ randomize_seed: bool,
369
+ resolution: str,
370
+ progress=gr.Progress(track_tqdm=True),
371
+ ):
372
+ if mode == "Image to Video" and input_image is None:
373
+ raise gr.Error("Please upload an image for Image to Video mode.")
374
+ if not prompt or not prompt.strip():
375
+ raise gr.Error("Please enter a prompt.")
376
+
377
+ # --- Resolve params ---
378
+ current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
379
+ width, height = RESOLUTION_PRESETS.get(resolution, (768, 512))
380
+ num_frames = calc_frames(duration, 24.0)
381
+ fps = 24.0
382
+
383
+ output_dir = Path(tempfile.mkdtemp())
384
+ stamp = int(time.time())
385
+ output_path = str(output_dir / f"ltx23_turbo_{current_seed}_{stamp}.mp4")
386
+
387
+ # --- Handle input image ---
388
+ images_data = []
389
+ image_path_for_enhance = None
390
+ if mode == "Image to Video" and input_image is not None:
391
+ temp_image_path = str(output_dir / f"input_{stamp}.jpg")
392
+ if hasattr(input_image, "save"):
393
+ input_image.save(temp_image_path)
394
+ else:
395
+ from PIL import Image as PILImage
396
+
397
+ PILImage.open(input_image).save(temp_image_path)
398
+ images_data = [{"path": temp_image_path, "frame_idx": 0, "strength": 1.0}]
399
+ image_path_for_enhance = temp_image_path
400
+
401
+ logger.info(
402
+ f"Request: seed={current_seed}, {width}x{height}, "
403
+ f"frames={num_frames}, fps={fps}, enhance={enhance_prompt}, "
404
+ f"mode={mode}, duration={duration}s"
405
+ )
406
+
407
+ t0 = time.time()
408
+ try:
409
+ # Phase 1: Encode prompt (separate GPU lease)
410
+ video_context, audio_context = encode_prompt_gpu(
411
+ prompt=prompt,
412
+ enhance_prompt=enhance_prompt,
413
+ image_path=image_path_for_enhance,
414
+ )
415
+
416
+ # Phase 2: Generate video (separate GPU lease)
417
+ generate_video_gpu(
418
+ video_context=video_context,
419
+ audio_context=audio_context,
420
+ seed=current_seed,
421
+ height=height,
422
+ width=width,
423
+ num_frames=num_frames,
424
+ frame_rate=fps,
425
+ images=images_data,
426
+ output_path=output_path,
427
+ )
428
+
429
+ elapsed = time.time() - t0
430
+ logger.info(f"Total generation time: {elapsed:.1f}s")
431
+
432
+ except torch.cuda.OutOfMemoryError:
433
+ elapsed = time.time() - t0
434
+ logger.error(f"OOM after {elapsed:.1f}s")
435
+ raise gr.Error("Out of GPU memory. Try a shorter duration or lower resolution.")
436
+ except Exception as e:
437
+ elapsed = time.time() - t0
438
+ tb = traceback.format_exc()
439
+ logger.error(f"Generation failed after {elapsed:.1f}s:\n{tb}")
440
+ raise gr.Error(f"Generation failed: {type(e).__name__}: {e}")
441
+
442
+ info_text = (
443
+ f"Seed: {current_seed}\n"
444
+ f"Resolution: {width}x{height} (upscaled from {width // 2}x{height // 2})\n"
445
+ f"Frames: {num_frames} @ {int(fps)} fps\n"
446
+ f"Duration: {duration}s\n"
447
+ f"Pipeline: Distilled 2-stage (8+4 steps, FP8 quantized)\n"
448
+ f"Total time: {elapsed:.1f}s\n"
449
+ f"Hardware: ZeroGPU"
450
+ )
451
+
452
+ return output_path, info_text, current_seed
453
+
454
+
455
+ # ---------------------------------------------------------------------------
456
+ # UI toggle
457
+ # ---------------------------------------------------------------------------
458
+ def toggle_image(mode: str):
459
+ return gr.update(visible=(mode == "Image to Video"))
460
+
461
+
462
+ # ---------------------------------------------------------------------------
463
+ # Gradio UI
464
+ # ---------------------------------------------------------------------------
465
+ CSS = """
466
+ .gradio-container { max-width: 1200px !important; }
467
+ .header { text-align: center; margin-bottom: 1rem; }
468
+ .generate-btn { min-height: 50px; }
469
+ """
470
+
471
+ with gr.Blocks(css=CSS, title="LTX-2.3 Turbo", theme=gr.themes.Soft()) as demo:
472
+ gr.Markdown(
473
+ """
474
+ # LTX-2.3 Turbo (ZeroGPU)
475
+ Generate synchronized **video + audio** from text or images using
476
+ [Lightricks/LTX-2.3](https://huggingface.co/Lightricks/LTX-2.3) —
477
+ a 22B parameter DiT-based audio-video foundation model.
478
+
479
+ Running on **free ZeroGPU** with FP8 quantization.
480
+ Distilled pipeline (8+4 denoising steps, two-stage with 2x spatial upscaling).
481
+ """,
482
+ elem_classes="header",
483
+ )
484
+
485
+ with gr.Row():
486
+ # --- Left: Controls ---
487
+ with gr.Column(scale=1):
488
+ mode = gr.Radio(
489
+ ["Text to Video", "Image to Video"],
490
+ value="Text to Video",
491
+ label="Mode",
492
+ )
493
+ input_image = gr.Image(
494
+ type="pil",
495
+ label="Input Image",
496
+ visible=False,
497
+ )
498
+ prompt = gr.Textbox(
499
+ label="Prompt",
500
+ lines=3,
501
+ placeholder="Describe the scene, motion, and audio...",
502
+ value=(
503
+ "A golden retriever puppy plays in fresh snow, "
504
+ "tossing it up with its paws, soft winter sunlight, "
505
+ "gentle wind sounds and playful barking"
506
+ ),
507
+ )
508
+
509
+ with gr.Row():
510
+ resolution = gr.Dropdown(
511
+ choices=list(RESOLUTION_PRESETS.keys()),
512
+ value="16:9 (768x512)",
513
+ label="Resolution",
514
+ )
515
+ duration = gr.Slider(1, 5, value=2, step=0.5, label="Duration (sec)")
516
+
517
+ with gr.Row():
518
+ enhance_prompt = gr.Checkbox(value=True, label="Enhance prompt")
519
+ randomize_seed = gr.Checkbox(value=True, label="Random seed")
520
+
521
+ seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Seed")
522
+
523
+ generate_btn = gr.Button(
524
+ "Generate Video",
525
+ variant="primary",
526
+ size="lg",
527
+ elem_classes="generate-btn",
528
+ )
529
+
530
+ # --- Right: Output ---
531
+ with gr.Column(scale=1):
532
+ output_video = gr.Video(label="Generated Video", autoplay=True)
533
+ run_info = gr.Textbox(label="Generation Info", lines=7, interactive=False)
534
+
535
+ # --- Events ---
536
+ mode.change(fn=toggle_image, inputs=mode, outputs=[input_image])
537
+
538
+ _inputs = [
539
+ mode,
540
+ input_image,
541
+ prompt,
542
+ duration,
543
+ enhance_prompt,
544
+ seed,
545
+ randomize_seed,
546
+ resolution,
547
+ ]
548
+ _outputs = [output_video, run_info, seed]
549
+
550
+ generate_btn.click(fn=generate, inputs=_inputs, outputs=_outputs)
551
+
552
+ # --- Examples ---
553
+ gr.Examples(
554
+ examples=[
555
+ [
556
+ "Text to Video",
557
+ None,
558
+ "Aerial drone shot of a coastal city at sunset, golden light "
559
+ "reflecting off glass buildings, gentle ocean waves, seagulls "
560
+ "calling, cinematic ambient soundtrack",
561
+ 3.0,
562
+ True,
563
+ 42,
564
+ True,
565
+ "16:9 (768x512)",
566
+ ],
567
+ [
568
+ "Text to Video",
569
+ None,
570
+ "Close-up of a barista pouring latte art in slow motion, "
571
+ "steam rising from the cup, coffee shop ambience with soft jazz",
572
+ 2.0,
573
+ True,
574
+ 123,
575
+ True,
576
+ "1:1 (512x512)",
577
+ ],
578
+ [
579
+ "Text to Video",
580
+ None,
581
+ "A cat sits on a windowsill watching rain fall outside, "
582
+ "soft indoor lighting, raindrops on glass, gentle rain sounds",
583
+ 3.0,
584
+ True,
585
+ 7,
586
+ True,
587
+ "16:9 (768x512)",
588
+ ],
589
+ ],
590
+ fn=generate,
591
+ inputs=_inputs,
592
+ outputs=_outputs,
593
+ cache_examples=False,
594
+ label="Example Prompts",
595
+ )
596
+
597
+ gr.Markdown(
598
+ """
599
+ ---
600
+ **Notes:**
601
+ - ZeroGPU provides limited GPU time per request. Shorter durations are more reliable.
602
+ - Max duration is capped at 5 seconds to stay within GPU time limits.
603
+ - FP8 quantization reduces VRAM usage by ~50% with minimal quality impact.
604
+ - The 2x spatial upscaler doubles the initial generation resolution.
605
+
606
+ Built with [Lightricks/LTX-2.3](https://huggingface.co/Lightricks/LTX-2.3)
607
+ | [GitHub](https://github.com/Lightricks/LTX-2)
608
+ """
609
+ )
610
+
611
+ if __name__ == "__main__":
612
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=5.0
2
+ spaces
3
+ huggingface_hub[hf_xet]
4
+ torch>=2.7
5
+ torchaudio
6
+ transformers>=4.52,<5.0
7
+ safetensors
8
+ accelerate
9
+ einops
10
+ numpy
11
+ pillow
12
+ scipy>=1.14
13
+ scikit-image>=0.25.2
14
+ av
15
+ tqdm
16
+ triton
17
+ ltx-core @ git+https://github.com/Lightricks/LTX-2.git#subdirectory=packages/ltx-core
18
+ ltx-pipelines @ git+https://github.com/Lightricks/LTX-2.git#subdirectory=packages/ltx-pipelines