linoyts HF Staff commited on
Commit
82403b3
·
1 Parent(s): 9608601

Switch backend to native LTX-2 (ICLoraPipeline) (#2)

Browse files

- Switch backend to native LTX-2 (ICLoraPipeline) (68a3f584144acdf70a7213b3954a793582693a97)

Files changed (3) hide show
  1. README.md +3 -3
  2. app.py +233 -110
  3. requirements.txt +9 -7
README.md CHANGED
@@ -11,7 +11,7 @@ pinned: false
11
  hardware: zero-a10g
12
  short_description: Fast reference-sheet to video with LTX-2.3 IC-LoRA
13
  models:
14
- - diffusers/LTX-2.3-Distilled-Diffusers
15
  - Lightricks/LTX-2.3-22b-IC-LoRA-Ingredients
16
  ---
17
 
@@ -23,5 +23,5 @@ recipe is non-distilled (30 steps, guidance 4.0) — see the companion non-disti
23
  maximum fidelity.
24
 
25
  Runs the IC-LoRA from [`linoyts/LTX-2.3-loras`](https://huggingface.co/linoyts/LTX-2.3-loras)
26
- on [`diffusers/LTX-2.3-Distilled-Diffusers`](https://huggingface.co/diffusers/LTX-2.3-Distilled-Diffusers)
27
- via `LTX2InContextPipeline`. 768×448 × 121 frames @ 24fps, LoRA scale 1.4.
 
11
  hardware: zero-a10g
12
  short_description: Fast reference-sheet to video with LTX-2.3 IC-LoRA
13
  models:
14
+ - Lightricks/LTX-2.3
15
  - Lightricks/LTX-2.3-22b-IC-LoRA-Ingredients
16
  ---
17
 
 
23
  maximum fidelity.
24
 
25
  Runs the IC-LoRA from [`linoyts/LTX-2.3-loras`](https://huggingface.co/linoyts/LTX-2.3-loras)
26
+ on [`Lightricks/LTX-2.3`](https://huggingface.co/Lightricks/LTX-2.3)
27
+ via the native LTX-2 pipeline. 768×448 × 121 frames @ 24fps, LoRA scale 1.4.
app.py CHANGED
@@ -1,57 +1,223 @@
1
  import os
 
 
2
 
3
- os.environ.setdefault("TORCH_COMPILE_DISABLE", "1")
4
- os.environ.setdefault("TORCHDYNAMO_DISABLE", "1")
 
5
 
6
- import math
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  import random
8
  import tempfile
9
- import threading
10
- import time
11
 
12
  import numpy as np
13
- import spaces
 
 
14
  import torch
 
 
 
 
15
  import gradio as gr
16
- from PIL import Image, ImageOps
17
- from huggingface_hub import hf_hub_download
18
- from safetensors.torch import load_file
19
-
20
- from diffusers import LTX2InContextPipeline, LTX2LatentUpsamplePipeline
21
- from diffusers.pipelines.ltx2.pipeline_ltx2_ic_lora import LTX2ReferenceCondition
22
- from diffusers.pipelines.ltx2.latent_upsampler import LTX2LatentUpsamplerModel
23
- from diffusers.pipelines.ltx2.utils import DISTILLED_SIGMA_VALUES, STAGE_2_DISTILLED_SIGMA_VALUES
24
- from diffusers.utils import encode_video
25
-
26
- # --- Config -----------------------------------------------------------------
27
- # FAST distilled variant of the ingredients (reference-sheet) IC-LoRA: 8-step schedule, CFG off.
28
- BASE_MODEL = "diffusers/LTX-2.3-Distilled-Diffusers"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  LORA_REPO = "Lightricks/LTX-2.3-22b-IC-LoRA-Ingredients"
30
  LORA_FILE = "ltx-2.3-22b-ic-lora-ingredients-0.9.safetensors"
31
  LORA_SCALE = 1.4
32
- FPS = 24
33
- WIDTH, HEIGHT = 768, 448
34
- NUM_FRAMES = 121
35
- NUM_STEPS = len(DISTILLED_SIGMA_VALUES) # 8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  MAX_SEED = np.iinfo(np.int32).max
37
  HF_TOKEN = os.environ.get("HF_TOKEN")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- UPSAMPLER_REPO = "dg845/LTX-2.3-Spatial-Upsampler-Diffusers" # LTX-2.3 spatial x2 latent upsampler
40
 
41
- pipe = LTX2InContextPipeline.from_pretrained(BASE_MODEL, torch_dtype=torch.bfloat16)
42
- pipe.to("cuda")
43
- pipe.vae.enable_tiling()
44
- _lora_path = hf_hub_download(LORA_REPO, LORA_FILE, token=HF_TOKEN)
45
- # Kept as a togglable adapter (NOT fused) so Stage 2 can run on the bare distilled model.
46
- pipe.load_lora_weights(load_file(_lora_path), adapter_name="ingredients")
47
- # NOTE: AOTI temporarily disabled while validating 2-stage inference; re-enable once confirmed.
48
- # spaces.aoti_load(module=pipe.transformer, repo_id="ltx-community/LTX-2.3-Transformer-GroupA-sm120-cu130-r9e")
 
 
 
 
 
 
49
 
50
- # Stage-2 latent upsampler (spatial x2) two-stage diffusers inference.
51
- _upsampler = LTX2LatentUpsamplerModel.from_pretrained(
52
- UPSAMPLER_REPO, subfolder="latent_upsampler", torch_dtype=torch.bfloat16)
53
- _upsampler.to("cuda")
54
- upsample_pipe = LTX2LatentUpsamplePipeline(vae=pipe.vae, latent_upsampler=_upsampler)
 
 
 
 
 
 
 
 
 
 
55
 
56
 
57
  def _gallery_paths(gallery):
@@ -67,6 +233,7 @@ def _gallery_paths(gallery):
67
 
68
 
69
  def compose_sheet(gallery):
 
70
  paths = _gallery_paths(gallery)
71
  if not paths:
72
  raise gr.Error("Upload at least one subject image to build a sheet.")
@@ -75,79 +242,45 @@ def compose_sheet(gallery):
75
  return imgs[0]
76
  CW, CH = 1536, 896
77
  canvas = Image.new("RGB", (CW, CH), (0, 0, 0))
78
- n = len(imgs)
79
- cols = math.ceil(math.sqrt(n))
80
- rows = math.ceil(n / cols)
81
- g = 16
82
- cw = (CW - g * (cols + 1)) // cols
83
- ch = (CH - g * (rows + 1)) // rows
84
  for i, im in enumerate(imgs):
85
  r, c = divmod(i, cols)
86
  canvas.paste(ImageOps.fit(im, (cw, ch), Image.LANCZOS), (g + c * (cw + g), g + r * (ch + g)))
87
  return canvas
88
 
89
 
90
- def _build_prompt(sheet, action):
91
- return f"Reference sheet: {sheet.strip()}\n\nGenerated video: {action.strip()}"
92
-
93
-
94
- def _export(video_np, audio, path):
95
- kw = {}
96
- if audio is not None:
97
- kw = dict(audio=audio[0].float().cpu(), audio_sample_rate=pipe.vocoder.config.output_sampling_rate)
98
- encode_video(video_np, fps=FPS, output_path=path, **kw)
99
-
100
-
101
  def _duration(*args, **kwargs):
102
- return 480 # two-stage (stage1 + x2 upsample + stage2 refine), AOTI off during validation
 
103
 
104
 
105
  @spaces.GPU(duration=_duration)
106
- def generate(sheet_image, sheet, action, lora_scale, seed, randomize, progress=gr.Progress(track_tqdm=True)):
 
107
  if sheet_image is None:
108
  raise gr.Error("Add a reference sheet (upload one, or build one from subject images in the other tab).")
109
  if not sheet.strip():
110
  raise gr.Error("Describe the elements in the reference sheet (characters, props, location).")
111
  if not action.strip():
112
  raise gr.Error("Describe the action / shot you want generated.")
113
-
114
- if randomize:
115
- seed = random.randint(0, MAX_SEED)
116
- seed = int(seed)
117
-
118
- sheet_img = sheet_image.convert("RGB").resize((WIDTH, HEIGHT), Image.LANCZOS)
119
- ref = [sheet_img] * NUM_FRAMES
120
- prompt = _build_prompt(sheet, action)
121
- gen = torch.Generator(device="cuda").manual_seed(seed)
122
-
123
- # --- Stage 1: base-res latents with the ingredients IC-LoRA (distilled 8-step) ---
124
- pipe.set_adapters("ingredients", float(lora_scale))
125
- video_latent, audio_latent = pipe(
126
- prompt=prompt, negative_prompt="",
127
- reference_conditions=[LTX2ReferenceCondition(frames=ref, strength=1.0)],
128
- reference_downscale_factor=1,
129
- width=WIDTH, height=HEIGHT, num_frames=NUM_FRAMES, frame_rate=FPS,
130
- num_inference_steps=NUM_STEPS, sigmas=DISTILLED_SIGMA_VALUES,
131
- guidance_scale=1.0, stg_scale=0.0, audio_guidance_scale=1.0, audio_stg_scale=0.0,
132
- generator=gen, output_type="latent", return_dict=False,
133
  )
134
- # --- Stage 2a: spatial x2 latent upsample ---
135
- up_latent = upsample_pipe(latents=video_latent, output_type="latent", return_dict=False)[0]
136
- # --- Stage 2b: short refine at 2x res on the bare distilled model (drop IC-LoRA + reference) ---
137
- pipe.disable_lora()
138
- video_out, audio_out = pipe(
139
- prompt=prompt, negative_prompt="",
140
- latents=up_latent, audio_latents=audio_latent,
141
- width=WIDTH * 2, height=HEIGHT * 2, num_frames=NUM_FRAMES, frame_rate=FPS,
142
- num_inference_steps=len(STAGE_2_DISTILLED_SIGMA_VALUES),
143
- sigmas=STAGE_2_DISTILLED_SIGMA_VALUES, noise_scale=STAGE_2_DISTILLED_SIGMA_VALUES[0],
144
- guidance_scale=1.0, stg_scale=0.0, audio_guidance_scale=1.0, audio_stg_scale=0.0,
145
- generator=gen, output_type="np", return_dict=False,
146
- )
147
- pipe.enable_lora()
148
-
149
- out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
150
- _export(video_out[0], audio_out, out_path)
151
  return out_path, seed
152
 
153
 
@@ -155,8 +288,8 @@ with gr.Blocks(title="LTX-2.3 Ingredients (Fast)") as demo:
155
  gr.Markdown(
156
  "# ⚡ LTX-2.3 Ingredients — Fast (Distilled)\n"
157
  "Reference-sheet control, fast. Upload a ready sheet, or build one from individual subject images. Using "
158
- "[LTX 2.3 Distilled](https://huggingface.co/diffusers/LTX-2.3-Distilled-Diffusers) with the "
159
- "[Ingredients IC-LoRA](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Ingredients), via diffusers 🧨. "
160
  "(For maximum fidelity, see the Dev demo.)"
161
  )
162
  with gr.Row():
@@ -174,7 +307,6 @@ with gr.Blocks(title="LTX-2.3 Ingredients (Fast)") as demo:
174
  action = gr.Textbox(label="Generated video — action / shot, plus any speech & sounds", lines=3,
175
  placeholder="the woman walks down the alley, checks the pocket watch and whispers 'almost time'; footsteps on cobblestone, distant city hum")
176
  with gr.Accordion("Settings", open=False):
177
- lora_scale = gr.Slider(0.8, 1.8, value=1.4, step=0.05, label="LoRA strength")
178
  randomize = gr.Checkbox(True, label="Randomize seed")
179
  seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Seed")
180
  run = gr.Button("Generate (fast)", variant="primary")
@@ -182,24 +314,15 @@ with gr.Blocks(title="LTX-2.3 Ingredients (Fast)") as demo:
182
  video_out = gr.Video(label="Generated video")
183
 
184
  build_btn.click(compose_sheet, inputs=gallery, outputs=sheet_image)
185
- run.click(generate, inputs=[sheet_image, sheet, action, lora_scale, seed, randomize], outputs=[video_out, seed])
186
 
187
  gr.Examples(
188
  examples=[
189
- ["examples/sheet_garden.png",
190
- "a friendly cartoon hedgehog with rounded chestnut-brown fur, a cream face and belly, large expressive dark eyes, a small black nose and tiny rounded ears, shown in a face close-up and a full-body turnaround standing upright on two short legs; a cheerful cartoon rabbit with soft grey-and-white fur, long upright ears with pale pink inner lining, round amber eyes and a fluffy white tail, shown in a body turnaround; a green coiled garden hose neatly wound on a matching green wall-mounted reel; a row of green plastic spray bottles with trigger nozzles; the bright interior of a 'Greenfield Home & Garden' store with tall wooden shelves stocked with leafy potted plants, terracotta pots and gardening supplies, warm overhead lighting and a green-and-white storefront sign",
191
- "cheerful family-animation commercial scene, a lively medium shot inside the sunlit 'Greenfield Home & Garden' store with its tall shelves of leafy potted plants and terracotta pots. the rounded chestnut-brown hedgehog waddles briskly up toward the camera on its short legs, its cream belly bouncing, then stops, lifts a tiny paw in a friendly wave and beams with wide sparkling eyes, announcing in a warm, bright, sing-song voice: 'welcome to Greenfield!'. behind it the soft grey-and-white rabbit hops past in the aisle, long ears bobbing, cradling a green spray bottle against its chest; it pauses, gives the bottle a playful little squeeze that puffs a fine mist into a shaft of light, and adds in a chirpy, slightly higher voice: 'everything your garden needs!'. the hedgehog nods enthusiastically, gestures with both paws toward the laden shelves and finishes with a cosy chuckle: 'come on in!'. the animation is glossy and expressive with squash-and-stretch motion, rounded shapes and saturated greens; the camera pushes in gently and tilts up to reveal the green-and-white storefront sign. the audio is bright and immersive: the hedgehog's cheerful voice, the rabbit's lighter reply, the soft puff of the spray bottle, light pattering footsteps on the store floor and a warm, upbeat acoustic-ukulele jingle playing softly underneath",
192
- 1.4, 42, False],
193
- ["examples/sheet_hiker.png",
194
- "a young Asian woman with a warm skin tone, dark hair parted down the middle in two long braids resting on her chest, an olive-green short-sleeved t-shirt, khaki cargo pants, dark brown hiking boots and a black wristwatch on her left arm, with a serious natural expression; a large heavy-duty blue hiking backpack with an external silver metal frame, multiple side and top pouches, black adjustable straps and a brown leather square patch near the bottom; a simple thick natural wooden walking stick with rough bark texture and a slight fork near one end; a large sturdy yak with long shaggy white-and-blonde hair and curved grey horns, wearing an ornate saddle blanket with intricate blue, red and yellow patterns, a saddle with metal stirrups and colorful tassels near its ears and chest; a sweeping majestic mountain landscape where a dirt path winds through green rocky slopes toward towering snow-capped peaks under a bright blue sky with scattered white clouds; a small traditional square stone shrine with a flat slightly tiered roof and a bright yellow fabric valance along the roofline, bright blue window trim and a red wooden door, with a small stone stupa beside it",
195
- "cinematic adventure documentary scene, a dynamic medium wide shot of the young asian woman with her dark hair in two long braids, wearing an olive-green t-shirt and khaki cargo pants. she sits on a rock along a mountain dirt path, resting beside the massive white shaggy yak with curved horns and its ornate blue, red and yellow patterned saddle blanket with metal stirrups. leaning against a nearby small stone building with blue window trim, a red door and a yellow fabric roof valance are her large blue external-frame backpack and thick wooden walking stick; majestic snow-capped mountains tower in the distant background under a bright blue sky. she looks at the yak, chest heaving slightly from exertion, and says with a breathy, tired but gentle voice: 'we've got a long way to go...'. she pauses, extending her hand to gently pat the thick white fur on the yak's neck; the yak shifts its weight, the colorful tassels near its ears swaying, and a faint exhausted smile breaks across her face as she continues softly: '...big guy.'. lowering her hand she grabs her wooden walking stick, leaning her weight onto it as she turns her gaze up toward the distant snowy peaks, her expression shifting from exhaustion to quiet determination as she adds, her voice growing firmer: 'but the pass...', then takes a deep grounding breath: '...is just over that ridge.'. the camera is dynamically handheld, slowly orbiting the woman and the yak to reveal the depth of the valley and the towering mountains behind them; naturalistic breathtaking film aesthetic, bright crisp sunlight casting sharp shadows across the rocky path and stone shrine. clear immersive audio: her wind-swept voice, the heavy rhythmic breathing of the yak, the faint jingle of metal stirrups and the distant ambient howl of mountain winds, no background music",
196
- 1.4, 42, False],
197
- ["examples/subj_composite.png",
198
- "a smiling young woman with warm fair skin and shoulder-length curly dark-brown hair, soft brown eyes and a gentle open expression, wearing a cream cable-knit sweater and dark jeans, shown in a relaxed three-quarter pose; a dappled grey horse with a dark charcoal mane and tail, a soft mottled grey-and-white coat, dark intelligent eyes and a calm posture, wearing a simple brown leather halter; a green misty mountain meadow of tall dewy grass and scattered wildflowers, with faint pine-covered slopes dissolving into low morning fog under a pale silver sky",
199
- "tender naturalistic cinematic scene, a soft medium shot in a green misty mountain meadow at dawn, tall dewy grass glistening and low fog drifting between faint pine slopes. the young woman with curly dark-brown hair and a cream cable-knit sweater walks slowly up to the dappled grey horse, her breath faintly visible in the cool air, and raises a careful open hand. she gently strokes the horse's soft mottled neck, her face softening into a warm reassuring smile, and murmurs in a low, soothing, slightly breathy voice: 'good boy… easy now'. the horse lowers its head toward her, its charcoal mane shifting, flicks an ear and lets out a soft snort, its breath misting in the cold; she leans her forehead lightly against its cheek, closes her eyes for a moment, then whispers with quiet affection: 'there you go'. the camera drifts slowly in a gentle arc around the pair, the shallow-focus background of fog and wildflowers blurring softly behind them; the film aesthetic is delicate and breathtaking with cool silver dawn light and fine atmospheric haze. the audio is intimate and immersive: her gentle hushed voice, the horse's soft nicker and snort, the swish of dewy grass, light birdsong waking in the distance and a faint cool breeze, with no background music",
200
- 1.4, 42, False],
201
  ],
202
- inputs=[sheet_image, sheet, action, lora_scale, seed, randomize],
203
  outputs=[video_out, seed], fn=generate, cache_examples=True, cache_mode="lazy",
204
  )
205
 
 
1
  import os
2
+ import subprocess
3
+ import sys
4
 
5
+ # ZeroGPU: torch.compile / dynamo unsupported — disable before any torch import.
6
+ os.environ["TORCH_COMPILE_DISABLE"] = "1"
7
+ os.environ["TORCHDYNAMO_DISABLE"] = "1"
8
 
9
+ # memory-efficient attention
10
+ subprocess.run([sys.executable, "-m", "pip", "install", "xformers==0.0.32.post2", "--no-build-isolation"], check=False)
11
+
12
+ # --- clone + install the NATIVE LTX-2 codebase at the pinned commit the working ZeroGPU spaces use ---
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 = "ae855f8538843825f9015a419cf4ba5edaf5eec2"
16
+ if not os.path.exists(LTX_REPO_DIR):
17
+ subprocess.run(["git", "clone", LTX_REPO_URL, LTX_REPO_DIR], check=True)
18
+ subprocess.run(["git", "-C", LTX_REPO_DIR, "checkout", LTX_COMMIT], check=True)
19
+ subprocess.run([sys.executable, "-m", "pip", "install", "--force-reinstall", "--no-deps",
20
+ "-e", os.path.join(LTX_REPO_DIR, "packages", "ltx-core"),
21
+ "-e", os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines")], check=True)
22
+ sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines", "src"))
23
+ sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-core", "src"))
24
+
25
+ import logging
26
  import random
27
  import tempfile
 
 
28
 
29
  import numpy as np
30
+ import imageio.v3 as iio
31
+ from PIL import Image, ImageOps
32
+
33
  import torch
34
+ torch._dynamo.config.suppress_errors = True
35
+ torch._dynamo.config.disable = True
36
+
37
+ import spaces
38
  import gradio as gr
39
+ from huggingface_hub import hf_hub_download, snapshot_download
40
+
41
+ # Import LTX modules in the proven order — importing ltx_core.quantization/loader FIRST hits a
42
+ # circular import (fp8_cast <-> loader.fuse_loras). Importing the model modules first forces the
43
+ # correct init order (mirrors the working reference Space).
44
+ from ltx_core.model.video_vae import TilingConfig, get_video_chunks_number, decode_video as _vae_decode_video # noqa: F401
45
+ from ltx_core.model.upsampler import upsample_video as _upsample_video # noqa: F401
46
+ from ltx_core.model.audio_vae import encode_audio as _vae_encode_audio # noqa: F401
47
+ from ltx_core.quantization import QuantizationPolicy
48
+ from ltx_core.loader import LoraPathStrengthAndSDOps, LTXV_LORA_COMFY_RENAMING_MAP
49
+ from ltx_pipelines.ic_lora import ICLoraPipeline
50
+ from ltx_pipelines.utils.media_io import encode_video
51
+
52
+ # --- ZeroGPU loader patch -------------------------------------------------------------
53
+ # The native loader opens safetensors directly on the CUDA device
54
+ # (safe_open(path, device="cuda")), doing the host->device copy in safetensors' own C++
55
+ # (cudaMemcpy) — bypassing torch.Tensor.to, the call ZeroGPU patches to virtualise + pack
56
+ # weights at module scope. Result: "No CUDA GPUs are available" at startup, nothing packs.
57
+ # Patch it to open on CPU then move via torch.Tensor.to (ZeroGPU-virtualisable).
58
+ import safetensors as _safetensors
59
+ import ltx_core.loader.sft_loader as _sft
60
+ from ltx_core.loader.primitives import StateDict as _StateDict
61
+
62
+ def _zerogpu_safe_load(self, path, sd_ops, device=None):
63
+ device = device or torch.device("cpu")
64
+ sd, size, dtype = {}, 0, set()
65
+ model_paths = path if isinstance(path, list) else [path]
66
+ for shard_path in model_paths:
67
+ with _safetensors.safe_open(shard_path, framework="pt", device="cpu") as f:
68
+ for name in f.keys():
69
+ expected = name if sd_ops is None else sd_ops.apply_to_key(name)
70
+ if expected is None:
71
+ continue
72
+ value = f.get_tensor(name).to(device=device) # torch path -> ZeroGPU-virtualised
73
+ kvs = ((expected, value),)
74
+ if sd_ops is not None:
75
+ kvs = sd_ops.apply_to_key_value(expected, value)
76
+ for k, v in kvs:
77
+ size += v.nbytes
78
+ dtype.add(v.dtype)
79
+ sd[k] = v
80
+ return _StateDict(sd=sd, device=device, size=size, dtype=dtype)
81
+
82
+ _sft.SafetensorsStateDictLoader.load = _zerogpu_safe_load
83
+ print("[PATCH] safetensors loader -> CPU-open + torch.to (ZeroGPU-virtualisable)")
84
+ # --------------------------------------------------------------------------------------
85
+
86
+ # --- attention backend patch (FA3 crashes on Blackwell ZeroGPU; use xformers/SDPA) ---
87
+ import torch.nn.functional as F
88
+ from ltx_core.model.transformer import attention as _attn_mod
89
+
90
+ def _sdpa_as_mea(query, key, value, attn_bias=None, scale=None, **kwargs):
91
+ q, k, v = query.transpose(1, 2), key.transpose(1, 2), value.transpose(1, 2)
92
+ return F.scaled_dot_product_attention(q, k, v, scale=scale).transpose(1, 2)
93
+
94
+ # IMPORTANT (ZeroGPU): never query CUDA at module scope. SDPA works on every GPU (incl.
95
+ # Blackwell ZeroGPU, where FA3 crashes), so patch it unconditionally.
96
+ _attn_mod.memory_efficient_attention = _sdpa_as_mea
97
+ print("[ATTN] SDPA (patched at module scope, no CUDA query)")
98
+
99
+ logging.getLogger().setLevel(logging.INFO)
100
+
101
+ # =========================== PER-LORA CONFIG (colorize) ===========================
102
+ TITLE = "LTX-2.3 Ingredients (native LTX-2)"
103
  LORA_REPO = "Lightricks/LTX-2.3-22b-IC-LoRA-Ingredients"
104
  LORA_FILE = "ltx-2.3-22b-ic-lora-ingredients-0.9.safetensors"
105
  LORA_SCALE = 1.4
106
+ SKIP_STAGE_2 = True
107
+ GRAYSCALE_REF = False
108
+ RES_PRESETS = {"768×448": (768, 448), "960×544": (960, 544)}
109
+ DEFAULT_PRESET = "768×448"
110
+ FRAME_CHOICES = [49, 73, 97, 121]
111
+ DEFAULT_FRAMES = 121
112
+
113
+ def build_prompt(sheet, action):
114
+ return f"Reference sheet: {sheet.strip()}\n\nGenerated video: {action.strip()}"
115
+
116
+ EXAMPLES = [
117
+ ["examples/sheet_garden.png",
118
+ "a friendly cartoon hedgehog with rounded chestnut-brown fur and big eyes, and a grey-and-white rabbit with long ears; a green coiled garden hose and green spray bottles; the bright interior of a 'Greenfield Home & Garden' store with leafy plants",
119
+ "the hedgehog waddles up and says 'welcome to Greenfield!' while the rabbit hops past with a spray bottle; warm acoustic store jingle, cheerful voice and soft footsteps",
120
+ "768×448", 121, 42, False],
121
+ ["examples/sheet_hiker.png",
122
+ "a young asian woman with two long braids in an olive t-shirt and khaki pants; a large blue external-frame backpack; a thick wooden walking stick; a shaggy white yak with an ornate blue-red-yellow saddle blanket; snowy mountains and a small stone shrine",
123
+ "she sits on a rock beside the yak, pats its neck and says in a tired gentle voice 'we've got a long way to go... big guy'; wind, the yak's low grunt and jingling stirrups, no music",
124
+ "768×448", 121, 42, False],
125
+ ]
126
+ # =================================================================================
127
+
128
+ FPS = 24.0
129
  MAX_SEED = np.iinfo(np.int32).max
130
  HF_TOKEN = os.environ.get("HF_TOKEN")
131
+ LTX_MODEL_REPO = "Lightricks/LTX-2.3"
132
+ GEMMA_REPO = "google/gemma-3-12b-it-qat-q4_0-unquantized"
133
+
134
+
135
+ def _src_fps(path, default=FPS):
136
+ try:
137
+ return float(iio.immeta(path, plugin="pyav").get("fps", default)) or default
138
+ except Exception:
139
+ return default
140
+
141
+
142
+ def _prep_reference(path, width, height, num_frames):
143
+ """Resample to 24fps, aspect-fit/crop to WxH, NF frames; (optionally grayscale); write temp mp4."""
144
+ vid = iio.imread(path, plugin="pyav")
145
+ src_fps = _src_fps(path)
146
+ n = len(vid)
147
+ out = []
148
+ for i in range(num_frames):
149
+ idx = min(int(round(i / FPS * src_fps)), n - 1)
150
+ im = Image.fromarray(vid[idx]).convert("RGB")
151
+ im = ImageOps.fit(im, (width, height), Image.LANCZOS)
152
+ if GRAYSCALE_REF:
153
+ im = im.convert("L").convert("RGB")
154
+ out.append(np.array(im))
155
+ tmp = tempfile.mktemp(suffix=".mp4")
156
+ iio.imwrite(tmp, np.stack(out), fps=FPS, plugin="pyav", codec="libx264")
157
+ return tmp
158
+
159
+
160
+ def _pick_resolution(path, preset):
161
+ w, h = RES_PRESETS[preset]
162
+ try:
163
+ f0 = iio.imread(path, plugin="pyav", index=0)
164
+ if f0.shape[0] > f0.shape[1]: # portrait
165
+ w, h = h, w
166
+ except Exception:
167
+ pass
168
+ return w, h
169
+
170
+
171
+ # --- Load native pipeline + IC-LoRA once at module scope (ZeroGPU packs weights here) ---
172
+ print("Downloading checkpoints…")
173
+ checkpoint_path = hf_hub_download(LTX_MODEL_REPO, "ltx-2.3-22b-distilled-1.1.safetensors", token=HF_TOKEN)
174
+ spatial_upsampler_path = hf_hub_download(LTX_MODEL_REPO, "ltx-2.3-spatial-upscaler-x2-1.1.safetensors", token=HF_TOKEN)
175
+ gemma_root = snapshot_download(GEMMA_REPO, token=HF_TOKEN)
176
+ lora_path = hf_hub_download(LORA_REPO, LORA_FILE, token=HF_TOKEN)
177
+
178
+ print("Building ICLoraPipeline…")
179
+ pipeline = ICLoraPipeline(
180
+ distilled_checkpoint_path=checkpoint_path,
181
+ spatial_upsampler_path=spatial_upsampler_path,
182
+ gemma_root=gemma_root,
183
+ loras=[LoraPathStrengthAndSDOps(lora_path, LORA_SCALE, LTXV_LORA_COMFY_RENAMING_MAP)],
184
+ # bf16 (NOT fp8): the IC-LoRA is fused into the transformer at MODULE SCOPE (the GPU
185
+ # worker can't re-open the checkpoint file). fp8_cast()'s fusion runs a custom CUDA kernel
186
+ # that can't be ZeroGPU-virtualised; the bf16 fuse rule is pure torch -> virtualisable.
187
+ quantization=None,
188
+ )
189
 
 
190
 
191
+ def _preload_pin(ledger, tag):
192
+ if ledger is None:
193
+ return
194
+ for name in ["transformer", "video_encoder", "video_decoder", "audio_encoder",
195
+ "audio_decoder", "vocoder", "spatial_upsampler", "text_encoder",
196
+ "gemma_embeddings_processor"]:
197
+ fn = getattr(ledger, name, None)
198
+ if callable(fn):
199
+ try:
200
+ obj = fn()
201
+ setattr(ledger, name, (lambda o=obj: o))
202
+ print(f"[preload {tag}] {name} ✓")
203
+ except Exception as e:
204
+ print(f"[preload {tag}] {name} skipped: {e}")
205
 
206
+ # Preload stage 1 always; preload stage 2 only when two-stage is used (skip_stage_2=False).
207
+ # Eagerly pinning both ledgers materializes TWO ~46GB transformers — too big for the ZeroGPU pack.
208
+ _preload_pin(getattr(pipeline, "stage_1_model_ledger", None), "stage1")
209
+ if not SKIP_STAGE_2:
210
+ _preload_pin(getattr(pipeline, "stage_2_model_ledger", None), "stage2")
211
+ print("Pipeline ready.")
212
+
213
+
214
+ def _sheet_to_video(img, width, height, num_frames):
215
+ """Repeat the reference-sheet image into an NF-frame video for video_conditioning."""
216
+ im = img.convert("RGB").resize((width, height), Image.LANCZOS)
217
+ vid = np.stack([np.array(im)] * num_frames)
218
+ tmp = tempfile.mktemp(suffix=".mp4")
219
+ iio.imwrite(tmp, vid, fps=FPS, plugin="pyav", codec="libx264")
220
+ return tmp
221
 
222
 
223
  def _gallery_paths(gallery):
 
233
 
234
 
235
  def compose_sheet(gallery):
236
+ import math
237
  paths = _gallery_paths(gallery)
238
  if not paths:
239
  raise gr.Error("Upload at least one subject image to build a sheet.")
 
242
  return imgs[0]
243
  CW, CH = 1536, 896
244
  canvas = Image.new("RGB", (CW, CH), (0, 0, 0))
245
+ cols = math.ceil(math.sqrt(len(imgs))); rows = math.ceil(len(imgs) / cols); g = 16
246
+ cw = (CW - g * (cols + 1)) // cols; ch = (CH - g * (rows + 1)) // rows
 
 
 
 
247
  for i, im in enumerate(imgs):
248
  r, c = divmod(i, cols)
249
  canvas.paste(ImageOps.fit(im, (cw, ch), Image.LANCZOS), (g + c * (cw + g), g + r * (ch + g)))
250
  return canvas
251
 
252
 
 
 
 
 
 
 
 
 
 
 
 
253
  def _duration(*args, **kwargs):
254
+ nf = next((a for a in args if isinstance(a, int) and a in FRAME_CHOICES), DEFAULT_FRAMES)
255
+ return int(60 + nf * 1.2)
256
 
257
 
258
  @spaces.GPU(duration=_duration)
259
+ @torch.inference_mode()
260
+ def generate(sheet_image, sheet, action, seed, randomize, progress=gr.Progress(track_tqdm=True)):
261
  if sheet_image is None:
262
  raise gr.Error("Add a reference sheet (upload one, or build one from subject images in the other tab).")
263
  if not sheet.strip():
264
  raise gr.Error("Describe the elements in the reference sheet (characters, props, location).")
265
  if not action.strip():
266
  raise gr.Error("Describe the action / shot you want generated.")
267
+ seed = random.randint(0, MAX_SEED) if randomize else int(seed)
268
+ # Fixed generation geometry (matches the public Space). The native IC-LoRA is fused at a
269
+ # fixed strength (LORA_SCALE) at module scope.
270
+ width, height, num_frames = 768, 448, 121
271
+ ref_path = _sheet_to_video(sheet_image, width, height, num_frames)
272
+ tiling = TilingConfig.default()
273
+ gen_w, gen_h = (width * 2, height * 2) if SKIP_STAGE_2 else (width, height)
274
+ video_out, audio_out = pipeline(
275
+ prompt=build_prompt(sheet, action),
276
+ seed=seed, height=gen_h, width=gen_w,
277
+ num_frames=num_frames, frame_rate=FPS,
278
+ images=[], video_conditioning=[(ref_path, 1.0)],
279
+ skip_stage_2=SKIP_STAGE_2, tiling_config=tiling,
 
 
 
 
 
 
 
280
  )
281
+ out_path = tempfile.mktemp(suffix=".mp4")
282
+ encode_video(video=video_out, fps=FPS, audio=audio_out, output_path=out_path,
283
+ video_chunks_number=get_video_chunks_number(num_frames, tiling))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  return out_path, seed
285
 
286
 
 
288
  gr.Markdown(
289
  "# ⚡ LTX-2.3 Ingredients — Fast (Distilled)\n"
290
  "Reference-sheet control, fast. Upload a ready sheet, or build one from individual subject images. Using "
291
+ "[LTX 2.3 Distilled](https://huggingface.co/Lightricks/LTX-2.3) with the "
292
+ "[Ingredients IC-LoRA](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Ingredients). "
293
  "(For maximum fidelity, see the Dev demo.)"
294
  )
295
  with gr.Row():
 
307
  action = gr.Textbox(label="Generated video — action / shot, plus any speech & sounds", lines=3,
308
  placeholder="the woman walks down the alley, checks the pocket watch and whispers 'almost time'; footsteps on cobblestone, distant city hum")
309
  with gr.Accordion("Settings", open=False):
 
310
  randomize = gr.Checkbox(True, label="Randomize seed")
311
  seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Seed")
312
  run = gr.Button("Generate (fast)", variant="primary")
 
314
  video_out = gr.Video(label="Generated video")
315
 
316
  build_btn.click(compose_sheet, inputs=gallery, outputs=sheet_image)
317
+ run.click(generate, inputs=[sheet_image, sheet, action, seed, randomize], outputs=[video_out, seed])
318
 
319
  gr.Examples(
320
  examples=[
321
+ ['examples/sheet_garden.png', "a friendly cartoon hedgehog with rounded chestnut-brown fur, a cream face and belly, large expressive dark eyes, a small black nose and tiny rounded ears, shown in a face close-up and a full-body turnaround standing upright on two short legs; a cheerful cartoon rabbit with soft grey-and-white fur, long upright ears with pale pink inner lining, round amber eyes and a fluffy white tail, shown in a body turnaround; a green coiled garden hose neatly wound on a matching green wall-mounted reel; a row of green plastic spray bottles with trigger nozzles; the bright interior of a 'Greenfield Home & Garden' store with tall wooden shelves stocked with leafy potted plants, terracotta pots and gardening supplies, warm overhead lighting and a green-and-white storefront sign", "cheerful family-animation commercial scene, a lively medium shot inside the sunlit 'Greenfield Home & Garden' store with its tall shelves of leafy potted plants and terracotta pots. the rounded chestnut-brown hedgehog waddles briskly up toward the camera on its short legs, its cream belly bouncing, then stops, lifts a tiny paw in a friendly wave and beams with wide sparkling eyes, announcing in a warm, bright, sing-song voice: 'welcome to Greenfield!'. behind it the soft grey-and-white rabbit hops past in the aisle, long ears bobbing, cradling a green spray bottle against its chest; it pauses, gives the bottle a playful little squeeze that puffs a fine mist into a shaft of light, and adds in a chirpy, slightly higher voice: 'everything your garden needs!'. the hedgehog nods enthusiastically, gestures with both paws toward the laden shelves and finishes with a cosy chuckle: 'come on in!'. the animation is glossy and expressive with squash-and-stretch motion, rounded shapes and saturated greens; the camera pushes in gently and tilts up to reveal the green-and-white storefront sign. the audio is bright and immersive: the hedgehog's cheerful voice, the rabbit's lighter reply, the soft puff of the spray bottle, light pattering footsteps on the store floor and a warm, upbeat acoustic-ukulele jingle playing softly underneath", 42, False],
322
+ ['examples/sheet_hiker.png', 'a young Asian woman with a warm skin tone, dark hair parted down the middle in two long braids resting on her chest, an olive-green short-sleeved t-shirt, khaki cargo pants, dark brown hiking boots and a black wristwatch on her left arm, with a serious natural expression; a large heavy-duty blue hiking backpack with an external silver metal frame, multiple side and top pouches, black adjustable straps and a brown leather square patch near the bottom; a simple thick natural wooden walking stick with rough bark texture and a slight fork near one end; a large sturdy yak with long shaggy white-and-blonde hair and curved grey horns, wearing an ornate saddle blanket with intricate blue, red and yellow patterns, a saddle with metal stirrups and colorful tassels near its ears and chest; a sweeping majestic mountain landscape where a dirt path winds through green rocky slopes toward towering snow-capped peaks under a bright blue sky with scattered white clouds; a small traditional square stone shrine with a flat slightly tiered roof and a bright yellow fabric valance along the roofline, bright blue window trim and a red wooden door, with a small stone stupa beside it', "cinematic adventure documentary scene, a dynamic medium wide shot of the young asian woman with her dark hair in two long braids, wearing an olive-green t-shirt and khaki cargo pants. she sits on a rock along a mountain dirt path, resting beside the massive white shaggy yak with curved horns and its ornate blue, red and yellow patterned saddle blanket with metal stirrups. leaning against a nearby small stone building with blue window trim, a red door and a yellow fabric roof valance are her large blue external-frame backpack and thick wooden walking stick; majestic snow-capped mountains tower in the distant background under a bright blue sky. she looks at the yak, chest heaving slightly from exertion, and says with a breathy, tired but gentle voice: 'we've got a long way to go...'. she pauses, extending her hand to gently pat the thick white fur on the yak's neck; the yak shifts its weight, the colorful tassels near its ears swaying, and a faint exhausted smile breaks across her face as she continues softly: '...big guy.'. lowering her hand she grabs her wooden walking stick, leaning her weight onto it as she turns her gaze up toward the distant snowy peaks, her expression shifting from exhaustion to quiet determination as she adds, her voice growing firmer: 'but the pass...', then takes a deep grounding breath: '...is just over that ridge.'. the camera is dynamically handheld, slowly orbiting the woman and the yak to reveal the depth of the valley and the towering mountains behind them; naturalistic breathtaking film aesthetic, bright crisp sunlight casting sharp shadows across the rocky path and stone shrine. clear immersive audio: her wind-swept voice, the heavy rhythmic breathing of the yak, the faint jingle of metal stirrups and the distant ambient howl of mountain winds, no background music", 42, False],
323
+ ['examples/subj_composite.png', 'a smiling young woman with warm fair skin and shoulder-length curly dark-brown hair, soft brown eyes and a gentle open expression, wearing a cream cable-knit sweater and dark jeans, shown in a relaxed three-quarter pose; a dappled grey horse with a dark charcoal mane and tail, a soft mottled grey-and-white coat, dark intelligent eyes and a calm posture, wearing a simple brown leather halter; a green misty mountain meadow of tall dewy grass and scattered wildflowers, with faint pine-covered slopes dissolving into low morning fog under a pale silver sky', "tender naturalistic cinematic scene, a soft medium shot in a green misty mountain meadow at dawn, tall dewy grass glistening and low fog drifting between faint pine slopes. the young woman with curly dark-brown hair and a cream cable-knit sweater walks slowly up to the dappled grey horse, her breath faintly visible in the cool air, and raises a careful open hand. she gently strokes the horse's soft mottled neck, her face softening into a warm reassuring smile, and murmurs in a low, soothing, slightly breathy voice: 'good boy… easy now'. the horse lowers its head toward her, its charcoal mane shifting, flicks an ear and lets out a soft snort, its breath misting in the cold; she leans her forehead lightly against its cheek, closes her eyes for a moment, then whispers with quiet affection: 'there you go'. the camera drifts slowly in a gentle arc around the pair, the shallow-focus background of fog and wildflowers blurring softly behind them; the film aesthetic is delicate and breathtaking with cool silver dawn light and fine atmospheric haze. the audio is intimate and immersive: her gentle hushed voice, the horse's soft nicker and snort, the swish of dewy grass, light birdsong waking in the distance and a faint cool breeze, with no background music", 42, False],
 
 
 
 
 
 
 
 
 
324
  ],
325
+ inputs=[sheet_image, sheet, action, seed, randomize],
326
  outputs=[video_out, seed], fn=generate, cache_examples=True, cache_mode="lazy",
327
  )
328
 
requirements.txt CHANGED
@@ -1,9 +1,11 @@
1
- git+https://github.com/huggingface/diffusers
2
- transformers
3
  accelerate
4
- peft
5
- safetensors
6
- sentencepiece
7
- imageio
8
- imageio-ffmpeg
9
  av
 
 
 
 
 
1
+ transformers==4.57.6
 
2
  accelerate
3
+ torch==2.8.0
4
+ torchaudio==2.8.0
5
+ einops
6
+ scipy
 
7
  av
8
+ scikit-image>=0.25.2
9
+ flashpack==0.1.2
10
+ imageio[ffmpeg]
11
+ pillow