44from __future__ import annotations
55
66import argparse
7+ import gc
78from pathlib import Path
89import shutil
910from typing import Any
1011
12+ import numpy as np
1113import pyarrow as pa
1214import pyarrow .parquet as pq
15+ import torch
1316
1417from fastvideo .configs .pipelines .minimax_h3 import MiniMaxH3PipelineConfig
15- from fastvideo .dataset .minimax_h3_ref2va_dataset import pyarrow_schema_minimax_h3_ref2va
18+ from fastvideo .dataset .minimax_h3_ref2va_dataset import (
19+ MINIMAX_H3_REF2VA_AUDIO_ROW_WIDTH ,
20+ MINIMAX_H3_REF2VA_SCHEMA_VERSION ,
21+ MINIMAX_H3_REF2VA_VISUAL_ROW_WIDTH ,
22+ collate_minimax_h3_ref2va_rows ,
23+ pyarrow_schema_minimax_h3_ref2va ,
24+ )
1625from fastvideo .fastvideo_args import FastVideoArgs
26+ from fastvideo .models .schedulers .scheduling_minimax_h3 import MiniMaxH3Scheduler
27+ from fastvideo .pipelines import ForwardBatch
28+ from fastvideo .pipelines .basic .minimax_h3 .packing import (
29+ MINIMAX_H3_KEYFRAME_ENCODE_SEED ,
30+ MINIMAX_H3_KEYFRAME_NOISE_AUG ,
31+ audio_latent_num_frames ,
32+ keyframe_condition_noise ,
33+ patchify_video_latents ,
34+ video_latent_num_frames ,
35+ )
1736from fastvideo .pipelines .basic .minimax_h3 .ref2va_manifest import (
1837 MiniMaxH3RawReference ,
1938 MiniMaxH3Ref2VARawSample ,
2039 build_minimax_h3_references ,
2140 load_minimax_h3_ref2va_raw_samples ,
2241)
23- from fastvideo .pipelines .basic .minimax_h3 .reference import MiniMaxH3PreparedReference , prepare_reference
42+ from fastvideo .pipelines .basic .minimax_h3 .reference import (
43+ MiniMaxH3PreparedReference ,
44+ prepare_reference ,
45+ trim_reference_num_frames ,
46+ )
47+ from fastvideo .pipelines .basic .minimax_h3 .stages .minimax_h3_conditioning import (
48+ MINIMAX_H3_TEXT_TOKEN_TAGS_KEY ,
49+ MiniMaxH3ConditioningStage ,
50+ )
51+ from fastvideo .pipelines .basic .minimax_h3 .stages .minimax_h3_input_preparation import MINIMAX_H3_KEYFRAMES_KEY
2452from fastvideo .pipelines .preprocess .preprocess_minimax_h3_overfit import (
2553 AUDIO_SAMPLE_RATE ,
2654 MODEL_PATH ,
2755 NUM_FRAMES ,
56+ VIDEO_HEIGHT ,
57+ VIDEO_WIDTH ,
2858 _init_single_process_distributed ,
59+ _load_component ,
60+ build_parquet_record ,
2961 encode_audio_latents ,
3062 encode_video_latents ,
3163 load_training_media ,
3264)
33- from fastvideo .pipelines .preprocess .preprocess_minimax_h3_ref2va_overfit import (
34- build_ref2va_parquet_record ,
35- encode_ref2va_conditioning ,
36- encode_ref_audio_anchor ,
37- encode_ref_visual_anchor ,
38- validate_record_contract ,
39- )
4065from fastvideo .utils import verify_model_config_and_directory
4166
42- DEFAULT_MANIFEST = Path ("data/my_openhumanvid /train.jsonl" )
43- DEFAULT_OUTPUT_DIR = Path ("data/my_openhumanvid_h3_ref2va_preprocessed " )
67+ DEFAULT_MANIFEST = Path ("examples/training/finetune/minimax-h3/openhumanvid /train.jsonl" )
68+ DEFAULT_OUTPUT_DIR = Path ("data/openhumanvid_h3_ref2va_single_sample_preprocessed " )
4469_PATCH_SIZE = (1 , 2 , 2 )
4570
4671
@@ -54,6 +79,241 @@ def parse_args() -> argparse.Namespace:
5479 return parser .parse_args ()
5580
5681
82+ def _sample_visual_posterior (posterior : Any ) -> torch .Tensor :
83+ generator = torch .Generator ("cpu" ).manual_seed (MINIMAX_H3_KEYFRAME_ENCODE_SEED )
84+ return posterior .sample (generator = generator )
85+
86+
87+ def encode_ref_visual_anchor (
88+ references : list [MiniMaxH3PreparedReference ],
89+ model_path : Path ,
90+ model_index : dict [str , Any ],
91+ fastvideo_args : FastVideoArgs ,
92+ patch_size : tuple [int , int , int ],
93+ ) -> torch .Tensor :
94+ """Encode and cache official 0.999-noised ordered visual condition rows."""
95+ visual_references = [reference for reference in references if reference .media_type != "audio" ]
96+ if not visual_references :
97+ return torch .empty ((0 , MINIMAX_H3_REF2VA_VISUAL_ROW_WIDTH ), dtype = torch .float32 )
98+
99+ print ("Loading MiniMax H3 video VAE for Ref2VA visual anchors" )
100+ vae = _load_component ("vae" , model_path , model_index , fastvideo_args )
101+ device = torch .device ("cuda:0" )
102+ clean_rows : list [torch .Tensor ] = []
103+ latent_channels = int (vae .latent_channels )
104+ with torch .no_grad ():
105+ for reference in references :
106+ if reference .media_type == "audio" :
107+ continue
108+ if reference .media_type == "image" :
109+ if reference .image is None :
110+ raise ValueError ("Prepared image reference is missing pixels" )
111+ pixels = torch .from_numpy (np .asarray (reference .image ).copy ()).permute (2 , 0 , 1 )[None , :, None ]
112+ pixels = pixels .to (device = device , dtype = torch .float32 ).div_ (255.0 )
113+ posterior = vae .encode_keyframe (vae .normalize_pixels (pixels )).latent_dist
114+ else :
115+ if reference .frames is None :
116+ raise ValueError ("Prepared video reference is missing frames" )
117+ frames = reference .frames [:trim_reference_num_frames (reference .frames .shape [0 ])]
118+ pixels = torch .from_numpy (frames .copy ()).permute (3 , 0 , 1 , 2 )[None ]
119+ pixels = pixels .to (device = device , dtype = torch .float32 ).div_ (255.0 )
120+ posterior = vae .encode (vae .normalize_pixels (pixels )).latent_dist
121+
122+ # The fp16 round trip before latent normalization is part of the
123+ # released Ref2VA condition encoding path.
124+ latents = vae .normalize_latents (_sample_visual_posterior (posterior ).to (torch .float16 ).float ()).cpu ()
125+ if latents .ndim != 5 or latents .shape [0 ] != 1 or latents .shape [1 ] != latent_channels :
126+ raise ValueError (f"Unexpected reference visual latent shape: { tuple (latents .shape )} " )
127+ reference .num_latent_frames = int (latents .shape [2 ])
128+ reference .latent_height = int (latents .shape [3 ])
129+ reference .latent_width = int (latents .shape [4 ])
130+ clean_rows .append (patchify_video_latents (latents , patch_size ).float ().contiguous ())
131+ del posterior , latents , pixels
132+
133+ clean_anchor = torch .cat (clean_rows ).to (device = device , dtype = torch .float32 )
134+ shapes = tuple ((reference .num_latent_frames , reference .latent_height , reference .latent_width )
135+ for reference in references if reference .media_type != "audio" )
136+ noise_generator = torch .Generator ("cpu" ).manual_seed (MINIMAX_H3_KEYFRAME_ENCODE_SEED )
137+ noise = keyframe_condition_noise (
138+ shapes ,
139+ patch_size ,
140+ latent_channels ,
141+ generator = noise_generator ,
142+ device = device ,
143+ dtype = torch .float32 ,
144+ )
145+ anchor = MiniMaxH3Scheduler (shift = 12.0 ).scale_noise (
146+ clean_anchor ,
147+ MINIMAX_H3_KEYFRAME_NOISE_AUG ,
148+ noise ,
149+ ).float ().cpu ().contiguous ()
150+
151+ expected_width = latent_channels * int (np .prod (patch_size ))
152+ if expected_width != MINIMAX_H3_REF2VA_VISUAL_ROW_WIDTH or anchor .shape [1 ] != expected_width :
153+ raise ValueError (
154+ f"Ref2VA visual anchor width must be { MINIMAX_H3_REF2VA_VISUAL_ROW_WIDTH } , got { anchor .shape [1 ]} " )
155+ del clean_anchor , clean_rows , noise , vae
156+ gc .collect ()
157+ torch .cuda .empty_cache ()
158+ print (f"Ref2VA visual anchor shape: { tuple (anchor .shape )} at fixed clean-time 0.999" )
159+ return anchor
160+
161+
162+ def encode_ref_audio_anchor (
163+ references : list [MiniMaxH3PreparedReference ],
164+ model_path : Path ,
165+ model_index : dict [str , Any ],
166+ fastvideo_args : FastVideoArgs ,
167+ ) -> torch .Tensor :
168+ """Encode clean channel-major audio anchors in ordered-reference order."""
169+ if not any (reference .has_audio for reference in references ):
170+ return torch .empty ((0 , MINIMAX_H3_REF2VA_AUDIO_ROW_WIDTH ), dtype = torch .float32 )
171+
172+ print ("Loading MiniMax H3 audio VAE for Ref2VA audio anchors" )
173+ audio_vae = _load_component ("audio_vae" , model_path , model_index , fastvideo_args )
174+ device = torch .device ("cuda:0" )
175+ latent_channels = int (audio_vae .latent_channels )
176+ if int (audio_vae .sampling_rate ) != AUDIO_SAMPLE_RATE :
177+ raise ValueError (f"Audio VAE sampling rate must be { AUDIO_SAMPLE_RATE } , got { audio_vae .sampling_rate } " )
178+ rows : list [torch .Tensor ] = []
179+ with torch .no_grad ():
180+ for reference in references :
181+ if not reference .has_audio :
182+ continue
183+ if reference .waveform is None :
184+ raise ValueError ("Audio-bearing reference is missing its prepared waveform" )
185+ posterior = audio_vae .encode (reference .waveform .to (device = device , dtype = torch .float32 )[:, None ]).latent_dist
186+ latents = audio_vae .normalize_latents (posterior .mode ().float ()).cpu ().transpose (1 , 2 )
187+ if latents .ndim != 3 or latents .shape [0 ] != 2 or latents .shape [2 ] != latent_channels :
188+ raise ValueError (f"Unexpected reference audio latent shape: { tuple (latents .shape )} " )
189+ reference .num_audio_latents = int (latents .shape [1 ])
190+ rows .append (latents .reshape (- 1 , latent_channels ).float ().contiguous ())
191+ del posterior , latents
192+ anchor = torch .cat (rows ).float ().contiguous ()
193+ if latent_channels != MINIMAX_H3_REF2VA_AUDIO_ROW_WIDTH or anchor .shape [1 ] != latent_channels :
194+ raise ValueError (
195+ f"Ref2VA audio anchor width must be { MINIMAX_H3_REF2VA_AUDIO_ROW_WIDTH } , got { anchor .shape [1 ]} " )
196+ del rows , audio_vae
197+ gc .collect ()
198+ torch .cuda .empty_cache ()
199+ print (f"Ref2VA audio anchor shape: { tuple (anchor .shape )} " )
200+ return anchor
201+
202+
203+ def encode_ref2va_conditioning (
204+ caption : str ,
205+ references : list [MiniMaxH3PreparedReference ],
206+ model_path : Path ,
207+ model_index : dict [str , Any ],
208+ fastvideo_args : FastVideoArgs ,
209+ ) -> tuple [torch .Tensor , torch .Tensor ]:
210+ """Encode the exact ordered presentation without padding or truncation."""
211+ print ("Loading MiniMax H3 tokenizer, processor, and Qwen3-VL encoder" )
212+ tokenizer = _load_component ("tokenizer" , model_path , model_index , fastvideo_args )
213+ processor = _load_component ("processor" , model_path , model_index , fastvideo_args )
214+ conditioner = _load_component ("text_encoder" , model_path , model_index , fastvideo_args )
215+ stage = MiniMaxH3ConditioningStage (
216+ conditioner = conditioner ,
217+ tokenizer = tokenizer ,
218+ processor = processor ,
219+ ref2va = bool (references ),
220+ )
221+ batch = ForwardBatch (data_type = "video" , prompt = caption , references = references )
222+ if not references :
223+ # The Ref stage intentionally rejects an empty list. Prompt-only rows
224+ # use the exact T2VA tokenizer path and contain only text tags.
225+ batch .extra [MINIMAX_H3_KEYFRAMES_KEY ] = []
226+ batch = stage .forward (batch , fastvideo_args )
227+ if len (batch .prompt_embeds ) != 1 :
228+ raise RuntimeError ("MiniMax H3 conditioning must return exactly one embedding" )
229+ text_embedding = batch .prompt_embeds [0 ].squeeze (0 ).float ().cpu ().contiguous ()
230+ text_token_tags = batch .extra .get (MINIMAX_H3_TEXT_TOKEN_TAGS_KEY )
231+ if not isinstance (text_token_tags , torch .Tensor ):
232+ raise RuntimeError ("MiniMax H3 conditioning did not return text token tags" )
233+ text_token_tags = text_token_tags .to (dtype = torch .long , device = "cpu" ).contiguous ()
234+ if text_embedding .ndim != 2 or text_embedding .shape [1 ] != 5120 or text_embedding .shape [0 ] == 0 :
235+ raise ValueError (f"Unexpected Qwen embedding shape: { tuple (text_embedding .shape )} " )
236+ if text_token_tags .shape != text_embedding .shape [:1 ]:
237+ raise ValueError ("Qwen text token tags do not align with its hidden states" )
238+ if not bool (((text_token_tags == 0 ) | (text_token_tags == 1 )).all ()):
239+ raise ValueError ("Qwen text token tags may contain only vision=0 and text=1" )
240+
241+ dynamic_length = int (text_embedding .shape [0 ])
242+ print (f"Qwen Ref2VA conditioning shape: { tuple (text_embedding .shape )} ; preserving all { dynamic_length } tokens" )
243+ del batch , stage , conditioner , processor , tokenizer
244+ gc .collect ()
245+ torch .cuda .empty_cache ()
246+ return text_embedding , text_token_tags
247+
248+
249+ def _serialize_float32_tensor (record : dict [str , Any ], name : str , tensor : torch .Tensor ) -> None :
250+ tensor = tensor .detach ().float ().cpu ().contiguous ()
251+ record [f"{ name } _bytes" ] = tensor .numpy ().tobytes ()
252+ record [f"{ name } _shape" ] = list (tensor .shape )
253+ record [f"{ name } _dtype" ] = "float32"
254+
255+
256+ def _canonical_prepared_references (references : list [MiniMaxH3PreparedReference ]) -> list [dict [str , Any ]]:
257+ canonical : list [dict [str , Any ]] = []
258+ for reference in references :
259+ is_audio = reference .media_type == "audio"
260+ canonical .append ({
261+ "media_type" : reference .media_type ,
262+ "has_audio" : bool (reference .has_audio ),
263+ # MiniMaxH3PreparedReference defaults num_latent_frames to 1, so
264+ # standalone audio must be canonicalized explicitly to zero visual
265+ # geometry rather than copying dataclass defaults.
266+ "num_latent_frames" : 0 if is_audio else int (reference .num_latent_frames ),
267+ "latent_height" : 0 if is_audio else int (reference .latent_height ),
268+ "latent_width" : 0 if is_audio else int (reference .latent_width ),
269+ "num_audio_latents" : int (reference .num_audio_latents ),
270+ })
271+ return canonical
272+
273+
274+ def build_ref2va_parquet_record (
275+ * ,
276+ file_name : str ,
277+ caption : str ,
278+ video_latents : torch .Tensor ,
279+ audio_latents : torch .Tensor ,
280+ text_embedding : torch .Tensor ,
281+ text_token_tags : torch .Tensor ,
282+ ref_visual_anchor : torch .Tensor ,
283+ ref_audio_anchor : torch .Tensor ,
284+ references : list [MiniMaxH3PreparedReference ],
285+ ) -> dict [str , Any ]:
286+ record = build_parquet_record (
287+ file_name = file_name ,
288+ caption = caption ,
289+ video_latents = video_latents ,
290+ audio_latents = audio_latents ,
291+ text_embedding = text_embedding ,
292+ )
293+ record ["schema_version" ] = MINIMAX_H3_REF2VA_SCHEMA_VERSION
294+ record ["text_token_tags" ] = text_token_tags .to (dtype = torch .long , device = "cpu" ).tolist ()
295+ _serialize_float32_tensor (record , "ref_visual_anchor" , ref_visual_anchor )
296+ _serialize_float32_tensor (record , "ref_audio_anchor" , ref_audio_anchor )
297+ record ["references" ] = _canonical_prepared_references (references )
298+ return record
299+
300+
301+ def validate_record_contract (record : dict [str , Any ]) -> None :
302+ missing = [name for name in pyarrow_schema_minimax_h3_ref2va .names if name not in record ]
303+ if missing :
304+ raise ValueError (f"Ref2VA record is missing schema fields: { missing } " )
305+ expected_target_shapes = {
306+ "vae_latent_shape" : [24 , video_latent_num_frames (NUM_FRAMES ), VIDEO_HEIGHT // 16 , VIDEO_WIDTH // 16 ],
307+ "audio_latent_shape" : [2 , 32 , audio_latent_num_frames (NUM_FRAMES )],
308+ }
309+ for name , expected in expected_target_shapes .items ():
310+ if record [name ] != expected :
311+ raise ValueError (f"{ name } must be { expected } , got { record [name ]} " )
312+ # Reuse the actual training collator as the authoritative nested-reference,
313+ # empty-anchor, dtype, row-count, and dynamic-text validation gate.
314+ collate_minimax_h3_ref2va_rows ([record ])
315+
316+
57317def _prepare_references (references : tuple [MiniMaxH3RawReference , ...], ) -> list [MiniMaxH3PreparedReference ]:
58318 raw_references = build_minimax_h3_references (references )
59319 prepared = [prepare_reference (reference , NUM_FRAMES , AUDIO_SAMPLE_RATE ) for reference in raw_references ]
@@ -118,6 +378,7 @@ def _build_record(
118378 fastvideo_args ,
119379 )
120380 record = build_ref2va_parquet_record (
381+ file_name = sample .target_file ,
121382 caption = sample .caption ,
122383 video_latents = video_latents ,
123384 audio_latents = audio_latents ,
@@ -128,7 +389,6 @@ def _build_record(
128389 references = references ,
129390 )
130391 record ["id" ] = sample .sample_id
131- record ["file_name" ] = sample .target_file
132392 validate_record_contract (record )
133393 return record
134394
0 commit comments