seawolf2357 commited on
Commit
bfa6fb3
ยท
verified ยท
1 Parent(s): a816f3f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +442 -480
app.py CHANGED
@@ -11,525 +11,444 @@ from PIL import Image
11
  import random
12
  import logging
13
  import gc
14
- import time
15
- import hashlib
16
- from dataclasses import dataclass
17
- from typing import Optional, Tuple
18
- from functools import wraps
19
- import threading
20
- import os
21
-
22
- # GPU ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ์„ค์ •
23
- os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'max_split_size_mb:256' # ๋” ์ž‘์€ ์ฒญํฌ ์‚ฌ์šฉ
24
 
25
  # ๋กœ๊น… ์„ค์ •
26
  logging.basicConfig(level=logging.INFO)
27
  logger = logging.getLogger(__name__)
28
 
29
- # ์„ค์ • ๊ด€๋ฆฌ
30
- @dataclass
31
- class VideoGenerationConfig:
32
- model_id: str = "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers"
33
- lora_repo_id: str = "Kijai/WanVideo_comfy"
34
- lora_filename: str = "Wan21_CausVid_14B_T2V_lora_rank32.safetensors"
35
- mod_value: int = 32
36
- # Zero GPU๋ฅผ ์œ„ํ•œ ๋งค์šฐ ๋ณด์ˆ˜์ ์ธ ๊ธฐ๋ณธ๊ฐ’
37
- default_height: int = 320
38
- default_width: int = 320
39
- max_area: float = 320.0 * 320.0 # Zero GPU์— ์ตœ์ ํ™”
40
- slider_min_h: int = 128
41
- slider_max_h: int = 512 # ๋” ๋‚ฎ์€ ์ตœ๋Œ€๊ฐ’
42
- slider_min_w: int = 128
43
- slider_max_w: int = 512 # ๋” ๋‚ฎ์€ ์ตœ๋Œ€๊ฐ’
44
- fixed_fps: int = 24
45
- min_frames: int = 8
46
- max_frames: int = 30 # ๋” ๋‚ฎ์€ ์ตœ๋Œ€ ํ”„๋ ˆ์ž„ (1.25์ดˆ)
47
- default_prompt: str = "make this image move, smooth motion"
48
- default_negative_prompt: str = "static, blur"
49
- # GPU ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™” ์„ค์ •
50
- enable_model_cpu_offload: bool = True
51
- enable_vae_slicing: bool = True
52
- enable_vae_tiling: bool = True
53
-
54
- @property
55
- def max_duration(self):
56
- """์ตœ๋Œ€ ํ—ˆ์šฉ duration (์ดˆ)"""
57
- return self.max_frames / self.fixed_fps
58
-
59
- @property
60
- def min_duration(self):
61
- """์ตœ์†Œ ํ—ˆ์šฉ duration (์ดˆ)"""
62
- return self.min_frames / self.fixed_fps
63
 
64
- config = VideoGenerationConfig()
 
65
  MAX_SEED = np.iinfo(np.int32).max
66
 
67
- # ๊ธ€๋กœ๋ฒŒ ๋ณ€์ˆ˜
68
- pipe = None
69
- generation_lock = threading.Lock()
70
-
71
- # ์„ฑ๋Šฅ ์ธก์ • ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ
72
- def measure_time(func):
73
- @wraps(func)
74
- def wrapper(*args, **kwargs):
75
- start = time.time()
76
- result = func(*args, **kwargs)
77
- logger.info(f"{func.__name__} took {time.time()-start:.2f}s")
78
- return result
79
- return wrapper
80
-
81
- # GPU ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ ํ•จ์ˆ˜
82
- def clear_gpu_memory():
83
- """๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ (Zero GPU ์•ˆ์ „)"""
84
- gc.collect()
85
- if torch.cuda.is_available():
86
- try:
87
- torch.cuda.empty_cache()
88
- torch.cuda.synchronize()
89
- except:
90
- pass
91
-
92
- # ๋น„๋””์˜ค ์ƒ์„ฑ๊ธฐ ํด๋ž˜์Šค
93
- class VideoGenerator:
94
- def __init__(self, config: VideoGenerationConfig):
95
- self.config = config
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- def calculate_dimensions(self, image: Image.Image) -> Tuple[int, int]:
98
- orig_w, orig_h = image.size
99
- if orig_w <= 0 or orig_h <= 0:
100
- return self.config.default_height, self.config.default_width
101
-
102
- aspect_ratio = orig_h / orig_w
103
-
104
- # Zero GPU์— ์ตœ์ ํ™”๋œ ๋งค์šฐ ์ž‘์€ ํ•ด์ƒ๋„
105
- max_area = 320.0 * 320.0 # 102,400 ํ”ฝ์…€
106
-
107
- # ์ข…ํšก๋น„๊ฐ€ ๋„ˆ๋ฌด ๊ทน๋‹จ์ ์ธ ๊ฒฝ์šฐ ์กฐ์ •
108
- if aspect_ratio > 2.0:
109
- aspect_ratio = 2.0
110
- elif aspect_ratio < 0.5:
111
- aspect_ratio = 0.5
112
-
113
- calc_h = round(np.sqrt(max_area * aspect_ratio))
114
- calc_w = round(np.sqrt(max_area / aspect_ratio))
115
-
116
- # mod_value์— ๋งž์ถค
117
- calc_h = max(self.config.mod_value, (calc_h // self.config.mod_value) * self.config.mod_value)
118
- calc_w = max(self.config.mod_value, (calc_w // self.config.mod_value) * self.config.mod_value)
119
-
120
- # ์ตœ๋Œ€ 512๋กœ ์ œํ•œ
121
- new_h = int(np.clip(calc_h, self.config.slider_min_h, 512))
122
- new_w = int(np.clip(calc_w, self.config.slider_min_w, 512))
123
-
124
- # mod_value์— ๋งž์ถค
125
- new_h = (new_h // self.config.mod_value) * self.config.mod_value
126
- new_w = (new_w // self.config.mod_value) * self.config.mod_value
127
-
128
- # ์ตœ์ข… ํ”ฝ์…€ ์ˆ˜ ํ™•์ธ
129
- if new_h * new_w > 102400: # 320x320
130
- # ๋น„์œจ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ์ถ•์†Œ
131
- scale = np.sqrt(102400 / (new_h * new_w))
132
- new_h = int((new_h * scale) // self.config.mod_value) * self.config.mod_value
133
- new_w = int((new_w * scale) // self.config.mod_value) * self.config.mod_value
134
-
135
- return new_h, new_w
136
 
137
- def validate_inputs(self, image: Image.Image, prompt: str, height: int,
138
- width: int, duration: float, steps: int) -> Tuple[bool, Optional[str]]:
139
- if image is None:
140
- return False, "๐Ÿ–ผ๏ธ Please upload an input image"
141
-
142
- if not prompt or len(prompt.strip()) == 0:
143
- return False, "โœ๏ธ Please provide a prompt"
144
-
145
- if len(prompt) > 200: # ๋” ์งง์€ ํ”„๋กฌํ”„ํŠธ ์ œํ•œ
146
- return False, "โš ๏ธ Prompt is too long (max 200 characters)"
147
-
148
- # Zero GPU์— ์ตœ์ ํ™”๋œ ์ œํ•œ
149
- if duration < 0.3:
150
- return False, "โฑ๏ธ Duration too short (min 0.3s)"
151
-
152
- if duration > 1.2: # ๋” ์งง์€ ์ตœ๋Œ€ duration
153
- return False, "โฑ๏ธ Duration too long (max 1.2s for stability)"
154
-
155
- # ํ”ฝ์…€ ์ˆ˜ ์ œํ•œ (๋” ๋ณด์ˆ˜์ ์œผ๋กœ)
156
- max_pixels = 320 * 320 # 102,400 ํ”ฝ์…€
157
- if height * width > max_pixels:
158
- return False, f"๐Ÿ“ Total pixels limited to {max_pixels:,} (e.g., 320ร—320, 256ร—384)"
159
-
160
- if height > 512 or width > 512: # ๋” ๋‚ฎ์€ ์ตœ๋Œ€๊ฐ’
161
- return False, "๐Ÿ“ Maximum dimension is 512 pixels"
162
-
163
- # ์ข…ํšก๋น„ ์ฒดํฌ
164
- aspect_ratio = max(height/width, width/height)
165
- if aspect_ratio > 2.0:
166
- return False, "๐Ÿ“ Aspect ratio too extreme (max 2:1 or 1:2)"
167
-
168
- if steps > 5: # ๋” ๋‚ฎ์€ ์ตœ๋Œ€ ์Šคํ…
169
- return False, "๐Ÿ”ง Maximum 5 steps in Zero GPU environment"
170
-
171
- return True, None
172
 
173
- def generate_unique_filename(self, seed: int) -> str:
174
- timestamp = int(time.time())
175
- unique_str = f"{timestamp}_{seed}_{random.randint(1000, 9999)}"
176
- hash_obj = hashlib.md5(unique_str.encode())
177
- return f"video_{hash_obj.hexdigest()[:8]}.mp4"
178
-
179
- video_generator = VideoGenerator(config)
180
-
181
- # Gradio ํ•จ์ˆ˜๋“ค
182
- def handle_image_upload(image):
183
- if image is None:
184
- return gr.update(value=config.default_height), gr.update(value=config.default_width)
185
 
 
 
 
 
 
186
  try:
187
- if not isinstance(image, Image.Image):
188
- raise ValueError("Invalid image format")
189
-
190
- new_h, new_w = video_generator.calculate_dimensions(image)
 
191
  return gr.update(value=new_h), gr.update(value=new_w)
192
-
193
  except Exception as e:
194
- logger.error(f"Error processing image: {e}")
195
- gr.Warning("โš ๏ธ Error processing image")
196
- return gr.update(value=config.default_height), gr.update(value=config.default_width)
197
-
198
- def get_duration(input_image, prompt, height, width, negative_prompt,
199
- duration_seconds, guidance_scale, steps, seed, randomize_seed, progress):
200
- # Zero GPU ํ™˜๊ฒฝ์—์„œ ๋งค์šฐ ๋ณด์ˆ˜์ ์ธ ์‹œ๊ฐ„ ํ• ๋‹น
201
- base_duration = 50 # ๊ธฐ๋ณธ 50์ดˆ๋กœ ์ฆ๊ฐ€
202
-
203
- # ํ”ฝ์…€ ์ˆ˜์— ๋”ฐ๋ฅธ ์ถ”๊ฐ€ ์‹œ๊ฐ„
204
- pixels = height * width
205
- if pixels > 147456: # 384x384 ์ด์ƒ
206
- base_duration += 20
207
- elif pixels > 100000: # ~316x316 ์ด์ƒ
208
- base_duration += 10
209
-
210
- # ์Šคํ… ์ˆ˜์— ๋”ฐ๋ฅธ ์ถ”๊ฐ€ ์‹œ๊ฐ„
211
- if steps > 4:
212
- base_duration += 15
213
- elif steps > 2:
214
- base_duration += 10
215
-
216
- # ์ข…ํšก๋น„๊ฐ€ ๊ทน๋‹จ์ ์ธ ๊ฒฝ์šฐ ์ถ”๊ฐ€ ์‹œ๊ฐ„
217
- aspect_ratio = max(height/width, width/height)
218
- if aspect_ratio > 1.5: # 3:2 ์ด์ƒ์˜ ๋น„์œจ
219
- base_duration += 10
220
 
221
- # ์ตœ๋Œ€ 90์ดˆ๋กœ ์ œํ•œ
222
- return min(base_duration, 90)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
  @spaces.GPU(duration=get_duration)
225
- @measure_time
226
  def generate_video(input_image, prompt, height, width,
227
- negative_prompt=config.default_negative_prompt,
228
- duration_seconds=0.8, guidance_scale=1, steps=3,
229
- seed=42, randomize_seed=False,
230
  progress=gr.Progress(track_tqdm=True)):
231
 
232
- global pipe
233
-
234
- # ๋™์‹œ ์‹คํ–‰ ๋ฐฉ์ง€
235
- if not generation_lock.acquire(blocking=False):
236
- raise gr.Error("โณ Another video is being generated. Please wait...")
237
 
238
- try:
239
- progress(0.05, desc="๐Ÿ” Validating inputs...")
240
- logger.info(f"Starting generation - Resolution: {height}x{width}, Duration: {duration_seconds}s, Steps: {steps}")
241
-
242
- # ์ž…๋ ฅ ๊ฒ€์ฆ
243
- is_valid, error_msg = video_generator.validate_inputs(
244
- input_image, prompt, height, width, duration_seconds, steps
245
- )
246
- if not is_valid:
247
- logger.warning(f"Validation failed: {error_msg}")
248
- raise gr.Error(error_msg)
249
-
250
- # ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
251
- clear_gpu_memory()
252
-
253
- progress(0.1, desc="๐Ÿš€ Loading model...")
254
-
255
- # ๋ชจ๋ธ ๋กœ๋”ฉ (GPU ํ•จ์ˆ˜ ๋‚ด์—์„œ)
256
- if pipe is None:
257
- try:
258
- logger.info("Loading model components...")
259
-
260
- # ์ปดํฌ๋„ŒํŠธ ๋กœ๋“œ
261
- image_encoder = CLIPVisionModel.from_pretrained(
262
- config.model_id,
263
- subfolder="image_encoder",
264
- torch_dtype=torch.float16,
265
- low_cpu_mem_usage=True
266
- )
267
-
268
- vae = AutoencoderKLWan.from_pretrained(
269
- config.model_id,
270
- subfolder="vae",
271
- torch_dtype=torch.float16,
272
- low_cpu_mem_usage=True
273
- )
274
-
275
- pipe = WanImageToVideoPipeline.from_pretrained(
276
- config.model_id,
277
- vae=vae,
278
- image_encoder=image_encoder,
279
- torch_dtype=torch.bfloat16,
280
- low_cpu_mem_usage=True,
281
- use_safetensors=True
282
- )
283
-
284
- # ์Šค์ผ€์ค„๋Ÿฌ ์„ค์ •
285
- pipe.scheduler = UniPCMultistepScheduler.from_config(
286
- pipe.scheduler.config, flow_shift=8.0
287
- )
288
-
289
- # LoRA ๋กœ๋“œ ๊ฑด๋„ˆ๋›ฐ๊ธฐ (์•ˆ์ •์„ฑ์„ ์œ„ํ•ด)
290
- logger.info("Skipping LoRA for stability")
291
-
292
- # GPU๋กœ ์ด๋™
293
- pipe.to("cuda")
294
-
295
- # ์ตœ์ ํ™” ํ™œ์„ฑํ™”
296
- pipe.enable_vae_slicing()
297
- pipe.enable_vae_tiling()
298
-
299
- # ๋ชจ๋ธ CPU ์˜คํ”„๋กœ๋“œ ํ™œ์„ฑํ™” (๋ฉ”๋ชจ๋ฆฌ ์ ˆ์•ฝ)
300
- pipe.enable_model_cpu_offload()
301
-
302
- logger.info("Model loaded successfully")
303
-
304
- except Exception as e:
305
- logger.error(f"Model loading failed: {e}")
306
- raise gr.Error("Failed to load model")
307
-
308
- progress(0.3, desc="๐ŸŽฏ Preparing image...")
309
-
310
- # ์ด๋ฏธ์ง€ ์ค€๋น„
311
- target_h = max(config.mod_value, (int(height) // config.mod_value) * config.mod_value)
312
- target_w = max(config.mod_value, (int(width) // config.mod_value) * config.mod_value)
313
-
314
- # ํ”„๋ ˆ์ž„ ์ˆ˜ ๊ณ„์‚ฐ (๋งค์šฐ ๋ณด์ˆ˜์ )
315
- num_frames = min(
316
- int(round(duration_seconds * config.fixed_fps)),
317
- 24 # ์ตœ๋Œ€ 24ํ”„๋ ˆ์ž„ (1์ดˆ)
318
- )
319
- num_frames = max(8, num_frames) # ์ตœ์†Œ 8ํ”„๋ ˆ์ž„
320
-
321
- logger.info(f"Generating {num_frames} frames at {target_h}x{target_w}")
322
-
323
- current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
324
-
325
- # ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ
326
- resized_image = input_image.resize((target_w, target_h), Image.Resampling.LANCZOS)
327
-
328
- progress(0.4, desc="๐ŸŽฌ Generating video...")
329
-
330
- # ๋น„๋””์˜ค ์ƒ์„ฑ
331
- with torch.inference_mode(), torch.amp.autocast('cuda', enabled=True, dtype=torch.float16):
332
- try:
333
- # ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ ์œ„ํ•œ ์„ค์ •
334
- torch.cuda.empty_cache()
335
-
336
- # ์ƒ์„ฑ ํŒŒ๋ผ๋ฏธํ„ฐ ์ตœ์ ํ™”
337
- output_frames_list = pipe(
338
- image=resized_image,
339
- prompt=prompt[:150], # ํ”„๋กฌํ”„ํŠธ ๊ธธ์ด ์ œํ•œ
340
- negative_prompt=negative_prompt[:50] if negative_prompt else "",
341
- height=target_h,
342
- width=target_w,
343
- num_frames=num_frames,
344
- guidance_scale=float(guidance_scale),
345
- num_inference_steps=int(steps),
346
- generator=torch.Generator(device="cuda").manual_seed(current_seed),
347
- return_dict=True,
348
- # ์ถ”๊ฐ€ ์ตœ์ ํ™” ํŒŒ๋ผ๋ฏธํ„ฐ
349
- output_type="pil"
350
- ).frames[0]
351
-
352
- logger.info("Video generation completed successfully")
353
-
354
- except torch.cuda.OutOfMemoryError:
355
- logger.error("GPU OOM error")
356
- clear_gpu_memory()
357
- raise gr.Error("๐Ÿ’พ GPU out of memory. Try smaller dimensions (256x256 recommended).")
358
- except RuntimeError as e:
359
- if "out of memory" in str(e).lower():
360
- logger.error("Runtime OOM error")
361
- clear_gpu_memory()
362
- raise gr.Error("๐Ÿ’พ GPU memory error. Please try again with smaller settings.")
363
- else:
364
- logger.error(f"Runtime error: {e}")
365
- raise gr.Error(f"โŒ Generation failed: {str(e)[:50]}")
366
- except Exception as e:
367
- logger.error(f"Generation error: {type(e).__name__}: {e}")
368
- raise gr.Error(f"โŒ Generation failed. Try reducing resolution or steps.")
369
-
370
- progress(0.9, desc="๐Ÿ’พ Saving video...")
371
-
372
- # ๋น„๋””์˜ค ์ €์žฅ
373
- try:
374
- filename = video_generator.generate_unique_filename(current_seed)
375
- with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmpfile:
376
- video_path = tmpfile.name
377
-
378
- export_to_video(output_frames_list, video_path, fps=config.fixed_fps)
379
- logger.info(f"Video saved: {video_path}")
380
- except Exception as e:
381
- logger.error(f"Save error: {e}")
382
- raise gr.Error("Failed to save video")
383
 
384
- progress(1.0, desc="โœจ Complete!")
385
- logger.info(f"Video generated: {num_frames} frames, {target_h}x{target_w}")
 
 
386
 
387
- # ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
388
- del output_frames_list
389
- del resized_image
390
- torch.cuda.empty_cache()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  gc.collect()
392
-
393
- return video_path, current_seed
394
-
395
- except gr.Error:
396
- raise
397
  except Exception as e:
398
- logger.error(f"Unexpected error: {type(e).__name__}: {e}")
399
- raise gr.Error(f"โŒ Unexpected error. Please try again with smaller settings.")
400
-
401
- finally:
402
- generation_lock.release()
403
- clear_gpu_memory()
 
 
 
 
 
 
 
 
 
404
 
405
- # CSS
406
  css = """
407
  .container {
408
- max-width: 1000px;
409
  margin: auto;
410
  padding: 20px;
411
  }
412
 
413
  .header {
414
  text-align: center;
415
- margin-bottom: 20px;
416
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
417
- padding: 30px;
418
- border-radius: 15px;
419
  color: white;
420
- box-shadow: 0 5px 15px rgba(0,0,0,0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  }
422
 
423
  .header h1 {
424
- font-size: 2.5em;
425
  margin-bottom: 10px;
 
 
 
426
  }
427
 
428
- .warning-box {
429
- background: #fff3cd;
430
- border: 1px solid #ffeaa7;
431
- border-radius: 8px;
432
- padding: 12px;
433
- margin: 10px 0;
434
- color: #856404;
435
- font-size: 0.9em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  }
437
 
438
  .generate-btn {
439
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
440
  color: white;
441
- font-size: 1.2em;
442
- padding: 12px 30px;
443
- border-radius: 25px;
444
  border: none;
445
  cursor: pointer;
 
 
446
  width: 100%;
447
- margin-top: 15px;
448
  }
449
 
450
  .generate-btn:hover {
451
  transform: translateY(-2px);
452
- box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  }
454
  """
455
 
456
- # Gradio UI
457
  with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
458
  with gr.Column(elem_classes="container"):
459
- # Header
460
  gr.HTML("""
461
  <div class="header">
462
- <h1>๐ŸŽฌ AI Video Generator</h1>
463
- <p>Transform images into videos with Wan 2.1 (Zero GPU Optimized)</p>
 
464
  </div>
465
  """)
466
 
467
- # ๊ฒฝ๊ณ 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  gr.HTML("""
469
- <div class="warning-box">
470
- <strong>โšก Zero GPU Strict Limitations:</strong>
471
- <ul style="margin: 5px 0; padding-left: 20px;">
472
- <li>Max resolution: 320ร—320 (recommended 256ร—256)</li>
473
- <li>Max duration: 1.2 seconds</li>
474
- <li>Max steps: 5 (2-3 recommended)</li>
475
- <li>Processing time: ~50-80 seconds</li>
476
- <li>Please wait for completion before next generation</li>
477
- </ul>
478
  </div>
479
  """)
480
 
481
- with gr.Row():
482
  with gr.Column(scale=1):
483
- input_image = gr.Image(
484
- type="pil",
485
- label="๐Ÿ–ผ๏ธ Upload Image"
486
- )
487
-
488
- prompt_input = gr.Textbox(
489
- label="โœจ Animation Prompt",
490
- value=config.default_prompt,
491
- placeholder="Describe the motion...",
492
- lines=2,
493
- max_lines=3
494
- )
495
-
496
- duration_input = gr.Slider(
497
- minimum=0.3,
498
- maximum=1.2,
499
- step=0.1,
500
- value=0.8,
501
- label="โฑ๏ธ Duration (seconds)"
502
- )
503
 
504
- with gr.Accordion("โš™๏ธ Settings", open=False):
505
- negative_prompt = gr.Textbox(
506
- label="Negative Prompt",
507
- value=config.default_negative_prompt,
508
- lines=1
509
  )
510
 
511
- with gr.Row():
512
- height_slider = gr.Slider(
513
- minimum=128,
514
- maximum=512,
515
- step=32,
516
- value=256,
517
- label="Height"
518
- )
519
- width_slider = gr.Slider(
520
- minimum=128,
521
- maximum=512,
522
- step=32,
523
- value=256,
524
- label="Width"
525
- )
526
 
527
- steps_slider = gr.Slider(
528
- minimum=1,
529
- maximum=5,
530
- step=1,
531
  value=2,
532
- label="Steps (2-3 recommended)"
 
 
 
 
 
 
 
 
533
  )
534
 
535
  with gr.Row():
@@ -538,19 +457,43 @@ with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
538
  maximum=MAX_SEED,
539
  step=1,
540
  value=42,
541
- label="Seed"
542
  )
543
  randomize_seed = gr.Checkbox(
544
- label="Random",
545
  value=True
546
  )
547
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  guidance_scale = gr.Slider(
549
  minimum=0.0,
550
- maximum=5.0,
551
  step=0.5,
552
  value=1.0,
553
- label="Guidance Scale",
554
  visible=False
555
  )
556
 
@@ -561,44 +504,63 @@ with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
561
  )
562
 
563
  with gr.Column(scale=1):
 
564
  video_output = gr.Video(
565
- label="Generated Video",
566
- autoplay=True
 
567
  )
568
 
569
- gr.Markdown("""
570
- ### ๐Ÿ’ก Tips for Zero GPU:
571
- - **Best**: 256ร—256 resolution
572
- - **Safe**: 2-3 steps only
573
- - **Duration**: 0.8s is optimal
574
- - **Prompts**: Keep short and simple
575
- - **Important**: Wait for completion!
576
-
577
- ### โš ๏ธ If GPU stops:
578
- - Reduce resolution to 256ร—256
579
- - Use only 2 steps
580
- - Keep duration under 1 second
581
- - Avoid extreme aspect ratios
582
  """)
583
 
584
- # Event handlers
585
- input_image.upload(
586
- fn=handle_image_upload,
587
- inputs=[input_image],
588
- outputs=[height_slider, width_slider]
589
- )
590
-
591
- generate_btn.click(
592
- fn=generate_video,
593
- inputs=[
594
- input_image, prompt_input, height_slider, width_slider,
595
- negative_prompt, duration_input, guidance_scale,
596
- steps_slider, seed, randomize_seed
597
  ],
598
- outputs=[video_output, seed]
 
 
 
599
  )
600
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  if __name__ == "__main__":
602
- logger.info("Starting app in Zero GPU environment")
603
- demo.queue(max_size=2) # ์ž‘์€ ํ ์‚ฌ์ด์ฆˆ
604
- demo.launch()
 
11
  import random
12
  import logging
13
  import gc
 
 
 
 
 
 
 
 
 
 
14
 
15
  # ๋กœ๊น… ์„ค์ •
16
  logging.basicConfig(level=logging.INFO)
17
  logger = logging.getLogger(__name__)
18
 
19
+ # ๋ชจ๋ธ ์„ค์ •
20
+ MODEL_ID = "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers"
21
+ LORA_REPO_ID = "Kijai/WanVideo_comfy"
22
+ LORA_FILENAME = "Wan21_CausVid_14B_T2V_lora_rank32.safetensors"
23
+
24
+ # ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •
25
+ MOD_VALUE = 32
26
+ DEFAULT_H_SLIDER_VALUE = 512
27
+ DEFAULT_W_SLIDER_VALUE = 512 # Zero GPU๋ฅผ ์œ„ํ•ด ์ •์‚ฌ๊ฐํ˜• ๊ธฐ๋ณธ๊ฐ’
28
+ NEW_FORMULA_MAX_AREA = 480.0 * 832.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ SLIDER_MIN_H, SLIDER_MAX_H = 128, 896
31
+ SLIDER_MIN_W, SLIDER_MAX_W = 128, 896
32
  MAX_SEED = np.iinfo(np.int32).max
33
 
34
+ FIXED_FPS = 24
35
+ MIN_FRAMES_MODEL = 8
36
+ MAX_FRAMES_MODEL = 81
37
+
38
+ default_prompt_i2v = "make this image come alive, cinematic motion, smooth animation"
39
+ default_negative_prompt = "static, blurred, low quality, watermark, text"
40
+
41
+ # ๋ชจ๋ธ ๊ธ€๋กœ๋ฒŒ ๋กœ๋”ฉ
42
+ logger.info("Loading model components...")
43
+ image_encoder = CLIPVisionModel.from_pretrained(MODEL_ID, subfolder="image_encoder", torch_dtype=torch.float32)
44
+ vae = AutoencoderKLWan.from_pretrained(MODEL_ID, subfolder="vae", torch_dtype=torch.float32)
45
+ pipe = WanImageToVideoPipeline.from_pretrained(
46
+ MODEL_ID, vae=vae, image_encoder=image_encoder, torch_dtype=torch.bfloat16
47
+ )
48
+ pipe.scheduler = UniPCMultistepScheduler.from_config(pipe.scheduler.config, flow_shift=8.0)
49
+ pipe.to("cuda")
50
+
51
+ # LoRA ๋กœ๋”ฉ
52
+ try:
53
+ causvid_path = hf_hub_download(repo_id=LORA_REPO_ID, filename=LORA_FILENAME)
54
+ pipe.load_lora_weights(causvid_path, adapter_name="causvid_lora")
55
+ pipe.set_adapters(["causvid_lora"], adapter_weights=[0.95])
56
+ pipe.fuse_lora()
57
+ logger.info("LoRA loaded successfully")
58
+ except Exception as e:
59
+ logger.warning(f"LoRA loading failed: {e}")
60
+
61
+ # ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™” ํ™œ์„ฑํ™”
62
+ pipe.enable_vae_slicing()
63
+ pipe.enable_vae_tiling()
64
+ pipe.enable_model_cpu_offload()
65
+
66
+ logger.info("Model loaded and ready")
67
+
68
+ def _calculate_new_dimensions_wan(pil_image, mod_val, calculation_max_area,
69
+ min_slider_h, max_slider_h,
70
+ min_slider_w, max_slider_w,
71
+ default_h, default_w):
72
+ orig_w, orig_h = pil_image.size
73
+ if orig_w <= 0 or orig_h <= 0:
74
+ return default_h, default_w
75
+
76
+ aspect_ratio = orig_h / orig_w
77
 
78
+ # Zero GPU๋ฅผ ์œ„ํ•œ ๋ณด์ˆ˜์ ์ธ ๊ณ„์‚ฐ
79
+ if hasattr(spaces, 'GPU'):
80
+ # ๋” ์ž‘์€ max_area ์‚ฌ์šฉ
81
+ calculation_max_area = min(calculation_max_area, 320.0 * 320.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
+ calc_h = round(np.sqrt(calculation_max_area * aspect_ratio))
84
+ calc_w = round(np.sqrt(calculation_max_area / aspect_ratio))
85
+
86
+ calc_h = max(mod_val, (calc_h // mod_val) * mod_val)
87
+ calc_w = max(mod_val, (calc_w // mod_val) * mod_val)
88
+
89
+ # Zero GPU ํ™˜๊ฒฝ์—์„œ ์ถ”๊ฐ€ ์ œํ•œ
90
+ if hasattr(spaces, 'GPU'):
91
+ max_slider_h = min(max_slider_h, 640)
92
+ max_slider_w = min(max_slider_w, 640)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ new_h = int(np.clip(calc_h, min_slider_h, (max_slider_h // mod_val) * mod_val))
95
+ new_w = int(np.clip(calc_w, min_slider_w, (max_slider_w // mod_val) * mod_val))
 
 
 
 
 
 
 
 
 
 
96
 
97
+ return new_h, new_w
98
+
99
+ def handle_image_upload_for_dims_wan(uploaded_pil_image, current_h_val, current_w_val):
100
+ if uploaded_pil_image is None:
101
+ return gr.update(value=DEFAULT_H_SLIDER_VALUE), gr.update(value=DEFAULT_W_SLIDER_VALUE)
102
  try:
103
+ new_h, new_w = _calculate_new_dimensions_wan(
104
+ uploaded_pil_image, MOD_VALUE, NEW_FORMULA_MAX_AREA,
105
+ SLIDER_MIN_H, SLIDER_MAX_H, SLIDER_MIN_W, SLIDER_MAX_W,
106
+ DEFAULT_H_SLIDER_VALUE, DEFAULT_W_SLIDER_VALUE
107
+ )
108
  return gr.update(value=new_h), gr.update(value=new_w)
 
109
  except Exception as e:
110
+ gr.Warning("Error attempting to calculate new dimensions")
111
+ return gr.update(value=DEFAULT_H_SLIDER_VALUE), gr.update(value=DEFAULT_W_SLIDER_VALUE)
112
+
113
+ def get_duration(input_image, prompt, height, width,
114
+ negative_prompt, duration_seconds,
115
+ guidance_scale, steps,
116
+ seed, randomize_seed,
117
+ progress):
118
+ # Zero GPU๋ฅผ ์œ„ํ•œ ๋ณด์ˆ˜์ ์ธ ์‹œ๊ฐ„ ํ• ๋‹น
119
+ base_time = 60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
+ if hasattr(spaces, 'GPU'):
122
+ # Zero GPU ํ™˜๊ฒฝ์—์„œ ๋” ๋งŽ์€ ์‹œ๊ฐ„ ํ• ๋‹น
123
+ if steps > 4 and duration_seconds > 2:
124
+ return 90
125
+ elif steps > 4 or duration_seconds > 2:
126
+ return 80
127
+ else:
128
+ return 70
129
+ else:
130
+ # ์ผ๋ฐ˜ GPU ํ™˜๊ฒฝ
131
+ if steps > 4 and duration_seconds > 2:
132
+ return 90
133
+ elif steps > 4 or duration_seconds > 2:
134
+ return 75
135
+ else:
136
+ return 60
137
 
138
  @spaces.GPU(duration=get_duration)
 
139
  def generate_video(input_image, prompt, height, width,
140
+ negative_prompt=default_negative_prompt, duration_seconds = 2,
141
+ guidance_scale = 1, steps = 4,
142
+ seed = 42, randomize_seed = False,
143
  progress=gr.Progress(track_tqdm=True)):
144
 
145
+ if input_image is None:
146
+ raise gr.Error("Please upload an input image.")
 
 
 
147
 
148
+ # Zero GPU ํ™˜๊ฒฝ์—์„œ ์ถ”๊ฐ€ ๊ฒ€์ฆ
149
+ if hasattr(spaces, 'GPU'):
150
+ # ํ”ฝ์…€ ์ œํ•œ
151
+ max_pixels = 409600 # 640x640
152
+ if height * width > max_pixels:
153
+ raise gr.Error(f"Resolution too high for Zero GPU. Maximum {max_pixels:,} pixels (e.g., 640ร—640)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
+ # Duration ์ œํ•œ
156
+ if duration_seconds > 2.5:
157
+ duration_seconds = 2.5
158
+ gr.Warning("Duration limited to 2.5s in Zero GPU environment")
159
 
160
+ # Steps ์ œํ•œ
161
+ if steps > 8:
162
+ steps = 8
163
+ gr.Warning("Steps limited to 8 in Zero GPU environment")
164
+
165
+ target_h = max(MOD_VALUE, (int(height) // MOD_VALUE) * MOD_VALUE)
166
+ target_w = max(MOD_VALUE, (int(width) // MOD_VALUE) * MOD_VALUE)
167
+
168
+ num_frames = np.clip(int(round(duration_seconds * FIXED_FPS)), MIN_FRAMES_MODEL, MAX_FRAMES_MODEL)
169
+
170
+ # Zero GPU์—์„œ ํ”„๋ ˆ์ž„ ์ˆ˜ ์ถ”๊ฐ€ ์ œํ•œ
171
+ if hasattr(spaces, 'GPU'):
172
+ max_frames_zerogpu = int(2.5 * FIXED_FPS) # 2.5์ดˆ
173
+ num_frames = min(num_frames, max_frames_zerogpu)
174
+
175
+ current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
176
+
177
+ logger.info(f"Generating video: {target_h}x{target_w}, {num_frames} frames, seed={current_seed}")
178
+
179
+ # ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ
180
+ resized_image = input_image.resize((target_w, target_h), Image.Resampling.LANCZOS)
181
+
182
+ try:
183
+ with torch.inference_mode():
184
+ output_frames_list = pipe(
185
+ image=resized_image,
186
+ prompt=prompt,
187
+ negative_prompt=negative_prompt,
188
+ height=target_h,
189
+ width=target_w,
190
+ num_frames=num_frames,
191
+ guidance_scale=float(guidance_scale),
192
+ num_inference_steps=int(steps),
193
+ generator=torch.Generator(device="cuda").manual_seed(current_seed)
194
+ ).frames[0]
195
+ except torch.cuda.OutOfMemoryError:
196
  gc.collect()
197
+ torch.cuda.empty_cache()
198
+ raise gr.Error("GPU out of memory. Try smaller resolution or shorter duration.")
 
 
 
199
  except Exception as e:
200
+ logger.error(f"Generation failed: {e}")
201
+ raise gr.Error(f"Video generation failed: {str(e)[:100]}")
202
+
203
+ # ๋น„๋””์˜ค ์ €์žฅ
204
+ with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmpfile:
205
+ video_path = tmpfile.name
206
+ export_to_video(output_frames_list, video_path, fps=FIXED_FPS)
207
+
208
+ # ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
209
+ del output_frames_list
210
+ gc.collect()
211
+ if torch.cuda.is_available():
212
+ torch.cuda.empty_cache()
213
+
214
+ return video_path, current_seed
215
 
216
+ # CSS ์Šคํƒ€์ผ (๊ธฐ์กด UI ์œ ์ง€)
217
  css = """
218
  .container {
219
+ max-width: 1200px;
220
  margin: auto;
221
  padding: 20px;
222
  }
223
 
224
  .header {
225
  text-align: center;
226
+ margin-bottom: 30px;
227
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
228
+ padding: 40px;
229
+ border-radius: 20px;
230
  color: white;
231
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
232
+ position: relative;
233
+ overflow: hidden;
234
+ }
235
+
236
+ .header::before {
237
+ content: '';
238
+ position: absolute;
239
+ top: -50%;
240
+ left: -50%;
241
+ width: 200%;
242
+ height: 200%;
243
+ background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
244
+ animation: pulse 4s ease-in-out infinite;
245
+ }
246
+
247
+ @keyframes pulse {
248
+ 0%, 100% { transform: scale(1); opacity: 0.5; }
249
+ 50% { transform: scale(1.1); opacity: 0.8; }
250
  }
251
 
252
  .header h1 {
253
+ font-size: 3em;
254
  margin-bottom: 10px;
255
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
256
+ position: relative;
257
+ z-index: 1;
258
  }
259
 
260
+ .header p {
261
+ font-size: 1.2em;
262
+ opacity: 0.95;
263
+ position: relative;
264
+ z-index: 1;
265
+ }
266
+
267
+ .gpu-status {
268
+ position: absolute;
269
+ top: 10px;
270
+ right: 10px;
271
+ background: rgba(0,0,0,0.3);
272
+ padding: 5px 15px;
273
+ border-radius: 20px;
274
+ font-size: 0.8em;
275
+ }
276
+
277
+ .main-content {
278
+ background: rgba(255, 255, 255, 0.95);
279
+ border-radius: 20px;
280
+ padding: 30px;
281
+ box-shadow: 0 5px 20px rgba(0,0,0,0.1);
282
+ backdrop-filter: blur(10px);
283
+ }
284
+
285
+ .input-section {
286
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
287
+ padding: 25px;
288
+ border-radius: 15px;
289
+ margin-bottom: 20px;
290
  }
291
 
292
  .generate-btn {
293
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
294
  color: white;
295
+ font-size: 1.3em;
296
+ padding: 15px 40px;
297
+ border-radius: 30px;
298
  border: none;
299
  cursor: pointer;
300
+ transition: all 0.3s ease;
301
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
302
  width: 100%;
303
+ margin-top: 20px;
304
  }
305
 
306
  .generate-btn:hover {
307
  transform: translateY(-2px);
308
+ box-shadow: 0 7px 20px rgba(102, 126, 234, 0.6);
309
+ }
310
+
311
+ .generate-btn:active {
312
+ transform: translateY(0);
313
+ }
314
+
315
+ .video-output {
316
+ background: #f8f9fa;
317
+ padding: 20px;
318
+ border-radius: 15px;
319
+ text-align: center;
320
+ min-height: 400px;
321
+ display: flex;
322
+ align-items: center;
323
+ justify-content: center;
324
+ }
325
+
326
+ .accordion {
327
+ background: rgba(255, 255, 255, 0.7);
328
+ border-radius: 10px;
329
+ margin-top: 15px;
330
+ padding: 15px;
331
+ }
332
+
333
+ .slider-container {
334
+ background: rgba(255, 255, 255, 0.5);
335
+ padding: 15px;
336
+ border-radius: 10px;
337
+ margin: 10px 0;
338
+ }
339
+
340
+ body {
341
+ background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
342
+ background-size: 400% 400%;
343
+ animation: gradient 15s ease infinite;
344
+ }
345
+
346
+ @keyframes gradient {
347
+ 0% { background-position: 0% 50%; }
348
+ 50% { background-position: 100% 50%; }
349
+ 100% { background-position: 0% 50%; }
350
+ }
351
+
352
+ .warning-box {
353
+ background: rgba(255, 193, 7, 0.1);
354
+ border: 1px solid rgba(255, 193, 7, 0.3);
355
+ border-radius: 10px;
356
+ padding: 15px;
357
+ margin: 10px 0;
358
+ color: #856404;
359
+ font-size: 0.9em;
360
+ }
361
+
362
+ .info-box {
363
+ background: rgba(52, 152, 219, 0.1);
364
+ border: 1px solid rgba(52, 152, 219, 0.3);
365
+ border-radius: 10px;
366
+ padding: 15px;
367
+ margin: 10px 0;
368
+ color: #2c5282;
369
+ font-size: 0.9em;
370
+ }
371
+
372
+ .footer {
373
+ text-align: center;
374
+ margin-top: 30px;
375
+ color: #666;
376
+ font-size: 0.9em;
377
  }
378
  """
379
 
380
+ # Gradio UI (๊ธฐ์กด ๊ตฌ์กฐ ์œ ์ง€)
381
  with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
382
  with gr.Column(elem_classes="container"):
383
+ # Header with GPU status
384
  gr.HTML("""
385
  <div class="header">
386
+ <h1>๐ŸŽฌ AI Video Magic Studio</h1>
387
+ <p>Transform your images into captivating videos with Wan 2.1 + CausVid LoRA</p>
388
+ <div class="gpu-status">๐Ÿ–ฅ๏ธ Zero GPU Optimized</div>
389
  </div>
390
  """)
391
 
392
+ # GPU ๋ฉ”๋ชจ๋ฆฌ ๊ฒฝ๊ณ 
393
+ if hasattr(spaces, 'GPU'):
394
+ gr.HTML("""
395
+ <div class="warning-box">
396
+ <strong>๐Ÿ’ก Zero GPU Performance Tips:</strong>
397
+ <ul style="margin: 5px 0; padding-left: 20px;">
398
+ <li>Maximum duration: 2.5 seconds</li>
399
+ <li>Maximum resolution: 640ร—640 pixels</li>
400
+ <li>Recommended: 512ร—512 at 2 seconds</li>
401
+ <li>Use 4-6 steps for optimal speed/quality balance</li>
402
+ <li>Processing time: ~60-90 seconds</li>
403
+ </ul>
404
+ </div>
405
+ """)
406
+
407
+ # ์ •๋ณด ๋ฐ•์Šค
408
  gr.HTML("""
409
+ <div class="info-box">
410
+ <strong>๐ŸŽฏ Quick Start Guide:</strong>
411
+ <ol style="margin: 5px 0; padding-left: 20px;">
412
+ <li>Upload your image - AI will calculate optimal dimensions</li>
413
+ <li>Enter a creative prompt or use the default</li>
414
+ <li>Adjust duration (2s recommended for best results)</li>
415
+ <li>Click Generate and wait for completion</li>
416
+ </ol>
 
417
  </div>
418
  """)
419
 
420
+ with gr.Row(elem_classes="main-content"):
421
  with gr.Column(scale=1):
422
+ gr.Markdown("### ๐Ÿ“ธ Input Settings")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
 
424
+ with gr.Column(elem_classes="input-section"):
425
+ input_image = gr.Image(
426
+ type="pil",
427
+ label="๐Ÿ–ผ๏ธ Upload Your Image",
428
+ elem_classes="image-upload"
429
  )
430
 
431
+ prompt_input = gr.Textbox(
432
+ label="โœจ Animation Prompt",
433
+ value=default_prompt_i2v,
434
+ placeholder="Describe how you want your image to move...",
435
+ lines=2
436
+ )
 
 
 
 
 
 
 
 
 
437
 
438
+ duration_input = gr.Slider(
439
+ minimum=round(MIN_FRAMES_MODEL/FIXED_FPS, 1),
440
+ maximum=round(MAX_FRAMES_MODEL/FIXED_FPS, 1) if not hasattr(spaces, 'GPU') else 2.5,
441
+ step=0.1,
442
  value=2,
443
+ label=f"โฑ๏ธ Video Duration (seconds) - Clamped to {MIN_FRAMES_MODEL}-{MAX_FRAMES_MODEL} frames at {FIXED_FPS}fps",
444
+ elem_classes="slider-container"
445
+ )
446
+
447
+ with gr.Accordion("๐ŸŽ›๏ธ Advanced Settings", open=False, elem_classes="accordion"):
448
+ negative_prompt = gr.Textbox(
449
+ label="๐Ÿšซ Negative Prompt",
450
+ value=default_negative_prompt,
451
+ lines=3
452
  )
453
 
454
  with gr.Row():
 
457
  maximum=MAX_SEED,
458
  step=1,
459
  value=42,
460
+ label="๐ŸŽฒ Seed"
461
  )
462
  randomize_seed = gr.Checkbox(
463
+ label="๐Ÿ”€ Randomize",
464
  value=True
465
  )
466
 
467
+ with gr.Row():
468
+ height_slider = gr.Slider(
469
+ minimum=SLIDER_MIN_H,
470
+ maximum=SLIDER_MAX_H if not hasattr(spaces, 'GPU') else 640,
471
+ step=MOD_VALUE,
472
+ value=DEFAULT_H_SLIDER_VALUE,
473
+ label=f"๐Ÿ“ Height (multiple of {MOD_VALUE})"
474
+ )
475
+ width_slider = gr.Slider(
476
+ minimum=SLIDER_MIN_W,
477
+ maximum=SLIDER_MAX_W if not hasattr(spaces, 'GPU') else 640,
478
+ step=MOD_VALUE,
479
+ value=DEFAULT_W_SLIDER_VALUE,
480
+ label=f"๐Ÿ“ Width (multiple of {MOD_VALUE})"
481
+ )
482
+
483
+ steps_slider = gr.Slider(
484
+ minimum=1,
485
+ maximum=30 if not hasattr(spaces, 'GPU') else 8,
486
+ step=1,
487
+ value=4,
488
+ label="๐Ÿ”ง Quality Steps (4-6 recommended)"
489
+ )
490
+
491
  guidance_scale = gr.Slider(
492
  minimum=0.0,
493
+ maximum=20.0,
494
  step=0.5,
495
  value=1.0,
496
+ label="๐ŸŽฏ Guidance Scale",
497
  visible=False
498
  )
499
 
 
504
  )
505
 
506
  with gr.Column(scale=1):
507
+ gr.Markdown("### ๐ŸŽฅ Generated Video")
508
  video_output = gr.Video(
509
+ label="",
510
+ autoplay=True,
511
+ elem_classes="video-output"
512
  )
513
 
514
+ gr.HTML("""
515
+ <div class="footer">
516
+ <p>๐Ÿ’ก Tip: For best results, use clear images with good lighting and distinct subjects</p>
517
+ </div>
 
 
 
 
 
 
 
 
 
518
  """)
519
 
520
+ # Examples
521
+ gr.Examples(
522
+ examples=[
523
+ ["peng.png", "a penguin playfully dancing in the snow, Antarctica", 512, 512],
524
+ ["forg.jpg", "the frog jumps around", 448, 576],
 
 
 
 
 
 
 
 
525
  ],
526
+ inputs=[input_image, prompt_input, height_slider, width_slider],
527
+ outputs=[video_output, seed],
528
+ fn=generate_video,
529
+ cache_examples=False # ์บ์‹œ ๋น„ํ™œ์„ฑํ™”๋กœ ๋ฉ”๋ชจ๋ฆฌ ์ ˆ์•ฝ
530
  )
531
 
532
+ # ๊ฐœ์„ ์‚ฌํ•ญ ์š”์•ฝ
533
+ gr.HTML("""
534
+ <div style="background: rgba(255,255,255,0.9); border-radius: 10px; padding: 15px; margin-top: 20px; font-size: 0.8em; text-align: center;">
535
+ <p style="margin: 0; color: #666;">
536
+ <strong style="color: #667eea;">Powered by:</strong>
537
+ Wan 2.1 I2V (14B) + CausVid LoRA โ€ข ๐Ÿš€ 4-8 steps fast inference โ€ข ๐ŸŽฌ Up to 81 frames
538
+ </p>
539
+ </div>
540
+ """)
541
+
542
+ # Event handlers
543
+ input_image.upload(
544
+ fn=handle_image_upload_for_dims_wan,
545
+ inputs=[input_image, height_slider, width_slider],
546
+ outputs=[height_slider, width_slider]
547
+ )
548
+
549
+ input_image.clear(
550
+ fn=handle_image_upload_for_dims_wan,
551
+ inputs=[input_image, height_slider, width_slider],
552
+ outputs=[height_slider, width_slider]
553
+ )
554
+
555
+ generate_btn.click(
556
+ fn=generate_video,
557
+ inputs=[
558
+ input_image, prompt_input, height_slider, width_slider,
559
+ negative_prompt, duration_input, guidance_scale,
560
+ steps_slider, seed, randomize_seed
561
+ ],
562
+ outputs=[video_output, seed]
563
+ )
564
+
565
  if __name__ == "__main__":
566
+ demo.queue().launch()