prithivMLmods commited on
Commit
16ef42e
·
verified ·
1 Parent(s): 8fc5e6e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +337 -84
app.py CHANGED
@@ -4,12 +4,18 @@ import torch
4
  from diffusers.pipelines.wan.pipeline_wan_i2v import WanImageToVideoPipeline
5
  from diffusers.models.transformers.transformer_wan import WanTransformer3DModel
6
  from diffusers.utils.export_utils import export_to_video
 
7
  import gradio as gr
8
  import tempfile
9
  import numpy as np
10
  from PIL import Image
11
  import random
12
  import gc
 
 
 
 
 
13
 
14
  from torchao.quantization import quantize_
15
  from torchao.quantization import Float8DynamicActivationFloat8WeightConfig
@@ -18,6 +24,7 @@ from torchao.quantization import Int8WeightOnlyConfig
18
  import aoti
19
 
20
 
 
21
  MODEL_ID = "Wan-AI/Wan2.2-I2V-A14B-Diffusers"
22
 
23
  MAX_DIM = 832
@@ -31,17 +38,22 @@ FIXED_FPS = 16
31
  MIN_FRAMES_MODEL = 8
32
  MAX_FRAMES_MODEL = 80
33
 
34
- MIN_DURATION = round(MIN_FRAMES_MODEL/FIXED_FPS,1)
35
- MAX_DURATION = round(MAX_FRAMES_MODEL/FIXED_FPS,1)
36
 
 
37
 
38
- pipe = WanImageToVideoPipeline.from_pretrained(MODEL_ID,
39
- transformer=WanTransformer3DModel.from_pretrained('cbensimon/Wan2.2-I2V-A14B-bf16-Diffusers',
 
 
 
40
  subfolder='transformer',
41
  torch_dtype=torch.bfloat16,
42
  device_map='cuda',
43
  ),
44
- transformer_2=WanTransformer3DModel.from_pretrained('cbensimon/Wan2.2-I2V-A14B-bf16-Diffusers',
 
45
  subfolder='transformer_2',
46
  torch_dtype=torch.bfloat16,
47
  device_map='cuda',
@@ -50,15 +62,15 @@ pipe = WanImageToVideoPipeline.from_pretrained(MODEL_ID,
50
  ).to('cuda')
51
 
52
  pipe.load_lora_weights(
53
- "Kijai/WanVideo_comfy",
54
- weight_name="Lightx2v/lightx2v_I2V_14B_480p_cfg_step_distill_rank128_bf16.safetensors",
55
  adapter_name="lightx2v"
56
  )
57
  kwargs_lora = {}
58
  kwargs_lora["load_into_transformer_2"] = True
59
  pipe.load_lora_weights(
60
- "Kijai/WanVideo_comfy",
61
- weight_name="Lightx2v/lightx2v_I2V_14B_480p_cfg_step_distill_rank128_bf16.safetensors",
62
  adapter_name="lightx2v_2", **kwargs_lora
63
  )
64
  pipe.set_adapters(["lightx2v", "lightx2v_2"], adapter_weights=[1., 1.])
@@ -79,10 +91,21 @@ spaces.aoti_load(
79
  repo_id='cbensimon/WanTransformer3DModel-sm120-cu130-raa',
80
  )
81
 
 
 
 
 
 
 
 
 
82
 
 
83
  default_prompt_i2v = "make this image come alive, cinematic motion, smooth animation"
84
  default_negative_prompt = "色调艳丽, 过曝, 静态, 细节模糊不清, 字幕, 风格, 作品, 画作, 画面, 静止, 整体发灰, 最差质量, 低质量, JPEG压缩残留, 丑陋的, 残缺的, 多余的手指, 画得不好的手部, 画得不好的脸部, 畸形的, 毁容的, 形态畸形的肢体, 手指融合, 静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走"
85
 
 
 
86
  def resize_image(image: Image.Image) -> Image.Image:
87
  """
88
  Resizes an image to fit within the model's constraints, preserving aspect ratio as much as possible.
@@ -94,12 +117,12 @@ def resize_image(image: Image.Image) -> Image.Image:
94
  return image.resize((SQUARE_DIM, SQUARE_DIM), Image.LANCZOS)
95
 
96
  aspect_ratio = width / height
97
-
98
- MAX_ASPECT_RATIO = MAX_DIM / MIN_DIM
99
- MIN_ASPECT_RATIO = MIN_DIM / MAX_DIM
100
 
101
  image_to_resize = image
102
-
103
  if aspect_ratio > MAX_ASPECT_RATIO:
104
  # Very wide image -> crop width to fit 832x480 aspect ratio
105
  target_w, target_h = MAX_DIM, MIN_DIM
@@ -125,7 +148,7 @@ def resize_image(image: Image.Image) -> Image.Image:
125
 
126
  final_w = max(MIN_DIM, min(MAX_DIM, final_w))
127
  final_h = max(MIN_DIM, min(MAX_DIM, final_h))
128
-
129
  return image_to_resize.resize((final_w, final_h), Image.LANCZOS)
130
 
131
 
@@ -137,6 +160,70 @@ def get_num_frames(duration_seconds: float):
137
  ))
138
 
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  def get_duration(
141
  input_image,
142
  prompt,
@@ -157,62 +244,26 @@ def get_duration(
157
  step_duration = BASE_STEP_DURATION * factor ** 1.5
158
  return 10 + int(steps) * step_duration
159
 
 
160
  @spaces.GPU(duration=get_duration)
161
  def generate_video(
162
  input_image,
163
  prompt,
164
- steps = 4,
165
  negative_prompt=default_negative_prompt,
166
- duration_seconds = MAX_DURATION,
167
- guidance_scale = 1,
168
- guidance_scale_2 = 1,
169
- seed = 42,
170
- randomize_seed = False,
171
  progress=gr.Progress(track_tqdm=True),
172
  ):
173
  """
174
  Generate a video from an input image using the Wan 2.2 14B I2V model with Lightning LoRA.
175
-
176
- This function takes an input image and generates a video animation based on the provided
177
- prompt and parameters. It uses an FP8 qunatized Wan 2.2 14B Image-to-Video model in with Lightning LoRA
178
- for fast generation in 4-8 steps.
179
-
180
- Args:
181
- input_image (PIL.Image): The input image to animate. Will be resized to target dimensions.
182
- prompt (str): Text prompt describing the desired animation or motion.
183
- steps (int, optional): Number of inference steps. More steps = higher quality but slower.
184
- Defaults to 4. Range: 1-30.
185
- negative_prompt (str, optional): Negative prompt to avoid unwanted elements.
186
- Defaults to default_negative_prompt (contains unwanted visual artifacts).
187
- duration_seconds (float, optional): Duration of the generated video in seconds.
188
- Defaults to 2. Clamped between MIN_FRAMES_MODEL/FIXED_FPS and MAX_FRAMES_MODEL/FIXED_FPS.
189
- guidance_scale (float, optional): Controls adherence to the prompt. Higher values = more adherence.
190
- Defaults to 1.0. Range: 0.0-20.0.
191
- guidance_scale_2 (float, optional): Controls adherence to the prompt. Higher values = more adherence.
192
- Defaults to 1.0. Range: 0.0-20.0.
193
- seed (int, optional): Random seed for reproducible results. Defaults to 42.
194
- Range: 0 to MAX_SEED (2147483647).
195
- randomize_seed (bool, optional): Whether to use a random seed instead of the provided seed.
196
- Defaults to False.
197
- progress (gr.Progress, optional): Gradio progress tracker. Defaults to gr.Progress(track_tqdm=True).
198
-
199
- Returns:
200
- tuple: A tuple containing:
201
- - video_path (str): Path to the generated video file (.mp4)
202
- - current_seed (int): The seed used for generation (useful when randomize_seed=True)
203
-
204
- Raises:
205
- gr.Error: If input_image is None (no image uploaded).
206
-
207
- Note:
208
- - Frame count is calculated as duration_seconds * FIXED_FPS (24)
209
- - Output dimensions are adjusted to be multiples of MOD_VALUE (32)
210
- - The function uses GPU acceleration via the @spaces.GPU decorator
211
- - Generation time varies based on steps and duration (see get_duration function)
212
  """
213
  if input_image is None:
214
  raise gr.Error("Please upload an input image.")
215
-
216
  num_frames = get_num_frames(duration_seconds)
217
  current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
218
  resized_image = resize_image(input_image)
@@ -237,36 +288,238 @@ def generate_video(
237
 
238
  return video_path, current_seed
239
 
240
- with gr.Blocks() as demo:
241
- gr.Markdown("# Fast 4 steps Wan 2.2 I2V (14B) with Lightning LoRA")
242
- gr.Markdown("run Wan 2.2 in just 4-8 steps, with [Lightning LoRA](https://huggingface.co/Kijai/WanVideo_comfy/tree/main/Wan22-Lightning), fp8 quantization & AoT compilation - compatible with 🧨 diffusers and ZeroGPU⚡️")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  with gr.Row():
244
- with gr.Column():
 
245
  input_image_component = gr.Image(type="pil", label="Input Image")
246
- prompt_input = gr.Textbox(label="Prompt", value=default_prompt_i2v, lines=3)
247
- duration_seconds_input = gr.Slider(minimum=MIN_DURATION, maximum=MAX_DURATION, step=0.1, value=3.5, label="Duration (seconds)", info=f"Clamped to model's {MIN_FRAMES_MODEL}-{MAX_FRAMES_MODEL} frames at {FIXED_FPS}fps.")
248
-
 
 
 
 
 
 
249
  with gr.Accordion("Advanced Settings", open=False):
250
- negative_prompt_input = gr.Textbox(label="Negative Prompt", value=default_negative_prompt, lines=3)
251
- seed_input = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=42, interactive=True)
252
- randomize_seed_checkbox = gr.Checkbox(label="Randomize seed", value=True, interactive=True)
253
- steps_slider = gr.Slider(minimum=1, maximum=30, step=1, value=6, label="Inference Steps")
254
- guidance_scale_input = gr.Slider(minimum=0.0, maximum=10.0, step=0.5, value=1, label="Guidance Scale - high noise stage")
255
- guidance_scale_2_input = gr.Slider(minimum=0.0, maximum=10.0, step=0.5, value=1, label="Guidance Scale 2 - low noise stage")
256
-
257
- generate_button = gr.Button("Generate Video", variant="primary")
258
- with gr.Column():
259
- video_output = gr.Video(label="Generated Video", autoplay=True, interactive=False)
260
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  ui_inputs = [
262
  input_image_component, prompt_input, steps_slider,
263
  negative_prompt_input, duration_seconds_input,
264
- guidance_scale_input, guidance_scale_2_input, seed_input, randomize_seed_checkbox
 
265
  ]
266
- generate_button.click(fn=generate_video, inputs=ui_inputs, outputs=[video_output, seed_input])
267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  gr.Examples(
269
- examples=[
270
  [
271
  "wan_i2v_input.JPG",
272
  "POV selfie video, white cat with sunglasses standing on surfboard, relaxed smile, tropical beach behind (clear water, green hills, blue sky with clouds). Surfboard tips, cat falls into ocean, camera plunges underwater with bubbles and sunlight beams. Brief underwater view of cat's face, then cat resurfaces, still filming selfie, playful summer vacation mood.",
@@ -283,12 +536,12 @@ with gr.Blocks() as demo:
283
  6,
284
  ],
285
  ],
286
- inputs=[input_image_component, prompt_input, steps_slider], outputs=[video_output, seed_input], fn=generate_video, cache_examples=True, cache_mode="lazy"
 
 
 
 
287
  )
288
 
289
-
290
- css = '''
291
- .gradio-container .contain{max-width: 1000px !important; margin: 0 auto !important}
292
- '''
293
  if __name__ == "__main__":
294
  demo.queue().launch(mcp_server=True, css=css)
 
4
  from diffusers.pipelines.wan.pipeline_wan_i2v import WanImageToVideoPipeline
5
  from diffusers.models.transformers.transformer_wan import WanTransformer3DModel
6
  from diffusers.utils.export_utils import export_to_video
7
+ from diffusers import Flux2KleinPipeline
8
  import gradio as gr
9
  import tempfile
10
  import numpy as np
11
  from PIL import Image
12
  import random
13
  import gc
14
+ import cv2
15
+ import os
16
+ import base64
17
+ from io import BytesIO
18
+ from typing import List
19
 
20
  from torchao.quantization import quantize_
21
  from torchao.quantization import Float8DynamicActivationFloat8WeightConfig
 
24
  import aoti
25
 
26
 
27
+ # ─── Model & dimension constants ───────────────────────────────────────────────
28
  MODEL_ID = "Wan-AI/Wan2.2-I2V-A14B-Diffusers"
29
 
30
  MAX_DIM = 832
 
38
  MIN_FRAMES_MODEL = 8
39
  MAX_FRAMES_MODEL = 80
40
 
41
+ MIN_DURATION = round(MIN_FRAMES_MODEL / FIXED_FPS, 1)
42
+ MAX_DURATION = round(MAX_FRAMES_MODEL / FIXED_FPS, 1)
43
 
44
+ device = "cuda"
45
 
46
+ # ─── Load Wan 2.2 I2V pipeline ────────────────────────────────────────────────
47
+ pipe = WanImageToVideoPipeline.from_pretrained(
48
+ MODEL_ID,
49
+ transformer=WanTransformer3DModel.from_pretrained(
50
+ 'cbensimon/Wan2.2-I2V-A14B-bf16-Diffusers',
51
  subfolder='transformer',
52
  torch_dtype=torch.bfloat16,
53
  device_map='cuda',
54
  ),
55
+ transformer_2=WanTransformer3DModel.from_pretrained(
56
+ 'cbensimon/Wan2.2-I2V-A14B-bf16-Diffusers',
57
  subfolder='transformer_2',
58
  torch_dtype=torch.bfloat16,
59
  device_map='cuda',
 
62
  ).to('cuda')
63
 
64
  pipe.load_lora_weights(
65
+ "Kijai/WanVideo_comfy",
66
+ weight_name="Lightx2v/lightx2v_I2V_14B_480p_cfg_step_distill_rank128_bf16.safetensors",
67
  adapter_name="lightx2v"
68
  )
69
  kwargs_lora = {}
70
  kwargs_lora["load_into_transformer_2"] = True
71
  pipe.load_lora_weights(
72
+ "Kijai/WanVideo_comfy",
73
+ weight_name="Lightx2v/lightx2v_I2V_14B_480p_cfg_step_distill_rank128_bf16.safetensors",
74
  adapter_name="lightx2v_2", **kwargs_lora
75
  )
76
  pipe.set_adapters(["lightx2v", "lightx2v_2"], adapter_weights=[1., 1.])
 
91
  repo_id='cbensimon/WanTransformer3DModel-sm120-cu130-raa',
92
  )
93
 
94
+ # ─── Load FLUX.2 Klein 4B upscaler pipeline ────────────────────────────────────
95
+ print("Loading FLUX.2 Klein 4B model...")
96
+ klein_pipe = Flux2KleinPipeline.from_pretrained(
97
+ "black-forest-labs/FLUX.2-klein-4B",
98
+ torch_dtype=torch.bfloat16,
99
+ ).to(device)
100
+ print("FLUX.2 Klein 4B loaded successfully.")
101
+
102
 
103
+ # ─── Prompts ───────────────────────────────────────────────────────────────────
104
  default_prompt_i2v = "make this image come alive, cinematic motion, smooth animation"
105
  default_negative_prompt = "色调艳丽, 过曝, 静态, 细节模糊不清, 字幕, 风格, 作品, 画作, 画面, 静止, 整体发灰, 最差质量, 低质量, JPEG压缩残留, 丑陋的, 残缺的, 多余的手指, 画得不好的手部, 画得不好的脸部, 畸形的, 毁容的, 形态畸形的肢体, 手指融合, 静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走"
106
 
107
+
108
+ # ─── Image & frame helpers ─────────────────────────────────────────────────────
109
  def resize_image(image: Image.Image) -> Image.Image:
110
  """
111
  Resizes an image to fit within the model's constraints, preserving aspect ratio as much as possible.
 
117
  return image.resize((SQUARE_DIM, SQUARE_DIM), Image.LANCZOS)
118
 
119
  aspect_ratio = width / height
120
+
121
+ MAX_ASPECT_RATIO = MAX_DIM / MIN_DIM
122
+ MIN_ASPECT_RATIO = MIN_DIM / MAX_DIM
123
 
124
  image_to_resize = image
125
+
126
  if aspect_ratio > MAX_ASPECT_RATIO:
127
  # Very wide image -> crop width to fit 832x480 aspect ratio
128
  target_w, target_h = MAX_DIM, MIN_DIM
 
148
 
149
  final_w = max(MIN_DIM, min(MAX_DIM, final_w))
150
  final_h = max(MIN_DIM, min(MAX_DIM, final_h))
151
+
152
  return image_to_resize.resize((final_w, final_h), Image.LANCZOS)
153
 
154
 
 
160
  ))
161
 
162
 
163
+ def extract_frames_from_video(video_path: str, duration_seconds: float) -> List[Image.Image]:
164
+ """
165
+ Extract one frame per whole second from the generated video.
166
+ E.g. 3.5 s → frames at ~0.5 s, ~1.5 s, ~2.5 s (midpoints of each second bucket).
167
+ Returns PIL images.
168
+ """
169
+ cap = cv2.VideoCapture(video_path)
170
+ fps = cap.get(cv2.CAP_PROP_FPS) or FIXED_FPS
171
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
172
+ total_secs = total_frames / fps
173
+
174
+ n_buckets = max(1, int(duration_seconds))
175
+ frames_out = []
176
+ for i in range(n_buckets):
177
+ t = i + 0.5
178
+ t = min(t, total_secs - 0.05)
179
+ frame_idx = int(t * fps)
180
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
181
+ ret, frame = cap.read()
182
+ if ret:
183
+ rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
184
+ frames_out.append(Image.fromarray(rgb))
185
+ cap.release()
186
+ return frames_out
187
+
188
+
189
+ def update_dimensions_for_klein(image: Image.Image):
190
+ """Calculate dimensions for Flux2Klein, snapped to multiples of 16."""
191
+ w, h = image.size
192
+ scale = min(1024 / w, 1024 / h)
193
+ nw = int(w * scale)
194
+ nh = int(h * scale)
195
+ return (nw // 16) * 16, (nh // 16) * 16
196
+
197
+
198
+ # ─── Upscale a single frame with FLUX.2 Klein 4B ──────────────────────────────
199
+ @spaces.GPU
200
+ def upscale_single_frame(frame: Image.Image) -> Image.Image:
201
+ """
202
+ Upscale a single PIL image using FLUX.2 Klein 4B img2img pipeline.
203
+ Uses: seed=0, randomize_seed=True, guidance_scale=1.0, steps=4
204
+ """
205
+ target_w, target_h = update_dimensions_for_klein(frame)
206
+
207
+ # Resize input to the target upscale dimensions
208
+ resized_input = frame.resize((target_w, target_h), Image.LANCZOS)
209
+
210
+ current_seed = random.randint(0, MAX_SEED)
211
+
212
+ result = klein_pipe(
213
+ prompt="high quality, ultra detailed, sharp focus, 8k resolution",
214
+ image=resized_input,
215
+ height=target_h,
216
+ width=target_w,
217
+ guidance_scale=1.0,
218
+ num_inference_steps=4,
219
+ strength=0.4,
220
+ generator=torch.Generator(device=device).manual_seed(current_seed),
221
+ ).images[0]
222
+
223
+ return result
224
+
225
+
226
+ # ─── Video generation ──────────────────────────────────────────────────────────
227
  def get_duration(
228
  input_image,
229
  prompt,
 
244
  step_duration = BASE_STEP_DURATION * factor ** 1.5
245
  return 10 + int(steps) * step_duration
246
 
247
+
248
  @spaces.GPU(duration=get_duration)
249
  def generate_video(
250
  input_image,
251
  prompt,
252
+ steps=4,
253
  negative_prompt=default_negative_prompt,
254
+ duration_seconds=MAX_DURATION,
255
+ guidance_scale=1,
256
+ guidance_scale_2=1,
257
+ seed=42,
258
+ randomize_seed=False,
259
  progress=gr.Progress(track_tqdm=True),
260
  ):
261
  """
262
  Generate a video from an input image using the Wan 2.2 14B I2V model with Lightning LoRA.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  """
264
  if input_image is None:
265
  raise gr.Error("Please upload an input image.")
266
+
267
  num_frames = get_num_frames(duration_seconds)
268
  current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
269
  resized_image = resize_image(input_image)
 
288
 
289
  return video_path, current_seed
290
 
291
+
292
+ # ─── Full pipeline: generate → extract frames → upscale → return card data ───
293
+ def generate_and_upscale(
294
+ input_image,
295
+ prompt,
296
+ steps,
297
+ negative_prompt,
298
+ duration_seconds,
299
+ guidance_scale,
300
+ guidance_scale_2,
301
+ seed,
302
+ randomize_seed,
303
+ progress=gr.Progress(track_tqdm=True),
304
+ ):
305
+ """
306
+ 1. Generate video
307
+ 2. Extract 1 frame per second
308
+ 3. Upscale each frame with FLUX.2 Klein 4B
309
+ 4. Return video, seed, and list of (original_frame, upscaled_frame) pairs
310
+ """
311
+ # Step 1: Generate video
312
+ video_path, current_seed = generate_video(
313
+ input_image, prompt, steps, negative_prompt,
314
+ duration_seconds, guidance_scale, guidance_scale_2,
315
+ seed, randomize_seed, progress
316
+ )
317
+
318
+ # Step 2: Extract frames (1 per second)
319
+ frames = extract_frames_from_video(video_path, duration_seconds)
320
+
321
+ if not frames:
322
+ return video_path, current_seed, []
323
+
324
+ # Step 3: Upscale each frame
325
+ upscaled_pairs = []
326
+ for i, frame in enumerate(frames):
327
+ progress(i / len(frames), desc=f"Upscaling frame {i+1}/{len(frames)}...")
328
+ upscaled = upscale_single_frame(frame)
329
+ upscaled_pairs.append((frame, upscaled))
330
+
331
+ return video_path, current_seed, upscaled_pairs
332
+
333
+
334
+ def pil_to_temp_path(img: Image.Image, prefix: str = "frame") -> str:
335
+ """Save a PIL image to a temp file and return the path."""
336
+ with tempfile.NamedTemporaryFile(suffix=".png", prefix=prefix + "_", delete=False) as f:
337
+ img.save(f, format="PNG")
338
+ return f.name
339
+
340
+
341
+ def run_pipeline(
342
+ input_image, prompt, steps, negative_prompt,
343
+ duration_seconds, guidance_scale, guidance_scale_2,
344
+ seed, randomize_seed,
345
+ progress=gr.Progress(track_tqdm=True),
346
+ ):
347
+ """
348
+ Orchestrate the full pipeline and return outputs for Gradio.
349
+ Returns:
350
+ - video_path
351
+ - seed
352
+ - list of ImageSlider pairs for up to MAX_CARDS cards
353
+ - list of upscaled image paths for download buttons
354
+ """
355
+ MAX_CARDS = 10 # maximum frames we support in UI
356
+
357
+ video_path, current_seed, upscaled_pairs = generate_and_upscale(
358
+ input_image, prompt, steps, negative_prompt,
359
+ duration_seconds, guidance_scale, guidance_scale_2,
360
+ seed, randomize_seed, progress,
361
+ )
362
+
363
+ # Build outputs for each card slot
364
+ slider_outputs = []
365
+ download_outputs = []
366
+ visibility_outputs = []
367
+
368
+ for i in range(MAX_CARDS):
369
+ if i < len(upscaled_pairs):
370
+ orig, upscaled = upscaled_pairs[i]
371
+ orig_path = pil_to_temp_path(orig, prefix=f"original_sec{i+1}")
372
+ upscaled_path = pil_to_temp_path(upscaled, prefix=f"upscaled_sec{i+1}")
373
+ slider_outputs.append((orig_path, upscaled_path))
374
+ download_outputs.append(upscaled_path)
375
+ visibility_outputs.append(gr.update(visible=True))
376
+ else:
377
+ slider_outputs.append(None)
378
+ download_outputs.append(None)
379
+ visibility_outputs.append(gr.update(visible=False))
380
+
381
+ # Flatten: video, seed, then interleaved (slider, download, visibility) per card
382
+ results = [video_path, current_seed]
383
+ for i in range(MAX_CARDS):
384
+ results.append(slider_outputs[i])
385
+ results.append(download_outputs[i])
386
+ results.append(visibility_outputs[i])
387
+
388
+ return results
389
+
390
+
391
+ # ─── Build Gradio UI ──────────────────────────────────────────────────────────
392
+
393
+ css = '''
394
+ .gradio-container .contain {
395
+ max-width: 1200px !important;
396
+ margin: 0 auto !important;
397
+ }
398
+ .upscale-card {
399
+ border: 1px solid var(--border-color-primary);
400
+ border-radius: 12px;
401
+ padding: 16px;
402
+ margin-bottom: 12px;
403
+ background: var(--background-fill-secondary);
404
+ }
405
+ .card-header {
406
+ font-size: 1.1em;
407
+ font-weight: 600;
408
+ margin-bottom: 8px;
409
+ color: var(--body-text-color);
410
+ }
411
+ '''
412
+
413
+ MAX_CARDS = 10
414
+
415
+ with gr.Blocks(css=css, title="wan2.2-i2v-fast") as demo:
416
+ gr.Markdown("# wan2.2-i2v-fast")
417
+ gr.Markdown(
418
+ "Run **Wan 2.2 I2V (14B)** in just 4-8 steps with "
419
+ "[Lightning LoRA](https://huggingface.co/Kijai/WanVideo_comfy/tree/main/Wan22-Lightning), "
420
+ "FP8 quantization & AoT compilation — compatible with 🧨 diffusers and ZeroGPU⚡️ \n"
421
+ "**+ FLUX.2 Klein 4B** frame upscaler — automatically extracts 1 frame/sec and upscales to ~1024px"
422
+ )
423
+
424
  with gr.Row():
425
+ # ─── LEFT COLUMN: Inputs ──────────────────────────────────
426
+ with gr.Column(scale=1):
427
  input_image_component = gr.Image(type="pil", label="Input Image")
428
+ prompt_input = gr.Textbox(
429
+ label="Prompt", value=default_prompt_i2v, lines=3
430
+ )
431
+ duration_seconds_input = gr.Slider(
432
+ minimum=MIN_DURATION, maximum=MAX_DURATION, step=0.1, value=3.5,
433
+ label="Duration (seconds)",
434
+ info=f"Clamped to model's {MIN_FRAMES_MODEL}-{MAX_FRAMES_MODEL} frames at {FIXED_FPS}fps."
435
+ )
436
+
437
  with gr.Accordion("Advanced Settings", open=False):
438
+ negative_prompt_input = gr.Textbox(
439
+ label="Negative Prompt", value=default_negative_prompt, lines=3
440
+ )
441
+ seed_input = gr.Slider(
442
+ label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=42,
443
+ interactive=True
444
+ )
445
+ randomize_seed_checkbox = gr.Checkbox(
446
+ label="Randomize seed", value=True, interactive=True
447
+ )
448
+ steps_slider = gr.Slider(
449
+ minimum=1, maximum=30, step=1, value=4,
450
+ label="Inference Steps"
451
+ )
452
+ guidance_scale_input = gr.Slider(
453
+ minimum=0.0, maximum=10.0, step=0.5, value=1,
454
+ label="Guidance Scale - high noise stage"
455
+ )
456
+ guidance_scale_2_input = gr.Slider(
457
+ minimum=0.0, maximum=10.0, step=0.5, value=1,
458
+ label="Guidance Scale 2 - low noise stage"
459
+ )
460
+
461
+ generate_button = gr.Button("🎬 Generate Video & Upscale Frames", variant="primary")
462
+
463
+ # ─── RIGHT COLUMN: Video + Upscale Cards ──────────────────
464
+ with gr.Column(scale=1):
465
+ video_output = gr.Video(
466
+ label="Generated Video", autoplay=True, interactive=False
467
+ )
468
+
469
+ gr.Markdown("### 🔍 Upscaled Frame Comparisons")
470
+ gr.Markdown(
471
+ "*Drag the slider on each card to compare the **original frame** (left) "
472
+ "vs the **FLUX.2 Klein 4B upscaled** version (right). "
473
+ "Click download to save the upscaled image.*"
474
+ )
475
+
476
+ # Create card slots dynamically
477
+ card_groups = [] # list of (group, slider, download_btn)
478
+ slider_components = []
479
+ download_components = []
480
+ group_components = []
481
+
482
+ for i in range(MAX_CARDS):
483
+ with gr.Group(visible=False, elem_classes="upscale-card") as card_group:
484
+ gr.Markdown(f"**Frame {i+1}** — Second {i+1}", elem_classes="card-header")
485
+ img_slider = gr.ImageSlider(
486
+ label=f"Original ↔ Upscaled (Second {i+1})",
487
+ type="filepath",
488
+ interactive=True,
489
+ )
490
+ download_btn = gr.File(
491
+ label=f"⬇️ Download Upscaled Frame {i+1}",
492
+ interactive=False,
493
+ )
494
+
495
+ slider_components.append(img_slider)
496
+ download_components.append(download_btn)
497
+ group_components.append(card_group)
498
+
499
+ # ─── Wire up inputs/outputs ────────────────────────────────────────────────
500
  ui_inputs = [
501
  input_image_component, prompt_input, steps_slider,
502
  negative_prompt_input, duration_seconds_input,
503
+ guidance_scale_input, guidance_scale_2_input,
504
+ seed_input, randomize_seed_checkbox,
505
  ]
 
506
 
507
+ # Build output list: video, seed, then (slider, download, group_visibility) × MAX_CARDS
508
+ ui_outputs = [video_output, seed_input]
509
+ for i in range(MAX_CARDS):
510
+ ui_outputs.append(slider_components[i])
511
+ ui_outputs.append(download_components[i])
512
+ ui_outputs.append(group_components[i])
513
+
514
+ generate_button.click(
515
+ fn=run_pipeline,
516
+ inputs=ui_inputs,
517
+ outputs=ui_outputs,
518
+ )
519
+
520
+ # ─── Examples ──────────────────────────────────────────────────────────────
521
  gr.Examples(
522
+ examples=[
523
  [
524
  "wan_i2v_input.JPG",
525
  "POV selfie video, white cat with sunglasses standing on surfboard, relaxed smile, tropical beach behind (clear water, green hills, blue sky with clouds). Surfboard tips, cat falls into ocean, camera plunges underwater with bubbles and sunlight beams. Brief underwater view of cat's face, then cat resurfaces, still filming selfie, playful summer vacation mood.",
 
536
  6,
537
  ],
538
  ],
539
+ inputs=[input_image_component, prompt_input, steps_slider],
540
+ outputs=[video_output, seed_input],
541
+ fn=generate_video,
542
+ cache_examples=True,
543
+ cache_mode="lazy",
544
  )
545
 
 
 
 
 
546
  if __name__ == "__main__":
547
  demo.queue().launch(mcp_server=True, css=css)