wan / app.py
68ed8100's picture
1
7da68af
Raw
History Blame Contribute Delete
9.1 kB
import os
import gc
import torch
import gradio as gr
import spaces
import ftfy
from diffusers import WanAnimatePipeline
from diffusers.utils import load_image, load_video, export_to_video
# ============================================================
# CONFIG
# ============================================================
MODEL_ID = "Wan-AI/Wan2.2-Animate-14B-Diffusers"
OUTPUT_DIR = "/tmp/outputs"
os.makedirs(OUTPUT_DIR, exist_ok=True)
pipe = None
# ============================================================
# PRESETS
# ============================================================
PRESETS = {
"Realistic": {
"prompt": (
"A highly realistic video of the reference character "
"performing the movements from the input motion video. "
"Preserve identity, facial appearance, hairstyle and clothing. "
"Natural body motion, realistic lighting and physics."
),
"steps": 20,
"guidance": 1.0,
},
"Cinematic": {
"prompt": (
"A cinematic photorealistic video of the character performing "
"the exact movements and actions from the reference motion video. "
"Natural facial expressions, realistic skin, detailed clothing, "
"cinematic lighting, shallow depth of field, professional camera."
),
"steps": 20,
"guidance": 1.0,
},
"Portrait": {
"prompt": (
"A photorealistic portrait video of the reference character. "
"Preserve the character's identity and facial features while "
"accurately following the body and facial motion from the input video. "
"Natural expression, realistic skin and cinematic portrait lighting."
),
"steps": 20,
"guidance": 1.0,
},
"Anime": {
"prompt": (
"An anime-style cinematic video featuring the reference character, "
"accurately reproducing the movement and performance from the input "
"motion video. Consistent character identity, expressive animation, "
"detailed anime background."
),
"steps": 20,
"guidance": 1.0,
},
}
# ============================================================
# MODEL LOADING
# ============================================================
def get_pipeline():
global pipe
if pipe is None:
print("Loading Wan2.2 Animate...")
pipe = WanAnimatePipeline.from_pretrained(
MODEL_ID,
torch_dtype=torch.bfloat16,
)
pipe.enable_model_cpu_offload()
print("Wan2.2 Animate loaded.")
return pipe
# ============================================================
# GENERATION
# ============================================================
@spaces.GPU
def generate_video(
prompt,
reference_image,
motion_video,
preset,
seed,
inference_steps,
guidance_scale,
progress=gr.Progress(),
):
if reference_image is None:
raise gr.Error(
"Please upload a reference image."
)
if motion_video is None:
raise gr.Error(
"Please upload a motion video."
)
# --------------------------------------------------------
# LOAD MODEL AFTER ZERO GPU ALLOCATION
# --------------------------------------------------------
progress(
0.05,
desc="Loading Wan2.2 Animate..."
)
pipe = get_pipeline()
# --------------------------------------------------------
# PRESET
# --------------------------------------------------------
config = PRESETS[preset]
if not prompt or not prompt.strip():
prompt = config["prompt"]
# --------------------------------------------------------
# LOAD INPUTS
# --------------------------------------------------------
progress(
0.15,
desc="Loading reference image..."
)
image = load_image(
reference_image
)
progress(
0.25,
desc="Loading motion video..."
)
motion = load_video(
motion_video
)
# --------------------------------------------------------
# TEMPORARY CONDITIONING
#
# NOTE:
# For the real Wan2.2 Animate workflow, the motion video
# should be processed into dedicated pose/face inputs.
# This is the basic prototype.
# --------------------------------------------------------
pose_video = motion
face_video = motion
# --------------------------------------------------------
# GENERATE
# --------------------------------------------------------
progress(
0.35,
desc="Generating video..."
)
generator = torch.Generator(
device="cuda"
).manual_seed(
int(seed)
)
with torch.inference_mode():
result = pipe(
image=image,
pose_video=pose_video,
face_video=face_video,
prompt=prompt,
mode="animate",
segment_frame_length=77,
prev_segment_conditioning_frames=1,
guidance_scale=float(
guidance_scale
),
num_inference_steps=int(
inference_steps
),
generator=generator,
).frames[0]
# --------------------------------------------------------
# EXPORT
# --------------------------------------------------------
progress(
0.9,
desc="Encoding video..."
)
output_path = os.path.join(
OUTPUT_DIR,
f"output_{int(seed)}.mp4"
)
export_to_video(
result,
output_path,
fps=30,
)
progress(
1.0,
desc="Done"
)
gc.collect()
return output_path
# ============================================================
# PRESET UPDATE
# ============================================================
def update_preset(preset):
config = PRESETS[preset]
return (
config["prompt"],
config["steps"],
config["guidance"],
)
# ============================================================
# GRADIO UI
# ============================================================
with gr.Blocks(
title="Wan2.2 Character Animation"
) as demo:
gr.Markdown(
"""
# 🎬 Wan2.2 Character Animation
**Reference Photo + Motion Video + Prompt → Video**
The reference photo provides the character identity.
The motion video provides the movement.
"""
)
with gr.Row():
with gr.Column():
prompt = gr.Textbox(
label="Prompt",
lines=5,
placeholder=(
"Describe the generated video..."
),
)
preset = gr.Dropdown(
choices=list(
PRESETS.keys()
),
value="Realistic",
label="Style Preset",
)
reference_image = gr.Image(
label="Reference Photo",
type="filepath",
)
motion_video = gr.Video(
label="Motion Video",
sources=["upload"],
)
with gr.Column():
output_video = gr.Video(
label="Generated Video",
autoplay=True,
)
generate_button = gr.Button(
"🎬 Generate",
variant="primary",
)
# ========================================================
# ADVANCED
# ========================================================
with gr.Accordion(
"Advanced Settings",
open=False,
):
seed = gr.Number(
label="Seed",
value=42,
precision=0,
)
inference_steps = gr.Slider(
minimum=5,
maximum=50,
value=20,
step=1,
label="Inference Steps",
)
guidance_scale = gr.Slider(
minimum=0.5,
maximum=5.0,
value=1.0,
step=0.1,
label="Guidance Scale",
)
# ========================================================
# EVENTS
# ========================================================
preset.change(
fn=update_preset,
inputs=preset,
outputs=[
prompt,
inference_steps,
guidance_scale,
],
)
generate_button.click(
fn=generate_video,
inputs=[
prompt,
reference_image,
motion_video,
preset,
seed,
inference_steps,
guidance_scale,
],
outputs=output_video,
)
# ============================================================
# LAUNCH
# ============================================================
demo.queue(
max_size=10,
default_concurrency_limit=1,
)
demo.launch()