multimodalart HF Staff commited on
Commit
7decf28
·
verified ·
1 Parent(s): f6aab21

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/ti2v_frame.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,29 @@
1
  ---
2
- title: Lingbot Video Dense
3
- emoji: 📉
4
- colorFrom: green
5
  colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.20.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: LingBot-Video Dense 1.3B
3
+ emoji: 🎬
4
+ colorFrom: red
5
  colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.20.0
 
8
  app_file: app.py
9
+ license: apache-2.0
10
+ short_description: Dense 1.3B embodied video generation — T2V, I2V, T2I
11
+ models:
12
+ - robbyant/lingbot-video-dense-1.3b
13
+ python_version: "3.12"
14
+ startup_duration_timeout: 1h
15
  ---
16
 
17
+ # LingBot-Video Dense 1.3B ZeroGPU demo
18
+
19
+ Text-to-video, image-to-video and text-to-image with
20
+ [LingBot-Video Dense 1.3B](https://huggingface.co/robbyant/lingbot-video-dense-1.3b),
21
+ a lightweight dense video generation model for embodied intelligence (1.3B parameters).
22
+
23
+ This demo runs the dense DiT with:
24
+
25
+ - bf16 transformer + text encoder (Qwen3-VL), fp32 Wan VAE
26
+ - 480p resolution buckets for video, up to 5 s at 24 fps (plus 720p/1080p text-to-image)
27
+ - batched CFG with SDPA attention, TF32
28
+ - automatic prompt enhancement: the official two-step rewriter recipe (expand → JSON map) runs
29
+ on the official rewriter base model (Qwen3.6-27B) via HF Inference Providers
app.py ADDED
@@ -0,0 +1,771 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ os.environ.setdefault("HF_HOME", "/tmp/hf_home")
4
+ os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules")
5
+ os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
6
+ # LingBot-Video runtime knobs (read at import / first forward).
7
+ os.environ.setdefault("DIFFUSERS_ATTN_BACKEND", "_native_flash")
8
+
9
+ import spaces # noqa: E402 — must be imported before torch
10
+
11
+ import base64
12
+ import io
13
+ import json
14
+ import random
15
+ import re
16
+ import time
17
+ import urllib.request
18
+ from pathlib import Path
19
+
20
+ import gradio as gr
21
+ import numpy as np
22
+ import torch
23
+ from PIL import Image
24
+ from diffusers import AutoencoderKLWan
25
+ from diffusers.utils import export_to_video
26
+ from huggingface_hub import snapshot_download
27
+ from transformers import AutoProcessor, Qwen3VLForConditionalGeneration
28
+
29
+ from lingbot_video import (
30
+ FlowUniPCMultistepScheduler,
31
+ LingBotVideoImageToVideoPipeline,
32
+ LingBotVideoPipeline,
33
+ LingBotVideoTransformer3DModel,
34
+ )
35
+ from lingbot_video.pipeline_lingbot_video import (
36
+ DEFAULT_NEGATIVE_PROMPT,
37
+ DEFAULT_NEGATIVE_PROMPT_IMAGE,
38
+ )
39
+ from lingbot_video.utils import num_frames_from_duration
40
+
41
+ from rewriter_prompts import (
42
+ IMAGE_STEP1_EXPAND,
43
+ IMAGE_STEP2_MAP,
44
+ VIDEO_STEP1_EXPAND,
45
+ VIDEO_STEP2_MAP,
46
+ )
47
+
48
+ torch.backends.cuda.matmul.allow_tf32 = True
49
+ torch.set_float32_matmul_precision("high")
50
+
51
+ MODEL_ID = "robbyant/lingbot-video-dense-1.3b"
52
+ FPS = 24
53
+ MAX_SEED = 2**31 - 1
54
+ MAX_GPU_SECONDS = 180
55
+ REWRITER_MODEL = os.environ.get("REWRITER_MODEL", "Qwen/Qwen3.6-27B:deepinfra")
56
+
57
+ # (height, width), multiples of 16 — official 480p buckets.
58
+ VIDEO_SIZES = {
59
+ "832 × 480 (16:9)": (480, 832),
60
+ "480 × 832 (9:16)": (832, 480),
61
+ "640 × 480 (4:3)": (480, 640),
62
+ "480 × 480 (1:1)": (480, 480),
63
+ }
64
+ IMAGE_SIZES = {
65
+ "832 × 480 (16:9)": (480, 832),
66
+ "480 × 832 (9:16)": (832, 480),
67
+ "480 × 480 (1:1)": (480, 480),
68
+ "1280 × 736 (16:9, 720p)": (736, 1280),
69
+ "736 × 1280 (9:16, 720p)": (1280, 736),
70
+ "1088 × 1088 (1:1, 1080p)": (1088, 1088),
71
+ }
72
+
73
+ EXAMPLES_DIR = Path(__file__).parent / "examples"
74
+
75
+
76
+ def _read_example(name: str) -> str:
77
+ return (EXAMPLES_DIR / name).read_text(encoding="utf-8").strip()
78
+
79
+
80
+ print(f"[startup] downloading {MODEL_ID} ...", flush=True)
81
+ t0 = time.perf_counter()
82
+ model_dir = snapshot_download(
83
+ MODEL_ID,
84
+ ignore_patterns=["*.bak_lingbot_video_diffusers"],
85
+ )
86
+ print(f"[startup] snapshot ready in {time.perf_counter() - t0:.1f}s", flush=True)
87
+
88
+ t0 = time.perf_counter()
89
+ transformer = LingBotVideoTransformer3DModel.from_pretrained(
90
+ model_dir, subfolder="transformer", torch_dtype=torch.bfloat16
91
+ )
92
+ text_encoder = Qwen3VLForConditionalGeneration.from_pretrained(
93
+ model_dir, subfolder="text_encoder", dtype=torch.bfloat16, attn_implementation="sdpa"
94
+ )
95
+ processor = AutoProcessor.from_pretrained(model_dir, subfolder="processor")
96
+ vae = AutoencoderKLWan.from_pretrained(model_dir, subfolder="vae", torch_dtype=torch.float32)
97
+ print(f"[startup] components loaded in {time.perf_counter() - t0:.1f}s", flush=True)
98
+
99
+ t0 = time.perf_counter()
100
+ pipe = LingBotVideoPipeline(
101
+ transformer=transformer,
102
+ vae=vae,
103
+ text_encoder=text_encoder,
104
+ processor=processor,
105
+ scheduler=FlowUniPCMultistepScheduler.from_pretrained(model_dir, subfolder="scheduler"),
106
+ ).to("cuda")
107
+ pipe_i2v = LingBotVideoImageToVideoPipeline(
108
+ transformer=transformer,
109
+ vae=vae,
110
+ text_encoder=text_encoder,
111
+ processor=processor,
112
+ scheduler=FlowUniPCMultistepScheduler.from_pretrained(model_dir, subfolder="scheduler"),
113
+ )
114
+ print(f"[startup] pipelines on cuda in {time.perf_counter() - t0:.1f}s", flush=True)
115
+
116
+
117
+ def _resolve_seed(seed: float, randomize: bool) -> int:
118
+ if randomize:
119
+ return random.randint(0, MAX_SEED)
120
+ return int(seed)
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # Prompt enhancement — the official LingBot rewriter recipe (expand -> JSON map)
125
+ # run on the official rewriter base model (Qwen3.6-27B) via HF Inference
126
+ # Providers. The DiT was trained on these structured JSON captions; plain
127
+ # prompts are far out of distribution and produce severe artifacts.
128
+ # ---------------------------------------------------------------------------
129
+
130
+
131
+ def _router_chat(text: str, max_tokens: int, image: Image.Image | None = None) -> str:
132
+ token = os.environ.get("HF_TOKEN")
133
+ if not token:
134
+ raise RuntimeError("HF_TOKEN is not configured for prompt enhancement.")
135
+ if image is not None:
136
+ image = image.convert("RGB")
137
+ image.thumbnail((768, 768))
138
+ buf = io.BytesIO()
139
+ image.save(buf, format="JPEG", quality=90)
140
+ data_uri = "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
141
+ content = [
142
+ {"type": "image_url", "image_url": {"url": data_uri}},
143
+ {"type": "text", "text": text},
144
+ ]
145
+ else:
146
+ content = text
147
+ body = {
148
+ "model": REWRITER_MODEL,
149
+ "messages": [{"role": "user", "content": content}],
150
+ "max_tokens": max_tokens,
151
+ "temperature": 0.0,
152
+ "chat_template_kwargs": {"enable_thinking": False},
153
+ }
154
+ last_error = None
155
+ for attempt in range(2):
156
+ try:
157
+ req = urllib.request.Request(
158
+ "https://router.huggingface.co/v1/chat/completions",
159
+ data=json.dumps(body).encode(),
160
+ headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
161
+ )
162
+ with urllib.request.urlopen(req, timeout=180) as resp:
163
+ data = json.load(resp)
164
+ out = (data["choices"][0]["message"].get("content") or "").strip()
165
+ if out:
166
+ return out
167
+ last_error = RuntimeError("empty rewriter response")
168
+ except Exception as exc: # noqa: BLE001
169
+ last_error = exc
170
+ if image is not None:
171
+ # provider may not accept image input — retry text-only
172
+ body["messages"][0]["content"] = text
173
+ image = None
174
+ raise RuntimeError(f"prompt enhancement failed: {last_error}")
175
+
176
+
177
+ def _extract_caption_json(raw: str) -> dict:
178
+ match = re.search(r"```(?:json)?\s*(\{.*\})\s*```", raw, re.DOTALL)
179
+ s = match.group(1) if match else raw
180
+ start = s.find("{")
181
+ if start < 0:
182
+ raise ValueError("no JSON object in rewriter output")
183
+ s = s[start:]
184
+ try:
185
+ return json.loads(s)
186
+ except json.JSONDecodeError:
187
+ from json_repair import repair_json
188
+
189
+ obj = repair_json(s, return_objects=True)
190
+ if not isinstance(obj, dict):
191
+ raise ValueError("rewriter output is not a JSON object")
192
+ return obj
193
+
194
+
195
+ def enhance_prompt(prompt: str, mode: str, duration_s: float | None, image: Image.Image | None = None) -> str:
196
+ """Two-stage official rewrite: plain prompt -> detailed prose -> JSON caption string."""
197
+ if mode == "t2i":
198
+ step1 = IMAGE_STEP1_EXPAND + "\n\nUser image prompt:\n" + prompt
199
+ else:
200
+ step1 = VIDEO_STEP1_EXPAND + "\n\n" + prompt + f"\n\nVideo Duration: {duration_s:g} seconds"
201
+ prose = _router_chat(step1, 1200, image=image)
202
+ if mode == "t2i":
203
+ step2 = IMAGE_STEP2_MAP + "\n\nDETAILED CAPTION:\n" + prose
204
+ else:
205
+ step2 = (
206
+ VIDEO_STEP2_MAP
207
+ + f"\n\nVideo Duration: {duration_s:g} seconds\n\nDETAILED CAPTION:\n"
208
+ + prose
209
+ + "\n\nOutput the JSON now."
210
+ )
211
+ caption = _extract_caption_json(_router_chat(step2, 6000, image=image))
212
+ return json.dumps(caption, ensure_ascii=False, separators=(",", ":"))
213
+
214
+
215
+ def _prepare_caption(prompt: str, mode: str, duration_s: float | None, enhance: bool,
216
+ image: Image.Image | None = None) -> str:
217
+ prompt = (prompt or "").strip()
218
+ if not prompt:
219
+ raise gr.Error("Please enter a prompt.")
220
+ if prompt.startswith("{") or not enhance:
221
+ return prompt # already a structured JSON caption, or enhancement disabled
222
+ t0 = time.perf_counter()
223
+ try:
224
+ caption = enhance_prompt(prompt, mode, duration_s, image=image)
225
+ except Exception as exc: # noqa: BLE001
226
+ print(f"[rewriter] failed: {exc}", flush=True)
227
+ raise gr.Error(
228
+ "Prompt enhancement failed (the model needs structured captions to work well). "
229
+ "Please try again, or paste a LingBot JSON caption and disable enhancement."
230
+ )
231
+ print(f"[rewriter] {mode} enhanced in {time.perf_counter() - t0:.1f}s ({len(caption)} chars)", flush=True)
232
+ return caption
233
+
234
+
235
+ def _latent_tokens(height: int, width: int, num_frames: int) -> int:
236
+ latent_frames = (num_frames - 1) // 4 + 1
237
+ return latent_frames * (height // 16) * (width // 16)
238
+
239
+
240
+ def _step_seconds(height: int, width: int, num_frames: int) -> float:
241
+ # Per-step cost (CFG included), calibrated relative to 81f @ 480x832.
242
+ # Dense 1.3B is ~3x faster than MoE 30B-A3B per the paper.
243
+ r = _latent_tokens(height, width, num_frames) / 32760.0
244
+ return 2.3 * r * r + 0.45 * r + 0.15
245
+
246
+
247
+ def _estimate_video_seconds(size_label: str, duration_s: float, steps: int, sizes=VIDEO_SIZES) -> int:
248
+ height, width = sizes[size_label]
249
+ num_frames = num_frames_from_duration(duration_s, FPS)
250
+ return int(20.0 + steps * _step_seconds(height, width, num_frames))
251
+
252
+
253
+ def _estimate_image_seconds(size_label: str, steps: int) -> int:
254
+ height, width = IMAGE_SIZES[size_label]
255
+ return int(10.0 + steps * _step_seconds(height, width, 1))
256
+
257
+
258
+ def _check_budget(estimate: int) -> None:
259
+ if estimate > MAX_GPU_SECONDS:
260
+ raise gr.Error(
261
+ f"These settings need ~{estimate}s of GPU time (max {MAX_GPU_SECONDS}s). "
262
+ "Reduce the video duration, steps, or resolution."
263
+ )
264
+
265
+
266
+ def _export_video(frames: np.ndarray) -> str:
267
+ path = f"/tmp/lingbot_{int(time.time() * 1000)}.mp4"
268
+ export_to_video(frames, path, fps=FPS)
269
+ return path
270
+
271
+
272
+ def _video_gpu_duration(caption, height, width, num_frames, steps, *args, **kwargs) -> int:
273
+ return min(MAX_GPU_SECONDS, int(20.0 + int(steps) * _step_seconds(height, width, num_frames)) + 20)
274
+
275
+
276
+ @spaces.GPU(duration=_video_gpu_duration)
277
+ def _gpu_generate_video(caption, height, width, num_frames, steps, guidance_scale, shift,
278
+ negative_prompt, seed, image=None):
279
+ target = pipe_i2v if image is not None else pipe
280
+ kwargs = {"image": image} if image is not None else {}
281
+ t0 = time.perf_counter()
282
+ output = target(
283
+ prompt=caption,
284
+ negative_prompt=negative_prompt.strip() or DEFAULT_NEGATIVE_PROMPT,
285
+ height=height,
286
+ width=width,
287
+ num_frames=num_frames,
288
+ num_inference_steps=int(steps),
289
+ guidance_scale=float(guidance_scale),
290
+ shift=float(shift),
291
+ batch_cfg=True,
292
+ generator=torch.Generator().manual_seed(seed),
293
+ **kwargs,
294
+ )
295
+ torch.cuda.synchronize()
296
+ print(
297
+ f"[timing] {'i2v' if image is not None else 't2v'} {width}x{height} f={num_frames} "
298
+ f"steps={int(steps)} tokens={_latent_tokens(height, width, num_frames)} "
299
+ f"took {time.perf_counter() - t0:.1f}s",
300
+ flush=True,
301
+ )
302
+ return _export_video(output.frames[0])
303
+
304
+
305
+ def _image_gpu_duration(caption, height, width, steps, *args, **kwargs) -> int:
306
+ return min(MAX_GPU_SECONDS, int(10.0 + int(steps) * _step_seconds(height, width, 1)) + 15)
307
+
308
+
309
+ @spaces.GPU(duration=_image_gpu_duration)
310
+ def _gpu_generate_image(caption, height, width, steps, guidance_scale, shift, negative_prompt, seed):
311
+ t0 = time.perf_counter()
312
+ output = pipe(
313
+ prompt=caption,
314
+ negative_prompt=negative_prompt.strip() or DEFAULT_NEGATIVE_PROMPT_IMAGE,
315
+ height=height,
316
+ width=width,
317
+ num_frames=1,
318
+ num_inference_steps=int(steps),
319
+ guidance_scale=float(guidance_scale),
320
+ shift=float(shift),
321
+ batch_cfg=True,
322
+ generator=torch.Generator().manual_seed(seed),
323
+ )
324
+ torch.cuda.synchronize()
325
+ print(f"[timing] t2i {width}x{height} steps={int(steps)} "
326
+ f"took {time.perf_counter() - t0:.1f}s", flush=True)
327
+ frame = output.frames[0][0]
328
+ return Image.fromarray((np.clip(frame, 0, 1) * 255).astype(np.uint8))
329
+
330
+
331
+ def generate_t2v(
332
+ prompt: str,
333
+ size_label: str = "832 × 480 (16:9)",
334
+ duration_s: float = 2.0,
335
+ steps: int = 30,
336
+ guidance_scale: float = 3.0,
337
+ shift: float = 3.0,
338
+ negative_prompt: str = "",
339
+ seed: int = 42,
340
+ randomize_seed: bool = True,
341
+ enhance: bool = True,
342
+ progress=gr.Progress(track_tqdm=True),
343
+ ):
344
+ """Generate a short video from a text prompt with LingBot-Video Dense 1.3B.
345
+
346
+ Args:
347
+ prompt: Scene description in natural language (it is auto-expanded into the
348
+ structured caption the model expects), or a raw LingBot JSON caption.
349
+ size_label: Resolution/aspect preset, e.g. "832 × 480 (16:9)", "480 × 832 (9:16)".
350
+ duration_s: Video length in seconds (1.0-5.0) at 24 fps.
351
+ steps: Number of denoising steps (more = more detail, slower).
352
+ guidance_scale: Classifier-free guidance strength.
353
+ shift: Flow-matching timestep shift.
354
+ negative_prompt: What to avoid; empty uses the model default.
355
+ seed: Random seed for reproducibility.
356
+ randomize_seed: If true, ignore seed and use a random one.
357
+ enhance: If true, expand a plain prompt into a structured caption before generation.
358
+
359
+ Returns:
360
+ The generated MP4 video, the seed used, and the structured caption fed to the model.
361
+ """
362
+ estimate = _estimate_video_seconds(size_label, duration_s, int(steps))
363
+ _check_budget(estimate)
364
+ height, width = VIDEO_SIZES[size_label]
365
+ num_frames = num_frames_from_duration(duration_s, FPS)
366
+ seed = _resolve_seed(seed, randomize_seed)
367
+ caption = _prepare_caption(prompt, "t2v", duration_s, enhance)
368
+ video = _gpu_generate_video(
369
+ caption, height, width, num_frames, steps, guidance_scale, shift, negative_prompt, seed,
370
+ )
371
+ return video, seed, caption
372
+
373
+
374
+ def generate_i2v(
375
+ image,
376
+ prompt: str,
377
+ size_label: str = "832 × 480 (16:9)",
378
+ duration_s: float = 2.0,
379
+ steps: int = 30,
380
+ guidance_scale: float = 3.0,
381
+ shift: float = 3.0,
382
+ negative_prompt: str = "",
383
+ seed: int = 42,
384
+ randomize_seed: bool = True,
385
+ enhance: bool = True,
386
+ progress=gr.Progress(track_tqdm=True),
387
+ ):
388
+ """Animate a first-frame image into a short video with LingBot-Video Dense 1.3B.
389
+
390
+ Args:
391
+ image: The first frame to animate (filepath or PIL image).
392
+ prompt: How the scene should evolve, in natural language (auto-expanded), or a raw LingBot JSON caption.
393
+ size_label: Resolution/aspect preset, e.g. "832 × 480 (16:9)".
394
+ duration_s: Video length in seconds (1.0-5.0) at 24 fps.
395
+ steps: Number of denoising steps.
396
+ guidance_scale: Classifier-free guidance strength.
397
+ shift: Flow-matching timestep shift.
398
+ negative_prompt: What to avoid; empty uses the model default.
399
+ seed: Random seed for reproducibility.
400
+ randomize_seed: If true, ignore seed and use a random one.
401
+ enhance: If true, expand a plain prompt into a structured caption before generation.
402
+
403
+ Returns:
404
+ The generated MP4 video, the seed used, and the structured caption fed to the model.
405
+ """
406
+ if image is None:
407
+ raise gr.Error("Please upload a first-frame image.")
408
+ estimate = _estimate_video_seconds(size_label, duration_s, int(steps)) + 15
409
+ _check_budget(estimate)
410
+ height, width = VIDEO_SIZES[size_label]
411
+ num_frames = num_frames_from_duration(duration_s, FPS)
412
+ seed = _resolve_seed(seed, randomize_seed)
413
+ caption = _prepare_caption(prompt, "ti2v", duration_s, enhance, image=image)
414
+ video = _gpu_generate_video(
415
+ caption, height, width, num_frames, steps, guidance_scale, shift, negative_prompt, seed,
416
+ image=image,
417
+ )
418
+ return video, seed, caption
419
+
420
+
421
+ def generate_t2i(
422
+ prompt: str,
423
+ size_label: str = "832 × 480 (16:9)",
424
+ steps: int = 30,
425
+ guidance_scale: float = 3.0,
426
+ shift: float = 3.0,
427
+ negative_prompt: str = "",
428
+ seed: int = 42,
429
+ randomize_seed: bool = True,
430
+ enhance: bool = True,
431
+ progress=gr.Progress(track_tqdm=True),
432
+ ):
433
+ """Generate an image from a text prompt with LingBot-Video Dense 1.3B.
434
+
435
+ Args:
436
+ prompt: Image description in natural language (auto-expanded), or a raw LingBot JSON caption.
437
+ size_label: Resolution/aspect preset, e.g. "832 × 480 (16:9)", "1088 × 1088 (1:1, 1080p)".
438
+ steps: Number of denoising steps.
439
+ guidance_scale: Classifier-free guidance strength.
440
+ shift: Flow-matching timestep shift.
441
+ negative_prompt: What to avoid; empty uses the model default.
442
+ seed: Random seed for reproducibility.
443
+ randomize_seed: If true, ignore seed and use a random one.
444
+ enhance: If true, expand a plain prompt into a structured caption before generation.
445
+
446
+ Returns:
447
+ The generated image, the seed used, and the structured caption fed to the model.
448
+ """
449
+ height, width = IMAGE_SIZES[size_label]
450
+ seed = _resolve_seed(seed, randomize_seed)
451
+ caption = _prepare_caption(prompt, "t2i", None, enhance)
452
+ image = _gpu_generate_image(
453
+ caption, height, width, steps, guidance_scale, shift, negative_prompt, seed,
454
+ )
455
+ return image, seed, caption
456
+
457
+
458
+ HEADER_HTML = """
459
+ <div id="lb-header">
460
+ <div class="lb-top">
461
+ <div class="lb-title">
462
+ LingBot-Video
463
+ <span class="lb-badge">Dense&nbsp;1.3B</span>
464
+ </div>
465
+ <nav class="lb-links">
466
+ <a href="https://huggingface.co/robbyant/lingbot-video-dense-1.3b" target="_blank" rel="noopener">Model</a>
467
+ <a href="https://github.com/Robbyant/lingbot-video" target="_blank" rel="noopener">GitHub</a>
468
+ <a href="https://technology.robbyant.com/lingbot-video" target="_blank" rel="noopener">Project</a>
469
+ </nav>
470
+ </div>
471
+ <p class="lb-sub">
472
+ Embodied-intelligence video generation from a lightweight dense model — text-to-video, image-to-video &amp; text-to-image.
473
+ Prompts are auto-expanded into structured JSON captions
474
+ (<a href="https://huggingface.co/Qwen/Qwen3.6-27B" target="_blank" rel="noopener">Qwen3.6-27B</a>);
475
+ paste a raw LingBot caption to skip that. 480p video, ZeroGPU.
476
+ </p>
477
+ </div>
478
+ """
479
+
480
+ CSS = """
481
+ #col-container { max-width: 1100px; margin: 0 auto; }
482
+ .dark .gradio-container { color: var(--body-text-color); }
483
+ /* Zero out Gradio's wrapper padding around the header HTML block. */
484
+ #lb-header-wrap, #lb-header-wrap .html-container {
485
+ padding: 0 !important;
486
+ border: none !important;
487
+ background: transparent !important;
488
+ }
489
+ /* Borderless header, flush with the app container. */
490
+ #lb-header {
491
+ margin: 0;
492
+ padding: 2px 0 8px 0;
493
+ }
494
+ #lb-header .lb-top {
495
+ display: flex;
496
+ align-items: center;
497
+ justify-content: flex-start;
498
+ flex-wrap: wrap;
499
+ gap: 6px 18px;
500
+ }
501
+ #lb-header .lb-title {
502
+ font-size: 1.35rem;
503
+ font-weight: 700;
504
+ line-height: 1.2;
505
+ color: var(--body-text-color);
506
+ display: flex;
507
+ align-items: center;
508
+ gap: 10px;
509
+ }
510
+ #lb-header .lb-badge {
511
+ font-size: 0.66rem;
512
+ font-weight: 600;
513
+ letter-spacing: 0.02em;
514
+ text-transform: uppercase;
515
+ color: var(--body-text-color-subdued);
516
+ background: var(--background-fill-primary);
517
+ border: 1px solid var(--border-color-primary);
518
+ border-radius: 999px;
519
+ padding: 2px 9px;
520
+ white-space: nowrap;
521
+ }
522
+ #lb-header .lb-links {
523
+ display: flex;
524
+ gap: 14px;
525
+ font-size: 0.85rem;
526
+ padding-left: 4px;
527
+ }
528
+ #lb-header .lb-links a {
529
+ color: var(--body-text-color);
530
+ opacity: 0.85;
531
+ text-decoration: none;
532
+ border-bottom: 1px solid transparent;
533
+ }
534
+ #lb-header .lb-links a:hover {
535
+ color: var(--link-text-color);
536
+ border-bottom-color: currentColor;
537
+ }
538
+ #lb-header .lb-sub {
539
+ margin: 7px 0 0 0;
540
+ font-size: 0.82rem;
541
+ line-height: 1.45;
542
+ color: var(--body-text-color);
543
+ opacity: 0.78;
544
+ }
545
+ #lb-header .lb-sub a { color: var(--link-text-color); text-decoration: none; }
546
+ #lb-header .lb-sub a:hover { text-decoration: underline; }
547
+ /* Gradio's theme pads <a> with 0 8px, which shows up as fake space inside the
548
+ "(Qwen3.6-27B)" parentheses — strip it on all header links. */
549
+ #lb-header a { padding: 0 !important; }
550
+
551
+ /* Compact, tidy examples: no wide horizontal overflow, subtle scrollbar */
552
+ .lb-examples .gr-samples-table td, .lb-examples table td { white-space: normal; }
553
+ .lb-examples { scrollbar-width: thin; }
554
+ .lb-examples ::-webkit-scrollbar { height: 8px; width: 8px; }
555
+ .lb-examples ::-webkit-scrollbar-thumb {
556
+ background: var(--border-color-primary);
557
+ border-radius: 999px;
558
+ }
559
+ .lb-examples ::-webkit-scrollbar-track { background: transparent; }
560
+ """
561
+
562
+
563
+ def _seed_row():
564
+ with gr.Row():
565
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=42)
566
+ randomize = gr.Checkbox(label="Randomize seed", value=True)
567
+ return seed, randomize
568
+
569
+
570
+ def _advanced(negative_default: str, steps_value: int):
571
+ with gr.Accordion("Advanced settings", open=False):
572
+ steps = gr.Slider(label="Inference steps", minimum=4, maximum=50, step=1, value=steps_value)
573
+ guidance = gr.Slider(label="Guidance scale", minimum=1.0, maximum=10.0, step=0.5, value=3.0)
574
+ shift = gr.Slider(label="Flow shift", minimum=1.0, maximum=9.0, step=0.5, value=3.0)
575
+ negative = gr.Textbox(label="Negative prompt", value=negative_default, lines=3)
576
+ seed, randomize = _seed_row()
577
+ return steps, guidance, shift, negative, seed, randomize
578
+
579
+
580
+ # Example runners: examples carry only the natural-language prompt (and first
581
+ # frame for I2V) — enhancement is on and every other control keeps its default,
582
+ # so the examples table stays to one or two clean columns instead of ten.
583
+ def _example_t2v(prompt):
584
+ return generate_t2v(prompt, "832 × 480 (16:9)", 2.0, 30, 3.0, 3.0,
585
+ DEFAULT_NEGATIVE_PROMPT, 42, False, True)
586
+
587
+
588
+ def _example_i2v(image, prompt):
589
+ return generate_i2v(image, prompt, "832 × 480 (16:9)", 2.0, 30, 3.0, 3.0,
590
+ DEFAULT_NEGATIVE_PROMPT, 42, False, True)
591
+
592
+
593
+ def _example_t2i(prompt):
594
+ return generate_t2i(prompt, "832 × 480 (16:9)", 30, 3.0, 3.0,
595
+ DEFAULT_NEGATIVE_PROMPT_IMAGE, 42, False, True)
596
+
597
+
598
+ # Pre-built example prompts derived from the official LingBot-Video example cases.
599
+ # For t2v/t2i, we use a short natural-language prompt that the rewriter expands.
600
+ # For i2v, we pass the first-frame image and a natural-language motion description.
601
+ T2V_EXAMPLES = [
602
+ "A young woman with long, wavy brown hair stands in a bright modern apartment living room, "
603
+ "wearing an oversized cream knit cardigan over a white tank top with beige trousers. She "
604
+ "smiles at the camera, shifts her weight, and adjusts her collar, showcasing her outfit.",
605
+ "A young child with short brown hair plays outdoors on a sunny day, blowing shimmering soap "
606
+ "bubbles with a wand. The bubbles float upwards, catching the warm afternoon light against a "
607
+ "soft-focus green background.",
608
+ "A robotic arm on a workbench reaches forward, grasps a black game controller, lifts it, "
609
+ "moves it to the right, and lowers it into an open box. Another robotic arm and headphones "
610
+ "remain stationary on the desk, top-down view.",
611
+ ]
612
+ T2V_LABELS = ["👗 Woman in apartment", "🫧 Child blowing bubbles", "🤖 Robot arm sorting"]
613
+
614
+ I2V_EXAMPLES = [
615
+ [str(EXAMPLES_DIR / "ti2v_frame.png"),
616
+ "A fit young man and a sleek white humanoid robot run side by side along a cherry blossom "
617
+ "lined promenade toward the camera, which tracks backward. Energetic, futuristic, daylight."],
618
+ ]
619
+ I2V_LABELS = ["🏃 Man and robot running"]
620
+
621
+ T2I_EXAMPLES = [
622
+ "A clear glass bottle of water on a sunlit wooden table acts as a lens, refracting bright "
623
+ "sunlight into a warm glow, extreme close-up, photorealistic.",
624
+ "A humanoid robot chef flipping a pancake in a bright modern kitchen, dramatic side lighting, "
625
+ "steam rising from the pan, shallow depth of field, 85mm lens.",
626
+ ]
627
+ T2I_LABELS = ["🔆 Bottle as a lens", "🤖 Robot chef"]
628
+
629
+
630
+ with gr.Blocks(title="LingBot-Video Dense 1.3B", theme=gr.themes.Citrus(), css=CSS) as demo:
631
+ gr.HTML(HEADER_HTML, elem_id="lb-header-wrap")
632
+
633
+ with gr.Tab("Text → Video"):
634
+ with gr.Row():
635
+ with gr.Column():
636
+ t2v_prompt = gr.Textbox(
637
+ label="Prompt",
638
+ placeholder="Describe the scene in detail (plain text or a LingBot JSON caption)...",
639
+ lines=5,
640
+ max_lines=12,
641
+ )
642
+ with gr.Row():
643
+ t2v_size = gr.Dropdown(
644
+ label="Resolution", choices=list(VIDEO_SIZES), value="832 × 480 (16:9)"
645
+ )
646
+ t2v_dur = gr.Slider(
647
+ label="Video duration (s)", minimum=1.0, maximum=5.0, step=0.5, value=2.0
648
+ )
649
+ t2v_enhance = gr.Checkbox(
650
+ label="Enhance prompt (official rewriter — required for plain prompts)", value=True
651
+ )
652
+ t2v_steps, t2v_guidance, t2v_shift, t2v_negative, t2v_seed, t2v_rand = _advanced(
653
+ DEFAULT_NEGATIVE_PROMPT, 30
654
+ )
655
+ t2v_btn = gr.Button("Generate video", variant="primary")
656
+ with gr.Column():
657
+ t2v_out = gr.Video(label="Generated video", autoplay=True)
658
+ t2v_seed_out = gr.Number(label="Seed used", interactive=False)
659
+ with gr.Accordion("Structured caption used", open=False):
660
+ t2v_caption_out = gr.Textbox(label="Caption", lines=4)
661
+
662
+ t2v_inputs = [
663
+ t2v_prompt, t2v_size, t2v_dur, t2v_steps, t2v_guidance, t2v_shift,
664
+ t2v_negative, t2v_seed, t2v_rand, t2v_enhance,
665
+ ]
666
+ t2v_outputs = [t2v_out, t2v_seed_out, t2v_caption_out]
667
+ t2v_btn.click(generate_t2v, inputs=t2v_inputs, outputs=t2v_outputs,
668
+ concurrency_id="gpu", concurrency_limit=1)
669
+ with gr.Column(elem_classes="lb-examples"):
670
+ gr.Examples(
671
+ examples=T2V_EXAMPLES,
672
+ example_labels=T2V_LABELS,
673
+ fn=_example_t2v,
674
+ inputs=[t2v_prompt],
675
+ outputs=t2v_outputs,
676
+ cache_examples=True,
677
+ cache_mode="lazy",
678
+ )
679
+
680
+ with gr.Tab("Image → Video"):
681
+ with gr.Row():
682
+ with gr.Column():
683
+ i2v_image = gr.Image(label="First frame", type="pil")
684
+ i2v_prompt = gr.Textbox(
685
+ label="Prompt",
686
+ placeholder="Describe how the scene should evolve...",
687
+ lines=4,
688
+ max_lines=12,
689
+ )
690
+ with gr.Row():
691
+ i2v_size = gr.Dropdown(
692
+ label="Resolution", choices=list(VIDEO_SIZES), value="832 × 480 (16:9)"
693
+ )
694
+ i2v_dur = gr.Slider(
695
+ label="Video duration (s)", minimum=1.0, maximum=5.0, step=0.5, value=2.0
696
+ )
697
+ i2v_enhance = gr.Checkbox(
698
+ label="Enhance prompt (official rewriter — required for plain prompts)", value=True
699
+ )
700
+ i2v_steps, i2v_guidance, i2v_shift, i2v_negative, i2v_seed, i2v_rand = _advanced(
701
+ DEFAULT_NEGATIVE_PROMPT, 30
702
+ )
703
+ i2v_btn = gr.Button("Generate video", variant="primary")
704
+ with gr.Column():
705
+ i2v_out = gr.Video(label="Generated video", autoplay=True)
706
+ i2v_seed_out = gr.Number(label="Seed used", interactive=False)
707
+ with gr.Accordion("Structured caption used", open=False):
708
+ i2v_caption_out = gr.Textbox(label="Caption", lines=4)
709
+
710
+ i2v_inputs = [
711
+ i2v_image, i2v_prompt, i2v_size, i2v_dur, i2v_steps, i2v_guidance, i2v_shift,
712
+ i2v_negative, i2v_seed, i2v_rand, i2v_enhance,
713
+ ]
714
+ i2v_outputs = [i2v_out, i2v_seed_out, i2v_caption_out]
715
+ i2v_btn.click(generate_i2v, inputs=i2v_inputs, outputs=i2v_outputs,
716
+ concurrency_id="gpu", concurrency_limit=1)
717
+ with gr.Column(elem_classes="lb-examples"):
718
+ gr.Examples(
719
+ examples=I2V_EXAMPLES,
720
+ example_labels=I2V_LABELS,
721
+ fn=_example_i2v,
722
+ inputs=[i2v_image, i2v_prompt],
723
+ outputs=i2v_outputs,
724
+ cache_examples=True,
725
+ cache_mode="lazy",
726
+ )
727
+
728
+ with gr.Tab("Text → Image"):
729
+ with gr.Row():
730
+ with gr.Column():
731
+ t2i_prompt = gr.Textbox(
732
+ label="Prompt",
733
+ placeholder="Describe the image in detail...",
734
+ lines=5,
735
+ max_lines=12,
736
+ )
737
+ t2i_size = gr.Dropdown(
738
+ label="Resolution", choices=list(IMAGE_SIZES), value="832 × 480 (16:9)"
739
+ )
740
+ t2i_enhance = gr.Checkbox(
741
+ label="Enhance prompt (official rewriter — required for plain prompts)", value=True
742
+ )
743
+ t2i_steps, t2i_guidance, t2i_shift, t2i_negative, t2i_seed, t2i_rand = _advanced(
744
+ DEFAULT_NEGATIVE_PROMPT_IMAGE, 30
745
+ )
746
+ t2i_btn = gr.Button("Generate image", variant="primary")
747
+ with gr.Column():
748
+ t2i_out = gr.Image(label="Generated image")
749
+ t2i_seed_out = gr.Number(label="Seed used", interactive=False)
750
+ with gr.Accordion("Structured caption used", open=False):
751
+ t2i_caption_out = gr.Textbox(label="Caption", lines=4)
752
+
753
+ t2i_inputs = [
754
+ t2i_prompt, t2i_size, t2i_steps, t2i_guidance, t2i_shift,
755
+ t2i_negative, t2i_seed, t2i_rand, t2i_enhance,
756
+ ]
757
+ t2i_outputs = [t2i_out, t2i_seed_out, t2i_caption_out]
758
+ t2i_btn.click(generate_t2i, inputs=t2i_inputs, outputs=t2i_outputs,
759
+ concurrency_id="gpu", concurrency_limit=1)
760
+ with gr.Column(elem_classes="lb-examples"):
761
+ gr.Examples(
762
+ examples=T2I_EXAMPLES,
763
+ example_labels=T2I_LABELS,
764
+ fn=_example_t2i,
765
+ inputs=[t2i_prompt],
766
+ outputs=t2i_outputs,
767
+ cache_examples=True,
768
+ cache_mode="lazy",
769
+ )
770
+
771
+ demo.queue(max_size=30).launch(css=CSS, mcp_server=True)
examples/t2v_bubbles.json ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "caption": {
3
+ "comprehensive_description": {
4
+ "scene_content_description": "A young child with short brown hair is outdoors on a bright, sunny day, playing with bubbles. The child is wearing a colorful striped shirt with horizontal bands of red, yellow, green, and blue. They are holding a small bottle of bubble solution and a wand, dipping the wand into the bottle and then blowing to create a stream of shimmering, iridescent bubbles. The background is a soft-focus outdoor setting with greenery and a hint of a building, creating a warm and joyful atmosphere. The lighting is bright and natural, highlighting the child's focused expression and the delicate, translucent nature of the bubbles.",
5
+ "camera_movement_description": "The camera is essentially stationary throughout the video, maintaining a medium close-up shot of the child from an eye-level angle. There is a very slight handheld tremor, but no intentional panning, tilting, or zooming occurs."
6
+ },
7
+ "camera_info": {
8
+ "color": "Saturated",
9
+ "frame_size": "Medium Close Up",
10
+ "shot_type_angle": "Low angle",
11
+ "lens_size": "Long Lens",
12
+ "composition": "Center",
13
+ "lighting": "Hard light",
14
+ "lighting_type": "Daylight"
15
+ },
16
+ "world_knowledge": [],
17
+ "prominent_elements": [
18
+ {
19
+ "name": "young child",
20
+ "description": "A young child with short brown hair and a joyful expression, focused on playing with bubbles.",
21
+ "actions": [
22
+ {
23
+ "timestamp": "[0.0s - 1.5s]",
24
+ "action": "Dips the bubble wand into the solution bottle and lifts it out."
25
+ },
26
+ {
27
+ "timestamp": "[1.5s - 2.5s]",
28
+ "action": "Brings the wand to their lips and blows to create bubbles."
29
+ },
30
+ {
31
+ "timestamp": "[2.5s - 5.0s]",
32
+ "action": "Watches the bubbles float away, smiling slightly."
33
+ }
34
+ ],
35
+ "location": "Center of the frame",
36
+ "relative_size": "dominant",
37
+ "shape_and_color": "Human form; wearing a multi-colored striped shirt (red, yellow, green, blue).",
38
+ "texture": "Soft skin, fine hair, fabric texture of the shirt.",
39
+ "appearance_details": "Short brown hair, bright eyes, colorful horizontal stripes on the shirt.",
40
+ "relationship": "Holding the bubble wand and bottle, interacting with the bubbles.",
41
+ "orientation": "Facing forward and slightly to the right.",
42
+ "pose": "Standing or sitting upright, arms raised to hold the bubble wand.",
43
+ "expression": "Focused and happy.",
44
+ "clothing": "A short-sleeved shirt with horizontal stripes in red, yellow, green, and blue.",
45
+ "gender": "Male",
46
+ "skin_tone_and_texture": "Fair skin with a smooth texture."
47
+ },
48
+ {
49
+ "name": "bubble wand and bottle",
50
+ "description": "A small plastic bottle containing bubble solution and a wand with a circular loop.",
51
+ "actions": [
52
+ {
53
+ "timestamp": "[0.0s - 1.5s]",
54
+ "action": "The wand is dipped into the bottle and then lifted out."
55
+ },
56
+ {
57
+ "timestamp": "[1.5s - 2.5s]",
58
+ "action": "The wand is held at the child's mouth as bubbles are blown."
59
+ }
60
+ ],
61
+ "location": "Lower center of the frame, held by the child.",
62
+ "relative_size": "small",
63
+ "shape_and_color": "Cylindrical bottle with a blue cap; circular wand loop.",
64
+ "texture": "Smooth plastic.",
65
+ "appearance_details": "The bottle is partially filled with clear liquid; the wand has a thin handle.",
66
+ "relationship": "Held by the child's hands.",
67
+ "orientation": "Vertical bottle, horizontal wand loop.",
68
+ "pose": "",
69
+ "expression": "",
70
+ "clothing": "",
71
+ "gender": "",
72
+ "skin_tone_and_texture": ""
73
+ },
74
+ {
75
+ "name": "bubbles",
76
+ "description": "A cluster of small, iridescent, translucent bubbles floating in the air.",
77
+ "actions": [
78
+ {
79
+ "timestamp": "[1.5s - 5.0s]",
80
+ "action": "The bubbles are blown from the wand and float upwards and towards the right side of the frame."
81
+ }
82
+ ],
83
+ "location": "Upper center and right side of the frame.",
84
+ "relative_size": "medium",
85
+ "shape_and_color": "Spherical and iridescent with rainbow-like reflections.",
86
+ "texture": "Glossy and translucent.",
87
+ "appearance_details": "Shimmering surfaces that catch the sunlight.",
88
+ "relationship": "Created by the child's blowing action.",
89
+ "orientation": "Floating in various directions.",
90
+ "pose": "",
91
+ "expression": "",
92
+ "clothing": "",
93
+ "gender": "",
94
+ "skin_tone_and_texture": "",
95
+ "is_cluster": true,
96
+ "number_of_objects": "many"
97
+ }
98
+ ]
99
+ },
100
+ "duration": 5
101
+ }
examples/t2v_robot.json ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "caption": {
3
+ "comprehensive_description": {
4
+ "scene_content_description": "The video presents a first-person perspective of a workspace, likely a desk, viewed from a fixed, slightly elevated angle. The environment is well-lit with neutral, even lighting, creating a clear and focused atmosphere. The desk surface is a light grey color. On the left side of the desk, there is a black game controller with a glowing blue light bar, resting on a black mousepad. In the center of the desk lies a pair of black over-ear headphones. To the right, there is an open, empty black box with a white interior. In the foreground, two robotic arms are visible. The left robotic arm, featuring a black and silver body with a two-fingered gripper, remains completely stationary throughout the video. The right robotic arm, similar in design, is the active subject. It begins by moving forward and to the left, positioning its gripper over the game controller. It then grasps the controller, lifts it off the desk, and transports it to the right, moving it over the open box. Finally, the right robotic arm lowers the controller into the box and releases it. The headphones and the left robotic arm remain undisturbed during this entire sequence.",
5
+ "camera_movement_description": ""
6
+ },
7
+ "camera_info": {
8
+ "color": "Cyan",
9
+ "frame_size": "Wide",
10
+ "shot_type_angle": "High angle",
11
+ "lens_size": "Ultra Wide / Fisheye",
12
+ "composition": "Balanced",
13
+ "lighting": "Soft light",
14
+ "lighting_type": "Artificial light"
15
+ },
16
+ "world_knowledge": [],
17
+ "prominent_elements": [
18
+ {
19
+ "name": "right robotic arm",
20
+ "description": "A mechanical arm with a black and silver body, equipped with a two-fingered gripper and visible wiring.",
21
+ "actions": [
22
+ {
23
+ "timestamp": "[0.0s - 1.5s]",
24
+ "action": "Moves forward and to the left towards the game controller."
25
+ },
26
+ {
27
+ "timestamp": "[1.5s - 2.5s]",
28
+ "action": "Grasps the game controller and lifts it upward."
29
+ },
30
+ {
31
+ "timestamp": "[2.5s - 4.0s]",
32
+ "action": "Moves to the right, carrying the game controller over the open box."
33
+ },
34
+ {
35
+ "timestamp": "[4.0s - 5.0s]",
36
+ "action": "Lowers the game controller into the box and releases it."
37
+ }
38
+ ],
39
+ "location": "Originates from the bottom right, moves to the center, then to the right.",
40
+ "relative_size": "large",
41
+ "shape_and_color": "Cylindrical and angular segments, black and silver.",
42
+ "texture": "Metallic and matte plastic.",
43
+ "appearance_details": "Visible joints, wiring, and a two-fingered gripper mechanism.",
44
+ "relationship": "Interacts directly with the game controller.",
45
+ "orientation": "Extends forward and slightly upward from the bottom right.",
46
+ "pose": "",
47
+ "expression": "",
48
+ "clothing": "",
49
+ "is_cluster": false,
50
+ "number_of_objects": ""
51
+ },
52
+ {
53
+ "name": "left robotic arm",
54
+ "description": "A mechanical arm with a black and silver body, equipped with a two-fingered gripper.",
55
+ "actions": [
56
+ {
57
+ "timestamp": "[0.0s - 5.0s]",
58
+ "action": "Remains stationary."
59
+ }
60
+ ],
61
+ "location": "Bottom left corner of the frame.",
62
+ "relative_size": "large",
63
+ "shape_and_color": "Cylindrical and angular segments, black and silver.",
64
+ "texture": "Metallic and matte plastic.",
65
+ "appearance_details": "Visible joints and a two-fingered gripper mechanism.",
66
+ "relationship": "Positioned opposite the right robotic arm, not interacting with other objects.",
67
+ "orientation": "Extends forward and slightly upward from the bottom left.",
68
+ "pose": "",
69
+ "expression": "",
70
+ "clothing": "",
71
+ "is_cluster": false,
72
+ "number_of_objects": ""
73
+ },
74
+ {
75
+ "name": "game controller",
76
+ "description": "A standard video game controller with joysticks, buttons, and a glowing light bar.",
77
+ "actions": [
78
+ {
79
+ "timestamp": "[0.0s - 1.5s]",
80
+ "action": "Rests stationary on the desk."
81
+ },
82
+ {
83
+ "timestamp": "[1.5s - 2.5s]",
84
+ "action": "Is grasped and lifted upward by the right robotic arm."
85
+ },
86
+ {
87
+ "timestamp": "[2.5s - 4.0s]",
88
+ "action": "Is moved to the right by the right robotic arm."
89
+ },
90
+ {
91
+ "timestamp": "[4.0s - 5.0s]",
92
+ "action": "Is lowered into the open box and released."
93
+ }
94
+ ],
95
+ "location": "Initially on the left side of the desk, moved to the right side inside the box.",
96
+ "relative_size": "medium",
97
+ "shape_and_color": "Contoured shape, black with a blue light bar.",
98
+ "texture": "Matte plastic.",
99
+ "appearance_details": "Two joysticks, directional pad, action buttons, and a glowing blue light bar in the center.",
100
+ "relationship": "Initially on the desk, then grasped and moved by the right robotic arm, finally placed in the open box.",
101
+ "orientation": "Horizontal on the desk, then tilted while being carried.",
102
+ "pose": "",
103
+ "expression": "",
104
+ "clothing": "",
105
+ "is_cluster": false,
106
+ "number_of_objects": ""
107
+ },
108
+ {
109
+ "name": "headphones",
110
+ "description": "A pair of over-ear headphones with a headband and ear cups.",
111
+ "actions": [
112
+ {
113
+ "timestamp": "[0.0s - 5.0s]",
114
+ "action": "Remains stationary on the desk."
115
+ }
116
+ ],
117
+ "location": "Center of the desk.",
118
+ "relative_size": "medium",
119
+ "shape_and_color": "Curved headband with circular ear cups, black.",
120
+ "texture": "Matte plastic and soft ear cushions.",
121
+ "appearance_details": "Visible ear cushions and a headband.",
122
+ "relationship": "Rests on the desk, untouched by the robotic arms.",
123
+ "orientation": "Lying flat on the desk.",
124
+ "pose": "",
125
+ "expression": "",
126
+ "clothing": "",
127
+ "is_cluster": false,
128
+ "number_of_objects": ""
129
+ },
130
+ {
131
+ "name": "open box",
132
+ "description": "A rectangular box with the lid open, revealing a white interior.",
133
+ "actions": [
134
+ {
135
+ "timestamp": "[0.0s - 5.0s]",
136
+ "action": "Remains stationary on the desk."
137
+ }
138
+ ],
139
+ "location": "Right side of the desk.",
140
+ "relative_size": "medium",
141
+ "shape_and_color": "Rectangular, black exterior with a white interior.",
142
+ "texture": "Smooth cardboard or plastic.",
143
+ "appearance_details": "Open lid, empty interior.",
144
+ "relationship": "Serves as the receptacle for the game controller.",
145
+ "orientation": "Horizontal on the desk.",
146
+ "pose": "",
147
+ "expression": "",
148
+ "clothing": "",
149
+ "is_cluster": false,
150
+ "number_of_objects": ""
151
+ }
152
+ ]
153
+ },
154
+ "duration": 5
155
+ }
examples/t2v_woman.json ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "caption": {
3
+ "comprehensive_description": {
4
+ "scene_content_description": "A young woman with long, wavy brown hair is standing in a bright, modern apartment living room. She is wearing a stylish, oversized cream-colored knit cardigan over a white tank top, paired with high-waisted, wide-leg beige trousers. She holds a small, structured tan leather handbag in her left hand. The background features a neutral-toned interior with a beige sofa, a potted plant, and large windows that let in soft, natural light, creating a warm and inviting atmosphere. The woman is smiling and looking directly at the camera, showcasing her outfit with a confident and friendly demeanor.",
5
+ "camera_movement_description": "The camera is positioned at eye level and remains essentially stationary throughout the video, maintaining a medium shot that captures the subject from the waist up. There is a very shallow depth of field, keeping the woman in sharp focus while the background remains softly blurred."
6
+ },
7
+ "camera_info": {
8
+ "color": "Warm",
9
+ "frame_size": "Medium",
10
+ "shot_type_angle": "Eye level",
11
+ "lens_size": "Medium",
12
+ "composition": "Center",
13
+ "lighting": "Soft light",
14
+ "lighting_type": "Daylight"
15
+ },
16
+ "world_knowledge": [],
17
+ "prominent_elements": [
18
+ {
19
+ "name": "young woman",
20
+ "description": "A woman with long, wavy brown hair and a friendly expression, modeling a fashion outfit.",
21
+ "actions": [
22
+ {
23
+ "timestamp": "[0.0s - 0.5s]",
24
+ "action": "stands still, smiling at the camera"
25
+ },
26
+ {
27
+ "timestamp": "[0.5s - 2.0s]",
28
+ "action": "shifts her weight and turns her body slightly to the right"
29
+ },
30
+ {
31
+ "timestamp": "[2.0s - 3.5s]",
32
+ "action": "adjusts the collar of her cardigan with her right hand"
33
+ },
34
+ {
35
+ "timestamp": "[3.5s - 5.0s]",
36
+ "action": "returns to a neutral pose, smiling at the camera"
37
+ }
38
+ ],
39
+ "location": "center of the frame",
40
+ "relative_size": "dominant",
41
+ "shape_and_color": "slender build; wearing cream, white, and beige",
42
+ "texture": "soft knit cardigan, smooth fabric trousers",
43
+ "appearance_details": "long wavy brown hair, gold hoop earrings, tan leather handbag",
44
+ "relationship": "the main subject of the video, standing in front of a blurred apartment background",
45
+ "orientation": "upright, facing the camera",
46
+ "pose": "standing, shifting weight, and adjusting clothing",
47
+ "expression": "smiling and confident",
48
+ "clothing": "oversized cream knit cardigan, white tank top, high-waisted wide-leg beige trousers",
49
+ "gender": "female",
50
+ "skin_tone_and_texture": "fair skin with a smooth texture"
51
+ },
52
+ {
53
+ "name": "tan handbag",
54
+ "description": "A small, structured leather handbag with a top handle.",
55
+ "actions": [
56
+ {
57
+ "timestamp": "[0.0s - 5.0s]",
58
+ "action": "held steady in the woman's left hand"
59
+ }
60
+ ],
61
+ "location": "held in the woman's left hand, lower center of the frame",
62
+ "relative_size": "small",
63
+ "shape_and_color": "rectangular, tan or light brown",
64
+ "texture": "smooth leather",
65
+ "appearance_details": "structured shape with a top handle",
66
+ "relationship": "held by the woman as an accessory",
67
+ "orientation": "upright",
68
+ "pose": "",
69
+ "expression": "",
70
+ "clothing": "",
71
+ "gender": "",
72
+ "skin_tone_and_texture": ""
73
+ }
74
+ ]
75
+ },
76
+ "duration": 5
77
+ }
examples/ti2v_frame.png ADDED

Git LFS Details

  • SHA256: 9b0a5052c67e320120831a01ffe7c9e991365b5e39f751261cc8e550a9e1dd04
  • Pointer size: 132 Bytes
  • Size of remote file: 2.45 MB
examples/ti2v_robot.json ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "caption": {
3
+ "comprehensive_description": {
4
+ "scene_content_description": "A dynamic, high-fidelity sequence of a fit young man and a sleek white humanoid robot running side-by-side along a paved promenade lined with blooming cherry blossom trees. The man is dressed in black athletic wear, while the robot features a glossy white chassis with black mechanical joints. They are running towards the camera, which tracks backward to maintain their position in the frame. The background features a blurred cityscape and a bridge over a river, creating a sense of depth and urban vitality. The atmosphere is energetic and futuristic, blending organic nature with advanced technology.",
5
+ "camera_movement_description": "The camera executes a smooth, continuous backward tracking shot, moving parallel to the subjects' forward motion to keep them centered in the frame. There is a subtle, rhythmic vertical bobbing motion synchronized with the cadence of the runners' footsteps, adding a visceral sense of speed and physical exertion. The camera maintains a steady focus on the two runners, allowing the background elements to exhibit motion blur."
6
+ },
7
+ "camera_info": {
8
+ "color": "Natural",
9
+ "frame_size": "Wide",
10
+ "shot_type_angle": "Eye level",
11
+ "lens_size": "Telephoto",
12
+ "composition": "Symmetrical",
13
+ "lighting": "Bright sunlight",
14
+ "lighting_type": "Daylight"
15
+ },
16
+ "world_knowledge": [],
17
+ "prominent_elements": [
18
+ {
19
+ "name": "fit young man",
20
+ "description": "A muscular male with short dark hair, wearing a tight black t-shirt and black running shorts.",
21
+ "actions": [
22
+ {
23
+ "timestamp": "[0.0s - 5.0s]",
24
+ "action": "He runs steadily towards the camera, his arms pumping rhythmically at his sides and his legs cycling with powerful strides, maintaining a consistent pace."
25
+ }
26
+ ],
27
+ "location": "Left side of the frame (viewer's perspective)",
28
+ "relative_size": "large",
29
+ "shape_and_color": "Athletic build, dark clothing contrasting with the bright surroundings.",
30
+ "texture": "Realistic skin and fabric",
31
+ "appearance_details": "White running shoes, focused and determined facial expression.",
32
+ "relationship": "The primary human subject",
33
+ "orientation": "Facing forward",
34
+ "pose": "Running posture, arms bent at the elbows",
35
+ "expression": "Focused and determined",
36
+ "clothing": "Black t-shirt, black shorts, white sneakers",
37
+ "gender": "Male",
38
+ "skin_tone_and_texture": "Tanned, muscular"
39
+ },
40
+ {
41
+ "name": "humanoid robot",
42
+ "description": "A sleek, anthropomorphic machine with a glossy white exterior and exposed black mechanical joints at the shoulders, elbows, knees, and ankles.",
43
+ "actions": [
44
+ {
45
+ "timestamp": "[0.0s - 5.0s]",
46
+ "action": "It runs with mechanical precision, its limbs moving in a fluid, lifelike gait that perfectly matches the man's stride, its arms swinging in coordination with its legs."
47
+ }
48
+ ],
49
+ "location": "Right side of the frame (viewer's perspective)",
50
+ "relative_size": "large",
51
+ "shape_and_color": "Angular and futuristic, white with black accents.",
52
+ "texture": "Smooth, reflective metal and matte composite materials",
53
+ "appearance_details": "A smooth, featureless black visor for a face; the label '07' is visible on its chest plate.",
54
+ "relationship": "The companion subject running alongside the man",
55
+ "orientation": "Facing forward",
56
+ "pose": "Running posture, mirroring the human",
57
+ "expression": "",
58
+ "clothing": "",
59
+ "gender": "",
60
+ "skin_tone_and_texture": ""
61
+ },
62
+ {
63
+ "name": "cherry blossom trees",
64
+ "description": "A dense row of mature trees with dark trunks and canopies full of vibrant pink flowers.",
65
+ "actions": [
66
+ {
67
+ "timestamp": "[0.0s - 5.0s]",
68
+ "action": "The trees appear to rush past the camera due to the backward tracking motion, with the pink blossoms blurring slightly to emphasize speed."
69
+ }
70
+ ],
71
+ "location": "Background, lining both sides of the path",
72
+ "relative_size": "large",
73
+ "shape_and_color": "Organic and sprawling, with dark brown bark and bright pink blossoms.",
74
+ "texture": "Rough bark and delicate petals",
75
+ "appearance_details": "Hanging branches that frame the top of the shot.",
76
+ "relationship": "The environmental setting",
77
+ "orientation": "Receding into the distance",
78
+ "pose": "",
79
+ "expression": "",
80
+ "clothing": "",
81
+ "gender": "",
82
+ "skin_tone_and_texture": ""
83
+ },
84
+ {
85
+ "name": "paved promenade",
86
+ "description": "A wide, brick-paved walkway stretching from the foreground into the distance.",
87
+ "actions": [
88
+ {
89
+ "timestamp": "[0.0s - 5.0s]",
90
+ "action": "The ground rushes towards the bottom of the frame, providing a strong sense of forward momentum."
91
+ }
92
+ ],
93
+ "location": "Lower center of the frame",
94
+ "relative_size": "large",
95
+ "shape_and_color": "Rectangular and linear, composed of reddish-brown bricks.",
96
+ "texture": "Rough and uneven",
97
+ "appearance_details": "Distinct pattern of the paving stones.",
98
+ "relationship": "The ground surface for the runners",
99
+ "orientation": "Leading the eye towards the horizon",
100
+ "pose": "",
101
+ "expression": "",
102
+ "clothing": "",
103
+ "gender": "",
104
+ "skin_tone_and_texture": ""
105
+ },
106
+ {
107
+ "name": "urban background",
108
+ "description": "A blurred cityscape featuring tall buildings and a steel bridge spanning a body of water.",
109
+ "actions": [
110
+ {
111
+ "timestamp": "[0.0s - 5.0s]",
112
+ "action": "The background remains relatively static but shifts slightly due to the camera's lateral movement, remaining out of focus to keep attention on the runners."
113
+ }
114
+ ],
115
+ "location": "Far background, visible through the gaps in the trees and to the right",
116
+ "relative_size": "medium",
117
+ "shape_and_color": "Geometric and muted, with grey, blue, and beige tones.",
118
+ "texture": "Indistinct due to depth of field",
119
+ "appearance_details": "Silhouette of skyscrapers and the truss structure of the bridge.",
120
+ "relationship": "The distant context",
121
+ "orientation": "Static in the distance",
122
+ "pose": "",
123
+ "expression": "",
124
+ "clothing": "",
125
+ "gender": "",
126
+ "skin_tone_and_texture": ""
127
+ }
128
+ ]
129
+ },
130
+ "duration": 5
131
+ }
lingbot_video/__init__.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ from typing import Any
5
+
6
+
7
+ _EXPORTS = {
8
+ "FlowUniPCMultistepScheduler": (
9
+ "lingbot_video.scheduling_flow_unipc",
10
+ "FlowUniPCMultistepScheduler",
11
+ ),
12
+ "LingBotVideoImageToVideoPipeline": (
13
+ "lingbot_video.pipeline_lingbot_video_i2v",
14
+ "LingBotVideoImageToVideoPipeline",
15
+ ),
16
+ "LingBotVideoPipeline": (
17
+ "lingbot_video.pipeline_lingbot_video",
18
+ "LingBotVideoPipeline",
19
+ ),
20
+ "LingBotVideoTransformer3DModel": (
21
+ "lingbot_video.transformer_lingbot_video",
22
+ "LingBotVideoTransformer3DModel",
23
+ ),
24
+ }
25
+
26
+ __all__ = sorted(_EXPORTS)
27
+
28
+
29
+ def __getattr__(name: str) -> Any:
30
+ try:
31
+ module_name, attr_name = _EXPORTS[name]
32
+ except KeyError as exc:
33
+ raise AttributeError(name) from exc
34
+ value = getattr(importlib.import_module(module_name), attr_name)
35
+ globals()[name] = value
36
+ return value
lingbot_video/default_negative_prompt.json ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "universal_negative": {
3
+ "visual_quality": [
4
+ "low quality",
5
+ "worst quality",
6
+ "blurry",
7
+ "pixelated",
8
+ "jpeg artifacts",
9
+ "low resolution",
10
+ "unstable color",
11
+ "color flicker",
12
+ "underexposed",
13
+ "overexposed",
14
+ "invisible subject",
15
+ "subject hidden in darkness"
16
+ ],
17
+ "artistic_style": [
18
+ "painting",
19
+ "illustration",
20
+ "drawing",
21
+ "cartoon",
22
+ "3d render",
23
+ "cgi",
24
+ "sketch",
25
+ "digital art"
26
+ ],
27
+ "composition_and_content": [
28
+ "text",
29
+ "watermark",
30
+ "signature",
31
+ "logo",
32
+ "subtitles",
33
+ "pillarboxed",
34
+ "side bars",
35
+ "portrait image in landscape frame"
36
+ ],
37
+ "temporal_and_motion_stability": [
38
+ "flickering",
39
+ "jittery",
40
+ "motion blur",
41
+ "temporal inconsistency",
42
+ "warping",
43
+ "morphing",
44
+ "incoherent motion",
45
+ "unnatural movement",
46
+ "static object with sudden jump",
47
+ "frame-to-frame inconsistency"
48
+ ],
49
+ "material_and_structure": [
50
+ "plastic-like glass",
51
+ "unrealistic texture",
52
+ "deformed bottle",
53
+ "liquid freezing improperly",
54
+ "distorted reflections"
55
+ ]
56
+ }
57
+ }
lingbot_video/default_negative_prompt_image.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "universal_negative": {
3
+ "visual_quality": [
4
+ "low quality",
5
+ "worst quality",
6
+ "blurry",
7
+ "pixelated",
8
+ "jpeg artifacts",
9
+ "low resolution",
10
+ "underexposed",
11
+ "overexposed",
12
+ "invisible subject",
13
+ "subject hidden in darkness"
14
+ ],
15
+ "artistic_style": [
16
+ "painting",
17
+ "illustration",
18
+ "drawing",
19
+ "cartoon",
20
+ "3d render",
21
+ "cgi",
22
+ "sketch",
23
+ "digital art"
24
+ ],
25
+ "composition_and_content": [
26
+ "text",
27
+ "watermark",
28
+ "signature",
29
+ "logo",
30
+ "pillarboxed",
31
+ "side bars",
32
+ "portrait image in landscape frame"
33
+ ],
34
+ "material_and_structure": [
35
+ "plastic-like glass",
36
+ "unrealistic texture",
37
+ "deformed bottle",
38
+ "distorted reflections"
39
+ ]
40
+ }
41
+ }
lingbot_video/fsdp_inference.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import gc
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ import torch
8
+ import torch.distributed as dist
9
+ from torch.distributed.device_mesh import DeviceMesh, init_device_mesh
10
+
11
+ try:
12
+ from torch.distributed._composable.fsdp import fully_shard
13
+ except Exception as exc: # pragma: no cover - depends on the installed torch build
14
+ fully_shard = None
15
+ FSDP_IMPORT_ERROR = exc
16
+ else:
17
+ FSDP_IMPORT_ERROR = None
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class FSDPInferenceInfo:
22
+ enabled: bool
23
+ world_size: int
24
+ wrapped_blocks: int
25
+ ignored_params: int
26
+
27
+
28
+ def init_fsdp_inference_mesh() -> DeviceMesh | None:
29
+ if not dist.is_available() or not dist.is_initialized():
30
+ return None
31
+ world_size = dist.get_world_size()
32
+ if world_size <= 1:
33
+ return None
34
+ return init_device_mesh("cuda", (world_size,), mesh_dim_names=("fsdp",))
35
+
36
+
37
+ def _move_buffers_to_device(module: torch.nn.Module, device: torch.device) -> None:
38
+ for submodule in module.modules():
39
+ for name, buffer in tuple(submodule.named_buffers(recurse=False)):
40
+ if buffer is not None and buffer.device != device:
41
+ submodule._buffers[name] = buffer.to(device=device)
42
+
43
+
44
+ def _move_parameters_to_device(
45
+ parameters: set[torch.nn.Parameter],
46
+ device: torch.device,
47
+ ) -> None:
48
+ for param in parameters:
49
+ if param.device != device:
50
+ param.data = param.data.to(device=device)
51
+ if param.grad is not None and param.grad.device != device:
52
+ param.grad.data = param.grad.data.to(device=device)
53
+
54
+
55
+ def _current_cuda_device() -> torch.device | None:
56
+ if not torch.cuda.is_available():
57
+ return None
58
+ return torch.device("cuda", torch.cuda.current_device())
59
+
60
+
61
+ def apply_fsdp_inference(
62
+ transformer: torch.nn.Module,
63
+ mesh: DeviceMesh | None,
64
+ ) -> FSDPInferenceInfo:
65
+ if mesh is None:
66
+ return FSDPInferenceInfo(enabled=False, world_size=1, wrapped_blocks=0, ignored_params=0)
67
+ if bool(getattr(transformer, "_lingbot_fsdp_inference_enabled", False)):
68
+ blocks = getattr(transformer, "blocks", ())
69
+ return FSDPInferenceInfo(
70
+ enabled=True,
71
+ world_size=int(mesh.size()),
72
+ wrapped_blocks=len(blocks),
73
+ ignored_params=int(getattr(transformer, "_lingbot_fsdp_inference_ignored_params", 0)),
74
+ )
75
+
76
+ if fully_shard is None:
77
+ raise RuntimeError("PyTorch composable FSDP is not importable.") from FSDP_IMPORT_ERROR
78
+
79
+ cuda_device = _current_cuda_device()
80
+ if cuda_device is not None:
81
+ _move_buffers_to_device(transformer, cuda_device)
82
+
83
+ dtype_counts: dict[torch.dtype, int] = {}
84
+ for param in transformer.parameters():
85
+ dtype_counts[param.dtype] = dtype_counts.get(param.dtype, 0) + param.numel()
86
+ if dtype_counts:
87
+ primary_dtype = max(dtype_counts.items(), key=lambda item: item[1])[0]
88
+ ignored_params = {param for param in transformer.parameters() if param.dtype != primary_dtype}
89
+ else:
90
+ ignored_params = set()
91
+ if cuda_device is not None and ignored_params:
92
+ _move_parameters_to_device(ignored_params, cuda_device)
93
+
94
+ blocks: Any = getattr(transformer, "blocks", ())
95
+ wrapped_blocks = 0
96
+ for block in blocks:
97
+ block_ignored_params = {
98
+ param for param in block.parameters() if param in ignored_params
99
+ }
100
+ fully_shard(block, mesh=mesh, ignored_params=block_ignored_params)
101
+ wrapped_blocks += 1
102
+ fully_shard(transformer, mesh=mesh, ignored_params=ignored_params)
103
+ gc.collect()
104
+ if cuda_device is not None:
105
+ torch.cuda.empty_cache()
106
+ transformer._lingbot_fsdp_inference_enabled = True
107
+ transformer._lingbot_fsdp_inference_ignored_params = len(ignored_params)
108
+ return FSDPInferenceInfo(
109
+ enabled=True,
110
+ world_size=int(mesh.size()),
111
+ wrapped_blocks=wrapped_blocks,
112
+ ignored_params=len(ignored_params),
113
+ )
lingbot_video/inference_backend.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Sequence, TextIO
8
+
9
+
10
+ BACKEND_TO_ENGINE = {
11
+ "diffusers": "diffusers",
12
+ "sglang": "sglang-native",
13
+ }
14
+ ENGINE_CHOICES = frozenset(BACKEND_TO_ENGINE.values())
15
+
16
+
17
+ def _option_present(argv: Sequence[str], option: str) -> bool:
18
+ return any(arg == option or arg.startswith(f"{option}=") for arg in argv)
19
+
20
+
21
+ def default_backend_argv(argv: Sequence[str]) -> list[str]:
22
+ """Default the public CLI to direct diffusers when no backend is requested."""
23
+
24
+ normalized = list(argv)
25
+ if _option_present(normalized, "--backend") or _option_present(normalized, "--engine"):
26
+ return normalized
27
+ return ["--backend", "diffusers", *normalized]
28
+
29
+
30
+ def _module_available(name: str) -> bool:
31
+ if name in sys.modules:
32
+ return True
33
+ try:
34
+ return importlib.util.find_spec(name) is not None
35
+ except (ImportError, ModuleNotFoundError, KeyError, ValueError):
36
+ return False
37
+
38
+
39
+ def sglang_native_available() -> bool:
40
+ """Return whether the local SGLang-native adapter can be imported."""
41
+
42
+ if not _module_available("sglang"):
43
+ return False
44
+ try:
45
+ from lingbot_video.native_backend import LingBotVideoNativePipeline
46
+ except Exception:
47
+ return False
48
+ return LingBotVideoNativePipeline is not None
49
+
50
+
51
+ def resolve_backend_engine(
52
+ *,
53
+ engine: str | None,
54
+ backend: str | None,
55
+ sglang_available: bool | None = None,
56
+ stderr: TextIO | None = None,
57
+ ) -> str:
58
+ """Resolve public ``--backend`` and internal ``--engine`` into a runner engine."""
59
+
60
+ explicit_engine = backend is None and engine is not None
61
+ if backend is not None:
62
+ if backend not in BACKEND_TO_ENGINE:
63
+ choices = ", ".join(sorted(BACKEND_TO_ENGINE))
64
+ raise ValueError(f"unsupported backend: {backend!r}; choices: {choices}")
65
+ resolved = BACKEND_TO_ENGINE[backend]
66
+ elif engine is not None:
67
+ if engine not in ENGINE_CHOICES:
68
+ choices = ", ".join(sorted(ENGINE_CHOICES))
69
+ raise ValueError(f"unsupported engine: {engine!r}; choices: {choices}")
70
+ resolved = engine
71
+ else:
72
+ resolved = "sglang-native"
73
+
74
+ if resolved != "sglang-native":
75
+ return resolved
76
+ if explicit_engine:
77
+ return resolved
78
+
79
+ available = sglang_available
80
+ if available is None:
81
+ available = sglang_native_available()
82
+ if available:
83
+ return resolved
84
+
85
+ stream = stderr if stderr is not None else sys.stderr
86
+ print(
87
+ "WARNING: SGLang backend requested but SGLang is not installed or its "
88
+ "native diffusion API is unavailable; falling back to diffusers. "
89
+ "Install requirements-sglang.txt to enable SGLang Diffusion.",
90
+ file=stream,
91
+ flush=True,
92
+ )
93
+ return "diffusers"
94
+
95
+
96
+ def load_negative_prompt_json(path: str | Path) -> str:
97
+ """Load an auto-negative JSON file and serialize it as the prompt string."""
98
+
99
+ with Path(path).open(encoding="utf-8") as f:
100
+ payload = json.load(f)
101
+ return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
102
+
103
+
104
+ def resolve_negative_prompt_arg(
105
+ negative_prompt: str | None,
106
+ negative_prompt_json: str | None,
107
+ ) -> str | None:
108
+ """Resolve manual and auto-negative prompt inputs for the runner."""
109
+
110
+ if negative_prompt and negative_prompt_json:
111
+ raise ValueError("Use either --negative_prompt or --negative_prompt_json, not both.")
112
+ if negative_prompt_json:
113
+ return load_negative_prompt_json(negative_prompt_json)
114
+ return negative_prompt
lingbot_video/model_paths.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+
7
+ def model_component_dir(model_dir: str | Path, component: str) -> Path:
8
+ """Return a named component directory under a public model root."""
9
+
10
+ path = Path(model_dir) / component
11
+ if not path.is_dir():
12
+ raise FileNotFoundError(f"missing model component `{component}`: {path}")
13
+ return path
14
+
15
+
16
+ def effective_refiner_model_dir(args: Any) -> Path | None:
17
+ """Resolve the model root to use for refiner loading.
18
+
19
+ Passing --run_refiner means the public runner should load `refiner/` from
20
+ the same model root by default. --refiner_model_dir remains an override for
21
+ nonstandard package layouts.
22
+ """
23
+
24
+ refiner_model_dir = getattr(args, "refiner_model_dir", None)
25
+ requested = bool(getattr(args, "run_refiner", False) or refiner_model_dir)
26
+ if not requested:
27
+ return None
28
+ return Path(refiner_model_dir or getattr(args, "model_dir"))
lingbot_video/moe_pack_kernels.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+
5
+ try:
6
+ import triton
7
+ import triton.language as tl
8
+ except Exception: # pragma: no cover - Triton is optional outside GPU deployments.
9
+ triton = None
10
+ tl = None
11
+
12
+
13
+ if triton is not None and tl is not None:
14
+ @triton.jit
15
+ def _moe_route_count_slots_kernel(
16
+ flat_indices,
17
+ counts,
18
+ route_slots,
19
+ num_routes: tl.constexpr,
20
+ block_m: tl.constexpr,
21
+ ):
22
+ offsets = tl.program_id(0) * block_m + tl.arange(0, block_m)
23
+ mask = offsets < num_routes
24
+ experts = tl.load(flat_indices + offsets, mask=mask, other=0)
25
+ slots = tl.atomic_add(counts + experts, 1, sem="relaxed", mask=mask)
26
+ tl.store(route_slots + offsets, slots, mask=mask)
27
+
28
+ @triton.jit
29
+ def _moe_pack_tokens_kernel(
30
+ tokens,
31
+ flat_scores,
32
+ flat_indices,
33
+ offsets,
34
+ route_slots,
35
+ permuted_tokens,
36
+ sorted_positions,
37
+ sorted_scores,
38
+ hidden_size: tl.constexpr,
39
+ top_k: tl.constexpr,
40
+ block_h: tl.constexpr,
41
+ ):
42
+ route_idx = tl.program_id(0)
43
+ hidden_block = tl.program_id(1)
44
+ offsets_h = hidden_block * block_h + tl.arange(0, block_h)
45
+ hidden_mask = offsets_h < hidden_size
46
+
47
+ expert_idx = tl.load(flat_indices + route_idx)
48
+ slot = tl.load(route_slots + route_idx)
49
+ expert_offset = tl.load(offsets + expert_idx)
50
+ dest_idx = expert_offset + slot
51
+ token_idx = route_idx // top_k
52
+
53
+ values = tl.load(
54
+ tokens + token_idx * hidden_size + offsets_h,
55
+ mask=hidden_mask,
56
+ other=0.0,
57
+ )
58
+ tl.store(
59
+ permuted_tokens + dest_idx * hidden_size + offsets_h,
60
+ values,
61
+ mask=hidden_mask,
62
+ )
63
+
64
+ first_hidden_block = hidden_block == 0
65
+ tl.store(sorted_positions + dest_idx, route_idx, mask=first_hidden_block)
66
+ score = tl.load(flat_scores + route_idx, mask=first_hidden_block, other=0.0)
67
+ tl.store(sorted_scores + dest_idx, score, mask=first_hidden_block)
68
+ else:
69
+ _moe_route_count_slots_kernel = None
70
+ _moe_pack_tokens_kernel = None
71
+
72
+
73
+ def reorder_tokens_triton_pack(
74
+ tokens: torch.Tensor,
75
+ top_scores: torch.Tensor,
76
+ top_indices: torch.Tensor,
77
+ num_experts: int,
78
+ block_h: int = 64,
79
+ block_m: int = 256,
80
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, int, int]:
81
+ if _moe_route_count_slots_kernel is None or _moe_pack_tokens_kernel is None:
82
+ raise RuntimeError("LINGBOT_MOE_REORDER_BACKEND=triton_pack requires Triton")
83
+ if tokens.device.type != "cuda":
84
+ raise RuntimeError("LINGBOT_MOE_REORDER_BACKEND=triton_pack requires CUDA tensors")
85
+ if tokens.ndim != 2:
86
+ raise ValueError(f"Expected 2D tokens, got {tokens.ndim}D")
87
+ if top_scores.shape != top_indices.shape:
88
+ raise ValueError("top_scores and top_indices must have the same shape")
89
+ if top_indices.ndim != 2:
90
+ raise ValueError(f"Expected 2D top_indices, got {top_indices.ndim}D")
91
+
92
+ num_tokens = tokens.shape[0]
93
+ hidden_size = tokens.shape[1]
94
+ top_k = top_indices.shape[1]
95
+ num_routes = top_indices.numel()
96
+ counts = torch.zeros(num_experts, dtype=torch.int32, device=tokens.device)
97
+
98
+ if num_routes == 0:
99
+ return (
100
+ tokens.new_empty((0, hidden_size)),
101
+ counts,
102
+ torch.empty(0, dtype=torch.int64, device=tokens.device),
103
+ top_scores.new_empty((0,)),
104
+ num_tokens,
105
+ top_k,
106
+ )
107
+
108
+ tokens = tokens.contiguous()
109
+ flat_scores = top_scores.contiguous().reshape(-1)
110
+ flat_indices = top_indices.contiguous().to(torch.int32).reshape(-1)
111
+ route_slots = torch.empty(num_routes, dtype=torch.int32, device=tokens.device)
112
+
113
+ _moe_route_count_slots_kernel[(triton.cdiv(num_routes, block_m),)](
114
+ flat_indices,
115
+ counts,
116
+ route_slots,
117
+ num_routes,
118
+ block_m,
119
+ )
120
+
121
+ counts_i64 = counts.to(torch.int64)
122
+ offsets = torch.empty(num_experts + 1, dtype=torch.int64, device=tokens.device)
123
+ offsets[0] = 0
124
+ offsets[1:] = torch.cumsum(counts_i64, dim=0)
125
+
126
+ permuted_tokens = torch.empty((num_routes, hidden_size), dtype=tokens.dtype, device=tokens.device)
127
+ sorted_positions = torch.empty(num_routes, dtype=torch.int64, device=tokens.device)
128
+ sorted_scores = torch.empty(num_routes, dtype=top_scores.dtype, device=tokens.device)
129
+ _moe_pack_tokens_kernel[(num_routes, triton.cdiv(hidden_size, block_h))](
130
+ tokens,
131
+ flat_scores,
132
+ flat_indices,
133
+ offsets,
134
+ route_slots,
135
+ permuted_tokens,
136
+ sorted_positions,
137
+ sorted_scores,
138
+ hidden_size,
139
+ top_k,
140
+ block_h,
141
+ )
142
+ return permuted_tokens, counts, sorted_positions, sorted_scores, num_tokens, top_k
lingbot_video/moe_restore_kernels.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+
5
+ try:
6
+ import triton
7
+ import triton.language as tl
8
+ except Exception: # pragma: no cover - Triton is optional outside GPU deployments.
9
+ triton = None
10
+ tl = None
11
+
12
+
13
+ if triton is not None and tl is not None:
14
+ @triton.jit
15
+ def _moe_restore_weighted_sum_kernel(
16
+ expert_output,
17
+ sorted_scores,
18
+ route_for_position,
19
+ output,
20
+ hidden_size: tl.constexpr,
21
+ top_k: tl.constexpr,
22
+ block_h: tl.constexpr,
23
+ ):
24
+ token_idx = tl.program_id(0)
25
+ hidden_block = tl.program_id(1)
26
+ offsets_h = hidden_block * block_h + tl.arange(0, block_h)
27
+ hidden_mask = offsets_h < hidden_size
28
+ acc = tl.zeros((block_h,), dtype=tl.float32)
29
+
30
+ for route_slot in tl.range(0, top_k):
31
+ route_pos = token_idx * top_k + route_slot
32
+ route_idx = tl.load(route_for_position + route_pos)
33
+ active = route_idx >= 0
34
+ safe_route_idx = tl.maximum(route_idx, 0)
35
+ values = tl.load(
36
+ expert_output + safe_route_idx * hidden_size + offsets_h,
37
+ mask=active & hidden_mask,
38
+ other=0.0,
39
+ ).to(tl.float32)
40
+ score = tl.load(sorted_scores + safe_route_idx, mask=active, other=0.0).to(tl.float32)
41
+ acc += values * score
42
+
43
+ tl.store(output + token_idx * hidden_size + offsets_h, acc, mask=hidden_mask)
44
+ else:
45
+ _moe_restore_weighted_sum_kernel = None
46
+
47
+
48
+ def restore_tokens_triton(
49
+ expert_output: torch.Tensor,
50
+ sorted_positions: torch.Tensor,
51
+ sorted_scores: torch.Tensor,
52
+ num_tokens: int,
53
+ top_k: int,
54
+ block_h: int = 64,
55
+ ) -> torch.Tensor:
56
+ if _moe_restore_weighted_sum_kernel is None:
57
+ raise RuntimeError("LINGBOT_MOE_RESTORE_BACKEND=triton requires Triton")
58
+ if expert_output.ndim != 2:
59
+ raise ValueError(f"Expected 2D expert_output, got {expert_output.ndim}D")
60
+ if sorted_positions.numel() != sorted_scores.numel():
61
+ raise ValueError("sorted_positions and sorted_scores must have the same length")
62
+ if sorted_positions.numel() == 0:
63
+ return expert_output.new_zeros((num_tokens, expert_output.shape[-1]))
64
+
65
+ hidden_size = expert_output.shape[-1]
66
+ route_for_position = torch.full(
67
+ (num_tokens * top_k,),
68
+ -1,
69
+ dtype=torch.int32,
70
+ device=expert_output.device,
71
+ )
72
+ route_for_position[sorted_positions] = torch.arange(
73
+ sorted_positions.numel(),
74
+ dtype=torch.int32,
75
+ device=expert_output.device,
76
+ )
77
+ output = torch.empty(
78
+ (num_tokens, hidden_size),
79
+ dtype=expert_output.dtype,
80
+ device=expert_output.device,
81
+ )
82
+ grid = (num_tokens, triton.cdiv(hidden_size, block_h))
83
+ _moe_restore_weighted_sum_kernel[grid](
84
+ expert_output.contiguous(),
85
+ sorted_scores.contiguous(),
86
+ route_for_position,
87
+ output,
88
+ hidden_size,
89
+ top_k,
90
+ block_h,
91
+ )
92
+ return output
lingbot_video/native_backend.py ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from contextlib import contextmanager
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import torch
10
+ from diffusers import DiffusionPipeline
11
+
12
+ from lingbot_video.model_paths import model_component_dir
13
+ from lingbot_video.pipeline_lingbot_video import (
14
+ DEFAULT_NEGATIVE_PROMPT,
15
+ LingBotVideoPipeline,
16
+ LingBotVideoPipelineOutput,
17
+ )
18
+ from lingbot_video.pipeline_lingbot_video_i2v import LingBotVideoImageToVideoPipeline
19
+ from lingbot_video.transformer_lingbot_video import LingBotVideoTransformer3DModel
20
+
21
+ try:
22
+ from sglang.multimodal_gen import registry as sglang_registry
23
+ except Exception: # pragma: no cover - SGLang is an optional deployment dep
24
+ sglang_registry = None
25
+
26
+ try:
27
+ from sglang.multimodal_gen.runtime.server_args import (
28
+ Backend,
29
+ ServerArgs,
30
+ set_global_server_args,
31
+ )
32
+ except Exception: # pragma: no cover - SGLang is an optional deployment dep
33
+ Backend = None
34
+ ServerArgs = None
35
+ set_global_server_args = None
36
+
37
+
38
+ @dataclass
39
+ class LingBotVideoNativePipelineConfig:
40
+ """Minimal config object for the LingBotVideo native adapter."""
41
+
42
+ flow_shift: float = 3.0
43
+
44
+
45
+ @dataclass
46
+ class LingBotVideoNativeSamplingParams:
47
+ """Default sampling parameters for the native pipeline."""
48
+
49
+ prompt: str | None = None
50
+ negative_prompt: str = DEFAULT_NEGATIVE_PROMPT
51
+ height: int = 480
52
+ width: int = 832
53
+ num_frames: int = 81
54
+ fps: int = 24
55
+ num_inference_steps: int = 40
56
+ guidance_scale: float = 3.0
57
+ seed: int = 42
58
+ shift: float = 3.0
59
+
60
+
61
+ @contextmanager
62
+ def _patch_qwen3vl_from_pretrained():
63
+ try:
64
+ from transformers import Qwen3VLForConditionalGeneration
65
+ except Exception:
66
+ yield
67
+ return
68
+
69
+ original_from_pretrained = Qwen3VLForConditionalGeneration.from_pretrained
70
+ attn_implementation = os.environ.get("LINGBOT_QWEN_ATTN_IMPLEMENTATION", "flash_attention_3")
71
+
72
+ @classmethod
73
+ def patched_from_pretrained(cls, pretrained_model_name_or_path, *args, **kwargs):
74
+ if attn_implementation:
75
+ kwargs.setdefault("attn_implementation", attn_implementation)
76
+ if "torch_dtype" in kwargs and "dtype" not in kwargs:
77
+ kwargs["dtype"] = kwargs.pop("torch_dtype")
78
+ return original_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
79
+
80
+ Qwen3VLForConditionalGeneration.from_pretrained = patched_from_pretrained
81
+ try:
82
+ yield
83
+ finally:
84
+ Qwen3VLForConditionalGeneration.from_pretrained = original_from_pretrained
85
+
86
+
87
+ def _module_dtype_name(module: Any) -> str | None:
88
+ if module is None:
89
+ return None
90
+ dtype = getattr(module, "dtype", None)
91
+ if isinstance(dtype, torch.dtype):
92
+ return str(dtype).replace("torch.", "")
93
+ if isinstance(module, torch.nn.Module):
94
+ try:
95
+ return str(next(module.parameters()).dtype).replace("torch.", "")
96
+ except StopIteration:
97
+ return None
98
+ return None
99
+
100
+
101
+ def _default_device() -> torch.device:
102
+ if not torch.cuda.is_available():
103
+ return torch.device("cpu")
104
+ return torch.device("cuda", torch.cuda.current_device())
105
+
106
+
107
+ def _pipeline_class_for_mode(mode: str) -> type[DiffusionPipeline]:
108
+ if mode == "ti2v":
109
+ return LingBotVideoImageToVideoPipeline
110
+ return LingBotVideoPipeline
111
+
112
+
113
+ def _load_lingbot_diffusers_pipe(
114
+ model_path: str | Path,
115
+ dtype_map: dict[str, torch.dtype] | torch.dtype | None,
116
+ mode: str,
117
+ transformer_subfolder: str,
118
+ ) -> DiffusionPipeline:
119
+ model_root = Path(model_path)
120
+ model_component_dir(model_root, transformer_subfolder)
121
+ transformer_dtype = (
122
+ dtype_map.get("transformer", dtype_map.get("default", torch.float32))
123
+ if isinstance(dtype_map, dict)
124
+ else dtype_map
125
+ )
126
+ transformer = LingBotVideoTransformer3DModel.from_pretrained(
127
+ str(model_root),
128
+ subfolder=transformer_subfolder,
129
+ torch_dtype=transformer_dtype,
130
+ )
131
+ load_kwargs: dict[str, Any] = {
132
+ "transformer": transformer,
133
+ "trust_remote_code": True,
134
+ }
135
+ if dtype_map is not None:
136
+ load_kwargs["torch_dtype"] = dtype_map
137
+ with _patch_qwen3vl_from_pretrained():
138
+ pipe = _pipeline_class_for_mode(mode).from_pretrained(
139
+ str(model_root),
140
+ **load_kwargs,
141
+ )
142
+ return pipe.to(_default_device())
143
+
144
+
145
+ class LingBotVideoNativeExecutionStage:
146
+ """Single-stage LingBotVideo execution."""
147
+
148
+ def __init__(self, diffusers_pipe: DiffusionPipeline):
149
+ self.diffusers_pipe = diffusers_pipe
150
+
151
+ def __call__(self, **kwargs: Any) -> LingBotVideoPipelineOutput:
152
+ return self.forward(**kwargs)
153
+
154
+ @torch.no_grad()
155
+ def forward(self, **kwargs: Any) -> LingBotVideoPipelineOutput:
156
+ output = self.diffusers_pipe(**kwargs)
157
+ frames = output.frames if hasattr(output, "frames") else output[0]
158
+ return LingBotVideoPipelineOutput(frames=frames)
159
+
160
+
161
+ class LingBotVideoNativePipeline:
162
+ """SGLang-native LingBotVideo pipeline adapter."""
163
+
164
+ pipeline_name = "LingBotVideoNativePipeline"
165
+ pipeline_config_cls = LingBotVideoNativePipelineConfig
166
+ sampling_params_cls = LingBotVideoNativeSamplingParams
167
+ is_video_pipeline = True
168
+
169
+ def __init__(
170
+ self,
171
+ diffusers_pipe: DiffusionPipeline | str | Path,
172
+ *,
173
+ model_path: str | Path | None = None,
174
+ server_args: Any | None = None,
175
+ dtype_map: dict[str, torch.dtype] | torch.dtype | None = None,
176
+ mode: str = "t2v",
177
+ transformer_subfolder: str = "transformer",
178
+ **_: Any,
179
+ ):
180
+ if isinstance(diffusers_pipe, (str, Path)):
181
+ model_path = diffusers_pipe if model_path is None else model_path
182
+ diffusers_pipe = _load_lingbot_diffusers_pipe(
183
+ model_path,
184
+ dtype_map,
185
+ mode=mode,
186
+ transformer_subfolder=transformer_subfolder,
187
+ )
188
+ if model_path is None:
189
+ raise ValueError("model_path is required for LingBotVideoNativePipeline")
190
+ self.diffusers_pipe = diffusers_pipe
191
+ self.model_path = str(model_path)
192
+ self.server_args = server_args
193
+ self.execution_stage = LingBotVideoNativeExecutionStage(diffusers_pipe)
194
+ self.modules = {"lingbot_video_pipeline": diffusers_pipe}
195
+ self.memory_usages: dict[str, float] = {}
196
+
197
+ @property
198
+ def transformer(self) -> Any:
199
+ return getattr(self.diffusers_pipe, "transformer", None)
200
+
201
+ @property
202
+ def text_encoder(self) -> Any:
203
+ return getattr(self.diffusers_pipe, "text_encoder", None)
204
+
205
+ @property
206
+ def vae(self) -> Any:
207
+ return getattr(self.diffusers_pipe, "vae", None)
208
+
209
+ @property
210
+ def scheduler(self) -> Any:
211
+ return getattr(self.diffusers_pipe, "scheduler", None)
212
+
213
+ def encode_prompt(self, *args: Any, **kwargs: Any) -> Any:
214
+ return self.diffusers_pipe.encode_prompt(*args, **kwargs)
215
+
216
+ def encode_video_latent(self, *args: Any, **kwargs: Any) -> Any:
217
+ return self.diffusers_pipe.encode_video_latent(*args, **kwargs)
218
+
219
+ @classmethod
220
+ def from_diffusers_pipe(
221
+ cls,
222
+ diffusers_pipe: DiffusionPipeline,
223
+ *,
224
+ model_path: str | Path,
225
+ server_args: Any | None = None,
226
+ ) -> "LingBotVideoNativePipeline":
227
+ return cls(diffusers_pipe, model_path=model_path, server_args=server_args)
228
+
229
+ @classmethod
230
+ def from_pretrained(
231
+ cls,
232
+ model_path: str | Path,
233
+ *,
234
+ torch_dtype: dict[str, torch.dtype] | torch.dtype | None = None,
235
+ server_args: Any | None = None,
236
+ mode: str = "t2v",
237
+ transformer_subfolder: str = "transformer",
238
+ **kwargs: Any,
239
+ ) -> "LingBotVideoNativePipeline":
240
+ return cls(
241
+ model_path,
242
+ model_path=model_path,
243
+ server_args=server_args,
244
+ dtype_map=torch_dtype,
245
+ mode=mode,
246
+ transformer_subfolder=transformer_subfolder,
247
+ **kwargs,
248
+ )
249
+
250
+ @torch.no_grad()
251
+ def __call__(
252
+ self,
253
+ *,
254
+ prompt: str,
255
+ negative_prompt: str = DEFAULT_NEGATIVE_PROMPT,
256
+ height: int = 480,
257
+ width: int = 832,
258
+ num_frames: int = 81,
259
+ num_inference_steps: int = 40,
260
+ guidance_scale: float = 3.0,
261
+ shift: float = 3.0,
262
+ generator: torch.Generator | None = None,
263
+ image: Any | None = None,
264
+ output_type: str = "np",
265
+ **kwargs: Any,
266
+ ) -> LingBotVideoPipelineOutput:
267
+ call_kwargs = dict(
268
+ prompt=prompt,
269
+ negative_prompt=negative_prompt,
270
+ height=height,
271
+ width=width,
272
+ num_frames=num_frames,
273
+ num_inference_steps=num_inference_steps,
274
+ guidance_scale=guidance_scale,
275
+ shift=shift,
276
+ generator=generator,
277
+ output_type=output_type,
278
+ )
279
+ if image is not None:
280
+ call_kwargs["image"] = image
281
+ call_kwargs.update(kwargs)
282
+ return self.execution_stage(**call_kwargs)
283
+
284
+ def component_dtypes(self) -> dict[str, str | None]:
285
+ return {
286
+ "transformer": _module_dtype_name(self.transformer),
287
+ "text_encoder": _module_dtype_name(self.text_encoder),
288
+ "vae": _module_dtype_name(self.vae),
289
+ }
290
+
291
+
292
+ def register_lingbot_native_pipeline() -> bool:
293
+ """Register the adapter in SGLang's native registry when SGLang is present."""
294
+
295
+ if sglang_registry is None:
296
+ return False
297
+
298
+ try:
299
+ sglang_registry._PIPELINE_REGISTRY[LingBotVideoNativePipeline.pipeline_name] = ( # type: ignore[attr-defined]
300
+ LingBotVideoNativePipeline
301
+ )
302
+ sglang_registry._PIPELINE_CONFIG_REGISTRY[LingBotVideoNativePipeline.pipeline_name] = ( # type: ignore[attr-defined]
303
+ LingBotVideoNativePipelineConfig,
304
+ LingBotVideoNativeSamplingParams,
305
+ )
306
+ except Exception:
307
+ return False
308
+ return True
309
+
310
+
311
+ def load_lingbot_native_pipeline(
312
+ model_dir: Path,
313
+ dtype_map: dict[str, torch.dtype],
314
+ mode: str = "t2v",
315
+ transformer_subfolder: str = "transformer",
316
+ ) -> LingBotVideoNativePipeline:
317
+ """Load a LingBotVideo native adapter from a diffusers-format model dir."""
318
+
319
+ server_args = None
320
+ if Backend is not None and ServerArgs is not None and set_global_server_args is not None:
321
+ try:
322
+ server_args = ServerArgs(
323
+ model_path=str(model_dir),
324
+ backend=Backend.SGLANG,
325
+ trust_remote_code=True,
326
+ pipeline_class_name=LingBotVideoNativePipeline.pipeline_name,
327
+ pipeline_config=LingBotVideoNativePipelineConfig(),
328
+ dit_cpu_offload=False,
329
+ dit_layerwise_offload=False,
330
+ layerwise_offload_components=[],
331
+ text_encoder_cpu_offload=False,
332
+ image_encoder_cpu_offload=False,
333
+ vae_cpu_offload=False,
334
+ )
335
+ set_global_server_args(server_args)
336
+ except Exception:
337
+ server_args = None
338
+
339
+ register_lingbot_native_pipeline()
340
+
341
+ return LingBotVideoNativePipeline.from_pretrained(
342
+ model_path=model_dir,
343
+ torch_dtype=dtype_map,
344
+ server_args=server_args,
345
+ mode=mode,
346
+ transformer_subfolder=transformer_subfolder,
347
+ )
lingbot_video/pipeline_lingbot_video.py ADDED
@@ -0,0 +1,591 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from contextlib import nullcontext
4
+ from dataclasses import dataclass
5
+ from typing import Any, Dict, List, Optional, Tuple, Union
6
+
7
+ import numpy as np
8
+ import torch
9
+ import torch.distributed as dist
10
+ from torchvision.transforms.functional import normalize as normalize_image_tensor
11
+
12
+ from diffusers import DiffusionPipeline
13
+ from diffusers.utils import BaseOutput
14
+ from diffusers.utils.torch_utils import randn_tensor
15
+
16
+ from .utils import (
17
+ LOW_NOISE_TAIL_V1_DEFAULT_STEPS,
18
+ batch_cfg_prompt_inputs,
19
+ compute_refiner_sigmas,
20
+ )
21
+ from .scheduling_flow_unipc import FlowUniPCMultistepScheduler
22
+
23
+
24
+ TOKEN_LENGTH = 37698
25
+ HIDDEN_STATE_SKIP_LAYER = 0
26
+
27
+ PROMPT_TEMPLATE = (
28
+ "<|im_start|>system\nGiven a user input that may include a text prompt alone, "
29
+ "a text prompt with an image reference, or a text prompt with a video reference "
30
+ "or a video reference alone, generate an \"Enhanced prompt\" that provides detailed "
31
+ "visual descriptions suitable for video generation. Evaluate the level of detail "
32
+ "in the user's input: if it is simple, enrich it by adding specifics about colors, "
33
+ "shapes, sizes, textures, lighting, motion dynamics, camera movement, temporal "
34
+ "progression, and spatial relationships to create vivid, concrete, and temporally "
35
+ "coherent scenes to create vivid and concrete scenes. Please generate only the "
36
+ "enhanced description for the prompt below and avoid including any additional "
37
+ "commentary or evaluations:<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n"
38
+ "<|im_start|>assistant\n"
39
+ )
40
+ IMG_PROMPT_TEMPLATE = "<|vision_start|><|image_pad|><|vision_end|>"
41
+ VIDEO_PROMPT_TEMPLATE = "<|vision_start|><|video_pad|><|vision_end|>"
42
+
43
+ DEFAULT_NEGATIVE_PROMPT = (
44
+ '{"universal_negative": {"visual_quality": ["low quality", "worst quality", "blurry", "pixelated", "jpeg artifacts", "low resolution", "unstable color", "color flicker", "underexposed", "overexposed", "invisible subject", "subject hidden in darkness"], "artistic_style": ["painting", "illustration", "drawing", "cartoon", "3d render", "cgi", "sketch", "digital art"], "composition_and_content": ["text", "watermark", "signature", "logo", "subtitles", "pillarboxed", "side bars", "portrait image in landscape frame"], "temporal_and_motion_stability": ["flickering", "jittery", "motion blur", "temporal inconsistency", "warping", "morphing", "incoherent motion", "unnatural movement", "static object with sudden jump", "frame-to-frame inconsistency"], "material_and_structure": ["plastic-like glass", "unrealistic texture", "deformed bottle", "liquid freezing improperly", "distorted reflections"]}}'
45
+ )
46
+
47
+ # Still-image default (t2i): drops the whole temporal/motion block and the video-only
48
+ # codec/temporal terms that cannot apply to a single frame.
49
+ DEFAULT_NEGATIVE_PROMPT_IMAGE = (
50
+ '{"universal_negative": {"visual_quality": ["low quality", "worst quality", "blurry", "pixelated", "jpeg artifacts", "low resolution", "underexposed", "overexposed", "invisible subject", "subject hidden in darkness"], "artistic_style": ["painting", "illustration", "drawing", "cartoon", "3d render", "cgi", "sketch", "digital art"], "composition_and_content": ["text", "watermark", "signature", "logo", "pillarboxed", "side bars", "portrait image in landscape frame"], "material_and_structure": ["plastic-like glass", "unrealistic texture", "deformed bottle", "distorted reflections"]}}'
51
+ )
52
+
53
+
54
+ @dataclass
55
+ class LingBotVideoPipelineOutput(BaseOutput):
56
+ frames: Union[List[np.ndarray], torch.Tensor]
57
+
58
+
59
+ def _module_dtype(module: torch.nn.Module) -> torch.dtype:
60
+ try:
61
+ return next(module.parameters()).dtype
62
+ except StopIteration:
63
+ return torch.float32
64
+
65
+
66
+ def _transformer_timestep(timestep: torch.Tensor, transformer_dtype: torch.dtype) -> torch.Tensor:
67
+ sigma = timestep.float() / 1000.0
68
+ if transformer_dtype in {torch.bfloat16, torch.float16}:
69
+ sigma = sigma.to(transformer_dtype)
70
+ return (sigma * 1000.0).float()
71
+
72
+
73
+ def _transformer_autocast(device: torch.device, transformer_dtype: torch.dtype):
74
+ if device.type != "cuda" or transformer_dtype not in {torch.bfloat16, torch.float16}:
75
+ return nullcontext()
76
+ return torch.autocast(device_type="cuda", dtype=transformer_dtype)
77
+
78
+
79
+ def _module_device(module: torch.nn.Module) -> torch.device:
80
+ try:
81
+ return next(module.parameters()).device
82
+ except StopIteration:
83
+ return torch.device("cpu")
84
+
85
+
86
+ def _group_global_rank(group: Optional[Any], group_rank: int) -> int:
87
+ if group is None:
88
+ return group_rank
89
+ get_global_rank = getattr(dist, "get_global_rank", None)
90
+ if get_global_rank is None:
91
+ return group_rank
92
+ return int(get_global_rank(group, group_rank))
93
+
94
+
95
+ class LingBotVideoPipeline(DiffusionPipeline):
96
+ """Minimal LingBotVideo t2v/t2i pipeline.
97
+
98
+ Standard CFG runs as two independent transformer forwards unless batched CFG
99
+ or CFG parallelism is explicitly requested.
100
+ """
101
+
102
+ model_cpu_offload_seq = "text_encoder->transformer->vae"
103
+
104
+ def __init__(self, transformer, vae, text_encoder, processor, scheduler):
105
+ super().__init__()
106
+ if (
107
+ scheduler is not None
108
+ and scheduler.__class__.__name__ != FlowUniPCMultistepScheduler.__name__
109
+ ):
110
+ raise TypeError(
111
+ "LingBotVideoPipeline requires vendored FlowUniPCMultistepScheduler; "
112
+ f"got {scheduler.__class__.__name__}."
113
+ )
114
+ self.register_modules(
115
+ transformer=transformer,
116
+ vae=vae,
117
+ text_encoder=text_encoder,
118
+ processor=processor,
119
+ scheduler=scheduler,
120
+ )
121
+ self.vae_scale_factor_temporal = 4
122
+ self.vae_scale_factor_spatial = 8
123
+ self.token_length = TOKEN_LENGTH
124
+ self.hidden_state_skip_layer = HIDDEN_STATE_SKIP_LAYER
125
+ self.prompt_template = PROMPT_TEMPLATE
126
+ self.img_prompt_template = IMG_PROMPT_TEMPLATE
127
+ self.video_prompt_template = VIDEO_PROMPT_TEMPLATE
128
+ self._crop_start: Optional[int] = None
129
+
130
+ @staticmethod
131
+ def check_inputs(height: int, width: int, num_frames: int) -> None:
132
+ if num_frames != 1 and (num_frames - 1) % 4 != 0:
133
+ raise ValueError(f"`num_frames` must be 1 or 4n+1, got {num_frames}.")
134
+ if height % 16 != 0 or width % 16 != 0:
135
+ raise ValueError(f"`height` and `width` must be multiples of 16, got {height}x{width}.")
136
+
137
+ @staticmethod
138
+ def _apply_inpainting(latents: torch.Tensor, cond_latent: torch.Tensor) -> torch.Tensor:
139
+ cond_t = cond_latent.shape[2]
140
+ latents[:, :, :cond_t] = cond_latent.float()
141
+ return latents
142
+
143
+ @staticmethod
144
+ def apply_text_to_template(text: str, template: str = PROMPT_TEMPLATE) -> str:
145
+ return template.format(text)
146
+
147
+ def _compute_crop_start(self) -> int:
148
+ if self._crop_start is None:
149
+ marker = "<|USER_INPUT_MARKER|>"
150
+ marked = self.prompt_template.format(marker)
151
+ marker_pos = marked.find(marker)
152
+ if marker_pos < 0:
153
+ self._crop_start = 0
154
+ else:
155
+ prefix = self.processor(
156
+ text=marked[:marker_pos],
157
+ images=None,
158
+ videos=None,
159
+ return_tensors="pt",
160
+ )
161
+ self._crop_start = int(prefix["input_ids"].shape[1])
162
+ return self._crop_start
163
+
164
+ def _build_prompt_inputs(
165
+ self,
166
+ prompt: Union[str, List[str]],
167
+ images: Optional[Any] = None,
168
+ videos: Optional[Any] = None,
169
+ video_metadata: Optional[Any] = None,
170
+ video_kwargs: Optional[Dict[str, Any]] = None,
171
+ ):
172
+ if isinstance(prompt, str):
173
+ prompts = [prompt]
174
+ else:
175
+ prompts = list(prompt)
176
+
177
+ visual_template = ""
178
+ if images is not None:
179
+ visual_template = self.img_prompt_template
180
+ elif videos is not None:
181
+ visual_template = self.video_prompt_template
182
+
183
+ texts = [
184
+ self.apply_text_to_template(visual_template + text, self.prompt_template)
185
+ for text in prompts
186
+ ]
187
+ kwargs = dict(video_kwargs or {})
188
+ return self.processor(
189
+ text=texts,
190
+ images=images,
191
+ videos=videos,
192
+ video_metadata=video_metadata,
193
+ do_resize=False,
194
+ truncation=True,
195
+ max_length=self.token_length,
196
+ padding="longest",
197
+ return_tensors="pt",
198
+ **kwargs,
199
+ )
200
+
201
+ @torch.no_grad()
202
+ def encode_prompt(
203
+ self,
204
+ prompt: Union[str, List[str]],
205
+ *,
206
+ images: Optional[Any] = None,
207
+ videos: Optional[Any] = None,
208
+ video_metadata: Optional[Any] = None,
209
+ video_kwargs: Optional[Dict[str, Any]] = None,
210
+ device: Optional[Union[str, torch.device]] = None,
211
+ return_inputs: bool = False,
212
+ ):
213
+ if self.text_encoder is None or self.processor is None:
214
+ raise ValueError("`text_encoder` and `processor` are required for encode_prompt().")
215
+
216
+ device = torch.device(device) if device is not None else self._execution_device
217
+ inputs = self._build_prompt_inputs(
218
+ prompt,
219
+ images=images,
220
+ videos=videos,
221
+ video_metadata=video_metadata,
222
+ video_kwargs=video_kwargs,
223
+ )
224
+ inputs = inputs.to(device)
225
+ outputs = self.text_encoder(
226
+ **inputs,
227
+ output_hidden_states=self.hidden_state_skip_layer is not None,
228
+ )
229
+ if self.hidden_state_skip_layer is not None:
230
+ prompt_embeds = outputs.hidden_states[-(self.hidden_state_skip_layer + 1)]
231
+ else:
232
+ prompt_embeds = outputs.last_hidden_state
233
+
234
+ prompt_mask = inputs["attention_mask"]
235
+ crop_start = self._compute_crop_start()
236
+ if crop_start > 0:
237
+ prompt_embeds = prompt_embeds[:, crop_start:]
238
+ prompt_mask = prompt_mask[:, crop_start:]
239
+
240
+ # Batch=1 can drop right padding before DiT inference.
241
+ if prompt_embeds.shape[0] == 1:
242
+ true_len = int(prompt_mask[0].sum().item())
243
+ prompt_embeds = prompt_embeds[:, :true_len]
244
+ prompt_mask = prompt_mask[:, :true_len]
245
+
246
+ if return_inputs:
247
+ return prompt_embeds, prompt_mask, inputs
248
+ return prompt_embeds, prompt_mask
249
+
250
+ def prepare_latents(
251
+ self,
252
+ num_frames: int,
253
+ height: int,
254
+ width: int,
255
+ generator: Optional[torch.Generator],
256
+ latents: Optional[torch.Tensor],
257
+ device: torch.device,
258
+ ) -> torch.Tensor:
259
+ latent_frames = (num_frames - 1) // self.vae_scale_factor_temporal + 1
260
+ latent_height = height // self.vae_scale_factor_spatial
261
+ latent_width = width // self.vae_scale_factor_spatial
262
+ shape = (
263
+ 1,
264
+ self.transformer.config.in_channels,
265
+ latent_frames,
266
+ latent_height,
267
+ latent_width,
268
+ )
269
+ if latents is None:
270
+ return randn_tensor(shape, generator=generator, device=device, dtype=torch.float32)
271
+ if tuple(latents.shape) != shape:
272
+ raise ValueError(f"`latents` shape must be {shape}, got {tuple(latents.shape)}.")
273
+ return latents.to(device=device, dtype=torch.float32)
274
+
275
+ def _dit_latent_to_vae(self, latents: torch.Tensor) -> torch.Tensor:
276
+ mean = torch.tensor(self.vae.config.latents_mean, device=latents.device, dtype=torch.float32)
277
+ std_inv = 1.0 / torch.tensor(
278
+ self.vae.config.latents_std, device=latents.device, dtype=torch.float32
279
+ )
280
+ mean = mean.view(1, -1, 1, 1, 1)
281
+ std_inv = std_inv.view(1, -1, 1, 1, 1)
282
+ return latents.float() / std_inv + mean
283
+
284
+ def _vae_latent_to_dit(self, latents: torch.Tensor) -> torch.Tensor:
285
+ mean = torch.tensor(self.vae.config.latents_mean, device=latents.device, dtype=torch.float32)
286
+ std_inv = 1.0 / torch.tensor(
287
+ self.vae.config.latents_std, device=latents.device, dtype=torch.float32
288
+ )
289
+ mean = mean.view(1, -1, 1, 1, 1)
290
+ std_inv = std_inv.view(1, -1, 1, 1, 1)
291
+ return (latents.float() - mean) * std_inv
292
+
293
+ @torch.no_grad()
294
+ def encode_video_latent(
295
+ self,
296
+ video: torch.Tensor,
297
+ generator: Optional[torch.Generator] = None,
298
+ ) -> torch.Tensor:
299
+ if self.vae is None:
300
+ raise ValueError("`vae` is required to encode video latents.")
301
+ vae_device = _module_device(self.vae)
302
+ video = video.to(device=vae_device, dtype=torch.float32)
303
+ bsz, channels, frames, height, width = video.shape
304
+ flat_video = video.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channels, height, width)
305
+ norm_flat_video = normalize_image_tensor(
306
+ flat_video,
307
+ [0.5, 0.5, 0.5],
308
+ [0.5, 0.5, 0.5],
309
+ inplace=False,
310
+ )
311
+ norm_video = (
312
+ norm_flat_video.reshape(bsz, frames, channels, height, width)
313
+ .permute(0, 2, 1, 3, 4)
314
+ .contiguous()
315
+ )
316
+ with torch.autocast(
317
+ "cuda",
318
+ dtype=torch.bfloat16,
319
+ enabled=vae_device.type == "cuda",
320
+ ):
321
+ encoded = self.vae.encode(norm_video)
322
+ if hasattr(encoded, "latent_dist"):
323
+ latents = encoded.latent_dist.sample(generator)
324
+ else:
325
+ latents = encoded[0] if isinstance(encoded, tuple) else encoded
326
+ return self._vae_latent_to_dit(latents).to(latents)
327
+
328
+ @torch.no_grad()
329
+ def _decode_latents(
330
+ self,
331
+ latents: torch.Tensor,
332
+ ) -> List[np.ndarray]:
333
+ vae_device = _module_device(self.vae)
334
+ vae_dtype = _module_dtype(self.vae)
335
+ vae_latents = self._dit_latent_to_vae(latents).to(device=vae_device, dtype=torch.float32)
336
+ if vae_latents.ndim == 5:
337
+ vae_latents = vae_latents.contiguous(memory_format=torch.channels_last_3d)
338
+ autocast_dtype = (
339
+ vae_dtype
340
+ if vae_device.type == "cuda" and vae_dtype in {torch.bfloat16, torch.float16}
341
+ else None
342
+ )
343
+ with torch.autocast(
344
+ "cuda",
345
+ dtype=autocast_dtype or torch.bfloat16,
346
+ enabled=autocast_dtype is not None,
347
+ ):
348
+ decoded = self.vae.decode(vae_latents)
349
+ frames = decoded[0] if isinstance(decoded, tuple) else decoded.sample
350
+ frames = frames.float().clamp_(-1, 1)
351
+ frames = (frames + 1.0) / 2.0
352
+ frames = frames.permute(0, 2, 3, 4, 1).cpu().numpy()
353
+ return [video for video in frames]
354
+
355
+ @torch.no_grad()
356
+ def __call__(
357
+ self,
358
+ prompt: str,
359
+ negative_prompt: str = DEFAULT_NEGATIVE_PROMPT,
360
+ height: int = 480,
361
+ width: int = 480,
362
+ num_frames: int = 81,
363
+ num_inference_steps: int = 40,
364
+ guidance_scale: float = 6.0,
365
+ shift: float = 3.0,
366
+ generator: Optional[torch.Generator] = None,
367
+ latents: Optional[torch.Tensor] = None,
368
+ cond_latent: Optional[torch.Tensor] = None,
369
+ prompt_embeds: Optional[torch.Tensor] = None,
370
+ prompt_mask: Optional[torch.Tensor] = None,
371
+ negative_prompt_embeds: Optional[torch.Tensor] = None,
372
+ negative_prompt_mask: Optional[torch.Tensor] = None,
373
+ output_type: str = "np",
374
+ cfg_parallel_group: Optional[Any] = None,
375
+ batch_cfg: bool = False,
376
+ null_cond_clone_zero: bool = False,
377
+ t_thresh: Optional[float] = None,
378
+ refiner_sigma_tail_steps: int = LOW_NOISE_TAIL_V1_DEFAULT_STEPS,
379
+ offload_vae_during_denoise: bool = False,
380
+ return_dict: bool = True,
381
+ ) -> Union[LingBotVideoPipelineOutput, Tuple[Union[List[np.ndarray], torch.Tensor]]]:
382
+ self.check_inputs(height, width, num_frames)
383
+ if self.transformer is None or self.scheduler is None:
384
+ raise ValueError("`transformer` and `scheduler` are required for generation.")
385
+
386
+ device = self._execution_device
387
+ do_cfg = guidance_scale > 1.0
388
+ requested_batch_cfg = bool(batch_cfg)
389
+ effective_batch_cfg = requested_batch_cfg
390
+ batch_cfg_fallback_reason = None
391
+ self._last_batch_cfg_requested = requested_batch_cfg
392
+ self._last_effective_batch_cfg = bool(effective_batch_cfg and do_cfg)
393
+ self._last_batch_cfg_fallback_reason = batch_cfg_fallback_reason
394
+
395
+ cfg_parallel = cfg_parallel_group is not None
396
+ if cfg_parallel and effective_batch_cfg:
397
+ raise ValueError("`cfg_parallel_group` and `batch_cfg` are mutually exclusive.")
398
+ cfg_parallel_rank = 0
399
+ cfg_parallel_world_size = 1
400
+ if cfg_parallel:
401
+ if not dist.is_available() or not dist.is_initialized():
402
+ raise ValueError("`cfg_parallel_group` requires an initialized process group.")
403
+ if not do_cfg:
404
+ raise ValueError("CFG parallel requires `guidance_scale > 1.0`.")
405
+ cfg_parallel_rank = dist.get_rank(cfg_parallel_group)
406
+ cfg_parallel_world_size = dist.get_world_size(cfg_parallel_group)
407
+ if cfg_parallel_world_size != 2:
408
+ raise ValueError(
409
+ f"CFG parallel currently requires exactly 2 ranks, got {cfg_parallel_world_size}."
410
+ )
411
+
412
+ if prompt_embeds is not None:
413
+ if prompt_mask is None:
414
+ raise ValueError("`prompt_mask` is required when `prompt_embeds` is provided.")
415
+ prompt_embeds = prompt_embeds.to(device=device)
416
+ prompt_mask = prompt_mask.to(device=device)
417
+ if negative_prompt_embeds is not None:
418
+ if negative_prompt_mask is None:
419
+ raise ValueError(
420
+ "`negative_prompt_mask` is required when `negative_prompt_embeds` is provided."
421
+ )
422
+ negative_prompt_embeds = negative_prompt_embeds.to(device=device)
423
+ negative_prompt_mask = negative_prompt_mask.to(device=device)
424
+
425
+ if cfg_parallel and cfg_parallel_rank == 1:
426
+ if negative_prompt_embeds is not None:
427
+ negative_embeds, negative_mask = negative_prompt_embeds, negative_prompt_mask
428
+ else:
429
+ negative_embeds, negative_mask = self.encode_prompt(negative_prompt, device=device)
430
+ prompt_embeds = prompt_mask = None
431
+ else:
432
+ if prompt_embeds is None:
433
+ prompt_embeds, prompt_mask = self.encode_prompt(prompt, device=device)
434
+ if do_cfg and not cfg_parallel:
435
+ if null_cond_clone_zero:
436
+ negative_embeds = torch.zeros_like(prompt_embeds)
437
+ negative_mask = prompt_mask.clone()
438
+ elif negative_prompt_embeds is not None:
439
+ negative_embeds, negative_mask = negative_prompt_embeds, negative_prompt_mask
440
+ else:
441
+ negative_embeds, negative_mask = self.encode_prompt(negative_prompt, device=device)
442
+
443
+ latents = self.prepare_latents(num_frames, height, width, generator, latents, device)
444
+ # Clean temporal-prefix condition (e.g. the ti2v refiner's first-frame
445
+ # latent): written into the latent before sampling and after every
446
+ # scheduler step, so the fixed frames stay clean while the rest denoise
447
+ # against them through attention.
448
+ if cond_latent is not None:
449
+ cond_latent = cond_latent.to(device=device, dtype=torch.float32)
450
+ latents = self._apply_inpainting(latents, cond_latent)
451
+ sigmas = compute_refiner_sigmas(
452
+ sigma_max=float(self.scheduler.sigma_max),
453
+ sigma_min=float(self.scheduler.sigma_min),
454
+ num_inference_steps=num_inference_steps,
455
+ shift=shift,
456
+ t_thresh=t_thresh,
457
+ tail_steps=refiner_sigma_tail_steps,
458
+ )
459
+ if sigmas is None:
460
+ self.scheduler.set_timesteps(num_inference_steps, device=device, shift=shift)
461
+ else:
462
+ self.scheduler.set_timesteps(
463
+ int(sigmas.shape[0]),
464
+ device=device,
465
+ sigmas=sigmas,
466
+ shift=1.0,
467
+ )
468
+ transformer_dtype = _module_dtype(self.transformer)
469
+ vae_restore_device: Optional[torch.device] = None
470
+ vae_offloaded = False
471
+ if offload_vae_during_denoise and output_type == "np" and self.vae is not None:
472
+ vae_device = _module_device(self.vae)
473
+ if vae_device.type == "cuda":
474
+ self.vae.to("cpu")
475
+ torch.cuda.empty_cache()
476
+ vae_restore_device = vae_device
477
+ vae_offloaded = True
478
+ cfg_latent_src = _group_global_rank(cfg_parallel_group, 0)
479
+ cfg_uncond_src = _group_global_rank(cfg_parallel_group, 1)
480
+ for i, timestep in enumerate(self.progress_bar(self.scheduler.timesteps)):
481
+ if cfg_parallel:
482
+ dist.broadcast(latents, src=cfg_latent_src, group=cfg_parallel_group)
483
+ timestep_batch = _transformer_timestep(timestep, transformer_dtype).expand(1).to(device)
484
+ latent_model_input = latents
485
+ if cfg_parallel:
486
+ if cfg_parallel_rank == 0:
487
+ branch_embeds = prompt_embeds
488
+ branch_mask = prompt_mask
489
+ branch_name = "transformer.cond"
490
+ else:
491
+ branch_embeds = negative_embeds
492
+ branch_mask = negative_mask
493
+ branch_name = "transformer.uncond"
494
+ branch_model_input = branch_embeds.to(transformer_dtype)
495
+ with _transformer_autocast(device, transformer_dtype):
496
+ branch_noise_pred = self.transformer(
497
+ latent_model_input,
498
+ timestep_batch,
499
+ branch_model_input,
500
+ encoder_attention_mask=branch_mask,
501
+ return_dict=False,
502
+ )[0].float()
503
+ if cfg_parallel_rank == 0:
504
+ noise_pred = branch_noise_pred
505
+ noise_pred_uncond = torch.empty_like(noise_pred)
506
+ else:
507
+ noise_pred_uncond = branch_noise_pred
508
+ dist.broadcast(noise_pred_uncond, src=cfg_uncond_src, group=cfg_parallel_group)
509
+ if cfg_parallel_rank != 0:
510
+ continue
511
+ noise_pred = noise_pred_uncond + guidance_scale * (noise_pred - noise_pred_uncond)
512
+ else:
513
+ prompt_model_input = prompt_embeds.to(transformer_dtype)
514
+ if do_cfg and effective_batch_cfg:
515
+ negative_model_input = negative_embeds.to(transformer_dtype)
516
+ cfg_embeds, cfg_mask = batch_cfg_prompt_inputs(
517
+ prompt_model_input,
518
+ prompt_mask,
519
+ negative_model_input,
520
+ negative_mask,
521
+ null_cond_clone_zero=False,
522
+ )
523
+ cfg_latents = torch.cat([latent_model_input, latent_model_input], dim=0)
524
+ cfg_timesteps = torch.cat([timestep_batch, timestep_batch], dim=0)
525
+ with _transformer_autocast(device, transformer_dtype):
526
+ noise_batched = self.transformer(
527
+ cfg_latents,
528
+ cfg_timesteps,
529
+ cfg_embeds,
530
+ encoder_attention_mask=cfg_mask,
531
+ return_dict=False,
532
+ )[0].float()
533
+ noise_pred, noise_pred_uncond = noise_batched.chunk(2, dim=0)
534
+ noise_pred = noise_pred_uncond + guidance_scale * (
535
+ noise_pred - noise_pred_uncond
536
+ )
537
+ else:
538
+ with _transformer_autocast(device, transformer_dtype):
539
+ noise_pred = self.transformer(
540
+ latent_model_input,
541
+ timestep_batch,
542
+ prompt_model_input,
543
+ encoder_attention_mask=prompt_mask,
544
+ return_dict=False,
545
+ )[0].float()
546
+
547
+ if do_cfg and not effective_batch_cfg:
548
+ negative_model_input = negative_embeds.to(transformer_dtype)
549
+ with _transformer_autocast(device, transformer_dtype):
550
+ noise_pred_uncond = self.transformer(
551
+ latent_model_input,
552
+ timestep_batch,
553
+ negative_model_input,
554
+ encoder_attention_mask=negative_mask,
555
+ return_dict=False,
556
+ )[0].float()
557
+ noise_pred = noise_pred_uncond + guidance_scale * (noise_pred - noise_pred_uncond)
558
+
559
+ latents = self.scheduler.step(
560
+ noise_pred,
561
+ timestep,
562
+ latents,
563
+ return_dict=False,
564
+ generator=generator,
565
+ )[0]
566
+ if cond_latent is not None:
567
+ latents = self._apply_inpainting(latents, cond_latent)
568
+
569
+ if cfg_parallel:
570
+ dist.barrier(group=cfg_parallel_group)
571
+ if cfg_parallel_rank != 0:
572
+ frames = latents if output_type == "latent" else []
573
+ self.maybe_free_model_hooks()
574
+ if not return_dict:
575
+ return (frames,)
576
+ return LingBotVideoPipelineOutput(frames=frames)
577
+
578
+ if output_type == "latent":
579
+ frames = latents
580
+ elif output_type == "np":
581
+ if vae_offloaded and vae_restore_device is not None:
582
+ self.vae.to(device=vae_restore_device)
583
+ torch.cuda.empty_cache()
584
+ frames = self._decode_latents(latents)
585
+ else:
586
+ raise ValueError(f"Unsupported output_type: {output_type}")
587
+
588
+ self.maybe_free_model_hooks()
589
+ if not return_dict:
590
+ return (frames,)
591
+ return LingBotVideoPipelineOutput(frames=frames)
lingbot_video/pipeline_lingbot_video_i2v.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from typing import Any, Optional, Tuple, Union
5
+
6
+ import numpy as np
7
+ import torch
8
+ import torch.distributed as dist
9
+ import torch.nn.functional as F
10
+ from PIL import Image
11
+ from diffusers.utils.torch_utils import randn_tensor
12
+
13
+ from .pipeline_lingbot_video import (
14
+ DEFAULT_NEGATIVE_PROMPT,
15
+ LingBotVideoPipeline,
16
+ LingBotVideoPipelineOutput,
17
+ _group_global_rank,
18
+ _module_device,
19
+ _module_dtype,
20
+ _transformer_autocast,
21
+ _transformer_timestep,
22
+ )
23
+ from .utils import batch_cfg_prompt_inputs
24
+
25
+
26
+ IMAGE_MIN_TOKEN_NUM = 4
27
+ IMAGE_MAX_TOKEN_NUM = 16384
28
+ MAX_RATIO = 200
29
+ SPATIAL_MERGE_SIZE = 2
30
+
31
+
32
+ def _round_by_factor(number: float, factor: int) -> int:
33
+ return round(number / factor) * factor
34
+
35
+
36
+ def _ceil_by_factor(number: float, factor: int) -> int:
37
+ return math.ceil(number / factor) * factor
38
+
39
+
40
+ def _floor_by_factor(number: float, factor: int) -> int:
41
+ return math.floor(number / factor) * factor
42
+
43
+
44
+ def smart_resize(
45
+ height: int,
46
+ width: int,
47
+ factor: int,
48
+ min_pixels: Optional[int] = None,
49
+ max_pixels: Optional[int] = None,
50
+ ) -> Tuple[int, int]:
51
+ max_pixels = max_pixels if max_pixels is not None else IMAGE_MAX_TOKEN_NUM * factor**2
52
+ min_pixels = min_pixels if min_pixels is not None else IMAGE_MIN_TOKEN_NUM * factor**2
53
+ if max_pixels < min_pixels:
54
+ raise ValueError("max_pixels must be greater than or equal to min_pixels.")
55
+ if max(height, width) / min(height, width) > MAX_RATIO:
56
+ raise ValueError(f"absolute aspect ratio must be smaller than {MAX_RATIO}.")
57
+
58
+ resized_height = max(factor, _round_by_factor(height, factor))
59
+ resized_width = max(factor, _round_by_factor(width, factor))
60
+ if resized_height * resized_width > max_pixels:
61
+ beta = math.sqrt((height * width) / max_pixels)
62
+ resized_height = _floor_by_factor(height / beta, factor)
63
+ resized_width = _floor_by_factor(width / beta, factor)
64
+ elif resized_height * resized_width < min_pixels:
65
+ beta = math.sqrt(min_pixels / (height * width))
66
+ resized_height = _ceil_by_factor(height * beta, factor)
67
+ resized_width = _ceil_by_factor(width * beta, factor)
68
+ return resized_height, resized_width
69
+
70
+
71
+ def _pixel_tensor_to_pil(pixel: torch.Tensor) -> Image.Image:
72
+ """Match torchvision.transforms.ToPILImage for a float CHW image in [0, 1]."""
73
+ frame = pixel[0, :, 0].detach().cpu().clamp(0, 1)
74
+ array = frame.permute(1, 2, 0).mul(255).byte().numpy()
75
+ return Image.fromarray(array, mode="RGB")
76
+
77
+
78
+ class LingBotVideoImageToVideoPipeline(LingBotVideoPipeline):
79
+ """LingBotVideo ti2v pipeline.
80
+
81
+ The condition frame is used twice: as visual input for Qwen3-VL and as a
82
+ clean latent that is written into the beginning of the diffusion latent
83
+ before sampling and after every scheduler step.
84
+ """
85
+
86
+ def preprocess_image(self, image: Image.Image, height: int, width: int) -> torch.Tensor:
87
+ if image is None:
88
+ raise ValueError("`image` is required when `image_tensor` is not provided.")
89
+ raw = torch.from_numpy(np.array(image.convert("RGB"))).permute(2, 0, 1).unsqueeze(0).contiguous()
90
+ old_h, old_w = raw.shape[-2:]
91
+ scale = max(height / old_h, width / old_w)
92
+ new_h = max(math.ceil(old_h * scale), height)
93
+ new_w = max(math.ceil(old_w * scale), width)
94
+ resized = F.interpolate(raw, size=(new_h, new_w), mode="bilinear", align_corners=False)
95
+ top = int(round((new_h - height) / 2.0))
96
+ left = int(round((new_w - width) / 2.0))
97
+ cropped = resized[:, :, top : top + height, left : left + width].float() / 255.0
98
+ return cropped.unsqueeze(2)
99
+
100
+ def _vision_patch_size(self) -> int:
101
+ for obj in (
102
+ getattr(getattr(self.text_encoder, "config", None), "vision_config", None),
103
+ getattr(getattr(self.processor, "image_processor", None), "config", None),
104
+ getattr(self.processor, "image_processor", None),
105
+ ):
106
+ patch = getattr(obj, "patch_size", None)
107
+ if patch is not None:
108
+ return int(patch)
109
+ return 16
110
+
111
+ def _vlm_image(self, pixel: torch.Tensor) -> Image.Image:
112
+ image = _pixel_tensor_to_pil(pixel)
113
+ patch_factor = self._vision_patch_size() * SPATIAL_MERGE_SIZE
114
+ width, height = image.size
115
+ resized_height, resized_width = smart_resize(height, width, factor=patch_factor)
116
+ return image.resize((resized_width, resized_height))
117
+
118
+ @torch.no_grad()
119
+ def encode_image_latent(
120
+ self,
121
+ pixel: torch.Tensor,
122
+ generator: Optional[torch.Generator] = None,
123
+ ) -> torch.Tensor:
124
+ if self.vae is None:
125
+ raise ValueError("`vae` is required to encode image latents.")
126
+ device = _module_device(self.vae)
127
+ pixel = pixel.to(device=device, dtype=torch.float32)
128
+ norm_pixel = (pixel - 0.5) / 0.5
129
+ with torch.autocast("cuda", dtype=torch.bfloat16, enabled=device.type == "cuda"):
130
+ latents = self.vae.encode(norm_pixel).latent_dist.sample(generator)
131
+
132
+ mean = torch.tensor(self.vae.config.latents_mean, device=latents.device, dtype=torch.float32)
133
+ std_inv = 1.0 / torch.tensor(
134
+ self.vae.config.latents_std, device=latents.device, dtype=torch.float32
135
+ )
136
+ mean = mean.view(1, -1, 1, 1, 1)
137
+ std_inv = std_inv.view(1, -1, 1, 1, 1)
138
+ return (latents.float() - mean) * std_inv
139
+
140
+ @torch.no_grad()
141
+ def __call__(
142
+ self,
143
+ prompt: str,
144
+ image: Optional[Image.Image] = None,
145
+ image_tensor: Optional[torch.Tensor] = None,
146
+ cond_latent: Optional[torch.Tensor] = None,
147
+ negative_prompt: str = DEFAULT_NEGATIVE_PROMPT,
148
+ height: int = 480,
149
+ width: int = 480,
150
+ num_frames: int = 81,
151
+ num_inference_steps: int = 40,
152
+ guidance_scale: float = 6.0,
153
+ shift: float = 3.0,
154
+ generator: Optional[torch.Generator] = None,
155
+ latents: Optional[torch.Tensor] = None,
156
+ prompt_embeds: Optional[torch.Tensor] = None,
157
+ prompt_mask: Optional[torch.Tensor] = None,
158
+ negative_prompt_embeds: Optional[torch.Tensor] = None,
159
+ negative_prompt_mask: Optional[torch.Tensor] = None,
160
+ output_type: str = "np",
161
+ cfg_parallel_group: Optional[Any] = None,
162
+ batch_cfg: bool = False,
163
+ null_cond_clone_zero: bool = False,
164
+ return_dict: bool = True,
165
+ ) -> Union[LingBotVideoPipelineOutput, Tuple[Union[list, torch.Tensor]]]:
166
+ self.check_inputs(height, width, num_frames)
167
+ if self.transformer is None or self.scheduler is None:
168
+ raise ValueError("`transformer` and `scheduler` are required for generation.")
169
+
170
+ device = self._execution_device
171
+ do_cfg = guidance_scale > 1.0
172
+ requested_batch_cfg = bool(batch_cfg)
173
+ effective_batch_cfg = requested_batch_cfg and do_cfg
174
+ self._last_batch_cfg_requested = requested_batch_cfg
175
+ self._last_effective_batch_cfg = effective_batch_cfg
176
+ self._last_batch_cfg_fallback_reason = None
177
+
178
+ cfg_parallel = cfg_parallel_group is not None
179
+ if cfg_parallel and effective_batch_cfg:
180
+ raise ValueError("`cfg_parallel_group` and `batch_cfg` are mutually exclusive.")
181
+ cfg_parallel_rank = 0
182
+ cfg_parallel_world_size = 1
183
+ if cfg_parallel:
184
+ if not dist.is_available() or not dist.is_initialized():
185
+ raise ValueError("`cfg_parallel_group` requires an initialized process group.")
186
+ if not do_cfg:
187
+ raise ValueError("CFG parallel requires `guidance_scale > 1.0`.")
188
+ cfg_parallel_rank = dist.get_rank(cfg_parallel_group)
189
+ cfg_parallel_world_size = dist.get_world_size(cfg_parallel_group)
190
+ if cfg_parallel_world_size != 2:
191
+ raise ValueError(
192
+ f"CFG parallel currently requires exactly 2 ranks, got {cfg_parallel_world_size}."
193
+ )
194
+
195
+ pixel = image_tensor if image_tensor is not None else self.preprocess_image(image, height, width)
196
+ pixel = pixel.to(device=device, dtype=torch.float32)
197
+ vlm_image = self._vlm_image(pixel)
198
+
199
+ if prompt_embeds is not None:
200
+ if prompt_mask is None:
201
+ raise ValueError("`prompt_mask` is required when `prompt_embeds` is provided.")
202
+ prompt_embeds = prompt_embeds.to(device=device)
203
+ prompt_mask = prompt_mask.to(device=device)
204
+ if negative_prompt_embeds is not None:
205
+ if negative_prompt_mask is None:
206
+ raise ValueError(
207
+ "`negative_prompt_mask` is required when `negative_prompt_embeds` is provided."
208
+ )
209
+ negative_prompt_embeds = negative_prompt_embeds.to(device=device)
210
+ negative_prompt_mask = negative_prompt_mask.to(device=device)
211
+
212
+ if cfg_parallel and cfg_parallel_rank == 1:
213
+ if negative_prompt_embeds is not None:
214
+ negative_embeds, negative_mask = negative_prompt_embeds, negative_prompt_mask
215
+ else:
216
+ negative_embeds, negative_mask = self.encode_prompt(
217
+ negative_prompt, images=[vlm_image], device=device
218
+ )
219
+ prompt_embeds = prompt_mask = None
220
+ else:
221
+ if prompt_embeds is None:
222
+ prompt_embeds, prompt_mask = self.encode_prompt(prompt, images=[vlm_image], device=device)
223
+ if do_cfg and not cfg_parallel:
224
+ if null_cond_clone_zero:
225
+ negative_embeds = torch.zeros_like(prompt_embeds)
226
+ negative_mask = prompt_mask.clone()
227
+ elif negative_prompt_embeds is not None:
228
+ negative_embeds, negative_mask = negative_prompt_embeds, negative_prompt_mask
229
+ else:
230
+ negative_embeds, negative_mask = self.encode_prompt(
231
+ negative_prompt, images=[vlm_image], device=device
232
+ )
233
+
234
+ if cond_latent is None:
235
+ cond_latent = self.encode_image_latent(pixel, generator=generator)
236
+ cond_latent = cond_latent.to(device=device, dtype=torch.float32)
237
+
238
+ latents = self.prepare_latents(num_frames, height, width, generator, latents, device)
239
+ latents = self._apply_inpainting(latents, cond_latent)
240
+ self.scheduler.set_timesteps(num_inference_steps, device=device, shift=shift)
241
+ transformer_dtype = _module_dtype(self.transformer)
242
+ cfg_latent_src = _group_global_rank(cfg_parallel_group, 0)
243
+ cfg_uncond_src = _group_global_rank(cfg_parallel_group, 1)
244
+
245
+ for i, timestep in enumerate(self.progress_bar(self.scheduler.timesteps)):
246
+ if cfg_parallel:
247
+ dist.broadcast(latents, src=cfg_latent_src, group=cfg_parallel_group)
248
+ timestep_batch = _transformer_timestep(timestep, transformer_dtype).expand(1).to(device)
249
+ latent_model_input = latents
250
+ if cfg_parallel:
251
+ if cfg_parallel_rank == 0:
252
+ branch_embeds = prompt_embeds
253
+ branch_mask = prompt_mask
254
+ else:
255
+ branch_embeds = negative_embeds
256
+ branch_mask = negative_mask
257
+ branch_model_input = branch_embeds.to(transformer_dtype)
258
+ with _transformer_autocast(device, transformer_dtype):
259
+ branch_noise_pred = self.transformer(
260
+ latent_model_input,
261
+ timestep_batch,
262
+ branch_model_input,
263
+ encoder_attention_mask=branch_mask,
264
+ return_dict=False,
265
+ )[0].float()
266
+ if cfg_parallel_rank == 0:
267
+ noise_pred = branch_noise_pred
268
+ noise_pred_uncond = torch.empty_like(noise_pred)
269
+ else:
270
+ noise_pred_uncond = branch_noise_pred
271
+ dist.broadcast(noise_pred_uncond, src=cfg_uncond_src, group=cfg_parallel_group)
272
+ if cfg_parallel_rank != 0:
273
+ continue
274
+ noise_pred = noise_pred_uncond + guidance_scale * (noise_pred - noise_pred_uncond)
275
+ else:
276
+ prompt_model_input = prompt_embeds.to(transformer_dtype)
277
+ if do_cfg and effective_batch_cfg:
278
+ negative_model_input = negative_embeds.to(transformer_dtype)
279
+ cfg_embeds, cfg_mask = batch_cfg_prompt_inputs(
280
+ prompt_model_input,
281
+ prompt_mask,
282
+ negative_model_input,
283
+ negative_mask,
284
+ null_cond_clone_zero=null_cond_clone_zero,
285
+ )
286
+ cfg_latents = torch.cat([latent_model_input, latent_model_input], dim=0)
287
+ cfg_timesteps = torch.cat([timestep_batch, timestep_batch], dim=0)
288
+ with _transformer_autocast(device, transformer_dtype):
289
+ noise_batched = self.transformer(
290
+ cfg_latents,
291
+ cfg_timesteps,
292
+ cfg_embeds,
293
+ encoder_attention_mask=cfg_mask,
294
+ return_dict=False,
295
+ )[0].float()
296
+ noise_pred, noise_pred_uncond = noise_batched.chunk(2, dim=0)
297
+ noise_pred = noise_pred_uncond + guidance_scale * (
298
+ noise_pred - noise_pred_uncond
299
+ )
300
+ else:
301
+ with _transformer_autocast(device, transformer_dtype):
302
+ noise_pred = self.transformer(
303
+ latent_model_input,
304
+ timestep_batch,
305
+ prompt_model_input,
306
+ encoder_attention_mask=prompt_mask,
307
+ return_dict=False,
308
+ )[0].float()
309
+
310
+ if do_cfg:
311
+ negative_model_input = negative_embeds.to(transformer_dtype)
312
+ with _transformer_autocast(device, transformer_dtype):
313
+ noise_pred_uncond = self.transformer(
314
+ latent_model_input,
315
+ timestep_batch,
316
+ negative_model_input,
317
+ encoder_attention_mask=negative_mask,
318
+ return_dict=False,
319
+ )[0].float()
320
+ noise_pred = noise_pred_uncond + guidance_scale * (
321
+ noise_pred - noise_pred_uncond
322
+ )
323
+
324
+ latents = self.scheduler.step(
325
+ noise_pred,
326
+ timestep,
327
+ latents,
328
+ return_dict=False,
329
+ generator=generator,
330
+ )[0]
331
+ latents = self._apply_inpainting(latents, cond_latent)
332
+
333
+ if cfg_parallel:
334
+ dist.barrier(group=cfg_parallel_group)
335
+ if cfg_parallel_rank != 0:
336
+ frames = latents if output_type == "latent" else []
337
+ self.maybe_free_model_hooks()
338
+ if not return_dict:
339
+ return (frames,)
340
+ return LingBotVideoPipelineOutput(frames=frames)
341
+
342
+ if output_type == "latent":
343
+ frames = latents
344
+ elif output_type == "np":
345
+ frames = self._decode_latents(latents)
346
+ else:
347
+ raise ValueError(f"Unsupported output_type: {output_type}")
348
+
349
+ self.maybe_free_model_hooks()
350
+ if not return_dict:
351
+ return (frames,)
352
+ return LingBotVideoPipelineOutput(frames=frames)
lingbot_video/runner.py ADDED
@@ -0,0 +1,1401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import gc
5
+ import json
6
+ import logging
7
+ import os
8
+ import sys
9
+ import warnings
10
+ from contextlib import contextmanager
11
+ from datetime import timedelta
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import numpy as np
16
+ import torch
17
+ import torch.distributed as dist
18
+ from torch.distributed.device_mesh import DeviceMesh
19
+ from PIL import Image
20
+
21
+ REPO_ROOT = Path(__file__).resolve().parents[1]
22
+ sys.path.insert(0, str(REPO_ROOT))
23
+
24
+
25
+ def _env_flag(name: str, default: bool = False) -> bool:
26
+ raw = os.environ.get(name)
27
+ if raw is None:
28
+ return default
29
+ return raw.lower() in {"1", "true", "yes", "on"}
30
+
31
+
32
+ def _configure_concise_import_logs() -> None:
33
+ if _env_flag("LINGBOT_VERBOSE_LOGS"):
34
+ return
35
+ os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
36
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
37
+ os.environ.setdefault("DIFFUSERS_VERBOSITY", "error")
38
+ os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
39
+ warnings.filterwarnings(
40
+ "ignore",
41
+ message=r"Unable to import `torchao` Tensor objects.*",
42
+ )
43
+ warnings.filterwarnings(
44
+ "ignore",
45
+ message=r"`enable_parallelism` is an experimental feature.*",
46
+ )
47
+ warnings.filterwarnings(
48
+ "ignore",
49
+ message=r"barrier\(\): using the device under current context.*",
50
+ )
51
+ logging.getLogger(
52
+ "sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe_triton_config"
53
+ ).setLevel(logging.ERROR)
54
+
55
+
56
+ _configure_concise_import_logs()
57
+
58
+ from lingbot_video.inference_backend import ( # noqa: E402
59
+ resolve_backend_engine,
60
+ resolve_negative_prompt_arg,
61
+ )
62
+
63
+ try:
64
+ import transformers as _transformers
65
+ import transformers.cache_utils as _transformers_cache_utils
66
+ except Exception as exc: # pragma: no cover - deployment dependency guard
67
+ _transformers = None
68
+ _transformers_cache_utils = None
69
+ _TRANSFORMERS_IMPORT_ERROR = exc
70
+ else:
71
+ _TRANSFORMERS_IMPORT_ERROR = None
72
+
73
+ try:
74
+ import diffusers.utils.import_utils as _diffusers_import_utils
75
+ except Exception as exc: # pragma: no cover - deployment dependency guard
76
+ _diffusers_import_utils = None
77
+ _DIFFUSERS_IMPORT_UTILS_ERROR = exc
78
+ else:
79
+ _DIFFUSERS_IMPORT_UTILS_ERROR = None
80
+
81
+ try:
82
+ from diffusers.utils import logging as _diffusers_logging
83
+ except Exception:
84
+ _diffusers_logging = None
85
+
86
+ try:
87
+ from transformers.utils import logging as _transformers_logging
88
+ except Exception:
89
+ _transformers_logging = None
90
+
91
+ try:
92
+ import huggingface_hub.utils as _hf_hub_utils
93
+ except Exception:
94
+ _hf_hub_utils = None
95
+
96
+
97
+ def _install_sglang_import_shims() -> None:
98
+ if _transformers is not None and _transformers_cache_utils is not None:
99
+ hybrid_cache = getattr(_transformers_cache_utils, "HybridCache", None)
100
+ if hybrid_cache is None:
101
+ hybrid_cache = getattr(_transformers_cache_utils, "DynamicCache", object)
102
+ _transformers_cache_utils.HybridCache = hybrid_cache
103
+ import_structure = getattr(_transformers, "_import_structure", {})
104
+ cache_exports = import_structure.setdefault("cache_utils", [])
105
+ if "HybridCache" not in cache_exports:
106
+ cache_exports.append("HybridCache")
107
+ _transformers.HybridCache = hybrid_cache
108
+ if _diffusers_import_utils is not None:
109
+ _diffusers_import_utils._peft_available = False
110
+
111
+
112
+ _install_sglang_import_shims()
113
+
114
+ try:
115
+ from lingbot_video.fsdp_inference import (
116
+ apply_fsdp_inference,
117
+ init_fsdp_inference_mesh,
118
+ )
119
+ from lingbot_video.model_paths import (
120
+ effective_refiner_model_dir,
121
+ model_component_dir,
122
+ )
123
+ from lingbot_video.pipeline_lingbot_video import (
124
+ DEFAULT_NEGATIVE_PROMPT,
125
+ DEFAULT_NEGATIVE_PROMPT_IMAGE,
126
+ LingBotVideoPipeline,
127
+ )
128
+ from lingbot_video.pipeline_lingbot_video_i2v import (
129
+ LingBotVideoImageToVideoPipeline,
130
+ )
131
+ from lingbot_video.utils import (
132
+ caption_from_sample,
133
+ load_first_frame_condition_tensor,
134
+ load_refiner_video_tensor,
135
+ num_frames_from_duration,
136
+ prepare_refiner_latent,
137
+ )
138
+ except Exception as exc: # pragma: no cover - reported when generation is attempted
139
+ DEFAULT_NEGATIVE_PROMPT = ""
140
+ DEFAULT_NEGATIVE_PROMPT_IMAGE = ""
141
+ LingBotVideoPipeline = None
142
+ LingBotVideoImageToVideoPipeline = None
143
+ apply_fsdp_inference = None
144
+ init_fsdp_inference_mesh = None
145
+ effective_refiner_model_dir = None
146
+ model_component_dir = None
147
+ caption_from_sample = None
148
+ load_first_frame_condition_tensor = None
149
+ load_refiner_video_tensor = None
150
+ num_frames_from_duration = None
151
+ prepare_refiner_latent = None
152
+ _LINGBOT_PIPELINE_IMPORT_ERROR = exc
153
+ else:
154
+ _LINGBOT_PIPELINE_IMPORT_ERROR = None
155
+
156
+ try:
157
+ from lingbot_video.transformer_lingbot_video import (
158
+ LingBotVideoTransformer3DModel,
159
+ )
160
+ except Exception as exc: # pragma: no cover - reported when generation is attempted
161
+ LingBotVideoTransformer3DModel = None
162
+ _TRANSFORMER_IMPORT_ERROR = exc
163
+ else:
164
+ _TRANSFORMER_IMPORT_ERROR = None
165
+
166
+ try:
167
+ from diffusers import ContextParallelConfig
168
+ from diffusers.hooks.context_parallel import apply_context_parallel
169
+ from diffusers.models._modeling_parallel import ParallelConfig
170
+ from diffusers.models.attention import AttentionModuleMixin
171
+ from diffusers.models.attention_processor import Attention, MochiAttention
172
+ from diffusers.utils import export_to_video
173
+ except Exception as exc: # pragma: no cover - reported by the selected engine
174
+ ContextParallelConfig = None
175
+ ParallelConfig = None
176
+ Attention = None
177
+ AttentionModuleMixin = None
178
+ MochiAttention = None
179
+ apply_context_parallel = None
180
+ export_to_video = None
181
+ _DIFFUSERS_IMPORT_ERROR = exc
182
+ else:
183
+ _DIFFUSERS_IMPORT_ERROR = None
184
+
185
+
186
+ def _cuda_sdp_state() -> dict[str, bool | None]:
187
+ state: dict[str, bool | None] = {}
188
+ cuda_backends = getattr(torch.backends, "cuda", None)
189
+ if cuda_backends is None:
190
+ return state
191
+ getters = {
192
+ "flash": "flash_sdp_enabled",
193
+ "mem_efficient": "mem_efficient_sdp_enabled",
194
+ "math": "math_sdp_enabled",
195
+ "cudnn": "cudnn_sdp_enabled",
196
+ }
197
+ for key, getter in getters.items():
198
+ fn = getattr(cuda_backends, getter, None)
199
+ if fn is None:
200
+ state[key] = None
201
+ continue
202
+ try:
203
+ state[key] = bool(fn())
204
+ except Exception:
205
+ state[key] = None
206
+ return state
207
+
208
+
209
+ def _restore_cuda_sdp_state(state: dict[str, bool | None]) -> None:
210
+ cuda_backends = getattr(torch.backends, "cuda", None)
211
+ if cuda_backends is None:
212
+ return
213
+ setters = {
214
+ "flash": "enable_flash_sdp",
215
+ "mem_efficient": "enable_mem_efficient_sdp",
216
+ "math": "enable_math_sdp",
217
+ "cudnn": "enable_cudnn_sdp",
218
+ }
219
+ for key, setter in setters.items():
220
+ value = state.get(key)
221
+ if value is None:
222
+ continue
223
+ fn = getattr(cuda_backends, setter, None)
224
+ if fn is None:
225
+ continue
226
+ try:
227
+ fn(value)
228
+ except Exception:
229
+ pass
230
+
231
+
232
+ _BASELINE_CUDA_SDP_STATE = _cuda_sdp_state()
233
+
234
+
235
+ RESOLUTION_BUCKETS: dict[str, dict[str, tuple[int, int]]] = {
236
+ "192p": {
237
+ "1:1": (192, 192),
238
+ "9:16": (192, 320),
239
+ "16:9": (320, 192),
240
+ "3:4": (192, 256),
241
+ "4:3": (256, 192),
242
+ },
243
+ "480p": {
244
+ "1:1": (480, 480),
245
+ "9:16": (480, 832),
246
+ "16:9": (832, 480),
247
+ "3:4": (480, 640),
248
+ "4:3": (640, 480),
249
+ },
250
+ "720p": {
251
+ "1:1": (736, 736),
252
+ "9:16": (736, 1280),
253
+ "16:9": (1280, 736),
254
+ "3:4": (736, 960),
255
+ "4:3": (960, 736),
256
+ },
257
+ "1080p": {
258
+ "1:1": (1088, 1088),
259
+ "9:16": (1088, 1920),
260
+ "16:9": (1920, 1088),
261
+ "3:4": (1088, 1440),
262
+ "4:3": (1440, 1088),
263
+ },
264
+ "2k": {
265
+ "1:1": (1440, 1440),
266
+ "9:16": (1440, 2560),
267
+ "16:9": (2560, 1440),
268
+ "3:4": (1440, 1920),
269
+ "4:3": (1920, 1440),
270
+ },
271
+ "4k": {
272
+ "1:1": (2176, 2176),
273
+ "9:16": (2176, 3840),
274
+ "16:9": (3840, 2176),
275
+ "3:4": (2176, 2880),
276
+ "4:3": (2880, 2176),
277
+ },
278
+ }
279
+
280
+
281
+ def _distributed_env() -> tuple[int, int, int]:
282
+ return (
283
+ int(os.environ.get("RANK", "0")),
284
+ int(os.environ.get("LOCAL_RANK", "0")),
285
+ int(os.environ.get("WORLD_SIZE", "1")),
286
+ )
287
+
288
+
289
+ def _default_device() -> torch.device:
290
+ if not torch.cuda.is_available():
291
+ return torch.device("cpu")
292
+ return torch.device("cuda", torch.cuda.current_device())
293
+
294
+
295
+ def _init_parallel(
296
+ cfg_degree: int,
297
+ context_degree: int,
298
+ enable_fsdp_inference: bool,
299
+ ) -> tuple[int, int, int, Any | None, DeviceMesh | None, int, int]:
300
+ rank, local_rank, world_size = _distributed_env()
301
+ if cfg_degree < 1 or context_degree < 1:
302
+ raise ValueError("Parallel degrees must be >= 1.")
303
+ expected_world_size = cfg_degree * context_degree
304
+ if expected_world_size <= 1:
305
+ if enable_fsdp_inference and world_size > 1:
306
+ if not torch.cuda.is_available():
307
+ raise RuntimeError("FSDP inference requires CUDA devices.")
308
+ torch.cuda.set_device(local_rank)
309
+ if not dist.is_initialized():
310
+ dist.init_process_group("nccl", timeout=timedelta(minutes=30))
311
+ return rank, local_rank, world_size, None, None, 0, 0
312
+ if torch.cuda.is_available():
313
+ torch.cuda.set_device(0)
314
+ return rank, local_rank, world_size, None, None, 0, 0
315
+ if world_size != expected_world_size:
316
+ raise ValueError(
317
+ f"Parallel topology cfg={cfg_degree}, context={context_degree} requires "
318
+ f"WORLD_SIZE={expected_world_size}, got {world_size}."
319
+ )
320
+ if not torch.cuda.is_available():
321
+ raise RuntimeError("Distributed parallel inference requires CUDA devices.")
322
+ torch.cuda.set_device(local_rank)
323
+ if not dist.is_initialized():
324
+ dist.init_process_group("nccl", timeout=timedelta(minutes=30))
325
+
326
+ cfg_branch_rank = rank // context_degree
327
+ context_rank = rank % context_degree
328
+ cfg_parallel_group = None
329
+ if cfg_degree > 1:
330
+ lane_groups = [
331
+ dist.new_group(
332
+ ranks=[cfg_rank * context_degree + context_rank for cfg_rank in range(cfg_degree)]
333
+ )
334
+ for context_rank in range(context_degree)
335
+ ]
336
+ cfg_parallel_group = lane_groups[context_rank]
337
+
338
+ context_mesh = None
339
+ if context_degree > 1 and cfg_degree > 1:
340
+ mesh_ranks = [
341
+ [
342
+ cfg_rank * context_degree + context_rank
343
+ for context_rank in range(context_degree)
344
+ ]
345
+ for cfg_rank in range(cfg_degree)
346
+ ]
347
+ context_mesh = DeviceMesh(
348
+ "cuda",
349
+ mesh_ranks,
350
+ mesh_dim_names=("ring", "ulysses"),
351
+ )
352
+
353
+ return rank, local_rank, world_size, cfg_parallel_group, context_mesh, cfg_branch_rank, context_rank
354
+
355
+ try:
356
+ from lingbot_video.native_backend import (
357
+ LingBotVideoNativePipeline,
358
+ register_lingbot_native_pipeline,
359
+ )
360
+ except Exception as exc: # pragma: no cover - only needed for --engine sglang-native
361
+ LingBotVideoNativePipeline = None
362
+ register_lingbot_native_pipeline = None
363
+ _NATIVE_BACKEND_IMPORT_ERROR = exc
364
+ else:
365
+ _NATIVE_BACKEND_IMPORT_ERROR = None
366
+ finally:
367
+ _restore_cuda_sdp_state(_BASELINE_CUDA_SDP_STATE)
368
+
369
+
370
+ def _parse_dtype(name: str) -> torch.dtype:
371
+ normalized = name.lower()
372
+ if normalized in {"bf16", "bfloat16"}:
373
+ return torch.bfloat16
374
+ if normalized in {"fp16", "float16"}:
375
+ return torch.float16
376
+ if normalized in {"fp32", "float32"}:
377
+ return torch.float32
378
+ raise ValueError(f"unsupported dtype: {name}")
379
+
380
+
381
+ def _dtype_name(dtype: torch.dtype) -> str:
382
+ return str(dtype).replace("torch.", "")
383
+
384
+
385
+ def _make_dtype_map(args: argparse.Namespace) -> dict[str, torch.dtype]:
386
+ default_dtype = _parse_dtype(args.default_dtype)
387
+ return {
388
+ "default": default_dtype,
389
+ "transformer": _parse_dtype(args.transformer_dtype),
390
+ "text_encoder": _parse_dtype(args.text_encoder_dtype),
391
+ "vae": _parse_dtype(args.vae_dtype),
392
+ }
393
+
394
+
395
+ def _make_default_image(height: int, width: int) -> Image.Image:
396
+ yy, xx = np.mgrid[0:height, 0:width]
397
+ red = (xx / max(width - 1, 1) * 255).astype(np.uint8)
398
+ green = (yy / max(height - 1, 1) * 255).astype(np.uint8)
399
+ blue = (((xx // 24 + yy // 24) % 2) * 180 + 40).astype(np.uint8)
400
+ return Image.fromarray(np.stack([red, green, blue], axis=-1), mode="RGB")
401
+
402
+
403
+ def _load_prompt_sample(path: Path) -> dict[str, Any]:
404
+ data = json.loads(path.read_text(encoding="utf-8"))
405
+ if isinstance(data, list):
406
+ if not data:
407
+ raise ValueError(f"`--prompt_json` is empty: {path}")
408
+ data = data[0]
409
+ if not isinstance(data, dict):
410
+ raise ValueError(f"`--prompt_json` must contain a dict or a non-empty list of dicts: {path}")
411
+ return data
412
+
413
+
414
+ def _caption_from_sample(sample: dict[str, Any]) -> str:
415
+ if caption_from_sample is not None:
416
+ return caption_from_sample(sample)
417
+ if "caption" in sample:
418
+ caption = sample["caption"]
419
+ else:
420
+ runtime_keys = {
421
+ "duration",
422
+ "fps",
423
+ "height",
424
+ "width",
425
+ "num_frames",
426
+ "resolution",
427
+ "ratio",
428
+ }
429
+ caption = {key: value for key, value in sample.items() if key not in runtime_keys}
430
+ if isinstance(caption, dict):
431
+ return str(caption.get("qwen_long_caption") or caption.get("comprehensive_description") or caption)
432
+ return str(caption)
433
+
434
+
435
+ def _height_width_from_bucket(resolution: str, ratio: str) -> tuple[int, int]:
436
+ if resolution not in RESOLUTION_BUCKETS:
437
+ choices = ", ".join(sorted(RESOLUTION_BUCKETS))
438
+ raise ValueError(f"unsupported resolution {resolution!r}; choices: {choices}")
439
+ ratios = RESOLUTION_BUCKETS[resolution]
440
+ if ratio not in ratios:
441
+ choices = ", ".join(sorted(ratios))
442
+ raise ValueError(f"unsupported ratio {ratio!r} for {resolution}; choices: {choices}")
443
+ return ratios[ratio]
444
+
445
+
446
+ def _module_dtype(module: Any) -> str | None:
447
+ if module is None:
448
+ return None
449
+ if hasattr(module, "dtype"):
450
+ dtype = getattr(module, "dtype")
451
+ if isinstance(dtype, torch.dtype):
452
+ return _dtype_name(dtype)
453
+ if isinstance(module, torch.nn.Module):
454
+ try:
455
+ return _dtype_name(next(module.parameters()).dtype)
456
+ except StopIteration:
457
+ return None
458
+ return None
459
+
460
+
461
+ def _component_dtypes(pipe: Any) -> dict[str, str | None]:
462
+ return {
463
+ "transformer": _module_dtype(getattr(pipe, "transformer", None)),
464
+ "text_encoder": _module_dtype(getattr(pipe, "text_encoder", None)),
465
+ "vae": _module_dtype(getattr(pipe, "vae", None)),
466
+ }
467
+
468
+
469
+ def _disable_external_progress_bars() -> None:
470
+ for logging_module in (_diffusers_logging, _transformers_logging):
471
+ fn = getattr(logging_module, "disable_progress_bar", None)
472
+ if callable(fn):
473
+ fn()
474
+ hf_fn = getattr(_hf_hub_utils, "disable_progress_bars", None)
475
+ if callable(hf_fn):
476
+ hf_fn()
477
+
478
+
479
+ def _configure_pipeline_logs(pipe: Any) -> None:
480
+ inner_pipe = _inner_diffusers_pipe(pipe)
481
+ set_progress_bar_config = getattr(inner_pipe, "set_progress_bar_config", None)
482
+ if callable(set_progress_bar_config):
483
+ set_progress_bar_config(
484
+ disable=not _show_progress(),
485
+ desc="denoising",
486
+ dynamic_ncols=True,
487
+ )
488
+
489
+
490
+ @contextmanager
491
+ def _patch_qwen3vl_from_pretrained():
492
+ try:
493
+ from transformers import Qwen3VLForConditionalGeneration
494
+ except Exception:
495
+ yield
496
+ return
497
+
498
+ original_from_pretrained = Qwen3VLForConditionalGeneration.from_pretrained
499
+ attn_implementation = os.environ.get("LINGBOT_QWEN_ATTN_IMPLEMENTATION", "flash_attention_3")
500
+
501
+ @classmethod
502
+ def patched_from_pretrained(cls, pretrained_model_name_or_path, *args, **kwargs):
503
+ if attn_implementation:
504
+ kwargs.setdefault("attn_implementation", attn_implementation)
505
+ if "torch_dtype" in kwargs and "dtype" not in kwargs:
506
+ kwargs["dtype"] = kwargs.pop("torch_dtype")
507
+ return original_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
508
+
509
+ Qwen3VLForConditionalGeneration.from_pretrained = patched_from_pretrained
510
+ try:
511
+ yield
512
+ finally:
513
+ Qwen3VLForConditionalGeneration.from_pretrained = original_from_pretrained
514
+
515
+
516
+ def _destroy_parallel_if_needed() -> None:
517
+ if dist.is_available() and dist.is_initialized():
518
+ dist.destroy_process_group()
519
+
520
+
521
+ def _sync_parallel_if_needed() -> None:
522
+ if dist.is_available() and dist.is_initialized():
523
+ dist.barrier()
524
+
525
+
526
+ def _current_rank() -> int:
527
+ if dist.is_available() and dist.is_initialized():
528
+ return dist.get_rank()
529
+ return 0
530
+
531
+
532
+ def _is_main_process() -> bool:
533
+ return _current_rank() == 0
534
+
535
+
536
+ def _show_progress() -> bool:
537
+ return _is_main_process() and not _env_flag("LINGBOT_QUIET_PROGRESS")
538
+
539
+
540
+ def _log_progress(message: str) -> None:
541
+ if _show_progress():
542
+ print(message, flush=True)
543
+
544
+
545
+ def _apply_fsdp_inference_if_requested(
546
+ pipe: Any,
547
+ enabled: bool,
548
+ mesh: DeviceMesh | None,
549
+ ) -> Any | None:
550
+ if not enabled:
551
+ return None
552
+ if apply_fsdp_inference is None:
553
+ raise RuntimeError("FSDP inference helpers are not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
554
+ inner_pipe = _inner_diffusers_pipe(pipe)
555
+ transformer = getattr(inner_pipe, "transformer", None)
556
+ if not isinstance(transformer, torch.nn.Module):
557
+ raise ValueError(
558
+ "FSDP inference requires a pipeline with a torch.nn.Module transformer."
559
+ )
560
+ _log_progress("applying FSDP inference sharding")
561
+ info = apply_fsdp_inference(transformer, mesh)
562
+ _log_progress(f"applied FSDP inference sharding: {info}")
563
+ return info
564
+
565
+
566
+ def _cache_prompt_conditions(
567
+ pipe: Any,
568
+ prompt: str,
569
+ negative_prompt: str,
570
+ *,
571
+ device: torch.device,
572
+ null_cond_clone_zero: bool,
573
+ images: list[Any] | None = None,
574
+ ) -> dict[str, torch.Tensor]:
575
+ encode_kwargs: dict[str, Any] = {"device": device}
576
+ if images is not None:
577
+ encode_kwargs["images"] = images
578
+ prompt_embeds, prompt_mask = pipe.encode_prompt(prompt, **encode_kwargs)
579
+ if null_cond_clone_zero:
580
+ negative_embeds = torch.zeros_like(prompt_embeds)
581
+ negative_mask = prompt_mask.clone()
582
+ else:
583
+ negative_embeds, negative_mask = pipe.encode_prompt(negative_prompt, **encode_kwargs)
584
+ return {
585
+ "prompt_embeds": prompt_embeds.detach().cpu(),
586
+ "prompt_mask": prompt_mask.detach().cpu(),
587
+ "negative_prompt_embeds": negative_embeds.detach().cpu(),
588
+ "negative_prompt_mask": negative_mask.detach().cpu(),
589
+ }
590
+
591
+
592
+ def _inner_diffusers_pipe(pipe: Any) -> Any:
593
+ return getattr(pipe, "diffusers_pipe", pipe)
594
+
595
+
596
+ def _cache_ti2v_prompt_conditions(
597
+ pipe: Any,
598
+ prompt: str,
599
+ negative_prompt: str,
600
+ image: Image.Image,
601
+ *,
602
+ height: int,
603
+ width: int,
604
+ device: torch.device,
605
+ null_cond_clone_zero: bool,
606
+ ) -> tuple[dict[str, torch.Tensor], torch.Tensor]:
607
+ condition_pipe = _inner_diffusers_pipe(pipe)
608
+ if not hasattr(condition_pipe, "preprocess_image") or not hasattr(condition_pipe, "_vlm_image"):
609
+ raise ValueError("TI2V condition reuse requires a LingBotVideo image-to-video pipeline.")
610
+ pixel = condition_pipe.preprocess_image(image, height, width).to(
611
+ device=device,
612
+ dtype=torch.float32,
613
+ )
614
+ vlm_image = condition_pipe._vlm_image(pixel)
615
+ return (
616
+ _cache_prompt_conditions(
617
+ condition_pipe,
618
+ prompt,
619
+ negative_prompt,
620
+ device=device,
621
+ null_cond_clone_zero=null_cond_clone_zero,
622
+ images=[vlm_image],
623
+ ),
624
+ pixel.detach().cpu(),
625
+ )
626
+
627
+
628
+ def _condition_call_kwargs(
629
+ cache: dict[str, torch.Tensor] | None,
630
+ device: torch.device,
631
+ ) -> dict[str, torch.Tensor]:
632
+ if not cache:
633
+ return {}
634
+ return {key: value.to(device=device) for key, value in cache.items()}
635
+
636
+
637
+ def _pipeline_class_for_mode(mode: str) -> Any:
638
+ if mode == "ti2v":
639
+ if LingBotVideoImageToVideoPipeline is None:
640
+ raise RuntimeError(
641
+ "LingBotVideoImageToVideoPipeline is not importable."
642
+ ) from _LINGBOT_PIPELINE_IMPORT_ERROR
643
+ return LingBotVideoImageToVideoPipeline
644
+ if LingBotVideoPipeline is None:
645
+ raise RuntimeError("LingBotVideoPipeline is not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
646
+ return LingBotVideoPipeline
647
+
648
+
649
+ def _load_transformer_component(
650
+ model_dir: Path,
651
+ transformer_subfolder: str,
652
+ dtype_map: dict[str, torch.dtype],
653
+ ) -> Any:
654
+ if LingBotVideoTransformer3DModel is None:
655
+ raise RuntimeError(
656
+ "LingBotVideoTransformer3DModel is not importable."
657
+ ) from _TRANSFORMER_IMPORT_ERROR
658
+ if model_component_dir is None:
659
+ raise RuntimeError("model path helpers are not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
660
+ model_component_dir(model_dir, transformer_subfolder)
661
+ transformer_dtype = dtype_map.get(
662
+ "transformer",
663
+ dtype_map.get("default", torch.float32),
664
+ )
665
+ _disable_external_progress_bars()
666
+ _log_progress(
667
+ f"loading transformer subfolder={transformer_subfolder} "
668
+ f"dtype={_dtype_name(transformer_dtype)} model_dir={model_dir}"
669
+ )
670
+ transformer = LingBotVideoTransformer3DModel.from_pretrained(
671
+ str(model_dir),
672
+ subfolder=transformer_subfolder,
673
+ torch_dtype=transformer_dtype,
674
+ )
675
+ _log_progress(f"loaded transformer subfolder={transformer_subfolder}")
676
+ return transformer
677
+
678
+
679
+ def _move_pipeline_aux_modules_to_device(pipe: Any, device: torch.device) -> Any:
680
+ inner_pipe = _inner_diffusers_pipe(pipe)
681
+ for name in ("text_encoder", "vae"):
682
+ module = getattr(inner_pipe, name, None)
683
+ if isinstance(module, torch.nn.Module):
684
+ _log_progress(f"moving {name} to {device}")
685
+ module.to(device)
686
+ return pipe
687
+
688
+
689
+ def _load_diffusers_pipe(
690
+ model_dir: Path,
691
+ dtype_map: dict[str, torch.dtype],
692
+ mode: str,
693
+ transformer_subfolder: str,
694
+ defer_transformer_to_device: bool = False,
695
+ ) -> Any:
696
+ pipeline_class = _pipeline_class_for_mode(mode)
697
+ transformer = _load_transformer_component(model_dir, transformer_subfolder, dtype_map)
698
+
699
+ _disable_external_progress_bars()
700
+ _log_progress(f"loading pipeline mode={mode} model_dir={model_dir}")
701
+ with _patch_qwen3vl_from_pretrained():
702
+ pipe = pipeline_class.from_pretrained(
703
+ str(model_dir),
704
+ transformer=transformer,
705
+ trust_remote_code=True,
706
+ torch_dtype=dtype_map,
707
+ )
708
+ _log_progress(f"loaded pipeline mode={mode}")
709
+ device = _default_device()
710
+ if defer_transformer_to_device:
711
+ return _move_pipeline_aux_modules_to_device(pipe, device)
712
+ _log_progress(f"moving pipeline to {device}")
713
+ return pipe.to(device)
714
+
715
+
716
+ def _load_sglang_native_pipe(
717
+ model_dir: Path,
718
+ dtype_map: dict[str, torch.dtype],
719
+ mode: str,
720
+ transformer_subfolder: str,
721
+ defer_transformer_to_device: bool = False,
722
+ ) -> Any:
723
+ if LingBotVideoNativePipeline is None or register_lingbot_native_pipeline is None:
724
+ raise RuntimeError(
725
+ "LingBotVideo SGLang native backend is not importable."
726
+ ) from _NATIVE_BACKEND_IMPORT_ERROR
727
+ _restore_cuda_sdp_state(_BASELINE_CUDA_SDP_STATE)
728
+ try:
729
+ register_lingbot_native_pipeline()
730
+ diffusers_pipe = _load_diffusers_pipe(
731
+ model_dir,
732
+ dtype_map,
733
+ mode=mode,
734
+ transformer_subfolder=transformer_subfolder,
735
+ defer_transformer_to_device=defer_transformer_to_device,
736
+ )
737
+ return LingBotVideoNativePipeline.from_diffusers_pipe(
738
+ diffusers_pipe,
739
+ model_path=model_dir,
740
+ )
741
+ finally:
742
+ _restore_cuda_sdp_state(_BASELINE_CUDA_SDP_STATE)
743
+
744
+
745
+ def _load_pipe(
746
+ args: argparse.Namespace,
747
+ dtype_map: dict[str, torch.dtype],
748
+ *,
749
+ defer_transformer_to_device: bool = False,
750
+ ):
751
+ model_dir = Path(args.model_dir).resolve()
752
+ if args.engine == "diffusers":
753
+ return (
754
+ _load_diffusers_pipe(
755
+ model_dir,
756
+ dtype_map,
757
+ mode=args.mode,
758
+ transformer_subfolder=args.transformer_subfolder,
759
+ defer_transformer_to_device=defer_transformer_to_device,
760
+ ),
761
+ "diffusers-reference",
762
+ )
763
+ if args.engine == "sglang-native":
764
+ return (
765
+ _load_sglang_native_pipe(
766
+ model_dir,
767
+ dtype_map,
768
+ mode=args.mode,
769
+ transformer_subfolder=args.transformer_subfolder,
770
+ defer_transformer_to_device=defer_transformer_to_device,
771
+ ),
772
+ "sglang-native",
773
+ )
774
+ raise ValueError(f"unsupported engine: {args.engine}")
775
+
776
+
777
+ def _refiner_model_available(
778
+ model_dir: str | None,
779
+ transformer_subfolder: str = "refiner",
780
+ ) -> bool:
781
+ if not model_dir:
782
+ return False
783
+ root = Path(model_dir)
784
+ if not root.is_dir():
785
+ return False
786
+ required = ("model_index.json", "scheduler", "text_encoder", "vae", transformer_subfolder)
787
+ return all((root / name).exists() for name in required)
788
+
789
+
790
+ def _refiner_skip_reason(
791
+ requested: bool,
792
+ model_dir: str | None,
793
+ transformer_subfolder: str = "refiner",
794
+ ) -> str | None:
795
+ if not requested:
796
+ return None
797
+ if not model_dir:
798
+ return "missing_refiner_model_dir"
799
+ if not _refiner_model_available(model_dir, transformer_subfolder):
800
+ return f"missing_refiner_component:{transformer_subfolder}"
801
+ return None
802
+
803
+
804
+ def _maybe_preload_refiner(
805
+ args: argparse.Namespace,
806
+ dtype_map: dict[str, torch.dtype],
807
+ device: torch.device,
808
+ context_parallel_rank: int,
809
+ context_parallel_mesh: DeviceMesh | None,
810
+ defer_transformer_to_device: bool,
811
+ ) -> dict[str, Any]:
812
+ requested = bool(args.run_refiner)
813
+ available = (
814
+ _refiner_model_available(args.refiner_model_dir, args.refiner_transformer_subfolder)
815
+ if requested
816
+ else False
817
+ )
818
+ state: dict[str, Any] = {
819
+ "refiner_requested": requested,
820
+ "refiner_available": available,
821
+ "refiner_skipped_reason": _refiner_skip_reason(
822
+ requested,
823
+ args.refiner_model_dir,
824
+ args.refiner_transformer_subfolder,
825
+ ),
826
+ "pipe": None,
827
+ "engine_name": None,
828
+ "component_dtypes": None,
829
+ }
830
+ if not requested or not available:
831
+ return state
832
+
833
+ refiner_args = argparse.Namespace(**vars(args))
834
+ refiner_args.model_dir = args.refiner_model_dir
835
+ refiner_args.transformer_subfolder = args.refiner_transformer_subfolder
836
+ refiner_args.vae_dtype = args.refiner_vae_dtype
837
+ # The refiner always samples through the text-only t2v pipeline; ti2v
838
+ # first-frame conditioning is injected as a clean frame-0 latent
839
+ # (`cond_latent`), not through the image-to-video pipeline.
840
+ refiner_args.mode = "t2v"
841
+ refiner_dtype_map = dict(dtype_map)
842
+ refiner_dtype_map["vae"] = _parse_dtype(args.refiner_vae_dtype)
843
+ refiner_pipe, refiner_engine_name = _load_pipe(
844
+ refiner_args,
845
+ refiner_dtype_map,
846
+ defer_transformer_to_device=defer_transformer_to_device,
847
+ )
848
+ refiner_component_dtypes = _component_dtypes(refiner_pipe)
849
+ expected_refiner_vae_dtype = _dtype_name(refiner_dtype_map["vae"])
850
+ if refiner_component_dtypes.get("vae") != expected_refiner_vae_dtype:
851
+ raise AssertionError(
852
+ f"Refiner VAE dtype mismatch: requested {expected_refiner_vae_dtype}, "
853
+ f"got {refiner_component_dtypes}"
854
+ )
855
+ if args.context_parallel_degree > 1:
856
+ _enable_context_parallel(
857
+ refiner_pipe.transformer,
858
+ args.context_parallel_degree,
859
+ args.context_parallel_ulysses_anything,
860
+ context_parallel_rank,
861
+ device,
862
+ context_parallel_mesh,
863
+ args.cfg_parallel_degree == 1,
864
+ )
865
+ state.update(
866
+ {
867
+ "pipe": refiner_pipe,
868
+ "engine_name": refiner_engine_name,
869
+ "component_dtypes": refiner_component_dtypes,
870
+ }
871
+ )
872
+ return state
873
+
874
+
875
+ def _extract_frames(result: Any) -> np.ndarray:
876
+ frames = result.frames if hasattr(result, "frames") else result[0]
877
+ if isinstance(frames, torch.Tensor):
878
+ tensor = frames.detach().cpu().float()
879
+ if tensor.ndim == 5:
880
+ # Accept B,T,C,H,W or B,C,T,H,W.
881
+ if tensor.shape[2] in (1, 3, 4):
882
+ tensor = tensor[0].permute(0, 2, 3, 1)
883
+ else:
884
+ tensor = tensor[0].permute(1, 2, 3, 0)
885
+ elif tensor.ndim == 4:
886
+ if tensor.shape[1] in (1, 3, 4):
887
+ tensor = tensor.permute(0, 2, 3, 1)
888
+ return tensor.numpy()
889
+ if isinstance(frames, list):
890
+ return np.asarray(frames[0])
891
+ return np.asarray(frames)
892
+
893
+
894
+ def _save_frames(
895
+ frames: np.ndarray,
896
+ mode: str,
897
+ output: Path,
898
+ fps: int,
899
+ ) -> None:
900
+ output.parent.mkdir(parents=True, exist_ok=True)
901
+
902
+ if mode == "t2i":
903
+ Image.fromarray((frames[0] * 255).clip(0, 255).astype(np.uint8)).save(output)
904
+ else:
905
+ if export_to_video is None:
906
+ raise RuntimeError("diffusers.utils.export_to_video is not importable.") from _DIFFUSERS_IMPORT_ERROR
907
+ export_to_video(frames, str(output), fps=fps)
908
+
909
+ print(f"saved {output} shape={tuple(frames.shape)}", flush=True)
910
+
911
+
912
+ def _enable_context_parallel(
913
+ transformer: torch.nn.Module,
914
+ degree: int,
915
+ ulysses_anything: bool,
916
+ rank: int,
917
+ device: torch.device,
918
+ mesh: DeviceMesh | None,
919
+ use_native_mesh: bool,
920
+ ) -> None:
921
+ if degree <= 1:
922
+ return
923
+ if (
924
+ ContextParallelConfig is None
925
+ or ParallelConfig is None
926
+ or apply_context_parallel is None
927
+ or Attention is None
928
+ or MochiAttention is None
929
+ or AttentionModuleMixin is None
930
+ ):
931
+ raise RuntimeError("diffusers context-parallel helpers are unavailable.") from _DIFFUSERS_IMPORT_ERROR
932
+ cp_config = ContextParallelConfig(
933
+ ulysses_degree=degree,
934
+ ulysses_anything=ulysses_anything,
935
+ )
936
+ if use_native_mesh:
937
+ transformer.enable_parallelism(config=cp_config)
938
+ return
939
+
940
+ if mesh is None:
941
+ raise ValueError("A context-parallel mesh is required when context_parallel_degree > 1.")
942
+
943
+ config = ParallelConfig(context_parallel_config=cp_config)
944
+ config.setup(rank, degree, device, mesh=mesh)
945
+ if cp_config.ring_degree == 1 and int(mesh.mesh.numel()) != degree:
946
+ cp_config._flattened_mesh = cp_config._ulysses_mesh._flatten()
947
+ transformer._parallel_config = config
948
+
949
+ attention_classes = (Attention, MochiAttention, AttentionModuleMixin)
950
+ for module in transformer.modules():
951
+ if not isinstance(module, attention_classes):
952
+ continue
953
+ processor = module.processor
954
+ if processor is not None and hasattr(processor, "_parallel_config"):
955
+ processor._parallel_config = config
956
+
957
+ cp_plan = getattr(transformer, "_cp_plan", None)
958
+ if cp_plan is None:
959
+ raise ValueError("Transformer does not define a context-parallel plan.")
960
+ apply_context_parallel(transformer, cp_config, cp_plan)
961
+
962
+
963
+ def main() -> None:
964
+ parser = argparse.ArgumentParser()
965
+ parser.add_argument("--model_dir", required=True, help="LingBot-Video model root directory.")
966
+ parser.add_argument(
967
+ "--backend",
968
+ choices=["diffusers", "sglang"],
969
+ default=None,
970
+ help=(
971
+ "public backend selector; `sglang` uses SGLang Diffusion when "
972
+ "available and falls back to diffusers otherwise"
973
+ ),
974
+ )
975
+ parser.add_argument(
976
+ "--engine",
977
+ choices=["sglang-native", "diffusers"],
978
+ default=None,
979
+ help=argparse.SUPPRESS,
980
+ )
981
+ parser.add_argument("--mode", choices=["t2i", "t2v", "ti2v"], required=True)
982
+ parser.add_argument("--prompt", default=None)
983
+ parser.add_argument("--prompt_json", default=None)
984
+ parser.add_argument("--negative_prompt", default=None,
985
+ help="negative prompt; if unset, uses the mode's default "
986
+ "(image default for t2i, video default for t2v/ti2v)")
987
+ parser.add_argument(
988
+ "--negative_prompt_json",
989
+ default=None,
990
+ help="path to the JSON file produced by rewriter/auto_negative.py",
991
+ )
992
+ parser.add_argument("--image", default=None)
993
+ parser.add_argument("--output", required=True)
994
+ parser.add_argument("--resolution", default=None)
995
+ parser.add_argument("--ratio", default=None)
996
+ parser.add_argument("--duration", type=float, default=None)
997
+ parser.add_argument("--height", type=int, default=192)
998
+ parser.add_argument("--width", type=int, default=320)
999
+ parser.add_argument("--num_frames", type=int, default=9)
1000
+ parser.add_argument("--steps", type=int, default=40)
1001
+ parser.add_argument("--guidance_scale", type=float, default=3.0)
1002
+ parser.add_argument("--shift", type=float, default=3.0)
1003
+ parser.add_argument("--seed", type=int, default=42)
1004
+ parser.add_argument("--fps", type=int, default=24)
1005
+ parser.add_argument("--default_dtype", default="bf16")
1006
+ parser.add_argument("--transformer_dtype", default="bf16")
1007
+ parser.add_argument("--transformer_subfolder", default="transformer")
1008
+ parser.add_argument("--text_encoder_dtype", default="bf16")
1009
+ parser.add_argument("--vae_dtype", default="fp32")
1010
+ parser.add_argument("--diffusers_attn_backend", default=os.environ.get("DIFFUSERS_ATTN_BACKEND", ""))
1011
+ parser.add_argument("--allow_tf32", action=argparse.BooleanOptionalAction, default=True)
1012
+ parser.add_argument(
1013
+ "--quiet_progress",
1014
+ action="store_true",
1015
+ help="Disable model-loading logs and denoising progress bars.",
1016
+ )
1017
+ parser.add_argument("--cfg_parallel_degree", type=int, default=1)
1018
+ parser.add_argument("--context_parallel_degree", type=int, default=1)
1019
+ parser.add_argument("--context_parallel_ulysses_anything", action="store_true")
1020
+ parser.add_argument(
1021
+ "--enable_fsdp_inference",
1022
+ action="store_true",
1023
+ help="Shard the base/refiner DiT transformers with PyTorch composable FSDP2.",
1024
+ )
1025
+ parser.add_argument("--batch_cfg", action="store_true")
1026
+ parser.add_argument("--null_cond_clone_zero", action="store_true")
1027
+ parser.add_argument("--reuse_condition_features", action="store_true")
1028
+ parser.add_argument("--run_refiner", action="store_true")
1029
+ parser.add_argument("--refiner_model_dir", default=None)
1030
+ parser.add_argument("--refiner_transformer_subfolder", default="refiner")
1031
+ parser.add_argument("--refiner_output", default=None)
1032
+ parser.add_argument("--refiner_height", type=int, default=1088)
1033
+ parser.add_argument("--refiner_width", type=int, default=1920)
1034
+ parser.add_argument("--refiner_steps", type=int, default=8)
1035
+ parser.add_argument("--refiner_guidance_scale", type=float, default=3.0)
1036
+ parser.add_argument("--refiner_shift", type=float, default=3.0)
1037
+ parser.add_argument("--refiner_t_thresh", type=float, default=0.85)
1038
+ parser.add_argument("--refiner_sigma_tail_steps", type=int, default=2)
1039
+ parser.add_argument("--refiner_fps", type=int, default=24)
1040
+ parser.add_argument("--refiner_sample_fps", type=int, default=24)
1041
+ parser.add_argument("--refiner_max_video_frames", type=int, default=None)
1042
+ parser.add_argument("--refiner_vae_dtype", default="fp32")
1043
+ parser.add_argument("--refiner_batch_cfg", action="store_true")
1044
+ parser.add_argument("--refiner_no_null_cond_clone_zero", action="store_true")
1045
+ parser.add_argument("--refiner_offload_vae_during_denoise", action="store_true")
1046
+ args = parser.parse_args()
1047
+ if args.quiet_progress:
1048
+ os.environ["LINGBOT_QUIET_PROGRESS"] = "1"
1049
+ args.engine = resolve_backend_engine(
1050
+ engine=args.engine,
1051
+ backend=args.backend,
1052
+ stderr=sys.stderr,
1053
+ )
1054
+ args.negative_prompt = resolve_negative_prompt_arg(
1055
+ args.negative_prompt,
1056
+ args.negative_prompt_json,
1057
+ )
1058
+
1059
+ prompt_sample = None
1060
+ if args.prompt_json:
1061
+ prompt_sample = _load_prompt_sample(Path(args.prompt_json))
1062
+ args.prompt = _caption_from_sample(prompt_sample)
1063
+ if args.duration is None and "duration" in prompt_sample:
1064
+ args.duration = float(prompt_sample["duration"])
1065
+ if args.prompt is None:
1066
+ raise ValueError(
1067
+ "Provide `--prompt_json <path_to_structured_prompt.json>` or an explicit `--prompt`."
1068
+ )
1069
+ if args.resolution or args.ratio:
1070
+ if not args.resolution or not args.ratio:
1071
+ raise ValueError("`--resolution` and `--ratio` must be provided together.")
1072
+ args.height, args.width = _height_width_from_bucket(args.resolution, args.ratio)
1073
+ if args.negative_prompt is None:
1074
+ args.negative_prompt = (
1075
+ DEFAULT_NEGATIVE_PROMPT_IMAGE if args.mode == "t2i" else DEFAULT_NEGATIVE_PROMPT
1076
+ )
1077
+ if args.mode != "t2i" and args.duration is not None:
1078
+ if num_frames_from_duration is None:
1079
+ raise RuntimeError("num_frames_from_duration is not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
1080
+ args.num_frames = num_frames_from_duration(args.duration, args.fps)
1081
+ args.run_refiner = bool(args.run_refiner or args.refiner_model_dir)
1082
+ if args.run_refiner:
1083
+ if effective_refiner_model_dir is None:
1084
+ raise RuntimeError("model path helpers are not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
1085
+ args.refiner_model_dir = str(effective_refiner_model_dir(args))
1086
+
1087
+ (
1088
+ rank,
1089
+ local_rank,
1090
+ world_size,
1091
+ cfg_parallel_group,
1092
+ context_parallel_mesh,
1093
+ cfg_branch_rank,
1094
+ context_parallel_rank,
1095
+ ) = _init_parallel(
1096
+ args.cfg_parallel_degree,
1097
+ args.context_parallel_degree,
1098
+ args.enable_fsdp_inference,
1099
+ )
1100
+ if args.cfg_parallel_degree > 1 and args.batch_cfg:
1101
+ raise ValueError("`--cfg_parallel_degree > 1` and `--batch_cfg` are mutually exclusive.")
1102
+ if args.refiner_model_dir and args.cfg_parallel_degree > 1 and args.refiner_batch_cfg:
1103
+ raise ValueError(
1104
+ "`--cfg_parallel_degree > 1` and `--refiner_batch_cfg` are mutually exclusive."
1105
+ )
1106
+ if args.run_refiner and args.mode == "t2i":
1107
+ raise ValueError("Refiner is only supported for video modes.")
1108
+ if args.run_refiner and args.mode == "ti2v" and not args.image:
1109
+ raise ValueError(
1110
+ "The ti2v refiner conditions on the clean first frame; `--image` is required."
1111
+ )
1112
+ if args.diffusers_attn_backend:
1113
+ os.environ["DIFFUSERS_ATTN_BACKEND"] = args.diffusers_attn_backend
1114
+ if args.allow_tf32:
1115
+ torch.backends.cuda.matmul.allow_tf32 = True
1116
+ torch.set_float32_matmul_precision("high")
1117
+ if args.mode == "t2i":
1118
+ args.num_frames = 1
1119
+
1120
+ fsdp_mesh = None
1121
+ if args.enable_fsdp_inference:
1122
+ if init_fsdp_inference_mesh is None:
1123
+ raise RuntimeError("FSDP inference helpers are not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
1124
+ fsdp_mesh = init_fsdp_inference_mesh()
1125
+
1126
+ dtype_map = _make_dtype_map(args)
1127
+ defer_transformer_to_device = fsdp_mesh is not None
1128
+ pipe, engine_name = _load_pipe(
1129
+ args,
1130
+ dtype_map,
1131
+ defer_transformer_to_device=defer_transformer_to_device,
1132
+ )
1133
+ _configure_pipeline_logs(pipe)
1134
+ component_dtypes = _component_dtypes(pipe)
1135
+ expected_vae_dtype = _dtype_name(dtype_map["vae"])
1136
+ if component_dtypes.get("vae") != expected_vae_dtype:
1137
+ raise AssertionError(
1138
+ f"VAE dtype mismatch: requested {expected_vae_dtype}, got {component_dtypes}"
1139
+ )
1140
+ device = _default_device()
1141
+ if args.context_parallel_degree > 1:
1142
+ _enable_context_parallel(
1143
+ pipe.transformer,
1144
+ args.context_parallel_degree,
1145
+ args.context_parallel_ulysses_anything,
1146
+ context_parallel_rank,
1147
+ device,
1148
+ context_parallel_mesh,
1149
+ args.cfg_parallel_degree == 1,
1150
+ )
1151
+ base_fsdp_info = _apply_fsdp_inference_if_requested(pipe, args.enable_fsdp_inference, fsdp_mesh)
1152
+
1153
+ refiner_state = _maybe_preload_refiner(
1154
+ args,
1155
+ dtype_map,
1156
+ device,
1157
+ context_parallel_rank,
1158
+ context_parallel_mesh,
1159
+ defer_transformer_to_device,
1160
+ )
1161
+ if refiner_state["pipe"] is not None:
1162
+ _configure_pipeline_logs(refiner_state["pipe"])
1163
+ refiner_fsdp_info = None
1164
+ if refiner_state["pipe"] is not None:
1165
+ refiner_fsdp_info = _apply_fsdp_inference_if_requested(
1166
+ refiner_state["pipe"],
1167
+ args.enable_fsdp_inference,
1168
+ fsdp_mesh,
1169
+ )
1170
+ if rank == 0 and refiner_state["refiner_requested"] and not refiner_state["refiner_available"]:
1171
+ print(
1172
+ "WARNING: refiner requested but unavailable; "
1173
+ f"reason={refiner_state['refiner_skipped_reason']} "
1174
+ f"refiner_model_dir={args.refiner_model_dir}",
1175
+ flush=True,
1176
+ )
1177
+
1178
+ generator = torch.Generator(device=device).manual_seed(args.seed)
1179
+ condition_cache = None
1180
+ input_image = None
1181
+ ti2v_image_tensor_cpu = None
1182
+ should_cache_conditions = (
1183
+ args.reuse_condition_features
1184
+ or bool(refiner_state["refiner_available"])
1185
+ or args.batch_cfg
1186
+ or args.null_cond_clone_zero
1187
+ )
1188
+ if args.mode == "ti2v":
1189
+ input_image = (
1190
+ Image.open(args.image).convert("RGB")
1191
+ if args.image
1192
+ else _make_default_image(args.height, args.width)
1193
+ )
1194
+ if should_cache_conditions:
1195
+ condition_cache, ti2v_image_tensor_cpu = _cache_ti2v_prompt_conditions(
1196
+ pipe,
1197
+ args.prompt,
1198
+ args.negative_prompt,
1199
+ input_image,
1200
+ height=args.height,
1201
+ width=args.width,
1202
+ device=device,
1203
+ null_cond_clone_zero=args.null_cond_clone_zero,
1204
+ )
1205
+ elif should_cache_conditions:
1206
+ condition_cache = _cache_prompt_conditions(
1207
+ pipe,
1208
+ args.prompt,
1209
+ args.negative_prompt,
1210
+ device=device,
1211
+ null_cond_clone_zero=args.null_cond_clone_zero,
1212
+ )
1213
+ call_kwargs = dict(
1214
+ prompt=args.prompt,
1215
+ negative_prompt=args.negative_prompt,
1216
+ height=args.height,
1217
+ width=args.width,
1218
+ num_frames=args.num_frames,
1219
+ num_inference_steps=args.steps,
1220
+ guidance_scale=args.guidance_scale,
1221
+ shift=args.shift,
1222
+ generator=generator,
1223
+ output_type="np",
1224
+ **_condition_call_kwargs(condition_cache, device),
1225
+ )
1226
+ call_kwargs["batch_cfg"] = args.batch_cfg
1227
+ if args.mode != "ti2v":
1228
+ call_kwargs["null_cond_clone_zero"] = args.null_cond_clone_zero
1229
+ if args.mode == "ti2v":
1230
+ call_kwargs["image"] = input_image
1231
+ if ti2v_image_tensor_cpu is not None:
1232
+ call_kwargs["image_tensor"] = ti2v_image_tensor_cpu.to(device=device)
1233
+ if args.cfg_parallel_degree > 1:
1234
+ call_kwargs["cfg_parallel_group"] = cfg_parallel_group
1235
+
1236
+ if rank == 0:
1237
+ print(
1238
+ "runtime "
1239
+ f"engine={engine_name} model_dir={args.model_dir} mode={args.mode} device={device} "
1240
+ f"rank={rank}/{world_size} local_rank={local_rank} "
1241
+ f"cfg_branch_rank={cfg_branch_rank} context_parallel_rank={context_parallel_rank} "
1242
+ f"cfg_parallel_degree={args.cfg_parallel_degree} "
1243
+ f"context_parallel_degree={args.context_parallel_degree} "
1244
+ f"height={args.height} width={args.width} frames={args.num_frames} steps={args.steps} "
1245
+ f"guidance={args.guidance_scale} shift={args.shift} seed={args.seed} "
1246
+ f"attn_backend={os.environ.get('DIFFUSERS_ATTN_BACKEND')} "
1247
+ f"allow_tf32={torch.backends.cuda.matmul.allow_tf32} "
1248
+ f"fsdp_inference={base_fsdp_info} "
1249
+ f"component_dtypes={component_dtypes}",
1250
+ flush=True,
1251
+ )
1252
+
1253
+ with torch.no_grad():
1254
+ result = pipe(**call_kwargs)
1255
+ frames = _extract_frames(result) if rank == 0 else None
1256
+ if rank == 0:
1257
+ _save_frames(
1258
+ frames,
1259
+ args.mode,
1260
+ Path(args.output),
1261
+ args.fps,
1262
+ )
1263
+
1264
+ if not refiner_state["refiner_requested"] or not refiner_state["refiner_available"]:
1265
+ _sync_parallel_if_needed()
1266
+ _destroy_parallel_if_needed()
1267
+ return
1268
+
1269
+ _sync_parallel_if_needed()
1270
+ del result, frames, call_kwargs
1271
+ gc.collect()
1272
+ if torch.cuda.is_available():
1273
+ torch.cuda.empty_cache()
1274
+
1275
+ if load_refiner_video_tensor is None or prepare_refiner_latent is None:
1276
+ raise RuntimeError("Refiner helpers are not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
1277
+ refiner_pipe = refiner_state["pipe"]
1278
+ if refiner_pipe is None:
1279
+ raise RuntimeError("Refiner was marked available but was not preloaded.")
1280
+ refiner_engine_name = str(refiner_state["engine_name"])
1281
+ refiner_component_dtypes = refiner_state["component_dtypes"]
1282
+
1283
+ refiner_generator = torch.Generator(device=device).manual_seed(args.seed)
1284
+ lowres_video, lowres_meta = load_refiner_video_tensor(
1285
+ args.output,
1286
+ args.refiner_height,
1287
+ args.refiner_width,
1288
+ sample_fps=args.refiner_sample_fps,
1289
+ vae_tc=getattr(refiner_pipe, "vae_scale_factor_temporal", 4),
1290
+ max_frames=args.refiner_max_video_frames,
1291
+ )
1292
+
1293
+ # TI2V always refines with the clean input first frame as a fixed frame-0
1294
+ # latent: injected into the initial latent here and re-clamped after every
1295
+ # scheduler step via `cond_latent`.
1296
+ first_frame_condition_enabled = args.mode == "ti2v"
1297
+ refiner_cond_latent = None
1298
+ with torch.no_grad():
1299
+ x_up = refiner_pipe.encode_video_latent(lowres_video, generator=refiner_generator)
1300
+ if first_frame_condition_enabled:
1301
+ condition_pixels = load_first_frame_condition_tensor(
1302
+ args.image,
1303
+ args.refiner_height,
1304
+ args.refiner_width,
1305
+ geometry_height=args.height,
1306
+ geometry_width=args.width,
1307
+ )
1308
+ clean_x0 = refiner_pipe.encode_video_latent(
1309
+ condition_pixels,
1310
+ generator=refiner_generator,
1311
+ )
1312
+ refiner_cond_latent = clean_x0[:, :, 0:1].contiguous()
1313
+ x_up[:, :, 0:1] = refiner_cond_latent.to(dtype=x_up.dtype)
1314
+ del condition_pixels, clean_x0
1315
+ noise = torch.randn(
1316
+ x_up.shape,
1317
+ device=device,
1318
+ dtype=x_up.dtype,
1319
+ generator=refiner_generator,
1320
+ )
1321
+ initial_latent = prepare_refiner_latent(x_up, noise, args.refiner_t_thresh)
1322
+ del lowres_video, x_up, noise
1323
+ refiner_null_clone_zero = not args.refiner_no_null_cond_clone_zero
1324
+ # The ti2v condition cache carries image tokens; the refiner conditions on
1325
+ # text only, so it rebuilds t2v-style embeddings instead of reusing it.
1326
+ refiner_condition_cache = None if args.mode == "ti2v" else condition_cache
1327
+ should_cache_refiner_conditions = not (
1328
+ args.cfg_parallel_degree > 1 and cfg_branch_rank == 1 and not refiner_null_clone_zero
1329
+ )
1330
+ if refiner_condition_cache is None and should_cache_refiner_conditions:
1331
+ refiner_condition_cache = _cache_prompt_conditions(
1332
+ refiner_pipe,
1333
+ args.prompt,
1334
+ args.negative_prompt,
1335
+ device=device,
1336
+ null_cond_clone_zero=refiner_null_clone_zero,
1337
+ )
1338
+ elif refiner_null_clone_zero:
1339
+ refiner_condition_cache = dict(refiner_condition_cache)
1340
+ refiner_condition_cache["negative_prompt_embeds"] = torch.zeros_like(
1341
+ refiner_condition_cache["prompt_embeds"]
1342
+ )
1343
+ refiner_condition_cache["negative_prompt_mask"] = refiner_condition_cache["prompt_mask"].clone()
1344
+
1345
+ if rank == 0:
1346
+ print(
1347
+ "runtime "
1348
+ f"engine={refiner_engine_name} model_dir={args.refiner_model_dir} mode=refiner "
1349
+ f"height={args.refiner_height} width={args.refiner_width} "
1350
+ f"frames={lowres_meta['sample_frame']} steps={args.refiner_steps} "
1351
+ f"guidance={args.refiner_guidance_scale} shift={args.refiner_shift} "
1352
+ f"t_thresh={args.refiner_t_thresh} tail_steps={args.refiner_sigma_tail_steps} "
1353
+ f"seed={args.seed} "
1354
+ f"batch_cfg={args.refiner_batch_cfg} "
1355
+ f"first_frame_condition={first_frame_condition_enabled} "
1356
+ f"fsdp_inference={refiner_fsdp_info} "
1357
+ f"component_dtypes={refiner_component_dtypes}",
1358
+ flush=True,
1359
+ )
1360
+
1361
+ refiner_call_kwargs = dict(
1362
+ prompt=args.prompt,
1363
+ negative_prompt=args.negative_prompt,
1364
+ height=args.refiner_height,
1365
+ width=args.refiner_width,
1366
+ num_frames=int(lowres_meta["sample_frame"]),
1367
+ num_inference_steps=args.refiner_steps,
1368
+ guidance_scale=args.refiner_guidance_scale,
1369
+ shift=args.refiner_shift,
1370
+ t_thresh=args.refiner_t_thresh,
1371
+ refiner_sigma_tail_steps=args.refiner_sigma_tail_steps,
1372
+ generator=refiner_generator,
1373
+ latents=initial_latent,
1374
+ output_type="np",
1375
+ batch_cfg=args.refiner_batch_cfg,
1376
+ null_cond_clone_zero=refiner_null_clone_zero,
1377
+ offload_vae_during_denoise=args.refiner_offload_vae_during_denoise,
1378
+ **_condition_call_kwargs(refiner_condition_cache, device),
1379
+ )
1380
+ if refiner_cond_latent is not None:
1381
+ refiner_call_kwargs["cond_latent"] = refiner_cond_latent
1382
+ if args.cfg_parallel_degree > 1:
1383
+ refiner_call_kwargs["cfg_parallel_group"] = cfg_parallel_group
1384
+
1385
+ with torch.no_grad():
1386
+ refiner_result = refiner_pipe(**refiner_call_kwargs)
1387
+ if rank == 0:
1388
+ refiner_frames = _extract_frames(refiner_result)
1389
+ _save_frames(
1390
+ refiner_frames,
1391
+ args.mode,
1392
+ Path(args.refiner_output or str(Path(args.output).with_name(Path(args.output).stem + "_refined.mp4"))),
1393
+ args.refiner_fps,
1394
+ )
1395
+
1396
+ _sync_parallel_if_needed()
1397
+ _destroy_parallel_if_needed()
1398
+
1399
+
1400
+ if __name__ == "__main__":
1401
+ main()
lingbot_video/scheduling_flow_unipc.py ADDED
@@ -0,0 +1,796 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import List, Optional, Tuple, Union
3
+
4
+ import numpy as np
5
+ import torch
6
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
7
+ from diffusers.schedulers.scheduling_utils import (KarrasDiffusionSchedulers,
8
+ SchedulerMixin,
9
+ SchedulerOutput)
10
+ from diffusers.utils import deprecate, is_scipy_available
11
+
12
+ if is_scipy_available():
13
+ import scipy.stats
14
+
15
+
16
+ class FlowUniPCMultistepScheduler(SchedulerMixin, ConfigMixin):
17
+ """
18
+ `UniPCMultistepScheduler` is a training-free framework designed for the fast sampling of diffusion models.
19
+
20
+ This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic
21
+ methods the library implements for all schedulers such as loading and saving.
22
+
23
+ Args:
24
+ num_train_timesteps (`int`, defaults to 1000):
25
+ The number of diffusion steps to train the model.
26
+ solver_order (`int`, default `2`):
27
+ The UniPC order which can be any positive integer. The effective order of accuracy is `solver_order + 1`
28
+ due to the UniC. It is recommended to use `solver_order=2` for guided sampling, and `solver_order=3` for
29
+ unconditional sampling.
30
+ prediction_type (`str`, defaults to "flow_prediction"):
31
+ Prediction type of the scheduler function; must be `flow_prediction` for this scheduler, which predicts
32
+ the flow of the diffusion process.
33
+ thresholding (`bool`, defaults to `False`):
34
+ Whether to use the "dynamic thresholding" method. This is unsuitable for latent-space diffusion models such
35
+ as Stable Diffusion.
36
+ dynamic_thresholding_ratio (`float`, defaults to 0.995):
37
+ The ratio for the dynamic thresholding method. Valid only when `thresholding=True`.
38
+ sample_max_value (`float`, defaults to 1.0):
39
+ The threshold value for dynamic thresholding. Valid only when `thresholding=True` and `predict_x0=True`.
40
+ predict_x0 (`bool`, defaults to `True`):
41
+ Whether to use the updating algorithm on the predicted x0.
42
+ solver_type (`str`, default `bh2`):
43
+ Solver type for UniPC. It is recommended to use `bh1` for unconditional sampling when steps < 10, and `bh2`
44
+ otherwise.
45
+ lower_order_final (`bool`, default `True`):
46
+ Whether to use lower-order solvers in the final steps. Only valid for < 15 inference steps. This can
47
+ stabilize the sampling of DPMSolver for steps < 15, especially for steps <= 10.
48
+ disable_corrector (`list`, default `[]`):
49
+ Decides which step to disable the corrector to mitigate the misalignment between `epsilon_theta(x_t, c)`
50
+ and `epsilon_theta(x_t^c, c)` which can influence convergence for a large guidance scale. Corrector is
51
+ usually disabled during the first few steps.
52
+ solver_p (`SchedulerMixin`, default `None`):
53
+ Any other scheduler that if specified, the algorithm becomes `solver_p + UniC`.
54
+ use_karras_sigmas (`bool`, *optional*, defaults to `False`):
55
+ Whether to use Karras sigmas for step sizes in the noise schedule during the sampling process. If `True`,
56
+ the sigmas are determined according to a sequence of noise levels {σi}.
57
+ use_exponential_sigmas (`bool`, *optional*, defaults to `False`):
58
+ Whether to use exponential sigmas for step sizes in the noise schedule during the sampling process.
59
+ timestep_spacing (`str`, defaults to `"linspace"`):
60
+ The way the timesteps should be scaled. Refer to Table 2 of the [Common Diffusion Noise Schedules and
61
+ Sample Steps are Flawed](https://huggingface.co/papers/2305.08891) for more information.
62
+ steps_offset (`int`, defaults to 0):
63
+ An offset added to the inference steps, as required by some model families.
64
+ final_sigmas_type (`str`, defaults to `"zero"`):
65
+ The final `sigma` value for the noise schedule during the sampling process. If `"sigma_min"`, the final
66
+ sigma is the same as the last sigma in the training schedule. If `zero`, the final sigma is set to 0.
67
+ """
68
+
69
+ _compatibles = [e.name for e in KarrasDiffusionSchedulers]
70
+ order = 1
71
+
72
+ @register_to_config
73
+ def __init__(
74
+ self,
75
+ num_train_timesteps: int = 1000,
76
+ solver_order: int = 2,
77
+ prediction_type: str = "flow_prediction",
78
+ shift: Optional[float] = 1.0,
79
+ use_dynamic_shifting=False,
80
+ thresholding: bool = False,
81
+ dynamic_thresholding_ratio: float = 0.995,
82
+ sample_max_value: float = 1.0,
83
+ predict_x0: bool = True,
84
+ solver_type: str = "bh2",
85
+ lower_order_final: bool = True,
86
+ disable_corrector: List[int] = [],
87
+ solver_p: SchedulerMixin = None,
88
+ timestep_spacing: str = "linspace",
89
+ steps_offset: int = 0,
90
+ final_sigmas_type: Optional[str] = "zero", # "zero", "sigma_min"
91
+ ):
92
+
93
+ if solver_type not in ["bh1", "bh2"]:
94
+ if solver_type in ["midpoint", "heun", "logrho"]:
95
+ self.register_to_config(solver_type="bh2")
96
+ else:
97
+ raise NotImplementedError(
98
+ f"{solver_type} is not implemented for {self.__class__}")
99
+
100
+ self.predict_x0 = predict_x0
101
+ # setable values
102
+ self.num_inference_steps = None
103
+ alphas = np.linspace(1, 1 / num_train_timesteps,
104
+ num_train_timesteps)[::-1].copy()
105
+ sigmas = 1.0 - alphas
106
+ sigmas = torch.from_numpy(sigmas).to(dtype=torch.float32)
107
+
108
+ if not use_dynamic_shifting:
109
+ # when use_dynamic_shifting is True, we apply the timestep shifting on the fly based on the image resolution
110
+ sigmas = shift * sigmas / (1 +
111
+ (shift - 1) * sigmas) # pyright: ignore
112
+
113
+ self.sigmas = sigmas
114
+ self.timesteps = sigmas * num_train_timesteps
115
+
116
+ self.model_outputs = [None] * solver_order
117
+ self.timestep_list = [None] * solver_order
118
+ self.lower_order_nums = 0
119
+ self.disable_corrector = disable_corrector
120
+ self.solver_p = solver_p
121
+ self.last_sample = None
122
+ self._step_index = None
123
+ self._begin_index = None
124
+
125
+ self.sigmas = self.sigmas.to(
126
+ "cpu") # to avoid too much CPU/GPU communication
127
+ self.sigma_min = self.sigmas[-1].item()
128
+ self.sigma_max = self.sigmas[0].item()
129
+
130
+ @property
131
+ def step_index(self):
132
+ """
133
+ The index counter for current timestep. It will increase 1 after each scheduler step.
134
+ """
135
+ return self._step_index
136
+
137
+ @property
138
+ def begin_index(self):
139
+ """
140
+ The index for the first timestep. It should be set from pipeline with `set_begin_index` method.
141
+ """
142
+ return self._begin_index
143
+
144
+ # Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.set_begin_index
145
+ def set_begin_index(self, begin_index: int = 0):
146
+ """
147
+ Sets the begin index for the scheduler. This function should be run from pipeline before the inference.
148
+
149
+ Args:
150
+ begin_index (`int`):
151
+ The begin index for the scheduler.
152
+ """
153
+ self._begin_index = begin_index
154
+
155
+ # Modified from diffusers.schedulers.scheduling_flow_match_euler_discrete.FlowMatchEulerDiscreteScheduler.set_timesteps
156
+ def set_timesteps(
157
+ self,
158
+ num_inference_steps: Union[int, None] = None,
159
+ device: Union[str, torch.device] = None,
160
+ sigmas: Optional[List[float]] = None,
161
+ mu: Optional[Union[float, None]] = None,
162
+ shift: Optional[Union[float, None]] = None,
163
+ ):
164
+ """
165
+ Sets the discrete timesteps used for the diffusion chain (to be run before inference).
166
+ Args:
167
+ num_inference_steps (`int`):
168
+ Total number of the spacing of the time steps.
169
+ device (`str` or `torch.device`, *optional*):
170
+ The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
171
+ """
172
+
173
+ if self.config.use_dynamic_shifting and mu is None:
174
+ raise ValueError(
175
+ " you have to pass a value for `mu` when `use_dynamic_shifting` is set to be `True`"
176
+ )
177
+
178
+ if sigmas is None:
179
+ sigmas = np.linspace(self.sigma_max, self.sigma_min,
180
+ num_inference_steps +
181
+ 1).copy()[:-1] # pyright: ignore
182
+
183
+ if self.config.use_dynamic_shifting:
184
+ sigmas = self.time_shift(mu, 1.0, sigmas) # pyright: ignore
185
+ else:
186
+ if shift is None:
187
+ shift = self.config.shift
188
+ sigmas = shift * sigmas / (1 +
189
+ (shift - 1) * sigmas) # pyright: ignore
190
+
191
+ if self.config.final_sigmas_type == "sigma_min":
192
+ sigma_last = ((1 - self.alphas_cumprod[0]) /
193
+ self.alphas_cumprod[0])**0.5
194
+ elif self.config.final_sigmas_type == "zero":
195
+ sigma_last = 0
196
+ else:
197
+ raise ValueError(
198
+ f"`final_sigmas_type` must be one of 'zero', or 'sigma_min', but got {self.config.final_sigmas_type}"
199
+ )
200
+
201
+ timesteps = sigmas * self.config.num_train_timesteps
202
+ sigmas = np.concatenate([sigmas, [sigma_last]
203
+ ]).astype(np.float32) # pyright: ignore
204
+
205
+ self.sigmas = torch.from_numpy(sigmas)
206
+ self.timesteps = torch.from_numpy(timesteps).to(
207
+ device=device, dtype=torch.int64)
208
+
209
+ self.num_inference_steps = len(timesteps)
210
+
211
+ self.model_outputs = [
212
+ None,
213
+ ] * self.config.solver_order
214
+ self.lower_order_nums = 0
215
+ self.last_sample = None
216
+ if self.solver_p:
217
+ self.solver_p.set_timesteps(self.num_inference_steps, device=device)
218
+
219
+ # add an index counter for schedulers that allow duplicated timesteps
220
+ self._step_index = None
221
+ self._begin_index = None
222
+ self.sigmas = self.sigmas.to(
223
+ "cpu") # to avoid too much CPU/GPU communication
224
+
225
+ # Copied from diffusers.schedulers.scheduling_ddpm.DDPMScheduler._threshold_sample
226
+ def _threshold_sample(self, sample: torch.Tensor) -> torch.Tensor:
227
+ """
228
+ "Dynamic thresholding: At each sampling step we set s to a certain percentile absolute pixel value in xt0 (the
229
+ prediction of x_0 at timestep t), and if s > 1, then we threshold xt0 to the range [-s, s] and then divide by
230
+ s. Dynamic thresholding pushes saturated pixels (those near -1 and 1) inwards, thereby actively preventing
231
+ pixels from saturation at each step. We find that dynamic thresholding results in significantly better
232
+ photorealism as well as better image-text alignment, especially when using very large guidance weights."
233
+
234
+ https://arxiv.org/abs/2205.11487
235
+ """
236
+ dtype = sample.dtype
237
+ batch_size, channels, *remaining_dims = sample.shape
238
+
239
+ if dtype not in (torch.float32, torch.float64):
240
+ sample = sample.float(
241
+ ) # upcast for quantile calculation, and clamp not implemented for cpu half
242
+
243
+ # Flatten sample for doing quantile calculation along each image
244
+ sample = sample.reshape(batch_size, channels * np.prod(remaining_dims))
245
+
246
+ abs_sample = sample.abs() # "a certain percentile absolute pixel value"
247
+
248
+ s = torch.quantile(
249
+ abs_sample, self.config.dynamic_thresholding_ratio, dim=1)
250
+ s = torch.clamp(
251
+ s, min=1, max=self.config.sample_max_value
252
+ ) # When clamped to min=1, equivalent to standard clipping to [-1, 1]
253
+ s = s.unsqueeze(
254
+ 1) # (batch_size, 1) because clamp will broadcast along dim=0
255
+ sample = torch.clamp(
256
+ sample, -s, s
257
+ ) / s # "we threshold xt0 to the range [-s, s] and then divide by s"
258
+
259
+ sample = sample.reshape(batch_size, channels, *remaining_dims)
260
+ sample = sample.to(dtype)
261
+
262
+ return sample
263
+
264
+ # Copied from diffusers.schedulers.scheduling_flow_match_euler_discrete.FlowMatchEulerDiscreteScheduler._sigma_to_t
265
+ def _sigma_to_t(self, sigma):
266
+ return sigma * self.config.num_train_timesteps
267
+
268
+ def _sigma_to_alpha_sigma_t(self, sigma):
269
+ return 1 - sigma, sigma
270
+
271
+ # Copied from diffusers.schedulers.scheduling_flow_match_euler_discrete.set_timesteps
272
+ def time_shift(self, mu: float, sigma: float, t: torch.Tensor):
273
+ return math.exp(mu) / (math.exp(mu) + (1 / t - 1)**sigma)
274
+
275
+ def convert_model_output(
276
+ self,
277
+ model_output: torch.Tensor,
278
+ *args,
279
+ sample: torch.Tensor = None,
280
+ **kwargs,
281
+ ) -> torch.Tensor:
282
+ r"""
283
+ Convert the model output to the corresponding type the UniPC algorithm needs.
284
+
285
+ Args:
286
+ model_output (`torch.Tensor`):
287
+ The direct output from the learned diffusion model.
288
+ timestep (`int`):
289
+ The current discrete timestep in the diffusion chain.
290
+ sample (`torch.Tensor`):
291
+ A current instance of a sample created by the diffusion process.
292
+
293
+ Returns:
294
+ `torch.Tensor`:
295
+ The converted model output.
296
+ """
297
+ timestep = args[0] if len(args) > 0 else kwargs.pop("timestep", None)
298
+ if sample is None:
299
+ if len(args) > 1:
300
+ sample = args[1]
301
+ else:
302
+ raise ValueError(
303
+ "missing `sample` as a required keyward argument")
304
+ if timestep is not None:
305
+ deprecate(
306
+ "timesteps",
307
+ "1.0.0",
308
+ "Passing `timesteps` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
309
+ )
310
+
311
+ sigma = self.sigmas[self.step_index]
312
+ alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
313
+
314
+ if self.predict_x0:
315
+ if self.config.prediction_type == "flow_prediction":
316
+ sigma_t = self.sigmas[self.step_index]
317
+ x0_pred = sample - sigma_t * model_output
318
+ else:
319
+ raise ValueError(
320
+ f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`,"
321
+ " `v_prediction` or `flow_prediction` for the UniPCMultistepScheduler."
322
+ )
323
+
324
+ if self.config.thresholding:
325
+ x0_pred = self._threshold_sample(x0_pred)
326
+
327
+ return x0_pred
328
+ else:
329
+ if self.config.prediction_type == "flow_prediction":
330
+ sigma_t = self.sigmas[self.step_index]
331
+ epsilon = sample - (1 - sigma_t) * model_output
332
+ else:
333
+ raise ValueError(
334
+ f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`,"
335
+ " `v_prediction` or `flow_prediction` for the UniPCMultistepScheduler."
336
+ )
337
+
338
+ if self.config.thresholding:
339
+ sigma_t = self.sigmas[self.step_index]
340
+ x0_pred = sample - sigma_t * model_output
341
+ x0_pred = self._threshold_sample(x0_pred)
342
+ epsilon = model_output + x0_pred
343
+
344
+ return epsilon
345
+
346
+ def multistep_uni_p_bh_update(
347
+ self,
348
+ model_output: torch.Tensor,
349
+ *args,
350
+ sample: torch.Tensor = None,
351
+ order: int = None, # pyright: ignore
352
+ **kwargs,
353
+ ) -> torch.Tensor:
354
+ """
355
+ One step for the UniP (B(h) version). Alternatively, `self.solver_p` is used if is specified.
356
+
357
+ Args:
358
+ model_output (`torch.Tensor`):
359
+ The direct output from the learned diffusion model at the current timestep.
360
+ prev_timestep (`int`):
361
+ The previous discrete timestep in the diffusion chain.
362
+ sample (`torch.Tensor`):
363
+ A current instance of a sample created by the diffusion process.
364
+ order (`int`):
365
+ The order of UniP at this timestep (corresponds to the *p* in UniPC-p).
366
+
367
+ Returns:
368
+ `torch.Tensor`:
369
+ The sample tensor at the previous timestep.
370
+ """
371
+ prev_timestep = args[0] if len(args) > 0 else kwargs.pop(
372
+ "prev_timestep", None)
373
+ if sample is None:
374
+ if len(args) > 1:
375
+ sample = args[1]
376
+ else:
377
+ raise ValueError(
378
+ " missing `sample` as a required keyward argument")
379
+ if order is None:
380
+ if len(args) > 2:
381
+ order = args[2]
382
+ else:
383
+ raise ValueError(
384
+ " missing `order` as a required keyward argument")
385
+ if prev_timestep is not None:
386
+ deprecate(
387
+ "prev_timestep",
388
+ "1.0.0",
389
+ "Passing `prev_timestep` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
390
+ )
391
+ model_output_list = self.model_outputs
392
+
393
+ s0 = self.timestep_list[-1]
394
+ m0 = model_output_list[-1]
395
+ x = sample
396
+
397
+ if self.solver_p:
398
+ x_t = self.solver_p.step(model_output, s0, x).prev_sample
399
+ return x_t
400
+
401
+ sigma_t, sigma_s0 = self.sigmas[self.step_index + 1], self.sigmas[
402
+ self.step_index] # pyright: ignore
403
+ alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t)
404
+ alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0)
405
+
406
+ lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
407
+ lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
408
+
409
+ h = lambda_t - lambda_s0
410
+ device = sample.device
411
+
412
+ rks = []
413
+ D1s = []
414
+ for i in range(1, order):
415
+ si = self.step_index - i # pyright: ignore
416
+ mi = model_output_list[-(i + 1)]
417
+ alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si])
418
+ lambda_si = torch.log(alpha_si) - torch.log(sigma_si)
419
+ rk = (lambda_si - lambda_s0) / h
420
+ rks.append(rk)
421
+ D1s.append((mi - m0) / rk) # pyright: ignore
422
+
423
+ rks.append(1.0)
424
+ rks = torch.tensor(rks, device=device)
425
+
426
+ R = []
427
+ b = []
428
+
429
+ hh = -h if self.predict_x0 else h
430
+ h_phi_1 = torch.expm1(hh) # h\phi_1(h) = e^h - 1
431
+ h_phi_k = h_phi_1 / hh - 1
432
+
433
+ factorial_i = 1
434
+
435
+ if self.config.solver_type == "bh1":
436
+ B_h = hh
437
+ elif self.config.solver_type == "bh2":
438
+ B_h = torch.expm1(hh)
439
+ else:
440
+ raise NotImplementedError()
441
+
442
+ for i in range(1, order + 1):
443
+ R.append(torch.pow(rks, i - 1))
444
+ b.append(h_phi_k * factorial_i / B_h)
445
+ factorial_i *= i + 1
446
+ h_phi_k = h_phi_k / hh - 1 / factorial_i
447
+
448
+ R = torch.stack(R)
449
+ b = torch.tensor(b, device=device)
450
+
451
+ if len(D1s) > 0:
452
+ D1s = torch.stack(D1s, dim=1) # (B, K)
453
+ # for order 2, we use a simplified version
454
+ if order == 2:
455
+ rhos_p = torch.tensor([0.5], dtype=x.dtype, device=device)
456
+ else:
457
+ rhos_p = torch.linalg.solve(R[:-1, :-1],
458
+ b[:-1]).to(device).to(x.dtype)
459
+ else:
460
+ D1s = None
461
+
462
+ if self.predict_x0:
463
+ x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0
464
+ if D1s is not None:
465
+ pred_res = torch.einsum("k,bkc...->bc...", rhos_p,
466
+ D1s) # pyright: ignore
467
+ else:
468
+ pred_res = 0
469
+ x_t = x_t_ - alpha_t * B_h * pred_res
470
+ else:
471
+ x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0
472
+ if D1s is not None:
473
+ pred_res = torch.einsum("k,bkc...->bc...", rhos_p,
474
+ D1s) # pyright: ignore
475
+ else:
476
+ pred_res = 0
477
+ x_t = x_t_ - sigma_t * B_h * pred_res
478
+
479
+ x_t = x_t.to(x.dtype)
480
+ return x_t
481
+
482
+ def multistep_uni_c_bh_update(
483
+ self,
484
+ this_model_output: torch.Tensor,
485
+ *args,
486
+ last_sample: torch.Tensor = None,
487
+ this_sample: torch.Tensor = None,
488
+ order: int = None, # pyright: ignore
489
+ **kwargs,
490
+ ) -> torch.Tensor:
491
+ """
492
+ One step for the UniC (B(h) version).
493
+
494
+ Args:
495
+ this_model_output (`torch.Tensor`):
496
+ The model outputs at `x_t`.
497
+ this_timestep (`int`):
498
+ The current timestep `t`.
499
+ last_sample (`torch.Tensor`):
500
+ The generated sample before the last predictor `x_{t-1}`.
501
+ this_sample (`torch.Tensor`):
502
+ The generated sample after the last predictor `x_{t}`.
503
+ order (`int`):
504
+ The `p` of UniC-p at this step. The effective order of accuracy should be `order + 1`.
505
+
506
+ Returns:
507
+ `torch.Tensor`:
508
+ The corrected sample tensor at the current timestep.
509
+ """
510
+ this_timestep = args[0] if len(args) > 0 else kwargs.pop(
511
+ "this_timestep", None)
512
+ if last_sample is None:
513
+ if len(args) > 1:
514
+ last_sample = args[1]
515
+ else:
516
+ raise ValueError(
517
+ " missing`last_sample` as a required keyward argument")
518
+ if this_sample is None:
519
+ if len(args) > 2:
520
+ this_sample = args[2]
521
+ else:
522
+ raise ValueError(
523
+ " missing`this_sample` as a required keyward argument")
524
+ if order is None:
525
+ if len(args) > 3:
526
+ order = args[3]
527
+ else:
528
+ raise ValueError(
529
+ " missing`order` as a required keyward argument")
530
+ if this_timestep is not None:
531
+ deprecate(
532
+ "this_timestep",
533
+ "1.0.0",
534
+ "Passing `this_timestep` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
535
+ )
536
+
537
+ model_output_list = self.model_outputs
538
+
539
+ m0 = model_output_list[-1]
540
+ x = last_sample
541
+ x_t = this_sample
542
+ model_t = this_model_output
543
+
544
+ sigma_t, sigma_s0 = self.sigmas[self.step_index], self.sigmas[
545
+ self.step_index - 1] # pyright: ignore
546
+ alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t)
547
+ alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0)
548
+
549
+ lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
550
+ lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
551
+
552
+ h = lambda_t - lambda_s0
553
+ device = this_sample.device
554
+
555
+ rks = []
556
+ D1s = []
557
+ for i in range(1, order):
558
+ si = self.step_index - (i + 1) # pyright: ignore
559
+ mi = model_output_list[-(i + 1)]
560
+ alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si])
561
+ lambda_si = torch.log(alpha_si) - torch.log(sigma_si)
562
+ rk = (lambda_si - lambda_s0) / h
563
+ rks.append(rk)
564
+ D1s.append((mi - m0) / rk) # pyright: ignore
565
+
566
+ rks.append(1.0)
567
+ rks = torch.tensor(rks, device=device)
568
+
569
+ R = []
570
+ b = []
571
+
572
+ hh = -h if self.predict_x0 else h
573
+ h_phi_1 = torch.expm1(hh) # h\phi_1(h) = e^h - 1
574
+ h_phi_k = h_phi_1 / hh - 1
575
+
576
+ factorial_i = 1
577
+
578
+ if self.config.solver_type == "bh1":
579
+ B_h = hh
580
+ elif self.config.solver_type == "bh2":
581
+ B_h = torch.expm1(hh)
582
+ else:
583
+ raise NotImplementedError()
584
+
585
+ for i in range(1, order + 1):
586
+ R.append(torch.pow(rks, i - 1))
587
+ b.append(h_phi_k * factorial_i / B_h)
588
+ factorial_i *= i + 1
589
+ h_phi_k = h_phi_k / hh - 1 / factorial_i
590
+
591
+ R = torch.stack(R)
592
+ b = torch.tensor(b, device=device)
593
+
594
+ if len(D1s) > 0:
595
+ D1s = torch.stack(D1s, dim=1)
596
+ else:
597
+ D1s = None
598
+
599
+ # for order 1, we use a simplified version
600
+ if order == 1:
601
+ rhos_c = torch.tensor([0.5], dtype=x.dtype, device=device)
602
+ else:
603
+ rhos_c = torch.linalg.solve(R, b).to(device).to(x.dtype)
604
+
605
+ if self.predict_x0:
606
+ x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0
607
+ if D1s is not None:
608
+ corr_res = torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s)
609
+ else:
610
+ corr_res = 0
611
+ D1_t = model_t - m0
612
+ x_t = x_t_ - alpha_t * B_h * (corr_res + rhos_c[-1] * D1_t)
613
+ else:
614
+ x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0
615
+ if D1s is not None:
616
+ corr_res = torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s)
617
+ else:
618
+ corr_res = 0
619
+ D1_t = model_t - m0
620
+ x_t = x_t_ - sigma_t * B_h * (corr_res + rhos_c[-1] * D1_t)
621
+ x_t = x_t.to(x.dtype)
622
+ return x_t
623
+
624
+ def index_for_timestep(self, timestep, schedule_timesteps=None):
625
+ if schedule_timesteps is None:
626
+ schedule_timesteps = self.timesteps
627
+
628
+ indices = (schedule_timesteps == timestep).nonzero()
629
+
630
+ # The sigma index that is taken for the **very** first `step`
631
+ # is always the second index (or the last index if there is only 1)
632
+ # This way we can ensure we don't accidentally skip a sigma in
633
+ # case we start in the middle of the denoising schedule (e.g. for image-to-image)
634
+ pos = 1 if len(indices) > 1 else 0
635
+
636
+ return indices[pos].item()
637
+
638
+ # Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler._init_step_index
639
+ def _init_step_index(self, timestep):
640
+ """
641
+ Initialize the step_index counter for the scheduler.
642
+ """
643
+
644
+ if self.begin_index is None:
645
+ if isinstance(timestep, torch.Tensor):
646
+ timestep = timestep.to(self.timesteps.device)
647
+ self._step_index = self.index_for_timestep(timestep)
648
+ else:
649
+ self._step_index = self._begin_index
650
+
651
+ def step(self,
652
+ model_output: torch.Tensor,
653
+ timestep: Union[int, torch.Tensor],
654
+ sample: torch.Tensor,
655
+ return_dict: bool = True,
656
+ generator=None) -> Union[SchedulerOutput, Tuple]:
657
+ """
658
+ Predict the sample from the previous timestep by reversing the SDE. This function propagates the sample with
659
+ the multistep UniPC.
660
+
661
+ Args:
662
+ model_output (`torch.Tensor`):
663
+ The direct output from learned diffusion model.
664
+ timestep (`int`):
665
+ The current discrete timestep in the diffusion chain.
666
+ sample (`torch.Tensor`):
667
+ A current instance of a sample created by the diffusion process.
668
+ return_dict (`bool`):
669
+ Whether or not to return a [`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`.
670
+
671
+ Returns:
672
+ [`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`:
673
+ If return_dict is `True`, [`~schedulers.scheduling_utils.SchedulerOutput`] is returned, otherwise a
674
+ tuple is returned where the first element is the sample tensor.
675
+
676
+ """
677
+ if self.num_inference_steps is None:
678
+ raise ValueError(
679
+ "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler"
680
+ )
681
+
682
+ if self.step_index is None:
683
+ self._init_step_index(timestep)
684
+
685
+ use_corrector = (
686
+ self.step_index > 0 and
687
+ self.step_index - 1 not in self.disable_corrector and
688
+ self.last_sample is not None # pyright: ignore
689
+ )
690
+
691
+ model_output_convert = self.convert_model_output(
692
+ model_output, sample=sample)
693
+ if use_corrector:
694
+ sample = self.multistep_uni_c_bh_update(
695
+ this_model_output=model_output_convert,
696
+ last_sample=self.last_sample,
697
+ this_sample=sample,
698
+ order=self.this_order,
699
+ )
700
+
701
+ for i in range(self.config.solver_order - 1):
702
+ self.model_outputs[i] = self.model_outputs[i + 1]
703
+ self.timestep_list[i] = self.timestep_list[i + 1]
704
+
705
+ self.model_outputs[-1] = model_output_convert
706
+ self.timestep_list[-1] = timestep # pyright: ignore
707
+
708
+ if self.config.lower_order_final:
709
+ this_order = min(self.config.solver_order,
710
+ len(self.timesteps) -
711
+ self.step_index) # pyright: ignore
712
+ else:
713
+ this_order = self.config.solver_order
714
+
715
+ self.this_order = min(this_order,
716
+ self.lower_order_nums + 1) # warmup for multistep
717
+ assert self.this_order > 0
718
+
719
+ self.last_sample = sample
720
+ prev_sample = self.multistep_uni_p_bh_update(
721
+ model_output=model_output, # pass the original non-converted model output, in case solver-p is used
722
+ sample=sample,
723
+ order=self.this_order,
724
+ )
725
+
726
+ if self.lower_order_nums < self.config.solver_order:
727
+ self.lower_order_nums += 1
728
+
729
+ # upon completion increase step index by one
730
+ self._step_index += 1 # pyright: ignore
731
+
732
+ if not return_dict:
733
+ return (prev_sample,)
734
+
735
+ return SchedulerOutput(prev_sample=prev_sample)
736
+
737
+ def scale_model_input(self, sample: torch.Tensor, *args,
738
+ **kwargs) -> torch.Tensor:
739
+ """
740
+ Ensures interchangeability with schedulers that need to scale the denoising model input depending on the
741
+ current timestep.
742
+
743
+ Args:
744
+ sample (`torch.Tensor`):
745
+ The input sample.
746
+
747
+ Returns:
748
+ `torch.Tensor`:
749
+ A scaled input sample.
750
+ """
751
+ return sample
752
+
753
+ # Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.add_noise
754
+ def add_noise(
755
+ self,
756
+ original_samples: torch.Tensor,
757
+ noise: torch.Tensor,
758
+ timesteps: torch.IntTensor,
759
+ ) -> torch.Tensor:
760
+ # Make sure sigmas and timesteps have the same device and dtype as original_samples
761
+ sigmas = self.sigmas.to(
762
+ device=original_samples.device, dtype=original_samples.dtype)
763
+ if original_samples.device.type == "mps" and torch.is_floating_point(
764
+ timesteps):
765
+ # mps does not support float64
766
+ schedule_timesteps = self.timesteps.to(
767
+ original_samples.device, dtype=torch.float32)
768
+ timesteps = timesteps.to(
769
+ original_samples.device, dtype=torch.float32)
770
+ else:
771
+ schedule_timesteps = self.timesteps.to(original_samples.device)
772
+ timesteps = timesteps.to(original_samples.device)
773
+
774
+ # begin_index is None when the scheduler is used for training or pipeline does not implement set_begin_index
775
+ if self.begin_index is None:
776
+ step_indices = [
777
+ self.index_for_timestep(t, schedule_timesteps)
778
+ for t in timesteps
779
+ ]
780
+ elif self.step_index is not None:
781
+ # add_noise is called after first denoising step (for inpainting)
782
+ step_indices = [self.step_index] * timesteps.shape[0]
783
+ else:
784
+ # add noise is called before first denoising step to create initial latent(img2img)
785
+ step_indices = [self.begin_index] * timesteps.shape[0]
786
+
787
+ sigma = sigmas[step_indices].flatten()
788
+ while len(sigma.shape) < len(original_samples.shape):
789
+ sigma = sigma.unsqueeze(-1)
790
+
791
+ alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
792
+ noisy_samples = alpha_t * original_samples + sigma_t * noise
793
+ return noisy_samples
794
+
795
+ def __len__(self):
796
+ return self.config.num_train_timesteps
lingbot_video/sglang_moe_shim.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import os
5
+ import sys
6
+ from contextlib import contextmanager
7
+ from dataclasses import dataclass
8
+ from types import ModuleType, SimpleNamespace
9
+ from typing import NamedTuple, Optional
10
+
11
+ import torch
12
+ import torch.nn.functional as F
13
+
14
+
15
+ @dataclass
16
+ class LightSglangMoeRunnerConfig:
17
+ num_experts: Optional[int] = None
18
+ num_local_experts: Optional[int] = None
19
+ hidden_size: Optional[int] = None
20
+ intermediate_size_per_partition: Optional[int] = None
21
+ layer_id: Optional[int] = None
22
+ top_k: Optional[int] = None
23
+ num_fused_shared_experts: Optional[int] = None
24
+ params_dtype: Optional[torch.dtype] = None
25
+ routing_method_type: Optional[object] = None
26
+ activation: str = "silu"
27
+ is_gated: bool = True
28
+ apply_router_weight_on_input: bool = False
29
+ inplace: bool = False
30
+ no_combine: bool = False
31
+ routed_scaling_factor: Optional[float] = None
32
+ gemm1_alpha: Optional[float] = None
33
+ gemm1_clamp_limit: Optional[float] = None
34
+ swiglu_limit: Optional[float] = None
35
+ gate_up_interleaved: bool = False
36
+
37
+
38
+ class LightSglangStandardTopKOutput(NamedTuple):
39
+ topk_weights: torch.Tensor
40
+ topk_ids: torch.Tensor
41
+ router_logits: torch.Tensor
42
+
43
+
44
+ SGLANG_MOE_SERVER_ARGS = SimpleNamespace(
45
+ enable_deterministic_inference=False,
46
+ enable_fused_moe_sum_all_reduce=False,
47
+ )
48
+ FP8_E4M3_MAX = 448.0
49
+ _SERVER_ARGS_READY = False
50
+
51
+
52
+ def sglang_env_flag(name: str, default: bool = False) -> bool:
53
+ raw = os.environ.get(name)
54
+ if raw is None:
55
+ return default
56
+ return raw.lower() in {"1", "true", "yes", "on"}
57
+
58
+
59
+ class SglangEnvFlag:
60
+ def __init__(self, name: str, default: bool = False):
61
+ self.name = name
62
+ self.default = default
63
+
64
+ def get(self) -> bool:
65
+ return sglang_env_flag(self.name, self.default)
66
+
67
+
68
+ def ensure_module(name: str, package_path: str | None = None) -> ModuleType:
69
+ module = sys.modules.get(name)
70
+ if module is None:
71
+ module = ModuleType(name)
72
+ sys.modules[name] = module
73
+ if package_path is not None:
74
+ module.__path__ = [package_path]
75
+ return module
76
+
77
+
78
+ def fp8_scale_from_amax(amax: torch.Tensor) -> torch.Tensor:
79
+ return torch.clamp(amax.float() / FP8_E4M3_MAX, min=1e-12)
80
+
81
+
82
+ def quantize_to_fp8_e4m3fn(input: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
83
+ return torch.clamp(input.float() / scale, -FP8_E4M3_MAX, FP8_E4M3_MAX).to(
84
+ torch.float8_e4m3fn
85
+ )
86
+
87
+
88
+ def raise_unsupported_sglang_quantization(*args, **kwargs):
89
+ raise RuntimeError("This LingBotVideo SGLang MoE shim only enables FP8 W8A8 quantization")
90
+
91
+
92
+ def sglang_scaled_fp8_quant(
93
+ input: torch.Tensor,
94
+ scale: Optional[torch.Tensor] = None,
95
+ num_token_padding: Optional[int] = None,
96
+ use_per_token_if_dynamic: bool = False,
97
+ ):
98
+ if input.ndim != 2:
99
+ raise ValueError(f"Expected 2D input tensor, got {input.ndim}D")
100
+ rows = input.shape[0]
101
+ output_rows = max(int(num_token_padding or 0), rows)
102
+ output = torch.empty((output_rows, input.shape[1]), device=input.device, dtype=torch.float8_e4m3fn)
103
+ input_contiguous = input.contiguous()
104
+ if scale is None:
105
+ if use_per_token_if_dynamic:
106
+ scale = fp8_scale_from_amax(input_contiguous.float().abs().amax(dim=1, keepdim=True))
107
+ if output_rows > rows:
108
+ padded_scale = torch.ones((output_rows, 1), device=input.device, dtype=torch.float32)
109
+ padded_scale[:rows] = scale
110
+ scale = padded_scale
111
+ else:
112
+ scale = fp8_scale_from_amax(input_contiguous.float().abs().amax()).reshape(1)
113
+ quant_scale = scale[:rows] if scale.ndim == 2 else scale
114
+ output[:rows] = quantize_to_fp8_e4m3fn(input_contiguous, quant_scale)
115
+ if output_rows > rows:
116
+ output[rows:] = torch.zeros(
117
+ (output_rows - rows, input.shape[1]), device=input.device, dtype=torch.float8_e4m3fn
118
+ )
119
+ return output, scale.contiguous()
120
+
121
+
122
+ def sglang_per_token_group_quant_fp8(
123
+ x: torch.Tensor,
124
+ group_size: int,
125
+ eps: float = 1e-10,
126
+ column_major_scales: bool = False,
127
+ scale_tma_aligned: bool = False,
128
+ scale_ue8m0: bool = False,
129
+ fuse_silu_and_mul: bool = False,
130
+ masked_m: Optional[torch.Tensor] = None,
131
+ enable_v2: Optional[bool] = None,
132
+ ):
133
+ if fuse_silu_and_mul:
134
+ half = x.shape[-1] // 2
135
+ x = (F.silu(x[..., :half]) * x[..., half:]).contiguous()
136
+ if x.shape[-1] % group_size != 0:
137
+ raise ValueError("The last dimension must be divisible by group_size")
138
+ x_view = x.contiguous().view(*x.shape[:-1], x.shape[-1] // group_size, group_size)
139
+ scales = fp8_scale_from_amax(x_view.float().abs().amax(dim=-1)).clamp_min(eps)
140
+ x_q = quantize_to_fp8_e4m3fn(x_view, scales.unsqueeze(-1)).view(x.shape)
141
+ if column_major_scales:
142
+ scales = scales.transpose(0, 1).contiguous()
143
+ return x_q.contiguous(), scales.contiguous()
144
+
145
+
146
+ def sglang_silu_and_mul(x: torch.Tensor, out: torch.Tensor | None = None, *args, **kwargs):
147
+ half = x.shape[-1] // 2
148
+ result = F.silu(x[..., :half]) * x[..., half:]
149
+ if out is not None:
150
+ out.copy_(result)
151
+ return out
152
+ return result
153
+
154
+
155
+ def sglang_gelu_and_mul(x: torch.Tensor, out: torch.Tensor | None = None, *args, **kwargs):
156
+ half = x.shape[-1] // 2
157
+ result = F.gelu(x[..., :half]) * x[..., half:]
158
+ if out is not None:
159
+ out.copy_(result)
160
+ return out
161
+ return result
162
+
163
+
164
+ @contextmanager
165
+ def null_sglang_config_override(config):
166
+ yield
167
+
168
+
169
+ def install_sglang_moe_import_shims() -> None:
170
+ try:
171
+ spec = importlib.util.find_spec("sglang")
172
+ except Exception:
173
+ spec = None
174
+ if spec is None or not spec.submodule_search_locations:
175
+ return
176
+
177
+ package_dir = next(iter(spec.submodule_search_locations))
178
+ srt_dir = os.path.join(package_dir, "srt")
179
+ layers_dir = os.path.join(srt_dir, "layers")
180
+ moe_dir = os.path.join(layers_dir, "moe")
181
+ moe_runner_dir = os.path.join(moe_dir, "moe_runner")
182
+ triton_utils_dir = os.path.join(moe_runner_dir, "triton_utils")
183
+ jit_kernel_dir = os.path.join(package_dir, "jit_kernel")
184
+
185
+ ensure_module("sglang", package_dir)
186
+ ensure_module("sglang.srt", srt_dir)
187
+ ensure_module("sglang.srt.layers", layers_dir)
188
+ ensure_module("sglang.srt.layers.moe", moe_dir)
189
+ moe_runner_module = ensure_module("sglang.srt.layers.moe.moe_runner", moe_runner_dir)
190
+ moe_runner_module.MoeRunnerConfig = LightSglangMoeRunnerConfig
191
+ triton_utils_module = ensure_module(
192
+ "sglang.srt.layers.moe.moe_runner.triton_utils", triton_utils_dir
193
+ )
194
+ triton_utils_module.get_config = lambda: None
195
+ triton_utils_module.override_config = null_sglang_config_override
196
+ ensure_module("sglang.jit_kernel", jit_kernel_dir)
197
+ activation_module = ensure_module("sglang.jit_kernel.activation")
198
+ activation_module.silu_and_mul = sglang_silu_and_mul
199
+ activation_module.gelu_and_mul = sglang_gelu_and_mul
200
+
201
+ server_args_module = ensure_module("sglang.srt.server_args")
202
+ server_args_module.get_global_server_args = lambda: SGLANG_MOE_SERVER_ARGS
203
+ server_args_module.set_global_server_args_for_scheduler = lambda args: None
204
+
205
+ batch_module = ensure_module("sglang.srt.batch_invariant_ops")
206
+ batch_module.is_batch_invariant_mode_enabled = lambda: False
207
+
208
+ environ_module = ensure_module("sglang.srt.environ")
209
+ environ_module.envs = SimpleNamespace(
210
+ SGLANG_OPT_SWIGLU_CLAMP_FUSION=SglangEnvFlag("SGLANG_OPT_SWIGLU_CLAMP_FUSION"),
211
+ SGLANG_EXPERIMENTAL_LORA_OPTI=SglangEnvFlag("SGLANG_EXPERIMENTAL_LORA_OPTI"),
212
+ )
213
+
214
+ utils_module = ensure_module("sglang.srt.utils")
215
+ utils_module.cpu_has_amx_support = lambda: False
216
+ utils_module.get_bool_env_var = sglang_env_flag
217
+ utils_module.is_cpu = lambda: not torch.cuda.is_available()
218
+ utils_module.is_cuda = torch.cuda.is_available
219
+ utils_module.is_hip = lambda: False
220
+ utils_module.is_musa = lambda: False
221
+ utils_module.is_xpu = lambda: False
222
+ utils_module.use_intel_xpu_backend = lambda: False
223
+ utils_module.get_device_name = lambda: torch.cuda.get_device_name() if torch.cuda.is_available() else "cpu"
224
+ utils_module.is_sm90_supported = (
225
+ lambda: torch.cuda.is_available() and torch.cuda.get_device_capability()[0] >= 9
226
+ )
227
+
228
+ custom_op_module = ensure_module("sglang.srt.utils.custom_op")
229
+ custom_op_module.register_custom_op = lambda *args, **kwargs: (lambda fn: fn)
230
+
231
+ moe_utils_module = ensure_module("sglang.srt.layers.moe.utils")
232
+ moe_utils_module.get_moe_padding_size = lambda is_aiter_moe: 0
233
+
234
+ fp8_module = ensure_module("sglang.srt.layers.quantization.fp8_kernel")
235
+ fp8_module.per_token_group_quant_fp8 = sglang_per_token_group_quant_fp8
236
+ fp8_module.scaled_fp8_quant = sglang_scaled_fp8_quant
237
+ fp8_module.sglang_per_token_group_quant_fp8 = sglang_per_token_group_quant_fp8
238
+
239
+ int8_module = ensure_module("sglang.srt.layers.quantization.int8_kernel")
240
+ int8_module.per_token_group_quant_int8 = raise_unsupported_sglang_quantization
241
+ int8_module.per_token_quant_int8 = raise_unsupported_sglang_quantization
242
+ int8_module.sglang_per_token_group_quant_int8 = raise_unsupported_sglang_quantization
243
+
244
+
245
+ try:
246
+ from sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe import (
247
+ fused_experts as sglang_fused_experts,
248
+ )
249
+ except Exception as direct_exc: # pragma: no cover - optional deployment dependency
250
+ install_sglang_moe_import_shims()
251
+ try:
252
+ from sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe import (
253
+ fused_experts as sglang_fused_experts,
254
+ )
255
+ except Exception as shim_exc: # pragma: no cover - optional deployment dependency
256
+ sglang_fused_experts = None
257
+ SGLANG_MOE_IMPORT_ERROR = shim_exc
258
+ else:
259
+ SGLANG_MOE_IMPORT_ERROR = None
260
+ else:
261
+ SGLANG_MOE_IMPORT_ERROR = None
262
+
263
+ try:
264
+ from sglang.srt.server_args import (
265
+ get_global_server_args as sglang_get_global_server_args,
266
+ set_global_server_args_for_scheduler as sglang_set_global_server_args_for_scheduler,
267
+ )
268
+ except Exception: # pragma: no cover - optional deployment dependency
269
+ sglang_get_global_server_args = None
270
+ sglang_set_global_server_args_for_scheduler = None
271
+
272
+
273
+ def _ensure_moe_server_args_attrs(args) -> None:
274
+ if not hasattr(args, "enable_deterministic_inference"):
275
+ args.enable_deterministic_inference = False
276
+ if not hasattr(args, "enable_fused_moe_sum_all_reduce"):
277
+ args.enable_fused_moe_sum_all_reduce = False
278
+
279
+
280
+ def ensure_sglang_moe_ready() -> None:
281
+ global _SERVER_ARGS_READY
282
+ if sglang_fused_experts is None:
283
+ raise RuntimeError(
284
+ "LINGBOT_MOE_EXPERT_BACKEND=sglang_triton requires SGLang MoE runtime"
285
+ ) from SGLANG_MOE_IMPORT_ERROR
286
+ if _SERVER_ARGS_READY:
287
+ return
288
+ _ensure_moe_server_args_attrs(SGLANG_MOE_SERVER_ARGS)
289
+ if sglang_get_global_server_args is not None:
290
+ try:
291
+ server_args = sglang_get_global_server_args()
292
+ except Exception:
293
+ server_args = SGLANG_MOE_SERVER_ARGS
294
+ if sglang_set_global_server_args_for_scheduler is not None:
295
+ sglang_set_global_server_args_for_scheduler(server_args)
296
+ _ensure_moe_server_args_attrs(server_args)
297
+ _SERVER_ARGS_READY = True
lingbot_video/transformer_lingbot_video.py ADDED
@@ -0,0 +1,1312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import os
3
+ from typing import Optional, Tuple
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+ import torch.distributed as dist
9
+
10
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
11
+ from diffusers.models.attention_dispatch import dispatch_attention_fn
12
+ from diffusers.models._modeling_parallel import ContextParallelInput, ContextParallelOutput
13
+ from diffusers.models.embeddings import TimestepEmbedding, Timesteps
14
+ from diffusers.models.modeling_outputs import Transformer2DModelOutput
15
+ from diffusers.models.modeling_utils import ModelMixin
16
+
17
+ try:
18
+ from flash_attn_interface import flash_attn_varlen_func as flash_attn_varlen_func_v3
19
+ except Exception: # pragma: no cover - optional CUDA kernel.
20
+ flash_attn_varlen_func_v3 = None
21
+
22
+ try:
23
+ from .moe_pack_kernels import reorder_tokens_triton_pack
24
+ from .moe_restore_kernels import restore_tokens_triton
25
+ from .sglang_moe_shim import (
26
+ LightSglangMoeRunnerConfig,
27
+ LightSglangStandardTopKOutput,
28
+ ensure_sglang_moe_ready,
29
+ fp8_scale_from_amax,
30
+ quantize_to_fp8_e4m3fn,
31
+ sglang_fused_experts,
32
+ )
33
+ except ImportError: # pragma: no cover - allows direct file loading in diagnostics.
34
+ from moe_pack_kernels import reorder_tokens_triton_pack
35
+ from moe_restore_kernels import restore_tokens_triton
36
+ from sglang_moe_shim import (
37
+ LightSglangMoeRunnerConfig,
38
+ LightSglangStandardTopKOutput,
39
+ ensure_sglang_moe_ready,
40
+ fp8_scale_from_amax,
41
+ quantize_to_fp8_e4m3fn,
42
+ sglang_fused_experts,
43
+ )
44
+
45
+
46
+ LINGBOT_VIDEO_FP32_MODULES = (
47
+ "time_embedder",
48
+ "time_modulation",
49
+ "scale_shift_table",
50
+ "norm",
51
+ "norm1",
52
+ "norm2",
53
+ "norm_q",
54
+ "norm_k",
55
+ "norm_post_attn",
56
+ "norm_post_ffn",
57
+ "norm_out",
58
+ "norm_out_modulation",
59
+ "router",
60
+ )
61
+
62
+
63
+ def should_keep_in_fp32(name: str) -> bool:
64
+ return any(module_name in name.split(".") for module_name in LINGBOT_VIDEO_FP32_MODULES)
65
+
66
+
67
+ def _moe_expert_backend() -> str:
68
+ return os.environ.get("LINGBOT_MOE_EXPERT_BACKEND", "grouped_mm").lower().strip()
69
+
70
+
71
+ def _moe_pad_backend() -> str:
72
+ return os.environ.get("LINGBOT_MOE_PAD_BACKEND", "loop").lower().strip()
73
+
74
+
75
+ def _moe_reorder_backend() -> str:
76
+ return os.environ.get("LINGBOT_MOE_REORDER_BACKEND", "sort").lower().strip()
77
+
78
+
79
+ def _moe_restore_backend() -> str:
80
+ return os.environ.get("LINGBOT_MOE_RESTORE_BACKEND", "scatter").lower().strip()
81
+
82
+
83
+ def _all_to_all_split_cat(
84
+ local_input: torch.Tensor,
85
+ scatter_dim: int,
86
+ gather_dim: int,
87
+ group: dist.ProcessGroup,
88
+ ) -> torch.Tensor:
89
+ world_size = dist.get_world_size(group)
90
+ input_list = [
91
+ tensor.contiguous()
92
+ for tensor in torch.tensor_split(local_input, world_size, scatter_dim)
93
+ ]
94
+ output_list = [torch.empty_like(input_list[0]) for _ in range(world_size)]
95
+ dist.all_to_all(output_list, input_list, group=group)
96
+ return torch.cat(output_list, dim=gather_dim).contiguous()
97
+
98
+
99
+ class LingBotVideoRMSNorm(nn.Module):
100
+ """RMSNorm with fp32 accumulation."""
101
+
102
+ def __init__(self, dim: int, eps: float = 1e-6):
103
+ super().__init__()
104
+ self.weight = nn.Parameter(torch.ones(dim))
105
+ self.variance_epsilon = eps
106
+
107
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
108
+ input_dtype = hidden_states.dtype
109
+ hidden_states = hidden_states.to(torch.float32)
110
+ variance = hidden_states.pow(2).mean(-1, keepdim=True)
111
+ hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
112
+ return (self.weight * hidden_states).to(input_dtype)
113
+
114
+
115
+ def apply_rotary_emb(x: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:
116
+ """Apply complex RoPE to `(B, S, H, D)` attention tensors."""
117
+ with torch.amp.autocast("cuda", enabled=False):
118
+ x_c = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
119
+ out = torch.view_as_real(x_c * freqs_cis.unsqueeze(2)).flatten(3)
120
+ return out.type_as(x)
121
+
122
+
123
+ class LingBotVideoRotaryEmbedding(nn.Module):
124
+ """Complex64 RoPE table indexed by position ids."""
125
+
126
+ def __init__(self, axes_dims: Tuple[int, ...], axes_lens: Tuple[int, ...], theta: float):
127
+ super().__init__()
128
+ self.axes_dims = tuple(axes_dims)
129
+ self.axes_lens = list(axes_lens)
130
+ self.theta = theta
131
+ self.freqs_cis = None
132
+
133
+ @staticmethod
134
+ def precompute_freqs_cis(dim: Tuple[int, ...], end: Tuple[int, ...], theta: float):
135
+ freqs_cis = []
136
+ for d, e in zip(dim, end):
137
+ freqs = 1.0 / (
138
+ theta ** (torch.arange(0, d, 2, dtype=torch.float64, device="cpu") / d)
139
+ )
140
+ timestep = torch.arange(e, device=freqs.device, dtype=torch.float64)
141
+ freqs = torch.outer(timestep, freqs).float()
142
+ freqs_cis.append(torch.polar(torch.ones_like(freqs), freqs).to(torch.complex64))
143
+ return freqs_cis
144
+
145
+ def forward(self, position_ids: torch.Tensor) -> torch.Tensor:
146
+ # position_ids: (S, 3) int → (S, head_dim/2) complex64
147
+ device = position_ids.device
148
+ max_vals = position_ids.max(dim=0).values.tolist()
149
+ needs_rebuild = self.freqs_cis is None or any(m >= l for m, l in zip(max_vals, self.axes_lens))
150
+ if needs_rebuild:
151
+ for i in range(len(self.axes_lens)):
152
+ if max_vals[i] >= self.axes_lens[i]:
153
+ self.axes_lens[i] = int(max_vals[i] * 1.5) + 1
154
+ self.freqs_cis = self.precompute_freqs_cis(
155
+ self.axes_dims, tuple(self.axes_lens), theta=self.theta
156
+ )
157
+ self.freqs_cis = [freqs_cis.to(device) for freqs_cis in self.freqs_cis]
158
+ elif self.freqs_cis[0].device != device:
159
+ self.freqs_cis = [freqs_cis.to(device) for freqs_cis in self.freqs_cis]
160
+
161
+ return torch.cat([self.freqs_cis[i][position_ids[:, i]] for i in range(len(self.axes_dims))], dim=-1)
162
+
163
+
164
+ def make_joint_position_ids(
165
+ text_len: int, grid_t: int, grid_h: int, grid_w: int, device: torch.device
166
+ ) -> torch.Tensor:
167
+ """3D positions in [video; text] order. Text t-axis is 1..text_len; video t-axis starts at text_len+1.
168
+
169
+ Matches patchify_and_embed: cap start (1,0,0); vision start (cap_len+1,0,0);
170
+ freqs ordered with x first and cap second (same order as cat_interleave).
171
+ """
172
+ tt = torch.arange(grid_t, device=device, dtype=torch.int32) + (text_len + 1)
173
+ hh = torch.arange(grid_h, device=device, dtype=torch.int32)
174
+ ww = torch.arange(grid_w, device=device, dtype=torch.int32)
175
+ grid = torch.stack(torch.meshgrid(tt, hh, ww, indexing="ij"), dim=-1).flatten(0, 2)
176
+ text_t = torch.arange(text_len, device=device, dtype=torch.int32) + 1
177
+ text_pos = torch.stack(
178
+ [text_t, torch.zeros_like(text_t), torch.zeros_like(text_t)], dim=-1
179
+ )
180
+ return torch.cat([grid, text_pos], dim=0) # (Nx + L, 3)
181
+
182
+
183
+ def _cat_interleave(
184
+ a: torch.Tensor,
185
+ len_a: list[int],
186
+ b: torch.Tensor,
187
+ len_b: list[int],
188
+ ) -> torch.Tensor:
189
+ a_split = torch.split(a, len_a, dim=1)
190
+ b_split = torch.split(b, len_b, dim=1)
191
+ blocks: list[torch.Tensor] = []
192
+ for x_part, text_part in zip(a_split, b_split):
193
+ blocks.extend([x_part, text_part])
194
+ return torch.cat(blocks, dim=1)
195
+
196
+
197
+ class LingBotVideoTextEmbedder(nn.Module):
198
+ """Matches CondProjection: RMSNorm(text_dim, eps=1e-6 fixed) -> Linear-SiLU-Linear."""
199
+
200
+ def __init__(self, text_dim: int, hidden_size: int):
201
+ super().__init__()
202
+ self.norm = LingBotVideoRMSNorm(text_dim, eps=1e-6)
203
+ self.linear_1 = nn.Linear(text_dim, hidden_size, bias=True)
204
+ self.linear_2 = nn.Linear(hidden_size, hidden_size, bias=True)
205
+
206
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
207
+ x = self.norm(x)
208
+ return self.linear_2(F.silu(self.linear_1(x)))
209
+
210
+
211
+ class LingBotVideoAttention(nn.Module):
212
+ def __init__(self, hidden_size, num_heads, norm_eps, qkv_bias, out_bias):
213
+ super().__init__()
214
+ self.num_heads = num_heads
215
+ self.head_dim = hidden_size // num_heads
216
+ self.to_q = nn.Linear(hidden_size, hidden_size, bias=qkv_bias)
217
+ self.to_k = nn.Linear(hidden_size, hidden_size, bias=qkv_bias)
218
+ self.to_v = nn.Linear(hidden_size, hidden_size, bias=qkv_bias)
219
+ self.norm_q = LingBotVideoRMSNorm(self.head_dim, norm_eps)
220
+ self.norm_k = LingBotVideoRMSNorm(self.head_dim, norm_eps)
221
+ self.to_out = nn.Linear(hidden_size, hidden_size, bias=out_bias)
222
+
223
+ def forward(
224
+ self,
225
+ x,
226
+ rotary_emb,
227
+ attention_mask=None,
228
+ packed_indices: Optional[dict[str, torch.Tensor]] = None,
229
+ parallel_config=None,
230
+ ):
231
+ B, S, _ = x.shape
232
+ if os.environ.get("LINGBOT_FUSED_QKV_LINEAR") == "1":
233
+ weight = torch.cat(
234
+ (self.to_q.weight, self.to_k.weight, self.to_v.weight),
235
+ dim=0,
236
+ )
237
+ bias = None
238
+ if self.to_q.bias is not None:
239
+ bias = torch.cat(
240
+ (self.to_q.bias, self.to_k.bias, self.to_v.bias),
241
+ dim=0,
242
+ )
243
+ qkv = F.linear(x, weight, bias)
244
+ q, k, v = qkv.view(B, S, 3, self.num_heads, self.head_dim).unbind(2)
245
+ else:
246
+ q = self.to_q(x).unflatten(2, (self.num_heads, self.head_dim))
247
+ k = self.to_k(x).unflatten(2, (self.num_heads, self.head_dim))
248
+ v = self.to_v(x).unflatten(2, (self.num_heads, self.head_dim))
249
+ q = apply_rotary_emb(self.norm_q(q), rotary_emb)
250
+ k = apply_rotary_emb(self.norm_k(k), rotary_emb)
251
+ # dispatch_attention_fn expects (B, S, H, D) in and out (same as the diffusers Wan processor)
252
+ if packed_indices is None:
253
+ out = dispatch_attention_fn(
254
+ q,
255
+ k,
256
+ v,
257
+ attn_mask=attention_mask,
258
+ parallel_config=parallel_config,
259
+ )
260
+ else:
261
+ if flash_attn_varlen_func_v3 is None:
262
+ raise RuntimeError("flash_attn_interface.flash_attn_varlen_func is required.")
263
+ if parallel_config is None:
264
+ result = flash_attn_varlen_func_v3(
265
+ q=q.reshape(-1, self.num_heads, self.head_dim),
266
+ k=k.reshape(-1, self.num_heads, self.head_dim),
267
+ v=v.reshape(-1, self.num_heads, self.head_dim),
268
+ cu_seqlens_q=packed_indices["cu_seqlens_kv"],
269
+ cu_seqlens_k=packed_indices["cu_seqlens_kv"],
270
+ max_seqlen_q=packed_indices["max_seqlen_in_batch_kv"],
271
+ max_seqlen_k=packed_indices["max_seqlen_in_batch_kv"],
272
+ causal=False,
273
+ )
274
+ out = result[0] if isinstance(result, tuple) else result
275
+ out = out.reshape(B, S, self.num_heads, self.head_dim)
276
+ else:
277
+ group = parallel_config.context_parallel_config._ulysses_mesh.get_group()
278
+ world_size = dist.get_world_size(group)
279
+ local_heads = self.num_heads // world_size
280
+ q_global = _all_to_all_split_cat(
281
+ q.reshape(B, S, self.num_heads * self.head_dim),
282
+ scatter_dim=2,
283
+ gather_dim=1,
284
+ group=group,
285
+ ).view(B, S * world_size, local_heads, self.head_dim)
286
+ k_global = _all_to_all_split_cat(
287
+ k.reshape(B, S, self.num_heads * self.head_dim),
288
+ scatter_dim=2,
289
+ gather_dim=1,
290
+ group=group,
291
+ ).view(B, S * world_size, local_heads, self.head_dim)
292
+ v_global = _all_to_all_split_cat(
293
+ v.reshape(B, S, self.num_heads * self.head_dim),
294
+ scatter_dim=2,
295
+ gather_dim=1,
296
+ group=group,
297
+ ).view(B, S * world_size, local_heads, self.head_dim)
298
+ q_flat = q_global.reshape(-1, local_heads, self.head_dim)
299
+ k_flat = k_global.reshape(-1, local_heads, self.head_dim)
300
+ v_flat = v_global.reshape(-1, local_heads, self.head_dim)
301
+ result = flash_attn_varlen_func_v3(
302
+ q=q_flat,
303
+ k=k_flat,
304
+ v=v_flat,
305
+ cu_seqlens_q=packed_indices["cu_seqlens_kv"],
306
+ cu_seqlens_k=packed_indices["cu_seqlens_kv"],
307
+ max_seqlen_q=packed_indices["max_seqlen_in_batch_kv"],
308
+ max_seqlen_k=packed_indices["max_seqlen_in_batch_kv"],
309
+ causal=False,
310
+ )
311
+ out_global = result[0] if isinstance(result, tuple) else result
312
+ out_global = out_global.reshape(B, S * world_size, local_heads * self.head_dim)
313
+ out = _all_to_all_split_cat(
314
+ out_global,
315
+ scatter_dim=1,
316
+ gather_dim=2,
317
+ group=group,
318
+ ).view(B, S, self.num_heads, self.head_dim)
319
+ return self.to_out(out.flatten(2, 3).type_as(x))
320
+
321
+
322
+ class LingBotVideoMLP(nn.Module):
323
+ def __init__(self, hidden_size, intermediate_size):
324
+ super().__init__()
325
+ self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
326
+ self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
327
+ self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)
328
+
329
+ def forward(self, x):
330
+ return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
331
+
332
+
333
+ class LingBotVideoRouter(nn.Module):
334
+ """Matches the TokenChoiceTopKRouter inference path (no capacity/jitter/load stats).
335
+
336
+ The asymmetry must be preserved: selection uses the bias-added score, while gating
337
+ weights gather the bias-free score.
338
+ """
339
+
340
+ def __init__(self, hidden_size, num_experts, top_k, score_func, norm_topk_prob,
341
+ n_group, topk_group, route_scale):
342
+ super().__init__()
343
+ self.num_experts = num_experts
344
+ self.top_k = top_k
345
+ self.score_func = score_func
346
+ self.norm_topk_prob = norm_topk_prob
347
+ self.n_group = n_group
348
+ self.topk_group = topk_group
349
+ self.route_scale = route_scale
350
+ self.weight = nn.Parameter(torch.empty(num_experts, hidden_size))
351
+ self.register_buffer("e_score_correction_bias", torch.zeros(num_experts), persistent=True)
352
+
353
+ def _group_limited_topk(self, scores_for_choice):
354
+ seq_len = scores_for_choice.shape[0]
355
+ experts_per_group = self.num_experts // self.n_group
356
+ grouped = scores_for_choice.view(seq_len, self.n_group, experts_per_group)
357
+ group_scores = grouped.topk(2, dim=-1)[0].sum(dim=-1)
358
+ group_idx = torch.topk(group_scores, k=self.topk_group, dim=-1, sorted=False)[1]
359
+ group_mask = torch.zeros_like(group_scores)
360
+ group_mask.scatter_(1, group_idx, 1)
361
+ score_mask = (
362
+ group_mask.unsqueeze(-1)
363
+ .expand(seq_len, self.n_group, experts_per_group)
364
+ .reshape(seq_len, -1)
365
+ )
366
+ masked = scores_for_choice.masked_fill(~score_mask.bool(), float("-inf"))
367
+ return torch.topk(masked, k=self.top_k, dim=-1, sorted=False)[1]
368
+
369
+ def forward(self, tokens: torch.Tensor):
370
+ with torch.amp.autocast(tokens.device.type, enabled=False):
371
+ logits = F.linear(tokens.float(), self.weight.float())
372
+ if self.score_func == "softmax":
373
+ scores = F.softmax(logits, dim=-1)
374
+ else:
375
+ scores = logits.sigmoid()
376
+ scores_for_choice = scores + self.e_score_correction_bias.unsqueeze(0)
377
+ if self.n_group is not None and self.n_group > 1:
378
+ top_indices = self._group_limited_topk(scores_for_choice)
379
+ else:
380
+ top_indices = torch.topk(scores_for_choice, k=self.top_k, dim=-1, sorted=False)[1]
381
+ top_scores = scores.gather(1, top_indices)
382
+ if self.top_k > 1 and self.norm_topk_prob:
383
+ top_scores = top_scores / (top_scores.sum(dim=-1, keepdim=True) + 1e-20)
384
+ top_scores = top_scores * self.route_scale
385
+ return top_indices, top_scores.to(tokens.dtype), logits, scores, scores_for_choice
386
+
387
+
388
+ class LingBotVideoGroupedExperts(nn.Module):
389
+ """Weight layout matches GroupedExperts: w1 [E,I,H], w2 [E,H,I], w3 [E,I,H]. Eager per-expert compute."""
390
+
391
+ def __init__(self, num_experts, hidden_size, intermediate_size):
392
+ super().__init__()
393
+ self.num_experts = num_experts
394
+ self.w1 = nn.Parameter(torch.empty(num_experts, intermediate_size, hidden_size))
395
+ self.w2 = nn.Parameter(torch.empty(num_experts, hidden_size, intermediate_size))
396
+ self.w3 = nn.Parameter(torch.empty(num_experts, intermediate_size, hidden_size))
397
+
398
+
399
+ def _round_up_to_multiple(value: int, multiple: int) -> int:
400
+ return ((value + multiple - 1) // multiple) * multiple
401
+
402
+
403
+ class LingBotVideoSparseMoeBlock(nn.Module):
404
+ def __init__(self, hidden_size, intermediate_size, num_experts, top_k,
405
+ moe_intermediate_size, score_func, norm_topk_prob, n_group, topk_group,
406
+ routed_scaling_factor, n_shared_experts):
407
+ super().__init__()
408
+ self.hidden_size = hidden_size
409
+ self.num_experts = num_experts
410
+ self.router = LingBotVideoRouter(
411
+ hidden_size, num_experts, top_k, score_func, norm_topk_prob,
412
+ n_group, topk_group, routed_scaling_factor,
413
+ )
414
+ self.experts = LingBotVideoGroupedExperts(num_experts, hidden_size, moe_intermediate_size)
415
+ self._sglang_w13_cache: Optional[torch.Tensor] = None
416
+ self._sglang_w13_cache_key = None
417
+ self._sglang_fp8_cache = None
418
+ self._sglang_fp8_cache_key = None
419
+ self.shared_experts = None
420
+ if n_shared_experts is not None and n_shared_experts > 0:
421
+ self.shared_experts = LingBotVideoMLP(
422
+ hidden_size, moe_intermediate_size * n_shared_experts
423
+ )
424
+
425
+ @staticmethod
426
+ def _reorder_tokens(tokens: torch.Tensor, top_scores: torch.Tensor, top_indices: torch.Tensor, num_experts: int):
427
+ backend = _moe_reorder_backend()
428
+ if backend in {"triton_pack", "pack", "triton"}:
429
+ return reorder_tokens_triton_pack(tokens, top_scores, top_indices, num_experts)
430
+ if backend not in {"sort", "argsort", "default"}:
431
+ raise ValueError(
432
+ f"Unsupported LINGBOT_MOE_REORDER_BACKEND={backend!r}; "
433
+ "expected sort or triton_pack"
434
+ )
435
+ num_tokens = tokens.shape[0]
436
+ top_k = top_indices.shape[1]
437
+ flat_scores = top_scores.reshape(-1)
438
+ flat_indices = top_indices.reshape(-1)
439
+ active_positions = torch.where(flat_scores != 0)[0]
440
+ active_experts = flat_indices[active_positions]
441
+
442
+ counts = torch.zeros(num_experts, device=tokens.device, dtype=torch.int64)
443
+ counts.scatter_add_(0, active_experts, torch.ones_like(active_experts, dtype=torch.int64))
444
+
445
+ sort_order = torch.argsort(active_experts, stable=True)
446
+ sorted_positions = active_positions[sort_order]
447
+ sorted_scores = flat_scores[sorted_positions]
448
+ original_token_idx = sorted_positions // top_k
449
+ permuted_tokens = tokens[original_token_idx]
450
+ return permuted_tokens, counts, sorted_positions, sorted_scores, num_tokens, top_k
451
+
452
+ @staticmethod
453
+ def _pad_grouped_tokens_loop(tokens: torch.Tensor, counts: torch.Tensor, align: int = 8):
454
+ num_tokens = tokens.shape[0]
455
+ num_experts = int(counts.shape[0])
456
+ max_len = _round_up_to_multiple(num_tokens + num_experts * align, align)
457
+ counts_i64 = counts.to(torch.int64)
458
+ total_per_expert = torch.clamp_min(counts_i64, align)
459
+ aligned_counts = (
460
+ (total_per_expert + align - 1) // align * align
461
+ ).to(torch.int32)
462
+ write_offsets = torch.cumsum(aligned_counts, dim=0) - aligned_counts
463
+ start_indices = torch.cumsum(counts_i64, dim=0) - counts_i64
464
+
465
+ fill_value = num_tokens
466
+ permuted_indices = torch.full(
467
+ (max_len,), fill_value, dtype=torch.int64, device=tokens.device
468
+ )
469
+ for expert_idx in range(num_experts):
470
+ length = int(counts_i64[expert_idx].item())
471
+ if length == 0:
472
+ continue
473
+ write_start = int(write_offsets[expert_idx].item())
474
+ start = int(start_indices[expert_idx].item())
475
+ permuted_indices[write_start:write_start + length] = torch.arange(
476
+ start, start + length, device=tokens.device, dtype=torch.int64
477
+ )
478
+
479
+ tokens_with_pad = torch.vstack((tokens, tokens.new_zeros((tokens.shape[-1],))))
480
+ input_shape = tokens_with_pad.shape
481
+ return input_shape, tokens_with_pad[permuted_indices], permuted_indices, aligned_counts
482
+
483
+ @staticmethod
484
+ def _pad_grouped_tokens_vectorized(tokens: torch.Tensor, counts: torch.Tensor, align: int = 8):
485
+ num_tokens = tokens.shape[0]
486
+ num_experts = int(counts.shape[0])
487
+ max_len = _round_up_to_multiple(num_tokens + num_experts * align, align)
488
+ counts_i64 = counts.to(torch.int64)
489
+ total_per_expert = torch.clamp_min(counts_i64, align)
490
+ aligned_counts_i64 = (total_per_expert + align - 1) // align * align
491
+ write_offsets = torch.cumsum(aligned_counts_i64, dim=0) - aligned_counts_i64
492
+ end_offsets = torch.cumsum(aligned_counts_i64, dim=0)
493
+ start_indices = torch.cumsum(counts_i64, dim=0) - counts_i64
494
+
495
+ slots = torch.arange(max_len, dtype=torch.int64, device=tokens.device)
496
+ expert_idx = torch.bucketize(slots, end_offsets, right=True)
497
+ valid_expert = expert_idx < num_experts
498
+ safe_expert_idx = expert_idx.clamp(max=num_experts - 1)
499
+ local_idx = slots - write_offsets[safe_expert_idx]
500
+ source_idx = start_indices[safe_expert_idx] + local_idx
501
+ valid = valid_expert & (local_idx < counts_i64[safe_expert_idx])
502
+ fill = torch.full_like(source_idx, num_tokens)
503
+ permuted_indices = torch.where(valid, source_idx, fill)
504
+
505
+ tokens_with_pad = torch.vstack((tokens, tokens.new_zeros((tokens.shape[-1],))))
506
+ input_shape = tokens_with_pad.shape
507
+ return (
508
+ input_shape,
509
+ tokens_with_pad[permuted_indices],
510
+ permuted_indices,
511
+ aligned_counts_i64.to(torch.int32),
512
+ )
513
+
514
+ @staticmethod
515
+ def _pad_grouped_tokens(tokens: torch.Tensor, counts: torch.Tensor, align: int = 8):
516
+ backend = _moe_pad_backend()
517
+ if backend in {"loop", "default"}:
518
+ return LingBotVideoSparseMoeBlock._pad_grouped_tokens_loop(tokens, counts, align)
519
+ if backend in {"vectorized", "torch"}:
520
+ return LingBotVideoSparseMoeBlock._pad_grouped_tokens_vectorized(tokens, counts, align)
521
+ raise ValueError(
522
+ f"Unsupported LINGBOT_MOE_PAD_BACKEND={backend!r}; expected loop or vectorized"
523
+ )
524
+
525
+ @staticmethod
526
+ def _unpad_grouped_tokens(output: torch.Tensor, input_shape: torch.Size, permuted_indices: torch.Tensor):
527
+ unpermuted = output.new_empty(input_shape)
528
+ unpermuted[permuted_indices, :] = output
529
+ return unpermuted[:-1]
530
+
531
+ def _run_grouped_experts(self, tokens: torch.Tensor, counts: torch.Tensor) -> torch.Tensor:
532
+ if not hasattr(torch, "_grouped_mm"):
533
+ return self._run_experts_for_loop(tokens, counts)
534
+ input_shape, padded_tokens, permuted_indices, aligned_counts = self._pad_grouped_tokens(tokens, counts)
535
+ offsets = torch.cumsum(aligned_counts, dim=0, dtype=torch.int32)
536
+ h = F.silu(
537
+ torch._grouped_mm(
538
+ padded_tokens.bfloat16(),
539
+ self.experts.w1.bfloat16().transpose(-2, -1),
540
+ offs=offsets,
541
+ )
542
+ )
543
+ h = h * torch._grouped_mm(
544
+ padded_tokens.bfloat16(),
545
+ self.experts.w3.bfloat16().transpose(-2, -1),
546
+ offs=offsets,
547
+ )
548
+ out = torch._grouped_mm(
549
+ h,
550
+ self.experts.w2.bfloat16().transpose(-2, -1),
551
+ offs=offsets,
552
+ ).type_as(padded_tokens)
553
+ return self._unpad_grouped_tokens(out, input_shape, permuted_indices)
554
+
555
+ def _run_experts_for_loop(self, tokens: torch.Tensor, counts: torch.Tensor) -> torch.Tensor:
556
+ count_list = counts.tolist()
557
+ splits = torch.split(tokens, count_list, dim=0)
558
+ outputs = []
559
+ for expert_idx, expert_tokens in enumerate(splits):
560
+ if expert_tokens.numel() == 0:
561
+ continue
562
+ h = F.silu(expert_tokens @ self.experts.w1[expert_idx].transpose(-2, -1))
563
+ h = h * (expert_tokens @ self.experts.w3[expert_idx].transpose(-2, -1))
564
+ h = h @ self.experts.w2[expert_idx].transpose(-2, -1)
565
+ outputs.append(h)
566
+ if not outputs:
567
+ return tokens.new_zeros(tokens.shape)
568
+ return torch.cat(outputs, dim=0)
569
+
570
+ def _get_sglang_w13(self) -> torch.Tensor:
571
+ key = (
572
+ self.experts.w1.data_ptr(),
573
+ self.experts.w3.data_ptr(),
574
+ self.experts.w1.device,
575
+ self.experts.w3.device,
576
+ self.experts.w1.dtype,
577
+ self.experts.w3.dtype,
578
+ )
579
+ if self._sglang_w13_cache is None or self._sglang_w13_cache_key != key:
580
+ self._sglang_w13_cache = torch.cat(
581
+ (self.experts.w1.bfloat16(), self.experts.w3.bfloat16()), dim=1
582
+ ).contiguous()
583
+ self._sglang_w13_cache_key = key
584
+ return self._sglang_w13_cache
585
+
586
+ @staticmethod
587
+ def _quantize_fp8_weight_per_expert(weight: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
588
+ weight_float = weight.float()
589
+ scale = fp8_scale_from_amax(weight_float.abs().amax(dim=(1, 2)))
590
+ quantized = quantize_to_fp8_e4m3fn(weight_float, scale[:, None, None]).contiguous()
591
+ return quantized, scale.contiguous()
592
+
593
+ def _get_sglang_fp8_weights(self) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
594
+ key = (
595
+ self.experts.w1.data_ptr(),
596
+ self.experts.w2.data_ptr(),
597
+ self.experts.w3.data_ptr(),
598
+ self.experts.w1.device,
599
+ self.experts.w2.device,
600
+ self.experts.w3.device,
601
+ self.experts.w1.dtype,
602
+ self.experts.w2.dtype,
603
+ self.experts.w3.dtype,
604
+ )
605
+ if self._sglang_fp8_cache is None or self._sglang_fp8_cache_key != key:
606
+ w13 = torch.cat((self.experts.w1.float(), self.experts.w3.float()), dim=1).contiguous()
607
+ w13_fp8, w13_scale = self._quantize_fp8_weight_per_expert(w13)
608
+ w2_fp8, w2_scale = self._quantize_fp8_weight_per_expert(self.experts.w2)
609
+ self._sglang_fp8_cache = (w13_fp8, w2_fp8, w13_scale, w2_scale)
610
+ self._sglang_fp8_cache_key = key
611
+ return self._sglang_fp8_cache
612
+
613
+ def _run_sglang_triton_experts(
614
+ self,
615
+ tokens: torch.Tensor,
616
+ top_scores: torch.Tensor,
617
+ top_indices: torch.Tensor,
618
+ ) -> torch.Tensor:
619
+ ensure_sglang_moe_ready()
620
+ topk_output = LightSglangStandardTopKOutput(
621
+ top_scores.float(),
622
+ top_indices.to(torch.int32),
623
+ torch.empty(0, device=tokens.device),
624
+ )
625
+ runner_config = LightSglangMoeRunnerConfig(
626
+ num_experts=self.num_experts,
627
+ num_local_experts=self.num_experts,
628
+ activation="silu",
629
+ is_gated=True,
630
+ inplace=False,
631
+ )
632
+ return sglang_fused_experts(
633
+ tokens.contiguous().bfloat16(),
634
+ self._get_sglang_w13(),
635
+ self.experts.w2.bfloat16().contiguous(),
636
+ topk_output,
637
+ runner_config,
638
+ ).type_as(tokens)
639
+
640
+ def _run_sglang_triton_fp8_experts(
641
+ self,
642
+ tokens: torch.Tensor,
643
+ top_scores: torch.Tensor,
644
+ top_indices: torch.Tensor,
645
+ ) -> torch.Tensor:
646
+ ensure_sglang_moe_ready()
647
+ topk_output = LightSglangStandardTopKOutput(
648
+ top_scores.float(),
649
+ top_indices.to(torch.int32),
650
+ torch.empty(0, device=tokens.device),
651
+ )
652
+ runner_config = LightSglangMoeRunnerConfig(
653
+ num_experts=self.num_experts,
654
+ num_local_experts=self.num_experts,
655
+ activation="silu",
656
+ is_gated=True,
657
+ inplace=False,
658
+ )
659
+ w13_fp8, w2_fp8, w13_scale, w2_scale = self._get_sglang_fp8_weights()
660
+ return sglang_fused_experts(
661
+ tokens.contiguous().bfloat16(),
662
+ w13_fp8,
663
+ w2_fp8,
664
+ topk_output,
665
+ runner_config,
666
+ use_fp8_w8a8=True,
667
+ w1_scale=w13_scale,
668
+ w2_scale=w2_scale,
669
+ ).type_as(tokens)
670
+
671
+ def _run_selected_experts(
672
+ self,
673
+ tokens: torch.Tensor,
674
+ top_scores: torch.Tensor,
675
+ top_indices: torch.Tensor,
676
+ ) -> torch.Tensor:
677
+ backend = _moe_expert_backend()
678
+ if backend in {"grouped_mm", "torch_grouped_mm", "default"}:
679
+ (
680
+ permuted_tokens,
681
+ counts,
682
+ sorted_positions,
683
+ sorted_scores,
684
+ num_tokens,
685
+ top_k,
686
+ ) = self._reorder_tokens(tokens, top_scores, top_indices, self.router.num_experts)
687
+ expert_output = self._run_grouped_experts(permuted_tokens, counts)
688
+ return self._restore_tokens(
689
+ expert_output,
690
+ sorted_positions,
691
+ sorted_scores,
692
+ num_tokens,
693
+ top_k,
694
+ )
695
+ if backend in {"sglang_triton", "triton", "sglang"}:
696
+ return self._run_sglang_triton_experts(tokens, top_scores, top_indices)
697
+ if backend in {"sglang_triton_fp8", "triton_fp8", "sglang_fp8"}:
698
+ return self._run_sglang_triton_fp8_experts(tokens, top_scores, top_indices)
699
+ raise ValueError(
700
+ f"Unsupported LINGBOT_MOE_EXPERT_BACKEND={backend!r}; "
701
+ "expected grouped_mm, sglang_triton, or sglang_triton_fp8"
702
+ )
703
+
704
+ @staticmethod
705
+ def _restore_tokens(
706
+ expert_output: torch.Tensor,
707
+ sorted_positions: torch.Tensor,
708
+ sorted_scores: torch.Tensor,
709
+ num_tokens: int,
710
+ top_k: int,
711
+ ) -> torch.Tensor:
712
+ backend = _moe_restore_backend()
713
+ if backend in {"triton", "triton_fused", "fused"}:
714
+ return LingBotVideoSparseMoeBlock._restore_tokens_triton(
715
+ expert_output,
716
+ sorted_positions,
717
+ sorted_scores,
718
+ num_tokens,
719
+ top_k,
720
+ )
721
+ if backend in {"index_add", "index_add_", "scatter_add"}:
722
+ return LingBotVideoSparseMoeBlock._restore_tokens_index_add(
723
+ expert_output,
724
+ sorted_positions,
725
+ sorted_scores,
726
+ num_tokens,
727
+ top_k,
728
+ )
729
+ if backend in {"weighted_scatter", "weighted", "fast_scatter"}:
730
+ return LingBotVideoSparseMoeBlock._restore_tokens_weighted_scatter(
731
+ expert_output,
732
+ sorted_positions,
733
+ sorted_scores,
734
+ num_tokens,
735
+ top_k,
736
+ )
737
+ if backend in {"chunked", "chunked_scatter", "scatter_chunked"}:
738
+ return LingBotVideoSparseMoeBlock._restore_tokens_chunked_scatter(
739
+ expert_output,
740
+ sorted_positions,
741
+ sorted_scores,
742
+ num_tokens,
743
+ top_k,
744
+ )
745
+ if backend not in {"scatter", "default"}:
746
+ raise ValueError(
747
+ f"Unsupported LINGBOT_MOE_RESTORE_BACKEND={backend!r}; "
748
+ "expected scatter, chunked_scatter, weighted_scatter, index_add, or triton"
749
+ )
750
+ dim = expert_output.shape[-1]
751
+ unsorted = torch.zeros(
752
+ (num_tokens * top_k, dim),
753
+ dtype=expert_output.dtype,
754
+ device=expert_output.device,
755
+ )
756
+ unsorted[sorted_positions] = expert_output
757
+ unsorted = unsorted.reshape(num_tokens, top_k, dim)
758
+
759
+ scores_unsorted = torch.zeros(
760
+ num_tokens * top_k,
761
+ dtype=sorted_scores.dtype,
762
+ device=sorted_scores.device,
763
+ )
764
+ scores_unsorted[sorted_positions] = sorted_scores
765
+ scores_unsorted = scores_unsorted.reshape(num_tokens, top_k, 1)
766
+ return (unsorted.float() * scores_unsorted).sum(dim=1).to(expert_output.dtype)
767
+
768
+ @staticmethod
769
+ def _restore_tokens_chunked_scatter(
770
+ expert_output: torch.Tensor,
771
+ sorted_positions: torch.Tensor,
772
+ sorted_scores: torch.Tensor,
773
+ num_tokens: int,
774
+ top_k: int,
775
+ ) -> torch.Tensor:
776
+ dim = expert_output.shape[-1]
777
+ chunk_size = int(os.environ.get("LINGBOT_MOE_RESTORE_CHUNK_SIZE", "128"))
778
+ if chunk_size <= 0:
779
+ raise ValueError("LINGBOT_MOE_RESTORE_CHUNK_SIZE must be positive")
780
+
781
+ scores_unsorted = torch.zeros(
782
+ num_tokens * top_k,
783
+ dtype=sorted_scores.dtype,
784
+ device=sorted_scores.device,
785
+ )
786
+ scores_unsorted[sorted_positions] = sorted_scores
787
+ scores_unsorted = scores_unsorted.reshape(num_tokens, top_k, 1)
788
+ output = expert_output.new_empty((num_tokens, dim))
789
+ for start in range(0, dim, chunk_size):
790
+ end = min(start + chunk_size, dim)
791
+ unsorted = torch.zeros(
792
+ (num_tokens * top_k, end - start),
793
+ dtype=expert_output.dtype,
794
+ device=expert_output.device,
795
+ )
796
+ unsorted[sorted_positions] = expert_output[:, start:end]
797
+ unsorted = unsorted.reshape(num_tokens, top_k, end - start)
798
+ output[:, start:end] = (unsorted.float() * scores_unsorted).sum(dim=1).to(
799
+ expert_output.dtype
800
+ )
801
+ return output
802
+
803
+ @staticmethod
804
+ def _restore_tokens_triton(
805
+ expert_output: torch.Tensor,
806
+ sorted_positions: torch.Tensor,
807
+ sorted_scores: torch.Tensor,
808
+ num_tokens: int,
809
+ top_k: int,
810
+ ) -> torch.Tensor:
811
+ return restore_tokens_triton(
812
+ expert_output,
813
+ sorted_positions,
814
+ sorted_scores,
815
+ num_tokens,
816
+ top_k,
817
+ )
818
+
819
+ @staticmethod
820
+ def _restore_tokens_weighted_scatter(
821
+ expert_output: torch.Tensor,
822
+ sorted_positions: torch.Tensor,
823
+ sorted_scores: torch.Tensor,
824
+ num_tokens: int,
825
+ top_k: int,
826
+ ) -> torch.Tensor:
827
+ weighted = (expert_output * sorted_scores[:, None].to(expert_output.dtype)).to(expert_output.dtype)
828
+ unsorted = torch.zeros(
829
+ (num_tokens * top_k, expert_output.shape[-1]),
830
+ dtype=expert_output.dtype,
831
+ device=expert_output.device,
832
+ )
833
+ unsorted[sorted_positions] = weighted
834
+ return unsorted.reshape(num_tokens, top_k, expert_output.shape[-1]).sum(dim=1)
835
+
836
+ @staticmethod
837
+ def _restore_tokens_index_add(
838
+ expert_output: torch.Tensor,
839
+ sorted_positions: torch.Tensor,
840
+ sorted_scores: torch.Tensor,
841
+ num_tokens: int,
842
+ top_k: int,
843
+ ) -> torch.Tensor:
844
+ token_indices = torch.div(sorted_positions, top_k, rounding_mode="floor")
845
+ weighted = expert_output.float() * sorted_scores[:, None].float()
846
+ out = torch.zeros(
847
+ (num_tokens, expert_output.shape[-1]),
848
+ dtype=torch.float32,
849
+ device=expert_output.device,
850
+ )
851
+ out.index_add_(0, token_indices, weighted)
852
+ return out.to(expert_output.dtype)
853
+
854
+ def forward(self, hidden_states: torch.Tensor, padding_mask: Optional[torch.Tensor] = None):
855
+ # hidden_states: (B, S, H); padding_mask: (B*S,) with 1=valid (only needed when B>1)
856
+ B = hidden_states.shape[0]
857
+ tokens = hidden_states.view(-1, self.hidden_size)
858
+ top_indices, top_scores, logits, scores, scores_for_choice = self.router(tokens)
859
+ del logits, scores, scores_for_choice
860
+ if padding_mask is not None:
861
+ pm = padding_mask.unsqueeze(-1).to(top_scores.dtype)
862
+ top_scores = top_scores * pm
863
+ top_scores = top_scores / (top_scores.sum(dim=-1, keepdim=True) + 1e-9)
864
+ top_scores = top_scores * self.router.route_scale
865
+
866
+ out = self._run_selected_experts(tokens, top_scores, top_indices)
867
+
868
+ out = out.view(B, -1, self.hidden_size)
869
+ if self.shared_experts is not None:
870
+ shared_output = self.shared_experts(hidden_states)
871
+ out = out + shared_output
872
+ return out
873
+
874
+
875
+ class LingBotVideoBlock(nn.Module):
876
+ def __init__(
877
+ self,
878
+ hidden_size,
879
+ num_attention_heads,
880
+ intermediate_size,
881
+ norm_eps,
882
+ qkv_bias,
883
+ out_bias,
884
+ num_experts,
885
+ num_experts_per_tok,
886
+ moe_intermediate_size,
887
+ decoder_sparse_step,
888
+ mlp_only_layers,
889
+ n_shared_experts,
890
+ score_func,
891
+ norm_topk_prob,
892
+ n_group,
893
+ topk_group,
894
+ routed_scaling_factor,
895
+ layer_idx: int,
896
+ ):
897
+ super().__init__()
898
+ self.layer_idx = layer_idx
899
+ h = hidden_size
900
+ self.scale_shift_table = nn.Parameter(torch.zeros(1, 6 * h))
901
+ self.norm1 = LingBotVideoRMSNorm(h, norm_eps)
902
+ self.attn = LingBotVideoAttention(
903
+ h, num_attention_heads, norm_eps, qkv_bias, out_bias
904
+ )
905
+ self.norm_post_attn = LingBotVideoRMSNorm(h, norm_eps)
906
+ self.norm2 = LingBotVideoRMSNorm(h, norm_eps)
907
+ # Sparsity decision matches MoEBlock: mlp_only_layers + decoder_sparse_step + num_experts
908
+ if layer_idx not in mlp_only_layers and (
909
+ num_experts > 0 and (layer_idx + 1) % decoder_sparse_step == 0
910
+ ):
911
+ self.ffn = LingBotVideoSparseMoeBlock(
912
+ h, intermediate_size, num_experts, num_experts_per_tok,
913
+ moe_intermediate_size, score_func, norm_topk_prob,
914
+ n_group, topk_group, routed_scaling_factor,
915
+ n_shared_experts,
916
+ )
917
+ else:
918
+ self.ffn = LingBotVideoMLP(h, intermediate_size)
919
+ self.norm_post_ffn = LingBotVideoRMSNorm(h, norm_eps)
920
+
921
+ def forward(
922
+ self,
923
+ x,
924
+ temb6,
925
+ rotary_emb,
926
+ attention_mask=None,
927
+ moe_padding_mask=None,
928
+ packed_indices: Optional[dict[str, torch.Tensor]] = None,
929
+ parallel_config=None,
930
+ ):
931
+ expected_tokens = x.shape[0] * x.shape[1]
932
+ if temb6.ndim != 2 or temb6.shape[0] != expected_tokens:
933
+ raise ValueError(
934
+ "LingBotVideoBlock expects token-level temb6 with shape "
935
+ f"(B*S, 6D); got {tuple(temb6.shape)} for hidden states {tuple(x.shape)}."
936
+ )
937
+ # AdaLN mod: dense and MoE both keep scale_shift_table fp32 (master
938
+ # moe/models.py:80 dropped the accidental `.to(dtype=c.dtype)` cast).
939
+ mod = temb6.view(x.shape[0], x.shape[1], -1) + self.scale_shift_table.unsqueeze(0)
940
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = mod.chunk(6, dim=-1)
941
+ gate_msa, gate_mlp = gate_msa.tanh(), gate_mlp.tanh()
942
+ scale_msa, scale_mlp = 1.0 + scale_msa, 1.0 + scale_mlp
943
+
944
+ # AdaLN modulation / norms run in fp32 (sensitive path); cast to the bulk
945
+ # compute dtype only at the bf16 Linear boundary. This replaces the old
946
+ # ambient autocast, which rounded Linear inputs to bf16 at the same point.
947
+ bulk_dtype = self.attn.to_q.weight.dtype
948
+ attn_in = (self.norm1(x) * scale_msa + shift_msa).to(bulk_dtype)
949
+ attn_out = self.attn(
950
+ attn_in,
951
+ rotary_emb,
952
+ attention_mask,
953
+ packed_indices=packed_indices,
954
+ parallel_config=parallel_config,
955
+ )
956
+ x = x + (gate_msa * self.norm_post_attn(attn_out)).to(x.dtype)
957
+
958
+ ffn_in = (self.norm2(x) * scale_mlp + shift_mlp).to(bulk_dtype)
959
+ if isinstance(self.ffn, LingBotVideoSparseMoeBlock):
960
+ ffn_out = self.ffn(ffn_in, padding_mask=moe_padding_mask)
961
+ else:
962
+ ffn_out = self.ffn(ffn_in)
963
+ ffn_normed = self.norm_post_ffn(ffn_out)
964
+ x = x + (gate_mlp * ffn_normed).to(x.dtype)
965
+ return x
966
+
967
+
968
+ class LingBotVideoTransformer3DModel(ModelMixin, ConfigMixin):
969
+ _supports_gradient_checkpointing = False
970
+ _no_split_modules = ["LingBotVideoBlock"]
971
+ _keep_in_fp32_modules = list(LINGBOT_VIDEO_FP32_MODULES)
972
+
973
+ def to(self, *args, **kwargs):
974
+ device, dtype, non_blocking, _ = torch._C._nn._parse_to(*args, **kwargs)
975
+ if dtype is None or dtype == torch.float32:
976
+ return super().to(*args, **kwargs)
977
+
978
+ dtype_is_floating = torch.is_floating_point(torch.empty((), dtype=dtype))
979
+ if not dtype_is_floating:
980
+ return super().to(*args, **kwargs)
981
+
982
+ if device is not None:
983
+ super().to(device=device, non_blocking=non_blocking)
984
+
985
+ for name, param in self.named_parameters():
986
+ if not torch.is_floating_point(param):
987
+ continue
988
+ target_dtype = torch.float32 if should_keep_in_fp32(name) else dtype
989
+ param.data = param.data.to(dtype=target_dtype, non_blocking=non_blocking)
990
+ if param.grad is not None:
991
+ param.grad.data = param.grad.data.to(dtype=target_dtype, non_blocking=non_blocking)
992
+
993
+ for name, buffer in self.named_buffers():
994
+ if not torch.is_floating_point(buffer):
995
+ continue
996
+ target_dtype = torch.float32 if should_keep_in_fp32(name) else dtype
997
+ buffer.data = buffer.data.to(dtype=target_dtype, non_blocking=non_blocking)
998
+
999
+ return self
1000
+
1001
+ @register_to_config
1002
+ def __init__(
1003
+ self,
1004
+ patch_size: Tuple[int, int, int] = (1, 2, 2),
1005
+ in_channels: int = 16,
1006
+ out_channels: int = 16,
1007
+ hidden_size: int = 2048,
1008
+ num_attention_heads: int = 16,
1009
+ depth: int = 24,
1010
+ intermediate_size: int = 6144,
1011
+ text_dim: int = 2560,
1012
+ freq_dim: int = 256,
1013
+ norm_eps: float = 1e-6,
1014
+ rope_theta: float = 256.0,
1015
+ axes_dims: Tuple[int, int, int] = (32, 48, 48),
1016
+ axes_lens: Tuple[int, int, int] = (8192, 1024, 1024),
1017
+ qkv_bias: bool = False,
1018
+ out_bias: bool = True,
1019
+ patch_embed_bias: bool = True,
1020
+ timestep_mlp_bias: bool = True,
1021
+ num_experts: int = 0,
1022
+ num_experts_per_tok: int = 8,
1023
+ moe_intermediate_size: int = 512,
1024
+ decoder_sparse_step: int = 1,
1025
+ mlp_only_layers: Tuple[int, ...] = (),
1026
+ n_shared_experts: Optional[int] = None,
1027
+ score_func: str = "sigmoid",
1028
+ norm_topk_prob: bool = True,
1029
+ n_group: Optional[int] = None,
1030
+ topk_group: Optional[int] = None,
1031
+ routed_scaling_factor: float = 1.0,
1032
+ ):
1033
+ super().__init__()
1034
+ head_dim = hidden_size // num_attention_heads
1035
+ assert head_dim == sum(axes_dims), f"head_dim {head_dim} != sum(axes_dims) {sum(axes_dims)}"
1036
+ mlp_only_layers = tuple(mlp_only_layers)
1037
+
1038
+ self.patch_embedder = nn.Linear(
1039
+ in_channels * math.prod(patch_size), hidden_size, bias=patch_embed_bias
1040
+ )
1041
+ self.time_proj = Timesteps(freq_dim, flip_sin_to_cos=True, downscale_freq_shift=0)
1042
+ self.time_embedder = TimestepEmbedding(
1043
+ freq_dim, hidden_size, act_fn="silu", sample_proj_bias=timestep_mlp_bias
1044
+ )
1045
+ self.time_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 6 * hidden_size))
1046
+ self.text_embedder = LingBotVideoTextEmbedder(text_dim, hidden_size)
1047
+ self.rope = LingBotVideoRotaryEmbedding(axes_dims, axes_lens, rope_theta)
1048
+ self.blocks = nn.ModuleList(
1049
+ [
1050
+ LingBotVideoBlock(
1051
+ hidden_size=hidden_size,
1052
+ num_attention_heads=num_attention_heads,
1053
+ intermediate_size=intermediate_size,
1054
+ norm_eps=norm_eps,
1055
+ qkv_bias=qkv_bias,
1056
+ out_bias=out_bias,
1057
+ num_experts=num_experts,
1058
+ num_experts_per_tok=num_experts_per_tok,
1059
+ moe_intermediate_size=moe_intermediate_size,
1060
+ decoder_sparse_step=decoder_sparse_step,
1061
+ mlp_only_layers=mlp_only_layers,
1062
+ n_shared_experts=n_shared_experts,
1063
+ score_func=score_func,
1064
+ norm_topk_prob=norm_topk_prob,
1065
+ n_group=n_group,
1066
+ topk_group=topk_group,
1067
+ routed_scaling_factor=routed_scaling_factor,
1068
+ layer_idx=i,
1069
+ )
1070
+ for i in range(depth)
1071
+ ]
1072
+ )
1073
+ self.norm_out = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=norm_eps)
1074
+ self.norm_out_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size))
1075
+ self.proj_out = nn.Linear(hidden_size, math.prod(patch_size) * out_channels)
1076
+ self.cp_joint = nn.Identity()
1077
+ self.cp_rotary = nn.Identity()
1078
+ self.cp_temb_input = nn.Identity()
1079
+ self.cp_temb6 = nn.Identity()
1080
+ self.cp_out = nn.Identity()
1081
+ self._cp_plan = {
1082
+ "cp_joint": {
1083
+ "input": ContextParallelInput(split_dim=1, expected_dims=3),
1084
+ },
1085
+ "cp_rotary": {
1086
+ "input": ContextParallelInput(split_dim=1, expected_dims=3),
1087
+ },
1088
+ "cp_temb_input": {
1089
+ "input": ContextParallelInput(split_dim=1, expected_dims=3),
1090
+ },
1091
+ "cp_temb6": {
1092
+ "input": ContextParallelInput(split_dim=1, expected_dims=3),
1093
+ },
1094
+ "cp_out": ContextParallelOutput(gather_dim=1, expected_dims=3),
1095
+ }
1096
+
1097
+ def forward(
1098
+ self,
1099
+ hidden_states: torch.Tensor, # (B, C, T, H, W)
1100
+ timestep: torch.Tensor, # (B,) ∈ [0, 1000](= sigma*1000)
1101
+ encoder_hidden_states: torch.Tensor, # (B, L, text_dim)
1102
+ encoder_attention_mask: Optional[torch.Tensor] = None, # (B, L) 1=valid
1103
+ return_dict: bool = True,
1104
+ ):
1105
+ B, C, T, H, W = hidden_states.shape
1106
+ pF, pH, pW = self.config.patch_size
1107
+ gt, gh, gw = T // pF, H // pH, W // pW
1108
+ n_video = gt * gh * gw
1109
+ L = encoder_hidden_states.shape[1]
1110
+ device = hidden_states.device
1111
+ if encoder_attention_mask is not None:
1112
+ text_lens = encoder_attention_mask.sum(dim=-1).long()
1113
+ else:
1114
+ text_lens = torch.full((B,), L, dtype=torch.long, device=device)
1115
+ text_lens_list = [int(v) for v in text_lens.detach().cpu().tolist()]
1116
+ packed_batch = B > 1
1117
+
1118
+ # patchify: token order (f h w), feature order (pf ph pw c) -- matches patchify_and_embed
1119
+ patch_tokens = hidden_states.reshape(B, C, gt, pF, gh, pH, gw, pW)
1120
+ patch_tokens = patch_tokens.permute(0, 2, 4, 6, 3, 5, 7, 1).reshape(
1121
+ B,
1122
+ n_video,
1123
+ pF * pH * pW * C,
1124
+ )
1125
+ if packed_batch:
1126
+ packed_patch_tokens = patch_tokens.reshape(1, B * n_video, -1)
1127
+ x = torch.cat(
1128
+ [self.patch_embedder(patch_tokens[i : i + 1]) for i in range(B)],
1129
+ dim=1,
1130
+ )
1131
+ else:
1132
+ x = self.patch_embedder(patch_tokens)
1133
+
1134
+ if packed_batch:
1135
+ text_parts = [
1136
+ self.text_embedder(encoder_hidden_states[i : i + 1, : text_lens_list[i], :])
1137
+ for i in range(B)
1138
+ ]
1139
+ text = torch.cat(text_parts, dim=1)
1140
+ joint = _cat_interleave(
1141
+ x,
1142
+ [n_video] * B,
1143
+ text,
1144
+ text_lens_list,
1145
+ )
1146
+ else:
1147
+ text = self.text_embedder(encoder_hidden_states)
1148
+ joint = torch.cat([x, text], dim=1) # [video; text]
1149
+ joint_seq_len = joint.shape[1]
1150
+
1151
+ # Per-sample RoPE: video t-axis start = real text length of this sample + 1
1152
+ rotary_parts = [
1153
+ self.rope(make_joint_position_ids(text_lens_list[i], gt, gh, gw, device))
1154
+ for i in range(B)
1155
+ ]
1156
+ if packed_batch:
1157
+ rotary = torch.cat(rotary_parts, dim=0).unsqueeze(0)
1158
+ else:
1159
+ rotary = torch.stack(rotary_parts, dim=0) # (B, S, head_dim/2) complex64
1160
+
1161
+ parallel_config = getattr(self, "_parallel_config", None)
1162
+ use_packed_attention = parallel_config is not None
1163
+
1164
+ attention_mask = None
1165
+ moe_padding_mask = None
1166
+ packed_indices = None
1167
+ has_padding = encoder_attention_mask is not None and bool((text_lens < L).any())
1168
+ if packed_batch or use_packed_attention:
1169
+ sample_seq_lens = [n_video + text_len for text_len in text_lens_list]
1170
+ cu_seqlens = torch.zeros(B + 1, device=device, dtype=torch.int32)
1171
+ cu_seqlens[1:] = torch.cumsum(
1172
+ torch.tensor(sample_seq_lens, device=device, dtype=torch.int32),
1173
+ dim=0,
1174
+ )
1175
+ packed_indices = {
1176
+ "cu_seqlens_kv": cu_seqlens,
1177
+ "max_seqlen_in_batch_kv": max(sample_seq_lens),
1178
+ }
1179
+ has_padding = False
1180
+ if has_padding:
1181
+ key_mask = torch.cat(
1182
+ [torch.ones(B, n_video, dtype=torch.bool, device=device),
1183
+ encoder_attention_mask.bool()],
1184
+ dim=1,
1185
+ )
1186
+ attention_mask = key_mask[:, None, None, :] # (B,1,1,S) → SDPA broadcast
1187
+ moe_padding_mask = key_mask.reshape(-1).float() # (B*S,)
1188
+ packed_cp = packed_indices is not None and parallel_config is not None
1189
+ padding_size = 0
1190
+ if packed_cp:
1191
+ cp_config = parallel_config.context_parallel_config
1192
+ cp_world_size = int(getattr(cp_config, "ulysses_degree", getattr(cp_config, "_world_size", 1)))
1193
+ padding_size = (cp_world_size - (joint_seq_len % cp_world_size)) % cp_world_size
1194
+ if padding_size:
1195
+ joint = torch.cat(
1196
+ [
1197
+ joint,
1198
+ torch.zeros(
1199
+ joint.shape[0],
1200
+ padding_size,
1201
+ joint.shape[2],
1202
+ device=joint.device,
1203
+ dtype=joint.dtype,
1204
+ ),
1205
+ ],
1206
+ dim=1,
1207
+ )
1208
+ rotary = torch.cat(
1209
+ [
1210
+ rotary,
1211
+ torch.zeros(
1212
+ rotary.shape[0],
1213
+ padding_size,
1214
+ rotary.shape[2],
1215
+ device=rotary.device,
1216
+ dtype=rotary.dtype,
1217
+ ),
1218
+ ],
1219
+ dim=1,
1220
+ )
1221
+ if packed_indices is None:
1222
+ raise RuntimeError("packed_indices must be initialized for packed context parallel.")
1223
+ packed_indices["cu_seqlens_kv"] = torch.cat(
1224
+ [
1225
+ packed_indices["cu_seqlens_kv"],
1226
+ packed_indices["cu_seqlens_kv"][-1:] + padding_size,
1227
+ ],
1228
+ dim=0,
1229
+ )
1230
+ packed_indices["max_seqlen_in_batch_kv"] = max(
1231
+ int(packed_indices["max_seqlen_in_batch_kv"]),
1232
+ int(padding_size),
1233
+ )
1234
+ joint_seq_len = joint.shape[1]
1235
+
1236
+ timestep_for_embed = timestep.float()
1237
+ timestep_proj = self.time_proj(timestep_for_embed)
1238
+ t_emb = self.time_embedder(timestep_proj) # (B, D)
1239
+ if packed_batch:
1240
+ temb_input = torch.cat(
1241
+ [
1242
+ t_emb[i : i + 1].unsqueeze(1).expand(1, n_video + text_lens_list[i], -1)
1243
+ for i in range(B)
1244
+ ],
1245
+ dim=1,
1246
+ )
1247
+ if padding_size:
1248
+ temb_input = torch.cat(
1249
+ [
1250
+ temb_input,
1251
+ torch.zeros(
1252
+ temb_input.shape[0],
1253
+ padding_size,
1254
+ temb_input.shape[2],
1255
+ device=temb_input.device,
1256
+ dtype=temb_input.dtype,
1257
+ ),
1258
+ ],
1259
+ dim=1,
1260
+ )
1261
+ temb6 = self.time_modulation(temb_input.reshape(joint_seq_len, -1))
1262
+ temb6 = temb6.reshape(1, joint_seq_len, -1)
1263
+ else:
1264
+ temb_input = t_emb.unsqueeze(1).expand(B, joint_seq_len, -1) # (B, S, D)
1265
+ temb6 = self.time_modulation(temb_input.reshape(B * joint_seq_len, -1))
1266
+ temb6 = temb6.reshape(B, joint_seq_len, -1) # (B, S, 6D)
1267
+
1268
+ joint = self.cp_joint(joint)
1269
+ rotary = self.cp_rotary(rotary)
1270
+ if packed_cp:
1271
+ temb_input = self.cp_temb_input(temb_input)
1272
+ temb6 = self.cp_temb6(temb6)
1273
+ temb6 = temb6.reshape(temb6.shape[0] * temb6.shape[1], -1)
1274
+
1275
+ for block in self.blocks:
1276
+ joint = block(
1277
+ joint,
1278
+ temb6,
1279
+ rotary,
1280
+ attention_mask,
1281
+ moe_padding_mask,
1282
+ packed_indices=packed_indices,
1283
+ parallel_config=parallel_config,
1284
+ )
1285
+ if not packed_cp:
1286
+ joint = self.cp_out(joint)
1287
+
1288
+ final_mod = self.norm_out_modulation(temb_input.reshape(joint.shape[0] * joint.shape[1], -1))
1289
+ shift, scale = final_mod.reshape(joint.shape[0], joint.shape[1], -1).chunk(2, dim=-1)
1290
+ final_hidden = self.norm_out(joint) * (1.0 + scale) + shift
1291
+ projected = self.proj_out(final_hidden.to(self.proj_out.weight.dtype))
1292
+ if packed_cp:
1293
+ projected = self.cp_out(projected)
1294
+ if padding_size:
1295
+ projected = projected[:, :-padding_size, :]
1296
+ if packed_batch:
1297
+ split_lengths: list[int] = []
1298
+ for text_len in text_lens_list:
1299
+ split_lengths.extend([n_video, text_len])
1300
+ parts = torch.split(projected, split_lengths, dim=1)
1301
+ x = torch.cat(parts[::2], dim=1).reshape(B, n_video, -1)
1302
+ else:
1303
+ x = projected[:, :n_video]
1304
+
1305
+ # unpatchify (matches the rearrange in postprocess)
1306
+ Cout = self.config.out_channels
1307
+ x = x.reshape(B, gt, gh, gw, pF, pH, pW, Cout)
1308
+ x = x.permute(0, 7, 1, 4, 2, 5, 3, 6).reshape(B, Cout, T, H, W)
1309
+
1310
+ if not return_dict:
1311
+ return (x,)
1312
+ return Transformer2DModelOutput(sample=x)
lingbot_video/utils.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Sequence
6
+
7
+ import numpy as np
8
+ import torch
9
+ import torch.nn.functional as F
10
+ from PIL import Image
11
+
12
+ LOW_NOISE_TAIL_V1_NAME = "low_noise_tail_v1"
13
+ LOW_NOISE_TAIL_V1_DEFAULT_STEPS = 2
14
+
15
+
16
+ def num_frames_from_duration(duration: float, fps: int) -> int:
17
+ frame_count = int(float(duration) * int(fps))
18
+ return ((frame_count - 1) // 4 + 1) * 4 + 1
19
+
20
+
21
+ def validate_refiner_sigmas(
22
+ sigmas: Sequence[float] | np.ndarray,
23
+ t_thresh: float | None = None,
24
+ ) -> np.ndarray:
25
+ arr = np.asarray(list(sigmas), dtype=np.float64)
26
+ if arr.ndim != 1 or arr.size == 0:
27
+ raise ValueError("refiner sigma schedule must be a non-empty 1D list")
28
+ if not np.all(np.isfinite(arr)):
29
+ raise ValueError("refiner sigma schedule contains non-finite values")
30
+ if np.any(arr < 0.0) or np.any(arr > 1.0):
31
+ raise ValueError(f"refiner sigma schedule values must be in [0, 1], got {arr.tolist()}")
32
+ if arr.size > 1 and not np.all(np.diff(arr) < 0.0):
33
+ raise ValueError(f"refiner sigma schedule must be strictly descending, got {arr.tolist()}")
34
+ if t_thresh is not None and abs(float(arr[0]) - float(t_thresh)) > 1e-6:
35
+ raise ValueError(f"refiner sigma schedule must start at t_thresh={float(t_thresh)}, got {float(arr[0])}")
36
+ return arr
37
+
38
+
39
+ def compute_refiner_sigmas(
40
+ *,
41
+ sigma_max: float,
42
+ sigma_min: float,
43
+ num_inference_steps: int,
44
+ shift: float,
45
+ t_thresh: float | None,
46
+ tail_steps: int = 0,
47
+ ) -> np.ndarray | None:
48
+ if t_thresh is None:
49
+ return None
50
+ t_value = float(t_thresh)
51
+ if not (0.0 < t_value <= 1.0):
52
+ raise ValueError(f"refiner t_thresh must lie in (0, 1], got {t_value}")
53
+ steps = int(num_inference_steps)
54
+ if steps < 1:
55
+ raise ValueError(f"num_inference_steps must be >= 1, got {steps}")
56
+ tail = int(tail_steps or 0)
57
+ if tail < 0:
58
+ raise ValueError(f"refiner_sigma_tail_steps must be >= 0, got {tail}")
59
+
60
+ base = np.linspace(float(sigma_max), float(sigma_min), steps + 1).copy()[:-1]
61
+ shift_value = float(shift)
62
+ shifted = shift_value * base / (1.0 + (shift_value - 1.0) * base)
63
+ eps = 1e-6
64
+ sigmas = shifted[shifted <= t_value + eps]
65
+ if sigmas.size == 0 or abs(float(sigmas[0]) - t_value) > eps:
66
+ sigmas = np.concatenate([[t_value], sigmas])
67
+ if tail > 0:
68
+ start = float(sigmas[-1])
69
+ stop = min(float(sigma_min), start)
70
+ extra = np.linspace(start, stop, tail + 2, dtype=np.float64)[1:-1]
71
+ sigmas = np.concatenate([sigmas, extra])
72
+ return validate_refiner_sigmas(sigmas, t_value).astype(np.float32)
73
+
74
+
75
+ def prepare_refiner_latent(
76
+ x_up: torch.Tensor,
77
+ noise: torch.Tensor,
78
+ t_thresh: float | torch.Tensor,
79
+ ) -> torch.Tensor:
80
+ if not torch.is_tensor(t_thresh):
81
+ t_thresh = torch.tensor(float(t_thresh), device=x_up.device, dtype=x_up.dtype)
82
+ while t_thresh.ndim < x_up.ndim:
83
+ t_thresh = t_thresh.view(*t_thresh.shape, *([1] * (x_up.ndim - t_thresh.ndim)))
84
+ return (1.0 - t_thresh) * x_up + t_thresh * noise
85
+
86
+
87
+ def _pad_prompt_embeds(
88
+ embeds: torch.Tensor,
89
+ mask: torch.Tensor,
90
+ target_length: int,
91
+ ) -> tuple[torch.Tensor, torch.Tensor]:
92
+ if embeds.ndim != 3:
93
+ raise ValueError(f"prompt embeds must be rank-3, got {tuple(embeds.shape)}")
94
+ if mask.ndim != 2:
95
+ raise ValueError(f"prompt mask must be rank-2, got {tuple(mask.shape)}")
96
+ if embeds.shape[:2] != mask.shape:
97
+ raise ValueError(
98
+ f"prompt embeds/mask shape mismatch: {tuple(embeds.shape)} vs {tuple(mask.shape)}"
99
+ )
100
+ if embeds.shape[0] != 1:
101
+ raise ValueError(f"batched CFG helper expects batch=1 inputs, got {embeds.shape[0]}")
102
+ if embeds.shape[1] > target_length:
103
+ raise ValueError(f"cannot pad length {embeds.shape[1]} down to {target_length}")
104
+ pad_len = target_length - embeds.shape[1]
105
+ if pad_len == 0:
106
+ return embeds, mask
107
+ embed_pad = torch.zeros(
108
+ embeds.shape[0],
109
+ pad_len,
110
+ embeds.shape[2],
111
+ dtype=embeds.dtype,
112
+ device=embeds.device,
113
+ )
114
+ mask_pad = torch.zeros(mask.shape[0], pad_len, dtype=mask.dtype, device=mask.device)
115
+ return torch.cat([embeds, embed_pad], dim=1), torch.cat([mask, mask_pad], dim=1)
116
+
117
+
118
+ def batch_cfg_prompt_inputs(
119
+ prompt_embeds: torch.Tensor,
120
+ prompt_mask: torch.Tensor,
121
+ negative_embeds: torch.Tensor,
122
+ negative_mask: torch.Tensor,
123
+ *,
124
+ null_cond_clone_zero: bool = False,
125
+ ) -> tuple[torch.Tensor, torch.Tensor]:
126
+ if null_cond_clone_zero:
127
+ zero_negative = torch.zeros_like(prompt_embeds)
128
+ return (
129
+ torch.cat([prompt_embeds, zero_negative], dim=0),
130
+ torch.cat([prompt_mask, prompt_mask.clone()], dim=0),
131
+ )
132
+
133
+ target_length = max(int(prompt_embeds.shape[1]), int(negative_embeds.shape[1]))
134
+ prompt_padded, prompt_mask_padded = _pad_prompt_embeds(
135
+ prompt_embeds, prompt_mask, target_length
136
+ )
137
+ negative_padded, negative_mask_padded = _pad_prompt_embeds(
138
+ negative_embeds, negative_mask, target_length
139
+ )
140
+ return (
141
+ torch.cat([prompt_padded, negative_padded], dim=0),
142
+ torch.cat([prompt_mask_padded, negative_mask_padded], dim=0),
143
+ )
144
+
145
+
146
+ def caption_from_sample(sample: dict[str, Any]) -> str:
147
+ if "caption" in sample:
148
+ caption = sample["caption"]
149
+ else:
150
+ runtime_keys = {
151
+ "duration",
152
+ "fps",
153
+ "height",
154
+ "width",
155
+ "num_frames",
156
+ "resolution",
157
+ "ratio",
158
+ }
159
+ caption = {key: value for key, value in sample.items() if key not in runtime_keys}
160
+ if isinstance(caption, (dict, list)):
161
+ return json.dumps(caption, ensure_ascii=False, separators=(",", ":"))
162
+ return str(caption)
163
+
164
+
165
+ def compute_training_frame_budget(
166
+ num_source_frames: int,
167
+ source_fps: float,
168
+ sample_fps: int = 24,
169
+ vae_tc: int = 4,
170
+ ) -> tuple[int, float, int]:
171
+ if num_source_frames <= 0:
172
+ return 1, 0.0, 1
173
+ if source_fps > sample_fps:
174
+ raw_val = int(num_source_frames / source_fps * sample_fps)
175
+ else:
176
+ raw_val = int(num_source_frames)
177
+ sample_frame = ((raw_val - 1) // vae_tc) * vae_tc + 1
178
+ sample_frame = max(sample_frame, 1)
179
+ vae_fps = sample_frame / num_source_frames * float(source_fps)
180
+ t_vae = (sample_frame - 1) // vae_tc + 1
181
+ return int(sample_frame), float(vae_fps), int(t_vae)
182
+
183
+
184
+ def compute_training_aligned_indices(
185
+ num_source_frames: int,
186
+ sample_frame: int,
187
+ ) -> np.ndarray:
188
+ if sample_frame <= 0:
189
+ return np.zeros(0, dtype=int)
190
+ if num_source_frames <= 0:
191
+ return np.zeros(sample_frame, dtype=int)
192
+ if num_source_frames >= sample_frame:
193
+ return np.linspace(0, num_source_frames - 1, sample_frame, dtype=int)
194
+ head = np.arange(num_source_frames, dtype=int)
195
+ pad = np.full(sample_frame - num_source_frames, num_source_frames - 1, dtype=int)
196
+ return np.concatenate([head, pad])
197
+
198
+
199
+ def resize_video_tensor(video: torch.Tensor, height: int, width: int) -> torch.Tensor:
200
+ if video.ndim != 5:
201
+ raise ValueError(f"video tensor must have shape [B,C,T,H,W], got {tuple(video.shape)}")
202
+ bsz, channels, frames, _height, _width = video.shape
203
+ flat = video.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channels, _height, _width)
204
+ resized = F.interpolate(flat, size=(height, width), mode="bicubic", align_corners=False)
205
+ resized = resized.clamp(0.0, 1.0)
206
+ return resized.reshape(bsz, frames, channels, height, width).permute(0, 2, 1, 3, 4).contiguous()
207
+
208
+
209
+ def load_refiner_video_tensor(
210
+ path: str | Path,
211
+ height: int,
212
+ width: int,
213
+ *,
214
+ sample_fps: int = 24,
215
+ vae_tc: int = 4,
216
+ max_frames: int | None = None,
217
+ ) -> tuple[torch.Tensor, dict[str, Any]]:
218
+ from decord import VideoReader, cpu
219
+
220
+ vr = VideoReader(str(path), ctx=cpu(0))
221
+ total = len(vr)
222
+ if total <= 0:
223
+ raise ValueError(f"Video has no frames: {path}")
224
+ src_fps = float(vr.get_avg_fps())
225
+ sample_frame, vae_fps, t_vae = compute_training_frame_budget(
226
+ total,
227
+ src_fps,
228
+ sample_fps=sample_fps,
229
+ vae_tc=vae_tc,
230
+ )
231
+ sample_frame_uncapped = int(sample_frame)
232
+ truncated = False
233
+ if max_frames is not None and sample_frame > int(max_frames):
234
+ sample_frame = int(max_frames)
235
+ truncated = True
236
+ vae_fps = float(sample_frame) / max(total, 1) * src_fps
237
+ t_vae = (sample_frame - 1) // vae_tc + 1
238
+ indices = compute_training_aligned_indices(total, sample_frame)
239
+ frames = torch.from_numpy(vr.get_batch(indices).asnumpy()).permute(0, 3, 1, 2).float()
240
+ frames = frames / 255.0
241
+ video = frames.permute(1, 0, 2, 3).unsqueeze(0).contiguous()
242
+ video = resize_video_tensor(video, height=height, width=width)
243
+ meta = {
244
+ "src_fps": float(src_fps),
245
+ "sample_frame": int(sample_frame),
246
+ "sample_frame_uncapped": int(sample_frame_uncapped),
247
+ "max_frames": None if max_frames is None else int(max_frames),
248
+ "truncated_by_max_frames": bool(truncated),
249
+ "vae_fps": float(vae_fps),
250
+ "t_vae": int(t_vae),
251
+ "num_source_frames": int(total),
252
+ "align_to_training": True,
253
+ }
254
+ return video, meta
255
+
256
+
257
+ def load_first_frame_condition_tensor(
258
+ path: str | Path,
259
+ target_height: int,
260
+ target_width: int,
261
+ geometry_height: int,
262
+ geometry_width: int,
263
+ ) -> torch.Tensor:
264
+ """Load the clean first frame center-cropped to the lowres video's aspect.
265
+
266
+ The refiner latent grid is encoded from the lowres video, so the injected
267
+ frame-0 condition must share that video's geometry, not the raw image's.
268
+ """
269
+ image = Image.open(path).convert("RGB")
270
+ image_width, image_height = image.size
271
+ geometry_aspect = float(geometry_width) / float(geometry_height)
272
+ image_aspect = float(image_width) / float(image_height)
273
+ if image_aspect > geometry_aspect:
274
+ crop_height = image_height
275
+ crop_width = max(1, int(round(crop_height * geometry_aspect)))
276
+ left = int(round((image_width - crop_width) / 2.0))
277
+ top = 0
278
+ else:
279
+ crop_width = image_width
280
+ crop_height = max(1, int(round(crop_width / geometry_aspect)))
281
+ left = 0
282
+ top = int(round((image_height - crop_height) / 2.0))
283
+ crop = image.crop((left, top, left + crop_width, top + crop_height))
284
+ crop = crop.resize((target_width, target_height), resample=Image.BICUBIC)
285
+ arr = np.asarray(crop, dtype=np.float32) / 255.0
286
+ frame = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0).contiguous()
287
+ return frame.permute(1, 0, 2, 3).unsqueeze(0)
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diffusers==0.39.0
2
+ transformers==5.8.1
3
+ accelerate>=1.10
4
+ safetensors>=0.4.5
5
+ torchvision
6
+ imageio>=2.35.0
7
+ imageio-ffmpeg>=0.5.1
8
+ numpy>=1.26
9
+ pillow>=10.4.0
10
+ scipy>=1.11
11
+ json_repair>=0.30
rewriter_prompts.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ VIDEO_STEP1_EXPAND = (
2
+ 'You write ONE short, natural, standalone video caption in English from a brief user video\n'
3
+ 'prompt — as if briefly recounting what happens in a real clip of that idea.\n'
4
+ '\n'
5
+ "INPUT: a short user prompt (and, when provided, the video's first frame as a visual anchor).\n"
6
+ '\n'
7
+ 'HARD LENGTH LIMIT: the output MUST be UNDER 1000 characters. Aim for roughly 400-800\n'
8
+ 'characters. Concise is a top priority.\n'
9
+ '\n'
10
+ 'STYLE — a flowing story, NOT a checklist:\n'
11
+ '- Connected, natural English prose. NO headings, NO bullets, NO field labels.\n'
12
+ '- The MAIN THREAD is what HAPPENS — lead with the action. Do NOT march subject-by-subject\n'
13
+ ' listing attributes; weave the few details you keep into the action naturally.\n'
14
+ '- One cohesive paragraph; never pad to add length.\n'
15
+ '\n'
16
+ 'WHAT TO COVER (only these):\n'
17
+ '- The scene in a brief phrase.\n'
18
+ '- Each main subject by name/what-it-is plus ONE most defining visual trait — not a full\n'
19
+ ' appearance list.\n'
20
+ '- What happens over the clip, in correct chronological order as the backbone, with an EXPLICIT\n'
21
+ ' timestamp in seconds on each action, distributed within the given video duration — e.g.\n'
22
+ ' "at 0.0s", "from 1.2s to 2.7s", "around the 3.4s mark", "finally from 4.0s to 5.0s". Span the\n'
23
+ ' actions from 0s up to (but never beyond) the stated duration; the last one ends at or before it.\n'
24
+ ' Attach a timestamp ONLY to a real action or change; mention a static/unchanging element once\n'
25
+ ' with NO timestamp — never put a whole-clip span on something that merely exists.\n'
26
+ '- A one-word shot type only if it matters; otherwise skip the camera.\n'
27
+ "- Named entities only if clearly implied — don't invent identities.\n"
28
+ '\n'
29
+ 'FAITHFUL EXPANSION:\n'
30
+ '- Stay consistent with the prompt; elaborate plausibly but never contradict it. If a first\n'
31
+ ' frame is given, ground the scene and subject in it.\n'
32
+ '- Keep actions in their natural, real-world direction — never reverse them. Commit to one\n'
33
+ " coherent interpretation; don't hedge with alternatives.\n"
34
+ '- Keep subject identities and counts consistent.\n'
35
+ '\n'
36
+ 'DOMAIN NOTES (apply whichever fits):\n'
37
+ '- Multiple events / sequence: lay out consecutive actions in STRICT chronological order as\n'
38
+ ' distinct, separated steps with approximate timing — never blur them into one vague action.\n'
39
+ '- Robot manipulation (VLA): subjects are robotic arm(s), gripper(s) and workspace objects,\n'
40
+ ' NOT people — no clothing/skin/gender/expression. When two arms are present, ALWAYS keep the\n'
41
+ ' LEFT and RIGHT arm distinct and state which arm/gripper does each motion; never swap, merge,\n'
42
+ ' or leave it ambiguous. Manipulation is STRICTLY NOT reversible.\n'
43
+ "- First-person (EGO): a head-mounted first-person view; camera motion IS the wearer's head\n"
44
+ ' movement. The agent is the camera-wearer, a PERSON shown through their own hands/arms and\n'
45
+ ' viewpoint; never a robot or external third-person subject. Stay strictly first-person.\n'
46
+ '\n'
47
+ 'Output ONLY the caption text — no preamble, no headings, no explanation.\n'
48
+ '\n'
49
+ "## EXAMPLES — one representative example per domain. When you write your own caption, match these examples' style, format, length, and timestamp convention (each action gets a numeric timestamp in seconds within the video's duration).\n"
50
+ '\n'
51
+ '### Example 1 — general\n'
52
+ 'USER PROMPT:\n'
53
+ '低角度广角镜头,镜头静止。中央大型条纹热气球缓慢上升并飘移(气球呈红橙黄绿蓝条纹,底部深色),背景中多个热气球在天空漂移。地面草地上散布着正在充气的热气球,一辆红色皮卡停放在旁。随后一辆蓝色拖车进入画面并横穿前景(覆盖蓝色防水布)。饱和色彩,硬光,日光,电影质感。\n'
54
+ '\n'
55
+ 'DETAILED CAPTION:\n'
56
+ 'At a vibrant hot air balloon festival on a sunny day, a wide shot shows a grassy field under a blue sky. A large, multi-colored striped balloon dominates the center, and from the start at 0.0 seconds until 5.3 seconds, it slowly rises and drifts slightly upward and to the right. Simultaneously during the 0.0s to 5.3s period, several small background balloons drift slowly across the sky in various directions above a cluster of grounded balloons. A red pickup truck and white van sit parked near the left side, while finally, around the 4.3s mark until 5.3s, a blue trailer enters the frame from the right and moves left across the foreground.\n'
57
+ '\n'
58
+ '### Example 2 — multi-event\n'
59
+ 'USER PROMPT:\n'
60
+ 'Subtitle: Outdoor High-Intensity Workout — All live-action, hyper-realistic, fixed shot with slight handheld unsteadiness.\n'
61
+ '\n'
62
+ 'Style: Hard daylight, high-angle wide shot, center composition, sharp shadows, athletic tension.\n'
63
+ '\n'
64
+ '1. Muscular shirtless man, black headwrap, black boxing gloves, blue digital camo pants, black combat boots. Standing on concrete surface, background includes beige wall, chain-link fence, and trash bins. Stands in a boxing stance with hands up.\n'
65
+ '\n'
66
+ '2. Drops into a deep squat, then jumps upward explosively.\n'
67
+ '\n'
68
+ '3. Lands and drops into a deep squat, repeating this explosive jump and squat landing cycle.\n'
69
+ '\n'
70
+ '4. After the final landing and squat, begins to stand up.\n'
71
+ '\n'
72
+ 'Overall movements coherent and natural, high contrast lighting, focused and athletic atmosphere.\n'
73
+ '\n'
74
+ 'DETAILED CAPTION:\n'
75
+ 'In an outdoor urban setting against a beige wall, a shirtless man wearing boxing gloves performs a workout on concrete. From 0.0s to 0.6s he stands in a boxing stance with hands up. Then from 0.6s to 1.2s he drops into a deep squat, followed by the period from 1.2s to 1.8s where he jumps upward explosively. From 1.8s to 2.4s he lands and drops into a deep squat, then from 2.4s to 3.0s jumps upward explosively again. Around the 3.0s to 3.6s mark he lands and drops into a deep squat, proceeding to jump upward explosively from 3.6s to 4.2s. From 4.2s to 4.8s he lands and drops into a deep squat once more, and finally from 4.8s to 5.0s he begins to stand up.\n'
76
+ '\n'
77
+ '### Example 3 — VLA\n'
78
+ 'USER PROMPT:\n'
79
+ '环境:自动化超市补货工作站,俯视视角,明亮均匀人工光,清晰功能化氛围。物体:左侧打开的棕色纸箱内含整齐堆叠的红色香肠包装,右侧白色矩形料箱内含红黄绿混合食品包装及带黑色把手的透明塑料隔板。机器人:双臂系统。左臂黑色机身银色底座黑色夹爪(全程静止悬停),右臂白色机身黑色夹爪腕部蓝色指示灯(活动主体)。动作:右臂向下向左移动伸入白色料箱,抓取隔板黑色把手,向右滑动隔板,释放把手,向上向右收回复位。左臂悬停于纸箱上方保持静止。相机:固定高角度俯视视角,全程静止镜头,宽画幅,超广角镜头,柔和人工光,极致细节。\n'
80
+ '\n'
81
+ 'DETAILED CAPTION:\n'
82
+ 'A top-down wide shot shows an automated workspace with a cardboard box of sausage packages on the left and a white bin on the right. A stationary black and silver left robotic arm hovers over the box, while a white and black right robotic arm operates above the bin containing a black handle. From 0.0s to 2.0s, the right arm moves downwards and to the left, reaching into the white bin as the handle remains still. Then from 2.0s to 6.0s, the right arm grasps the black handle and moves to the right, pulling the handle and sliding the divider across the bin. Finally from 6.0s to 9.2s, the right arm releases the handle and moves upwards and to the right, returning to a resting position while the handle stays stationary. The box and bin remain fixed throughout.\n'
83
+ '\n'
84
+ '### Example 4 — EGO\n'
85
+ 'USER PROMPT:\n'
86
+ 'First-person POV, brightly lit grocery store produce section, soft artificial lighting. Two rectangular bins side-by-side in front; left filled with red/yellow apples, right piled with bright orange oranges. Background shows aisles and shoppers, one in red shirt with basket. Right hand enters from bottom, reaches into right orange pile, grasps one orange, lifts it upwards and slightly left. Camera moves forward and slightly down approaching bins, then remains stable with minor panning/tilting following hand movement.\n'
87
+ '\n'
88
+ 'DETAILED CAPTION:\n'
89
+ "From a first-person perspective in a grocery store produce section, the camera approaches display bins containing apples on the left and bright oranges on the right. In the background aisles, a shopper wearing a red shirt walks from the left side of the frame towards the right between 0.0s and 2.0s, while another shopper in a striped shirt walks away from the camera down the aisle from 0.0s to 5.0s. The operator's right hand enters the frame from the bottom from 0.0s to 2.5s, then reaches into the pile of oranges and grasps one from 2.5s to 3.5s. Finally, the hand lifts the grasped orange upwards and to the left from 3.5s to 5.0s, selecting it from the cluster."
90
+ )
91
+
92
+ VIDEO_STEP2_MAP = (
93
+ 'You are a structuring engine. You convert a DETAILED natural-language video caption (which concisely states the scene and the full motion/timing plan) into a STRUCTURED JSON caption. Your job is FAITHFUL structural extraction ONLY: do NOT drop, alter, reorder, or re-time anything the prose states (keep every subject, every action, and every timestamp exactly), and do NOT re-plan motion; BUT the prose is brief and intentionally omits fine visual attributes and camera settings — you MUST fill those omitted fields (texture, skin tone, precise colors, relative size, pose/orientation, clothing, and all camera_info) with plausible values that stay consistent with and never contradict the prose — just map what the prose already states into the schema.\n'
94
+ '\n'
95
+ '## OUTPUT FORMAT (JSON)\n'
96
+ '\n'
97
+ '```json\n'
98
+ '{\n'
99
+ ' "comprehensive_description": {\n'
100
+ ' "scene_content_description": "(String) A detailed description focusing on scene content, subject appearance, lighting, atmosphere, narrative, and interactions between elements. Text physically printed on visual objects should be mentioned alongside the object and detailed further in prominent_elements. For standalone OCR/text elements, briefly describe their role, relative scale, font style, color, and orientation; categorize them as \'static overlays\' (like watermarks) or \'integrated scene text\' (like subtitles/scrolling text); and describe their temporal behavior if they appear or disappear. Provide an exact transcription for prominent text (preserving spelling, punctuation, and capitalization), but summarize long or dense text blocks instead of transcribing them fully. **Do not describe camera movement here**. Maximum 800 words.",\n'
101
+ ' "camera_movement_description": "(String) A detailed description focusing on camera behavior. Include camera movement types (Pan, Tilt, Zoom, Dolly, Truck, Roll), shooting angles (high/low/eye-level), shot size changes, and stability. Maximum 100 words. If the camera is essentially stationary, set to \'\'."\n'
102
+ ' },\n'
103
+ '\n'
104
+ ' "prominent_elements": [\n'
105
+ ' {\n'
106
+ ' "name": "(String) Short label for the object (e.g., \'red sports car\', \'elderly man\')",\n'
107
+ ' "description": "(String) Detailed visual description of this specific element",\n'
108
+ ' "actions": [\n'
109
+ ' {\n'
110
+ ' "timestamp": "(String) Time range when this action occurs, e.g., \'[0.0s - 3.0s]\'",\n'
111
+ ' "action": "(String) Specific action description during this time period. **Direction must be described from the observer\'s perspective**. If the element has no action throughout, the entire actions array contains only one element with action set to \'\'."\n'
112
+ ' }\n'
113
+ ' ],\n'
114
+ ' "location": "(String) Precise position in the frame or main area of activity (from observer\'s perspective)",\n'
115
+ ' "relative_size": "(String) small / medium / large / dominant",\n'
116
+ ' "shape_and_color": "(String) Basic geometric shape and dominant colors",\n'
117
+ ' "texture": "(String) e.g., smooth, rough, metallic, furry, glossy, matte",\n'
118
+ ' "appearance_details": "(String) Specific details such as patterns, text physically printed on the object, wear marks, or distinctive markings",\n'
119
+ ' "relationship": "(String) This object\'s spatial or contextual relationship with other elements in the scene. If this object obscures text, specify exactly what it covers (e.g., \'blocking the letter O in COW). If it is obscured by floating text, describe what part of the object is covered.",\n'
120
+ ' "orientation": "(String) e.g., upright, tilted, horizontal, facing away, diagonal",\n'
121
+ ' \n'
122
+ ' "pose": "(String) Body posture and its changes (human/humanoid only, otherwise empty)",\n'
123
+ ' "expression": "(String) Facial expression and emotional changes (human/humanoid only, otherwise empty)",\n'
124
+ ' "clothing": "(String) Clothing description, including colors and styles (human/humanoid only, otherwise empty)",\n'
125
+ ' "gender": "(String) Apparent gender (human/humanoid only, otherwise empty)",\n'
126
+ ' "skin_tone_and_texture": "(String) Skin appearance (human/humanoid only, otherwise empty)",\n'
127
+ ' \n'
128
+ ' "is_cluster": "true (only for cluster objects, omit otherwise)",\n'
129
+ ' "number_of_objects": "(String) Exact number if countable, otherwise \'several\' (3-6), \'many\' (7-20), or \'numerous\' (20+)"\n'
130
+ ' }\n'
131
+ ' ],\n'
132
+ '\n'
133
+ ' "camera_info": {\n'
134
+ ' "color": "(String) Warm, Cool, Mixed, Saturated, Desaturated, Black and White, Red, Orange, Yellow, Green, Cyan, Blue, Magenta, or Pink",\n'
135
+ ' "frame_size": "(String) Extreme Wide, Wide, Medium Wide, Medium, Medium Close Up, Close Up, or Extreme Close Up",\n'
136
+ ' "shot_type_angle": "(String) High angle, Low angle, Dutch angle, Overhead, Aerial, or Eye level",\n'
137
+ ' "lens_size": "(String) Ultra Wide / Fisheye, Wide, Medium, Long Lens, or Telephoto",\n'
138
+ ' "composition": "(String) Center, Balanced, Symmetrical, Left heavy, Right heavy, or Short side",\n'
139
+ ' "lighting": "(String) Hard light, Soft light, High contrast, Low contrast, Side light, Top light, Underlight, Backlight, Edge light, or Silhouette",\n'
140
+ ' "lighting_type": "(String) Daylight, Sunny, Overcast, Moonlight, Artificial light, Practical light, Tungsten, Fluorescent, Firelight, or Mixed light"\n'
141
+ ' }\n'
142
+ '}\n'
143
+ '```\n'
144
+ '\n'
145
+ '## IMPORTANT RULES\n'
146
+ '\n'
147
+ '1. **Think First:** Before generating the JSON, perform an internal "Let\'s think step by step" synthesis.\n'
148
+ '2. **Output Only JSON:** Do not output any thinking process or Markdown text outside of the JSON code block.\n'
149
+ '3. **Strict Fidelity (No Modification)**:\n'
150
+ '- Identity: Never alter the identity of subjects (e.g., a "real tiger" remains "real tiger").\n'
151
+ '- Actions: Verbs must be preserved exactly (e.g., "walking" cannot be changed to "running"). Sequence Integrity: You must preserve the complete action chain defined in the prompt. Do not summarize, skip, or merge distinct sequential steps into a single state. No Collective Summarization: Do not use summary phrases (e.g., "a sequence of...", "various movements") to cover multiple steps. If the prompt defines A -> B -> C, every step must be represented as a separate action segment. Any skipped step or summarized sequence is a CRITICAL ERROR.\n'
152
+ '- Quantity: If a number is specified, it must be exact. Quantifiers must be strictly preserved. "All" means all, "every" means every, "a single" means exactly 1. Do not paraphrase quantifiers into vague terms.\n'
153
+ '- Spatial Integrity: Relative positions (e.g., "A on the left of B") must be fixed as stated.\n'
154
+ '- Color & Text: Colors must be accurate; OCR text must be transcribed character-for-character (case-sensitive).\n'
155
+ '4. **Creative Expansion Constraints:** When expanding a simple prompt, ensure added details (e.g., "sun-drenched oak windowsill") never contradict, replace, or occlude the user\'s explicit subject requirements.\n'
156
+ '5. **Text Placement Logic:** \n'
157
+ '- Surface-Bound Text (Printed on objects): Must be transcribed exactly within the appearance_details field of its respective object in prominent_elements.\n'
158
+ '- Standalone Graphic Text (Floating/Poster text): Must NOT be an entry in prominent_elements. Describe its role, style, and exact transcription exclusively within the comprehensive_description.\n'
159
+ "- Dynamic Text: If the scene contains moving or updating text (e.g., a scrolling ticker or a subtitle), you must explicitly describe the text's movement path and content change frequency within the actions array of the respective prominent_elements or the comprehensive_description.\n"
160
+ '6. **Occlusion & Relational Mapping:** Explicitly document overlaps. If text occludes an object, or an object occludes text, specify which character(s) or object parts are affected in both relationship and comprehensive_description.\n'
161
+ '7. **Handle Missing Data:** If an attribute is not applicable to an element (e.g., pose for a mountain), set its value to "" (an empty string). Never use "N/A", "unknown", "not applicable", etc.\n'
162
+ '8. **Counting:** For number_of_objects, provide an exact number if specified by the user. Otherwise, use: "several" (3-6), "many" (7-20), or "numerous" (20+).\n'
163
+ '9. **Perspective:** Always describe positions (location) and directions (orientation) as "left" and "right" from the viewer\'s perspective.\n'
164
+ '10. **Visual Descriptive Realism:** Avoid abstract or emotional adjectives (e.g., "beautiful," "sad"). Instead, translate them into visually observable details (e.g., "soft golden-hour rim lighting," "desaturated cool blue tones with falling rain droplets").\n'
165
+ '11. **Clusters:** When describing a cluster (e.g., "a forest"), describe the collective appearance of the group rather than listing every individual tree.\n'
166
+ '12. **Attribute Consistency:** Ensure that the comprehensive_description and the individual prominent_elements are perfectly synchronized. Every element mentioned in the elements list must exist in the paragraph description and vice versa.\n'
167
+ '13. **Negative Constraints:** Any explicit prohibitions (e.g., "no text," "no background") must be strictly upheld. Never add elements that the user has explicitly requested to exclude.\n'
168
+ '14. **Subject Priority:** The primary subject specified by the user must be the anchor of the scene. Its description and prominence in the JSON must reflect its status as the most important element.\n'
169
+ '15. **Integrity:** Before generating JSON, you must cross-verify all keywords, actions, and states against the User Prompt; if any item is missing or any state intensity deviates from the original, you MUST regenerate the content to ensure 100% fidelity. \n'
170
+ '16. **Non-OCR Classification:** Do not force objects into "text" status. If a word appears in the prompt without context of "sign," "label," or "document," describe it as a physical attribute or brand marking on the object, not as OCR/Typography.\n'
171
+ "17. **Camera Fidelity:** The camera_info must be logically consistent with the visual scene. For example, if the scene is a vast landscape, the camera should reflect a 'Wide' or 'Ultra Wide' frame size and appropriate lens settings. Avoid contradictory pairings (e.g., 'Extreme Close Up' with a 'Wide' lens).\n"
172
+ '18. **Temporal Continuity:** Ensure temporal continuity for all subjects and environment settings. If an object is introduced, it must not disappear or reappear without a logical reason (e.g., leaving the frame or being occluded). Texture, color, and lighting must remain consistent across all action segments.\n'
173
+ "19. **Action-Camera Sync:** The camera movement must logically justify the subject's displacement. If the camera follows a subject (tracking shot), the subject's position in the frame should remain relatively stable. If the camera is static, the subject must show movement within the frame.\n"
174
+ "20. **Physical Logic:** All movements must respect physical laws unless the prompt specifies a surreal or fantasy context. Avoid 'gliding' motions; ensure footsteps or object interactions (e.g., picking up an item) align with the timestamped actions.\n"
175
+ '21. **Motion and Velocity:** Verbs must describe the process of an action, not just the result (e.g., use \'rising from the chair\' instead of just \'standing\'). Every action in the actions array must include a velocity modifier (e.g., "slowly walking," "abruptly turning," "smoothly panning," "rapidly accelerating"). Avoid generic verbs without speed or force descriptors.\n'
176
+ '22. **Language:** Output all JSON content in professional English, except for OCR text, which must be transcribed exactly as provided to maintain absolute fidelity.\n'
177
+ '23. **Audio Neglect:** The User Prompt may contain audio, music, or voiceover instructions. Strictly ignore all audio-related instructions. Do not attempt to rewrite, describe, or represent these as part of the JSON output. Focus exclusively on the visual, temporal, and narrative elements and OCR texts.\n'
178
+ '24. **Duration Adherence:** A target video duration is provided in the input. Every timestamp in the `actions` array MUST fall within `[0.0s, the given duration]`, and the action timeline should span up to (but never beyond) this duration. Distribute the action segments proportionally to the given length; the final segment must end at or before the given duration.\n'
179
+ '\n'
180
+ "## THIS STEP'S INPUT/OUTPUT\n"
181
+ '- Input: the detailed prose caption + the target video duration.\n'
182
+ '- The expansion/thinking is already done in the prose — preserve the stated core EXACTLY (scene, subjects, every action + timestamp, named entities); ONLY the fine visual attributes and camera fields that the brief prose omits may be completed — plausibly, consistently, never contradicting the prose.\n'
183
+ '- Output ONLY the JSON object (valid and parseable), following the schema and rules above.'
184
+ )
185
+
186
+ IMAGE_STEP1_EXPAND = (
187
+ 'You write ONE short, natural, standalone IMAGE caption in English from a brief user image\n'
188
+ 'prompt — as if briefly describing a real photo of that idea.\n'
189
+ '\n'
190
+ 'This is a SINGLE STILL IMAGE: there is NO motion, NO time, NO timestamps, NO camera movement.\n'
191
+ 'Describe only what the photo shows; never invent action, sequence, or a moving camera.\n'
192
+ '\n'
193
+ 'HARD LENGTH LIMIT: the output MUST be UNDER 1000 characters. Aim for roughly 400-800\n'
194
+ 'characters. Concise is a top priority.\n'
195
+ '\n'
196
+ 'STYLE — a flowing description, NOT a checklist:\n'
197
+ '- Connected, natural English prose. NO headings, NO bullets, NO field labels.\n'
198
+ '- The MAIN THREAD is the main subject and the scene — lead with WHAT THE IMAGE SHOWS. Do NOT\n'
199
+ ' march subject-by-subject listing attributes; weave the few details you keep into the\n'
200
+ ' description naturally. One cohesive paragraph; never pad to add length.\n'
201
+ '\n'
202
+ 'WHAT TO COVER (only these):\n'
203
+ '- The scene in a brief phrase (where it is / overall vibe).\n'
204
+ '- Each main subject by name/what-it-is plus ONE most defining visual trait — not a full\n'
205
+ ' appearance list. The key spatial arrangement only if it defines the image.\n'
206
+ '- A one-word shot type only if it matters (e.g. close-up, wide shot, aerial); otherwise skip the camera.\n'
207
+ "- Named entities (real people / places / landmarks / brands) only if clearly implied — don't invent identities.\n"
208
+ '\n'
209
+ 'FAITHFUL EXPANSION:\n'
210
+ '- Stay consistent with the prompt; elaborate plausibly but never contradict it. Commit to one\n'
211
+ " coherent interpretation; don't hedge with alternatives.\n"
212
+ '- Keep subject identities and counts consistent.\n'
213
+ '\n'
214
+ 'Output ONLY the caption text — no preamble, no headings, no explanation.\n'
215
+ '\n'
216
+ '\n'
217
+ "## EXAMPLES — one representative example per type. When you write your own caption, match these examples' style, format, and length (one flowing paragraph, no timestamps, no camera movement — it is a single still image).\n"
218
+ '\n'
219
+ '### Example 1 — animal / wildlife\n'
220
+ 'USER PROMPT:\n'
221
+ 'A leopard tortoise dominates the center of the frame, captured from a high angle with its head and front legs extended toward the left. Its domed shell displays a striking mosaic of black and tan geometric patterns, contrasting with the rough, yellowish-tan scales of its skin and sharp claws. The creature is positioned on reddish-brown sandy soil scattered with dry twigs and pebbles, illuminated by warm daylight that creates a soft shadow beneath it against a backdrop of green grass on the right.\n'
222
+ '\n'
223
+ 'DETAILED CAPTION:\n'
224
+ 'A leopard tortoise is positioned centrally in a natural outdoor setting, resting on reddish-brown sandy soil scattered with small pebbles and dry twigs. Facing toward the left with its head and front legs extended, the animal displays a domed shell marked by intricate black and tan geometric patterns reminiscent of leopard spots. Its yellowish-tan skin contrasts with the earthy ground beneath its clawed feet, while a patch of green grass and low-lying vegetation rises in the background to the right. The distinct markings of the shell stand out against the textured ground and sparse greenery, capturing the tortoise within its earthy environment.\n'
225
+ '\n'
226
+ '### Example 2 — architecture / landmark\n'
227
+ 'USER PROMPT:\n'
228
+ '极度广角镜头,高角度拍摄。明亮日光,硬光,暖色调,清晰阴影。宏伟的古典风格宫殿庭院,浅色石材。中央巨大的多层拱形入口,饰有金色和蓝色装饰带,深色凹陷内部可见小金门。宽阔石阶通向圆形分层平台。两侧对称亭阁,顶部为反射金色圆顶,蓝色瓷砖拱门,装饰攀爬绿藤和粉色花朵。露台布满茂盛绿植和小花。若干穿着传统长袍(白、灰、棕、红)的小人物散布庭院以示比例。背景岩石山坡,远处建筑,晴朗明亮天空。写实建筑摄影。\n'
229
+ '\n'
230
+ 'DETAILED CAPTION:\n'
231
+ 'A grand, sun-drenched courtyard of a classical-style palace complex is dominated by a massive central building featuring a large, multi-tiered arched entrance. Wide stone stairs lead up to the structure, flanked on either side by identical pavilions topped with brilliant, reflective golden domes and blue-tiled arches. The light-colored stone complex includes various terraces filled with lush greenery, while several small figures dressed in traditional robes are scattered throughout the courtyard, providing a sense of scale against the monumental architecture. In the background, a rocky hillside rises under a clear bright sky, dotted with additional buildings and vegetation surrounding the temple-like grounds.\n'
232
+ '\n'
233
+ '### Example 3 — landscape / nature\n'
234
+ 'USER PROMPT:\n'
235
+ "Make me an image of a serene coastal scene featuring a classic white lighthouse attached to a keeper's house, with a vintage red pickup truck and a small boat on a trailer parked on a dirt path nearby, set against a lush green lawn, wooden fence, and bright blue sky over calm waters.\n"
236
+ '\n'
237
+ 'DETAILED CAPTION:\n'
238
+ "A wide aerial view captures a serene coastal scene dominated by a classic white lighthouse and its attached keeper's house. The tall cylindrical tower features a black lantern room and rises from a white residence with a brown shingled roof and green shutters. To the left, a classic red pickup truck is parked on a dirt path with a small white and red boat resting on a trailer behind it. A lush green lawn slopes down toward a rocky shoreline where a weathered wooden fence runs along the edge of the deep blue water. In the background, a calm sea stretches to the horizon, meeting a distant tree-covered coastline under a bright blue sky filled with soft clouds.\n"
239
+ '\n'
240
+ '### Example 4 — person / portrait\n'
241
+ 'USER PROMPT:\n'
242
+ 'A stylish woman with long wavy blonde hair and bold red lipstick stands confidently on a city sidewalk, wearing a black long-sleeved dress with sheer patterned sleeves and a ruffled waistline. She carries a small black crossbody bag with a gold Saint Laurent logo slung over her shoulder, her left arm slightly extended and right arm by her side in a high-angle medium wide shot. The background features brick buildings and a blurred crosswalk with distant pedestrians under soft daylight, creating a sophisticated urban atmosphere with a left-heavy composition.\n'
243
+ '\n'
244
+ 'DETAILED CAPTION:\n'
245
+ 'A stylish woman with long blonde hair and bold red lipstick stands on a city sidewalk, gazing directly forward in a black long-sleeved dress with sheer patterned sleeves and a ruffled waistline. A small black crossbody bag featuring a gold Saint Laurent logo hangs at her side, complementing her sophisticated look. The setting is a classic urban street scene characterized by brick buildings and a black wall-mounted lamp visible behind her. In the distance, a blurred crosswalk shows several pedestrians, adding depth to the modern atmosphere surrounding the central figure.'
246
+ )
247
+
248
+ IMAGE_STEP2_MAP = (
249
+ 'You convert a SHORT natural-language STILL-IMAGE caption (the DETAILED CAPTION) into ONE\n'
250
+ 'structured JSON caption describing the image. Output ONLY the JSON object — no prose, no code fence.\n'
251
+ '\n'
252
+ '## TASK SEMANTICS (read carefully)\n'
253
+ 'The detailed caption is SHORT and LOSSY: it states the scene, the main subjects, and each\n'
254
+ "subject's single most defining trait, but it deliberately OMITS fine attributes (exact colors,\n"
255
+ 'texture, relative size, orientation, clothing, skin tone, pose, expression) and the camera\n'
256
+ 'metadata. Your job:\n'
257
+ '- KEEP faithfully everything the prose states (scene, every named subject, named entities) — do\n'
258
+ ' not drop, rename, merge, or split subjects, and do not contradict the prose.\n'
259
+ '- REASONABLY COMPLETE the fields the prose omits (fine attributes + camera_info) with plausible,\n'
260
+ ' self-consistent values that do NOT contradict the prose. This is expected: the target JSON is a\n'
261
+ ' FULL caption, so every schema field must be filled even when the prose did not mention it.\n'
262
+ '- This is a STILL IMAGE: no actions, no timestamps, no camera movement anywhere.\n'
263
+ '\n'
264
+ '## OUTPUT SCHEMA (exact keys)\n'
265
+ 'A single JSON object with EXACTLY these top-level keys:\n'
266
+ '- "comprehensive_description": string — one flowing prose paragraph describing the whole image\n'
267
+ ' (scene, subjects, their arrangement and look). No field labels inside it.\n'
268
+ '- "camera_info": object with EXACTLY these 7 string keys, each set to ONE value from its list:\n'
269
+ ' - "color": Warm | Cool | Mixed | Saturated | Desaturated | White | Green | Blue | Red | Cyan\n'
270
+ ' - "frame_size": Extreme Close Up | Close Up | Medium Close Up | Medium | Medium Wide | Wide | Extreme Wide\n'
271
+ ' - "shot_type_angle": Eye level | Low angle | High angle | Overhead | Aerial\n'
272
+ ' - "lens_size": Ultra Wide / Fisheye | Wide | Medium | Long Lens | Telephoto\n'
273
+ ' - "composition": Center | Balanced | Symmetrical | Left heavy | Right heavy\n'
274
+ ' - "lighting": Soft light | Hard light | Top light | Underlight | Backlight\n'
275
+ ' - "lighting_type": Daylight | Artificial light\n'
276
+ '- "world_knowledge": array of strings — named entities / real-world facts (people, places,\n'
277
+ ' landmarks, brands) stated or clearly implied by the prose; [] if none. NEVER invent identities.\n'
278
+ '- "prominent_elements": array of objects, one per notable subject/object. Each element has:\n'
279
+ ' - always: "name", "description", "location", "relative_size" (one of: dominant | large | medium | small),\n'
280
+ ' "shape_and_color", "texture", "appearance_details", "relationship", "orientation"\n'
281
+ ' - for a PERSON, additionally: "pose", "expression", "clothing", "gender" (male | female),\n'
282
+ ' "skin_tone_and_texture"\n'
283
+ ' - for a GROUP/crowd of like items, additionally: "is_cluster": true and "number_of_objects": "<count or range as string>"\n'
284
+ '\n'
285
+ '## RULES\n'
286
+ '- Output STRICT valid JSON only (double quotes, no trailing commas, no comments, no code fence).\n'
287
+ '- Fill EVERY field; never leave a required field empty. Values you complete must be consistent\n'
288
+ ' with the prose and with each other.\n'
289
+ '- Keep the number and identity of prominent_elements aligned with the subjects the prose names\n'
290
+ ' (you may add a clearly-implied background element, but do not invent unrelated subjects).\n'
291
+ '- Copy named entities verbatim into world_knowledge.\n'
292
+ '- No actions, no timestamps, no camera-movement fields — this is a still image.'
293
+ )