@@ -92,17 +92,25 @@ def _process_string(string_tensor):
9292
9393def reformat_prompt (example , column , image_placeholder , model_name ):
9494 """reformat prompt for multimodal SFT"""
95- if isinstance (example ["images" ], list ):
96- num_images = len (example ["images" ])
95+ if isinstance (example ["images" ], str ):
96+ example [column ] = mm_processor .reformat_prompt (
97+ example [column ], image_placeholder , model_name , num_images = 0 , video_placeholder = image_placeholder , num_videos = 1
98+ )
9799 else :
98- num_images = 1
99- example [column ] = mm_processor .reformat_prompt (example [column ], image_placeholder , model_name , num_images )
100+ if isinstance (example ["images" ], list ):
101+ num_images = len (example ["images" ])
102+ else :
103+ num_images = 1
104+ example [column ] = mm_processor .reformat_prompt (example [column ], image_placeholder , model_name , num_images )
100105 return example
101106
102107
103108def reformat_response (example , column , model_name ):
104109 """reformat response for multimodal SFT"""
105- example [column ] = mm_processor .reformat_response (example [column ][0 ], model_name )
110+ val = example [column ]
111+ if isinstance (val , (list , tuple )) and len (val ) > 0 :
112+ val = val [0 ]
113+ example [column ] = mm_processor .reformat_response (val , model_name )
106114 return example
107115
108116
@@ -120,9 +128,17 @@ def merge_image_columns(example, image_columns, max_num_images_per_example):
120128
121129
122130def pre_process_image_sft (example , image_column , config ):
123- """pre-process image for multimodal SFT"""
131+ """pre-process image or video for multimodal SFT"""
124132
125133 def _process_image_fn (image ):
134+ if isinstance (image , str ):
135+ import os
136+
137+ video_directory = getattr (config , "video_directory" , "" )
138+ if video_directory :
139+ image = os .path .join (video_directory , image )
140+ return mm_processor .preprocess_image_for_training (image , config )
141+
126142 if isinstance (image , list ):
127143 image = [np .array (mm_utils .convert_to_RGB (img )) for img in image ]
128144 else :
@@ -131,7 +147,7 @@ def _process_image_fn(image):
131147 image = mm_processor .preprocess_image_for_training (image , config )
132148 return image
133149
134- example [image_column ] = _process_image_fn (example [image_column ])
150+ example [image_column ] = _process_image_fn (example [image_column ]) if example . get ( image_column ) is not None else None
135151 return example
136152
137153
@@ -702,12 +718,80 @@ def _pad_image_and_mask(self, preprocessed_image: mm_utils.PreprocessorOutput) -
702718 if not isinstance (preprocessed_image , mm_utils .PreprocessorOutput ):
703719 raise TypeError (f"Input must be multimodal_utils.PreprocessorOutput, but got { type (preprocessed_image )} " )
704720
705- if preprocessed_image .pixel_values is None :
706- raise ValueError ("Input preprocessed_image must have pixel_values to pad images." )
707-
708721 if self .config .model_name and self .config .model_name .startswith ("qwen3-omni" ):
722+ # Pad video_values and audio_values to fixed shapes so grain.Batch can stack them.
723+ video_values = getattr (preprocessed_image , "video_values" , None )
724+ video_grid_thw = getattr (preprocessed_image , "video_grid_thw" , None )
725+ if video_values is not None :
726+ target_shape = mm_processor .get_dummy_video_shape_for_init (
727+ self .config .model_name , batch_size = 1 , config = self .config
728+ )
729+ # target_shape = (1, C, max_T_px, max_H_px, max_W_px)
730+ padded = np .zeros (target_shape [1 :], dtype = video_values .dtype ) # (C, max_T, max_H, max_W)
731+ _ , c , t , h , w = video_values .shape
732+ max_t_px , max_h_px , max_w_px = target_shape [2 ], target_shape [3 ], target_shape [4 ]
733+ t_clip = min (t , max_t_px )
734+ h_clip = min (h , max_h_px )
735+ w_clip = min (w , max_w_px )
736+ padded [:, :t_clip , :h_clip , :w_clip ] = video_values [0 , :, :t_clip , :h_clip , :w_clip ]
737+ preprocessed_image .video_values = padded
738+
739+ if video_grid_thw is not None :
740+ from maxtext .multimodal .processor_qwen3_omni import VIDEO_MAX_GRID_T , VIDEO_MAX_GRID_H , VIDEO_MAX_GRID_W
741+ merge_size = getattr (self .config , "spatial_merge_size_for_vit" , 2 )
742+ max_t = VIDEO_MAX_GRID_T
743+ max_h_merged = VIDEO_MAX_GRID_H // merge_size
744+ max_w_merged = VIDEO_MAX_GRID_W // merge_size
745+
746+ actual_t , actual_h , actual_w = video_grid_thw [0 ]
747+ actual_t = min (actual_t , VIDEO_MAX_GRID_T )
748+ actual_h = min (actual_h , VIDEO_MAX_GRID_H )
749+ actual_w = min (actual_w , VIDEO_MAX_GRID_W )
750+
751+ actual_h_merged = actual_h // merge_size
752+ actual_w_merged = actual_w // merge_size
753+
754+ mask_3d = np .zeros ((max_t , max_h_merged , max_w_merged ), dtype = np .int32 )
755+ mask_3d [:actual_t , :actual_h_merged , :actual_w_merged ] = 1
756+ preprocessed_image .video_mask = mask_3d .flatten ()
757+
758+ print (
759+ f"[SFT_DEBUG] Padding Video: Original Pixel Shape: { video_values .shape } , Padded Pixel Shape: { padded .shape } . "
760+ f"Original Grid (THW): { video_grid_thw [0 ]} , Clipped Grid: [{ actual_t } , { actual_h } , { actual_w } ]. "
761+ f"Video Mask Shape (flattened): { preprocessed_image .video_mask .shape } , Valid tokens count: { np .sum (preprocessed_image .video_mask )} "
762+ )
763+
764+ audio_values = getattr (preprocessed_image , "audio_values" , None )
765+ audio_lengths = getattr (preprocessed_image , "audio_lengths" , None )
766+ if audio_values is not None :
767+ target_audio = mm_processor .get_dummy_audio_shape_for_sft (
768+ self .config .model_name , batch_size = 1 , config = self .config
769+ )
770+ # target_audio = (1, num_mel_bins, AUDIO_MAX_TIME)
771+ _ , mel , t_audio = audio_values .shape
772+ padded_audio = np .zeros (target_audio [1 :], dtype = audio_values .dtype ) # (mel, max_time)
773+ padded_audio [:, :t_audio ] = audio_values [0 ]
774+ preprocessed_image .audio_values = padded_audio
775+
776+ if audio_lengths is not None :
777+ from maxtext .multimodal .processor_qwen3_omni import AUDIO_MAX_TIME , _get_feat_extract_output_lengths
778+ max_audio_tokens = _get_feat_extract_output_lengths (AUDIO_MAX_TIME )
779+ actual_audio_tokens = audio_lengths [0 ]
780+
781+ audio_token_mask = np .zeros (max_audio_tokens , dtype = np .int32 )
782+ audio_token_mask [:actual_audio_tokens ] = 1
783+ preprocessed_image .audio_token_mask = audio_token_mask
784+
785+ print (
786+ f"[SFT_DEBUG] Padding Audio: Original Mel Shape: { audio_values .shape } , Padded Mel Shape: { padded_audio .shape } . "
787+ f"Audio Mask Shape: { preprocessed_image .audio_token_mask .shape } , Valid audio tokens count: { np .sum (preprocessed_image .audio_token_mask )} "
788+ )
789+
709790 return preprocessed_image
710791
792+ if preprocessed_image .pixel_values is None :
793+ raise ValueError ("Input preprocessed_image must have pixel_values to pad images." )
794+
711795 # Determine the maximum number of images/masks allowed.
712796 image_offsets = mm_processor .get_image_offsets (self .config , preprocessed_image )
713797 single_image_offset = image_offsets // preprocessed_image .pixel_values .shape [0 ]
@@ -812,6 +896,27 @@ def map(self, element: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
812896 if preprocessed_image .pixel_mask is not None :
813897 output ["image_masks" ] = preprocessed_image .pixel_mask
814898
899+ # Extract video and audio tensors from Qwen3OmniPreprocessorOutput.
900+ video_values = getattr (preprocessed_image , "video_values" , None )
901+ if video_values is not None :
902+ output ["videos" ] = video_values
903+ video_grid_thw = getattr (preprocessed_image , "video_grid_thw" , None )
904+ if video_grid_thw is not None :
905+ output ["video_grid_thw" ] = video_grid_thw
906+ video_mask = getattr (preprocessed_image , "video_mask" , None )
907+ if video_mask is not None :
908+ output ["video_masks" ] = video_mask
909+
910+ audio_values = getattr (preprocessed_image , "audio_values" , None )
911+ if audio_values is not None :
912+ output ["audios" ] = audio_values
913+ audio_lengths = getattr (preprocessed_image , "audio_lengths" , None )
914+ if audio_lengths is not None :
915+ output ["audio_lengths" ] = audio_lengths
916+ audio_token_mask = getattr (preprocessed_image , "audio_token_mask" , None )
917+ if audio_token_mask is not None :
918+ output ["audio_token_masks" ] = audio_token_mask
919+
815920 return output
816921
817922
0 commit comments