lightx2v commited on
Commit
37f833e
·
verified ·
1 Parent(s): 5788c70

Upload 8B multitask prompt rewriter LoRA

Browse files
Files changed (4) hide show
  1. README.md +154 -0
  2. infer.py +258 -0
  3. prompt_template.py +157 -0
  4. 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
+ [![Model](https://img.shields.io/badge/Hugging%20Face-model-yellow)](https://huggingface.co/lightx2v/MiniMax-H3-Prompt-Rewriter-LoRA-8B)
24
+ [![LightX2V](https://img.shields.io/badge/LightX2V-GitHub-black)](https://github.com/ModelTC/LightX2V)
25
+ [![Base model](https://img.shields.io/badge/Base-Qwen3--VL--8B--Instruct-blue)](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