Spaces:
Running on Zero
Running on Zero
| """EverAnimate — minute-scale human animation via latent flow restoration. | |
| Faithful port of `scripts/everanimate_inference.py` + `test.sh` from | |
| https://github.com/vita-epfl/EverAnimate onto ZeroGPU. | |
| Base model : Wan-AI/Wan2.2-Animate-14B (DiffSynth layout) | |
| Adapter : epfl-vita/everanimate ckpts/everanimate-v1-lora32/stage2_480p.safetensors | |
| """ | |
| import os | |
| import sys | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| CKPT_DIR = os.path.join(BASE_DIR, "ckpts") | |
| os.makedirs(CKPT_DIR, exist_ok=True) | |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") | |
| os.environ.setdefault("DIFFSYNTH_SKIP_DOWNLOAD", "True") | |
| os.environ.setdefault("DIFFSYNTH_MODEL_BASE_PATH", CKPT_DIR) | |
| os.environ.setdefault("DIFFSYNTH_DOWNLOAD_SOURCE", "huggingface") | |
| os.environ.setdefault("NUMBA_DISABLE_CUDA", "1") | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| sys.path.insert(0, BASE_DIR) | |
| import spaces # noqa: E402 (must precede torch / CUDA-touching imports) | |
| import time # noqa: E402 | |
| import tempfile # noqa: E402 | |
| import traceback # noqa: E402 | |
| import numpy as np # noqa: E402 | |
| import torch # noqa: E402 | |
| import gradio as gr # noqa: E402 | |
| import imageio # noqa: E402 | |
| from PIL import Image # noqa: E402 | |
| from huggingface_hub import hf_hub_download, snapshot_download # noqa: E402 | |
| from diffsynth.utils.data import crop_and_resize, save_video # noqa: E402 | |
| from diffsynth.pipelines.wan_video_svi import WanVideoSviPipeline, ModelConfig # noqa: E402 | |
| # -------------------------------------------------------------------------------------- | |
| # Fixed configuration — mirrors test.sh exactly | |
| # -------------------------------------------------------------------------------------- | |
| BASE_MODEL_ID = "Wan-AI/Wan2.2-Animate-14B" | |
| LORA_REPO = "epfl-vita/everanimate" | |
| LORA_FILE = "ckpts/everanimate-v1-lora32/stage2_480p.safetensors" | |
| HEIGHT = 480 | |
| WIDTH = 832 | |
| FPS = 25 | |
| FRAMES_PER_CLIP = 77 | |
| NUM_OVERLAP_FRAME = 4 | |
| NUM_VIDEO_ANCHOR_LATENTS = 4 | |
| NUM_MOTION_LATENTS = 1 | |
| NUM_MOTION_FRAME = 1 | |
| RANDOM_ANCHOR_FRAMES = 3 | |
| CFG_SCALE = 1.0 | |
| FACE_SIZE = 512 | |
| DEFAULT_PROMPT = "视频中的人在做动作" | |
| PREPROCESS_MAX_SECONDS = 30 | |
| # -------------------------------------------------------------------------------------- | |
| # Weights | |
| # -------------------------------------------------------------------------------------- | |
| print("[boot] downloading Wan2.2-Animate-14B base weights …", flush=True) | |
| WAN_DIR = os.path.join(CKPT_DIR, "Wan-AI", "Wan2.2-Animate-14B") | |
| snapshot_download( | |
| repo_id=BASE_MODEL_ID, | |
| local_dir=WAN_DIR, | |
| allow_patterns=[ | |
| "diffusion_pytorch_model*.safetensors", | |
| "models_t5_umt5-xxl-enc-bf16.pth", | |
| "Wan2.1_VAE.pth", | |
| "models_clip_open-clip-xlm-roberta-large-vit-huge-14.pth", | |
| "process_checkpoint/det/yolov10m.onnx", | |
| "process_checkpoint/pose2d/vitpose_h_wholebody.onnx/**", | |
| ], | |
| max_workers=8, | |
| ) | |
| _vitpose_manifest = os.path.join( | |
| WAN_DIR, "process_checkpoint", "pose2d", "vitpose_h_wholebody.onnx", "end2end.onnx" | |
| ) | |
| if not os.path.isfile(_vitpose_manifest): | |
| raise RuntimeError("Failed to download the required Wan ViTPose checkpoint directory.") | |
| print("[boot] downloading umt5-xxl tokenizer …", flush=True) | |
| snapshot_download( | |
| repo_id="google/umt5-xxl", | |
| local_dir=os.path.join(CKPT_DIR, "Wan-AI", "Wan2.1-T2V-1.3B", "google", "umt5-xxl"), | |
| allow_patterns=["tokenizer*", "special_tokens_map.json", "spiece.model"], | |
| ) | |
| print("[boot] downloading EverAnimate LoRA …", flush=True) | |
| LORA_PATH = hf_hub_download(repo_id=LORA_REPO, filename=LORA_FILE) | |
| # -------------------------------------------------------------------------------------- | |
| # Pipeline (built on CPU, then moved to CUDA so ZeroGPU can pack the weights) | |
| # -------------------------------------------------------------------------------------- | |
| print("[boot] building WanVideoSviPipeline …", flush=True) | |
| _t0 = time.time() | |
| pipe = WanVideoSviPipeline.from_pretrained( | |
| torch_dtype=torch.bfloat16, | |
| device="cpu", | |
| model_configs=[ | |
| ModelConfig( | |
| model_id=BASE_MODEL_ID, | |
| origin_file_pattern="diffusion_pytorch_model*.safetensors", | |
| computation_device="cpu", | |
| ), | |
| ModelConfig( | |
| model_id=BASE_MODEL_ID, | |
| origin_file_pattern="models_t5_umt5-xxl-enc-bf16.pth", | |
| computation_device="cpu", | |
| ), | |
| ModelConfig( | |
| model_id=BASE_MODEL_ID, | |
| origin_file_pattern="Wan2.1_VAE.pth", | |
| computation_device="cpu", | |
| ), | |
| ModelConfig( | |
| model_id=BASE_MODEL_ID, | |
| origin_file_pattern="models_clip_open-clip-xlm-roberta-large-vit-huge-14.pth", | |
| computation_device="cpu", | |
| ), | |
| ], | |
| redirect_common_files=False, | |
| ) | |
| print(f"[boot] models loaded in {time.time() - _t0:.0f}s — fusing LoRA …", flush=True) | |
| pipe.load_lora(pipe.dit, LORA_PATH, alpha=1) | |
| # test.sh flags: --use_image_anchor --num_video_anchor_latents 4 --use_random_frame_anchor | |
| # --random_anchor_with_user_first --random_anchor_frames 3 | |
| # --num_motion_latents 1 --num_overlap_frame 4 --use_pingpong | |
| pipe.num_video_anchor_latents = NUM_VIDEO_ANCHOR_LATENTS | |
| pipe.use_zero_padding = False | |
| pipe.add_noise_to_motion_latent = False | |
| pipe.motion_latent_shared_noise = False | |
| pipe.bi_sink = False | |
| pipe.enable_anchor_key_focus = False | |
| pipe.remove_pose = False | |
| pipe.mask_anchor_motion = False | |
| pipe.pad_first_clip_with_anchor = False | |
| if pipe.dit is not None: | |
| pipe.dit.enable_anchor_key_focus = False | |
| print("[boot] moving pipeline to CUDA …", flush=True) | |
| pipe.to("cuda") | |
| pipe.eval() | |
| print(f"[boot] ready in {time.time() - _t0:.0f}s", flush=True) | |
| # -------------------------------------------------------------------------------------- | |
| # Helpers (ported from scripts/everanimate_inference.py) | |
| # -------------------------------------------------------------------------------------- | |
| def _to_pil_rgb(frame): | |
| if isinstance(frame, Image.Image): | |
| return frame.convert("RGB") | |
| if isinstance(frame, torch.Tensor): | |
| frame = frame.detach().cpu().numpy() | |
| if isinstance(frame, np.ndarray): | |
| arr = frame | |
| if arr.ndim == 3 and arr.shape[0] in (1, 3, 4) and arr.shape[-1] not in (1, 3, 4): | |
| arr = np.transpose(arr, (1, 2, 0)) | |
| if np.issubdtype(arr.dtype, np.floating): | |
| max_val = float(np.nanmax(arr)) if arr.size > 0 else 0.0 | |
| arr = np.clip(arr * 255.0, 0, 255) if max_val <= 1.0 else np.clip(arr, 0, 255) | |
| arr = arr.astype(np.uint8) | |
| if arr.ndim == 3 and arr.shape[2] == 1: | |
| arr = arr[:, :, 0] | |
| return Image.fromarray(arr).convert("RGB") | |
| raise TypeError(f"Unsupported frame type: {type(frame)}") | |
| def _read_frames(video_path, height, width, max_frames): | |
| """Decode at most `max_frames` frames, cropped/resized exactly like diffsynth VideoData.""" | |
| frames = [] | |
| reader = imageio.get_reader(video_path) | |
| try: | |
| for idx, raw in enumerate(reader): | |
| if idx >= max_frames: | |
| break | |
| frame = Image.fromarray(np.array(raw)).convert("RGB") | |
| if frame.size != (width, height): | |
| frame = crop_and_resize(frame, height, width) | |
| frames.append(frame) | |
| finally: | |
| reader.close() | |
| if not frames: | |
| raise gr.Error(f"Could not decode any frame from {os.path.basename(video_path)}.") | |
| return frames | |
| def _pingpong_clip(all_frames, start_frame, num_frames): | |
| """Ping-pong looping, identical to StreamingAnimateVideoProcessor.load_video_clip.""" | |
| forward = all_frames | |
| backward = all_frames[-2:0:-1] | |
| sequence = forward + backward | |
| n = len(sequence) | |
| start = start_frame % n | |
| return [sequence[(start + i) % n] for i in range(num_frames)] | |
| def _encode_single_latent(frame, tiled): | |
| frame = _to_pil_rgb(frame).resize((WIDTH, HEIGHT)) | |
| tensor = ( | |
| pipe.preprocess_image(frame) | |
| .to(pipe.device) | |
| .transpose(0, 1) | |
| .to(dtype=pipe.torch_dtype, device=pipe.device) | |
| ) | |
| latent = pipe.vae.encode([tensor], device=pipe.device, tiled=tiled)[0] | |
| latent = latent.to(dtype=pipe.torch_dtype, device=pipe.device) | |
| return latent[:, :1] | |
| def _normalize_anchor_latent_count(latent_slices, target_count): | |
| normalized = list(latent_slices[:target_count]) | |
| while len(normalized) < target_count: | |
| normalized.append(normalized[-1].clone()) | |
| return normalized | |
| def _build_video_anchor_latent(clip_frames, anchor_image): | |
| """--use_image_anchor --use_random_frame_anchor --random_anchor_with_user_first branch.""" | |
| user_anchor_latent = _encode_single_latent(anchor_image, tiled=False) | |
| num_random_slots = max(NUM_VIDEO_ANCHOR_LATENTS - 1, 0) | |
| total = len(clip_frames) | |
| if total <= 1 or num_random_slots == 0: | |
| sampled_latents = [] | |
| selected = [] | |
| else: | |
| candidate_ids = np.arange(1, total) | |
| sample_count = min(num_random_slots, len(candidate_ids)) | |
| selected = sorted( | |
| int(x) for x in np.random.choice(candidate_ids, size=sample_count, replace=False).tolist() | |
| ) | |
| sampled_latents = [_encode_single_latent(clip_frames[int(i)], tiled=True) for i in selected] | |
| latent_slices = sampled_latents + [user_anchor_latent.clone()] | |
| latent_slices = _normalize_anchor_latent_count(latent_slices, NUM_VIDEO_ANCHOR_LATENTS) | |
| print(f"Random-anchor (random + user first): indices {selected}") | |
| return torch.concat(latent_slices, dim=1) | |
| def _side_by_side(rgb_frames, pose_frames): | |
| out = [] | |
| for rgb, pose in zip(rgb_frames, pose_frames): | |
| rgb = _to_pil_rgb(rgb) | |
| pose = _to_pil_rgb(pose).resize(rgb.size) | |
| combined = Image.new("RGB", (rgb.size[0] * 2, rgb.size[1])) | |
| combined.paste(rgb, (0, 0)) | |
| combined.paste(pose, (rgb.size[0], 0)) | |
| out.append(combined) | |
| return out | |
| # -------------------------------------------------------------------------------------- | |
| # Wan direct pose + face extraction (CPU-only) | |
| # -------------------------------------------------------------------------------------- | |
| _wan_preprocessor = None | |
| def _get_wan_preprocessor(progress=None): | |
| global _wan_preprocessor | |
| if _wan_preprocessor is None: | |
| from wan_preprocess import WanDirectPreprocessor | |
| if progress is not None: | |
| progress(0.02, desc="Loading Wan YOLO detector (first use may take a moment)") | |
| _wan_preprocessor = WanDirectPreprocessor( | |
| detector_path=os.path.join(WAN_DIR, "process_checkpoint", "det", "yolov10m.onnx"), | |
| vitpose_path=os.path.join( | |
| WAN_DIR, "process_checkpoint", "pose2d", "vitpose_h_wholebody.onnx" | |
| ), | |
| progress=progress, | |
| prefer_cuda=True, | |
| ) | |
| return _wan_preprocessor | |
| def extract_driving_pair(reference_image, source_video, progress=gr.Progress()): | |
| """Generate a matched Wan pose/face driving pair from one uploaded video.""" | |
| if not reference_image: | |
| raise gr.Error("Please provide the character image before extracting a driving pair.") | |
| if not source_video: | |
| raise gr.Error("Please upload or record a single-person source video to extract its driving pair.") | |
| try: | |
| progress(0.01, desc="Preparing Wan pose extractor") | |
| output_dir = tempfile.mkdtemp(prefix="wan_preprocess_") | |
| pose_path, face_path = _get_wan_preprocessor(progress=progress).process( | |
| source_video, reference_image, output_dir, progress=progress | |
| ) | |
| return pose_path, face_path | |
| except gr.Error: | |
| raise | |
| except (RuntimeError, ValueError) as exc: | |
| raise gr.Error(str(exc)) from exc | |
| except Exception as exc: | |
| traceback.print_exc() | |
| raise gr.Error(f"Wan preprocessing failed: {exc}") from exc | |
| def use_extracted_driving_pair(pose_path, face_path): | |
| """Copy the completed extraction outputs into the animation driving inputs.""" | |
| if not pose_path or not face_path: | |
| raise gr.Error("Extract a pose and face pair before using it as driving input.") | |
| return pose_path, face_path | |
| # -------------------------------------------------------------------------------------- | |
| # Inference | |
| # -------------------------------------------------------------------------------------- | |
| def _estimate_duration( | |
| reference_image=None, | |
| pose_video=None, | |
| face_video=None, | |
| num_chunks=2, | |
| num_inference_steps=20, | |
| seed=42, | |
| prompt=DEFAULT_PROMPT, | |
| sigma_shift=5.0, | |
| show_pose=False, | |
| *args, | |
| **kwargs, | |
| ): | |
| # Measured on ZeroGPU xlarge: 1 chunk x 20 steps = 147 s (~6.5 s/step + ~17 s | |
| # of VAE encode/decode & anchor work). Keep this tight: xlarge costs 2x quota. | |
| chunks = int(num_chunks or 1) | |
| steps = int(num_inference_steps or 20) | |
| raw = 25 + chunks * 4 + chunks * (steps * 6.6 + 20) | |
| return int(min(1500, raw * 1.12)) | |
| def animate( | |
| reference_image: str, | |
| pose_video: str, | |
| face_video: str, | |
| num_chunks: int = 2, | |
| num_inference_steps: int = 20, | |
| seed: int = 42, | |
| prompt: str = DEFAULT_PROMPT, | |
| sigma_shift: float = 5.0, | |
| show_pose: bool = False, | |
| progress=gr.Progress(track_tqdm=True), | |
| ) -> str: | |
| """Animate a reference character with a Wan-Animate pose + face driving pair. | |
| Args: | |
| reference_image: Path to the still image of the character to animate. | |
| pose_video: Path to the Wan-Animate skeleton (pose) driving video. | |
| face_video: Path to the matching 512x512 cropped-face driving video. | |
| num_chunks: Number of 77-frame chunks to generate (~3 s of video each). | |
| num_inference_steps: Flow-matching denoising steps per chunk. | |
| seed: Base seed; chunk i uses seed * i. | |
| prompt: Text prompt fed to the Wan text encoder. | |
| sigma_shift: Flow-matching timestep shift. | |
| show_pose: Also render the driving skeleton next to the result. | |
| Returns: | |
| Path to the generated mp4 video. | |
| """ | |
| if not reference_image: | |
| raise gr.Error("Please provide a reference character image.") | |
| if not pose_video or not face_video: | |
| raise gr.Error("Please provide both a pose video and a matching face video.") | |
| num_chunks = max(1, int(num_chunks)) | |
| num_inference_steps = max(1, int(num_inference_steps)) | |
| seed = int(seed) | |
| prompt = (prompt or "").strip() or DEFAULT_PROMPT | |
| np.random.seed(seed if seed != 0 else 42) | |
| torch.manual_seed(seed) | |
| stride = FRAMES_PER_CLIP - NUM_OVERLAP_FRAME | |
| frames_needed = (num_chunks - 1) * stride + FRAMES_PER_CLIP | |
| print(f"[run] {num_chunks} chunk(s) x {num_inference_steps} steps, need {frames_needed} driving frames") | |
| pose_frames_all = _read_frames(pose_video, HEIGHT, WIDTH, frames_needed) | |
| face_frames_all = _read_frames(face_video, FACE_SIZE, FACE_SIZE, frames_needed) | |
| input_image = crop_and_resize(Image.open(reference_image).convert("RGB"), HEIGHT, WIDTH) | |
| anchor_image = input_image | |
| all_video_frames = [] | |
| all_pose_frames = [] | |
| current_input_image = input_image | |
| prev_last_latent = None | |
| video_anchor_latent = None | |
| prev_clip_latent_trajectory = None | |
| t_start = time.time() | |
| for clip_idx in range(num_chunks): | |
| start_frame = clip_idx * stride | |
| animate_pose_video = _pingpong_clip(pose_frames_all, start_frame, FRAMES_PER_CLIP) | |
| animate_face_video = _pingpong_clip(face_frames_all, start_frame, FRAMES_PER_CLIP) | |
| pose_frames_pil = [_to_pil_rgb(f) for f in animate_pose_video] | |
| video_anchor_for_call = ( | |
| video_anchor_latent.clone() if (clip_idx > 0 and video_anchor_latent is not None) else None | |
| ) | |
| print(f"[run] chunk {clip_idx + 1}/{num_chunks} …", flush=True) | |
| out = pipe( | |
| prompt=prompt, | |
| seed=clip_idx * seed, | |
| tiled=False, | |
| input_image=current_input_image, | |
| animate_pose_video=animate_pose_video, | |
| animate_face_video=animate_face_video, | |
| anchor=anchor_image, | |
| num_frames=FRAMES_PER_CLIP + 4 * NUM_VIDEO_ANCHOR_LATENTS, | |
| height=HEIGHT, | |
| width=WIDTH, | |
| num_inference_steps=num_inference_steps, | |
| sigma_shift=float(sigma_shift), | |
| cfg_scale=CFG_SCALE, | |
| prev_last_latent=prev_last_latent, | |
| video_anchor_latent=video_anchor_for_call, | |
| num_motion_latents=NUM_MOTION_LATENTS, | |
| use_face_anchor=False, | |
| debug_save_latents=False, | |
| clip_idx=clip_idx, | |
| num_video_anchor_latents=NUM_VIDEO_ANCHOR_LATENTS, | |
| prev_clip_latent_trajectory=prev_clip_latent_trajectory, | |
| ) | |
| video_clip = out["video"] | |
| prev_last_latent = out["prev_last_latent"] | |
| prev_clip_latent_trajectory = out.get("latent_trajectory", None) | |
| video_frames = [_to_pil_rgb(f) for f in video_clip] | |
| if clip_idx == 0: | |
| video_anchor_latent = _build_video_anchor_latent(video_frames, anchor_image) | |
| all_video_frames.extend(video_frames) | |
| all_pose_frames.extend(pose_frames_pil) | |
| else: | |
| all_video_frames.extend(video_frames[NUM_OVERLAP_FRAME:]) | |
| all_pose_frames.extend(pose_frames_pil[NUM_OVERLAP_FRAME:]) | |
| current_input_image = video_frames[-NUM_MOTION_FRAME:] | |
| print( | |
| f"[run] chunk {clip_idx + 1} done — {len(all_video_frames)} frames " | |
| f"({time.time() - t_start:.0f}s elapsed)", | |
| flush=True, | |
| ) | |
| frames = _side_by_side(all_video_frames, all_pose_frames) if show_pose else all_video_frames | |
| out_path = os.path.join(tempfile.mkdtemp(), "everanimate.mp4") | |
| save_video(frames, out_path, fps=FPS, quality=8) | |
| print( | |
| f"[run] finished: {len(all_video_frames)} frames " | |
| f"({len(all_video_frames) / FPS:.1f}s) in {time.time() - t_start:.0f}s", | |
| flush=True, | |
| ) | |
| return out_path | |
| # -------------------------------------------------------------------------------------- | |
| # UI | |
| # -------------------------------------------------------------------------------------- | |
| EX = os.path.join(BASE_DIR, "examples") | |
| DESCRIPTION = """ | |
| # EverAnimate — minute-scale human animation | |
| <a href="https://huggingface.co/epfl-vita/everanimate">model</a> · | |
| <a href="https://github.com/vita-epfl/EverAnimate">code</a> · | |
| <a href="https://arxiv.org/abs/2605.15042">paper</a> | |
| **EverAnimate** (EPFL VITA) is a LoRA on top of **Wan2.2-Animate-14B** that keeps chunk-based | |
| human animation stable over long horizons: *Persistent Latent Propagation* carries an identity / | |
| motion memory across chunks, and *Restorative Flow Matching* corrects drifted latent trajectories. | |
| Give it a **character image** plus a **Wan-Animate pose + face driving pair** and it animates your | |
| character chunk by chunk at 480×832 / 25 fps. Short driving clips are ping-pong looped, so a few | |
| seconds of motion can drive a much longer video. | |
| """ | |
| NOTE = """ | |
| > **Driving inputs.** The pose video is the Wan-Animate skeleton rendering and the face video is the | |
| > matching 512×512 face crop, both produced by | |
| > [Wan2.2-Animate's `preprocess_data.py`](https://github.com/Wan-Video/Wan2.2). Two ready-made | |
| > driving pairs from the EverAnimate release are provided below — swap in **your own character | |
| > image** and keep a provided pair to see the model transfer that motion onto your character. | |
| """ | |
| SOURCE_VIDEO_CSS = """ | |
| #source-motion-video button, | |
| #source-motion-video button svg { | |
| color: #f8fafc !important; | |
| fill: currentColor !important; | |
| } | |
| """ | |
| with gr.Blocks(title="EverAnimate") as demo: | |
| gr.Markdown(DESCRIPTION) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| reference_image = gr.Image( | |
| label="Character image (reference)", type="filepath", height=260 | |
| ) | |
| with gr.Row(): | |
| pose_video = gr.Video(label="Pose driving video", height=200) | |
| face_video = gr.Video(label="Face driving video (512×512)", height=200) | |
| with gr.Accordion("Create driving pair from a video", open=False): | |
| gr.Markdown( | |
| "Upload a clear **single-person** video (up to " | |
| f"{PREPROCESS_MAX_SECONDS} seconds). Wan preprocessing extracts a matched " | |
| "skeleton and 512×512 face crop at 25 fps." | |
| ) | |
| source_video = gr.Video( | |
| label="Source motion video", | |
| sources=["upload", "webcam"], | |
| height=200, | |
| format="mp4", | |
| elem_id="source-motion-video", | |
| ) | |
| extract_btn = gr.Button("Extract pose and face", variant="secondary") | |
| with gr.Row(): | |
| extracted_pose = gr.Video(label="Extracted pose preview", height=180) | |
| extracted_face = gr.Video(label="Extracted face preview", height=180) | |
| use_extracted_btn = gr.Button("Use extracted driving pair") | |
| num_chunks = gr.Slider( | |
| 1, 6, value=2, step=1, | |
| label="Chunks to generate", | |
| info="Each chunk is 77 frames ≈ 2.9 s of video at 25 fps (~2.5 min of GPU each).", | |
| ) | |
| run_btn = gr.Button("Animate", variant="primary") | |
| gr.Markdown(NOTE) | |
| with gr.Column(scale=1): | |
| output_video = gr.Video(label="EverAnimate result", height=380, autoplay=True) | |
| with gr.Accordion("Advanced settings", open=False): | |
| num_inference_steps = gr.Slider( | |
| 4, 30, value=20, step=1, label="Denoising steps per chunk" | |
| ) | |
| seed = gr.Slider( | |
| 0, 100000, value=42, step=1, label="Seed", | |
| info="Faithful to the reference script: chunk i is sampled with seed × i.", | |
| ) | |
| sigma_shift = gr.Slider(1.0, 12.0, value=5.0, step=0.5, label="Sigma shift") | |
| prompt = gr.Textbox( | |
| label="Prompt", value=DEFAULT_PROMPT, | |
| info="Default is the paper's prompt: “the person in the video is performing an action”.", | |
| ) | |
| show_pose = gr.Checkbox( | |
| value=False, label="Show driving skeleton side by side" | |
| ) | |
| inputs = [ | |
| reference_image, pose_video, face_video, num_chunks, | |
| num_inference_steps, seed, prompt, sigma_shift, show_pose, | |
| ] | |
| run_btn.click(fn=animate, inputs=inputs, outputs=output_video) | |
| extract_btn.click( | |
| fn=extract_driving_pair, | |
| inputs=[reference_image, source_video], | |
| outputs=[extracted_pose, extracted_face], | |
| ) | |
| use_extracted_btn.click( | |
| fn=use_extracted_driving_pair, | |
| inputs=[extracted_pose, extracted_face], | |
| outputs=[pose_video, face_video], | |
| ) | |
| gr.Examples( | |
| examples=[ | |
| [ | |
| os.path.join(EX, "demo_character.png"), | |
| os.path.join(EX, "demo_pose.mp4"), | |
| os.path.join(EX, "demo_face.mp4"), | |
| ], | |
| [ | |
| os.path.join(EX, "sample_character.png"), | |
| os.path.join(EX, "sample_pose.mp4"), | |
| os.path.join(EX, "sample_face.mp4"), | |
| ], | |
| ], | |
| inputs=[reference_image, pose_video, face_video], | |
| label="Official EverAnimate demo inputs", | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue().launch( | |
| theme=gr.themes.Citrus(), css=SOURCE_VIDEO_CSS, mcp_server=True, show_error=True | |
| ) | |