# SPDX-License-Identifier: Apache-2.0 """Viggle Remake: re-render a video with the people (swap) or the scene (edit) changed. python inference/sample.py --mode swap --video source.mp4 --prompt prompt.txt \ --ref ref1.png [--ref ref2.png ...] [--pin 0 [--pin 96 ...]] \ --frames 124 --short-edge 480 --seed 0 \ --base MiniMax-H3 --lora-repo Viggle-Remake --out out.mp4 `--video` supplies motion, camera, framing and everything the prompt does not change. Each `--ref` is an image the prompt refers to as . `--pin` is optional and, when given, appears once per `--ref`: it pins that ref as a keyframe at that real frame of the output for the first 90% of the denoise (`--keyframe-until`), which is how a painted frame of the source becomes an anchor rather than only a reference. The target audio rows hold the source soundtrack (`--audio`, defaults to `--video`) as a clean latent for the whole denoise, so mouths follow the real speech and the output carries that soundtrack back through the audio VAE. Memory order: the 64-layer Qwen3-VL conditioner (~59 GiB) is loaded, encodes the prompt once, and is freed before the pipeline loads, so the two never share the card. Runs on upstream diffusers d6726f3. The keyframe pin (`ref2va` references *plus* keyframes inside the generated span, the layout the LoRAs are run with) is not in upstream; the three small patches under "keyframes on a ref2va layout" below add it. """ import argparse import gc import subprocess import tempfile import time from pathlib import Path import av import numpy as np import torch from PIL import Image from transformers import AutoProcessor, AutoTokenizer, Qwen3VLForConditionalGeneration from diffusers import MiniMaxH3Transformer3DModel, ModularPipeline, attention_backend from diffusers.modular_pipelines.minimax_h3 import MiniMaxH3ImageReference, MiniMaxH3VideoReference from diffusers.modular_pipelines.minimax_h3.before_denoise import (_ROPE_FRAME_RESCALE, MiniMaxH3Ref2VAPrepareLayoutStep, MiniMaxH3SetTimestepsStep, _frame_position_grid) from diffusers.modular_pipelines.minimax_h3.denoise import MiniMaxH3LoopSchedulerStep, MiniMaxH3Ref2VALoopDenoiser from diffusers.modular_pipelines.minimax_h3.encoders import (MiniMaxH3Ref2VAReferenceEncoderStep, MiniMaxH3Ref2VATextEncoderStep, encode_vae_condition, get_qwen3vl_prompt_embeds) from diffusers.modular_pipelines.minimax_h3.modular_pipeline import audio_latent_num_frames from diffusers.utils.export_utils import encode_video # 6-step grids, in the video stream's sigma; each stream's own shift is applied below SIGMAS = {"swap": [1, .990476, .965243, .923077, .8, .631579, 0], "edit": [1, .983607, .96, .923077, .857143, .705882, 0]} TEXT_TAG = 1 parser = argparse.ArgumentParser() parser.add_argument("--mode", required=True, choices=["swap", "edit"]) parser.add_argument("--video", required=True, help="the source clip") parser.add_argument("--audio", default=None, help="the soundtrack pinned into the output; defaults to --video") parser.add_argument("--prompt", required=True, help="text file with the six-section prompt, read verbatim") parser.add_argument("--ref", action="append", required=True, help="reference image, in the prompt") parser.add_argument("--pin", action="append", type=int, default=[], help="output frame each --ref is pinned to as a keyframe; absent, or once per --ref") parser.add_argument("--frames", type=int, default=124, help="17n+5 in 90..345, at 24 fps") parser.add_argument("--short-edge", type=int, default=480, help="480 (832x480 for 16:9) or 736 (1280x736)") parser.add_argument("--seed", type=int, default=0) parser.add_argument("--base", required=True, help="local copy of MiniMaxAI/MiniMax-H3") parser.add_argument("--lora-repo", required=True, help="local copy of Viggle/Viggle-Remake") parser.add_argument("--rank", type=int, default=256, choices=[256, 128, 64], help="256 loads lora/; 128 or 64 load the smaller lora_lowrank/-r") parser.add_argument("--keyframe-until", type=float, default=0.9, help="drop the pinned keyframes once 1-t reaches this") parser.add_argument("--attn-backend", default="_native_cudnn") parser.add_argument("--out", required=True) args = parser.parse_args() nf, se, refs, pin = args.frames, args.short_edge, args.ref, args.pin assert 90 <= nf <= 345 and (nf - 5) % 17 == 0, f"--frames must be 17n+5 in 90..345, got {nf}" assert se % 32 == 0, f"--short-edge must be a multiple of 32, got {se}" assert len(pin) in (0, len(refs)), "give --pin once per --ref, or not at all" assert all(0 <= k < nf for k in pin), f"--pin must be in [0, {nf})" prompt = Path(args.prompt).read_text() sigmas = SIGMAS[args.mode] # The source on its canvas. A clip already at the canvas (short edge == --short-edge, both sides multiples of 32) is used # as is; anything else is cover-cropped onto the 16:9-class canvas the way our renderer does (832x480, 1280x736). with av.open(args.video) as c: w, h = c.streams.video[0].codec_context.width, c.streams.video[0].codec_context.height video_path = args.video if min(w, h) != se or w % 32 or h % 32: long = se * 16 // 9 // 32 * 32 w, h = (long, se) if w >= h else (se, long) video_path = str(Path(tempfile.mkdtemp()) / "cond.mp4") subprocess.run(["ffmpeg", "-v", "error", "-i", args.video, "-vf", f"fps=24,scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h}", "-frames:v", str(nf), "-an", "-c:v", "libx264", "-crf", "12", "-pix_fmt", "yuv420p", video_path], check=True) video = MiniMaxH3VideoReference.from_file(video_path) video.audio, video.sample_rate = None, None # the soundtrack goes in as the target audio pin, not as reference audio height, width = video.frames.shape[1], video.frames.shape[2] # --- prompt -> embeds. The presentation is text only: a label per 2 fps block of the source, one per ref, then the # prompt, each piece tokenized on its own. H3 reads hidden_states[50] of the 64-layer conditioner. t0 = time.time() processor = AutoProcessor.from_pretrained(args.base, subfolder="processor") tokenizer = AutoTokenizer.from_pretrained(args.base, subfolder="tokenizer") text_encoder = Qwen3VLForConditionalGeneration.from_pretrained(args.base, subfolder="text_encoder", dtype=torch.bfloat16).to("cuda") t1 = time.time() _, stamps = MiniMaxH3Ref2VATextEncoderStep._sample_video_condition_frames( np.zeros((nf, 1, 1, 3), dtype=np.uint8), 24.0, 2.0, processor.video_processor.temporal_patch_size) segments = ["