Skip to content

Commit ea1b119

Browse files
committed
Removed need for constructor args and updated other files as such.
1 parent 0b924bc commit ea1b119

3 files changed

Lines changed: 116 additions & 64 deletions

File tree

python/GeminiVideoSummarization/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ These are the properties read directly by the current component code.
88

99
| Property | Default | Description |
1010
| :--- | :--- | :--- |
11-
| `MODEL_NAME` | empty | Model name sent to the backend, such as a Gemini model name or an OpenAI/vLLM model id. Required for API calls. |
12-
| `APPLICATION_CREDENTIALS` | empty | Path to a credentials file. In Google mode this becomes `GOOGLE_APPLICATION_CREDENTIALS`. In the current OpenAI-compatible path this value is also used to look up an environment variable containing the API key, so confirm the deployed credential convention before production use. |
11+
| `API` | `OpenAI` | Backend API to use for model inference. Supported values are `OpenAI` and `Google`. |
12+
| `OPENAI_BASE_URL` | empty | Optional base URL for an OpenAI-compatible API service such as vLLM. Leave empty to use the OpenAI client default. |
13+
| `MODEL_NAME` | empty | Model name sent to the backend. Required for API calls. |
14+
| `APPLICATION_CREDENTIALS` | empty | Path to a credential file or the name of an environment variable containing credentials. |
1315
| `GENERATION_PROMPT_PATH` | `data/default_prompt.txt` or `data/default_prompt_no_tl.txt` | Optional path to a prompt file. If unset, the component selects the timeline or no-timeline default based on `ENABLE_TIMELINE`. |
1416
| `ENABLE_TIMELINE` | `1` | `1` asks the model for a summary and event timeline. `0` asks only for a summary. |
1517
| `GENERATION_MAX_ATTEMPTS` | `5` | Number of attempts for getting valid JSON and, when enabled, a valid timeline. |
@@ -82,6 +84,8 @@ gemini-video-summarization:
8284
- host_directory/prompt_file.txt:/opt/mpf/share/prompt_file.txt:ro # optional
8385
- shared_data:/opt/mpf/share
8486
environment:
87+
- MPF_PROP_API=OpenAI
88+
- MPF_PROP_OPENAI_BASE_URL=http://vllm:8000/v1
8589
- MPF_PROP_MODEL_NAME=<MODEL NAME>
8690
- MPF_PROP_APPLICATION_CREDENTIALS=/run/secrets/openai_api_key.txt
8791
- MPF_PROP_GENERATION_PROMPT_PATH=/opt/mpf/share/prompt_file.txt # optional

python/GeminiVideoSummarization/gemini_video_summarization_component/gemini_video_summarization_component.py

Lines changed: 94 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -66,24 +66,7 @@
6666

6767
class GeminiVideoSummarizationComponent:
6868

69-
def __init__(self, model: Gemma4ForConditionalGeneration=None, processor: Gemma4Processor=None, API="OpenAI", base_url=None, device=None):
70-
self.model = model
71-
self.processor = processor
72-
self.device = device
73-
self.base_url = base_url
74-
75-
if self.model is not None:
76-
assert self.processor is not None, "If a model is provided, a tokenizer must also be provided."
77-
if self.model is not None and self.device is None:
78-
self.device = model.device
79-
80-
self.api = API
81-
self.application_credentials = ''
82-
self.project_id = ''
83-
self.bucket_name = ''
84-
self.label_prefix = ''
85-
self.label_user = ''
86-
self.label_purpose = ''
69+
def __init__(self):
8770
self._last_preprocessed_frame_timestamps = []
8871
self._last_preprocessed_fps = None
8972

@@ -98,17 +81,14 @@ def get_detections_from_video(self, job: mpf.VideoJob) -> Iterable[mpf.VideoTrac
9881
raise mpf.DetectionError.UNSUPPORTED_DATA_TYPE.exception(
9982
'Job stop frame must be >= 0.')
10083

101-
config = JobConfig(job.job_properties, job.media_properties, model=self.model is not None)
84+
has_local_model = getattr(self, "model", None) is not None
85+
config = JobConfig(
86+
job.job_properties,
87+
job.media_properties,
88+
model=has_local_model)
10289

10390
tracks = []
10491

105-
self.application_credentials = config.application_credentials
106-
self.project_id = config.project_id
107-
self.bucket_name = config.bucket_name
108-
self.label_prefix = config.label_prefix
109-
self.label_user = config.label_user
110-
self.label_purpose = config.label_purpose
111-
11292
fps = config.process_fps
11393
enable_timeline=config.enable_timeline
11494

@@ -129,8 +109,8 @@ def get_detections_from_video(self, job: mpf.VideoJob) -> Iterable[mpf.VideoTrac
129109

130110
while max(attempts.values()) < max_attempts:
131111
error= None
132-
if self.model is None: response = self._get_response(job, prompt, model_name, fps)
133-
else: response = self._local_get_response(job, prompt)
112+
if has_local_model: response = self._local_get_response(job, prompt)
113+
else: response = self._get_response(job, prompt, model_name, fps, config)
134114

135115
response = self._extract_json_object(response)
136116
response_json, error = self._check_response(attempts, max_attempts, response)
@@ -1664,13 +1644,30 @@ def _model_supports_audio_input(model_name: str) -> bool:
16641644
def _is_enabled(value) -> bool:
16651645
return str(value).strip().lower() not in ('0', 'false', 'no', 'off')
16661646

1667-
def _openai_response(self, job: mpf.VideoJob, prompt: str, model_name: str, fps: float) -> str:
1647+
@staticmethod
1648+
def _get_openai_api_key(application_credentials: str) -> str:
1649+
if application_credentials:
1650+
env_value = os.environ.get(application_credentials)
1651+
if env_value:
1652+
return env_value
1653+
if os.path.exists(application_credentials):
1654+
with open(application_credentials, 'r') as api_key_file:
1655+
api_key = api_key_file.read().strip()
1656+
if api_key:
1657+
return api_key
1658+
return "Empty"
1659+
1660+
def _openai_response(self, job: mpf.VideoJob, prompt: str, model_name: str, fps: float, config=None) -> str:
16681661
if not model_name:
16691662
raise mpf.DetectionException(
16701663
"MODEL_NAME must be provided for OpenAI API requests.",
16711664
mpf.DetectionError.INVALID_PROPERTY
16721665
)
1673-
api_key = os.environ.get(self.application_credentials, "Empty")
1666+
config = config or JobConfig(
1667+
job.job_properties,
1668+
job.media_properties,
1669+
model=getattr(self, "model", None) is not None)
1670+
api_key = self._get_openai_api_key(config.application_credentials)
16741671

16751672
preprocessed_video = None
16761673

@@ -1686,11 +1683,11 @@ def _openai_response(self, job: mpf.VideoJob, prompt: str, model_name: str, fps:
16861683
timeout_seconds = float(mpf_util.get_property(
16871684
job.job_properties, "OPENAI_REQUEST_TIMEOUT_SECONDS", "600"))
16881685
max_retries = int(mpf_util.get_property(
1689-
job.job_properties, "OPENAI_MAX_RETRIES", "2" if not self.base_url else "0"))
1686+
job.job_properties, "OPENAI_MAX_RETRIES", "2" if not config.base_url else "0"))
16901687

16911688
client = OpenAI(
16921689
api_key=api_key,
1693-
base_url=self.base_url,
1690+
base_url=config.base_url or None,
16941691
timeout=timeout_seconds,
16951692
max_retries=max_retries)
16961693

@@ -1766,7 +1763,7 @@ def _openai_response(self, job: mpf.VideoJob, prompt: str, model_name: str, fps:
17661763
}
17671764
],
17681765
}
1769-
default_json_response_format = "true" if not self.base_url else "false"
1766+
default_json_response_format = "true" if not config.base_url else "false"
17701767
use_json_response_format = mpf_util.get_property(
17711768
job.job_properties,
17721769
"OPENAI_RESPONSE_FORMAT_JSON_OBJECT",
@@ -1799,10 +1796,14 @@ def _openai_response(self, job: mpf.VideoJob, prompt: str, model_name: str, fps:
17991796
finally:
18001797
self._cleanup_preprocessed_video(preprocessed_video)
18011798

1802-
def _google_response(self, job: mpf.VideoJob, prompt: str, model_name: str, fps: float) -> str:
1799+
def _google_response(self, job: mpf.VideoJob, prompt: str, model_name: str, fps: float, config=None) -> str:
1800+
config = config or JobConfig(
1801+
job.job_properties,
1802+
job.media_properties,
1803+
model=getattr(self, "model", None) is not None)
18031804
preprocessed_video = None
18041805
try:
1805-
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = self.application_credentials
1806+
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = config.application_credentials
18061807
if not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"):
18071808
raise mpf.DetectionException(
18081809
f"Environment variable 'GOOGLE_APPLICATION_CREDENTIALS' is not set.",
@@ -1812,30 +1813,30 @@ def _google_response(self, job: mpf.VideoJob, prompt: str, model_name: str, fps:
18121813

18131814
# Video segment storage information
18141815
FILE_NAME = os.path.basename(preprocessed_video['path'])
1815-
STORAGE_PATH = self.label_user + "/" + FILE_NAME
1816+
STORAGE_PATH = config.label_user + "/" + FILE_NAME
18161817

18171818
# Uploads file to GCP bucket
1818-
client = storage.Client(project=self.project_id)
1819-
bucket = client.bucket(self.bucket_name)
1819+
client = storage.Client(project=config.project_id)
1820+
bucket = client.bucket(config.bucket_name)
18201821
blob = bucket.blob(STORAGE_PATH)
18211822
blob.upload_from_filename(preprocessed_video['path'])
18221823

1823-
file_uri = f"gs://{self.bucket_name}/{STORAGE_PATH}"
1824+
file_uri = f"gs://{config.bucket_name}/{STORAGE_PATH}"
18241825

18251826
# Generate Gemini response
18261827
genai_client = genai.Client(
1827-
project=self.project_id,
1828+
project=config.project_id,
18281829
location="global",
18291830
enterprise=True
18301831
)
18311832

18321833
content_config = None
1833-
if self.label_user and self.label_prefix and self.label_purpose:
1834+
if config.label_user and config.label_prefix and config.label_purpose:
18341835
content_config = types.GenerateContentConfig(
18351836
labels={
1836-
self.label_prefix + "user": self.label_user,
1837-
self.label_prefix + "purpose": self.label_purpose,
1838-
self.label_prefix + "modality": "video"
1837+
config.label_prefix + "user": config.label_user,
1838+
config.label_prefix + "purpose": config.label_purpose,
1839+
config.label_prefix + "modality": "video"
18391840
}
18401841
)
18411842

@@ -1873,15 +1874,19 @@ def _google_response(self, job: mpf.VideoJob, prompt: str, model_name: str, fps:
18731874
finally:
18741875
self._cleanup_preprocessed_video(preprocessed_video)
18751876

1876-
def _get_response(self, job: mpf.VideoJob, prompt: str, model_name: str, fps: float):
1877+
def _get_response(self, job: mpf.VideoJob, prompt: str, model_name: str, fps: float, config=None):
1878+
config = config or JobConfig(
1879+
job.job_properties,
1880+
job.media_properties,
1881+
model=getattr(self, "model", None) is not None)
18771882
try:
1878-
if self.api == "OpenAI":
1879-
return self._openai_response(job, prompt, model_name, fps)
1880-
elif self.api == "Google":
1881-
return self._google_response(job, prompt, model_name, fps)
1883+
if config.api == "OpenAI":
1884+
return self._openai_response(job, prompt, model_name, fps, config)
1885+
elif config.api == "Google":
1886+
return self._google_response(job, prompt, model_name, fps, config)
18821887
else:
18831888
raise mpf.DetectionException(
1884-
f"Unsupported API specified: {self.api}",
1889+
f"Unsupported API specified: {config.api}",
18851890
mpf.DetectionError.INVALID_PROPERTY
18861891
)
18871892

@@ -1899,7 +1904,7 @@ def _get_response(self, job: mpf.VideoJob, prompt: str, model_name: str, fps: fl
18991904
except Exception as e:
19001905
logger.error(f"Error in _get_response: {e}")
19011906
raise mpf.DetectionException(
1902-
f"{self.api} API call failed: {e}",
1907+
f"{config.api} API call failed: {e}",
19031908
mpf.DetectionError.DETECTION_FAILED
19041909
)
19051910

@@ -1916,7 +1921,15 @@ def _read_file(path: str) -> str:
19161921
) from e
19171922

19181923
class JobConfig:
1919-
def __init__(self, job_properties: Mapping[str, str], media_properties=None, model=False):
1924+
def __init__(
1925+
self,
1926+
job_properties: Mapping[str, str],
1927+
media_properties=None,
1928+
model=False):
1929+
self.api = self._get_prop(job_properties, "API", "OpenAI", ["OpenAI", "Google"])
1930+
self.base_url = self._get_prop(job_properties, "OPENAI_BASE_URL", "")
1931+
self.base_url = str(self.base_url).strip() or None
1932+
19201933
self.generation_prompt_path = self._get_prop(job_properties, "GENERATION_PROMPT_PATH", "")
19211934
self.enable_timeline = int(self._get_prop(job_properties, "ENABLE_TIMELINE", "1"))
19221935

@@ -1925,18 +1938,29 @@ def __init__(self, job_properties: Mapping[str, str], media_properties=None, mod
19251938
elif self.generation_prompt_path == "" and self.enable_timeline == 0:
19261939
self.generation_prompt_path= os.path.join(os.path.dirname(__file__), 'data', 'default_prompt_no_tl.txt')
19271940

1928-
if not os.path.exists(self.generation_prompt_path):
1941+
self.generation_prompt_path = self._resolve_existing_path(self.generation_prompt_path)
1942+
if not self.generation_prompt_path:
19291943
raise mpf.DetectionException(
19301944
"Invalid path provided for prompt file: ",
19311945
mpf.DetectionError.COULD_NOT_OPEN_DATAFILE
19321946
)
19331947

19341948
self.application_credentials = self._get_prop(job_properties, "APPLICATION_CREDENTIALS", "")
1935-
if not os.path.exists(self.application_credentials) and not model:
1936-
raise mpf.DetectionException(
1937-
"Invalid path provided for GCP credential file: ",
1938-
mpf.DetectionError.COULD_NOT_OPEN_DATAFILE
1939-
)
1949+
if self.api == "Google":
1950+
self.application_credentials = self._resolve_existing_path(self.application_credentials)
1951+
if not self.application_credentials and not model:
1952+
raise mpf.DetectionException(
1953+
"Invalid path provided for GCP credential file: ",
1954+
mpf.DetectionError.COULD_NOT_OPEN_DATAFILE
1955+
)
1956+
if (
1957+
self.api == "OpenAI"
1958+
and self.application_credentials
1959+
and not os.path.exists(self.application_credentials)
1960+
and not os.environ.get(self.application_credentials)):
1961+
logger.warning(
1962+
"APPLICATION_CREDENTIALS did not match an environment variable or file path; "
1963+
"using the OpenAI client dummy key fallback.")
19401964

19411965
self.model_name = self._get_prop(job_properties, "MODEL_NAME", "")
19421966
self.project_id = self._get_prop(job_properties, "PROJECT_ID", "")
@@ -1948,6 +1972,18 @@ def __init__(self, job_properties: Mapping[str, str], media_properties=None, mod
19481972
self.timeline_check_target_threshold = self._get_prop(job_properties, "TIMELINE_CHECK_TARGET_THRESHOLD", "10")
19491973
self.process_fps = self._get_prop(job_properties, "PROCESS_FPS", 1.0)
19501974

1975+
@staticmethod
1976+
def _resolve_existing_path(path: str) -> str:
1977+
if not path:
1978+
return ""
1979+
if os.path.exists(path):
1980+
return path
1981+
if not os.path.isabs(path):
1982+
component_relative_path = os.path.join(os.path.dirname(__file__), path)
1983+
if os.path.exists(component_relative_path):
1984+
return component_relative_path
1985+
return ""
1986+
19511987
@staticmethod
19521988
def _get_prop(job_properties, key, default_value, accept_values=[]):
19531989
prop = mpf_util.get_property(job_properties, key, default_value)

python/GeminiVideoSummarization/plugin-files/descriptor/descriptor.json

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,31 @@
2424
"properties": [
2525
{
2626
"name": "GENERATION_PROMPT_PATH",
27-
"description": "Path to a custom a file which contains the prompt that Gemini will use on the video.",
27+
"description": "Path to a custom prompt file.",
28+
"type": "STRING",
29+
"defaultValue": ""
30+
},
31+
{
32+
"name": "API",
33+
"description": "Backend API to use for model inference. OpenAI uses an OpenAI-compatible chat completion endpoint. Google uses Vertex AI/Gemini.",
34+
"type": "STRING",
35+
"defaultValue": "OpenAI"
36+
},
37+
{
38+
"name": "OPENAI_BASE_URL",
39+
"description": "Optional base URL for an OpenAI-compatible API service such as vLLM. Leave empty to use the OpenAI client default.",
2840
"type": "STRING",
2941
"defaultValue": ""
3042
},
3143
{
3244
"name": "MODEL_NAME",
33-
"description": "The model to use for Gemini inference. Examples: 'gemma-4-12b', 'gemma-4-31b'.",
45+
"description": "The model to use for inference.",
3446
"type": "STRING",
3547
"defaultValue": "gemma-4-12b"
3648
},
3749
{
38-
"name": "GOOGLE_APPLICATION_CREDENTIALS",
39-
"description": "Path to a JSON file containing your GCP credentials for Vertex AI.",
50+
"name": "APPLICATION_CREDENTIALS",
51+
"description": "Path to a credential file or the name of an environment variable containing credentials.",
4052
"type": "STRING",
4153
"defaultValue": ""
4254
},

0 commit comments

Comments
 (0)