| 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 |
|
|
|
|
| |
| |
| |
|
|
| MODEL_ID = "Wan-AI/Wan2.2-Animate-14B-Diffusers" |
|
|
| OUTPUT_DIR = "/tmp/outputs" |
| os.makedirs(OUTPUT_DIR, exist_ok=True) |
|
|
| pipe = None |
|
|
|
|
| |
| |
| |
|
|
| 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, |
| }, |
| } |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
| @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." |
| ) |
|
|
| |
| |
| |
|
|
| progress( |
| 0.05, |
| desc="Loading Wan2.2 Animate..." |
| ) |
|
|
| pipe = get_pipeline() |
|
|
| |
| |
| |
|
|
| config = PRESETS[preset] |
|
|
| if not prompt or not prompt.strip(): |
| prompt = config["prompt"] |
|
|
| |
| |
| |
|
|
| progress( |
| 0.15, |
| desc="Loading reference image..." |
| ) |
|
|
| image = load_image( |
| reference_image |
| ) |
|
|
| progress( |
| 0.25, |
| desc="Loading motion video..." |
| ) |
|
|
| motion = load_video( |
| motion_video |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| pose_video = motion |
| face_video = motion |
|
|
| |
| |
| |
|
|
| 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] |
|
|
| |
| |
| |
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
| def update_preset(preset): |
|
|
| config = PRESETS[preset] |
|
|
| return ( |
| config["prompt"], |
| config["steps"], |
| config["guidance"], |
| ) |
|
|
|
|
| |
| |
| |
|
|
| 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", |
| ) |
|
|
| |
| |
| |
|
|
| 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", |
| ) |
|
|
| |
| |
| |
|
|
| 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, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| demo.queue( |
| max_size=10, |
| default_concurrency_limit=1, |
| ) |
|
|
| demo.launch() |
|
|