seawolf2357's picture
Update app.py
fde0767 verified
raw
history blame
32.8 kB
import torch
from diffusers import AutoencoderKLWan, WanImageToVideoPipeline, UniPCMultistepScheduler
from diffusers.utils import export_to_video
from transformers import CLIPVisionModel
import gradio as gr
import tempfile
import spaces
from huggingface_hub import hf_hub_download
import numpy as np
from PIL import Image
import random
import logging
import gc
import time
import hashlib
from dataclasses import dataclass
from typing import Optional, Tuple
from functools import wraps
import threading
import os
# GPU λ©”λͺ¨λ¦¬ 관리 μ„€μ •
os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'max_split_size_mb:512'
# λ‘œκΉ… μ„€μ •
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# μ„€μ • 관리
@dataclass
class VideoGenerationConfig:
model_id: str = "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers"
lora_repo_id: str = "Kijai/WanVideo_comfy"
lora_filename: str = "Wan21_CausVid_14B_T2V_lora_rank32.safetensors"
mod_value: int = 32
default_height: int = 512
default_width: int = 512 # Zero GPU ν™˜κ²½μ„ μœ„ν•΄ κΈ°λ³Έκ°’ μˆ˜μ •
max_area: float = 480.0 * 832.0
slider_min_h: int = 128
slider_max_h: int = 832 # Zero GPU ν™˜κ²½μ„ μœ„ν•΄ μˆ˜μ •
slider_min_w: int = 128
slider_max_w: int = 832 # Zero GPU ν™˜κ²½μ„ μœ„ν•΄ μˆ˜μ •
fixed_fps: int = 24
min_frames: int = 8
max_frames: int = 81
default_prompt: str = "make this image come alive, cinematic motion, smooth animation"
default_negative_prompt: str = "static, blurred, low quality, watermark, text"
# GPU λ©”λͺ¨λ¦¬ μ΅œμ ν™” μ„€μ •
enable_model_cpu_offload: bool = True
enable_vae_slicing: bool = True
enable_vae_tiling: bool = True
@property
def max_duration(self):
"""μ΅œλŒ€ ν—ˆμš© duration (초)"""
return self.max_frames / self.fixed_fps
@property
def min_duration(self):
"""μ΅œμ†Œ ν—ˆμš© duration (초)"""
return self.min_frames / self.fixed_fps
config = VideoGenerationConfig()
MAX_SEED = np.iinfo(np.int32).max
# κΈ€λ‘œλ²Œ 락 (λ™μ‹œ μ‹€ν–‰ λ°©μ§€)
generation_lock = threading.Lock()
# μ„±λŠ₯ μΈ‘μ • λ°μ½”λ ˆμ΄ν„°
def measure_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
logger.info(f"{func.__name__} took {time.time()-start:.2f}s")
return result
return wrapper
# GPU λ©”λͺ¨λ¦¬ 정리 ν•¨μˆ˜
def clear_gpu_memory():
"""κ°•λ ₯ν•œ GPU λ©”λͺ¨λ¦¬ 정리"""
# Zero GPU ν™˜κ²½μ—μ„œλŠ” 메인 ν”„λ‘œμ„ΈμŠ€μ—μ„œ CUDA μ΄ˆκΈ°ν™” λ°©μ§€
if hasattr(spaces, 'GPU'):
# Zero GPU ν™˜κ²½μ—μ„œλŠ” @spaces.GPU λ‚΄μ—μ„œλ§Œ GPU μž‘μ—… μˆ˜ν–‰
gc.collect()
return
if torch.cuda.is_available():
try:
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
gc.collect()
# GPU λ©”λͺ¨λ¦¬ μƒνƒœ λ‘œκΉ…
allocated = torch.cuda.memory_allocated() / 1024**3
reserved = torch.cuda.memory_reserved() / 1024**3
logger.info(f"GPU Memory - Allocated: {allocated:.2f}GB, Reserved: {reserved:.2f}GB")
except Exception as e:
logger.warning(f"GPU memory clear failed: {e}")
gc.collect()
# λͺ¨λΈ κ΄€λ¦¬μž (싱글톀 νŒ¨ν„΄)
class ModelManager:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not hasattr(self, '_initialized'):
self._pipe = None
self._is_loaded = False
self._initialized = True
@property
def pipe(self):
if not self._is_loaded:
self._load_model()
return self._pipe
@measure_time
def _load_model(self):
"""λ©”λͺ¨λ¦¬ 효율적인 λͺ¨λΈ λ‘œλ”©"""
with self._lock:
if self._is_loaded:
return
try:
logger.info("Loading model with memory optimizations...")
clear_gpu_memory()
# λͺ¨λΈ μ»΄ν¬λ„ŒνŠΈ λ‘œλ“œ (λ©”λͺ¨λ¦¬ 효율적) - autocast μˆ˜μ •
if torch.cuda.is_available() and not hasattr(spaces, 'GPU'):
# 일반 GPU ν™˜κ²½
with torch.amp.autocast('cuda', enabled=False): # μˆ˜μ •λœ λΆ€λΆ„
image_encoder = CLIPVisionModel.from_pretrained(
config.model_id,
subfolder="image_encoder",
torch_dtype=torch.float16,
low_cpu_mem_usage=True
)
vae = AutoencoderKLWan.from_pretrained(
config.model_id,
subfolder="vae",
torch_dtype=torch.float16,
low_cpu_mem_usage=True
)
else:
# CPU ν™˜κ²½ λ˜λŠ” Zero GPU ν™˜κ²½
image_encoder = CLIPVisionModel.from_pretrained(
config.model_id,
subfolder="image_encoder",
torch_dtype=torch.float16 if hasattr(spaces, 'GPU') else torch.float32,
low_cpu_mem_usage=True
)
vae = AutoencoderKLWan.from_pretrained(
config.model_id,
subfolder="vae",
torch_dtype=torch.float16 if hasattr(spaces, 'GPU') else torch.float32,
low_cpu_mem_usage=True
)
self._pipe = WanImageToVideoPipeline.from_pretrained(
config.model_id,
vae=vae,
image_encoder=image_encoder,
torch_dtype=torch.bfloat16 if (torch.cuda.is_available() or hasattr(spaces, 'GPU')) else torch.float32,
low_cpu_mem_usage=True,
use_safetensors=True
)
# μŠ€μΌ€μ€„λŸ¬ μ„€μ •
self._pipe.scheduler = UniPCMultistepScheduler.from_config(
self._pipe.scheduler.config, flow_shift=8.0
)
# LoRA λ‘œλ“œ
try:
causvid_path = hf_hub_download(
repo_id=config.lora_repo_id, filename=config.lora_filename
)
self._pipe.load_lora_weights(causvid_path, adapter_name="causvid_lora")
self._pipe.set_adapters(["causvid_lora"], adapter_weights=[0.95])
self._pipe.fuse_lora()
logger.info("LoRA weights loaded successfully")
except Exception as e:
logger.warning(f"Failed to load LoRA weights: {e}")
# GPU μ΅œμ ν™” μ„€μ •
if hasattr(spaces, 'GPU'): # Zero GPU ν™˜κ²½
# Zero GPU ν™˜κ²½μ—μ„œλŠ” μžλ™μœΌλ‘œ 처리됨
logger.info("Model loaded for Zero GPU environment")
elif config.enable_model_cpu_offload and torch.cuda.is_available():
self._pipe.enable_model_cpu_offload()
logger.info("CPU offload enabled")
elif torch.cuda.is_available():
self._pipe.to("cuda")
logger.info("Model moved to CUDA")
else:
logger.info("Running on CPU")
if config.enable_vae_slicing:
self._pipe.enable_vae_slicing()
if config.enable_vae_tiling:
self._pipe.enable_vae_tiling()
# xFormers λ©”λͺ¨λ¦¬ 효율적인 attention ν™œμ„±ν™” (κ°€λŠ₯ν•œ 경우)
try:
self._pipe.enable_xformers_memory_efficient_attention()
logger.info("xFormers memory efficient attention enabled")
except:
logger.info("xFormers not available, using default attention")
self._is_loaded = True
logger.info("Model loaded successfully with optimizations")
clear_gpu_memory()
except Exception as e:
logger.error(f"Error loading model: {e}")
self._is_loaded = False
clear_gpu_memory()
raise
def unload_model(self):
"""λͺ¨λΈ μ–Έλ‘œλ“œ 및 λ©”λͺ¨λ¦¬ ν•΄μ œ"""
with self._lock:
if self._pipe is not None:
del self._pipe
self._pipe = None
self._is_loaded = False
clear_gpu_memory()
logger.info("Model unloaded and memory cleared")
# 싱글톀 μΈμŠ€ν„΄μŠ€
model_manager = ModelManager()
# λΉ„λ””μ˜€ 생성기 클래슀
class VideoGenerator:
def __init__(self, config: VideoGenerationConfig, model_manager: ModelManager):
self.config = config
self.model_manager = model_manager
def calculate_dimensions(self, image: Image.Image) -> Tuple[int, int]:
orig_w, orig_h = image.size
if orig_w <= 0 or orig_h <= 0:
return self.config.default_height, self.config.default_width
aspect_ratio = orig_h / orig_w
# Zero GPU ν™˜κ²½μ—μ„œλŠ” 더 μž‘μ€ max_area μ‚¬μš©
if hasattr(spaces, 'GPU'):
max_area = 640.0 * 640.0 # 409,600 pixels
else:
max_area = self.config.max_area
calc_h = round(np.sqrt(max_area * aspect_ratio))
calc_w = round(np.sqrt(max_area / aspect_ratio))
calc_h = max(self.config.mod_value, (calc_h // self.config.mod_value) * self.config.mod_value)
calc_w = max(self.config.mod_value, (calc_w // self.config.mod_value) * self.config.mod_value)
# Zero GPU ν™˜κ²½μ—μ„œ μΆ”κ°€ μ œν•œ
if hasattr(spaces, 'GPU'):
max_dim = 832
new_h = int(np.clip(calc_h, self.config.slider_min_h, min(max_dim, self.config.slider_max_h)))
new_w = int(np.clip(calc_w, self.config.slider_min_w, min(max_dim, self.config.slider_max_w)))
else:
new_h = int(np.clip(calc_h, self.config.slider_min_h,
(self.config.slider_max_h // self.config.mod_value) * self.config.mod_value))
new_w = int(np.clip(calc_w, self.config.slider_min_w,
(self.config.slider_max_w // self.config.mod_value) * self.config.mod_value))
return new_h, new_w
def validate_inputs(self, image: Image.Image, prompt: str, height: int,
width: int, duration: float, steps: int) -> Tuple[bool, Optional[str]]:
if image is None:
return False, "πŸ–ΌοΈ Please upload an input image"
if not prompt or len(prompt.strip()) == 0:
return False, "✍️ Please provide a prompt"
if len(prompt) > 500:
return False, "⚠️ Prompt is too long (max 500 characters)"
# μ •ν™•ν•œ duration λ²”μœ„ 체크
min_duration = self.config.min_duration
max_duration = self.config.max_duration
if duration < min_duration:
return False, f"⏱️ Duration too short (min {min_duration:.1f}s)"
if duration > max_duration:
return False, f"⏱️ Duration too long (max {max_duration:.1f}s)"
# Zero GPU ν™˜κ²½μ—μ„œλŠ” 더 보수적인 μ œν•œ 적용
if hasattr(spaces, 'GPU'): # Spaces ν™˜κ²½ 체크
if duration > 2.5: # Zero GPUμ—μ„œλŠ” 2.5초둜 μ œν•œ
return False, "⏱️ In Zero GPU environment, duration is limited to 2.5s for stability"
# ν”½μ…€ 수 기반 μ œν•œ (640x640 = 409,600 ν”½μ…€)
max_pixels = 640 * 640
if height * width > max_pixels:
return False, f"πŸ“ In Zero GPU environment, total pixels limited to {max_pixels:,} (e.g., 640Γ—640, 512Γ—832)"
if height > 832 or width > 832: # ν•œ λ³€μ˜ μ΅œλŒ€ 길이
return False, "πŸ“ In Zero GPU environment, maximum dimension is 832 pixels"
# GPU λ©”λͺ¨λ¦¬ 체크 (Zero GPU ν™˜κ²½μ΄ 아닐 λ•Œλ§Œ)
if torch.cuda.is_available() and not hasattr(spaces, 'GPU'):
try:
free_memory = torch.cuda.get_device_properties(0).total_memory - torch.cuda.memory_allocated()
required_memory = (height * width * 3 * 8 * duration * self.config.fixed_fps) / (1024**3)
if free_memory < required_memory * 2:
clear_gpu_memory()
# μž¬ν™•μΈ
free_memory = torch.cuda.get_device_properties(0).total_memory - torch.cuda.memory_allocated()
if free_memory < required_memory * 1.5:
return False, "⚠️ Not enough GPU memory. Try smaller dimensions or shorter duration."
except Exception as e:
logger.warning(f"GPU memory check failed: {e}")
return True, None
def generate_unique_filename(self, seed: int) -> str:
timestamp = int(time.time())
unique_str = f"{timestamp}_{seed}_{random.randint(1000, 9999)}"
hash_obj = hashlib.md5(unique_str.encode())
return f"video_{hash_obj.hexdigest()[:8]}.mp4"
video_generator = VideoGenerator(config, model_manager)
# Gradio ν•¨μˆ˜λ“€
def handle_image_upload(image):
if image is None:
return gr.update(value=config.default_height), gr.update(value=config.default_width)
try:
if not isinstance(image, Image.Image):
raise ValueError("Invalid image format")
new_h, new_w = video_generator.calculate_dimensions(image)
return gr.update(value=new_h), gr.update(value=new_w)
except Exception as e:
logger.error(f"Error processing image: {e}")
gr.Warning("⚠️ Error processing image")
return gr.update(value=config.default_height), gr.update(value=config.default_width)
def get_duration(input_image, prompt, height, width, negative_prompt,
duration_seconds, guidance_scale, steps, seed, randomize_seed, progress):
# Zero GPU ν™˜κ²½μ—μ„œλŠ” 더 보수적인 μ‹œκ°„ ν• λ‹Ή
base_duration = 60
# 단계별 μΆ”κ°€ μ‹œκ°„
if steps > 8:
base_duration += 30
elif steps > 4:
base_duration += 15
# Duration별 μΆ”κ°€ μ‹œκ°„
if duration_seconds > 2:
base_duration += 20
elif duration_seconds > 1.5:
base_duration += 10
# 해상도별 μΆ”κ°€ μ‹œκ°„ (ν”½μ…€ 수 기반)
pixels = height * width
if pixels > 400000: # 640x640 근처
base_duration += 20
elif pixels > 250000: # 512x512 근처
base_duration += 10
# Zero GPU ν™˜κ²½μ—μ„œλŠ” μ΅œλŒ€ 90초둜 μ œν•œ
return min(base_duration, 90)
@spaces.GPU(duration=get_duration)
@measure_time
def generate_video(input_image, prompt, height, width,
negative_prompt=config.default_negative_prompt,
duration_seconds=1.5, guidance_scale=1, steps=4,
seed=42, randomize_seed=False,
progress=gr.Progress(track_tqdm=True)):
# λ™μ‹œ μ‹€ν–‰ λ°©μ§€
if not generation_lock.acquire(blocking=False):
raise gr.Error("⏳ Another video is being generated. Please wait...")
try:
# Zero GPU ν™˜κ²½μ—μ„œλŠ” 이제 GPU μ‚¬μš© κ°€λŠ₯
if hasattr(spaces, 'GPU') and torch.cuda.is_available():
logger.info("GPU initialized in Zero GPU environment")
progress(0.1, desc="πŸ” Validating inputs...")
# Zero GPU ν™˜κ²½μ—μ„œ μΆ”κ°€ 검증
if hasattr(spaces, 'GPU'):
logger.info(f"Zero GPU environment detected. Duration: {duration_seconds}s, Resolution: {height}x{width}, Pixels: {height*width:,}")
# μž…λ ₯ 검증
is_valid, error_msg = video_generator.validate_inputs(
input_image, prompt, height, width, duration_seconds, steps
)
if not is_valid:
raise gr.Error(error_msg)
# λ©”λͺ¨λ¦¬ 정리
clear_gpu_memory()
progress(0.2, desc="🎯 Preparing image...")
target_h = max(config.mod_value, (int(height) // config.mod_value) * config.mod_value)
target_w = max(config.mod_value, (int(width) // config.mod_value) * config.mod_value)
# ν”„λ ˆμž„ 수 계산 (Zero GPU ν™˜κ²½μ—μ„œ μΆ”κ°€ μ œν•œ)
max_allowed_frames = int(2.5 * config.fixed_fps) if hasattr(spaces, 'GPU') else config.max_frames
num_frames = min(
int(round(duration_seconds * config.fixed_fps)),
max_allowed_frames
)
num_frames = np.clip(num_frames, config.min_frames, max_allowed_frames)
current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
# 이미지 λ¦¬μ‚¬μ΄μ¦ˆ (λ©”λͺ¨λ¦¬ 효율적)
resized_image = input_image.resize((target_w, target_h), Image.Resampling.LANCZOS)
progress(0.3, desc="🎨 Loading model...")
pipe = model_manager.pipe
progress(0.4, desc="🎬 Generating video frames...")
# λ©”λͺ¨λ¦¬ 효율적인 생성
device = "cuda" if torch.cuda.is_available() else "cpu"
if device == "cuda":
with torch.inference_mode(), torch.amp.autocast('cuda', enabled=True): # μˆ˜μ •λœ λΆ€λΆ„
try:
output_frames_list = pipe(
image=resized_image,
prompt=prompt,
negative_prompt=negative_prompt,
height=target_h,
width=target_w,
num_frames=num_frames,
guidance_scale=float(guidance_scale),
num_inference_steps=int(steps),
generator=torch.Generator(device="cuda").manual_seed(current_seed),
return_dict=True
).frames[0]
except torch.cuda.OutOfMemoryError:
clear_gpu_memory()
raise gr.Error("πŸ’Ύ GPU out of memory. Try smaller dimensions or shorter duration.")
except Exception as e:
logger.error(f"Generation error: {e}")
raise gr.Error(f"❌ Generation failed: {str(e)}")
else:
# CPU ν™˜κ²½
with torch.inference_mode():
try:
output_frames_list = pipe(
image=resized_image,
prompt=prompt,
negative_prompt=negative_prompt,
height=target_h,
width=target_w,
num_frames=num_frames,
guidance_scale=float(guidance_scale),
num_inference_steps=int(steps),
generator=torch.Generator().manual_seed(current_seed),
return_dict=True
).frames[0]
except Exception as e:
logger.error(f"Generation error: {e}")
raise gr.Error(f"❌ Generation failed: {str(e)}")
progress(0.9, desc="πŸ’Ύ Saving video...")
filename = video_generator.generate_unique_filename(current_seed)
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmpfile:
video_path = tmpfile.name
export_to_video(output_frames_list, video_path, fps=config.fixed_fps)
progress(1.0, desc="✨ Complete!")
logger.info(f"Video generated successfully: {num_frames} frames, {target_h}x{target_w}")
# 성곡 정보 λ°˜ν™˜
info_text = f"βœ… Generated {num_frames} frames at {target_h}x{target_w} with seed {current_seed}"
gr.Info(info_text)
return video_path, current_seed
except gr.Error:
# Gradio μ—λŸ¬λŠ” κ·ΈλŒ€λ‘œ 전달
raise
except Exception as e:
logger.error(f"Unexpected error: {e}")
raise gr.Error(f"❌ Unexpected error: {str(e)}")
finally:
# 항상 λ©”λͺ¨λ¦¬ 정리 및 락 ν•΄μ œ
generation_lock.release()
# λ©”λͺ¨λ¦¬ 정리
if 'output_frames_list' in locals():
del output_frames_list
if 'resized_image' in locals():
del resized_image
clear_gpu_memory()
# κ°œμ„ λœ CSS μŠ€νƒ€μΌ
css = """
.container {
max-width: 1200px;
margin: auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px;
border-radius: 20px;
color: white;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: pulse 4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.1); opacity: 0.8; }
}
.header h1 {
font-size: 3em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
position: relative;
z-index: 1;
}
.header p {
font-size: 1.2em;
opacity: 0.95;
position: relative;
z-index: 1;
}
.gpu-status {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.3);
padding: 5px 15px;
border-radius: 20px;
font-size: 0.8em;
}
.main-content {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
}
.input-section {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 25px;
border-radius: 15px;
margin-bottom: 20px;
}
.generate-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 1.3em;
padding: 15px 40px;
border-radius: 30px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
width: 100%;
margin-top: 20px;
}
.generate-btn:hover {
transform: translateY(-2px);
box-shadow: 0 7px 20px rgba(102, 126, 234, 0.6);
}
.generate-btn:active {
transform: translateY(0);
}
.video-output {
background: #f8f9fa;
padding: 20px;
border-radius: 15px;
text-align: center;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
.accordion {
background: rgba(255, 255, 255, 0.7);
border-radius: 10px;
margin-top: 15px;
padding: 15px;
}
.slider-container {
background: rgba(255, 255, 255, 0.5);
padding: 15px;
border-radius: 10px;
margin: 10px 0;
}
body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
}
@keyframes gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.warning-box {
background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 10px;
padding: 15px;
margin: 10px 0;
color: #856404;
font-size: 0.9em;
}
.info-box {
background: rgba(52, 152, 219, 0.1);
border: 1px solid rgba(52, 152, 219, 0.3);
border-radius: 10px;
padding: 15px;
margin: 10px 0;
color: #2c5282;
font-size: 0.9em;
}
.footer {
text-align: center;
margin-top: 30px;
color: #666;
font-size: 0.9em;
}
/* λ‘œλ”© μ• λ‹ˆλ©”μ΄μ…˜ κ°œμ„  */
.progress-bar {
background: linear-gradient(90deg, #667eea 0%, #764ba2 50%, #667eea 100%);
background-size: 200% 100%;
animation: loading 1.5s ease-in-out infinite;
}
@keyframes loading {
0% { background-position: 0% 0%; }
100% { background-position: 200% 0%; }
}
"""
# Gradio UI
with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
with gr.Column(elem_classes="container"):
# Header with GPU status
gr.HTML("""
<div class="header">
<h1>🎬 AI Video Magic Studio</h1>
<p>Transform your images into captivating videos with Wan 2.1 + CausVid LoRA</p>
<div class="gpu-status">πŸ–₯️ Zero GPU Optimized</div>
</div>
""")
# GPU λ©”λͺ¨λ¦¬ κ²½κ³ 
gr.HTML("""
<div class="warning-box">
<strong>πŸ’‘ Zero GPU Performance Tips:</strong>
<ul style="margin: 5px 0; padding-left: 20px;">
<li>Maximum duration: 2.5 seconds (limited by Zero GPU)</li>
<li>Maximum total pixels: 409,600 (e.g., 640Γ—640, 512Γ—832, 448Γ—896)</li>
<li>Maximum single dimension: 832 pixels</li>
<li>Use 4-6 steps for optimal speed/quality balance</li>
<li>Wait between generations to avoid queue errors</li>
</ul>
</div>
""")
# μƒˆλ‘œμš΄ 정보 λ°•μŠ€ μΆ”κ°€
gr.HTML("""
<div class="info-box">
<strong>🎯 Quick Start Guide:</strong>
<ol style="margin: 5px 0; padding-left: 20px;">
<li>Upload your image - AI will calculate optimal dimensions</li>
<li>Enter a creative prompt or use the default</li>
<li>Adjust duration (1.5s recommended for best results)</li>
<li>Click Generate and wait ~60 seconds</li>
</ol>
</div>
""")
with gr.Row(elem_classes="main-content"):
with gr.Column(scale=1):
gr.Markdown("### πŸ“Έ Input Settings")
with gr.Column(elem_classes="input-section"):
input_image = gr.Image(
type="pil",
label="πŸ–ΌοΈ Upload Your Image",
elem_classes="image-upload"
)
prompt_input = gr.Textbox(
label="✨ Animation Prompt",
value=config.default_prompt,
placeholder="Describe how you want your image to move...",
lines=2
)
duration_input = gr.Slider(
minimum=round(config.min_duration, 1),
maximum=2.5 if hasattr(spaces, 'GPU') else round(config.max_duration, 1), # Zero GPU ν™˜κ²½ μ œν•œ
step=0.1,
value=1.5, # μ•ˆμ „ν•œ κΈ°λ³Έκ°’
label="⏱️ Video Duration (seconds) - Limited to 2.5s in Zero GPU",
elem_classes="slider-container"
)
with gr.Accordion("πŸŽ›οΈ Advanced Settings", open=False, elem_classes="accordion"):
negative_prompt = gr.Textbox(
label="🚫 Negative Prompt",
value=config.default_negative_prompt,
lines=2
)
with gr.Row():
seed = gr.Slider(
minimum=0,
maximum=MAX_SEED,
step=1,
value=42,
label="🎲 Seed"
)
randomize_seed = gr.Checkbox(
label="πŸ”€ Randomize",
value=True
)
with gr.Row():
height_slider = gr.Slider(
minimum=config.slider_min_h,
maximum=config.slider_max_h,
step=config.mod_value,
value=config.default_height,
label="πŸ“ Height (max 832px in Zero GPU)"
)
width_slider = gr.Slider(
minimum=config.slider_min_w,
maximum=config.slider_max_w,
step=config.mod_value,
value=config.default_width,
label="πŸ“ Width (max 832px in Zero GPU)"
)
steps_slider = gr.Slider(
minimum=1,
maximum=30,
step=1,
value=4,
label="πŸ”§ Quality Steps (4-8 recommended)"
)
guidance_scale = gr.Slider(
minimum=0.0,
maximum=20.0,
step=0.5,
value=1.0,
label="🎯 Guidance Scale",
visible=False
)
generate_btn = gr.Button(
"🎬 Generate Video",
variant="primary",
elem_classes="generate-btn"
)
with gr.Column(scale=1):
gr.Markdown("### πŸŽ₯ Generated Video")
video_output = gr.Video(
label="",
autoplay=True,
elem_classes="video-output"
)
gr.HTML("""
<div class="footer">
<p>πŸ’‘ Tip: For best results, use clear images with good lighting and distinct subjects</p>
</div>
""")
# Examples - 파일λͺ… 확인 ν•„μš”
try:
gr.Examples(
examples=[
["peng.png", "a penguin playfully dancing in the snow, Antarctica", 512, 512],
["forg.jpg", "the frog jumps around", 576, 320], # 16:9 aspect ratio within limits
],
inputs=[input_image, prompt_input, height_slider, width_slider],
outputs=[video_output, seed],
fn=generate_video,
cache_examples=False # μΊμ‹œ λΉ„ν™œμ„±ν™”λ‘œ λ©”λͺ¨λ¦¬ μ ˆμ•½
)
except Exception as e:
logger.warning(f"Failed to load examples: {e}")
# κ°œμ„ μ‚¬ν•­ μš”μ•½ (μž‘κ²Œ)
gr.HTML("""
<div style="background: rgba(255,255,255,0.9); border-radius: 10px; padding: 15px; margin-top: 20px; font-size: 0.8em; text-align: center;">
<p style="margin: 0; color: #666;">
<strong style="color: #667eea;">Enhanced with:</strong>
πŸ›‘οΈ GPU Crash Protection β€’ ⚑ Memory Optimization β€’ 🎨 Modern UI β€’ πŸ”§ Clean Architecture
</p>
</div>
""")
# Event handlers
input_image.upload(
fn=handle_image_upload,
inputs=[input_image],
outputs=[height_slider, width_slider]
)
input_image.clear(
fn=handle_image_upload,
inputs=[input_image],
outputs=[height_slider, width_slider]
)
generate_btn.click(
fn=generate_video,
inputs=[
input_image, prompt_input, height_slider, width_slider,
negative_prompt, duration_input, guidance_scale,
steps_slider, seed, randomize_seed
],
outputs=[video_output, seed]
)
if __name__ == "__main__":
# Zero GPU ν™˜κ²½ 체크 λ‘œκΉ…
if hasattr(spaces, 'GPU'):
logger.info("Running in Zero GPU environment")
else:
logger.info("Running in standard environment")
# μ•± μ‹€ν–‰
demo.queue(concurrency_count=1) # λ™μ‹œ μ‹€ν–‰ μ œν•œ
demo.launch()