RosticFACE commited on
Commit
8a673d4
·
verified ·
1 Parent(s): 2df5e52

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +216 -68
app.py CHANGED
@@ -1,92 +1,240 @@
1
- import gradio as gr
2
  import torch
3
- from diffusers import DiffusionPipeline
 
 
 
 
 
4
  from PIL import Image
 
 
5
 
6
- # -------------------------
7
- # LOAD MODEL (MULTI-GPU)
8
- # -------------------------
9
 
10
- MODEL_ID = "Wan-AI/Wan2.2-I2V-A14B-Diffusers"
11
 
12
- print("Loading model... This may take a minute.")
13
-
14
- # Balanced spread across ALL GPUs (8×L40S)
15
- pipe = DiffusionPipeline.from_pretrained(
16
- MODEL_ID,
17
- torch_dtype=torch.float16,
18
- device_map="balanced", # full multi-GPU sharding
19
- max_memory={i: "44GiB" for i in range(torch.cuda.device_count())},
20
- )
21
 
22
- pipe.enable_model_cpu_offload()
23
- pipe.enable_vae_slicing()
24
 
25
- print("Model loaded on", torch.cuda.device_count(), "GPUs.")
 
 
 
26
 
 
27
 
28
- # -------------------------
29
- # INFERENCE
30
- # -------------------------
31
 
32
- def generate_video(image, prompt, steps, guidance, seed):
33
- if image is None:
34
- raise ValueError("Upload an input image first.")
35
 
36
- if seed == -1:
37
- seed = torch.randint(0, 2**32, (1,)).item()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- generator = torch.manual_seed(seed)
40
- input_image = Image.fromarray(image)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
- print(f"[INFO] Generation started, seed={seed}")
43
 
44
- result = pipe(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  prompt=prompt,
46
- image=input_image,
 
 
 
 
 
47
  num_inference_steps=int(steps),
48
- guidance_scale=float(guidance),
49
- generator=generator,
50
- ).videos[0]
51
-
52
- output_path = "output.mp4"
53
- pipe.video_processor.save_video(result, output_path, fps=24)
54
 
55
- print("[INFO] Video saved:", output_path)
 
56
 
57
- return output_path
 
58
 
59
 
60
- # -------------------------
61
- # UI
62
- # -------------------------
63
-
64
  with gr.Blocks() as demo:
65
- gr.Markdown(
66
- """
67
- # 🎬 Wan2.2 I2V — Multi-GPU Video Generator
68
- **Модель автоматически распределена по всем доступным GPU (balanced device_map).**
69
- Загрузите изображение → получите видео.
70
- """
71
- )
72
-
73
  with gr.Row():
74
  with gr.Column():
75
- image = gr.Image(type="numpy", label="Input Image")
76
- prompt = gr.Textbox(label="Prompt", placeholder="Describe the motion...")
77
- steps = gr.Slider(10, 60, value=30, step=1, label="Inference Steps")
78
- guidance = gr.Slider(1, 15, value=6.5, step=0.5, label="Guidance Scale")
79
- seed = gr.Number(value=-1, precision=0, label="Seed (-1 = random)")
80
-
81
- run = gr.Button("🎥 Generate Video")
82
-
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  with gr.Column():
84
- output = gr.Video(label="Generated Video")
85
-
86
- run.click(
87
- generate_video,
88
- inputs=[image, prompt, steps, guidance, seed],
89
- outputs=[output],
90
- )
91
-
92
- demo.launch()
 
 
 
 
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
+ import gradio as gr
7
+ import tempfile
8
+ import numpy as np
9
  from PIL import Image
10
+ import random
11
+ import gc
12
 
13
+ from torchao.quantization import quantize_
14
+ from torchao.quantization import Float8DynamicActivationFloat8WeightConfig
15
+ from torchao.quantization import Int8WeightOnlyConfig
16
 
17
+ import aoti
18
 
 
 
 
 
 
 
 
 
 
19
 
20
+ MODEL_ID = "Wan-AI/Wan2.2-I2V-A14B-Diffusers"
 
21
 
22
+ MAX_DIM = 832
23
+ MIN_DIM = 480
24
+ SQUARE_DIM = 640
25
+ MULTIPLE_OF = 16
26
 
27
+ MAX_SEED = np.iinfo(np.int32).max
28
 
29
+ FIXED_FPS = 16
30
+ MIN_FRAMES_MODEL = 8
 
31
 
32
+ MIN_DURATION = round(MIN_FRAMES_MODEL / FIXED_FPS, 1)
33
+ DEFAULT_DURATION = 5.0
 
34
 
35
+ # Модель загружается с device_map='auto' для распределения больших трансформеров
36
+ pipe = WanImageToVideoPipeline.from_pretrained(
37
+ MODEL_ID,
38
+ transformer=WanTransformer3DModel.from_pretrained(
39
+ 'cbensimon/Wan2.2-I2V-A14B-bf16-Diffusers',
40
+ subfolder='transformer',
41
+ torch_dtype=torch.bfloat16,
42
+ device_map='auto',
43
+ ),
44
+ transformer_2=WanTransformer3DModel.from_pretrained(
45
+ 'cbensimon/Wan2.2-I2V-A14B-bf16-Diffusers',
46
+ subfolder='transformer_2',
47
+ torch_dtype=torch.bfloat16,
48
+ device_map='auto',
49
+ ),
50
+ torch_dtype=torch.bfloat16,
51
+ )
52
 
53
+ # Загрузка и фьюзинг LoRA
54
+ pipe.load_lora_weights(
55
+ "Kijai/WanVideo_comfy",
56
+ weight_name="Lightx2v/lightx2v_I2V_14B_480p_cfg_step_distill_rank128_bf16.safetensors",
57
+ adapter_name="lightx2v"
58
+ )
59
+ kwargs_lora = {"load_into_transformer_2": True}
60
+ pipe.load_lora_weights(
61
+ "Kijai/WanVideo_comfy",
62
+ weight_name="Lightx2v/lightx2v_I2V_14B_480p_cfg_step_distill_rank128_bf16.safetensors",
63
+ adapter_name="lightx2v_2",
64
+ **kwargs_lora
65
+ )
66
+ pipe.set_adapters(["lightx2v", "lightx2v_2"], adapter_weights=[1., 1.])
67
+ pipe.fuse_lora(adapter_names=["lightx2v"], lora_scale=3., components=["transformer"])
68
+ pipe.fuse_lora(adapter_names=["lightx2v_2"], lora_scale=1., components=["transformer_2"])
69
+ pipe.unload_lora_weights()
70
+
71
+ # Квантизация
72
+ quantize_(pipe.text_encoder, Int8WeightOnlyConfig())
73
+ quantize_(pipe.transformer, Float8DynamicActivationFloat8WeightConfig())
74
+ quantize_(pipe.transformer_2, Float8DynamicActivationFloat8WeightConfig())
75
+
76
+ # AOTI
77
+ aoti.aoti_blocks_load(pipe.transformer, 'zerogpu-aoti/Wan2', variant='fp8da')
78
+ aoti.aoti_blocks_load(pipe.transformer_2, 'zerogpu-aoti/Wan2', variant='fp8da')
79
+
80
+ # 🟢 ИСПРАВЛЕНИЕ 1: Явно переводим пайплайн на GPU.
81
+ # Это решает проблему "Cannot generate a cpu tensor from a generator of type cuda."
82
+ pipe.to("cuda")
83
+
84
+ default_prompt_i2v = "make this image come alive, cinematic motion, smooth animation"
85
+ default_negative_prompt = (
86
+ "色调艳丽, 过曝, 静态, 细节模糊不清, 字幕, 风格, 作品, 画作, 画面, 静止, "
87
+ "整体发灰, 最差质量, 低质量, JPEG压缩残留, 丑陋의, 残缺的, 多余的手指, "
88
+ "画得不хорошие руки, 画得不хорошие лица, 畸形の, 毀容の, 形态畸形的肢体, 手指融合, "
89
+ "静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走"
90
+ )
91
 
 
92
 
93
+ def resize_image(image: Image.Image) -> Image.Image:
94
+ width, height = image.size
95
+
96
+ if width == height:
97
+ return image.resize((SQUARE_DIM, SQUARE_DIM), Image.LANCZOS)
98
+
99
+ aspect_ratio = width / height
100
+ MAX_ASPECT_RATIO = MAX_DIM / MIN_DIM
101
+ MIN_ASPECT_RATIO = MIN_DIM / MAX_DIM
102
+
103
+ target_w, target_h = width, height
104
+ image_to_resize = image
105
+
106
+ if aspect_ratio > MAX_ASPECT_RATIO:
107
+ crop_width = int(round(height * MAX_ASPECT_RATIO))
108
+ left = (width - crop_width) // 2
109
+ image_to_resize = image.crop((left, 0, left + crop_width, height))
110
+ target_w = MAX_DIM
111
+ target_h = int(round(target_w / MAX_ASPECT_RATIO))
112
+ elif aspect_ratio < MIN_ASPECT_RATIO:
113
+ crop_height = int(round(width / MIN_ASPECT_RATIO))
114
+ top = (height - crop_height) // 2
115
+ image_to_resize = image.crop((0, top, width, top + crop_height))
116
+ target_h = MAX_DIM
117
+ target_w = int(round(target_h * MIN_ASPECT_RATIO))
118
+ else:
119
+ if width > height:
120
+ target_w = MAX_DIM
121
+ target_h = int(round(target_w / aspect_ratio))
122
+ else:
123
+ target_h = MAX_DIM
124
+ target_w = int(round(target_h * aspect_ratio))
125
+
126
+ final_w = round(target_w / MULTIPLE_OF) * MULTIPLE_OF
127
+ final_h = round(target_h / MULTIPLE_OF) * MULTIPLE_OF
128
+ final_w = max(MIN_DIM, min(MAX_DIM, final_w))
129
+ final_h = max(MIN_DIM, min(MAX_DIM, final_h))
130
+ return image_to_resize.resize((final_w, final_h), Image.LANCZOS)
131
+
132
+
133
+ def get_num_frames(duration_seconds: float):
134
+ return 1 + int(round(duration_seconds * FIXED_FPS))
135
+
136
+
137
+ def get_duration(
138
+ input_image,
139
+ prompt,
140
+ steps,
141
+ negative_prompt,
142
+ duration_seconds,
143
+ guidance_scale,
144
+ guidance_scale_2,
145
+ seed,
146
+ randomize_seed,
147
+ progress,
148
+ ):
149
+ BASE_FRAMES_HEIGHT_WIDTH = 81 * 832 * 624
150
+ BASE_STEP_DURATION = 15
151
+ width, height = resize_image(input_image).size
152
+ frames = get_num_frames(duration_seconds)
153
+ factor = frames * width * height / BASE_FRAMES_HEIGHT_WIDTH
154
+ step_duration = BASE_STEP_DURATION * factor ** 1.5
155
+ return 10 + int(steps) * step_duration
156
+
157
+
158
+ @spaces.GPU(duration=get_duration)
159
+ def generate_video(
160
+ input_image,
161
+ prompt,
162
+ steps=4,
163
+ negative_prompt=default_negative_prompt,
164
+ duration_seconds=DEFAULT_DURATION,
165
+ guidance_scale=1,
166
+ guidance_scale_2=1,
167
+ seed=42,
168
+ randomize_seed=False,
169
+ progress=gr.Progress(track_tqdm=True),
170
+ ):
171
+ if input_image is None:
172
+ raise gr.Error("Please upload an input image.")
173
+
174
+ num_frames = get_num_frames(duration_seconds)
175
+ current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
176
+ resized_image = resize_image(input_image)
177
+
178
+ # 🟢 ИСПРАВЛЕНИЕ 2: Удален аргумент 'device="cuda"', чтобы избежать TypeError,
179
+ # так как пайплайн уже был переведен на CUDA перед функцией.
180
+ output_frames_list = pipe(
181
+ image=resized_image,
182
  prompt=prompt,
183
+ negative_prompt=negative_prompt,
184
+ height=resized_image.height,
185
+ width=resized_image.width,
186
+ num_frames=num_frames,
187
+ guidance_scale=float(guidance_scale),
188
+ guidance_scale_2=float(guidance_scale_2),
189
  num_inference_steps=int(steps),
190
+ generator=torch.Generator(device="cuda").manual_seed(current_seed),
191
+ ).frames[0]
 
 
 
 
192
 
193
+ with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmpfile:
194
+ video_path = tmpfile.name
195
 
196
+ export_to_video(output_frames_list, video_path, fps=FIXED_FPS)
197
+ return video_path, current_seed
198
 
199
 
200
+ # --- Gradio Interface ---
 
 
 
201
  with gr.Blocks() as demo:
202
+ gr.Markdown("# 🚀 Wan 2.2 I2V (14B) — Unlimited Duration Edition 🕒")
203
+ gr.Markdown("Generate cinematic I2V animations without duration limits. Optimized for ZeroCPU.")
204
+
 
 
 
 
 
205
  with gr.Row():
206
  with gr.Column():
207
+ input_image_component = gr.Image(type="pil", label="Input Image")
208
+ prompt_input = gr.Textbox(label="Prompt", value=default_prompt_i2v)
209
+
210
+ duration_seconds_input = gr.Slider(
211
+ minimum=0.5,
212
+ maximum=60.0,
213
+ step=0.5,
214
+ value=DEFAULT_DURATION,
215
+ label="Duration (seconds)",
216
+ info=f"Each second = {FIXED_FPS} frames. Longer videos require more VRAM/time."
217
+ )
218
+
219
+ with gr.Accordion("Advanced Settings", open=False):
220
+ negative_prompt_input = gr.Textbox(label="Negative Prompt", value=default_negative_prompt, lines=3)
221
+ seed_input = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=42, interactive=True)
222
+ randomize_seed_checkbox = gr.Checkbox(label="Randomize seed", value=True, interactive=True)
223
+ steps_slider = gr.Slider(minimum=1, maximum=30, step=1, value=6, label="Inference Steps")
224
+ guidance_scale_input = gr.Slider(minimum=0.0, maximum=10.0, step=0.5, value=1, label="Guidance Scale - high noise stage")
225
+ guidance_scale_2_input = gr.Slider(minimum=0.0, maximum=10.0, step=0.5, value=1, label="Guidance Scale 2 - low noise stage")
226
+
227
+ generate_button = gr.Button("Generate Video", variant="primary")
228
  with gr.Column():
229
+ video_output = gr.Video(label="Generated Video", autoplay=True, interactive=False)
230
+
231
+ ui_inputs = [
232
+ input_image_component, prompt_input, steps_slider,
233
+ negative_prompt_input, duration_seconds_input,
234
+ guidance_scale_input, guidance_scale_2_input,
235
+ seed_input, randomize_seed_checkbox
236
+ ]
237
+ generate_button.click(fn=generate_video, inputs=ui_inputs, outputs=[video_output, seed_input])
238
+
239
+ if __name__ == "__main__":
240
+ demo.queue().launch(mcp_server=True)