# Copyright (c) 2026 Bytedance Ltd. and/or its affiliate # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Shared command-line plumbing for the Bernini Renderer inference scripts.""" import argparse import json import logging from transformers import PretrainedConfig from .pipeline import BerniniRendererPipeline, BerniniPipeline from .prompt_enhancer import get_system_prompt_for_task # Standard Wan2.2 negative prompt; suppresses common quality / anatomy # artefacts when no neg prompt is supplied on the command line. DEFAULT_NEG_PROMPT = ( "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰," "最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部," "画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面," "杂乱的背景,三条腿,背景人很多,倒着走" ) # Allowed --guidance_mode values, shared by argparse and the case-file loader. GUIDANCE_MODES = ["rv2v", "v2v", "v2v_chain", "t2v", "r2v_apg", "v2v_apg", "t2v_apg", "vae_txt_vit_wapg", "rv2v_wapg"] # Keys a case file under assets/testcases/ may set: one example's routing and # inputs. Generation params (seed, num_frames, omega_*, ...) stay on the CLI. CASE_KEYS = ("task_type", "guidance_mode", "prompt", "video", "image", "images", "output") def add_common_args(parser): g = parser.add_argument_group("model") g.add_argument( "--config", default="configs/bernini_renderer_wan22", help="model config directory or you can pass the Diffusers directory to load model directly", ) g.add_argument("--high_noise_ckpt", default=None, help="high-noise checkpoint (dir or HF repo)") g.add_argument("--low_noise_ckpt", default=None, help="low-noise checkpoint (dir or HF repo)") g.add_argument("--use_unipc", action=argparse.BooleanOptionalAction, default=True, help="use the UniPC scheduler, required by the *_apg guidance modes " "(default: on; pass --no-use_unipc to disable)") g.add_argument("--use_src_tgt_id", action=argparse.BooleanOptionalAction, default=True, help="use source-id rotary embeddings, i.e. use_src_id_rotary_emb " "(default: on; pass --no-use_src_tgt_id to disable)") g.add_argument("--interpolate_src_id", action=argparse.BooleanOptionalAction, default=True, help="when reference sources exceed --max_trained_src_id, evenly map their " "source ids into the trained range instead of extrapolating " "(default: on; pass --no-interpolate_src_id to disable)") g.add_argument("--max_trained_src_id", type=int, default=5, help="largest source_id seen during training; sources beyond this are " "interpolated into [1, max_trained_src_id] (default: 5)") g = parser.add_argument_group("input") g.add_argument("--prompt", default=None, help="text prompt / editing instruction") g.add_argument("--task_type", default="v2v", help="task type (drives prompt enhancement)") g.add_argument("--video", nargs="+", default=None, help="source video path(s)") g.add_argument("--image", default=None, help="single source image for image editing") g.add_argument("--images", nargs="+", default=None, help="reference image(s)") g.add_argument("--output", default="outputs/output.mp4", help="output path") g.add_argument("--inputs", default=None, help="json/jsonl list of tasks (batch mode)") g.add_argument("--case", default=None, help="json file describing a single example case (see assets/testcases/)") g = parser.add_argument_group("generation") g.add_argument("--neg_prompt", default=DEFAULT_NEG_PROMPT, help="negative prompt (defaults to the standard Wan2.2 neg prompt)") g.add_argument("--system_prompt", default="", help="system prompt prefix (default: auto-selected from --task_type)") g.add_argument("--num_frames", type=int, default=81) g.add_argument("--max_image_size", type=int, default=848) g.add_argument( "--height", type=int, default=480, help=( "output height. For Bernini: if --height/--width are positive, they override the output size; " "if both are 0, the output uses the input video's resolution" ), ) g.add_argument( "--width", type=int, default=848, help=( "output width. For Bernini: if --height/--width are positive, they override the output size; " "if both are 0, the output uses the input video's resolution" ), ) g.add_argument("--num_inference_steps", type=int, default=40) g.add_argument("--guidance_mode", default="rv2v", choices=GUIDANCE_MODES) g.add_argument("--omega_vid", type=float, default=1.25) g.add_argument("--omega_img", type=float, default=4.5) g.add_argument("--omega_txt", type=float, default=4.0) g.add_argument("--omega_tgt", type=float, default=0.5) g.add_argument("--omega_scale", type=float, default=0.8) g.add_argument("--planning_step", type=int, default=25) g.add_argument("--vit_txt_cfg", type=float, default=1.2) g.add_argument("--vit_img_cfg", type=float, default=1.0) g.add_argument("--vit_denoising_step", type=int, default=5) g.add_argument("--flow_shift", type=float, default=5.0) g.add_argument("--seed", type=int, default=42) g.add_argument("--fps", type=int, default=16) g.add_argument("--eta", type=float, default=0.5) g.add_argument("--norm_threshold", type=float, nargs="+", default=[50.0, 50.0, 50.0]) g.add_argument("--momentum", type=float, default=0) g = parser.add_argument_group("prompt enhancer") g.add_argument("--use_pe", action="store_true", help="enhance the prompt via an OpenAI-compatible endpoint") g.add_argument("--pe_model", default=None, help="prompt-enhancer model name") return parser def setup_logging(): logging.basicConfig( level=logging.INFO, format="[%(asctime)s] %(name)s: %(message)s", datefmt="%H:%M:%S", ) def build_pipeline(args, device): config_dict, _ = PretrainedConfig.get_config_dict(args.config) model_type = config_dict.get("model_type") if model_type == "bernini": if args.high_noise_ckpt is not None or args.low_noise_ckpt is not None: raise ValueError( "Bernini uses the self-contained Bernini-Diffusers layout; " "pass that directory to --config and omit --high_noise_ckpt/--low_noise_ckpt" ) logging.getLogger("bernini.cli").info( "Loading full Bernini directly from the Bernini-Diffusers dir '%s'", args.config ) return BerniniPipeline.from_pretrained( args.config, device=device, use_unipc=args.use_unipc, use_src_id_rotary_emb=args.use_src_tgt_id, interpolate_src_id=args.interpolate_src_id, max_trained_src_id=args.max_trained_src_id, ) if (args.high_noise_ckpt is None) != (args.low_noise_ckpt is None): raise ValueError( "--high_noise_ckpt and --low_noise_ckpt must be given together; " "got only one of them" ) else: load_ckpt_weights = args.high_noise_ckpt is not None and args.low_noise_ckpt is not None if not load_ckpt_weights: logging.getLogger("bernini.cli").info( "no --high_noise_ckpt/--low_noise_ckpt given; loading Bernini weights directly " "from the diffusers-format dir '%s' (transformer/transformer_2)", args.config ) return BerniniRendererPipeline.from_pretrained( args.config, high_noise_ckpt=args.high_noise_ckpt, low_noise_ckpt=args.low_noise_ckpt, device=device, load_ckpt_weights=load_ckpt_weights, use_unipc=args.use_unipc, shift=args.flow_shift, use_src_id_rotary_emb=args.use_src_tgt_id, interpolate_src_id=args.interpolate_src_id, max_trained_src_id=args.max_trained_src_id, ) def generation_kwargs(args) -> dict: """Pipeline kwargs shared by every task in a run.""" return dict( neg_prompt=args.neg_prompt, num_frames=args.num_frames, max_image_size=args.max_image_size, height=args.height, width=args.width, num_inference_steps=args.num_inference_steps, guidance_mode=args.guidance_mode, omega_vid=args.omega_vid, omega_img=args.omega_img, omega_txt=args.omega_txt, omega_tgt=args.omega_tgt, omega_scale=args.omega_scale, planning_step=args.planning_step, vit_txt_cfg=args.vit_txt_cfg, vit_img_cfg=args.vit_img_cfg, vit_denoising_step=args.vit_denoising_step, flow_shift=args.flow_shift, seed=args.seed, fps=args.fps, eta=args.eta, norm_threshold=tuple(args.norm_threshold), momentum=args.momentum, ) def resolve_system_prompt(task: dict, args) -> str: """System-prompt prefix for one task. An explicit ``--system_prompt`` wins; otherwise it is auto-selected from the task type (per-task in batch mode, else the ``--task_type`` default). """ if args.system_prompt: return args.system_prompt return get_system_prompt_for_task(task.get("task_type", args.task_type)) def apply_case_file(args) -> None: """Merge a single-example case file (``--case``) into ``args``. A case file under ``assets/testcases/`` bundles one example's routing and inputs -- task_type, guidance_mode, prompt, source media paths and the output path -- so a run is just ``--case assets/testcases//.json`` instead of a command line carrying a wall of prompt text. The file fully defines those fields; generation params (``--seed``, ``--num_frames``, ...) are still taken from the command line. """ if not args.case: return if args.inputs: raise ValueError("--case (single example) and --inputs (batch) cannot be combined") with open(args.case) as f: case = json.load(f) if not isinstance(case, dict): raise ValueError(f"--case file '{args.case}' must hold a single JSON object") unknown = sorted(set(case) - set(CASE_KEYS)) if unknown: raise ValueError( f"--case file '{args.case}' has unsupported keys {unknown}; " f"allowed keys: {list(CASE_KEYS)}" ) if not case.get("prompt"): raise ValueError(f"--case file '{args.case}' must set 'prompt'") mode = case.get("guidance_mode") if mode is not None and mode not in GUIDANCE_MODES: raise ValueError( f"--case file '{args.case}': invalid guidance_mode '{mode}'; " f"choose from {GUIDANCE_MODES}" ) for key, value in case.items(): setattr(args, key, value) def load_tasks(args) -> list: """Return task dicts from `--inputs`, or a single task built from CLI args.""" if args.inputs: with open(args.inputs) as f: if args.inputs.endswith(".jsonl"): tasks = [json.loads(line) for line in f if line.strip()] else: tasks = json.load(f) return tasks if args.prompt is None: raise ValueError("provide --prompt, or --inputs for batch mode") return [ { "prompt": args.prompt, "task_type": args.task_type, "video": args.video, "image": args.image, "images": args.images, "output": args.output, } ]