Spaces:
Running on Zero
Running on Zero
Download app.py from bibibi12345/Prism: direct link, hf CLI and curl.
- Browser
- Download file 16.1 kB
-
https://huggingface.co/spaces/bibibi12345/Prism/resolve/main/app.py
- Command line
-
hf download hf://spaces/bibibi12345/Prism/app.py
-
curl -L -o app.py https://huggingface.co/spaces/bibibi12345/Prism/resolve/main/app.py
16.1 kB
| """Prism (FrancisRing/Prism) joint video-audio generation on ZeroGPU (xlarge, 96GB).""" | |
| import os | |
| import sys | |
| import time | |
| import types | |
| import random | |
| import tempfile | |
| import subprocess | |
| import torch | |
| # --------------------------------------------------------------------------- | |
| # ZeroGPU does not support torch.compile -> turn every @torch.compile into a no-op | |
| # (must happen before the model code is imported). | |
| # --------------------------------------------------------------------------- | |
| def _no_compile(fn=None, *args, **kwargs): | |
| if callable(fn): | |
| return fn | |
| return lambda f: f | |
| torch.compile = _no_compile | |
| # The DAC audio VAE only needs `audiotools` for training/IO helpers. Stub it so we | |
| # don't have to install descript-audiotools (it pins protobuf<3.20). | |
| _at = types.ModuleType("audiotools") | |
| _at_ml = types.ModuleType("audiotools.ml") | |
| class _AudioSignal: # never used in the decode path | |
| def __init__(self, *a, **k): | |
| raise RuntimeError("audiotools.AudioSignal is not available in this Space") | |
| _at.AudioSignal = _AudioSignal | |
| class _BaseModel: # plain mixin; DAC gets nn.Module from diffusers' ModelMixin | |
| pass | |
| _at_ml.BaseModel = _BaseModel | |
| _at.ml = _at_ml | |
| sys.modules.setdefault("audiotools", _at) | |
| sys.modules.setdefault("audiotools.ml", _at_ml) | |
| import spaces # noqa: E402 (must be imported before CUDA is touched) | |
| import numpy as np # noqa: E402 | |
| import gradio as gr # noqa: E402 | |
| from PIL import Image # noqa: E402 | |
| from huggingface_hub import snapshot_download # noqa: E402 | |
| from safetensors import safe_open # noqa: E402 | |
| from accelerate import init_empty_weights # noqa: E402 | |
| from accelerate.utils import set_module_tensor_to_device # noqa: E402 | |
| from hymm.models.modules.mova import MOVABridge # noqa: E402 | |
| from hymm.models.modules.wan_video_dit import WanModel # noqa: E402 | |
| from hymm.models.modules.wan_audio_dit import WanAudioModel # noqa: E402 | |
| from hymm.models.modules.interactionv2 import DualTowerConditionalBridge # noqa: E402 | |
| from hymm.models.modules.dac_vae import DAC # noqa: E402 | |
| from hymm.diffusion.pipelines.mova_pipeline import MOVAPipeline # noqa: E402 | |
| from hymm.diffusion.schedulers.flow_match_pair import FlowMatchPairScheduler # noqa: E402 | |
| from diffusers.models.autoencoders import AutoencoderKLWan # noqa: E402 | |
| from transformers import T5TokenizerFast, UMT5EncoderModel # noqa: E402 | |
| REPO_ID = "FrancisRing/Prism" | |
| VARIANT = os.environ.get("PRISM_VARIANT", "preview_alpha") # or preview_beta (motion) | |
| BASE = "pretrained_models/MOVA-360p" | |
| DTYPE = torch.bfloat16 | |
| NEGATIVE_PROMPT = ( | |
| "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止," | |
| "整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指" | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Download only what inference needs: the Prism transformer checkpoint (it holds | |
| # both video experts, the audio tower and the bridge) + text encoder / VAEs. | |
| # The 57GB of base MOVA DiT weights are fully overridden, so they are skipped. | |
| # --------------------------------------------------------------------------- | |
| t0 = time.time() | |
| root = snapshot_download( | |
| REPO_ID, | |
| allow_patterns=[ | |
| f"{VARIANT}/*", | |
| f"{BASE}/model_index.json", | |
| f"{BASE}/*/config.json", | |
| f"{BASE}/text_encoder/*", | |
| f"{BASE}/tokenizer/*", | |
| f"{BASE}/scheduler/*", | |
| f"{BASE}/video_vae/*", | |
| f"{BASE}/audio_vae/*", | |
| ], | |
| ) | |
| base = os.path.join(root, BASE) | |
| print(f"[prism] download done in {time.time() - t0:.0f}s -> {root}") | |
| # --- transformer: build on meta, then stream the checkpoint straight to cuda --- | |
| t0 = time.time() | |
| with init_empty_weights(): | |
| video_dit = WanModel.from_config(WanModel.load_config(base, subfolder="video_dit")) | |
| video_dit_2 = WanModel.from_config(WanModel.load_config(base, subfolder="video_dit_2")) | |
| audio_dit = WanAudioModel.from_config(WanAudioModel.load_config(base, subfolder="audio_dit")) | |
| dual_bridge = DualTowerConditionalBridge.from_config( | |
| DualTowerConditionalBridge.load_config(base, subfolder="dual_tower_bridge") | |
| ) | |
| transformer = MOVABridge( | |
| video_dit=video_dit, | |
| video_dit_2=video_dit_2, | |
| audio_dit=audio_dit, | |
| dual_tower_bridge=dual_bridge, | |
| boundary_ratio=0.9, | |
| ) | |
| ckpt_file = os.path.join(root, VARIANT, "diffusion_pytorch_model.safetensors") | |
| expected = set(transformer.state_dict().keys()) | |
| loaded = set() | |
| with safe_open(ckpt_file, framework="pt", device="cpu") as f: | |
| for k in f.keys(): | |
| if k not in expected: | |
| print(f"[prism] unexpected key {k}") | |
| continue | |
| set_module_tensor_to_device(transformer, k, "cuda", value=f.get_tensor(k), dtype=DTYPE) | |
| loaded.add(k) | |
| missing = expected - loaded | |
| if missing: | |
| raise RuntimeError(f"[prism] {len(missing)} transformer keys missing, e.g. {sorted(missing)[:5]}") | |
| transformer.to("cuda") | |
| transformer.eval().requires_grad_(False) | |
| print(f"[prism] transformer ready ({len(loaded)} tensors) in {time.time() - t0:.0f}s") | |
| # --- frozen components --- | |
| video_vae = AutoencoderKLWan.from_pretrained(base, subfolder="video_vae", torch_dtype=DTYPE).to("cuda").eval() | |
| audio_vae = DAC.from_pretrained(base, subfolder="audio_vae", torch_dtype=DTYPE).to("cuda").eval() | |
| text_encoder = UMT5EncoderModel.from_pretrained(base, subfolder="text_encoder", torch_dtype=DTYPE).to("cuda").eval() | |
| tokenizer = T5TokenizerFast.from_pretrained(base, subfolder="tokenizer") | |
| scheduler = FlowMatchPairScheduler.from_pretrained(base, subfolder="scheduler") | |
| pipe = MOVAPipeline( | |
| transformer=transformer, | |
| video_vae=video_vae, | |
| audio_vae=audio_vae, | |
| text_encoder=text_encoder, | |
| tokenizer=tokenizer, | |
| scheduler=scheduler, | |
| boundary_ratio=0.9, | |
| device=torch.device("cuda"), | |
| ) | |
| print("[prism] pipeline ready") | |
| RESOLUTIONS = { | |
| "480p (848×480)": (480, 848), | |
| "720p (1280×720)": (720, 1280), | |
| } | |
| def configure_sparse(sparsity, use_ivpq): | |
| transformer.set_sparse_high_noise_only(False) | |
| transformer.configure_bsa( | |
| enable_bsa=True, | |
| bsa_params={ | |
| "sparsity": float(sparsity), | |
| "cdf_threshold": 0.20, | |
| "chunk_3d_shape_q": [4, 4, 4], | |
| "chunk_3d_shape_k": [4, 4, 4], | |
| }, | |
| enable_bsa_v2a=False, | |
| bsa_params_v2a=None, | |
| ) | |
| for sa in transformer._video_self_attns(): | |
| sa.bsa_params["dynamic_block_lambda_a"] = 0.5 | |
| sa.bsa_params["dynamic_block_tau_128"] = 0.25 | |
| sa.bsa_params["dynamic_block_lambda_128"] = 1.0 | |
| transformer.configure_ivpq_dynamic_block(enable=bool(use_ivpq)) | |
| def crop_and_resize(img, height, width): | |
| w, h = img.size | |
| target = width / height | |
| if w / h > target: | |
| nw = int(h * target) | |
| img = img.crop(((w - nw) // 2, 0, (w - nw) // 2 + nw, h)) | |
| elif w / h < target: | |
| nh = int(w / target) | |
| img = img.crop((0, (h - nh) // 2, w, (h - nh) // 2 + nh)) | |
| return img.resize((width, height), Image.LANCZOS) | |
| def target_size(image, resolution): | |
| h, w = RESOLUTIONS[resolution] | |
| if image is not None and image.height > image.width: # portrait input -> portrait video | |
| h, w = w, h | |
| return h, w | |
| def num_tokens(h, w, frames): | |
| return ((frames - 1) // 4 + 1) * (h // 16) * (w // 16) | |
| # GPU-time model (seconds per transformer forward), calibrated on the Space: | |
| # 480p/49f/30 steps = 255s, 720p/49f/20 steps = 448s (RTX Pro 6000, sparsity 0.75). | |
| SEC_PER_TOKEN = float(os.environ.get("PRISM_SEC_PER_TOKEN", "1.57e-4")) | |
| SEC_PER_TOKEN2 = float(os.environ.get("PRISM_SEC_PER_TOKEN2", "5.1e-9")) | |
| MAX_DURATION = int(os.environ.get("PRISM_MAX_DURATION", "1500")) | |
| def estimate_seconds(image, resolution, frames, steps, cfg, sparsity): | |
| h, w = target_size(image, resolution) | |
| tok = num_tokens(h, w, int(frames)) | |
| per_forward = SEC_PER_TOKEN * tok + SEC_PER_TOKEN2 * tok * tok * (1.0 - float(sparsity) + 0.05) | |
| passes = 1 if float(cfg) == 1.0 else 2 | |
| return int(40 + int(steps) * passes * per_forward) | |
| def get_duration(image, prompt, audio_prompt, resolution, frames, steps, cfg, shift, audio_shift, | |
| seed, sparsity, use_ivpq, *args, **kwargs): | |
| return min(MAX_DURATION, int(estimate_seconds(image, resolution, frames, steps, cfg, sparsity) * 1.15)) | |
| def _generate_gpu(image, prompt, audio_prompt, resolution, frames, steps, cfg, shift, audio_shift, | |
| seed, sparsity, use_ivpq, progress=gr.Progress(track_tqdm=True)): | |
| t_start = time.time() | |
| h, w = target_size(image, resolution) | |
| ref = crop_and_resize(image.convert("RGB"), h, w) | |
| configure_sparse(sparsity, use_ivpq) | |
| torch.manual_seed(int(seed)) | |
| video, audio = pipe( | |
| prompt=prompt, | |
| image=ref, | |
| audio_prompt=audio_prompt or prompt, | |
| negative_prompt=NEGATIVE_PROMPT, | |
| seed=int(seed), | |
| height=h, | |
| width=w, | |
| num_frames=int(frames), | |
| video_fps=24.0, | |
| num_inference_steps=int(steps), | |
| visual_shift=float(shift), | |
| audio_shift=float(audio_shift), | |
| cfg_scale=float(cfg), | |
| enable_vae_tiling=(h * w >= 720 * 1280), | |
| ) | |
| frames_np = np.stack([np.asarray(fr) for fr in video[0]]) | |
| audio_np = audio[0].float().cpu().squeeze().numpy() | |
| elapsed = time.time() - t_start | |
| print(f"[prism] {h}x{w} {frames}f {steps} steps cfg={cfg} -> {elapsed:.1f}s on GPU " | |
| f"(peak mem {torch.cuda.max_memory_allocated() / 2**30:.1f} GiB)") | |
| return frames_np, audio_np, elapsed | |
| def save_video_with_audio(frames, audio_np, path, fps, sample_rate): | |
| import wave | |
| import imageio | |
| with tempfile.TemporaryDirectory() as tmp: | |
| tmp_video = os.path.join(tmp, "video.mp4") | |
| tmp_audio = os.path.join(tmp, "audio.wav") | |
| writer = imageio.get_writer(tmp_video, fps=fps, quality=9, macro_block_size=8) | |
| for fr in frames: | |
| writer.append_data(fr) | |
| writer.close() | |
| if audio_np.ndim == 1: | |
| audio_np = audio_np[None, :] | |
| audio_np = audio_np[:2] | |
| pcm = (np.clip(audio_np, -1.0, 1.0) * 32767.0).astype(np.int16) | |
| with wave.open(tmp_audio, "wb") as wf: | |
| wf.setnchannels(pcm.shape[0]) | |
| wf.setsampwidth(2) | |
| wf.setframerate(int(sample_rate)) | |
| wf.writeframes(pcm.T.reshape(-1).tobytes()) | |
| cmd = ["ffmpeg", "-y", "-i", tmp_video, "-i", tmp_audio, "-c:v", "copy", "-c:a", "aac", | |
| "-b:a", "192k", "-movflags", "+faststart", "-shortest", path] | |
| r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| if r.returncode != 0: | |
| print("[prism] ffmpeg mux failed:", r.stderr.decode(errors="ignore")[-500:]) | |
| import shutil | |
| shutil.copyfile(tmp_video, path) | |
| def generate(image, prompt, audio_prompt, resolution, frames, steps, cfg, shift, audio_shift, | |
| seed, randomize_seed, sparsity, use_ivpq, progress=gr.Progress(track_tqdm=True)): | |
| if image is None: | |
| raise gr.Error("Please upload a reference image (Prism is an image+text → video+audio model).") | |
| if not prompt or not prompt.strip(): | |
| raise gr.Error("Please enter a prompt.") | |
| if randomize_seed: | |
| seed = random.randint(0, 2**31 - 1) | |
| frames = int(frames) | |
| frames = frames - (frames - 1) % 4 | |
| est = estimate_seconds(image, resolution, frames, steps, cfg, sparsity) | |
| if est * 1.15 > MAX_DURATION: | |
| raise gr.Error(f"This setting needs ~{est / 60:.0f} min of GPU time, above the {MAX_DURATION // 60} min " | |
| "per-call limit. Lower frames/steps or use 480p.") | |
| frames_np, audio_np, elapsed = _generate_gpu( | |
| image, prompt, audio_prompt, resolution, frames, steps, cfg, shift, audio_shift, | |
| seed, sparsity, use_ivpq, | |
| ) | |
| out = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name | |
| save_video_with_audio(frames_np, audio_np, out, fps=24.0, sample_rate=pipe.audio_sample_rate) | |
| return out, seed, f"GPU time: {elapsed:.0f}s (xlarge quota counts ×2)" | |
| def update_estimate(image, resolution, frames, steps, cfg, sparsity): | |
| h, w = target_size(image, resolution) | |
| est = estimate_seconds(image, resolution, int(frames), steps, cfg, sparsity) | |
| return (f"Output {w}×{h}, {int(frames)} frames ({int(frames) / 24:.1f}s) · " | |
| f"rough GPU estimate ≈ {est / 60:.1f} min → ≈ {2 * est / 60:.1f} min of daily ZeroGPU quota") | |
| EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "examples") | |
| examples = [] | |
| if os.path.isdir(EXAMPLES_DIR): | |
| for name in sorted(os.listdir(EXAMPLES_DIR)): | |
| d = os.path.join(EXAMPLES_DIR, name) | |
| img, txt = os.path.join(d, "reference_image.jpg"), os.path.join(d, "prompt.txt") | |
| if os.path.exists(img) and os.path.exists(txt): | |
| with open(txt, encoding="utf-8") as fh: | |
| examples.append([img, fh.read().strip()]) | |
| with gr.Blocks(title="Prism · joint video + audio") as demo: | |
| gr.Markdown( | |
| "# Prism: image + text → video with audio\n" | |
| f"[Model](https://huggingface.co/{REPO_ID}) · [Code](https://github.com/Tencent-Hunyuan/Prism) · " | |
| f"[Paper](https://arxiv.org/abs/2610.05416). Checkpoint: `{VARIANT}`. Runs on one ZeroGPU **xlarge** " | |
| "(RTX Pro 6000, 96GB). xlarge uses quota at 2×, so keep clips short. " | |
| "Sound can be tagged in the prompt with `<music>`, `<sfx>`, `<speech>`." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| image = gr.Image(label="Reference image (first frame)", type="pil") | |
| prompt = gr.Textbox(label="Prompt (video)", lines=6) | |
| audio_prompt = gr.Textbox(label="Audio prompt (optional, defaults to the video prompt)", lines=2) | |
| with gr.Row(): | |
| resolution = gr.Radio(list(RESOLUTIONS), value="720p (1280×720)", label="Resolution") | |
| frames = gr.Slider(25, 121, value=49, step=4, label="Frames (24 fps)") | |
| steps = gr.Slider(10, 50, value=20, step=1, label="Denoising steps") | |
| estimate = gr.Markdown() | |
| with gr.Accordion("Advanced", open=False): | |
| cfg = gr.Slider(1.0, 10.0, value=5.0, step=0.5, label="CFG scale (1.0 = no CFG, ~2× faster)") | |
| shift = gr.Slider(1.0, 17.0, value=7.0, step=0.5, label="Visual shift (720p: 7, 480p: 5)") | |
| audio_shift = gr.Slider(1.0, 15.0, value=7.0, step=0.5, label="Audio shift") | |
| sparsity = gr.Dropdown([0.75, 0.85, 0.93], value=0.75, label="Block-sparse attention sparsity") | |
| use_ivpq = gr.Checkbox(value=True, label="Dynamic block shape (IVPQ)") | |
| with gr.Row(): | |
| seed = gr.Number(value=42, precision=0, label="Seed") | |
| randomize_seed = gr.Checkbox(value=False, label="Random seed") | |
| run = gr.Button("Generate", variant="primary") | |
| with gr.Column(): | |
| out_video = gr.Video(label="Result (with audio)") | |
| used_seed = gr.Number(label="Seed used", interactive=False) | |
| info = gr.Markdown() | |
| est_inputs = [image, resolution, frames, steps, cfg, sparsity] | |
| for c in est_inputs: | |
| c.change(update_estimate, est_inputs, estimate) | |
| demo.load(update_estimate, est_inputs, estimate) | |
| run.click( | |
| generate, | |
| [image, prompt, audio_prompt, resolution, frames, steps, cfg, shift, audio_shift, | |
| seed, randomize_seed, sparsity, use_ivpq], | |
| [out_video, used_seed, info], | |
| ) | |
| if examples: | |
| gr.Examples(examples, [image, prompt]) | |
| demo.queue(max_size=20).launch() | |