seawolf2357 commited on
Commit
5b5b696
·
verified ·
1 Parent(s): 8b98825

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +270 -107
app.py CHANGED
@@ -16,6 +16,11 @@ import hashlib
16
  from dataclasses import dataclass
17
  from typing import Optional, Tuple
18
  from functools import wraps
 
 
 
 
 
19
 
20
  # 로깅 설정
21
  logging.basicConfig(level=logging.INFO)
@@ -40,10 +45,17 @@ class VideoGenerationConfig:
40
  max_frames: int = 81
41
  default_prompt: str = "make this image come alive, cinematic motion, smooth animation"
42
  default_negative_prompt: str = "static, blurred, low quality, watermark, text"
 
 
 
 
43
 
44
  config = VideoGenerationConfig()
45
  MAX_SEED = np.iinfo(np.int32).max
46
 
 
 
 
47
  # 성능 측정 데코레이터
48
  def measure_time(func):
49
  @wraps(func)
@@ -54,12 +66,37 @@ def measure_time(func):
54
  return result
55
  return wrapper
56
 
57
- # 모델 관리자
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  class ModelManager:
 
 
 
 
 
 
 
 
 
 
59
  def __init__(self):
60
- self._pipe = None
61
- self._is_loaded = False
62
-
 
 
63
  @property
64
  def pipe(self):
65
  if not self._is_loaded:
@@ -68,30 +105,93 @@ class ModelManager:
68
 
69
  @measure_time
70
  def _load_model(self):
71
- logger.info("Loading model...")
72
- image_encoder = CLIPVisionModel.from_pretrained(
73
- config.model_id, subfolder="image_encoder", torch_dtype=torch.float32
74
- )
75
- vae = AutoencoderKLWan.from_pretrained(
76
- config.model_id, subfolder="vae", torch_dtype=torch.float32
77
- )
78
- self._pipe = WanImageToVideoPipeline.from_pretrained(
79
- config.model_id, vae=vae, image_encoder=image_encoder, torch_dtype=torch.bfloat16
80
- )
81
- self._pipe.scheduler = UniPCMultistepScheduler.from_config(
82
- self._pipe.scheduler.config, flow_shift=8.0
83
- )
84
- self._pipe.to("cuda")
85
-
86
- causvid_path = hf_hub_download(
87
- repo_id=config.lora_repo_id, filename=config.lora_filename
88
- )
89
- self._pipe.load_lora_weights(causvid_path, adapter_name="causvid_lora")
90
- self._pipe.set_adapters(["causvid_lora"], adapter_weights=[0.95])
91
- self._pipe.fuse_lora()
92
- self._is_loaded = True
93
- logger.info("Model loaded successfully")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
 
95
  model_manager = ModelManager()
96
 
97
  # 비디오 생성기 클래스
@@ -136,6 +236,14 @@ class VideoGenerator:
136
  if duration > self.config.max_frames / self.config.fixed_fps:
137
  return False, f"⏱️ Duration too long (max {self.config.max_frames/self.config.fixed_fps:.1f}s)"
138
 
 
 
 
 
 
 
 
 
139
  return True, None
140
 
141
  def generate_unique_filename(self, seed: int) -> str:
@@ -165,12 +273,19 @@ def handle_image_upload(image):
165
 
166
  def get_duration(input_image, prompt, height, width, negative_prompt,
167
  duration_seconds, guidance_scale, steps, seed, randomize_seed, progress):
168
- if steps > 4 and duration_seconds > 2:
169
- return 90
170
- elif steps > 4 or duration_seconds > 2:
171
- return 75
172
- else:
173
- return 60
 
 
 
 
 
 
 
174
 
175
  @spaces.GPU(duration=get_duration)
176
  @measure_time
@@ -180,16 +295,23 @@ def generate_video(input_image, prompt, height, width,
180
  seed=42, randomize_seed=False,
181
  progress=gr.Progress(track_tqdm=True)):
182
 
183
- progress(0.1, desc="🔍 Validating inputs...")
184
-
185
- # 입력 검증
186
- is_valid, error_msg = video_generator.validate_inputs(
187
- input_image, prompt, height, width, duration_seconds, steps
188
- )
189
- if not is_valid:
190
- raise gr.Error(error_msg)
191
 
192
  try:
 
 
 
 
 
 
 
 
 
 
 
 
193
  progress(0.2, desc="🎯 Preparing image...")
194
  target_h = max(config.mod_value, (int(height) // config.mod_value) * config.mod_value)
195
  target_w = max(config.mod_value, (int(width) // config.mod_value) * config.mod_value)
@@ -197,24 +319,35 @@ def generate_video(input_image, prompt, height, width,
197
  config.min_frames, config.max_frames)
198
  current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
199
 
 
200
  resized_image = input_image.resize((target_w, target_h), Image.Resampling.LANCZOS)
201
 
202
  progress(0.3, desc="🎨 Loading model...")
203
  pipe = model_manager.pipe
204
 
205
  progress(0.4, desc="🎬 Generating video frames...")
206
- with torch.inference_mode():
207
- output_frames_list = pipe(
208
- image=resized_image,
209
- prompt=prompt,
210
- negative_prompt=negative_prompt,
211
- height=target_h,
212
- width=target_w,
213
- num_frames=num_frames,
214
- guidance_scale=float(guidance_scale),
215
- num_inference_steps=int(steps),
216
- generator=torch.Generator(device="cuda").manual_seed(current_seed)
217
- ).frames[0]
 
 
 
 
 
 
 
 
 
 
218
 
219
  progress(0.9, desc="💾 Saving video...")
220
  filename = video_generator.generate_unique_filename(current_seed)
@@ -226,14 +359,23 @@ def generate_video(input_image, prompt, height, width,
226
  progress(1.0, desc="✨ Complete!")
227
  return video_path, current_seed
228
 
 
 
 
 
229
  finally:
 
 
 
230
  # 메모리 정리
231
  if 'output_frames_list' in locals():
232
  del output_frames_list
233
- gc.collect()
234
- torch.cuda.empty_cache()
 
 
235
 
236
- # CSS 스타일
237
  css = """
238
  .container {
239
  max-width: 1200px;
@@ -249,17 +391,49 @@ css = """
249
  border-radius: 20px;
250
  color: white;
251
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  }
253
 
254
  .header h1 {
255
  font-size: 3em;
256
  margin-bottom: 10px;
257
  text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
 
 
258
  }
259
 
260
  .header p {
261
  font-size: 1.2em;
262
  opacity: 0.95;
 
 
 
 
 
 
 
 
 
 
 
 
263
  }
264
 
265
  .main-content {
@@ -296,6 +470,10 @@ css = """
296
  box-shadow: 0 7px 20px rgba(102, 126, 234, 0.6);
297
  }
298
 
 
 
 
 
299
  .video-output {
300
  background: #f8f9fa;
301
  padding: 20px;
@@ -333,8 +511,14 @@ body {
333
  100% { background-position: 0% 50%; }
334
  }
335
 
336
- .gr-button-secondary {
337
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
 
 
 
 
 
 
338
  }
339
 
340
  .footer {
@@ -348,11 +532,24 @@ body {
348
  # Gradio UI
349
  with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
350
  with gr.Column(elem_classes="container"):
351
- # Header
352
  gr.HTML("""
353
  <div class="header">
354
  <h1>🎬 AI Video Magic Studio</h1>
355
  <p>Transform your images into captivating videos with Wan 2.1 + CausVid LoRA</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  </div>
357
  """)
358
 
@@ -409,14 +606,14 @@ with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
409
  maximum=config.slider_max_h,
410
  step=config.mod_value,
411
  value=config.default_height,
412
- label="📏 Height"
413
  )
414
  width_slider = gr.Slider(
415
  minimum=config.slider_min_w,
416
  maximum=config.slider_max_w,
417
  step=config.mod_value,
418
  value=config.default_width,
419
- label="📐 Width"
420
  )
421
 
422
  steps_slider = gr.Slider(
@@ -459,58 +656,24 @@ with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
459
  # Examples
460
  gr.Examples(
461
  examples=[
462
- ["peng.png", "a penguin playfully dancing in the snow, Antarctica", 896, 512],
463
- ["forg.jpg", "the frog jumps around", 448, 832],
464
  ],
465
  inputs=[input_image, prompt_input, height_slider, width_slider],
466
  outputs=[video_output, seed],
467
  fn=generate_video,
468
- cache_examples="lazy"
469
  )
470
 
471
- # Examples 섹션 후에 추가
472
- gr.HTML("""
473
- <div class="improvements-container" style="background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border-radius: 15px; padding: 20px; margin: 20px auto; max-width: 800px; box-shadow: 0 5px 20px rgba(0,0,0,0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
474
- <div class="improvements-header" style="text-align: center; margin-bottom: 20px;">
475
- <h3 style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-size: 1.5em; margin: 0; font-weight: 700;">✨ Enhanced Features</h3>
476
- <p style="color: #666; font-size: 0.9em; margin-top: 5px;">Optimized for performance, stability, and user experience</p>
477
- </div>
478
-
479
- <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
480
- <div style="background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); border-radius: 10px; padding: 15px;">
481
- <span style="font-size: 1.5em; margin-bottom: 8px; display: block;">🛡️</span>
482
- <div style="font-weight: 600; color: #333; font-size: 0.95em; margin-bottom: 5px;">Robust Error Handling</div>
483
- <div style="font-size: 0.75em; color: #666; line-height: 1.4;">Advanced validation and recovery mechanisms</div>
484
- </div>
485
-
486
- <div style="background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); border-radius: 10px; padding: 15px;">
487
- <span style="font-size: 1.5em; margin-bottom: 8px; display: block;">⚡</span>
488
- <div style="font-weight: 600; color: #333; font-size: 0.95em; margin-bottom: 5px;">Performance Optimized</div>
489
- <div style="font-size: 0.75em; color: #666; line-height: 1.4;">Faster processing with smart resource management</div>
490
- </div>
491
-
492
- <div style="background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); border-radius: 10px; padding: 15px;">
493
- <span style="font-size: 1.5em; margin-bottom: 8px; display: block;">🎨</span>
494
- <div style="font-weight: 600; color: #333; font-size: 0.95em; margin-bottom: 5px;">Modern UI/UX</div>
495
- <div style="font-size: 0.75em; color: #666; line-height: 1.4;">Beautiful interface with smooth animations</div>
496
- </div>
497
-
498
- <div style="background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); border-radius: 10px; padding: 15px;">
499
- <span style="font-size: 1.5em; margin-bottom: 8px; display: block;">🔧</span>
500
- <div style="font-weight: 600; color: #333; font-size: 0.95em; margin-bottom: 5px;">Clean Architecture</div>
501
- <div style="font-size: 0.75em; color: #666; line-height: 1.4;">Modular design for easy maintenance</div>
502
- </div>
503
- </div>
504
-
505
- <div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 15px; justify-content: center;">
506
- <span style="background: rgba(102, 126, 234, 0.1); color: #667eea; padding: 3px 10px; border-radius: 20px; font-size: 0.7em; font-weight: 500;">PyTorch</span>
507
- <span style="background: rgba(102, 126, 234, 0.1); color: #667eea; padding: 3px 10px; border-radius: 20px; font-size: 0.7em; font-weight: 500;">Diffusers</span>
508
- <span style="background: rgba(102, 126, 234, 0.1); color: #667eea; padding: 3px 10px; border-radius: 20px; font-size: 0.7em; font-weight: 500;">Gradio</span>
509
- <span style="background: rgba(102, 126, 234, 0.1); color: #667eea; padding: 3px 10px; border-radius: 20px; font-size: 0.7em; font-weight: 500;">CUDA Optimized</span>
510
- <span style="background: rgba(102, 126, 234, 0.1); color: #667eea; padding: 3px 10px; border-radius: 20px; font-size: 0.7em; font-weight: 500;">LoRA Enhanced</span>
511
  </div>
512
- </div>
513
- """)
514
 
515
  # Event handlers
516
  input_image.upload(
@@ -536,4 +699,4 @@ with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
536
  )
537
 
538
  if __name__ == "__main__":
539
- demo.queue().launch()
 
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:512'
24
 
25
  # 로깅 설정
26
  logging.basicConfig(level=logging.INFO)
 
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
51
+ enable_vae_tiling: bool = True
52
 
53
  config = VideoGenerationConfig()
54
  MAX_SEED = np.iinfo(np.int32).max
55
 
56
+ # 글로벌 락 (동시 실행 방지)
57
+ generation_lock = threading.Lock()
58
+
59
  # 성능 측정 데코레이터
60
  def measure_time(func):
61
  @wraps(func)
 
66
  return result
67
  return wrapper
68
 
69
+ # GPU 메모리 정리 함수
70
+ def clear_gpu_memory():
71
+ """강력한 GPU 메모리 정리"""
72
+ if torch.cuda.is_available():
73
+ torch.cuda.empty_cache()
74
+ torch.cuda.ipc_collect()
75
+ gc.collect()
76
+
77
+ # GPU 메모리 상태 로깅
78
+ allocated = torch.cuda.memory_allocated() / 1024**3
79
+ reserved = torch.cuda.memory_reserved() / 1024**3
80
+ logger.info(f"GPU Memory - Allocated: {allocated:.2f}GB, Reserved: {reserved:.2f}GB")
81
+
82
+ # 모델 관리자 (싱글톤 패턴)
83
  class ModelManager:
84
+ _instance = None
85
+ _lock = threading.Lock()
86
+
87
+ def __new__(cls):
88
+ if cls._instance is None:
89
+ with cls._lock:
90
+ if cls._instance is None:
91
+ cls._instance = super().__new__(cls)
92
+ return cls._instance
93
+
94
  def __init__(self):
95
+ if not hasattr(self, '_initialized'):
96
+ self._pipe = None
97
+ self._is_loaded = False
98
+ self._initialized = True
99
+
100
  @property
101
  def pipe(self):
102
  if not self._is_loaded:
 
105
 
106
  @measure_time
107
  def _load_model(self):
108
+ """메모리 효율적인 모델 로딩"""
109
+ with self._lock:
110
+ if self._is_loaded:
111
+ return
112
+
113
+ try:
114
+ logger.info("Loading model with memory optimizations...")
115
+ clear_gpu_memory()
116
+
117
+ # 모델 컴포넌트 로드 (메모리 효율적)
118
+ with torch.cuda.amp.autocast(enabled=False):
119
+ image_encoder = CLIPVisionModel.from_pretrained(
120
+ config.model_id,
121
+ subfolder="image_encoder",
122
+ torch_dtype=torch.float16, # float32 대신 float16 사용
123
+ low_cpu_mem_usage=True
124
+ )
125
+
126
+ vae = AutoencoderKLWan.from_pretrained(
127
+ config.model_id,
128
+ subfolder="vae",
129
+ torch_dtype=torch.float16, # float32 대신 float16 사용
130
+ low_cpu_mem_usage=True
131
+ )
132
+
133
+ self._pipe = WanImageToVideoPipeline.from_pretrained(
134
+ config.model_id,
135
+ vae=vae,
136
+ image_encoder=image_encoder,
137
+ torch_dtype=torch.bfloat16,
138
+ low_cpu_mem_usage=True,
139
+ use_safetensors=True
140
+ )
141
+
142
+ # 스케줄러 설정
143
+ self._pipe.scheduler = UniPCMultistepScheduler.from_config(
144
+ self._pipe.scheduler.config, flow_shift=8.0
145
+ )
146
+
147
+ # LoRA 로��
148
+ causvid_path = hf_hub_download(
149
+ repo_id=config.lora_repo_id, filename=config.lora_filename
150
+ )
151
+ self._pipe.load_lora_weights(causvid_path, adapter_name="causvid_lora")
152
+ self._pipe.set_adapters(["causvid_lora"], adapter_weights=[0.95])
153
+ self._pipe.fuse_lora()
154
+
155
+ # GPU 최적화 설정
156
+ if config.enable_model_cpu_offload:
157
+ self._pipe.enable_model_cpu_offload()
158
+ else:
159
+ self._pipe.to("cuda")
160
+
161
+ if config.enable_vae_slicing:
162
+ self._pipe.enable_vae_slicing()
163
+
164
+ if config.enable_vae_tiling:
165
+ self._pipe.enable_vae_tiling()
166
+
167
+ # xFormers 메모리 효율적인 attention 활성화 (가능한 경우)
168
+ try:
169
+ self._pipe.enable_xformers_memory_efficient_attention()
170
+ logger.info("xFormers memory efficient attention enabled")
171
+ except:
172
+ logger.info("xFormers not available, using default attention")
173
+
174
+ self._is_loaded = True
175
+ logger.info("Model loaded successfully with optimizations")
176
+ clear_gpu_memory()
177
+
178
+ except Exception as e:
179
+ logger.error(f"Error loading model: {e}")
180
+ self._is_loaded = False
181
+ clear_gpu_memory()
182
+ raise
183
+
184
+ def unload_model(self):
185
+ """모델 언로드 및 메모리 해제"""
186
+ with self._lock:
187
+ if self._pipe is not None:
188
+ del self._pipe
189
+ self._pipe = None
190
+ self._is_loaded = False
191
+ clear_gpu_memory()
192
+ logger.info("Model unloaded and memory cleared")
193
 
194
+ # 싱글톤 인스턴스
195
  model_manager = ModelManager()
196
 
197
  # 비디오 생성기 클래스
 
236
  if duration > self.config.max_frames / self.config.fixed_fps:
237
  return False, f"⏱️ Duration too long (max {self.config.max_frames/self.config.fixed_fps:.1f}s)"
238
 
239
+ # GPU 메모리 체크
240
+ if torch.cuda.is_available():
241
+ free_memory = torch.cuda.get_device_properties(0).total_memory - torch.cuda.memory_allocated()
242
+ required_memory = (height * width * 3 * 8 * duration * config.fixed_fps) / (1024**3) # 대략적인 추정
243
+ if free_memory < required_memory * 2: # 2배 여유 확보
244
+ clear_gpu_memory()
245
+ return False, "⚠️ Not enough GPU memory. Try smaller dimensions or shorter duration."
246
+
247
  return True, None
248
 
249
  def generate_unique_filename(self, seed: int) -> str:
 
273
 
274
  def get_duration(input_image, prompt, height, width, negative_prompt,
275
  duration_seconds, guidance_scale, steps, seed, randomize_seed, progress):
276
+ # GPU 사용량에 따라 동적으로 duration 조정
277
+ base_duration = 60
278
+ if steps > 4:
279
+ base_duration += 15
280
+ if duration_seconds > 2:
281
+ base_duration += 15
282
+
283
+ # 해상도에 따른 추가 시간
284
+ pixels = height * width
285
+ if pixels > 500000:
286
+ base_duration += 20
287
+
288
+ return min(base_duration, 120) # 최대 120초
289
 
290
  @spaces.GPU(duration=get_duration)
291
  @measure_time
 
295
  seed=42, randomize_seed=False,
296
  progress=gr.Progress(track_tqdm=True)):
297
 
298
+ # 동시 실행 방지
299
+ if not generation_lock.acquire(blocking=False):
300
+ raise gr.Error("⏳ Another video is being generated. Please wait...")
 
 
 
 
 
301
 
302
  try:
303
+ progress(0.1, desc="🔍 Validating inputs...")
304
+
305
+ # 입력 검증
306
+ is_valid, error_msg = video_generator.validate_inputs(
307
+ input_image, prompt, height, width, duration_seconds, steps
308
+ )
309
+ if not is_valid:
310
+ raise gr.Error(error_msg)
311
+
312
+ # 메모리 정리
313
+ clear_gpu_memory()
314
+
315
  progress(0.2, desc="🎯 Preparing image...")
316
  target_h = max(config.mod_value, (int(height) // config.mod_value) * config.mod_value)
317
  target_w = max(config.mod_value, (int(width) // config.mod_value) * config.mod_value)
 
319
  config.min_frames, config.max_frames)
320
  current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
321
 
322
+ # 이미지 리사이즈 (메모리 효율적)
323
  resized_image = input_image.resize((target_w, target_h), Image.Resampling.LANCZOS)
324
 
325
  progress(0.3, desc="🎨 Loading model...")
326
  pipe = model_manager.pipe
327
 
328
  progress(0.4, desc="🎬 Generating video frames...")
329
+
330
+ # 메모리 효율적인 생성
331
+ with torch.inference_mode(), torch.cuda.amp.autocast(enabled=True):
332
+ try:
333
+ output_frames_list = pipe(
334
+ image=resized_image,
335
+ prompt=prompt,
336
+ negative_prompt=negative_prompt,
337
+ height=target_h,
338
+ width=target_w,
339
+ num_frames=num_frames,
340
+ guidance_scale=float(guidance_scale),
341
+ num_inference_steps=int(steps),
342
+ generator=torch.Generator(device="cuda").manual_seed(current_seed),
343
+ return_dict=True
344
+ ).frames[0]
345
+ except torch.cuda.OutOfMemoryError:
346
+ clear_gpu_memory()
347
+ raise gr.Error("💾 GPU out of memory. Try smaller dimensions or shorter duration.")
348
+ except Exception as e:
349
+ logger.error(f"Generation error: {e}")
350
+ raise gr.Error(f"❌ Generation failed: {str(e)}")
351
 
352
  progress(0.9, desc="💾 Saving video...")
353
  filename = video_generator.generate_unique_filename(current_seed)
 
359
  progress(1.0, desc="✨ Complete!")
360
  return video_path, current_seed
361
 
362
+ except Exception as e:
363
+ logger.error(f"Unexpected error: {e}")
364
+ raise
365
+
366
  finally:
367
+ # 항상 메모리 정리 및 락 해제
368
+ generation_lock.release()
369
+
370
  # 메모리 정리
371
  if 'output_frames_list' in locals():
372
  del output_frames_list
373
+ if 'resized_image' in locals():
374
+ del resized_image
375
+
376
+ clear_gpu_memory()
377
 
378
+ # 개선된 CSS 스타일
379
  css = """
380
  .container {
381
  max-width: 1200px;
 
391
  border-radius: 20px;
392
  color: white;
393
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
394
+ position: relative;
395
+ overflow: hidden;
396
+ }
397
+
398
+ .header::before {
399
+ content: '';
400
+ position: absolute;
401
+ top: -50%;
402
+ left: -50%;
403
+ width: 200%;
404
+ height: 200%;
405
+ background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
406
+ animation: pulse 4s ease-in-out infinite;
407
+ }
408
+
409
+ @keyframes pulse {
410
+ 0%, 100% { transform: scale(1); opacity: 0.5; }
411
+ 50% { transform: scale(1.1); opacity: 0.8; }
412
  }
413
 
414
  .header h1 {
415
  font-size: 3em;
416
  margin-bottom: 10px;
417
  text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
418
+ position: relative;
419
+ z-index: 1;
420
  }
421
 
422
  .header p {
423
  font-size: 1.2em;
424
  opacity: 0.95;
425
+ position: relative;
426
+ z-index: 1;
427
+ }
428
+
429
+ .gpu-status {
430
+ position: absolute;
431
+ top: 10px;
432
+ right: 10px;
433
+ background: rgba(0,0,0,0.3);
434
+ padding: 5px 15px;
435
+ border-radius: 20px;
436
+ font-size: 0.8em;
437
  }
438
 
439
  .main-content {
 
470
  box-shadow: 0 7px 20px rgba(102, 126, 234, 0.6);
471
  }
472
 
473
+ .generate-btn:active {
474
+ transform: translateY(0);
475
+ }
476
+
477
  .video-output {
478
  background: #f8f9fa;
479
  padding: 20px;
 
511
  100% { background-position: 0% 50%; }
512
  }
513
 
514
+ .warning-box {
515
+ background: rgba(255, 193, 7, 0.1);
516
+ border: 1px solid rgba(255, 193, 7, 0.3);
517
+ border-radius: 10px;
518
+ padding: 15px;
519
+ margin: 10px 0;
520
+ color: #856404;
521
+ font-size: 0.9em;
522
  }
523
 
524
  .footer {
 
532
  # Gradio UI
533
  with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
534
  with gr.Column(elem_classes="container"):
535
+ # Header with GPU status
536
  gr.HTML("""
537
  <div class="header">
538
  <h1>🎬 AI Video Magic Studio</h1>
539
  <p>Transform your images into captivating videos with Wan 2.1 + CausVid LoRA</p>
540
+ <div class="gpu-status">🖥️ GPU Optimized</div>
541
+ </div>
542
+ """)
543
+
544
+ # GPU 메모리 경고
545
+ gr.HTML("""
546
+ <div class="warning-box">
547
+ <strong>💡 Performance Tips:</strong>
548
+ <ul style="margin: 5px 0; padding-left: 20px;">
549
+ <li>Start with lower resolution (512x512) for testing</li>
550
+ <li>Keep duration under 2 seconds for stable generation</li>
551
+ <li>Use 4-8 steps for optimal speed/quality balance</li>
552
+ </ul>
553
  </div>
554
  """)
555
 
 
606
  maximum=config.slider_max_h,
607
  step=config.mod_value,
608
  value=config.default_height,
609
+ label="📏 Height (lower = more stable)"
610
  )
611
  width_slider = gr.Slider(
612
  minimum=config.slider_min_w,
613
  maximum=config.slider_max_w,
614
  step=config.mod_value,
615
  value=config.default_width,
616
+ label="📐 Width (lower = more stable)"
617
  )
618
 
619
  steps_slider = gr.Slider(
 
656
  # Examples
657
  gr.Examples(
658
  examples=[
659
+ ["peng.png", "a penguin playfully dancing in the snow, Antarctica", 512, 512],
660
+ ["forg.jpg", "the frog jumps around", 448, 448],
661
  ],
662
  inputs=[input_image, prompt_input, height_slider, width_slider],
663
  outputs=[video_output, seed],
664
  fn=generate_video,
665
+ cache_examples=False # 캐시 비활성화로 메모리 절약
666
  )
667
 
668
+ # 개선사항 요약 (작게)
669
+ gr.HTML("""
670
+ <div style="background: rgba(255,255,255,0.9); border-radius: 10px; padding: 15px; margin-top: 20px; font-size: 0.8em; text-align: center;">
671
+ <p style="margin: 0; color: #666;">
672
+ <strong style="color: #667eea;">Enhanced with:</strong>
673
+ 🛡️ GPU Crash Protection Memory Optimization 🎨 Modern UI • 🔧 Clean Architecture
674
+ </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675
  </div>
676
+ """)
 
677
 
678
  # Event handlers
679
  input_image.upload(
 
699
  )
700
 
701
  if __name__ == "__main__":
702
+ demo.queue(max_size=1).launch() # 큐 크기 제한으로 메모리 관리