Spaces:
Running on Zero
Running on Zero
multimodalart HF Staff
Run graph under torch.inference_mode() (fix inference-tensor inplace error)
178c0e6 verified | import os | |
| # Allocator config for the video-DiT transient spikes (see zerogpu known-errors). | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| import spaces # noqa: E402 MUST be before torch / any CUDA-touching import | |
| import sys | |
| import shutil | |
| import subprocess | |
| import tempfile | |
| import random | |
| from pathlib import Path | |
| import torch # noqa: E402 | |
| import gradio as gr # noqa: E402 | |
| from huggingface_hub import hf_hub_download, snapshot_download # noqa: E402 | |
| # ---------------------------------------------------------------------------- | |
| # 0. Vendor ComfyUI (the reference implementation lives entirely in ComfyUI's | |
| # Wan S2V nodes + the Bernini custom node). We drive its node `execute`/ | |
| # FUNCTION methods directly ("ComfyUI as a library") rather than running the | |
| # server, so everything happens in-process and works with the ZeroGPU hijack. | |
| # ---------------------------------------------------------------------------- | |
| COMFY_COMMIT = "04a30fb375a6c1312365fbbbdd0a7d0669212e92" | |
| COMFY_DIR = Path(__file__).parent / "ComfyUI" | |
| if not COMFY_DIR.exists(): | |
| subprocess.run( | |
| ["git", "clone", "https://github.com/comfyanonymous/ComfyUI.git", str(COMFY_DIR)], | |
| check=True, | |
| ) | |
| subprocess.run(["git", "checkout", COMFY_COMMIT], cwd=str(COMFY_DIR), check=True) | |
| sys.path.insert(0, str(COMFY_DIR)) | |
| # ---------------------------------------------------------------------------- | |
| # 1. Candidate + dependency model IDs | |
| # ---------------------------------------------------------------------------- | |
| BERNINI_REPO = "rzgar/Bernini-R-S2V" | |
| LORA_REPO = "rzgar/Bernini-R-LightX2V-4step-loras" | |
| COMFY_WAN22 = "Comfy-Org/Wan_2.2_ComfyUI_Repackaged" | |
| COMFY_WAN21 = "Comfy-Org/Wan_2.1_ComfyUI_repackaged" | |
| MODELS_ROOT = Path(os.environ.get("COMFY_MODELS", "/data/models")) | |
| try: | |
| MODELS_ROOT.mkdir(parents=True, exist_ok=True) | |
| except Exception: | |
| MODELS_ROOT = Path(tempfile.gettempdir()) / "comfy_models" | |
| MODELS_ROOT.mkdir(parents=True, exist_ok=True) | |
| DIRS = { | |
| "diffusion_models": MODELS_ROOT / "diffusion_models", | |
| "text_encoders": MODELS_ROOT / "text_encoders", | |
| "vae": MODELS_ROOT / "vae", | |
| "audio_encoders": MODELS_ROOT / "audio_encoders", | |
| "loras": MODELS_ROOT / "loras", | |
| } | |
| for d in DIRS.values(): | |
| d.mkdir(parents=True, exist_ok=True) | |
| def _fetch(repo, filename, dest_dir, out_name=None): | |
| out = DIRS[dest_dir] / (out_name or Path(filename).name) | |
| p = hf_hub_download(repo_id=repo, filename=filename) | |
| if out.exists() or out.is_symlink(): | |
| out.unlink() | |
| # symlink into ComfyUI's model dir (avoid copying tens of GB) | |
| try: | |
| out.symlink_to(p) | |
| except OSError: | |
| shutil.copy(p, out) | |
| return out | |
| print("Downloading model weights (this happens once) ...", flush=True) | |
| HIGH = _fetch(BERNINI_REPO, "Bernini-R-S2V-FP8/wan2.2_bernini_r_high_noise_fp8_scaled_s2v.safetensors", | |
| "diffusion_models", "bernini_high_fp8_s2v.safetensors") | |
| LOW = _fetch(BERNINI_REPO, "Bernini-R-S2V-FP8/wan2.2_bernini_r_low_noise_fp8_scaled_s2v.safetensors", | |
| "diffusion_models", "bernini_low_fp8_s2v.safetensors") | |
| WAV2VEC = _fetch(BERNINI_REPO, "audio_encoders/wav2vec2_large_english_fp16.safetensors", | |
| "audio_encoders") | |
| UMT5 = _fetch(COMFY_WAN22, "split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors", | |
| "text_encoders") | |
| VAE_FILE = _fetch(COMFY_WAN21, "split_files/vae/wan_2.1_vae.safetensors", "vae") | |
| LORA_HIGH = _fetch(LORA_REPO, "Bernini-R_LightX2V_high_noise.safetensors", "loras") | |
| LORA_LOW = _fetch(LORA_REPO, "Bernini-R_LightX2V_low_noise.safetensors", "loras") | |
| print("All weights present.", flush=True) | |
| # ---------------------------------------------------------------------------- | |
| # 2. Point ComfyUI's folder_paths at our model dirs, then import nodes. | |
| # ---------------------------------------------------------------------------- | |
| import folder_paths # noqa: E402 | |
| for key, d in DIRS.items(): | |
| folder_paths.add_model_folder_path(key, str(d)) | |
| import asyncio # noqa: E402 | |
| import nodes as comfy_nodes # noqa: E402 | |
| # init_extra_nodes is async in this ComfyUI revision. | |
| _res = comfy_nodes.init_extra_nodes(init_custom_nodes=False, init_api_nodes=False) | |
| if asyncio.iscoroutine(_res): | |
| asyncio.new_event_loop().run_until_complete(_res) | |
| import comfy.model_management as mm # noqa: E402 | |
| import comfy.utils # noqa: E402 | |
| import node_helpers # noqa: E402 | |
| from comfy_extras.nodes_wan import get_audio_embed_bucket_fps, linear_interpolation # noqa: E402 | |
| # ---------------------------------------------------------------------------- | |
| # 3. Apply the Bernini S2V model patch (adds context_latents support to the | |
| # Wan S2V forward). Ported verbatim from the candidate's custom node so the | |
| # output matches the ComfyUI reference path. | |
| # ---------------------------------------------------------------------------- | |
| import inspect # noqa: E402 | |
| import logging # noqa: E402 | |
| def _append_context_latents(self, x, kwargs): | |
| context_latents = kwargs.get("context_latents", None) | |
| if context_latents is None: | |
| return x | |
| for lat in context_latents: | |
| cl = self.patch_embedding(lat.float().to(x.device)).to(x.dtype).flatten(2).transpose(1, 2) | |
| x = torch.cat([x, cl], dim=1) | |
| return x | |
| def _apply_bernini_patch(): | |
| from comfy.ldm.wan.model import WanModel_S2V, sinusoidal_embedding_1d # noqa: F401 | |
| try: | |
| source = inspect.getsource(WanModel_S2V.forward_orig) | |
| except (OSError, TypeError): | |
| source = "" | |
| if "context_latents" in source: | |
| return | |
| if getattr(WanModel_S2V.forward_orig, "__wan_bernini_s2v_patch__", False): | |
| return | |
| original = WanModel_S2V.forward_orig | |
| def forward_orig(self, x, t, context, audio_embed=None, reference_latent=None, | |
| control_video=None, reference_motion=None, clip_fea=None, freqs=None, | |
| transformer_options={}, **kwargs): | |
| from comfy.ldm.wan.model import sinusoidal_embedding_1d | |
| if audio_embed is not None: | |
| num_embeds = x.shape[-3] * 4 | |
| audio_emb_global, audio_emb = self.casual_audio_encoder(audio_embed[:, :, :, :num_embeds]) | |
| else: | |
| audio_emb = None | |
| audio_emb_global = None | |
| bs, _, time, height, width = x.shape | |
| x = self.patch_embedding(x.float()).to(x.dtype) | |
| if control_video is not None: | |
| x = x + self.cond_encoder(control_video) | |
| if t.ndim == 1: | |
| t = t.unsqueeze(1).repeat(1, x.shape[2]) | |
| grid_sizes = x.shape[2:] | |
| x = x.flatten(2).transpose(1, 2) | |
| seq_len = x.size(1) | |
| cond_mask_weight = mm.cast_to(self.trainable_cond_mask.weight, dtype=x.dtype, | |
| device=x.device).unsqueeze(1).unsqueeze(1) | |
| x = x + cond_mask_weight[0] | |
| x = _append_context_latents(self, x, kwargs) | |
| if reference_latent is not None: | |
| ref = self.patch_embedding(reference_latent.float()).to(x.dtype) | |
| ref = ref.flatten(2).transpose(1, 2) | |
| freqs_ref = self.rope_encode(reference_latent.shape[-3], reference_latent.shape[-2], | |
| reference_latent.shape[-1], t_start=max(30, time + 9), | |
| device=x.device, dtype=x.dtype) | |
| ref = ref + cond_mask_weight[1] | |
| x = torch.cat([x, ref], dim=1) | |
| freqs = torch.cat([freqs, freqs_ref], dim=1) | |
| t = torch.cat([t, torch.zeros((t.shape[0], reference_latent.shape[-3]), | |
| device=t.device, dtype=t.dtype)], dim=1) | |
| if reference_motion is not None: | |
| motion_encoded, freqs_motion = self.frame_packer(reference_motion, self) | |
| motion_encoded = motion_encoded + cond_mask_weight[2] | |
| x = torch.cat([x, motion_encoded], dim=1) | |
| freqs = torch.cat([freqs, freqs_motion], dim=1) | |
| t = torch.repeat_interleave(t, 2, dim=1) | |
| t = torch.cat([t, torch.zeros((t.shape[0], 3), device=t.device, dtype=t.dtype)], dim=1) | |
| e = self.time_embedding( | |
| sinusoidal_embedding_1d(self.freq_dim, t.flatten()).to(dtype=x[0].dtype)) | |
| e = e.reshape(t.shape[0], -1, e.shape[-1]) | |
| e0 = self.time_projection(e).unflatten(2, (6, self.dim)) | |
| context = self.text_embedding(context) | |
| patches_replace = transformer_options.get("patches_replace", {}) | |
| blocks_replace = patches_replace.get("dit", {}) | |
| transformer_options["total_blocks"] = len(self.blocks) | |
| transformer_options["block_type"] = "double" | |
| for i, block in enumerate(self.blocks): | |
| transformer_options["block_index"] = i | |
| if ("double_block", i) in blocks_replace: | |
| def block_wrap(args): | |
| out = {} | |
| out["img"] = block(args["img"], context=args["txt"], e=args["vec"], | |
| freqs=args["pe"], transformer_options=args["transformer_options"]) | |
| return out | |
| out = blocks_replace[("double_block", i)]( | |
| {"img": x, "txt": context, "vec": e0, "pe": freqs, | |
| "transformer_options": transformer_options}, | |
| {"original_block": block_wrap}) | |
| x = out["img"] | |
| else: | |
| x = block(x, e=e0, freqs=freqs, context=context, transformer_options=transformer_options) | |
| if audio_emb is not None: | |
| x = self.audio_injector(x, i, audio_emb, audio_emb_global, seq_len) | |
| x = self.head(x, e) | |
| x = self.unpatchify(x, grid_sizes) | |
| return x | |
| forward_orig.__wan_bernini_s2v_patch__ = True | |
| forward_orig.__wan_bernini_s2v_original__ = original | |
| WanModel_S2V.forward_orig = forward_orig | |
| logging.info("Applied Bernini S2V context_latents patch") | |
| # ---------------------------------------------------------------------------- | |
| # 4. Load models at module scope (ZeroGPU eager pattern). | |
| # ---------------------------------------------------------------------------- | |
| NODES = comfy_nodes.NODE_CLASS_MAPPINGS | |
| print("Loading models ...", flush=True) | |
| _apply_bernini_patch() | |
| clip = NODES["CLIPLoader"]().load_clip("umt5_xxl_fp8_e4m3fn_scaled.safetensors", "wan", "default")[0] | |
| vae = NODES["VAELoader"]().load_vae("wan_2.1_vae.safetensors")[0] | |
| audio_encoder = NODES["AudioEncoderLoader"].execute("wav2vec2_large_english_fp16.safetensors").result[0] | |
| _model_high_base = NODES["UNETLoader"]().load_unet("bernini_high_fp8_s2v.safetensors", "default")[0] | |
| _model_low_base = NODES["UNETLoader"]().load_unet("bernini_low_fp8_s2v.safetensors", "default")[0] | |
| # Fuse the LightX2V 4-step distillation LoRAs (this is what makes 4-step, | |
| # sub-minute inference feasible and matches the reference workflow). | |
| lora_node = NODES["LoraLoaderModelOnly"]() | |
| model_high = lora_node.load_lora_model_only(_model_high_base, "Bernini-R_LightX2V_high_noise.safetensors", 1.0)[0] | |
| model_low = lora_node.load_lora_model_only(_model_low_base, "Bernini-R_LightX2V_low_noise.safetensors", 1.0)[0] | |
| # ModelSamplingSD3 shift = 8 (from the reference workflow). | |
| msd3 = NODES["ModelSamplingSD3"]() | |
| model_high = msd3.patch(model_high, 8.0)[0] | |
| model_low = msd3.patch(model_low, 8.0)[0] | |
| print("Models loaded.", flush=True) | |
| _reencode = NODES["CLIPTextEncode"]() | |
| _ksampler = NODES["KSamplerAdvanced"]() | |
| _vaedecode = NODES["VAEDecode"]() | |
| _vaedecode_tiled = NODES["VAEDecodeTiled"]() | |
| DEFAULT_NEG = ("Vivid color tone, overexposed, static, unclear details, subtitles, style, artwork, " | |
| "painting, image, still, overall grayish, worst quality, low quality, leftover JPEG " | |
| "compression artifacts, ugly, incomplete, missing parts, extra fingers, poorly drawn " | |
| "hands, poorly drawn face, disfigured, malformed body parts, fused fingers, a completely " | |
| "motionless image, messy background, three legs, many people in the background, walking backward.") | |
| VIDEO_FPS = 16 # generation fps (matches the S2V audio bucketing) | |
| # ---------------------------------------------------------------------------- | |
| # 5. Helpers: load image / audio into ComfyUI tensor formats. | |
| # ---------------------------------------------------------------------------- | |
| def _load_image_tensor(path): | |
| from PIL import Image, ImageOps | |
| import numpy as np | |
| img = Image.open(path) | |
| img = ImageOps.exif_transpose(img).convert("RGB") | |
| arr = np.array(img).astype("float32") / 255.0 | |
| return torch.from_numpy(arr)[None,] # [1, H, W, 3] | |
| def _load_audio(path, max_seconds): | |
| # Use soundfile directly: newer torchaudio.load dispatches to torchcodec | |
| # (which isn't installed on the Space) and raises ImportError. | |
| import soundfile as sf | |
| import numpy as np | |
| data, sr = sf.read(path, dtype="float32", always_2d=True) # (frames, channels) | |
| waveform = torch.from_numpy(np.ascontiguousarray(data.T)) # (channels, frames) | |
| if max_seconds: | |
| waveform = waveform[:, : int(sr * max_seconds)] | |
| return {"waveform": waveform.unsqueeze(0), "sample_rate": sr} | |
| def _bernini_s2v_conditioning(positive, negative, width, height, length, | |
| audio_encoder_output, ref_image, ref_max_size=848): | |
| """Port of BerniniS2VConditioning.execute for the single-reference-image case.""" | |
| latent_t = ((length - 1) // 4) + 1 | |
| # audio conditioning | |
| if audio_encoder_output is not None: | |
| feat = torch.cat(audio_encoder_output["encoded_audio_all_layers"]) | |
| video_rate = 30 | |
| fps = 16 | |
| feat = linear_interpolation(feat, input_fps=50, output_fps=video_rate) | |
| batch_frames = latent_t * 4 | |
| audio_embed_bucket, _ = get_audio_embed_bucket_fps(feat, fps=fps, batch_frames=batch_frames, | |
| m=0, video_rate=video_rate) | |
| audio_embed_bucket = audio_embed_bucket.unsqueeze(0) | |
| if len(audio_embed_bucket.shape) == 3: | |
| audio_embed_bucket = audio_embed_bucket.permute(0, 2, 1) | |
| elif len(audio_embed_bucket.shape) == 4: | |
| audio_embed_bucket = audio_embed_bucket.permute(0, 2, 3, 1) | |
| audio_embed_bucket = audio_embed_bucket[:, :, :, 0:batch_frames] | |
| if audio_embed_bucket.shape[3] > 0: | |
| positive = node_helpers.conditioning_set_values(positive, {"audio_embed": audio_embed_bucket}) | |
| negative = node_helpers.conditioning_set_values(negative, {"audio_embed": audio_embed_bucket * 0.0}) | |
| latent = torch.zeros([1, 16, latent_t, height // 8, width // 8], | |
| device=mm.intermediate_device()) | |
| context = [] | |
| if ref_image is not None: | |
| h, w = ref_image.shape[1], ref_image.shape[2] | |
| scale = min(ref_max_size / max(h, w), 1.0) | |
| stride = 16 | |
| nh = max(stride, round(h * scale / stride) * stride) | |
| nw = max(stride, round(w * scale / stride) * stride) | |
| img = comfy.utils.common_upscale(ref_image[:, :, :, :3].movedim(-1, 1), nw, nh, | |
| "area", "disabled").movedim(1, -1) | |
| context.append(vae.encode(img[:, :, :, :3])) | |
| if context: | |
| positive = node_helpers.conditioning_set_values(positive, {"context_latents": context}) | |
| negative = node_helpers.conditioning_set_values(negative, {"context_latents": context}) | |
| return positive, negative, {"samples": latent} | |
| def _estimate(image, audio, seconds, *args, **kwargs): | |
| seconds = int(seconds) if seconds else 5 | |
| return min(300, 90 + seconds * 22) | |
| def generate(image, audio, seconds=5, prompt="", width=832, height=480, | |
| seed=0, randomize_seed=True, | |
| progress=gr.Progress(track_tqdm=True)): | |
| """Generate a lip-synced talking-head video from a reference image and speech audio. | |
| Args: | |
| image: reference image (a portrait / character to animate). | |
| audio: speech audio file to drive the lip-sync and motion. | |
| seconds: length of the output video in seconds (max ~10). | |
| prompt: optional text prompt describing the scene / action. | |
| width: output width in pixels. | |
| height: output height in pixels. | |
| seed: RNG seed. | |
| randomize_seed: pick a fresh random seed each run. | |
| """ | |
| if image is None or audio is None: | |
| raise gr.Error("Please provide both a reference image and an audio file.") | |
| if randomize_seed: | |
| seed = random.randint(0, 2**31 - 1) | |
| seed = int(seed) | |
| seconds = max(1, min(10, int(seconds))) | |
| # length must be 4k+1 frames; 16 fps generation. | |
| length = int(round(seconds * VIDEO_FPS)) | |
| length = ((length - 1) // 4) * 4 + 1 | |
| width = int(width) // 16 * 16 | |
| height = int(height) // 16 * 16 | |
| # Run the whole graph under inference_mode, matching ComfyUI's own | |
| # execution context (its samplers create inference-mode tensors; decoding | |
| # outside that context otherwise raises "Inplace update to inference | |
| # tensor outside InferenceMode is not allowed"). | |
| with torch.inference_mode(): | |
| ref_img = _load_image_tensor(image) | |
| audio_dict = _load_audio(audio, seconds) | |
| aenc = NODES["AudioEncoderEncode"].execute(audio_encoder, audio_dict).result[0] | |
| positive = _reencode.encode(clip, prompt or "")[0] | |
| negative = _reencode.encode(clip, DEFAULT_NEG)[0] | |
| positive, negative, latent = _bernini_s2v_conditioning( | |
| positive, negative, width, height, length, aenc, ref_img) | |
| # Two-expert MoE sampling: high-noise (steps 0-2) then low-noise (steps 2-4). | |
| # 4 total steps, cfg=1 (LightX2V distilled), dpmpp_2m_sde / sgm_uniform. | |
| stage1 = _ksampler.sample( | |
| model_high, "enable", seed, 4, 1.0, "dpmpp_2m_sde", "sgm_uniform", | |
| positive, negative, latent, start_at_step=0, end_at_step=2, | |
| return_with_leftover_noise="enable")[0] | |
| stage2 = _ksampler.sample( | |
| model_low, "disable", seed, 4, 1.0, "dpmpp_2m_sde", "sgm_uniform", | |
| positive, negative, stage1, start_at_step=2, end_at_step=4, | |
| return_with_leftover_noise="disable")[0] | |
| # Tiled decode keeps the video-VAE peak VRAM bounded (avoids allocator | |
| # fragmentation / NVML assert on the transient decode spike). | |
| images = _vaedecode_tiled.decode( | |
| vae, stage2, tile_size=256, overlap=64, | |
| temporal_size=16, temporal_overlap=8, | |
| )[0] # [T, H, W, 3] | |
| images = images.detach().clone() | |
| # write to mp4 with audio, at generation fps | |
| out_path = _write_video(images, audio_dict, VIDEO_FPS) | |
| return out_path, seed | |
| def _write_video(images, audio_dict, fps): | |
| import numpy as np | |
| import av | |
| frames = (images.cpu().numpy() * 255.0).clip(0, 255).astype("uint8") # [T,H,W,3] | |
| T, H, W, _ = frames.shape | |
| out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name | |
| container = av.open(out_path, mode="w") | |
| vstream = container.add_stream("libx264", rate=fps) | |
| vstream.width = W | |
| vstream.height = H | |
| vstream.pix_fmt = "yuv420p" | |
| vstream.options = {"crf": "19"} | |
| # audio stream (resample waveform to fit) | |
| wav = audio_dict["waveform"][0] # [C, N] | |
| sr = audio_dict["sample_rate"] | |
| if wav.shape[0] > 1: | |
| wav = wav.mean(dim=0, keepdim=True) | |
| astream = container.add_stream("aac", rate=sr) | |
| astream.layout = "mono" | |
| for i in range(T): | |
| frame = av.VideoFrame.from_ndarray(frames[i], format="rgb24") | |
| for packet in vstream.encode(frame): | |
| container.mux(packet) | |
| for packet in vstream.encode(): | |
| container.mux(packet) | |
| audio_np = wav.numpy().astype("float32") | |
| aframe = av.AudioFrame.from_ndarray(audio_np, format="fltp", layout="mono") | |
| aframe.sample_rate = sr | |
| for packet in astream.encode(aframe): | |
| container.mux(packet) | |
| for packet in astream.encode(): | |
| container.mux(packet) | |
| container.close() | |
| return out_path | |
| # ---------------------------------------------------------------------------- | |
| # 6. UI | |
| # ---------------------------------------------------------------------------- | |
| CSS = """ | |
| #col-container { max-width: 1100px; margin: 0 auto; } | |
| .dark .gradio-container { color: var(--body-text-color); } | |
| """ | |
| with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo: | |
| with gr.Column(elem_id="col-container"): | |
| gr.Markdown( | |
| "# 🎙️ Bernini-R-S2V — Talking Head\n" | |
| "Speech-driven video generation on **[rzgar/Bernini-R-S2V]" | |
| "(https://huggingface.co/rzgar/Bernini-R-S2V)** (Wan2.2 S2V grafted onto Bernini-R). " | |
| "Upload a portrait and a speech clip to get a lip-synced talking-head video. " | |
| "4-step LightX2V distilled sampling for fast inference on ZeroGPU." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| image = gr.Image(label="Reference image (portrait)", type="filepath", sources=["upload"]) | |
| audio = gr.Audio(label="Speech audio (mono, clear speech)", type="filepath", sources=["upload"]) | |
| seconds = gr.Slider(1, 10, value=5, step=1, label="Video length (seconds)") | |
| prompt = gr.Textbox(label="Prompt (optional)", placeholder="a person talking, cinematic") | |
| run = gr.Button("Generate", variant="primary") | |
| with gr.Column(): | |
| out_video = gr.Video(label="Result") | |
| used_seed = gr.Number(label="Seed used", interactive=False) | |
| with gr.Accordion("Advanced settings", open=False): | |
| with gr.Row(): | |
| width = gr.Slider(320, 1280, value=832, step=16, label="Width") | |
| height = gr.Slider(320, 1280, value=480, step=16, label="Height") | |
| with gr.Row(): | |
| seed = gr.Number(label="Seed", value=0, precision=0) | |
| randomize_seed = gr.Checkbox(label="Randomize seed", value=True) | |
| run.click( | |
| fn=generate, | |
| inputs=[image, audio, seconds, prompt, width, height, seed, randomize_seed], | |
| outputs=[out_video, used_seed], | |
| api_name="generate", | |
| ) | |
| gr.Examples( | |
| examples=[ | |
| ["examples/demo_face.jpg", "examples/scream_mono.wav", 5, | |
| "a person expressing intense emotion, cinematic"], | |
| ], | |
| inputs=[image, audio, seconds, prompt], | |
| outputs=[out_video, used_seed], | |
| fn=generate, | |
| cache_examples=False, | |
| run_on_click=True, | |
| ) | |
| demo.launch(mcp_server=True) | |