@@ -161,6 +161,104 @@ def test_add_video_embeds_frame_ordinals_outside_normal_data(self):
161161 _ , bodies = table .scan ().read_blobs ("video" , parallelism = 2 )
162162 self .assertEqual ([video_bytes ] * 3 , bodies ["video" ])
163163
164+ def test_add_images_as_video_encodes_and_stores_frame_rows (self ):
165+ table = self .conn .create_table (
166+ "image_video_frames" ,
167+ schema = _schema ({
168+ "episode_id" : pa .int64 (),
169+ "video" : pa .large_binary (),
170+ }),
171+ options = dict (_PARQUET_OPTIONS , ** {
172+ "video-frame-field" : "video" ,
173+ "blob-as-descriptor" : "true" ,
174+ }),
175+ )
176+ calls = []
177+
178+ def encode (images , output_path , ** options ):
179+ images = list (images )
180+ calls .append ((images , options ))
181+ with open (output_path , "wb" ) as output :
182+ output .write (b"encoded-video" )
183+ return len (images )
184+
185+ with patch (
186+ "pypaimon.multimodal.video._encode_images_to_video" ,
187+ encode ):
188+ table .add_images_as_video (
189+ [b"frame-0" , b"frame-1" ],
190+ [{"episode_id" : 42 }, {"episode_id" : 42 }],
191+ fps = 30 ,
192+ codec = "mpeg4" ,
193+ pixel_format = "yuv420p" ,
194+ gop_size = 2 ,
195+ codec_options = {"qscale" : "3" },
196+ )
197+
198+ self .assertEqual (
199+ [
200+ (
201+ [b"frame-0" , b"frame-1" ],
202+ {
203+ "fps" : 30 ,
204+ "codec" : "mpeg4" ,
205+ "pixel_format" : "yuv420p" ,
206+ "gop_size" : 2 ,
207+ "codec_options" : {"qscale" : "3" },
208+ },
209+ )
210+ ],
211+ calls ,
212+ )
213+ rows = table .scan ().select (["episode_id" , "video" ]).to_list ()
214+ descriptors = [
215+ pmm .VideoFrameDescriptor .deserialize (row ["video" ])
216+ for row in rows
217+ ]
218+ self .assertEqual ([0 , 1 ], [value .frame_index for value in descriptors ])
219+ _ , bodies = table .scan ().read_blobs ("video" )
220+ self .assertEqual ([b"encoded-video" ] * 2 , bodies ["video" ])
221+
222+ def test_add_images_as_video_rejects_frame_count_mismatch (self ):
223+ table = self .conn .create_table (
224+ "mismatched_image_video_frames" ,
225+ schema = _schema ({
226+ "episode_id" : pa .int64 (),
227+ "video" : pa .large_binary (),
228+ }),
229+ options = dict (_PARQUET_OPTIONS , ** {
230+ "video-frame-field" : "video" ,
231+ "blob-as-descriptor" : "true" ,
232+ }),
233+ )
234+ output_paths = []
235+
236+ def encode (unused_images , output_path , ** unused_options ):
237+ output_paths .append (output_path )
238+ with open (output_path , "wb" ) as output :
239+ output .write (b"one-frame-video" )
240+ return 1
241+
242+ with patch (
243+ "pypaimon.multimodal.video._encode_images_to_video" ,
244+ encode ):
245+ with self .assertRaisesRegex (
246+ ValueError ,
247+ "Image count 1 does not match frame row count 2" ):
248+ table .add_images_as_video (
249+ [b"frame-0" ],
250+ [{"episode_id" : 42 }, {"episode_id" : 42 }],
251+ fps = 30 ,
252+ codec = "mpeg4" ,
253+ pixel_format = "yuv420p" ,
254+ gop_size = 2 ,
255+ )
256+
257+ self .assertFalse (os .path .exists (output_paths [0 ]))
258+ self .assertIsNone (
259+ table .raw_table .snapshot_manager ().get_latest_snapshot ()
260+ )
261+
164262 def test_add_videos_packs_multiple_videos_in_one_commit (self ):
165263 from pypaimon .table .row .blob import Blob , VideoFrameDescriptor
166264
0 commit comments