thomasf1 commited on
Commit
675d6df
·
verified ·
1 Parent(s): 3f1a1fd

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +13 -7
  2. app.py +674 -0
  3. requirements.txt +9 -0
README.md CHANGED
@@ -1,13 +1,19 @@
1
  ---
2
- title: Ltx V2v
3
- emoji: 😻
4
- colorFrom: indigo
5
- colorTo: purple
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 V2V
3
+ emoji: 🎥
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 6.8.0
 
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # LTX V2V - Video to Video and Generation
13
+
14
+ This Hugging Face Space runs a distilled LTX-2.3 (22B parameter) Diffusion Transformer (DiT) model. It supports two primary functions:
15
+ 1. **Video to Video (V2V)**: Upload a video, specify a prompt, and choose a denoising strength (higher strength = more change) to stylize or edit the video. Includes options to keep the original audio, generate new audio, or output silent video.
16
+ 2. **Text/Image to Video**: The classic distilled LTX-2.3 generation from a text prompt and/or static image.
17
+
18
+ ## Setup & Running
19
+ This app is designed to run on Hugging Face ZeroGPU. At startup, it automatically clones the LTX-2 codebase and installs its dependencies. It dynamically falls back to downloading models if the Hugging Face pre-mounts are not available.
app.py ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import sys
4
+
5
+ # Disable torch.compile / dynamo before any torch import
6
+ os.environ["TORCH_COMPILE_DISABLE"] = "1"
7
+ os.environ["TORCHDYNAMO_DISABLE"] = "1"
8
+
9
+ # Install xformers for memory-efficient attention
10
+ subprocess.run([sys.executable, "-m", "pip", "install", "xformers==0.0.32.post2", "--no-build-isolation"], check=False)
11
+
12
+ # Clone LTX-2 repo and install packages
13
+ LTX_REPO_URL = "https://github.com/Lightricks/LTX-2.git"
14
+ LTX_REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LTX-2")
15
+ LTX_COMMIT_SHA = "ae855f8538843825f9015a419cf4ba5edaf5eec2"
16
+
17
+ if not os.path.exists(LTX_REPO_DIR):
18
+ print(f"Cloning {LTX_REPO_URL}...")
19
+ os.makedirs(LTX_REPO_DIR)
20
+ subprocess.run(["git", "init", LTX_REPO_DIR], check=True)
21
+ subprocess.run(["git", "remote", "add", "origin", LTX_REPO_URL], cwd=LTX_REPO_DIR, check=True)
22
+ subprocess.run(["git", "fetch", "--depth", "1", "origin", LTX_COMMIT_SHA], cwd=LTX_REPO_DIR, check=True)
23
+ subprocess.run(["git", "checkout", LTX_COMMIT_SHA], cwd=LTX_REPO_DIR, check=True)
24
+
25
+ print("Installing ltx-core and ltx-pipelines from cloned repo...")
26
+ subprocess.run(
27
+ [sys.executable, "-m", "pip", "install", "--force-reinstall", "--no-deps", "-e",
28
+ os.path.join(LTX_REPO_DIR, "packages", "ltx-core"),
29
+ "-e", os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines")],
30
+ check=True,
31
+ )
32
+
33
+ sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines", "src"))
34
+ sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-core", "src"))
35
+
36
+ import logging
37
+ import random
38
+ import tempfile
39
+ from pathlib import Path
40
+
41
+ import torch
42
+ torch._dynamo.config.suppress_errors = True
43
+ torch._dynamo.config.disable = True
44
+
45
+ import spaces
46
+ import gradio as gr
47
+ import numpy as np
48
+ from huggingface_hub import hf_hub_download, snapshot_download
49
+
50
+ from ltx_core.model.video_vae import TilingConfig, get_video_chunks_number
51
+ from ltx_core.quantization import QuantizationPolicy
52
+ from ltx_pipelines.distilled import DistilledPipeline
53
+ from ltx_pipelines.utils.args import ImageConditioningInput
54
+ from ltx_pipelines.utils.media_io import encode_video, load_video_conditioning, decode_audio_from_file
55
+ from ltx_pipelines.utils.helpers import (
56
+ encode_prompts,
57
+ cleanup_memory,
58
+ euler_denoising_loop,
59
+ simple_denoising_func,
60
+ denoise_audio_video,
61
+ get_videostream_metadata
62
+ )
63
+ from ltx_pipelines.utils.constants import DISTILLED_SIGMA_VALUES, STAGE_2_DISTILLED_SIGMA_VALUES
64
+ from ltx_core.components.noisers import GaussianNoiser
65
+ from ltx_core.components.diffusion_steps import EulerDiffusionStep
66
+ from ltx_core.types import VideoPixelShape, LatentState
67
+ from ltx_core.components.protocols import DiffusionStepProtocol
68
+ from ltx_core.model.audio_vae import decode_audio as vae_decode_audio
69
+ from ltx_core.model.video_vae import decode_video as vae_decode_video
70
+ from ltx_core.model.upsampler import upsample_video
71
+
72
+ # Force-patch xformers attention into the LTX attention module.
73
+ from ltx_core.model.transformer import attention as _attn_mod
74
+ print(f"[ATTN] Before patch: memory_efficient_attention={_attn_mod.memory_efficient_attention}")
75
+ try:
76
+ from xformers.ops import memory_efficient_attention as _mea
77
+ _attn_mod.memory_efficient_attention = _mea
78
+ print(f"[ATTN] After patch: memory_efficient_attention={_attn_mod.memory_efficient_attention}")
79
+ except Exception as e:
80
+ print(f"[ATTN] xformers patch FAILED: {type(e).__name__}: {e}")
81
+
82
+ # Disable xformers FA3 dispatch
83
+ try:
84
+ from xformers.ops.fmha import _set_use_fa3
85
+ _set_use_fa3(False)
86
+ print("[ATTN] xformers FA3 dispatch disabled (Blackwell-incompatible)")
87
+ except Exception as e:
88
+ print(f"[ATTN] FA3 disable FAILED: {type(e).__name__}: {e}")
89
+
90
+ # FUSE/mmap workaround
91
+ import json
92
+ import struct
93
+ from ltx_core.loader.primitives import StateDict
94
+ from ltx_core.loader.sft_loader import SafetensorsStateDictLoader
95
+
96
+ _SAFETENSORS_DTYPE_MAP = {
97
+ "F64": torch.float64,
98
+ "F32": torch.float32,
99
+ "F16": torch.float16,
100
+ "BF16": torch.bfloat16,
101
+ "F8_E5M2": torch.float8_e5m2,
102
+ "F8_E4M3": torch.float8_e4m3fn,
103
+ "I64": torch.int64,
104
+ "I32": torch.int32,
105
+ "I16": torch.int16,
106
+ "I8": torch.int8,
107
+ "U8": torch.uint8,
108
+ "BOOL": torch.bool,
109
+ }
110
+
111
+ def _patched_load(self, path, sd_ops, device=None):
112
+ sd = {}
113
+ size = 0
114
+ dtype = set()
115
+ device = device or torch.device("cpu")
116
+ model_paths = path if isinstance(path, list) else [path]
117
+ for shard_path in model_paths:
118
+ with open(shard_path, "rb") as f:
119
+ header_len = struct.unpack("<Q", f.read(8))[0]
120
+ header = json.loads(f.read(header_len).decode("utf-8"))
121
+ data_base = 8 + header_len
122
+ for name, meta in header.items():
123
+ if name == "__metadata__":
124
+ continue
125
+ expected_name = name if sd_ops is None else sd_ops.apply_to_key(name)
126
+ if expected_name is None:
127
+ continue
128
+ start, end = meta["data_offsets"]
129
+ f.seek(data_base + start)
130
+ buf = f.read(end - start)
131
+ t = torch.frombuffer(
132
+ bytearray(buf), dtype=_SAFETENSORS_DTYPE_MAP[meta["dtype"]]
133
+ ).reshape(meta["shape"])
134
+ t = t.to(device=device, non_blocking=True, copy=False)
135
+ kvs = (
136
+ ((expected_name, t),)
137
+ if sd_ops is None
138
+ else sd_ops.apply_to_key_value(expected_name, t)
139
+ )
140
+ for key, v in kvs:
141
+ size += v.nbytes
142
+ dtype.add(v.dtype)
143
+ sd[key] = v
144
+ return StateDict(sd=sd, device=device, size=size, dtype=dtype)
145
+
146
+ SafetensorsStateDictLoader.load = _patched_load
147
+ print("[FUSE-PATCH] SafetensorsStateDictLoader.load replaced (chunked-read)")
148
+
149
+ logging.getLogger().setLevel(logging.INFO)
150
+
151
+ MAX_SEED = np.iinfo(np.int32).max
152
+ DEFAULT_FRAME_RATE = 24.0
153
+
154
+ RESOLUTIONS = {
155
+ "high": {"16:9": (1536, 1024), "9:16": (1024, 1536), "1:1": (1024, 1024)},
156
+ "low": {"16:9": (768, 512), "9:16": (512, 768), "1:1": (768, 768)},
157
+ }
158
+
159
+ LTX_MOUNT = "/models/ltx"
160
+ GEMMA_MOUNT = "/models/gemma"
161
+
162
+ DISTILLED_FILENAME = "ltx-2.3-22b-distilled-1.1.safetensors"
163
+ UPSCALER_FILENAME = "ltx-2.3-spatial-upscaler-x2-1.1.safetensors"
164
+
165
+ # Detect mounts or download fallback
166
+ if os.path.exists(LTX_MOUNT):
167
+ distilled_checkpoint_path = os.path.join(LTX_MOUNT, DISTILLED_FILENAME)
168
+ spatial_upsampler_path = os.path.join(LTX_MOUNT, UPSCALER_FILENAME)
169
+ else:
170
+ print("LTX Mount not found. Downloading weights from Hugging Face...")
171
+ distilled_checkpoint_path = hf_hub_download(
172
+ repo_id="Lightricks/LTX-2.3",
173
+ filename=DISTILLED_FILENAME
174
+ )
175
+ spatial_upsampler_path = hf_hub_download(
176
+ repo_id="Lightricks/LTX-2.3",
177
+ filename=UPSCALER_FILENAME
178
+ )
179
+
180
+ if os.path.exists(GEMMA_MOUNT):
181
+ gemma_root = GEMMA_MOUNT
182
+ else:
183
+ print("Gemma Mount not found. Downloading Gemma model from Hugging Face...")
184
+ gemma_root = snapshot_download(
185
+ repo_id="Lightricks/LTX-2.3",
186
+ allow_patterns=["gemma/*"]
187
+ )
188
+ gemma_root = os.path.join(gemma_root, "gemma")
189
+
190
+ # Initialize pipeline
191
+ print("Initializing DistilledPipeline...")
192
+ pipeline = DistilledPipeline(
193
+ distilled_checkpoint_path=distilled_checkpoint_path,
194
+ spatial_upsampler_path=spatial_upsampler_path,
195
+ gemma_root=gemma_root,
196
+ loras=[],
197
+ quantization=QuantizationPolicy.fp8_cast(),
198
+ )
199
+
200
+ # Preload models
201
+ print("Preloading models for ZeroGPU...")
202
+ ledger = pipeline.model_ledger
203
+ _transformer = ledger.transformer()
204
+ _video_encoder = ledger.video_encoder()
205
+ _video_decoder = ledger.video_decoder()
206
+ _audio_decoder = ledger.audio_decoder()
207
+ _vocoder = ledger.vocoder()
208
+ _spatial_upsampler = ledger.spatial_upsampler()
209
+ _text_encoder = ledger.text_encoder()
210
+ _embeddings_processor = ledger.gemma_embeddings_processor()
211
+
212
+ ledger.transformer = lambda: _transformer
213
+ ledger.video_encoder = lambda: _video_encoder
214
+ ledger.video_decoder = lambda: _video_decoder
215
+ ledger.audio_decoder = lambda: _audio_decoder
216
+ ledger.vocoder = lambda: _vocoder
217
+ ledger.spatial_upsampler = lambda: _spatial_upsampler
218
+ ledger.text_encoder = lambda: _text_encoder
219
+ ledger.gemma_embeddings_processor = lambda: _embeddings_processor
220
+ print("All models preloaded and mapped!")
221
+
222
+
223
+ def log_memory(tag: str):
224
+ if torch.cuda.is_available():
225
+ allocated = torch.cuda.memory_allocated() / 1024**3
226
+ peak = torch.cuda.max_memory_allocated() / 1024**3
227
+ free, total = torch.cuda.mem_get_info()
228
+ print(f"[VRAM {tag}] allocated={allocated:.2f}GB peak={peak:.2f}GB free={free / 1024**3:.2f}GB total={total / 1024**3:.2f}GB")
229
+
230
+
231
+ def detect_aspect_ratio(image) -> str:
232
+ if image is None:
233
+ return "16:9"
234
+ if hasattr(image, "size"):
235
+ w, h = image.size
236
+ elif hasattr(image, "shape"):
237
+ h, w = image.shape[:2]
238
+ else:
239
+ return "16:9"
240
+ ratio = w / h
241
+ candidates = {"16:9": 16 / 9, "9:16": 9 / 16, "1:1": 1.0}
242
+ return min(candidates, key=lambda k: abs(ratio - candidates[k]))
243
+
244
+
245
+ def on_image_upload(image, high_res):
246
+ aspect = detect_aspect_ratio(image)
247
+ tier = "high" if high_res else "low"
248
+ w, h = RESOLUTIONS[tier][aspect]
249
+ return gr.update(value=w), gr.update(value=h)
250
+
251
+
252
+ def on_highres_toggle(image, high_res):
253
+ aspect = detect_aspect_ratio(image)
254
+ tier = "high" if high_res else "low"
255
+ w, h = RESOLUTIONS[tier][aspect]
256
+ return gr.update(value=w), gr.update(value=h)
257
+
258
+
259
+ # VIDEO TO VIDEO INFERENCE
260
+ @spaces.GPU(duration=120)
261
+ @torch.inference_mode()
262
+ def generate_video_to_video(
263
+ input_video: str,
264
+ prompt: str,
265
+ strength: float = 0.6,
266
+ duration: float = 3.0,
267
+ audio_mode: str = "Keep original audio",
268
+ enhance_prompt: bool = False,
269
+ seed: int = 42,
270
+ randomize_seed: bool = True,
271
+ height: int = 512,
272
+ width: int = 768,
273
+ progress=gr.Progress(track_tqdm=True),
274
+ ):
275
+ try:
276
+ if input_video is None:
277
+ raise ValueError("An input video must be uploaded for Video-to-Video generation.")
278
+
279
+ torch.cuda.reset_peak_memory_stats()
280
+ log_memory("V2V start")
281
+
282
+ current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
283
+ generator = torch.Generator(device=pipeline.device).manual_seed(current_seed)
284
+ noiser = GaussianNoiser(generator=generator)
285
+ stepper = EulerDiffusionStep()
286
+ dtype = pipeline.dtype
287
+
288
+ # Detect original metadata
289
+ try:
290
+ fps, orig_frames, w, h = get_videostream_metadata(input_video)
291
+ print(f"Loaded original video: {orig_frames} frames, {fps} fps, size={w}x{h}")
292
+ except Exception as e:
293
+ print(f"Could not load stream metadata: {e}. Defaulting to 24 FPS.")
294
+ fps = DEFAULT_FRAME_RATE
295
+
296
+ frame_rate = float(fps) if fps > 0 else DEFAULT_FRAME_RATE
297
+ num_frames = int(duration * frame_rate) + 1
298
+ num_frames = ((num_frames - 1 + 7) // 8) * 8 + 1
299
+
300
+ print(f"Processing V2V: {height}x{width}, target={num_frames} frames ({duration}s), seed={current_seed}")
301
+
302
+ # Load video frames for Stage 1 (half resolution)
303
+ video_pixel_stage_1 = load_video_conditioning(
304
+ video_path=input_video,
305
+ height=int(height // 2),
306
+ width=int(width // 2),
307
+ frame_cap=num_frames,
308
+ dtype=dtype,
309
+ device=pipeline.device
310
+ )
311
+ # Handle cases where the video has fewer frames than expected
312
+ F_actual = video_pixel_stage_1.shape[2]
313
+ if F_actual < num_frames:
314
+ num_frames = ((F_actual - 1) // 8) * 8 + 1
315
+ if num_frames < 9:
316
+ num_frames = 9
317
+ video_pixel_stage_1 = video_pixel_stage_1[:, :, :num_frames]
318
+ print(f"Capping frame count to actual video frames: {num_frames}")
319
+
320
+ # Load video frames for Stage 2 (full resolution)
321
+ video_pixel_stage_2 = load_video_conditioning(
322
+ video_path=input_video,
323
+ height=int(height),
324
+ width=int(width),
325
+ frame_cap=num_frames,
326
+ dtype=dtype,
327
+ device=pipeline.device
328
+ )
329
+ video_pixel_stage_2 = video_pixel_stage_2[:, :, :num_frames]
330
+
331
+ # Encode prompts
332
+ (ctx_p,) = encode_prompts(
333
+ [prompt],
334
+ pipeline.model_ledger,
335
+ enhance_first_prompt=enhance_prompt,
336
+ enhance_prompt_image=None,
337
+ )
338
+ video_context, audio_context = ctx_p.video_encoding, ctx_p.audio_encoding
339
+
340
+ # Stage 1: Initial low resolution video denoising
341
+ video_encoder = pipeline.model_ledger.video_encoder()
342
+ transformer = pipeline.model_ledger.transformer()
343
+
344
+ # Map strength to starting step in the 8-step distilled schedule
345
+ num_steps = max(1, int(strength * 8))
346
+ start_idx = 8 - num_steps
347
+ stage_1_sigmas = torch.Tensor(DISTILLED_SIGMA_VALUES[start_idx:]).to(pipeline.device)
348
+ print(f"V2V Stage 1 schedule: {len(stage_1_sigmas)-1} steps, starting at sigma={stage_1_sigmas[0]:.4f}")
349
+
350
+ # Encode downscaled video to latents
351
+ stage_1_initial_video_latent = video_encoder(video_pixel_stage_1)
352
+
353
+ def denoising_loop(
354
+ sigmas: torch.Tensor, video_state: LatentState, audio_state: LatentState, stepper: DiffusionStepProtocol
355
+ ) -> tuple[LatentState, LatentState]:
356
+ return euler_denoising_loop(
357
+ sigmas=sigmas,
358
+ video_state=video_state,
359
+ audio_state=audio_state,
360
+ stepper=stepper,
361
+ denoise_fn=simple_denoising_func(
362
+ video_context=video_context,
363
+ audio_context=audio_context,
364
+ transformer=transformer,
365
+ ),
366
+ )
367
+
368
+ stage_1_output_shape = VideoPixelShape(
369
+ batch=1,
370
+ frames=num_frames,
371
+ width=width // 2,
372
+ height=height // 2,
373
+ fps=frame_rate,
374
+ )
375
+
376
+ video_state, audio_state = denoise_audio_video(
377
+ output_shape=stage_1_output_shape,
378
+ conditionings=[],
379
+ noiser=noiser,
380
+ sigmas=stage_1_sigmas,
381
+ stepper=stepper,
382
+ denoising_loop_fn=denoising_loop,
383
+ components=pipeline.pipeline_components,
384
+ dtype=dtype,
385
+ device=pipeline.device,
386
+ noise_scale=stage_1_sigmas[0],
387
+ initial_video_latent=stage_1_initial_video_latent,
388
+ initial_audio_latent=None,
389
+ )
390
+
391
+ # Stage 2: Upsample and refine
392
+ upscaled_video_latent = upsample_video(
393
+ latent=video_state.latent[:1],
394
+ video_encoder=video_encoder,
395
+ upsampler=pipeline.model_ledger.spatial_upsampler()
396
+ )
397
+
398
+ torch.cuda.synchronize()
399
+ cleanup_memory()
400
+
401
+ stage_2_sigmas = torch.Tensor(STAGE_2_DISTILLED_SIGMA_VALUES).to(pipeline.device)
402
+ stage_2_output_shape = VideoPixelShape(
403
+ batch=1,
404
+ frames=num_frames,
405
+ width=width,
406
+ height=height,
407
+ fps=frame_rate
408
+ )
409
+
410
+ video_state, audio_state = denoise_audio_video(
411
+ output_shape=stage_2_output_shape,
412
+ conditionings=[],
413
+ noiser=noiser,
414
+ sigmas=stage_2_sigmas,
415
+ stepper=stepper,
416
+ denoising_loop_fn=denoising_loop,
417
+ components=pipeline.pipeline_components,
418
+ dtype=dtype,
419
+ device=pipeline.device,
420
+ noise_scale=stage_2_sigmas[0],
421
+ initial_video_latent=upscaled_video_latent,
422
+ initial_audio_latent=audio_state.latent,
423
+ )
424
+
425
+ torch.cuda.synchronize()
426
+ cleanup_memory()
427
+
428
+ # VAE decoding
429
+ decoded_video = vae_decode_video(
430
+ video_state.latent,
431
+ pipeline.model_ledger.video_decoder(),
432
+ TilingConfig.default(),
433
+ generator
434
+ )
435
+
436
+ # Handle audio mode
437
+ output_audio = None
438
+ if audio_mode == "Keep original audio":
439
+ try:
440
+ original_audio = decode_audio_from_file(
441
+ path=input_video,
442
+ device=pipeline.device,
443
+ start_time=0.0,
444
+ max_duration=duration,
445
+ )
446
+ output_audio = original_audio
447
+ print("Original audio successfully extracted.")
448
+ except Exception as e:
449
+ print(f"Failed to extract original audio: {e}. Outputting silent or generated audio.")
450
+
451
+ if output_audio is None and audio_mode != "No audio":
452
+ decoded_audio = vae_decode_audio(
453
+ audio_state.latent,
454
+ pipeline.model_ledger.audio_decoder(),
455
+ pipeline.model_ledger.vocoder()
456
+ )
457
+ output_audio = decoded_audio
458
+ print("Generated synchronized audio.")
459
+
460
+ # Encode and save output video file
461
+ tiling_config = TilingConfig.default()
462
+ video_chunks_number = get_video_chunks_number(num_frames, tiling_config)
463
+ output_path = tempfile.mktemp(suffix=".mp4")
464
+
465
+ encode_video(
466
+ video=decoded_video,
467
+ fps=frame_rate,
468
+ audio=output_audio,
469
+ output_path=output_path,
470
+ video_chunks_number=video_chunks_number,
471
+ )
472
+
473
+ log_memory("V2V finished")
474
+ return str(output_path), current_seed
475
+
476
+ except Exception as e:
477
+ import traceback
478
+ log_memory("V2V error")
479
+ print(f"Error in V2V: {str(e)}\n{traceback.format_exc()}")
480
+ return None, current_seed
481
+
482
+
483
+ # STANDARD GENERATION INFERENCE (Tab 2)
484
+ @spaces.GPU(duration=75)
485
+ @torch.inference_mode()
486
+ def generate_video(
487
+ input_image,
488
+ prompt: str,
489
+ duration: float,
490
+ enhance_prompt: bool = False,
491
+ seed: int = 42,
492
+ randomize_seed: bool = True,
493
+ height: int = 1024,
494
+ width: int = 1536,
495
+ progress=gr.Progress(track_tqdm=True),
496
+ ):
497
+ try:
498
+ torch.cuda.reset_peak_memory_stats()
499
+ log_memory("T2V start")
500
+
501
+ current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
502
+ frame_rate = DEFAULT_FRAME_RATE
503
+ num_frames = int(duration * frame_rate) + 1
504
+ num_frames = ((num_frames - 1 + 7) // 8) * 8 + 1
505
+
506
+ print(f"Generating Video: {height}x{width}, {num_frames} frames ({duration}s), seed={current_seed}")
507
+
508
+ images = []
509
+ if input_image is not None:
510
+ output_dir = Path("outputs")
511
+ output_dir.mkdir(exist_ok=True)
512
+ temp_image_path = output_dir / f"temp_input_{current_seed}.jpg"
513
+ if hasattr(input_image, "save"):
514
+ input_image.save(temp_image_path)
515
+ else:
516
+ temp_image_path = Path(input_image)
517
+ images = [ImageConditioningInput(path=str(temp_image_path), frame_idx=0, strength=1.0)]
518
+
519
+ tiling_config = TilingConfig.default()
520
+ video_chunks_number = get_video_chunks_number(num_frames, tiling_config)
521
+
522
+ video, audio = pipeline(
523
+ prompt=prompt,
524
+ seed=current_seed,
525
+ height=int(height),
526
+ width=int(width),
527
+ num_frames=num_frames,
528
+ frame_rate=frame_rate,
529
+ images=images,
530
+ tiling_config=tiling_config,
531
+ enhance_prompt=enhance_prompt,
532
+ )
533
+
534
+ output_path = tempfile.mktemp(suffix=".mp4")
535
+ encode_video(
536
+ video=video,
537
+ fps=frame_rate,
538
+ audio=audio,
539
+ output_path=output_path,
540
+ video_chunks_number=video_chunks_number,
541
+ )
542
+
543
+ log_memory("T2V finished")
544
+ return str(output_path), current_seed
545
+
546
+ except Exception as e:
547
+ import traceback
548
+ log_memory("T2V error")
549
+ print(f"Error in T2V: {str(e)}\n{traceback.format_exc()}")
550
+ return None, current_seed
551
+
552
+
553
+ # GRADIO UI SETUP
554
+ with gr.Blocks(title="LTX V2V") as demo:
555
+ gr.Markdown("# LTX V2V: Distilled 22B Video-to-Video & Generation")
556
+ gr.Markdown(
557
+ "Highly efficient video translation (stylization, restyling, editing) and text/image-to-video generation using LTX-2.3. "
558
+ "[[model]](https://huggingface.co/Lightricks/LTX-2.3) "
559
+ "[[code]](https://github.com/Lightricks/LTX-2)"
560
+ )
561
+
562
+ with gr.Tabs():
563
+ # TAB 1: Video to Video
564
+ with gr.TabItem("Video-to-Video (V2V)"):
565
+ with gr.Row():
566
+ with gr.Column():
567
+ v2v_input_video = gr.Video(label="Input Video", sources=["upload"])
568
+ v2v_prompt = gr.Textbox(
569
+ label="Prompt",
570
+ info="Describe the style, aesthetic, actions or changes to apply (e.g. 'Turn the person into a robot', 'Anime style')",
571
+ value="A cinematic cartoon rendering of the motion, vibrant styling, detailed painting look",
572
+ lines=3
573
+ )
574
+ v2v_strength = gr.Slider(
575
+ label="Denoising Strength (0.0 = original, 1.0 = completely new)",
576
+ minimum=0.1,
577
+ maximum=1.0,
578
+ value=0.6,
579
+ step=0.05
580
+ )
581
+ with gr.Row():
582
+ v2v_duration = gr.Slider(label="Duration (seconds)", minimum=1.0, maximum=10.0, value=3.0, step=0.1)
583
+ v2v_audio_mode = gr.Dropdown(
584
+ label="Audio Mode",
585
+ choices=["Keep original audio", "Generate new audio", "No audio"],
586
+ value="Keep original audio"
587
+ )
588
+
589
+ v2v_generate_btn = gr.Button("Transform Video", variant="primary", size="lg")
590
+
591
+ with gr.Accordion("Advanced Settings", open=False):
592
+ v2v_seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, value=42, step=1)
593
+ v2v_randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
594
+ with gr.Row():
595
+ v2v_width = gr.Dropdown(label="Width", choices=[512, 768, 1024, 1536], value=768)
596
+ v2v_height = gr.Dropdown(label="Height", choices=[512, 768, 1024, 1536], value=512)
597
+
598
+ with gr.Column():
599
+ v2v_output_video = gr.Video(label="Transformed Video", autoplay=True)
600
+
601
+ v2v_generate_btn.click(
602
+ fn=generate_video_to_video,
603
+ inputs=[
604
+ v2v_input_video,
605
+ v2v_prompt,
606
+ v2v_strength,
607
+ v2v_duration,
608
+ v2v_audio_mode,
609
+ gr.Checkbox(visible=False, value=False), # enhance_prompt hidden or set False
610
+ v2v_seed,
611
+ v2v_randomize_seed,
612
+ v2v_height,
613
+ v2v_width
614
+ ],
615
+ outputs=[v2v_output_video, v2v_seed]
616
+ )
617
+
618
+ # TAB 2: Text/Image to Video
619
+ with gr.TabItem("Text/Image-to-Video"):
620
+ with gr.Row():
621
+ with gr.Column():
622
+ input_image = gr.Image(label="Input Image (Optional)", type="pil")
623
+ t2v_prompt = gr.Textbox(
624
+ label="Prompt",
625
+ info="for best results - make it as elaborate as possible",
626
+ value="Make this image come alive with cinematic motion, smooth animation",
627
+ lines=3,
628
+ )
629
+ with gr.Row():
630
+ t2v_duration = gr.Slider(label="Duration (seconds)", minimum=1.0, maximum=10.0, value=3.0, step=0.1)
631
+ with gr.Column():
632
+ t2v_enhance_prompt = gr.Checkbox(label="Enhance Prompt", value=False)
633
+ high_res = gr.Checkbox(label="High Resolution", value=True)
634
+
635
+ t2v_generate_btn = gr.Button("Generate Video", variant="primary", size="lg")
636
+
637
+ with gr.Accordion("Advanced Settings", open=False):
638
+ t2v_seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, value=10, step=1)
639
+ t2v_randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
640
+ with gr.Row():
641
+ t2v_width = gr.Number(label="Width", value=1536, precision=0)
642
+ t2v_height = gr.Number(label="Height", value=1024, precision=0)
643
+
644
+ with gr.Column():
645
+ t2v_output_video = gr.Video(label="Generated Video", autoplay=True)
646
+
647
+ # Auto-detect resolution from image
648
+ input_image.change(
649
+ fn=on_image_upload,
650
+ inputs=[input_image, high_res],
651
+ outputs=[t2v_width, t2v_height],
652
+ )
653
+ high_res.change(
654
+ fn=on_highres_toggle,
655
+ inputs=[input_image, high_res],
656
+ outputs=[t2v_width, t2v_height],
657
+ )
658
+
659
+ t2v_generate_btn.click(
660
+ fn=generate_video,
661
+ inputs=[
662
+ input_image, t2v_prompt, t2v_duration, t2v_enhance_prompt,
663
+ t2v_seed, t2v_randomize_seed, t2v_height, t2v_width,
664
+ ],
665
+ outputs=[t2v_output_video, t2v_seed],
666
+ )
667
+
668
+ css = """
669
+ .fillable{max-width: 1200px !important}
670
+ .progress-text {color: white}
671
+ """
672
+
673
+ if __name__ == "__main__":
674
+ demo.launch(theme=gr.themes.Citrus(), css=css)
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