Skip to content

Commit c28de36

Browse files
committed
Added dockerignore so that the test data is not included in the container, fixed typo in vllm dockerfile, added optional testing at image build without GPU
1 parent 7729895 commit c28de36

6 files changed

Lines changed: 99 additions & 597 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.venv/
2+
.vscode/
3+
**/__pycache__/
4+
*.egg-info/
5+
tests/*
6+
!tests/test_gemini.py
7+
!tests/data/
8+
tests/data/*
9+
!tests/data/NOTICE
10+
!tests/data/cat.mp4
11+
!tests/data/dog.mp4
12+
!tests/data/short.mp4

python/GeminiVideoSummarization/Dockerfile

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
ARG BUILD_REGISTRY
3030
ARG BUILD_TAG=latest
31-
FROM ${BUILD_REGISTRY}openmpf_python_executor_ssb:${BUILD_TAG}
31+
FROM ${BUILD_REGISTRY}openmpf_python_executor_ssb:${BUILD_TAG} AS component
3232

3333
RUN pip3 install --no-cache-dir \
3434
tenacity \
@@ -43,11 +43,25 @@ ARG RUN_TESTS=false
4343

4444
RUN --mount=target=.,readwrite \
4545
install-component.sh; \
46-
if [ "${RUN_TESTS,,}" == true ]; then python tests/test_gemini_local.py; fi
46+
if [ "${RUN_TESTS,,}" == true ]; then python tests/test_gemini.py; fi
4747

4848
LABEL org.label-schema.license="Apache 2.0" \
4949
org.label-schema.name="OpenMPF Gemini Video Summarization" \
5050
org.label-schema.schema-version="1.1" \
5151
org.label-schema.url="https://openmpf.github.io" \
5252
org.label-schema.vcs-url="https://github.com/openmpf/openmpf-components" \
53-
org.label-schema.vendor="MITRE"
53+
org.label-schema.vendor="MITRE"
54+
55+
56+
FROM component AS vllm-tests
57+
ARG RUN_TESTS=false
58+
ENV RUN_TESTS=${RUN_TESTS}
59+
60+
WORKDIR /opt/gemini-tests
61+
COPY tests/test_gemini.py ./
62+
COPY tests/data/cat.mp4 data/cat.mp4
63+
64+
ENTRYPOINT ["/bin/bash", "-lc", "if [ \"${RUN_TESTS,,}\" == \"true\" ]; then exec /opt/mpf/plugin-venv/bin/python3 -m unittest -v test_gemini.TestGemini.test_vllm_openai_compatible_api; fi"]
65+
66+
67+
FROM component AS final

python/GeminiVideoSummarization/Dockerfile.vllm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ ENV VLLM_MAX_NUM_SEQS=1
7070
ENV VLLM_NUM_SPECULATIVE_TOKENS=1
7171
ENV VLLM_ENFORCE_EAGER=1
7272
ENV VLLM_SPECULATIVE_ENFORCE_EAGER=1
73-
ENV MPF_LIMIT_MM_PER_PROMPT={"video":1,"audio":1}
73+
ENV MPF_LIMIT_MM_PER_PROMPT='{"video":1,"audio":1}'
7474
ENV VLLM_ENABLE_MTP=0
7575

7676
EXPOSE 8000

python/GeminiVideoSummarization/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,9 @@ gemini-video-summarization:
9696
```
9797
9898
`MODEL_NAME` should match the model exposed by the OpenAI-compatible service. For a local vLLM server, run vLLM separately and configure this component deployment to use that service as the OpenAI base URL.
99+
100+
## Tests
101+
102+
`RUN_TESTS=true` runs the OpenAI-compatible unit suite during the component image build. These tests use a mocked response client, following the `LlmSpeechSummarization` build-time test pattern, so normal CI builds do not require a GPU.
103+
104+
The `vllm-tests` Docker target runs only the live integration test with `RUN_VLLM_TESTS=true`, which sends one real video request to an OpenAI-compatible endpoint. In `openmpf-docker`, `docker-compose.components.test.yml` provides a separate `gemini-video-summarization-vllm-tests` service that waits for `gemini-video-summarization-server` to become healthy before running this target.

python/GeminiVideoSummarization/tests/test_gemini.py

Lines changed: 63 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@
2424
# limitations under the License. #
2525
#############################################################################
2626

27-
import sys
28-
import os
29-
import logging
3027
import json
28+
import logging
29+
import os
30+
import sys
3131
from pathlib import Path
3232

3333
import unittest
@@ -37,27 +37,24 @@
3737
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
3838
from gemini_video_summarization_component.gemini_video_summarization_component import GeminiVideoSummarizationComponent
3939

40-
import unittest
4140
import mpf_component_api as mpf
42-
from transformers import AutoProcessor, AutoModelForCausalLM
43-
import torch
4441

4542
logging.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

6259
CAT_TIMELINE = {
6360
"video_summary": "A cat is sitting on a cobblestone street, looking around as people walk by.",
@@ -257,116 +254,13 @@
257254

258255
class 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\nSummarize.<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+
592507
if __name__ == "__main__":
593508
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)