prithivMLmods commited on
Commit
5e9baec
·
verified ·
1 Parent(s): 6e83bcd

update app

Browse files
Files changed (1) hide show
  1. wan_app.py +174 -370
wan_app.py CHANGED
@@ -1,28 +1,29 @@
1
- import spaces
 
 
 
 
 
 
 
 
 
 
2
  import torch
 
 
 
 
 
3
  from diffusers.pipelines.wan.pipeline_wan_i2v import WanImageToVideoPipeline
4
  from diffusers.models.transformers.transformer_wan import WanTransformer3DModel
5
  from diffusers.utils.export_utils import export_to_video
6
  from diffusers import Flux2KleinPipeline
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
- import cv2
14
- import os
15
- import base64
16
- from io import BytesIO
17
- from typing import List
18
 
19
  from torchao.quantization import quantize_
20
  from torchao.quantization import Float8DynamicActivationFloat8WeightConfig
21
  from torchao.quantization import Int8WeightOnlyConfig
22
 
23
- import aoti
24
- from typing import Iterable
25
-
26
  MODEL_ID = "Wan-AI/Wan2.2-I2V-A14B-Diffusers"
27
 
28
  MAX_DIM = 832
@@ -41,6 +42,8 @@ MAX_DURATION = round(MAX_FRAMES_MODEL / FIXED_FPS, 1)
41
 
42
  device = "cuda"
43
 
 
 
44
  pipe = WanImageToVideoPipeline.from_pretrained(
45
  MODEL_ID,
46
  transformer=WanTransformer3DModel.from_pretrained(
@@ -79,14 +82,18 @@ quantize_(pipe.text_encoder, Int8WeightOnlyConfig())
79
  quantize_(pipe.transformer, Float8DynamicActivationFloat8WeightConfig())
80
  quantize_(pipe.transformer_2, Float8DynamicActivationFloat8WeightConfig())
81
 
82
- spaces.aoti_load(
83
- module=pipe.transformer,
84
- repo_id='cbensimon/WanTransformer3DModel-sm120-cu130-raa',
85
- )
86
- spaces.aoti_load(
87
- module=pipe.transformer_2,
88
- repo_id='cbensimon/WanTransformer3DModel-sm120-cu130-raa',
89
- )
 
 
 
 
90
 
91
  print("Loading FLUX.2 Klein 4B model...")
92
  klein_pipe = Flux2KleinPipeline.from_pretrained(
@@ -98,52 +105,87 @@ print("FLUX.2 Klein 4B loaded successfully.")
98
  default_prompt_i2v = "make this image come alive, cinematic motion, smooth animation"
99
  default_negative_prompt = "色调艳丽, 过曝, 静态, 细节模糊不清, 字幕, 风格, 作品, 画作, 画面, 静止, 整体发灰, 最差质量, 低质量, JPEG压缩残留, 丑陋的, 残缺的, 多余的手指, 画得不好的手部, 画得不好的脸部, 畸形的, 毁容的, 形态畸形的肢体, 手指融合, 静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走"
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  def resize_image(image: Image.Image) -> Image.Image:
102
- """
103
- Resizes an image to fit within the model's constraints, preserving aspect ratio as much as possible.
104
- """
105
  width, height = image.size
106
-
107
- # Handle square case
108
  if width == height:
109
  return image.resize((SQUARE_DIM, SQUARE_DIM), Image.LANCZOS)
110
 
111
  aspect_ratio = width / height
112
-
113
  MAX_ASPECT_RATIO = MAX_DIM / MIN_DIM
114
  MIN_ASPECT_RATIO = MIN_DIM / MAX_DIM
115
 
116
  image_to_resize = image
117
 
118
  if aspect_ratio > MAX_ASPECT_RATIO:
119
- # Very wide image -> crop width to fit 832x480 aspect ratio
120
  target_w, target_h = MAX_DIM, MIN_DIM
121
  crop_width = int(round(height * MAX_ASPECT_RATIO))
122
  left = (width - crop_width) // 2
123
  image_to_resize = image.crop((left, 0, left + crop_width, height))
124
  elif aspect_ratio < MIN_ASPECT_RATIO:
125
- # Very tall image -> crop height to fit 480x832 aspect ratio
126
  target_w, target_h = MIN_DIM, MAX_DIM
127
  crop_height = int(round(width / MIN_ASPECT_RATIO))
128
  top = (height - crop_height) // 2
129
  image_to_resize = image.crop((0, top, width, top + crop_height))
130
  else:
131
- if width > height: # Landscape
132
  target_w = MAX_DIM
133
  target_h = int(round(target_w / aspect_ratio))
134
- else: # Portrait
135
  target_h = MAX_DIM
136
  target_w = int(round(target_h * aspect_ratio))
137
 
138
  final_w = round(target_w / MULTIPLE_OF) * MULTIPLE_OF
139
  final_h = round(target_h / MULTIPLE_OF) * MULTIPLE_OF
140
-
141
  final_w = max(MIN_DIM, min(MAX_DIM, final_w))
142
  final_h = max(MIN_DIM, min(MAX_DIM, final_h))
143
 
144
  return image_to_resize.resize((final_w, final_h), Image.LANCZOS)
145
 
146
-
147
  def get_num_frames(duration_seconds: float):
148
  return 1 + int(np.clip(
149
  int(round(duration_seconds * FIXED_FPS)),
@@ -151,13 +193,7 @@ def get_num_frames(duration_seconds: float):
151
  MAX_FRAMES_MODEL,
152
  ))
153
 
154
-
155
  def extract_frames_from_video(video_path: str, duration_seconds: float) -> List[Image.Image]:
156
- """
157
- Extract one frame per whole second from the generated video.
158
- E.g. 3.5 s → frames at ~0.5 s, ~1.5 s, ~2.5 s (midpoints of each second bucket).
159
- Returns PIL images.
160
- """
161
  cap = cv2.VideoCapture(video_path)
162
  fps = cap.get(cv2.CAP_PROP_FPS) or FIXED_FPS
163
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
@@ -177,90 +213,58 @@ def extract_frames_from_video(video_path: str, duration_seconds: float) -> List[
177
  cap.release()
178
  return frames_out
179
 
180
-
181
  def update_dimensions_for_klein(image: Image.Image):
182
- """Calculate dimensions for Flux2Klein, snapped to multiples of 16."""
183
  w, h = image.size
184
  scale = min(1024 / w, 1024 / h)
185
  nw = int(w * scale)
186
  nh = int(h * scale)
187
  return (nw // 16) * 16, (nh // 16) * 16
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
- def upscale_frames_batch(frames: List[Image.Image], progress=None) -> List[Image.Image]:
191
- """
192
- Upscale ALL extracted frames in a single pass (no separate GPU decorator).
193
- Called from within the main @spaces.GPU function so everything
194
- shares one GPU session.
195
- """
196
- upscaled = []
197
- for i, frame in enumerate(frames):
198
- if progress:
199
- progress(0.5 + 0.5 * (i / len(frames)),
200
- desc=f"Upscaling frame {i+1}/{len(frames)}...")
201
- target_w, target_h = update_dimensions_for_klein(frame)
202
- current_seed = random.randint(0, MAX_SEED)
203
- result = klein_pipe(
204
- prompt="high quality, ultra detailed, sharp focus, 8k resolution",
205
- image=frame,
206
- height=target_h,
207
- width=target_w,
208
- guidance_scale=1.0,
209
- num_inference_steps=4,
210
- generator=torch.Generator(device=device).manual_seed(current_seed),
211
- ).images[0]
212
- upscaled.append(result)
213
- return upscaled
214
-
215
-
216
- def get_duration(
217
- input_image,
218
- prompt,
219
- steps,
220
- negative_prompt,
221
- duration_seconds,
222
- guidance_scale,
223
- guidance_scale_2,
224
- seed,
225
- randomize_seed,
226
- progress,
227
- ):
228
- BASE_FRAMES_HEIGHT_WIDTH = 81 * 832 * 624
229
- BASE_STEP_DURATION = 15
230
- width, height = resize_image(input_image).size
231
- num_frames = get_num_frames(duration_seconds)
232
- factor = num_frames * width * height / BASE_FRAMES_HEIGHT_WIDTH
233
- step_duration = BASE_STEP_DURATION * factor ** 1.5
234
- n_upscale_frames = max(1, int(duration_seconds))
235
- upscale_budget = n_upscale_frames * 15
236
- return 10 + int(steps) * step_duration + upscale_budget
237
-
238
-
239
- @spaces.GPU(duration=get_duration, size="xlarge")
240
- def generate_and_upscale_gpu(
241
- input_image,
242
- prompt,
243
- steps=4,
244
- negative_prompt=default_negative_prompt,
245
- duration_seconds=MAX_DURATION,
246
- guidance_scale=1,
247
- guidance_scale_2=1,
248
- seed=42,
249
- randomize_seed=False,
250
- progress=gr.Progress(track_tqdm=True),
251
- ):
252
- """
253
- Single GPU session: generate video → extract frames → upscale ALL frames.
254
- Everything runs under one @spaces.GPU allocation.
255
- """
256
- if input_image is None:
257
- raise gr.Error("Please upload an input image.")
258
-
259
- # ── Step 1: Generate video ──────────────────────────────────
260
- progress(0, desc="Generating video...")
261
  num_frames = get_num_frames(duration_seconds)
262
  current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
263
- resized_image = resize_image(input_image)
264
 
265
  output_frames_list = pipe(
266
  image=resized_image,
@@ -279,262 +283,62 @@ def generate_and_upscale_gpu(
279
  video_path = tmpfile.name
280
  export_to_video(output_frames_list, video_path, fps=FIXED_FPS)
281
 
282
- # ── Step 2: Extract frames (1 per second) ───────────────────
283
- progress(0.5, desc="Extracting frames...")
284
  frames = extract_frames_from_video(video_path, duration_seconds)
 
 
285
 
286
- if not frames:
287
- return video_path, current_seed, []
288
-
289
- # ── Step 3: Upscale ALL frames in this same GPU session ─────
290
- upscaled_frames = upscale_frames_batch(frames, progress)
291
-
292
- upscaled_pairs = list(zip(frames, upscaled_frames))
293
- return video_path, current_seed, upscaled_pairs
294
-
295
-
296
- @spaces.GPU(duration=get_duration, size="xlarge")
297
- def generate_video(
298
- input_image,
299
- prompt,
300
- steps=4,
301
- negative_prompt=default_negative_prompt,
302
- duration_seconds=MAX_DURATION,
303
- guidance_scale=1,
304
- guidance_scale_2=1,
305
- seed=42,
306
- randomize_seed=False,
307
- progress=gr.Progress(track_tqdm=True),
308
- ):
309
- """
310
- Generate video only (used by Examples which don't need upscaling).
311
- """
312
- if input_image is None:
313
- raise gr.Error("Please upload an input image.")
314
-
315
- num_frames = get_num_frames(duration_seconds)
316
- current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
317
- resized_image = resize_image(input_image)
318
-
319
- output_frames_list = pipe(
320
- image=resized_image,
321
- prompt=prompt,
322
- negative_prompt=negative_prompt,
323
- height=resized_image.height,
324
- width=resized_image.width,
325
- num_frames=num_frames,
326
- guidance_scale=float(guidance_scale),
327
- guidance_scale_2=float(guidance_scale_2),
328
- num_inference_steps=int(steps),
329
- generator=torch.Generator(device="cuda").manual_seed(current_seed),
330
- ).frames[0]
331
-
332
- with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmpfile:
333
- video_path = tmpfile.name
334
- export_to_video(output_frames_list, video_path, fps=FIXED_FPS)
335
-
336
- return video_path, current_seed
337
-
338
-
339
- def pil_to_temp_path(img: Image.Image, prefix: str = "frame") -> str:
340
- """Save a PIL image to a temp file and return the path."""
341
- with tempfile.NamedTemporaryFile(suffix=".png", prefix=prefix + "_", delete=False) as f:
342
- img.save(f, format="PNG")
343
- return f.name
344
-
345
-
346
- def run_pipeline(
347
- input_image, prompt, steps, negative_prompt,
348
- duration_seconds, guidance_scale, guidance_scale_2,
349
- seed, randomize_seed,
350
- progress=gr.Progress(track_tqdm=True),
351
- ):
352
- """
353
- Orchestrate the full pipeline and return outputs for Gradio.
354
- Calls generate_and_upscale_gpu which runs everything in ONE GPU session.
355
- """
356
- MAX_CARDS = 10
357
-
358
- video_path, current_seed, upscaled_pairs = generate_and_upscale_gpu(
359
- input_image, prompt, steps, negative_prompt,
360
- duration_seconds, guidance_scale, guidance_scale_2,
361
- seed, randomize_seed, progress,
362
- )
363
-
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
- results = [video_path, current_seed]
382
- for i in range(MAX_CARDS):
383
- results.append(slider_outputs[i])
384
- results.append(download_outputs[i])
385
- results.append(visibility_outputs[i])
386
-
387
- return results
388
-
389
- css = '''
390
- .upscale-card {
391
- border: 1px solid var(--border-color-primary);
392
- border-radius: 12px;
393
- padding: 16px;
394
- margin-bottom: 12px;
395
- background: var(--background-fill-secondary);
396
- }
397
- .card-header {
398
- font-size: 1.1em;
399
- font-weight: 600;
400
- margin-bottom: 8px;
401
- color: var(--body-text-color);
402
- }
403
- '''
404
-
405
- MAX_CARDS = 10
406
-
407
- with gr.Blocks() as demo:
408
- gr.Markdown("# **Wan2.2-Fast**")
409
- gr.Markdown(
410
- "Run **Wan 2.2 I2V (14B)** in just 4-8 steps with "
411
- "[Lightning LoRA](https://huggingface.co/Kijai/WanVideo_comfy/tree/main/Wan22-Lightning), "
412
- "FP8 quantization & AoT compilation — compatible with 🧨 diffusers and ZeroGPU⚡️ \n"
413
- "**+ FLUX.2 Klein 4B** frame upscaler — automatically extracts 1 frame/sec and upscales to ~1024px."
414
- " [GitHub ↗](https://github.com/PRITHIVSAKTHIUR/wan2.2-i2v-fast)"
415
- )
416
-
417
- with gr.Row():
418
- with gr.Column(scale=1):
419
- input_image_component = gr.Image(type="pil", label="Input Image", height=300)
420
- prompt_input = gr.Textbox(
421
- label="Prompt", value=default_prompt_i2v, lines=3
422
- )
423
- duration_seconds_input = gr.Slider(
424
- minimum=MIN_DURATION, maximum=MAX_DURATION, step=0.1, value=3.5,
425
- label="Duration (seconds)",
426
- info=f"Clamped to model's {MIN_FRAMES_MODEL}-{MAX_FRAMES_MODEL} frames at {FIXED_FPS}fps."
427
- )
428
-
429
- with gr.Accordion("Advanced Settings", open=False):
430
- negative_prompt_input = gr.Textbox(
431
- label="Negative Prompt", value=default_negative_prompt, lines=3
432
- )
433
- seed_input = gr.Slider(
434
- label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=42,
435
- interactive=True
436
- )
437
- randomize_seed_checkbox = gr.Checkbox(
438
- label="Randomize seed", value=True, interactive=True
439
- )
440
- steps_slider = gr.Slider(
441
- minimum=1, maximum=30, step=1, value=4,
442
- label="Inference Steps"
443
- )
444
- guidance_scale_input = gr.Slider(
445
- minimum=0.0, maximum=10.0, step=0.5, value=1,
446
- label="Guidance Scale - high noise stage"
447
- )
448
- guidance_scale_2_input = gr.Slider(
449
- minimum=0.0, maximum=10.0, step=0.5, value=1,
450
- label="Guidance Scale 2 - low noise stage"
451
- )
452
-
453
- generate_button = gr.Button("Generate Video & Upscale Frames", variant="primary")
454
-
455
- with gr.Column(scale=1):
456
- video_output = gr.Video(
457
- label="Generated Video", autoplay=True, interactive=False
458
- )
459
-
460
- with gr.Accordion("get_upscaled_samples()", open=False):
461
-
462
- gr.Markdown("### Upscaled Frame Comparisons")
463
- gr.Markdown(
464
- "*Drag the slider on each card to compare the **original frame** (left) "
465
- "vs the **FLUX.2 Klein 4B upscaled** version (right). "
466
- "Click download to save the upscaled image.*"
467
- )
468
-
469
- card_groups = [] # list of (group, slider, download_btn)
470
- slider_components = []
471
- download_components = []
472
- group_components = []
473
-
474
- for i in range(MAX_CARDS):
475
- with gr.Group(visible=False, elem_classes="upscale-card") as card_group:
476
- gr.Markdown(f"**Frame {i+1}** — Second {i+1}", elem_classes="card-header")
477
- img_slider = gr.ImageSlider(
478
- label=f"Original ↔ Upscaled (Second {i+1})",
479
- type="filepath",
480
- interactive=True,
481
- )
482
- download_btn = gr.File(
483
- label=f"Download Upscaled Frame {i+1}",
484
- interactive=False,
485
- )
486
-
487
- slider_components.append(img_slider)
488
- download_components.append(download_btn)
489
- group_components.append(card_group)
490
-
491
- gr.Examples(
492
- examples=[
493
- [
494
- "example-file/6b2842cf438d086f556eef05cc29d2d1.jpg",
495
- "make this image come alive, cinematic motion, smooth animation.",
496
- 4,
497
- ],
498
- [
499
- "example-file/wan_i2v_input.JPG",
500
- "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.",
501
- 4,
502
- ],
503
- [
504
- "example-file/wan22_input_2.jpg",
505
- "A sleek lunar vehicle glides into view from left to right, kicking up moon dust as astronauts in white spacesuits hop aboard with characteristic lunar bouncing movements. In the distant background, a VTOL craft descends straight down and lands silently on the surface. Throughout the entire scene, ethereal aurora borealis ribbons dance across the star-filled sky, casting shimmering curtains of green, blue, and purple light that bathe the lunar landscape in an otherworldly, magical glow.",
506
- 4,
507
- ],
508
- [
509
- "example-file/kill_bill.jpeg",
510
- "Uma Thurman's character, Beatrix Kiddo, holds her razor-sharp katana blade steady in the cinematic lighting. Suddenly, the polished steel begins to soften and distort, like heated metal starting to lose its structural integrity. The blade's perfect edge slowly warps and droops, molten steel beginning to flow downward in silvery rivulets while maintaining its metallic sheen. The transformation starts subtly at first - a slight bend in the blade - then accelerates as the metal becomes increasingly fluid. The camera holds steady on her face as her piercing eyes gradually narrow, not with lethal focus, but with confusion and growing alarm as she watches her weapon dissolve before her eyes. Her breathing quickens slightly as she witnesses this impossible transformation. The melting intensifies, the katana's perfect form becoming increasingly abstract, dripping like liquid mercury from her grip. Molten droplets fall to the ground with soft metallic impacts. Her expression shifts from calm readiness to bewilderment and concern as her legendary instrument of vengeance literally liquefies in her hands, leaving her defenseless and disoriented.",
511
- 6,
512
- ],
513
- ],
514
- inputs=[input_image_component, prompt_input, steps_slider],
515
- outputs=[video_output, seed_input],
516
- fn=generate_video,
517
- cache_examples=False,
518
- )
519
-
520
- ui_inputs = [
521
- input_image_component, prompt_input, steps_slider,
522
- negative_prompt_input, duration_seconds_input,
523
- guidance_scale_input, guidance_scale_2_input,
524
- seed_input, randomize_seed_checkbox,
525
- ]
526
-
527
- ui_outputs = [video_output, seed_input]
528
- for i in range(MAX_CARDS):
529
- ui_outputs.append(slider_components[i])
530
- ui_outputs.append(download_components[i])
531
- ui_outputs.append(group_components[i])
532
-
533
- generate_button.click(
534
- fn=run_pipeline,
535
- inputs=ui_inputs,
536
- outputs=ui_outputs,
537
- )
538
 
539
  if __name__ == "__main__":
540
- demo.queue().launch(mcp_server=True, css=css, show_error=True)
 
1
+ import os
2
+ import gc
3
+ import tempfile
4
+ import base64
5
+ import json
6
+ from io import BytesIO
7
+ import random
8
+ import numpy as np
9
+ import cv2
10
+ from PIL import Image
11
+ from typing import List
12
  import torch
13
+ import gradio as gr
14
+ from gradio import Server
15
+ from fastapi.responses import HTMLResponse
16
+ import spaces
17
+
18
  from diffusers.pipelines.wan.pipeline_wan_i2v import WanImageToVideoPipeline
19
  from diffusers.models.transformers.transformer_wan import WanTransformer3DModel
20
  from diffusers.utils.export_utils import export_to_video
21
  from diffusers import Flux2KleinPipeline
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  from torchao.quantization import quantize_
24
  from torchao.quantization import Float8DynamicActivationFloat8WeightConfig
25
  from torchao.quantization import Int8WeightOnlyConfig
26
 
 
 
 
27
  MODEL_ID = "Wan-AI/Wan2.2-I2V-A14B-Diffusers"
28
 
29
  MAX_DIM = 832
 
42
 
43
  device = "cuda"
44
 
45
+ # --- Model Loading ---
46
+ print("Loading Wan 2.2 I2V Pipeline...")
47
  pipe = WanImageToVideoPipeline.from_pretrained(
48
  MODEL_ID,
49
  transformer=WanTransformer3DModel.from_pretrained(
 
82
  quantize_(pipe.transformer, Float8DynamicActivationFloat8WeightConfig())
83
  quantize_(pipe.transformer_2, Float8DynamicActivationFloat8WeightConfig())
84
 
85
+ try:
86
+ import aoti
87
+ spaces.aoti_load(
88
+ module=pipe.transformer,
89
+ repo_id='cbensimon/WanTransformer3DModel-sm120-cu130-raa',
90
+ )
91
+ spaces.aoti_load(
92
+ module=pipe.transformer_2,
93
+ repo_id='cbensimon/WanTransformer3DModel-sm120-cu130-raa',
94
+ )
95
+ except Exception as e:
96
+ print(f"AoT compilation loading skipped/failed: {e}")
97
 
98
  print("Loading FLUX.2 Klein 4B model...")
99
  klein_pipe = Flux2KleinPipeline.from_pretrained(
 
105
  default_prompt_i2v = "make this image come alive, cinematic motion, smooth animation"
106
  default_negative_prompt = "色调艳丽, 过曝, 静态, 细节模糊不清, 字幕, 风格, 作品, 画作, 画面, 静止, 整体发灰, 最差质量, 低质量, JPEG压缩残留, 丑陋的, 残缺的, 多余的手指, 画得不好的手部, 画得不好的脸部, 畸形的, 毁容的, 形态畸形的肢体, 手指融合, 静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走"
107
 
108
+ # --- Config ---
109
+ EXAMPLES_CONFIG = [
110
+ {"image": "example-file/6b2842cf438d086f556eef05cc29d2d1.jpg", "prompt": "make this image come alive, cinematic motion, smooth animation.", "steps": 4},
111
+ {"image": "example-file/wan_i2v_input.JPG", "prompt": "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.", "steps": 4},
112
+ {"image": "example-file/wan22_input_2.jpg", "prompt": "A sleek lunar vehicle glides into view from left to right, kicking up moon dust as astronauts in white spacesuits hop aboard with characteristic lunar bouncing movements. In the distant background, a VTOL craft descends straight down and lands silently on the surface. Throughout the entire scene, ethereal aurora borealis ribbons dance across the star-filled sky, casting shimmering curtains of green, blue, and purple light that bathe the lunar landscape in an otherworldly, magical glow.", "steps": 4},
113
+ {"image": "example-file/kill_bill.jpeg", "prompt": "Uma Thurman's character, Beatrix Kiddo, holds her razor-sharp katana blade steady in the cinematic lighting. Suddenly, the polished steel begins to soften and distort, like heated metal starting to lose its structural integrity. The blade's perfect edge slowly warps and droops, molten steel beginning to flow downward in silvery rivulets while maintaining its metallic sheen. The transformation starts subtly at first - a slight bend in the blade - then accelerates as the metal becomes increasingly fluid. The camera holds steady on her face as her piercing eyes gradually narrow, not with lethal focus, but with confusion and growing alarm as she watches her weapon dissolve before her eyes. Her breathing quickens slightly as she witnesses this impossible transformation. The melting intensifies, the katana's perfect form becoming increasingly abstract, dripping like liquid mercury from her grip. Molten droplets fall to the ground with soft metallic impacts. Her expression shifts from calm readiness to bewilderment and concern as her legendary instrument of vengeance literally liquefies in her hands, leaving her defenseless and disoriented.", "steps": 6},
114
+ ]
115
+
116
+ def make_thumb_b64(path, max_dim=220):
117
+ if not os.path.exists(path):
118
+ return ""
119
+ try:
120
+ img = Image.open(path).convert("RGB")
121
+ img.thumbnail((max_dim, max_dim), Image.LANCZOS)
122
+ buf = BytesIO()
123
+ img.save(buf, format="JPEG", quality=65)
124
+ return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
125
+ except Exception as e:
126
+ return ""
127
+
128
+ def encode_full_image(path):
129
+ if not os.path.exists(path):
130
+ return ""
131
+ try:
132
+ with open(path, "rb") as f:
133
+ data = f.read()
134
+ ext = path.rsplit(".", 1)[-1].lower()
135
+ mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp"}.get(ext, "image/jpeg")
136
+ return f"data:{mime};base64,{base64.b64encode(data).decode()}"
137
+ except Exception as e:
138
+ return ""
139
+
140
+ def build_client_config():
141
+ examples = []
142
+ for i, ex in enumerate(EXAMPLES_CONFIG):
143
+ examples.append({
144
+ "idx": i,
145
+ "thumb": make_thumb_b64(ex["image"]),
146
+ "prompt": ex["prompt"],
147
+ "steps": ex["steps"],
148
+ })
149
+ return {"examples": examples}
150
+
151
+ CLIENT_CONFIG = build_client_config()
152
+
153
  def resize_image(image: Image.Image) -> Image.Image:
 
 
 
154
  width, height = image.size
 
 
155
  if width == height:
156
  return image.resize((SQUARE_DIM, SQUARE_DIM), Image.LANCZOS)
157
 
158
  aspect_ratio = width / height
 
159
  MAX_ASPECT_RATIO = MAX_DIM / MIN_DIM
160
  MIN_ASPECT_RATIO = MIN_DIM / MAX_DIM
161
 
162
  image_to_resize = image
163
 
164
  if aspect_ratio > MAX_ASPECT_RATIO:
 
165
  target_w, target_h = MAX_DIM, MIN_DIM
166
  crop_width = int(round(height * MAX_ASPECT_RATIO))
167
  left = (width - crop_width) // 2
168
  image_to_resize = image.crop((left, 0, left + crop_width, height))
169
  elif aspect_ratio < MIN_ASPECT_RATIO:
 
170
  target_w, target_h = MIN_DIM, MAX_DIM
171
  crop_height = int(round(width / MIN_ASPECT_RATIO))
172
  top = (height - crop_height) // 2
173
  image_to_resize = image.crop((0, top, width, top + crop_height))
174
  else:
175
+ if width > height:
176
  target_w = MAX_DIM
177
  target_h = int(round(target_w / aspect_ratio))
178
+ else:
179
  target_h = MAX_DIM
180
  target_w = int(round(target_h * aspect_ratio))
181
 
182
  final_w = round(target_w / MULTIPLE_OF) * MULTIPLE_OF
183
  final_h = round(target_h / MULTIPLE_OF) * MULTIPLE_OF
 
184
  final_w = max(MIN_DIM, min(MAX_DIM, final_w))
185
  final_h = max(MIN_DIM, min(MAX_DIM, final_h))
186
 
187
  return image_to_resize.resize((final_w, final_h), Image.LANCZOS)
188
 
 
189
  def get_num_frames(duration_seconds: float):
190
  return 1 + int(np.clip(
191
  int(round(duration_seconds * FIXED_FPS)),
 
193
  MAX_FRAMES_MODEL,
194
  ))
195
 
 
196
  def extract_frames_from_video(video_path: str, duration_seconds: float) -> List[Image.Image]:
 
 
 
 
 
197
  cap = cv2.VideoCapture(video_path)
198
  fps = cap.get(cv2.CAP_PROP_FPS) or FIXED_FPS
199
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
 
213
  cap.release()
214
  return frames_out
215
 
 
216
  def update_dimensions_for_klein(image: Image.Image):
 
217
  w, h = image.size
218
  scale = min(1024 / w, 1024 / h)
219
  nw = int(w * scale)
220
  nh = int(h * scale)
221
  return (nw // 16) * 16, (nh // 16) * 16
222
 
223
+ def pil_to_b64_png(image: Image.Image) -> str:
224
+ buf = BytesIO()
225
+ image.save(buf, format="PNG")
226
+ return f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"
227
+
228
+ def video_to_b64(video_path: str) -> str:
229
+ with open(video_path, "rb") as f:
230
+ data = f.read()
231
+ return f"data:video/mp4;base64,{base64.b64encode(data).decode()}"
232
+
233
+ # ── Gradio Server (Server mode) ────────────
234
+ app = Server(title="Wan2.2-Fast")
235
+
236
+ @app.mcp.tool(name="generate_video")
237
+ @app.api(name="generate_video")
238
+ @spaces.GPU(duration=300, size="xlarge")
239
+ def infer(
240
+ image_b64: str,
241
+ prompt: str,
242
+ steps: int,
243
+ negative_prompt: str,
244
+ duration_seconds: float,
245
+ guidance_scale: float,
246
+ guidance_scale_2: float,
247
+ seed: int,
248
+ randomize_seed: bool,
249
+ ) -> dict:
250
+ """Generates a video and upscales 1 frame per second using FLUX.2 Klein."""
251
+ gc.collect()
252
+ torch.cuda.empty_cache()
253
+
254
+ if not image_b64:
255
+ raise gr.Error("Please upload an image.")
256
+ if not prompt or prompt.strip() == "":
257
+ raise gr.Error("Please enter a prompt.")
258
+
259
+ try:
260
+ header, data = image_b64.split(",", 1)
261
+ pil_image = Image.open(BytesIO(base64.b64decode(data))).convert("RGB")
262
+ except Exception as e:
263
+ raise gr.Error(f"Invalid image data: {e}")
264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  num_frames = get_num_frames(duration_seconds)
266
  current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
267
+ resized_image = resize_image(pil_image)
268
 
269
  output_frames_list = pipe(
270
  image=resized_image,
 
283
  video_path = tmpfile.name
284
  export_to_video(output_frames_list, video_path, fps=FIXED_FPS)
285
 
 
 
286
  frames = extract_frames_from_video(video_path, duration_seconds)
287
+ video_b64 = video_to_b64(video_path)
288
+ os.remove(video_path)
289
 
290
+ upscaled_pairs_b64 = []
291
+ for i, frame in enumerate(frames):
292
+ target_w, target_h = update_dimensions_for_klein(frame)
293
+ upscaler_seed = random.randint(0, MAX_SEED)
294
+ result = klein_pipe(
295
+ prompt="high quality, ultra detailed, sharp focus, 8k resolution",
296
+ image=frame,
297
+ height=target_h,
298
+ width=target_w,
299
+ guidance_scale=1.0,
300
+ num_inference_steps=4,
301
+ generator=torch.Generator(device=device).manual_seed(upscaler_seed),
302
+ ).images[0]
303
+
304
+ upscaled_pairs_b64.append({
305
+ "orig": pil_to_b64_png(frame),
306
+ "upscaled": pil_to_b64_png(result)
307
+ })
308
+
309
+ return {
310
+ "video": video_b64,
311
+ "seed": current_seed,
312
+ "frames": upscaled_pairs_b64
313
+ }
314
+
315
+ @app.api(name="load_example", queue=False)
316
+ def load_example(idx: float) -> dict:
317
+ try:
318
+ i = int(idx)
319
+ except (ValueError, TypeError):
320
+ i = -1
321
+ if i < 0 or i >= len(EXAMPLES_CONFIG):
322
+ return {"image": "", "prompt": "", "steps": 4, "status": "error"}
323
+ ex = EXAMPLES_CONFIG[i]
324
+ b64 = encode_full_image(ex["image"])
325
+ return {
326
+ "image": b64,
327
+ "prompt": ex["prompt"],
328
+ "steps": ex["steps"],
329
+ "name": os.path.basename(ex["image"]),
330
+ "status": "ok" if b64 else "error"
331
+ }
332
+
333
+ @app.get("/api/config")
334
+ def client_config():
335
+ return CLIENT_CONFIG
336
+
337
+ @app.get("/", response_class=HTMLResponse)
338
+ async def homepage():
339
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
340
+ with open(html_path, "r", encoding="utf-8") as f:
341
+ return f.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
 
343
  if __name__ == "__main__":
344
+ app.launch(show_error=True, mcp_server=True)