seawolf2357 commited on
Commit
c68ae83
ยท
verified ยท
1 Parent(s): 323d2ce

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +267 -601
app.py CHANGED
@@ -20,7 +20,7 @@ import threading
20
  import os
21
 
22
  # GPU ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ์„ค์ •
23
- os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'max_split_size_mb:512'
24
 
25
  # ๋กœ๊น… ์„ค์ •
26
  logging.basicConfig(level=logging.INFO)
@@ -33,18 +33,19 @@ class VideoGenerationConfig:
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
- default_height: int = 512
37
- default_width: int = 512 # Zero GPU ํ™˜๊ฒฝ์„ ์œ„ํ•ด ๊ธฐ๋ณธ๊ฐ’ ์ˆ˜์ •
38
- max_area: float = 480.0 * 832.0
 
39
  slider_min_h: int = 128
40
- slider_max_h: int = 832 # Zero GPU ํ™˜๊ฒฝ์„ ์œ„ํ•ด ์ˆ˜์ •
41
  slider_min_w: int = 128
42
- slider_max_w: int = 832 # Zero GPU ํ™˜๊ฒฝ์„ ์œ„ํ•ด ์ˆ˜์ •
43
  fixed_fps: int = 24
44
  min_frames: int = 8
45
- max_frames: int = 81
46
- default_prompt: str = "make this image come alive, cinematic motion, smooth animation"
47
- default_negative_prompt: str = "static, blurred, low quality, watermark, text"
48
  # GPU ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™” ์„ค์ •
49
  enable_model_cpu_offload: bool = True
50
  enable_vae_slicing: bool = True
@@ -63,7 +64,8 @@ class VideoGenerationConfig:
63
  config = VideoGenerationConfig()
64
  MAX_SEED = np.iinfo(np.int32).max
65
 
66
- # ๊ธ€๋กœ๋ฒŒ ๋ฝ (๋™์‹œ ์‹คํ–‰ ๋ฐฉ์ง€)
 
67
  generation_lock = threading.Lock()
68
 
69
  # ์„ฑ๋Šฅ ์ธก์ • ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ
@@ -78,175 +80,19 @@ def measure_time(func):
78
 
79
  # GPU ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ ํ•จ์ˆ˜
80
  def clear_gpu_memory():
81
- """๊ฐ•๋ ฅํ•œ GPU ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ"""
82
- # Zero GPU ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์—์„œ CUDA ์ดˆ๊ธฐํ™” ๋ฐฉ์ง€
83
- if hasattr(spaces, 'GPU'):
84
- # Zero GPU ํ™˜๊ฒฝ์—์„œ๋Š” @spaces.GPU ๋‚ด์—์„œ๋งŒ GPU ์ž‘์—… ์ˆ˜ํ–‰
85
- gc.collect()
86
- return
87
-
88
  if torch.cuda.is_available():
89
  try:
90
  torch.cuda.empty_cache()
91
- torch.cuda.ipc_collect()
92
- gc.collect()
93
-
94
- # GPU ๋ฉ”๋ชจ๋ฆฌ ์ƒํƒœ ๋กœ๊น…
95
- allocated = torch.cuda.memory_allocated() / 1024**3
96
- reserved = torch.cuda.memory_reserved() / 1024**3
97
- logger.info(f"GPU Memory - Allocated: {allocated:.2f}GB, Reserved: {reserved:.2f}GB")
98
- except Exception as e:
99
- logger.warning(f"GPU memory clear failed: {e}")
100
- gc.collect()
101
-
102
- # ๋ชจ๋ธ ๊ด€๋ฆฌ์ž (์‹ฑ๊ธ€ํ†ค ํŒจํ„ด)
103
- class ModelManager:
104
- _instance = None
105
- _lock = threading.Lock()
106
-
107
- def __new__(cls):
108
- if cls._instance is None:
109
- with cls._lock:
110
- if cls._instance is None:
111
- cls._instance = super().__new__(cls)
112
- return cls._instance
113
-
114
- def __init__(self):
115
- if not hasattr(self, '_initialized'):
116
- self._pipe = None
117
- self._is_loaded = False
118
- self._initialized = True
119
-
120
- @property
121
- def pipe(self):
122
- if not self._is_loaded:
123
- self._load_model()
124
- return self._pipe
125
-
126
- @measure_time
127
- def _load_model(self):
128
- """๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ์ธ ๋ชจ๋ธ ๋กœ๋”ฉ"""
129
- with self._lock:
130
- if self._is_loaded:
131
- return
132
-
133
- try:
134
- logger.info("Loading model with memory optimizations...")
135
- clear_gpu_memory()
136
-
137
- # ๋ชจ๋ธ ์ปดํฌ๋„ŒํŠธ ๋กœ๋“œ (๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ) - autocast ์ˆ˜์ •
138
- if torch.cuda.is_available() and not hasattr(spaces, 'GPU'):
139
- # ์ผ๋ฐ˜ GPU ํ™˜๊ฒฝ
140
- with torch.amp.autocast('cuda', enabled=False): # ์ˆ˜์ •๋œ ๋ถ€๋ถ„
141
- image_encoder = CLIPVisionModel.from_pretrained(
142
- config.model_id,
143
- subfolder="image_encoder",
144
- torch_dtype=torch.float16,
145
- low_cpu_mem_usage=True
146
- )
147
-
148
- vae = AutoencoderKLWan.from_pretrained(
149
- config.model_id,
150
- subfolder="vae",
151
- torch_dtype=torch.float16,
152
- low_cpu_mem_usage=True
153
- )
154
- else:
155
- # CPU ํ™˜๊ฒฝ ๋˜๋Š” Zero GPU ํ™˜๊ฒฝ
156
- image_encoder = CLIPVisionModel.from_pretrained(
157
- config.model_id,
158
- subfolder="image_encoder",
159
- torch_dtype=torch.float16 if hasattr(spaces, 'GPU') else torch.float32,
160
- low_cpu_mem_usage=True
161
- )
162
-
163
- vae = AutoencoderKLWan.from_pretrained(
164
- config.model_id,
165
- subfolder="vae",
166
- torch_dtype=torch.float16 if hasattr(spaces, 'GPU') else torch.float32,
167
- low_cpu_mem_usage=True
168
- )
169
-
170
- self._pipe = WanImageToVideoPipeline.from_pretrained(
171
- config.model_id,
172
- vae=vae,
173
- image_encoder=image_encoder,
174
- torch_dtype=torch.bfloat16 if (torch.cuda.is_available() or hasattr(spaces, 'GPU')) else torch.float32,
175
- low_cpu_mem_usage=True,
176
- use_safetensors=True
177
- )
178
-
179
- # ์Šค์ผ€์ค„๋Ÿฌ ์„ค์ •
180
- self._pipe.scheduler = UniPCMultistepScheduler.from_config(
181
- self._pipe.scheduler.config, flow_shift=8.0
182
- )
183
-
184
- # LoRA ๋กœ๋“œ
185
- try:
186
- causvid_path = hf_hub_download(
187
- repo_id=config.lora_repo_id, filename=config.lora_filename
188
- )
189
- self._pipe.load_lora_weights(causvid_path, adapter_name="causvid_lora")
190
- self._pipe.set_adapters(["causvid_lora"], adapter_weights=[0.95])
191
- self._pipe.fuse_lora()
192
- logger.info("LoRA weights loaded successfully")
193
- except Exception as e:
194
- logger.warning(f"Failed to load LoRA weights: {e}")
195
-
196
- # GPU ์ตœ์ ํ™” ์„ค์ •
197
- if hasattr(spaces, 'GPU'): # Zero GPU ํ™˜๊ฒฝ
198
- # Zero GPU ํ™˜๊ฒฝ์—์„œ๋Š” ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋จ
199
- logger.info("Model loaded for Zero GPU environment")
200
- elif config.enable_model_cpu_offload and torch.cuda.is_available():
201
- self._pipe.enable_model_cpu_offload()
202
- logger.info("CPU offload enabled")
203
- elif torch.cuda.is_available():
204
- self._pipe.to("cuda")
205
- logger.info("Model moved to CUDA")
206
- else:
207
- logger.info("Running on CPU")
208
-
209
- if config.enable_vae_slicing:
210
- self._pipe.enable_vae_slicing()
211
-
212
- if config.enable_vae_tiling:
213
- self._pipe.enable_vae_tiling()
214
-
215
- # xFormers ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ์ธ attention ํ™œ์„ฑํ™” (๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ)
216
- try:
217
- self._pipe.enable_xformers_memory_efficient_attention()
218
- logger.info("xFormers memory efficient attention enabled")
219
- except:
220
- logger.info("xFormers not available, using default attention")
221
-
222
- self._is_loaded = True
223
- logger.info("Model loaded successfully with optimizations")
224
- clear_gpu_memory()
225
-
226
- except Exception as e:
227
- logger.error(f"Error loading model: {e}")
228
- self._is_loaded = False
229
- clear_gpu_memory()
230
- raise
231
-
232
- def unload_model(self):
233
- """๋ชจ๋ธ ์–ธ๋กœ๋“œ ๋ฐ ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ"""
234
- with self._lock:
235
- if self._pipe is not None:
236
- del self._pipe
237
- self._pipe = None
238
- self._is_loaded = False
239
- clear_gpu_memory()
240
- logger.info("Model unloaded and memory cleared")
241
-
242
- # ์‹ฑ๊ธ€ํ†ค ์ธ์Šคํ„ด์Šค
243
- model_manager = ModelManager()
244
 
245
  # ๋น„๋””์˜ค ์ƒ์„ฑ๊ธฐ ํด๋ž˜์Šค
246
  class VideoGenerator:
247
- def __init__(self, config: VideoGenerationConfig, model_manager: ModelManager):
248
  self.config = config
249
- self.model_manager = model_manager
250
 
251
  def calculate_dimensions(self, image: Image.Image) -> Tuple[int, int]:
252
  orig_w, orig_h = image.size
@@ -255,11 +101,8 @@ class VideoGenerator:
255
 
256
  aspect_ratio = orig_h / orig_w
257
 
258
- # Zero GPU ํ™˜๊ฒฝ์—์„œ๋Š” ๋” ์ž‘์€ max_area ์‚ฌ์šฉ
259
- if hasattr(spaces, 'GPU'):
260
- max_area = 640.0 * 640.0 # 409,600 pixels
261
- else:
262
- max_area = self.config.max_area
263
 
264
  calc_h = round(np.sqrt(max_area * aspect_ratio))
265
  calc_w = round(np.sqrt(max_area / aspect_ratio))
@@ -267,16 +110,13 @@ class VideoGenerator:
267
  calc_h = max(self.config.mod_value, (calc_h // self.config.mod_value) * self.config.mod_value)
268
  calc_w = max(self.config.mod_value, (calc_w // self.config.mod_value) * self.config.mod_value)
269
 
270
- # Zero GPU ํ™˜๊ฒฝ์—์„œ ์ถ”๊ฐ€ ์ œํ•œ
271
- if hasattr(spaces, 'GPU'):
272
- max_dim = 832
273
- new_h = int(np.clip(calc_h, self.config.slider_min_h, min(max_dim, self.config.slider_max_h)))
274
- new_w = int(np.clip(calc_w, self.config.slider_min_w, min(max_dim, self.config.slider_max_w)))
275
- else:
276
- new_h = int(np.clip(calc_h, self.config.slider_min_h,
277
- (self.config.slider_max_h // self.config.mod_value) * self.config.mod_value))
278
- new_w = int(np.clip(calc_w, self.config.slider_min_w,
279
- (self.config.slider_max_w // self.config.mod_value) * self.config.mod_value))
280
 
281
  return new_h, new_w
282
 
@@ -288,43 +128,26 @@ class VideoGenerator:
288
  if not prompt or len(prompt.strip()) == 0:
289
  return False, "โœ๏ธ Please provide a prompt"
290
 
291
- if len(prompt) > 500:
292
- return False, "โš ๏ธ Prompt is too long (max 500 characters)"
293
 
294
- # ์ •ํ™•ํ•œ duration ๋ฒ”์œ„ ์ฒดํฌ
295
- min_duration = self.config.min_duration
296
- max_duration = self.config.max_duration
297
 
298
- if duration < min_duration:
299
- return False, f"โฑ๏ธ Duration too short (min {min_duration:.1f}s)"
300
 
301
- if duration > max_duration:
302
- return False, f"โฑ๏ธ Duration too long (max {max_duration:.1f}s)"
 
 
303
 
304
- # Zero GPU ํ™˜๊ฒฝ์—์„œ๋Š” ๋” ๋ณด์ˆ˜์ ์ธ ์ œํ•œ ์ ์šฉ
305
- if hasattr(spaces, 'GPU'): # Spaces ํ™˜๊ฒฝ ์ฒดํฌ
306
- if duration > 2.5: # Zero GPU์—์„œ๋Š” 2.5์ดˆ๋กœ ์ œํ•œ
307
- return False, "โฑ๏ธ In Zero GPU environment, duration is limited to 2.5s for stability"
308
- # ํ”ฝ์…€ ์ˆ˜ ๊ธฐ๋ฐ˜ ์ œํ•œ (640x640 = 409,600 ํ”ฝ์…€)
309
- max_pixels = 640 * 640
310
- if height * width > max_pixels:
311
- return False, f"๐Ÿ“ In Zero GPU environment, total pixels limited to {max_pixels:,} (e.g., 640ร—640, 512ร—832)"
312
- if height > 832 or width > 832: # ํ•œ ๋ณ€์˜ ์ตœ๋Œ€ ๊ธธ์ด
313
- return False, "๐Ÿ“ In Zero GPU environment, maximum dimension is 832 pixels"
314
 
315
- # GPU ๋ฉ”๋ชจ๋ฆฌ ์ฒดํฌ (Zero GPU ํ™˜๊ฒฝ์ด ์•„๋‹ ๋•Œ๋งŒ)
316
- if torch.cuda.is_available() and not hasattr(spaces, 'GPU'):
317
- try:
318
- free_memory = torch.cuda.get_device_properties(0).total_memory - torch.cuda.memory_allocated()
319
- required_memory = (height * width * 3 * 8 * duration * self.config.fixed_fps) / (1024**3)
320
- if free_memory < required_memory * 2:
321
- clear_gpu_memory()
322
- # ์žฌํ™•์ธ
323
- free_memory = torch.cuda.get_device_properties(0).total_memory - torch.cuda.memory_allocated()
324
- if free_memory < required_memory * 1.5:
325
- return False, "โš ๏ธ Not enough GPU memory. Try smaller dimensions or shorter duration."
326
- except Exception as e:
327
- logger.warning(f"GPU memory check failed: {e}")
328
 
329
  return True, None
330
 
@@ -334,7 +157,7 @@ class VideoGenerator:
334
  hash_obj = hashlib.md5(unique_str.encode())
335
  return f"video_{hash_obj.hexdigest()[:8]}.mp4"
336
 
337
- video_generator = VideoGenerator(config, model_manager)
338
 
339
  # Gradio ํ•จ์ˆ˜๋“ค
340
  def handle_image_upload(image):
@@ -355,53 +178,40 @@ def handle_image_upload(image):
355
 
356
  def get_duration(input_image, prompt, height, width, negative_prompt,
357
  duration_seconds, guidance_scale, steps, seed, randomize_seed, progress):
358
- # Zero GPU ํ™˜๊ฒฝ์—์„œ๋Š” ๋” ๋ณด์ˆ˜์ ์ธ ์‹œ๊ฐ„ ํ• ๋‹น
359
- base_duration = 60
360
-
361
- # ๋‹จ๊ณ„๋ณ„ ์ถ”๊ฐ€ ์‹œ๊ฐ„
362
- if steps > 8:
363
- base_duration += 30
364
- elif steps > 4:
365
- base_duration += 15
366
 
367
- # Duration๋ณ„ ์ถ”๊ฐ€ ์‹œ๊ฐ„
368
- if duration_seconds > 2:
 
369
  base_duration += 20
370
- elif duration_seconds > 1.5:
371
  base_duration += 10
372
 
373
- # ํ•ด์ƒ๋„๋ณ„ ์ถ”๊ฐ€ ์‹œ๊ฐ„ (ํ”ฝ์…€ ์ˆ˜ ๊ธฐ๋ฐ˜)
374
- pixels = height * width
375
- if pixels > 400000: # 640x640 ๊ทผ์ฒ˜
376
- base_duration += 20
377
- elif pixels > 250000: # 512x512 ๊ทผ์ฒ˜
378
  base_duration += 10
379
 
380
- # Zero GPU ํ™˜๊ฒฝ์—์„œ๋Š” ์ตœ๋Œ€ 90์ดˆ๋กœ ์ œํ•œ
381
- return min(base_duration, 90)
382
 
383
  @spaces.GPU(duration=get_duration)
384
  @measure_time
385
  def generate_video(input_image, prompt, height, width,
386
  negative_prompt=config.default_negative_prompt,
387
- duration_seconds=1.5, guidance_scale=1, steps=4,
388
  seed=42, randomize_seed=False,
389
  progress=gr.Progress(track_tqdm=True)):
390
 
 
 
391
  # ๋™์‹œ ์‹คํ–‰ ๋ฐฉ์ง€
392
  if not generation_lock.acquire(blocking=False):
393
  raise gr.Error("โณ Another video is being generated. Please wait...")
394
 
395
  try:
396
- # Zero GPU ํ™˜๊ฒฝ์—์„œ๋Š” ์ด์ œ GPU ์‚ฌ์šฉ ๊ฐ€๋Šฅ
397
- if hasattr(spaces, 'GPU') and torch.cuda.is_available():
398
- logger.info("GPU initialized in Zero GPU environment")
399
-
400
- progress(0.1, desc="๐Ÿ” Validating inputs...")
401
-
402
- # Zero GPU ํ™˜๊ฒฝ์—์„œ ์ถ”๊ฐ€ ๊ฒ€์ฆ
403
- if hasattr(spaces, 'GPU'):
404
- logger.info(f"Zero GPU environment detected. Duration: {duration_seconds}s, Resolution: {height}x{width}, Pixels: {height*width:,}")
405
 
406
  # ์ž…๋ ฅ ๊ฒ€์ฆ
407
  is_valid, error_msg = video_generator.validate_inputs(
@@ -413,73 +223,117 @@ def generate_video(input_image, prompt, height, width,
413
  # ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
414
  clear_gpu_memory()
415
 
416
- progress(0.2, desc="๐ŸŽฏ Preparing image...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  target_h = max(config.mod_value, (int(height) // config.mod_value) * config.mod_value)
418
  target_w = max(config.mod_value, (int(width) // config.mod_value) * config.mod_value)
419
 
420
- # ํ”„๋ ˆ์ž„ ์ˆ˜ ๊ณ„์‚ฐ (Zero GPU ํ™˜๊ฒฝ์—์„œ ์ถ”๊ฐ€ ์ œํ•œ)
421
- max_allowed_frames = int(2.5 * config.fixed_fps) if hasattr(spaces, 'GPU') else config.max_frames
422
  num_frames = min(
423
  int(round(duration_seconds * config.fixed_fps)),
424
- max_allowed_frames
425
  )
426
- num_frames = np.clip(num_frames, config.min_frames, max_allowed_frames)
427
 
428
  current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
429
 
430
- # ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ (๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ )
431
  resized_image = input_image.resize((target_w, target_h), Image.Resampling.LANCZOS)
432
 
433
- progress(0.3, desc="๐ŸŽจ Loading model...")
434
- pipe = model_manager.pipe
435
 
436
- progress(0.4, desc="๐ŸŽฌ Generating video frames...")
437
-
438
- # ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ์ธ ์ƒ์„ฑ
439
- device = "cuda" if torch.cuda.is_available() else "cpu"
440
-
441
- if device == "cuda":
442
- with torch.inference_mode(), torch.amp.autocast('cuda', enabled=True): # ์ˆ˜์ •๋œ ๋ถ€๋ถ„
443
- try:
444
- output_frames_list = pipe(
445
- image=resized_image,
446
- prompt=prompt,
447
- negative_prompt=negative_prompt,
448
- height=target_h,
449
- width=target_w,
450
- num_frames=num_frames,
451
- guidance_scale=float(guidance_scale),
452
- num_inference_steps=int(steps),
453
- generator=torch.Generator(device="cuda").manual_seed(current_seed),
454
- return_dict=True
455
- ).frames[0]
456
- except torch.cuda.OutOfMemoryError:
457
- clear_gpu_memory()
458
- raise gr.Error("๐Ÿ’พ GPU out of memory. Try smaller dimensions or shorter duration.")
459
- except Exception as e:
460
- logger.error(f"Generation error: {e}")
461
- raise gr.Error(f"โŒ Generation failed: {str(e)}")
462
- else:
463
- # CPU ํ™˜๊ฒฝ
464
- with torch.inference_mode():
465
- try:
466
- output_frames_list = pipe(
467
- image=resized_image,
468
- prompt=prompt,
469
- negative_prompt=negative_prompt,
470
- height=target_h,
471
- width=target_w,
472
- num_frames=num_frames,
473
- guidance_scale=float(guidance_scale),
474
- num_inference_steps=int(steps),
475
- generator=torch.Generator().manual_seed(current_seed),
476
- return_dict=True
477
- ).frames[0]
478
- except Exception as e:
479
- logger.error(f"Generation error: {e}")
480
- raise gr.Error(f"โŒ Generation failed: {str(e)}")
481
 
482
  progress(0.9, desc="๐Ÿ’พ Saving video...")
 
 
483
  filename = video_generator.generate_unique_filename(current_seed)
484
  with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmpfile:
485
  video_path = tmpfile.name
@@ -487,325 +341,173 @@ def generate_video(input_image, prompt, height, width,
487
  export_to_video(output_frames_list, video_path, fps=config.fixed_fps)
488
 
489
  progress(1.0, desc="โœจ Complete!")
490
- logger.info(f"Video generated successfully: {num_frames} frames, {target_h}x{target_w}")
491
 
492
- # ์„ฑ๊ณต ์ •๋ณด ๋ฐ˜ํ™˜
493
- info_text = f"โœ… Generated {num_frames} frames at {target_h}x{target_w} with seed {current_seed}"
494
- gr.Info(info_text)
 
495
 
496
  return video_path, current_seed
497
 
498
  except gr.Error:
499
- # Gradio ์—๋Ÿฌ๋Š” ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ
500
  raise
501
  except Exception as e:
502
  logger.error(f"Unexpected error: {e}")
503
- raise gr.Error(f"โŒ Unexpected error: {str(e)}")
504
 
505
  finally:
506
- # ํ•ญ์ƒ ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ ๋ฐ ๋ฝ ํ•ด์ œ
507
  generation_lock.release()
508
-
509
- # ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
510
- if 'output_frames_list' in locals():
511
- del output_frames_list
512
- if 'resized_image' in locals():
513
- del resized_image
514
-
515
  clear_gpu_memory()
516
 
517
- # ๊ฐœ์„ ๋œ CSS ์Šคํƒ€์ผ
518
  css = """
519
  .container {
520
- max-width: 1200px;
521
  margin: auto;
522
  padding: 20px;
523
  }
524
 
525
  .header {
526
  text-align: center;
527
- margin-bottom: 30px;
528
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
529
- padding: 40px;
530
- border-radius: 20px;
531
  color: white;
532
- box-shadow: 0 10px 30px rgba(0,0,0,0.2);
533
- position: relative;
534
- overflow: hidden;
535
- }
536
-
537
- .header::before {
538
- content: '';
539
- position: absolute;
540
- top: -50%;
541
- left: -50%;
542
- width: 200%;
543
- height: 200%;
544
- background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
545
- animation: pulse 4s ease-in-out infinite;
546
- }
547
-
548
- @keyframes pulse {
549
- 0%, 100% { transform: scale(1); opacity: 0.5; }
550
- 50% { transform: scale(1.1); opacity: 0.8; }
551
  }
552
 
553
  .header h1 {
554
- font-size: 3em;
555
  margin-bottom: 10px;
556
- text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
557
- position: relative;
558
- z-index: 1;
559
- }
560
-
561
- .header p {
562
- font-size: 1.2em;
563
- opacity: 0.95;
564
- position: relative;
565
- z-index: 1;
566
- }
567
-
568
- .gpu-status {
569
- position: absolute;
570
- top: 10px;
571
- right: 10px;
572
- background: rgba(0,0,0,0.3);
573
- padding: 5px 15px;
574
- border-radius: 20px;
575
- font-size: 0.8em;
576
  }
577
 
578
- .main-content {
579
- background: rgba(255, 255, 255, 0.95);
580
- border-radius: 20px;
581
- padding: 30px;
582
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
583
- backdrop-filter: blur(10px);
584
- }
585
-
586
- .input-section {
587
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
588
- padding: 25px;
589
- border-radius: 15px;
590
- margin-bottom: 20px;
591
  }
592
 
593
  .generate-btn {
594
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
595
  color: white;
596
- font-size: 1.3em;
597
- padding: 15px 40px;
598
- border-radius: 30px;
599
  border: none;
600
  cursor: pointer;
601
- transition: all 0.3s ease;
602
- box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
603
  width: 100%;
604
- margin-top: 20px;
605
  }
606
 
607
  .generate-btn:hover {
608
  transform: translateY(-2px);
609
- box-shadow: 0 7px 20px rgba(102, 126, 234, 0.6);
610
- }
611
-
612
- .generate-btn:active {
613
- transform: translateY(0);
614
- }
615
-
616
- .video-output {
617
- background: #f8f9fa;
618
- padding: 20px;
619
- border-radius: 15px;
620
- text-align: center;
621
- min-height: 400px;
622
- display: flex;
623
- align-items: center;
624
- justify-content: center;
625
- }
626
-
627
- .accordion {
628
- background: rgba(255, 255, 255, 0.7);
629
- border-radius: 10px;
630
- margin-top: 15px;
631
- padding: 15px;
632
- }
633
-
634
- .slider-container {
635
- background: rgba(255, 255, 255, 0.5);
636
- padding: 15px;
637
- border-radius: 10px;
638
- margin: 10px 0;
639
- }
640
-
641
- body {
642
- background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
643
- background-size: 400% 400%;
644
- animation: gradient 15s ease infinite;
645
- }
646
-
647
- @keyframes gradient {
648
- 0% { background-position: 0% 50%; }
649
- 50% { background-position: 100% 50%; }
650
- 100% { background-position: 0% 50%; }
651
- }
652
-
653
- .warning-box {
654
- background: rgba(255, 193, 7, 0.1);
655
- border: 1px solid rgba(255, 193, 7, 0.3);
656
- border-radius: 10px;
657
- padding: 15px;
658
- margin: 10px 0;
659
- color: #856404;
660
- font-size: 0.9em;
661
- }
662
-
663
- .info-box {
664
- background: rgba(52, 152, 219, 0.1);
665
- border: 1px solid rgba(52, 152, 219, 0.3);
666
- border-radius: 10px;
667
- padding: 15px;
668
- margin: 10px 0;
669
- color: #2c5282;
670
- font-size: 0.9em;
671
- }
672
-
673
- .footer {
674
- text-align: center;
675
- margin-top: 30px;
676
- color: #666;
677
- font-size: 0.9em;
678
- }
679
-
680
- /* ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐœ์„  */
681
- .progress-bar {
682
- background: linear-gradient(90deg, #667eea 0%, #764ba2 50%, #667eea 100%);
683
- background-size: 200% 100%;
684
- animation: loading 1.5s ease-in-out infinite;
685
- }
686
-
687
- @keyframes loading {
688
- 0% { background-position: 0% 0%; }
689
- 100% { background-position: 200% 0%; }
690
  }
691
  """
692
 
693
  # Gradio UI
694
  with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
695
  with gr.Column(elem_classes="container"):
696
- # Header with GPU status
697
  gr.HTML("""
698
  <div class="header">
699
- <h1>๐ŸŽฌ AI Video Magic Studio</h1>
700
- <p>Transform your images into captivating videos with Wan 2.1 + CausVid LoRA</p>
701
- <div class="gpu-status">๐Ÿ–ฅ๏ธ Zero GPU Optimized</div>
702
  </div>
703
  """)
704
 
705
- # GPU ๋ฉ”๋ชจ๋ฆฌ ๊ฒฝ๊ณ 
706
  gr.HTML("""
707
  <div class="warning-box">
708
- <strong>๐Ÿ’ก Zero GPU Performance Tips:</strong>
709
  <ul style="margin: 5px 0; padding-left: 20px;">
710
- <li>Maximum duration: 2.5 seconds (limited by Zero GPU)</li>
711
- <li>Maximum total pixels: 409,600 (e.g., 640ร—640, 512ร—832, 448ร—896)</li>
712
- <li>Maximum single dimension: 832 pixels</li>
713
- <li>Use 4-6 steps for optimal speed/quality balance</li>
714
- <li>Wait between generations to avoid queue errors</li>
715
  </ul>
716
  </div>
717
  """)
718
 
719
- # ์ƒˆ๋กœ์šด ์ •๋ณด ๋ฐ•์Šค ์ถ”๊ฐ€
720
- gr.HTML("""
721
- <div class="info-box">
722
- <strong>๐ŸŽฏ Quick Start Guide:</strong>
723
- <ol style="margin: 5px 0; padding-left: 20px;">
724
- <li>Upload your image - AI will calculate optimal dimensions</li>
725
- <li>Enter a creative prompt or use the default</li>
726
- <li>Adjust duration (1.5s recommended for best results)</li>
727
- <li>Click Generate and wait ~60 seconds</li>
728
- </ol>
729
- </div>
730
- """)
731
-
732
- with gr.Row(elem_classes="main-content"):
733
  with gr.Column(scale=1):
734
- gr.Markdown("### ๐Ÿ“ธ Input Settings")
 
 
 
735
 
736
- with gr.Column(elem_classes="input-section"):
737
- input_image = gr.Image(
738
- type="pil",
739
- label="๐Ÿ–ผ๏ธ Upload Your Image",
740
- elem_classes="image-upload"
741
- )
742
-
743
- prompt_input = gr.Textbox(
744
- label="โœจ Animation Prompt",
745
- value=config.default_prompt,
746
- placeholder="Describe how you want your image to move...",
747
- lines=2
748
- )
749
-
750
- duration_input = gr.Slider(
751
- minimum=round(config.min_duration, 1),
752
- maximum=2.5 if hasattr(spaces, 'GPU') else round(config.max_duration, 1), # Zero GPU ํ™˜๊ฒฝ ์ œํ•œ
753
- step=0.1,
754
- value=1.5, # ์•ˆ์ „ํ•œ ๊ธฐ๋ณธ๊ฐ’
755
- label="โฑ๏ธ Video Duration (seconds) - Limited to 2.5s in Zero GPU",
756
- elem_classes="slider-container"
757
- )
758
 
759
- with gr.Accordion("๐ŸŽ›๏ธ Advanced Settings", open=False, elem_classes="accordion"):
 
 
 
 
 
 
 
 
760
  negative_prompt = gr.Textbox(
761
- label="๐Ÿšซ Negative Prompt",
762
  value=config.default_negative_prompt,
763
- lines=2
764
  )
765
 
766
- with gr.Row():
767
- seed = gr.Slider(
768
- minimum=0,
769
- maximum=MAX_SEED,
770
- step=1,
771
- value=42,
772
- label="๐ŸŽฒ Seed"
773
- )
774
- randomize_seed = gr.Checkbox(
775
- label="๐Ÿ”€ Randomize",
776
- value=True
777
- )
778
-
779
  with gr.Row():
780
  height_slider = gr.Slider(
781
- minimum=config.slider_min_h,
782
- maximum=config.slider_max_h,
783
- step=config.mod_value,
784
- value=config.default_height,
785
- label="๐Ÿ“ Height (max 832px in Zero GPU)"
786
  )
787
  width_slider = gr.Slider(
788
- minimum=config.slider_min_w,
789
- maximum=config.slider_max_w,
790
- step=config.mod_value,
791
- value=config.default_width,
792
- label="๐Ÿ“ Width (max 832px in Zero GPU)"
793
  )
794
 
795
  steps_slider = gr.Slider(
796
  minimum=1,
797
- maximum=30,
798
  step=1,
799
- value=4,
800
- label="๐Ÿ”ง Quality Steps (4-8 recommended)"
801
  )
802
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
  guidance_scale = gr.Slider(
804
  minimum=0.0,
805
- maximum=20.0,
806
  step=0.5,
807
  value=1.0,
808
- label="๐ŸŽฏ Guidance Scale",
809
  visible=False
810
  )
811
 
@@ -816,73 +518,37 @@ with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
816
  )
817
 
818
  with gr.Column(scale=1):
819
- gr.Markdown("### ๐ŸŽฅ Generated Video")
820
  video_output = gr.Video(
821
- label="",
822
- autoplay=True,
823
- elem_classes="video-output"
824
  )
825
 
826
- gr.HTML("""
827
- <div class="footer">
828
- <p>๐Ÿ’ก Tip: For best results, use clear images with good lighting and distinct subjects</p>
829
- </div>
 
 
830
  """)
831
 
832
- # Examples - ํŒŒ์ผ๋ช… ํ™•์ธ ํ•„์š”
833
- try:
834
- gr.Examples(
835
- examples=[
836
- ["peng.png", "a penguin playfully dancing in the snow, Antarctica", 512, 512],
837
- ["forg.jpg", "the frog jumps around", 576, 320], # 16:9 aspect ratio within limits
838
- ],
839
- inputs=[input_image, prompt_input, height_slider, width_slider],
840
- outputs=[video_output, seed],
841
- fn=generate_video,
842
- cache_examples=False # ์บ์‹œ ๋น„ํ™œ์„ฑํ™”๋กœ ๋ฉ”๋ชจ๋ฆฌ ์ ˆ์•ฝ
843
- )
844
- except Exception as e:
845
- logger.warning(f"Failed to load examples: {e}")
846
-
847
- # ๊ฐœ์„ ์‚ฌํ•ญ ์š”์•ฝ (์ž‘๊ฒŒ)
848
- gr.HTML("""
849
- <div style="background: rgba(255,255,255,0.9); border-radius: 10px; padding: 15px; margin-top: 20px; font-size: 0.8em; text-align: center;">
850
- <p style="margin: 0; color: #666;">
851
- <strong style="color: #667eea;">Enhanced with:</strong>
852
- ๐Ÿ›ก๏ธ GPU Crash Protection โ€ข โšก Memory Optimization โ€ข ๐ŸŽจ Modern UI โ€ข ๐Ÿ”ง Clean Architecture
853
- </p>
854
- </div>
855
- """)
856
-
857
- # Event handlers
858
- input_image.upload(
859
- fn=handle_image_upload,
860
- inputs=[input_image],
861
- outputs=[height_slider, width_slider]
862
- )
863
-
864
- input_image.clear(
865
- fn=handle_image_upload,
866
- inputs=[input_image],
867
- outputs=[height_slider, width_slider]
868
- )
869
-
870
- generate_btn.click(
871
- fn=generate_video,
872
- inputs=[
873
- input_image, prompt_input, height_slider, width_slider,
874
- negative_prompt, duration_input, guidance_scale,
875
- steps_slider, seed, randomize_seed
876
- ],
877
- outputs=[video_output, seed]
878
- )
879
 
880
  if __name__ == "__main__":
881
- # Zero GPU ํ™˜๊ฒฝ ์ฒดํฌ ๋กœ๊น…
882
- if hasattr(spaces, 'GPU'):
883
- logger.info("Running in Zero GPU environment")
884
- else:
885
- logger.info("Running in standard environment")
886
-
887
- # ์•ฑ ์‹คํ–‰
888
  demo.launch()
 
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)
 
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 = 384
38
+ default_width: int = 384
39
+ max_area: float = 384.0 * 384.0 # Zero GPU์— ์ตœ์ ํ™”
40
  slider_min_h: int = 128
41
+ slider_max_h: int = 640 # ๋” ๋‚ฎ์€ ์ตœ๋Œ€๊ฐ’
42
  slider_min_w: int = 128
43
+ slider_max_w: int = 640 # ๋” ๋‚ฎ์€ ์ตœ๋Œ€๊ฐ’
44
  fixed_fps: int = 24
45
  min_frames: int = 8
46
+ max_frames: int = 36 # ๋” ๋‚ฎ์€ ์ตœ๋Œ€ ํ”„๋ ˆ์ž„
47
+ default_prompt: str = "make this image come alive, cinematic motion"
48
+ default_negative_prompt: str = "static, blurred, low quality"
49
  # GPU ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™” ์„ค์ •
50
  enable_model_cpu_offload: bool = True
51
  enable_vae_slicing: bool = True
 
64
  config = VideoGenerationConfig()
65
  MAX_SEED = np.iinfo(np.int32).max
66
 
67
+ # ๊ธ€๋กœ๋ฒŒ ๋ณ€์ˆ˜
68
+ pipe = None
69
  generation_lock = threading.Lock()
70
 
71
  # ์„ฑ๋Šฅ ์ธก์ • ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ
 
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
 
101
 
102
  aspect_ratio = orig_h / orig_w
103
 
104
+ # Zero GPU์— ์ตœ์ ํ™”๋œ ์ž‘์€ ํ•ด์ƒ๋„
105
+ max_area = 384.0 * 384.0
 
 
 
106
 
107
  calc_h = round(np.sqrt(max_area * aspect_ratio))
108
  calc_w = round(np.sqrt(max_area / aspect_ratio))
 
110
  calc_h = max(self.config.mod_value, (calc_h // self.config.mod_value) * self.config.mod_value)
111
  calc_w = max(self.config.mod_value, (calc_w // self.config.mod_value) * self.config.mod_value)
112
 
113
+ # ์ตœ๋Œ€ 640์œผ๋กœ ์ œํ•œ
114
+ new_h = int(np.clip(calc_h, self.config.slider_min_h, 640))
115
+ new_w = int(np.clip(calc_w, self.config.slider_min_w, 640))
116
+
117
+ # mod_value์— ๋งž์ถค
118
+ new_h = (new_h // self.config.mod_value) * self.config.mod_value
119
+ new_w = (new_w // self.config.mod_value) * self.config.mod_value
 
 
 
120
 
121
  return new_h, new_w
122
 
 
128
  if not prompt or len(prompt.strip()) == 0:
129
  return False, "โœ๏ธ Please provide a prompt"
130
 
131
+ if len(prompt) > 300: # ๋” ์งง์€ ํ”„๋กฌํ”„ํŠธ ์ œํ•œ
132
+ return False, "โš ๏ธ Prompt is too long (max 300 characters)"
133
 
134
+ # Zero GPU์— ์ตœ์ ํ™”๋œ ์ œํ•œ
135
+ if duration < 0.3:
136
+ return False, "โฑ๏ธ Duration too short (min 0.3s)"
137
 
138
+ if duration > 1.5:
139
+ return False, "โฑ๏ธ Duration too long (max 1.5s for stability)"
140
 
141
+ # ํ”ฝ์…€ ์ˆ˜ ์ œํ•œ (384x384 = 147,456 ํ”ฝ์…€)
142
+ max_pixels = 384 * 384
143
+ if height * width > max_pixels:
144
+ return False, f"๐Ÿ“ Total pixels limited to {max_pixels:,} (e.g., 384ร—384)"
145
 
146
+ if height > 640 or width > 640:
147
+ return False, "๐Ÿ“ Maximum dimension is 640 pixels"
 
 
 
 
 
 
 
 
148
 
149
+ if steps > 6:
150
+ return False, "๐Ÿ”ง Maximum 6 steps in Zero GPU environment"
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  return True, None
153
 
 
157
  hash_obj = hashlib.md5(unique_str.encode())
158
  return f"video_{hash_obj.hexdigest()[:8]}.mp4"
159
 
160
+ video_generator = VideoGenerator(config)
161
 
162
  # Gradio ํ•จ์ˆ˜๋“ค
163
  def handle_image_upload(image):
 
178
 
179
  def get_duration(input_image, prompt, height, width, negative_prompt,
180
  duration_seconds, guidance_scale, steps, seed, randomize_seed, progress):
181
+ # Zero GPU ํ™˜๊ฒฝ์—์„œ ๋งค์šฐ ๋ณด์ˆ˜์ ์ธ ์‹œ๊ฐ„ ํ• ๋‹น
182
+ base_duration = 40 # ๊ธฐ๋ณธ 40์ดˆ
 
 
 
 
 
 
183
 
184
+ # ํ”ฝ์…€ ์ˆ˜์— ๋”ฐ๋ฅธ ์ถ”๊ฐ€ ์‹œ๊ฐ„
185
+ pixels = height * width
186
+ if pixels > 200000: # 448x448 ์ด์ƒ
187
  base_duration += 20
188
+ elif pixels > 147456: # 384x384 ์ด์ƒ
189
  base_duration += 10
190
 
191
+ # ์Šคํ… ์ˆ˜์— ๋”ฐ๋ฅธ ์ถ”๊ฐ€ ์‹œ๊ฐ„
192
+ if steps > 4:
 
 
 
193
  base_duration += 10
194
 
195
+ # ์ตœ๋Œ€ 70์ดˆ๋กœ ์ œํ•œ (Zero GPU์˜ ์•ˆ์ „ํ•œ ํ•œ๊ณ„)
196
+ return min(base_duration, 70)
197
 
198
  @spaces.GPU(duration=get_duration)
199
  @measure_time
200
  def generate_video(input_image, prompt, height, width,
201
  negative_prompt=config.default_negative_prompt,
202
+ duration_seconds=1.0, guidance_scale=1, steps=3,
203
  seed=42, randomize_seed=False,
204
  progress=gr.Progress(track_tqdm=True)):
205
 
206
+ global pipe
207
+
208
  # ๋™์‹œ ์‹คํ–‰ ๋ฐฉ์ง€
209
  if not generation_lock.acquire(blocking=False):
210
  raise gr.Error("โณ Another video is being generated. Please wait...")
211
 
212
  try:
213
+ progress(0.05, desc="๐Ÿ” Validating inputs...")
214
+ logger.info(f"Starting generation - Resolution: {height}x{width}, Duration: {duration_seconds}s, Steps: {steps}")
 
 
 
 
 
 
 
215
 
216
  # ์ž…๋ ฅ ๊ฒ€์ฆ
217
  is_valid, error_msg = video_generator.validate_inputs(
 
223
  # ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
224
  clear_gpu_memory()
225
 
226
+ progress(0.1, desc="๐Ÿš€ Loading model...")
227
+
228
+ # ๋ชจ๋ธ ๋กœ๋”ฉ (GPU ํ•จ์ˆ˜ ๋‚ด์—์„œ)
229
+ if pipe is None:
230
+ try:
231
+ # ์ปดํฌ๋„ŒํŠธ ๋กœ๋“œ
232
+ image_encoder = CLIPVisionModel.from_pretrained(
233
+ config.model_id,
234
+ subfolder="image_encoder",
235
+ torch_dtype=torch.float16,
236
+ low_cpu_mem_usage=True
237
+ )
238
+
239
+ vae = AutoencoderKLWan.from_pretrained(
240
+ config.model_id,
241
+ subfolder="vae",
242
+ torch_dtype=torch.float16,
243
+ low_cpu_mem_usage=True
244
+ )
245
+
246
+ pipe = WanImageToVideoPipeline.from_pretrained(
247
+ config.model_id,
248
+ vae=vae,
249
+ image_encoder=image_encoder,
250
+ torch_dtype=torch.bfloat16,
251
+ low_cpu_mem_usage=True,
252
+ use_safetensors=True
253
+ )
254
+
255
+ # ์Šค์ผ€์ค„๋Ÿฌ ์„ค์ •
256
+ pipe.scheduler = UniPCMultistepScheduler.from_config(
257
+ pipe.scheduler.config, flow_shift=8.0
258
+ )
259
+
260
+ # LoRA ๋กœ๋“œ (์„ ํƒ์ )
261
+ try:
262
+ causvid_path = hf_hub_download(
263
+ repo_id=config.lora_repo_id, filename=config.lora_filename
264
+ )
265
+ pipe.load_lora_weights(causvid_path, adapter_name="causvid_lora")
266
+ pipe.set_adapters(["causvid_lora"], adapter_weights=[0.95])
267
+ pipe.fuse_lora()
268
+ except:
269
+ logger.warning("LoRA loading skipped")
270
+
271
+ # GPU๋กœ ์ด๋™
272
+ pipe.to("cuda")
273
+
274
+ # ์ตœ์ ํ™” ํ™œ์„ฑํ™”
275
+ pipe.enable_vae_slicing()
276
+ pipe.enable_vae_tiling()
277
+
278
+ # xFormers ์‹œ๋„
279
+ try:
280
+ pipe.enable_xformers_memory_efficient_attention()
281
+ except:
282
+ pass
283
+
284
+ logger.info("Model loaded successfully")
285
+
286
+ except Exception as e:
287
+ logger.error(f"Model loading failed: {e}")
288
+ raise gr.Error("Failed to load model")
289
+
290
+ progress(0.3, desc="๐ŸŽฏ Preparing image...")
291
+
292
+ # ์ด๋ฏธ์ง€ ์ค€๋น„
293
  target_h = max(config.mod_value, (int(height) // config.mod_value) * config.mod_value)
294
  target_w = max(config.mod_value, (int(width) // config.mod_value) * config.mod_value)
295
 
296
+ # ํ”„๋ ˆ์ž„ ์ˆ˜ ๊ณ„์‚ฐ (๋งค์šฐ ๋ณด์ˆ˜์ )
 
297
  num_frames = min(
298
  int(round(duration_seconds * config.fixed_fps)),
299
+ 36 # ์ตœ๋Œ€ 36ํ”„๋ ˆ์ž„ (1.5์ดˆ)
300
  )
301
+ num_frames = max(8, num_frames) # ์ตœ์†Œ 8ํ”„๋ ˆ์ž„
302
 
303
  current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
304
 
305
+ # ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ
306
  resized_image = input_image.resize((target_w, target_h), Image.Resampling.LANCZOS)
307
 
308
+ progress(0.4, desc="๐ŸŽฌ Generating video...")
 
309
 
310
+ # ๋น„๋””์˜ค ์ƒ์„ฑ
311
+ with torch.inference_mode(), torch.amp.autocast('cuda', enabled=True):
312
+ try:
313
+ # ์งง์€ ํƒ€์ž„์•„์›ƒ์œผ๋กœ ์ƒ์„ฑ
314
+ output_frames_list = pipe(
315
+ image=resized_image,
316
+ prompt=prompt[:200], # ํ”„๋กฌํ”„ํŠธ ๊ธธ์ด ์ œํ•œ
317
+ negative_prompt=negative_prompt[:100], # ๋„ค๊ฑฐํ‹ฐ๋ธŒ ํ”„๋กฌํ”„ํŠธ๋„ ์ œํ•œ
318
+ height=target_h,
319
+ width=target_w,
320
+ num_frames=num_frames,
321
+ guidance_scale=float(guidance_scale),
322
+ num_inference_steps=int(steps),
323
+ generator=torch.Generator(device="cuda").manual_seed(current_seed),
324
+ return_dict=True
325
+ ).frames[0]
326
+
327
+ except torch.cuda.OutOfMemoryError:
328
+ clear_gpu_memory()
329
+ raise gr.Error("๐Ÿ’พ GPU out of memory. Try smaller dimensions.")
330
+ except Exception as e:
331
+ logger.error(f"Generation error: {e}")
332
+ raise gr.Error(f"โŒ Generation failed: {str(e)[:100]}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
 
334
  progress(0.9, desc="๐Ÿ’พ Saving video...")
335
+
336
+ # ๋น„๋””์˜ค ์ €์žฅ
337
  filename = video_generator.generate_unique_filename(current_seed)
338
  with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmpfile:
339
  video_path = tmpfile.name
 
341
  export_to_video(output_frames_list, video_path, fps=config.fixed_fps)
342
 
343
  progress(1.0, desc="โœจ Complete!")
344
+ logger.info(f"Video generated: {num_frames} frames, {target_h}x{target_w}")
345
 
346
+ # ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
347
+ del output_frames_list
348
+ del resized_image
349
+ clear_gpu_memory()
350
 
351
  return video_path, current_seed
352
 
353
  except gr.Error:
 
354
  raise
355
  except Exception as e:
356
  logger.error(f"Unexpected error: {e}")
357
+ raise gr.Error(f"โŒ Error: {str(e)[:100]}")
358
 
359
  finally:
 
360
  generation_lock.release()
 
 
 
 
 
 
 
361
  clear_gpu_memory()
362
 
363
+ # CSS
364
  css = """
365
  .container {
366
+ max-width: 1000px;
367
  margin: auto;
368
  padding: 20px;
369
  }
370
 
371
  .header {
372
  text-align: center;
373
+ margin-bottom: 20px;
374
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
375
+ padding: 30px;
376
+ border-radius: 15px;
377
  color: white;
378
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  }
380
 
381
  .header h1 {
382
+ font-size: 2.5em;
383
  margin-bottom: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  }
385
 
386
+ .warning-box {
387
+ background: #fff3cd;
388
+ border: 1px solid #ffeaa7;
389
+ border-radius: 8px;
390
+ padding: 12px;
391
+ margin: 10px 0;
392
+ color: #856404;
393
+ font-size: 0.9em;
 
 
 
 
 
394
  }
395
 
396
  .generate-btn {
397
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
398
  color: white;
399
+ font-size: 1.2em;
400
+ padding: 12px 30px;
401
+ border-radius: 25px;
402
  border: none;
403
  cursor: pointer;
 
 
404
  width: 100%;
405
+ margin-top: 15px;
406
  }
407
 
408
  .generate-btn:hover {
409
  transform: translateY(-2px);
410
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  }
412
  """
413
 
414
  # Gradio UI
415
  with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
416
  with gr.Column(elem_classes="container"):
417
+ # Header
418
  gr.HTML("""
419
  <div class="header">
420
+ <h1>๐ŸŽฌ AI Video Generator</h1>
421
+ <p>Transform images into videos with Wan 2.1 (Zero GPU Optimized)</p>
 
422
  </div>
423
  """)
424
 
425
+ # ๊ฒฝ๊ณ 
426
  gr.HTML("""
427
  <div class="warning-box">
428
+ <strong>โšก Zero GPU Limitations:</strong>
429
  <ul style="margin: 5px 0; padding-left: 20px;">
430
+ <li>Max resolution: 384ร—384 (recommended)</li>
431
+ <li>Max duration: 1.5 seconds</li>
432
+ <li>Max steps: 6 (3-4 recommended)</li>
433
+ <li>Processing time: ~40-60 seconds</li>
 
434
  </ul>
435
  </div>
436
  """)
437
 
438
+ with gr.Row():
 
 
 
 
 
 
 
 
 
 
 
 
 
439
  with gr.Column(scale=1):
440
+ input_image = gr.Image(
441
+ type="pil",
442
+ label="๐Ÿ–ผ๏ธ Upload Image"
443
+ )
444
 
445
+ prompt_input = gr.Textbox(
446
+ label="โœจ Animation Prompt",
447
+ value=config.default_prompt,
448
+ placeholder="Describe the motion...",
449
+ lines=2,
450
+ max_lines=3
451
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
 
453
+ duration_input = gr.Slider(
454
+ minimum=0.3,
455
+ maximum=1.5,
456
+ step=0.1,
457
+ value=1.0,
458
+ label="โฑ๏ธ Duration (seconds)"
459
+ )
460
+
461
+ with gr.Accordion("โš™๏ธ Settings", open=False):
462
  negative_prompt = gr.Textbox(
463
+ label="Negative Prompt",
464
  value=config.default_negative_prompt,
465
+ lines=1
466
  )
467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  with gr.Row():
469
  height_slider = gr.Slider(
470
+ minimum=128,
471
+ maximum=640,
472
+ step=32,
473
+ value=384,
474
+ label="Height"
475
  )
476
  width_slider = gr.Slider(
477
+ minimum=128,
478
+ maximum=640,
479
+ step=32,
480
+ value=384,
481
+ label="Width"
482
  )
483
 
484
  steps_slider = gr.Slider(
485
  minimum=1,
486
+ maximum=6,
487
  step=1,
488
+ value=3,
489
+ label="Steps (3-4 recommended)"
490
  )
491
 
492
+ with gr.Row():
493
+ seed = gr.Slider(
494
+ minimum=0,
495
+ maximum=MAX_SEED,
496
+ step=1,
497
+ value=42,
498
+ label="Seed"
499
+ )
500
+ randomize_seed = gr.Checkbox(
501
+ label="Random",
502
+ value=True
503
+ )
504
+
505
  guidance_scale = gr.Slider(
506
  minimum=0.0,
507
+ maximum=5.0,
508
  step=0.5,
509
  value=1.0,
510
+ label="Guidance Scale",
511
  visible=False
512
  )
513
 
 
518
  )
519
 
520
  with gr.Column(scale=1):
 
521
  video_output = gr.Video(
522
+ label="Generated Video",
523
+ autoplay=True
 
524
  )
525
 
526
+ gr.Markdown("""
527
+ ### ๐Ÿ’ก Tips:
528
+ - Use 384ร—384 for best results
529
+ - Keep prompts simple and clear
530
+ - 3-4 steps is optimal
531
+ - Wait for completion before next generation
532
  """)
533
 
534
+ # Event handlers
535
+ input_image.upload(
536
+ fn=handle_image_upload,
537
+ inputs=[input_image],
538
+ outputs=[height_slider, width_slider]
539
+ )
540
+
541
+ generate_btn.click(
542
+ fn=generate_video,
543
+ inputs=[
544
+ input_image, prompt_input, height_slider, width_slider,
545
+ negative_prompt, duration_input, guidance_scale,
546
+ steps_slider, seed, randomize_seed
547
+ ],
548
+ outputs=[video_output, seed]
549
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
 
551
  if __name__ == "__main__":
552
+ logger.info("Starting app in Zero GPU environment")
553
+ demo.queue(max_size=3) # ์ž‘์€ ํ ์‚ฌ์ด์ฆˆ
 
 
 
 
 
554
  demo.launch()