Image-Text-to-Text
PEFT
Safetensors
MiniMax H3
English
lora
qwen3-vl
prompt-rewriting
multimodal
audio-video-generation
text-to-audio-video
image-to-audio-video
Instructions to use lightx2v/MiniMax-H3-Prompt-Rewriter-LoRA-8B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use lightx2v/MiniMax-H3-Prompt-Rewriter-LoRA-8B with PEFT:
from peft import PeftModel from transformers import AutoModelForCausalLM base_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-VL-8B-Instruct") model = PeftModel.from_pretrained(base_model, "lightx2v/MiniMax-H3-Prompt-Rewriter-LoRA-8B") - Notebooks
- Google Colab
- Kaggle
Upload 8B multitask prompt rewriter LoRA
Browse files- README.md +154 -0
- infer.py +258 -0
- prompt_template.py +157 -0
- requirements.txt +7 -0
README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
base_model: Qwen/Qwen3-VL-8B-Instruct
|
| 3 |
+
base_model_relation: adapter
|
| 4 |
+
library_name: peft
|
| 5 |
+
pipeline_tag: image-text-to-text
|
| 6 |
+
tags:
|
| 7 |
+
- peft
|
| 8 |
+
- lora
|
| 9 |
+
- safetensors
|
| 10 |
+
- qwen3-vl
|
| 11 |
+
- minimax-h3
|
| 12 |
+
- prompt-rewriting
|
| 13 |
+
- multimodal
|
| 14 |
+
- audio-video-generation
|
| 15 |
+
- text-to-audio-video
|
| 16 |
+
- image-to-audio-video
|
| 17 |
+
language:
|
| 18 |
+
- en
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
# MiniMax-H3 Prompt Rewriter LoRA 8B
|
| 22 |
+
|
| 23 |
+
[](https://huggingface.co/lightx2v/MiniMax-H3-Prompt-Rewriter-LoRA-8B)
|
| 24 |
+
[](https://github.com/ModelTC/LightX2V)
|
| 25 |
+
[](https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct)
|
| 26 |
+
|
| 27 |
+
This repository contains a PEFT LoRA adapter that turns short user requests into production-oriented prompts for MiniMax-H3 joint audio-video generation. It is built on **Qwen3-VL-8B-Instruct** and supports text-only and keyframe-conditioned rewriting.
|
| 28 |
+
|
| 29 |
+
Compared with [MiniMax-H3-Prompt-Rewriter-LoRA](https://huggingface.co/lightx2v/MiniMax-H3-Prompt-Rewriter-LoRA), this 8B variant accepts visual references and covers all four prompt-rewriting tasks used to train this adapter:
|
| 30 |
+
|
| 31 |
+
- **T2VA / T2AV**: text to audio-video.
|
| 32 |
+
- **I2VA / I2AV**: first-frame image plus text to audio-video.
|
| 33 |
+
- **L2VA / L2AV**: last-frame image plus text to audio-video.
|
| 34 |
+
- **FL2VA / FL2AV / FLF2V**: first- and last-frame images plus text to audio-video.
|
| 35 |
+
|
| 36 |
+
Other MiniMax-H3 conditioning modes, including Ref2VA, are not supported by this adapter release.
|
| 37 |
+
|
| 38 |
+
The rewriter produces a structured prompt with an integrated shot timeline, synchronized physical and ambient sound, and non-diegetic music guidance. This repository contains the prompt rewriter only; MiniMax-H3 weights and a compatible inference framework such as LightX2V are still required to generate the final video.
|
| 39 |
+
|
| 40 |
+
## Repository layout
|
| 41 |
+
|
| 42 |
+
- `adapter_config.json` and `adapter_model.safetensors`: LoRA files to place at the repository root.
|
| 43 |
+
- `infer.py`: single-sample inference for all four tasks.
|
| 44 |
+
- `prompt_template.py`: the single source of truth for the system prompt, task aliases, and image/message ordering.
|
| 45 |
+
- `requirements.txt`: minimal inference dependencies.
|
| 46 |
+
|
| 47 |
+
## Installation
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
git lfs install
|
| 51 |
+
hf auth login # required while the repository is private
|
| 52 |
+
git clone https://huggingface.co/lightx2v/MiniMax-H3-Prompt-Rewriter-LoRA-8B
|
| 53 |
+
cd MiniMax-H3-Prompt-Rewriter-LoRA-8B
|
| 54 |
+
pip install -r requirements.txt
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
The Qwen3-VL base weights are downloaded separately on first use. You can also pass a local base-model path with `--base-model`.
|
| 58 |
+
|
| 59 |
+
## Inference
|
| 60 |
+
|
| 61 |
+
The script automatically uses the adapter in the current checkout when `adapter_config.json` is present. Otherwise it downloads the adapter from this Hugging Face repository. Greedy decoding is recommended for deterministic prompt rewriting.
|
| 62 |
+
|
| 63 |
+
### T2VA
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
python infer.py \
|
| 67 |
+
--task t2va \
|
| 68 |
+
--prompt "A corgi runs through a rainy neon-lit alley." \
|
| 69 |
+
--duration 5 \
|
| 70 |
+
--resolution 16:9 \
|
| 71 |
+
--greedy \
|
| 72 |
+
--output outputs/t2va.json
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
### I2VA
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
python infer.py \
|
| 79 |
+
--task i2va \
|
| 80 |
+
--first-frame /path/to/first_frame.jpg \
|
| 81 |
+
--prompt "The subject turns toward the camera and smiles." \
|
| 82 |
+
--duration 5 \
|
| 83 |
+
--resolution adaptive \
|
| 84 |
+
--greedy \
|
| 85 |
+
--output outputs/i2va.json
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### L2VA
|
| 89 |
+
|
| 90 |
+
```bash
|
| 91 |
+
python infer.py \
|
| 92 |
+
--task l2va \
|
| 93 |
+
--last-frame /path/to/last_frame.jpg \
|
| 94 |
+
--prompt "A paper airplane crosses the room and lands in the shown final pose." \
|
| 95 |
+
--duration 5 \
|
| 96 |
+
--resolution adaptive \
|
| 97 |
+
--greedy \
|
| 98 |
+
--output outputs/l2va.json
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
### FL2VA
|
| 102 |
+
|
| 103 |
+
```bash
|
| 104 |
+
python infer.py \
|
| 105 |
+
--task fl2va \
|
| 106 |
+
--first-frame /path/to/first_frame.jpg \
|
| 107 |
+
--last-frame /path/to/last_frame.jpg \
|
| 108 |
+
--prompt "Create a continuous cinematic transition between the two reference frames." \
|
| 109 |
+
--duration 5 \
|
| 110 |
+
--resolution adaptive \
|
| 111 |
+
--greedy \
|
| 112 |
+
--output outputs/fl2va.json
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
Use `--prompt-file prompt.txt` instead of `--prompt` for long input. A `.json` output records the request metadata and writes the rewritten text to `enhanced_prompt`; any other extension writes plain text. Run `python infer.py --help` for sampling, dtype, processor pixel-budget, device-map, and local-path options.
|
| 116 |
+
|
| 117 |
+
To compare against the unadapted base model, add `--base-only`. This intentionally skips the LoRA while keeping the same system prompt and task-specific input layout.
|
| 118 |
+
|
| 119 |
+
## Using the rewritten prompt
|
| 120 |
+
|
| 121 |
+
Feed `enhanced_prompt` to the corresponding MiniMax-H3 task in LightX2V, preserving the same duration, resolution, and reference-frame roles:
|
| 122 |
+
|
| 123 |
+
- T2VA uses no image.
|
| 124 |
+
- I2VA uses the same first frame.
|
| 125 |
+
- L2VA uses the same last frame.
|
| 126 |
+
- FL2VA uses the same first frame followed by the same last frame.
|
| 127 |
+
|
| 128 |
+
Keeping the task geometry and image order unchanged is important because the rewriter describes their exact temporal roles.
|
| 129 |
+
|
| 130 |
+
In LightX2V, the corresponding task and reference-image arguments are:
|
| 131 |
+
|
| 132 |
+
- T2VA: `--task t2av`
|
| 133 |
+
- I2VA: `--task i2av --image_path FIRST_FRAME`
|
| 134 |
+
- L2VA: `--task l2av --last_frame_path LAST_FRAME`
|
| 135 |
+
- FL2VA: `--task fl2av --image_path FIRST_FRAME --last_frame_path LAST_FRAME`
|
| 136 |
+
|
| 137 |
+
## System prompt
|
| 138 |
+
|
| 139 |
+
The complete system instruction and all task-dependent message construction live in `prompt_template.py`. `infer.py` imports that file directly and renders the conversation with `enable_thinking=False`, matching the training input format. Edit that one file if you need a custom prompting policy.
|
| 140 |
+
|
| 141 |
+
## Limitations
|
| 142 |
+
|
| 143 |
+
- The adapter is a learned approximation of prompt rewriting and is not the hosted MiniMax Context-IR service.
|
| 144 |
+
- It can add plausible creative detail beyond a very short request; review identity, dialogue, visible text, and safety-sensitive content before generation.
|
| 145 |
+
- Image-conditioned quality depends on the clarity and consistency of the supplied reference frames.
|
| 146 |
+
- Final audio-video quality depends on the downstream MiniMax-H3 checkpoint, inference settings, and task geometry.
|
| 147 |
+
- Use of the adapter, Qwen base model, and downstream video model remains subject to their respective licenses and terms.
|
| 148 |
+
|
| 149 |
+
## Acknowledgements
|
| 150 |
+
|
| 151 |
+
- [Qwen3-VL-8B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct)
|
| 152 |
+
- [MiniMax-H3](https://huggingface.co/MiniMaxAI/MiniMax-H3)
|
| 153 |
+
- [LightX2V](https://github.com/ModelTC/LightX2V)
|
| 154 |
+
- [PEFT](https://github.com/huggingface/peft)
|
infer.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Rewrite one MiniMax-H3 T2VA/I2VA/L2VA/FL2VA prompt with the 8B LoRA."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
from PIL import Image, ImageOps
|
| 11 |
+
import torch
|
| 12 |
+
import transformers
|
| 13 |
+
from peft import PeftModel
|
| 14 |
+
from transformers import AutoProcessor
|
| 15 |
+
|
| 16 |
+
from prompt_template import build_messages, expected_image_count, normalize_task
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
DEFAULT_BASE_MODEL = "Qwen/Qwen3-VL-8B-Instruct"
|
| 20 |
+
DEFAULT_ADAPTER_REPO = "lightx2v/MiniMax-H3-Prompt-Rewriter-LoRA-8B"
|
| 21 |
+
REPO_ROOT = Path(__file__).resolve().parent
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def get_model_class():
|
| 25 |
+
"""Prefer Qwen3-VL's concrete class, with portable AutoModel fallbacks."""
|
| 26 |
+
|
| 27 |
+
candidates = (
|
| 28 |
+
"Qwen3VLForConditionalGeneration",
|
| 29 |
+
"AutoModelForImageTextToText",
|
| 30 |
+
"AutoModelForVision2Seq",
|
| 31 |
+
"AutoModelForMultimodalLM",
|
| 32 |
+
)
|
| 33 |
+
for name in candidates:
|
| 34 |
+
model_class = getattr(transformers, name, None)
|
| 35 |
+
if model_class is not None:
|
| 36 |
+
return model_class
|
| 37 |
+
raise RuntimeError(
|
| 38 |
+
"This Transformers installation does not expose a Qwen3-VL-compatible "
|
| 39 |
+
"conditional-generation class. Upgrade Transformers and retry."
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def default_adapter_path() -> str:
|
| 44 |
+
"""Use weights in this checkout when present, otherwise use the Hub repo."""
|
| 45 |
+
|
| 46 |
+
if (REPO_ROOT / "adapter_config.json").is_file():
|
| 47 |
+
return str(REPO_ROOT)
|
| 48 |
+
return DEFAULT_ADAPTER_REPO
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def parse_args() -> argparse.Namespace:
|
| 52 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 53 |
+
parser.add_argument("--base-model", default=DEFAULT_BASE_MODEL)
|
| 54 |
+
parser.add_argument(
|
| 55 |
+
"--adapter-path",
|
| 56 |
+
default=None,
|
| 57 |
+
help=(
|
| 58 |
+
"Local LoRA directory or Hugging Face repo ID. By default, use this "
|
| 59 |
+
"checkout when adapter_config.json exists, otherwise use the 8B Hub repo."
|
| 60 |
+
),
|
| 61 |
+
)
|
| 62 |
+
parser.add_argument("--base-only", action="store_true", help="Run the base model without loading the LoRA")
|
| 63 |
+
parser.add_argument("--task", default="t2va", help="T2VA, I2VA, L2VA, or FL2VA (common *2V/*2AV aliases work)")
|
| 64 |
+
|
| 65 |
+
prompt_source = parser.add_mutually_exclusive_group(required=True)
|
| 66 |
+
prompt_source.add_argument("--prompt", help="Original user prompt")
|
| 67 |
+
prompt_source.add_argument("--prompt-file", type=Path, help="UTF-8 file containing the original prompt")
|
| 68 |
+
|
| 69 |
+
parser.add_argument("--duration", type=int, choices=range(4, 16), default=10)
|
| 70 |
+
parser.add_argument(
|
| 71 |
+
"--resolution",
|
| 72 |
+
choices=("adaptive", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"),
|
| 73 |
+
help="Defaults to 16:9 for T2VA and adaptive for image-conditioned tasks",
|
| 74 |
+
)
|
| 75 |
+
parser.add_argument("--first-frame", type=Path, help="First-frame image for I2VA or FL2VA")
|
| 76 |
+
parser.add_argument("--last-frame", type=Path, help="Last-frame image for L2VA or FL2VA")
|
| 77 |
+
parser.add_argument("--output", type=Path, help="Optional .txt or .json output path")
|
| 78 |
+
|
| 79 |
+
parser.add_argument("--max-new-tokens", type=int, default=4096)
|
| 80 |
+
parser.add_argument("--min-pixels", type=int, default=256 * 256)
|
| 81 |
+
parser.add_argument("--max-pixels", type=int, default=1024 * 1024)
|
| 82 |
+
parser.add_argument("--dtype", choices=("bfloat16", "float16"), default="bfloat16")
|
| 83 |
+
parser.add_argument("--attn-implementation", default="sdpa")
|
| 84 |
+
parser.add_argument("--device-map", default="auto")
|
| 85 |
+
parser.add_argument("--greedy", action="store_true", help="Disable sampling")
|
| 86 |
+
parser.add_argument("--temperature", type=float, default=0.7)
|
| 87 |
+
parser.add_argument("--top-p", type=float, default=0.8)
|
| 88 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 89 |
+
args = parser.parse_args()
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
args.task = normalize_task(args.task)
|
| 93 |
+
except ValueError as exc:
|
| 94 |
+
parser.error(str(exc))
|
| 95 |
+
|
| 96 |
+
if args.prompt_file is not None:
|
| 97 |
+
if not args.prompt_file.is_file():
|
| 98 |
+
parser.error(f"prompt file not found: {args.prompt_file}")
|
| 99 |
+
args.prompt = args.prompt_file.read_text(encoding="utf-8").strip()
|
| 100 |
+
else:
|
| 101 |
+
args.prompt = str(args.prompt or "").strip()
|
| 102 |
+
if not args.prompt:
|
| 103 |
+
parser.error("prompt must not be empty")
|
| 104 |
+
|
| 105 |
+
if args.max_new_tokens <= 0:
|
| 106 |
+
parser.error("--max-new-tokens must be positive")
|
| 107 |
+
if args.min_pixels <= 0 or args.max_pixels < args.min_pixels:
|
| 108 |
+
parser.error("pixel limits must satisfy 0 < --min-pixels <= --max-pixels")
|
| 109 |
+
if not args.greedy and (args.temperature <= 0 or not 0 < args.top_p <= 1):
|
| 110 |
+
parser.error("sampling requires --temperature > 0 and 0 < --top-p <= 1")
|
| 111 |
+
|
| 112 |
+
required = expected_image_count(args.task)
|
| 113 |
+
supplied = int(args.first_frame is not None) + int(args.last_frame is not None)
|
| 114 |
+
if supplied != required:
|
| 115 |
+
parser.error(f"{args.task} requires {required} reference image(s), but {supplied} were supplied")
|
| 116 |
+
if args.task == "i2av" and args.last_frame is not None:
|
| 117 |
+
parser.error("I2VA accepts --first-frame only")
|
| 118 |
+
if args.task == "l2av" and args.first_frame is not None:
|
| 119 |
+
parser.error("L2VA accepts --last-frame only")
|
| 120 |
+
for path in (args.first_frame, args.last_frame):
|
| 121 |
+
if path is not None and not path.is_file():
|
| 122 |
+
parser.error(f"image not found: {path}")
|
| 123 |
+
|
| 124 |
+
args.resolution = args.resolution or ("16:9" if args.task == "t2av" else "adaptive")
|
| 125 |
+
args.adapter_path = args.adapter_path or default_adapter_path()
|
| 126 |
+
return args
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def load_images(args: argparse.Namespace) -> list[Image.Image]:
|
| 130 |
+
"""Load images in the exact placeholder order used by prompt_template.py."""
|
| 131 |
+
|
| 132 |
+
paths: list[Path] = []
|
| 133 |
+
if args.task in {"i2av", "fl2av"}:
|
| 134 |
+
paths.append(args.first_frame)
|
| 135 |
+
if args.task in {"l2av", "fl2av"}:
|
| 136 |
+
paths.append(args.last_frame)
|
| 137 |
+
|
| 138 |
+
images: list[Image.Image] = []
|
| 139 |
+
for path in paths:
|
| 140 |
+
with Image.open(path) as image:
|
| 141 |
+
images.append(ImageOps.exif_transpose(image).convert("RGB").copy())
|
| 142 |
+
return images
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def load_processor(args: argparse.Namespace):
|
| 146 |
+
processor_source = args.base_model if args.base_only else args.adapter_path
|
| 147 |
+
processor_kwargs = {
|
| 148 |
+
"trust_remote_code": True,
|
| 149 |
+
"min_pixels": args.min_pixels,
|
| 150 |
+
"max_pixels": args.max_pixels,
|
| 151 |
+
}
|
| 152 |
+
try:
|
| 153 |
+
return AutoProcessor.from_pretrained(processor_source, **processor_kwargs)
|
| 154 |
+
except (OSError, ValueError, KeyError):
|
| 155 |
+
if processor_source == args.base_model:
|
| 156 |
+
raise
|
| 157 |
+
return AutoProcessor.from_pretrained(args.base_model, **processor_kwargs)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def model_input_device(model: torch.nn.Module) -> torch.device:
|
| 161 |
+
embedding_device = model.get_input_embeddings().weight.device
|
| 162 |
+
if embedding_device.type != "meta":
|
| 163 |
+
return embedding_device
|
| 164 |
+
for parameter in model.parameters():
|
| 165 |
+
if parameter.device.type != "meta":
|
| 166 |
+
return parameter.device
|
| 167 |
+
raise RuntimeError("Could not determine a real input device for the loaded model")
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def main() -> None:
|
| 171 |
+
args = parse_args()
|
| 172 |
+
torch.manual_seed(args.seed)
|
| 173 |
+
if torch.cuda.is_available():
|
| 174 |
+
torch.cuda.manual_seed_all(args.seed)
|
| 175 |
+
|
| 176 |
+
processor = load_processor(args)
|
| 177 |
+
load_kwargs = {
|
| 178 |
+
"torch_dtype": getattr(torch, args.dtype),
|
| 179 |
+
"low_cpu_mem_usage": True,
|
| 180 |
+
"trust_remote_code": True,
|
| 181 |
+
"device_map": args.device_map,
|
| 182 |
+
}
|
| 183 |
+
if args.attn_implementation:
|
| 184 |
+
load_kwargs["attn_implementation"] = args.attn_implementation
|
| 185 |
+
|
| 186 |
+
model = get_model_class().from_pretrained(args.base_model, **load_kwargs)
|
| 187 |
+
if not args.base_only:
|
| 188 |
+
model = PeftModel.from_pretrained(model, args.adapter_path)
|
| 189 |
+
model.eval()
|
| 190 |
+
|
| 191 |
+
messages = build_messages(
|
| 192 |
+
args.prompt,
|
| 193 |
+
task=args.task,
|
| 194 |
+
resolution=args.resolution,
|
| 195 |
+
duration=args.duration,
|
| 196 |
+
)
|
| 197 |
+
rendered = processor.apply_chat_template(
|
| 198 |
+
messages,
|
| 199 |
+
tokenize=False,
|
| 200 |
+
add_generation_prompt=True,
|
| 201 |
+
enable_thinking=False,
|
| 202 |
+
)
|
| 203 |
+
processor_kwargs = {
|
| 204 |
+
"text": [rendered],
|
| 205 |
+
"return_tensors": "pt",
|
| 206 |
+
"padding": False,
|
| 207 |
+
"return_mm_token_type_ids": True,
|
| 208 |
+
}
|
| 209 |
+
images = load_images(args)
|
| 210 |
+
if images:
|
| 211 |
+
processor_kwargs["images"] = images
|
| 212 |
+
inputs = processor(**processor_kwargs)
|
| 213 |
+
input_device = model_input_device(model)
|
| 214 |
+
inputs = {
|
| 215 |
+
key: value.to(input_device) if isinstance(value, torch.Tensor) else value
|
| 216 |
+
for key, value in inputs.items()
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
generation_kwargs = {"max_new_tokens": args.max_new_tokens}
|
| 220 |
+
if args.greedy:
|
| 221 |
+
generation_kwargs["do_sample"] = False
|
| 222 |
+
else:
|
| 223 |
+
generation_kwargs.update(
|
| 224 |
+
do_sample=True,
|
| 225 |
+
temperature=args.temperature,
|
| 226 |
+
top_p=args.top_p,
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
with torch.inference_mode():
|
| 230 |
+
output_ids = model.generate(**inputs, **generation_kwargs)
|
| 231 |
+
generated_ids = output_ids[0, inputs["input_ids"].shape[1] :]
|
| 232 |
+
rewritten_prompt = processor.decode(generated_ids, skip_special_tokens=True).strip()
|
| 233 |
+
print(rewritten_prompt)
|
| 234 |
+
|
| 235 |
+
if args.output is not None:
|
| 236 |
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 237 |
+
if args.output.suffix.lower() == ".json":
|
| 238 |
+
payload = {
|
| 239 |
+
"task": args.task,
|
| 240 |
+
"resolution": args.resolution,
|
| 241 |
+
"duration": args.duration,
|
| 242 |
+
"prompt": args.prompt,
|
| 243 |
+
"first_frame": str(args.first_frame) if args.first_frame else None,
|
| 244 |
+
"last_frame": str(args.last_frame) if args.last_frame else None,
|
| 245 |
+
"base_model": args.base_model,
|
| 246 |
+
"adapter_path": None if args.base_only else str(args.adapter_path),
|
| 247 |
+
"enhanced_prompt": rewritten_prompt,
|
| 248 |
+
}
|
| 249 |
+
args.output.write_text(
|
| 250 |
+
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
| 251 |
+
encoding="utf-8",
|
| 252 |
+
)
|
| 253 |
+
else:
|
| 254 |
+
args.output.write_text(rewritten_prompt + "\n", encoding="utf-8")
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
if __name__ == "__main__":
|
| 258 |
+
main()
|
prompt_template.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MiniMax-H3 task-aware prompt rewriting templates.
|
| 2 |
+
|
| 3 |
+
This is the single source of truth for the system prompt and the multimodal
|
| 4 |
+
message layout used by ``infer.py``.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
TASK_ALIASES = {
|
| 11 |
+
"t2v": "t2av",
|
| 12 |
+
"t2va": "t2av",
|
| 13 |
+
"t2av": "t2av",
|
| 14 |
+
"i2v": "i2av",
|
| 15 |
+
"i2va": "i2av",
|
| 16 |
+
"i2av": "i2av",
|
| 17 |
+
"l2v": "l2av",
|
| 18 |
+
"l2va": "l2av",
|
| 19 |
+
"l2av": "l2av",
|
| 20 |
+
"flf2v": "fl2av",
|
| 21 |
+
"flf2va": "fl2av",
|
| 22 |
+
"flf2av": "fl2av",
|
| 23 |
+
"fl2va": "fl2av",
|
| 24 |
+
"fl2av": "fl2av",
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
SYSTEM_PROMPT = """You are a professional MiniMax-H3 prompt rewriter for joint video-and-audio generation.
|
| 30 |
+
|
| 31 |
+
Rewrite the user's request according to the supplied duration, task type, and reference-frame roles. Return only the final production-ready prompt. Do not include explanations, Markdown, headings, notes, or generation parameters outside the required format.
|
| 32 |
+
|
| 33 |
+
Task-name mapping:
|
| 34 |
+
- T2AV corresponds to T2VA in the MiniMax-H3 prompt-writing guide.
|
| 35 |
+
- I2AV corresponds to I2VA.
|
| 36 |
+
- FL2AV corresponds to FL2VA.
|
| 37 |
+
- L2AV corresponds to L2VA.
|
| 38 |
+
|
| 39 |
+
Write the descriptive sections in English. Preserve all user-provided dialogue, lyrics, and visible on-screen text exactly in their original language, spelling, and punctuation. Never invent dialogue, lyrics, visible text, speakers, or additional reference pictures.
|
| 40 |
+
|
| 41 |
+
The output body must contain exactly these three fields in this order:
|
| 42 |
+
integrated_multimodal_description: ...
|
| 43 |
+
overall_soundscape: ...
|
| 44 |
+
non_diegetic_music: ...
|
| 45 |
+
|
| 46 |
+
For T2AV, begin directly with the three fields and do not add an image-alignment instruction.
|
| 47 |
+
|
| 48 |
+
For I2AV, the first line must be exactly:
|
| 49 |
+
For the target video, at 0.00 seconds into the target video, <Picture 1> (from [Shot 1]) is fully referenced.
|
| 50 |
+
|
| 51 |
+
For FL2AV, the first line must follow exactly:
|
| 52 |
+
How the reference pictures align with the target video — Picture 1 (from Shot 1) aligns with the 0.00-second mark of the target video; Picture 2 (from Shot N) aligns with the S.SS-second mark of the target video.
|
| 53 |
+
|
| 54 |
+
For L2AV, the first line must follow exactly:
|
| 55 |
+
How the reference pictures align with the target video — <Picture 1> (from [Shot N]) aligns with the S.SS-second mark of the target video.
|
| 56 |
+
|
| 57 |
+
Replace N with the actual final shot number. Replace S.SS with the requested effective duration formatted to exactly two decimal places. Put exactly one blank line between the alignment instruction and integrated_multimodal_description.
|
| 58 |
+
|
| 59 |
+
Reference-frame behavior:
|
| 60 |
+
- I2AV: Treat <Picture 1> as the exact first frame at 0.00 seconds. Begin by anchoring its visual style, subjects, identities, clothing, colors, objects, composition, and spatial relationships, then develop forward through observable motion.
|
| 61 |
+
- FL2AV: Begin from Picture 1 and describe a continuous, physically plausible path that reaches the pose, object state, lighting, spacing, and composition of Picture 2 at the requested end time. Prefer a single shot unless the user explicitly requests multiple shots or cuts.
|
| 62 |
+
- L2AV: Infer a plausible preceding state and describe a continuous path that progressively converges to <Picture 1> as the exact final frame.
|
| 63 |
+
- Preserve identity and scene continuity across all shots, but apply exact composition matching only at the reference frame's assigned timestamp.
|
| 64 |
+
|
| 65 |
+
In integrated_multimodal_description:
|
| 66 |
+
- Begin with [Shot 1] and state the visual style and initial composition.
|
| 67 |
+
- Describe only concrete visible or audible events: subjects, environment, actions, reactions, camera behavior, dialogue, singing, visible text, and synchronized diegetic sound.
|
| 68 |
+
- Number shots sequentially.
|
| 69 |
+
- Do not timestamp [Shot 1].
|
| 70 |
+
- Begin every later shot with a strictly increasing timestamp inside the requested duration, using the format: [Shot 2] At 00:03.500, the camera cuts to...
|
| 71 |
+
- Add a cut only when it introduces meaningful new visual, spatial, temporal, or narrative information. Otherwise prefer continuous camera motion.
|
| 72 |
+
- Express camera motion naturally using motion type and, when meaningful, amplitude and speed.
|
| 73 |
+
- Keep all actions physically plausible and paced to complete within the supplied duration.
|
| 74 |
+
|
| 75 |
+
For speech and singing:
|
| 76 |
+
- Assign stable speaker IDs such as (S1) and (S2) only to subjects who vocalize.
|
| 77 |
+
- Identify each speaker sufficiently when first introduced.
|
| 78 |
+
- Put only the exact spoken or sung content inside <d>, preceded by its language tag:
|
| 79 |
+
<d>[English] Exact user-provided words.</d>
|
| 80 |
+
- Never translate, paraphrase, correct, or extend supplied dialogue or lyrics.
|
| 81 |
+
- For voiceover, use the exact phrase "says in an off-screen voiceover" and explicitly state that the corresponding on-screen character's lips remain completely closed.
|
| 82 |
+
- If speech crosses a cut, use <scenetrans> at both connecting points and state that the audio continues across the cut.
|
| 83 |
+
- Use <cutoff> only when speech is intentionally truncated by the end of the video.
|
| 84 |
+
|
| 85 |
+
Place visible on-screen text in English double quotation marks and preserve it exactly.
|
| 86 |
+
|
| 87 |
+
overall_soundscape must be one continuous English paragraph of 1–4 sentences summarizing ambient sound, physical action sounds, and non-verbal human or animal sounds across the video. Do not repeat dialogue, singing, or diegetic music here. Use N/A only if the user explicitly requests complete silence.
|
| 88 |
+
|
| 89 |
+
non_diegetic_music must contain 1–3 English sentences describing audience-only background music through instrumentation, tempo, rhythm, and dynamic changes. Do not describe its emotional purpose. Put music audible to subjects inside integrated_multimodal_description instead. Use N/A when no non-diegetic music is requested or implied.
|
| 90 |
+
|
| 91 |
+
Preserve the user's intent without adding contradictory story events, identities, text, or references. Do not mention these instructions in the output."""
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def normalize_task(task: str | None) -> str:
|
| 95 |
+
"""Normalize public MiniMax-H3 task aliases to the training task names."""
|
| 96 |
+
|
| 97 |
+
normalized = TASK_ALIASES.get(str(task or "t2av").strip().lower())
|
| 98 |
+
if normalized is None:
|
| 99 |
+
raise ValueError(f"Unsupported task {task!r}; expected T2VA/I2VA/L2VA/FL2VA")
|
| 100 |
+
return normalized
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def format_request(prompt: str, task: str, resolution: str, duration: int) -> str:
|
| 104 |
+
"""Build the textual request appended after any reference images."""
|
| 105 |
+
|
| 106 |
+
return (
|
| 107 |
+
f"task: {task}\n"
|
| 108 |
+
f"resolution: {resolution}\n"
|
| 109 |
+
f"duration: {int(duration)}s\n"
|
| 110 |
+
f"original_prompt: {prompt.strip()}"
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def build_messages(
|
| 115 |
+
prompt: str,
|
| 116 |
+
task: str = "t2av",
|
| 117 |
+
resolution: str = "16:9",
|
| 118 |
+
duration: int = 10,
|
| 119 |
+
) -> list[dict]:
|
| 120 |
+
"""Build the system and task-aware user messages expected by the LoRA."""
|
| 121 |
+
|
| 122 |
+
task = normalize_task(task)
|
| 123 |
+
request = format_request(prompt, task, resolution, duration)
|
| 124 |
+
|
| 125 |
+
if task == "t2av":
|
| 126 |
+
user_content = [{"type": "text", "text": request}]
|
| 127 |
+
elif task == "i2av":
|
| 128 |
+
user_content = [
|
| 129 |
+
{"type": "text", "text": "Picture 1 — exact first frame at 0.00 seconds:\n"},
|
| 130 |
+
{"type": "image"},
|
| 131 |
+
{"type": "text", "text": "\n" + request},
|
| 132 |
+
]
|
| 133 |
+
elif task == "l2av":
|
| 134 |
+
user_content = [
|
| 135 |
+
{"type": "text", "text": "Picture 1 — exact final frame at the end of the target video:\n"},
|
| 136 |
+
{"type": "image"},
|
| 137 |
+
{"type": "text", "text": "\n" + request},
|
| 138 |
+
]
|
| 139 |
+
else:
|
| 140 |
+
user_content = [
|
| 141 |
+
{"type": "text", "text": "Picture 1 — exact first frame at 0.00 seconds:\n"},
|
| 142 |
+
{"type": "image"},
|
| 143 |
+
{"type": "text", "text": "\nPicture 2 — exact final frame at the end of the target video:\n"},
|
| 144 |
+
{"type": "image"},
|
| 145 |
+
{"type": "text", "text": "\n" + request},
|
| 146 |
+
]
|
| 147 |
+
|
| 148 |
+
return [
|
| 149 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 150 |
+
{"role": "user", "content": user_content},
|
| 151 |
+
]
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def expected_image_count(task: str) -> int:
|
| 155 |
+
"""Return the required number of ordered reference images for a task."""
|
| 156 |
+
|
| 157 |
+
return {"t2av": 0, "i2av": 1, "l2av": 1, "fl2av": 2}[normalize_task(task)]
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=2.6
|
| 2 |
+
torchvision>=0.21
|
| 3 |
+
transformers>=4.57.1
|
| 4 |
+
accelerate>=1.10
|
| 5 |
+
peft>=0.18
|
| 6 |
+
safetensors>=0.5
|
| 7 |
+
pillow>=10
|