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()