2424# limitations under the License. #
2525#############################################################################
2626
27- import sys
28- import os
29- import logging
3027import json
28+ import logging
29+ import os
30+ import sys
3131from pathlib import Path
3232
3333import unittest
3737sys .path .append (os .path .join (os .path .dirname (__file__ ), '..' ))
3838from gemini_video_summarization_component .gemini_video_summarization_component import GeminiVideoSummarizationComponent
3939
40- import unittest
4140import mpf_component_api as mpf
42- from transformers import AutoProcessor , AutoModelForCausalLM
43- import torch
4441
4542logging .basicConfig (level = logging .ERROR )
46- USE_MOCKS = True
47- TEST_DATA = Path ( "data" )
48-
49- # Replace with your own desired model name
50- MODEL_NAME = " gemini-2.5-flash"
51- OPENAI_MODEL_NAME = "o4-mini"
52-
53- # Replace with your own path to the Google Application Credentials JSON file
54- GOOGLE_APPLICATION_CREDENTIALS = "../application_default_credentials.json"
55- OPENAI_APPLICATION_CREDENTIALS = "../openai_api_key.txt"
56-
57- job_properties = dict (
58- APPLICATION_CREDENTIALS = None ,
59- GENERATION_PROMPT_PATH = "../gemini_video_summarization_component/data/default_prompt.txt"
60- )
43+ TEST_DATA = Path ( __file__ ). resolve (). parent / "data"
44+ RUN_VLLM_TESTS = os . environ . get ( "RUN_VLLM_TESTS" , "false" ). lower () == "true"
45+ OPENAI_BASE_URL = os . environ . get (
46+ "GEMINI_TEST_OPENAI_BASE_URL" ,
47+ "http:// gemini-video-summarization-server:8000/v1" )
48+ OPENAI_MODEL_NAME = os . environ . get (
49+ "GEMINI_TEST_MODEL_NAME" ,
50+ "google/gemma-4-12B-it" )
51+
52+ job_properties = {
53+ "API" : "OpenAI" ,
54+ "OPENAI_BASE_URL" : OPENAI_BASE_URL ,
55+ "MODEL_NAME" : OPENAI_MODEL_NAME ,
56+ "GENERATION_MAX_ATTEMPTS" : "1" ,
57+ }
6158
6259CAT_TIMELINE = {
6360 "video_summary" : "A cat is sitting on a cobblestone street, looking around as people walk by." ,
257254
258255class TestGemini (unittest .TestCase ):
259256
260- def test_local_chat_template_falls_back_when_processor_template_missing (self ):
261- class FakeProcessor :
262- def apply_chat_template (self , messages , tokenize = False , add_generation_prompt = True ):
263- raise ValueError (
264- "Cannot use chat template functions because tokenizer.chat_template "
265- "is not set and no template argument was passed!"
266- )
267-
268- component = GeminiVideoSummarizationComponent (
269- model = object (),
270- processor = FakeProcessor (),
271- device = "cpu"
272- )
273- messages = [
274- {
275- "role" : "user" ,
276- "content" : [
277- {"type" : "video" , "video" : "/tmp/video.mp4" },
278- {"type" : "text" , "text" : "Summarize." }
279- ]
280- }
281- ]
282-
283- self .assertEqual (
284- "<bos><|turn>user\n \n \n <|video|>\n \n Summarize.<turn|>\n <|turn>model\n " ,
285- component ._apply_local_chat_template (messages )
286- )
287-
288- def test_local_num_frames_avoids_torchvision_endpoint_index (self ):
289- self .assertEqual (
290- 29 ,
291- GeminiVideoSummarizationComponent ._avoid_torchvision_endpoint_frame_index (290 , 30 )
292- )
293- self .assertEqual (
294- 32 ,
295- GeminiVideoSummarizationComponent ._avoid_torchvision_endpoint_frame_index (290 , 32 )
296- )
297-
298- def test_local_model_uses_preprocessed_frame_count_for_num_frames (self ):
299- response = json .dumps (MISSILE_TIMELINE )
300-
301- class FakeInputs (dict ):
302- def __init__ (self ):
303- super ().__init__ (input_ids = [[1 , 2 ]])
304-
305- @property
306- def input_ids (self ):
307- return self ["input_ids" ]
308-
309- def to (self , device ):
310- return self
311-
312- class FakeModel :
313- def generate (self , ** kwargs ):
314- return [[1 , 2 , 3 ]]
315-
316- class FakeVideoProcessor :
317- num_frames = 32
318-
319- class FakeProcessor :
320- video_processor = FakeVideoProcessor ()
321-
322- def apply_chat_template (self , messages , tokenize = False , add_generation_prompt = True ):
323- return "<|video|> Summarize."
324-
325- def __call__ (self , ** kwargs ):
326- self .call_kwargs = kwargs
327- return FakeInputs ()
328-
329- def batch_decode (self , generated_ids , skip_special_tokens = True ):
330- return [response ]
331-
332- processor = FakeProcessor ()
333- component = GeminiVideoSummarizationComponent (
334- model = FakeModel (),
335- processor = processor ,
336- device = "cpu"
337- )
338- job = mpf .VideoJob (
339- "short local model job" ,
340- str (TEST_DATA / "falling-missile-cropped-speedup-trimmed 2.mp4" ),
341- 0 ,
342- 29 ,
343- {},
344- MISSILE_VIDEO_PROPERTIES
345- )
346-
347- self .assertEqual (response , component ._local_get_response (job , "Summarize." ))
348- self .assertEqual (32 , processor .call_kwargs ["num_frames" ])
349-
350257 def run_patched_job (self , component , job , response ):
351- if not USE_MOCKS :
258+ patch_path = (
259+ "gemini_video_summarization_component.gemini_video_summarization_component."
260+ "GeminiVideoSummarizationComponent._openai_response"
261+ )
262+ with unittest .mock .patch (patch_path , return_value = response ):
352263 return component .get_detections_from_video (job )
353-
354- if USE_MOCKS :
355- if component .model is not None :
356- response_method = "_local_get_response"
357- elif component .api == "OpenAI" :
358- response_method = "_openai_response"
359- elif component .api == "Google" :
360- response_method = "_google_response"
361- else :
362- response_method = "_get_response"
363-
364- patch_path = (
365- "gemini_video_summarization_component.gemini_video_summarization_component."
366- f"GeminiVideoSummarizationComponent.{ response_method } "
367- )
368- with unittest .mock .patch (patch_path , return_value = response ):
369- return component .get_detections_from_video (job )
370264
371265 def assert_detection_region (self , detection , frame_width , frame_height ):
372266 self .assertEqual (0 , detection .x_left_upper )
@@ -387,13 +281,12 @@ def assert_first_middle_last_detections(self, track, frame_width, frame_height):
387281 self .assert_detection_region (track .frame_locations [middle_frame ], frame_width , frame_height )
388282
389283 def test_openai_api_routes_to_openai_response (self ):
390- component = GeminiVideoSummarizationComponent (API = "OpenAI" )
284+ component = GeminiVideoSummarizationComponent ()
391285
392286 job = mpf .VideoJob ('openai cat job' , str (TEST_DATA / 'cat.mp4' ), 0 , 170 ,
393287 {
394- "APPLICATION_CREDENTIALS" : OPENAI_APPLICATION_CREDENTIALS ,
288+ ** job_properties ,
395289 "MODEL_NAME" : OPENAI_MODEL_NAME ,
396- "GENERATION_PROMPT_PATH" :"../gemini_video_summarization_component/data/default_prompt.txt" ,
397290 "GENERATION_MAX_ATTEMPTS" : "1" ,
398291 },
399292 CAT_VIDEO_PROPERTIES )
@@ -418,13 +311,12 @@ def test_openai_api_routes_to_openai_response(self):
418311 self .assert_first_middle_last_detections (results [0 ], frame_width , frame_height )
419312
420313 def test_openai_api_invalid_json_response (self ):
421- component = GeminiVideoSummarizationComponent (API = "OpenAI" )
314+ component = GeminiVideoSummarizationComponent ()
422315
423316 job = mpf .VideoJob ('openai invalid cat job JSON' , str (TEST_DATA / 'cat.mp4' ), 0 , 100 ,
424317 {
425- "APPLICATION_CREDENTIALS" : OPENAI_APPLICATION_CREDENTIALS ,
318+ ** job_properties ,
426319 "MODEL_NAME" : OPENAI_MODEL_NAME ,
427- "GENERATION_PROMPT_PATH" :"../gemini_video_summarization_component/data/default_prompt.txt" ,
428320 "GENERATION_MAX_ATTEMPTS" : "1" ,
429321 },
430322 CAT_VIDEO_PROPERTIES )
@@ -436,9 +328,8 @@ def test_openai_api_invalid_json_response(self):
436328 self .assertIn ("not valid JSON" , str (cm .exception ))
437329
438330 def test_multiple_videos (self ):
439- component = GeminiVideoSummarizationComponent (API = "Google" )
331+ component = GeminiVideoSummarizationComponent ()
440332 job_props = job_properties .copy ()
441- job_props ["APPLICATION_CREDENTIALS" ] = GOOGLE_APPLICATION_CREDENTIALS
442333
443334 job = mpf .VideoJob ('valid cat job' , str (TEST_DATA / 'cat.mp4' ), 0 , 170 , job_props , CAT_VIDEO_PROPERTIES )
444335 frame_width = int (job .media_properties ['FRAME_WIDTH' ])
@@ -526,12 +417,11 @@ def test_multiple_videos(self):
526417 self .assert_first_middle_last_detections (results [1 ], frame_width , frame_height )
527418
528419 def test_invalid_timeline (self ):
529- component = GeminiVideoSummarizationComponent (API = "Google" )
420+ component = GeminiVideoSummarizationComponent ()
530421
531422 job = mpf .VideoJob ('invalid cat job' , str (TEST_DATA / 'cat.mp4' ), 0 , 15000 ,
532423 {
533- "APPLICATION_CREDENTIALS" : GOOGLE_APPLICATION_CREDENTIALS ,
534- "GENERATION_PROMPT_PATH" :"../gemini_video_summarization_component/data/default_prompt.txt" ,
424+ ** job_properties ,
535425 "GENERATION_MAX_ATTEMPTS" : "1" ,
536426 },
537427 CAT_VIDEO_PROPERTIES )
@@ -545,8 +435,7 @@ def test_invalid_timeline(self):
545435 # test disabling time check
546436 job = mpf .VideoJob ('invalid cat job' , str (TEST_DATA / 'cat.mp4' ), 0 , 15000 ,
547437 {
548- "GOOGLE_APPLICATION_CREDENTIALS" : GOOGLE_APPLICATION_CREDENTIALS ,
549- "GENERATION_PROMPT_PATH" :"../gemini_video_summarization_component/data/default_prompt.txt" ,
438+ ** job_properties ,
550439 "GENERATION_MAX_ATTEMPTS" : "1" ,
551440 "TIMELINE_CHECK_TARGET_THRESHOLD" : "-1"
552441 },
@@ -557,12 +446,11 @@ def test_invalid_timeline(self):
557446 self .assertIn ("cat" , results [0 ].detection_properties ["TEXT" ])
558447
559448 def test_invalid_json_response (self ):
560- component = GeminiVideoSummarizationComponent (API = "Google" )
449+ component = GeminiVideoSummarizationComponent ()
561450
562451 job = mpf .VideoJob ('invalid cat job JSON' , str (TEST_DATA / 'cat.mp4' ), 0 , 100 ,
563452 {
564- "APPLICATION_CREDENTIALS" : GOOGLE_APPLICATION_CREDENTIALS ,
565- "GENERATION_PROMPT_PATH" :"../gemini_video_summarization_component/data/default_prompt.txt" ,
453+ ** job_properties ,
566454 "GENERATION_MAX_ATTEMPTS" : "1" ,
567455 },
568456 CAT_VIDEO_PROPERTIES )
@@ -574,12 +462,11 @@ def test_invalid_json_response(self):
574462 self .assertIn ("not valid JSON" , str (cm .exception ))
575463
576464 def test_empty_response (self ):
577- component = GeminiVideoSummarizationComponent (API = "Google" )
465+ component = GeminiVideoSummarizationComponent ()
578466
579467 job = mpf .VideoJob ('empty cat job' , str (TEST_DATA / 'cat.mp4' ), 0 , 170 ,
580468 {
581- "GENERATION_PROMPT_PATH" :"../gemini_video_summarization_component/data/default_prompt.txt" ,
582- "GENERATION_MAX_ATTEMPTS" : "1" ,
469+ ** job_properties ,
583470 },
584471 CAT_VIDEO_PROPERTIES )
585472
@@ -589,5 +476,33 @@ def test_empty_response(self):
589476 self .assertEqual (mpf .DetectionError .DETECTION_FAILED , cm .exception .error_code )
590477 self .assertIn ("Empty response" , str (cm .exception ))
591478
479+
480+ @unittest .skipUnless (RUN_VLLM_TESTS , "RUN_VLLM_TESTS is disabled" )
481+ def test_vllm_openai_compatible_api (self ):
482+ component = GeminiVideoSummarizationComponent ()
483+ integration_properties = {
484+ ** job_properties ,
485+ "ENABLE_AUDIO" : "0" ,
486+ "ENABLE_TIMELINE" : "0" ,
487+ "GENERATION_MAX_ATTEMPTS" : "2" ,
488+ "OPENAI_REQUEST_TIMEOUT_SECONDS" : "600" ,
489+ "OPENAI_RESPONSE_FORMAT_JSON_OBJECT" : "false" ,
490+ }
491+ job = mpf .VideoJob (
492+ "vllm cat job" ,
493+ str (TEST_DATA / "cat.mp4" ),
494+ 0 ,
495+ 170 ,
496+ integration_properties ,
497+ CAT_VIDEO_PROPERTIES )
498+
499+ results = component .get_detections_from_video (job )
500+
501+ self .assertEqual (1 , len (results ))
502+ self .assertEqual (
503+ "TRUE" ,
504+ results [0 ].detection_properties ["SEGMENT SUMMARY" ])
505+ self .assertTrue (results [0 ].detection_properties ["TEXT" ].strip ())
506+
592507if __name__ == "__main__" :
593508 unittest .main (verbosity = 2 )
0 commit comments