From 34d732383c365ab06eac6b5044cb63530957bd16 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 25 Mar 2026 09:14:06 -0600 Subject: [PATCH] Cleanup jobs --- frigate/genai/gemini.py | 36 +++++++++++++++++++++++++------- frigate/jobs/media_sync.py | 8 +++---- frigate/jobs/motion_search.py | 39 +++++++++++++++++++++-------------- frigate/jobs/vlm_watch.py | 17 ++++++++------- frigate/mypy.ini | 2 +- 5 files changed, 67 insertions(+), 35 deletions(-) diff --git a/frigate/genai/gemini.py b/frigate/genai/gemini.py index b031b5263..e0c0c7698 100644 --- a/frigate/genai/gemini.py +++ b/frigate/genai/gemini.py @@ -113,7 +113,11 @@ class GeminiClient(GenAIClient): # Map roles to Gemini format if role == "system": # Gemini doesn't have system role, prepend to first user message - if gemini_messages and gemini_messages[0].role == "user" and gemini_messages[0].parts: + if ( + gemini_messages + and gemini_messages[0].role == "user" + and gemini_messages[0].parts + ): gemini_messages[0].parts[ 0 ].text = f"{content}\n\n{gemini_messages[0].parts[0].text}" @@ -174,15 +178,21 @@ class GeminiClient(GenAIClient): if tool_choice: if tool_choice == "none": tool_config = types.ToolConfig( - function_calling_config=types.FunctionCallingConfig(mode=FunctionCallingConfigMode.NONE) + function_calling_config=types.FunctionCallingConfig( + mode=FunctionCallingConfigMode.NONE + ) ) elif tool_choice == "auto": tool_config = types.ToolConfig( - function_calling_config=types.FunctionCallingConfig(mode=FunctionCallingConfigMode.AUTO) + function_calling_config=types.FunctionCallingConfig( + mode=FunctionCallingConfigMode.AUTO + ) ) elif tool_choice == "required": tool_config = types.ToolConfig( - function_calling_config=types.FunctionCallingConfig(mode=FunctionCallingConfigMode.ANY) + function_calling_config=types.FunctionCallingConfig( + mode=FunctionCallingConfigMode.ANY + ) ) # Build request config @@ -310,7 +320,11 @@ class GeminiClient(GenAIClient): # Map roles to Gemini format if role == "system": # Gemini doesn't have system role, prepend to first user message - if gemini_messages and gemini_messages[0].role == "user" and gemini_messages[0].parts: + if ( + gemini_messages + and gemini_messages[0].role == "user" + and gemini_messages[0].parts + ): gemini_messages[0].parts[ 0 ].text = f"{content}\n\n{gemini_messages[0].parts[0].text}" @@ -371,15 +385,21 @@ class GeminiClient(GenAIClient): if tool_choice: if tool_choice == "none": tool_config = types.ToolConfig( - function_calling_config=types.FunctionCallingConfig(mode=FunctionCallingConfigMode.NONE) + function_calling_config=types.FunctionCallingConfig( + mode=FunctionCallingConfigMode.NONE + ) ) elif tool_choice == "auto": tool_config = types.ToolConfig( - function_calling_config=types.FunctionCallingConfig(mode=FunctionCallingConfigMode.AUTO) + function_calling_config=types.FunctionCallingConfig( + mode=FunctionCallingConfigMode.AUTO + ) ) elif tool_choice == "required": tool_config = types.ToolConfig( - function_calling_config=types.FunctionCallingConfig(mode=FunctionCallingConfigMode.ANY) + function_calling_config=types.FunctionCallingConfig( + mode=FunctionCallingConfigMode.ANY + ) ) # Build request config diff --git a/frigate/jobs/media_sync.py b/frigate/jobs/media_sync.py index 803a80a9d..4a3fdc355 100644 --- a/frigate/jobs/media_sync.py +++ b/frigate/jobs/media_sync.py @@ -5,7 +5,7 @@ import os import threading from dataclasses import dataclass, field from datetime import datetime -from typing import Optional +from typing import Optional, cast from frigate.comms.inter_process import InterProcessRequestor from frigate.const import CONFIG_DIR, UPDATE_JOB_STATE @@ -122,7 +122,7 @@ def start_media_sync_job( if job_is_running("media_sync"): current = get_current_job("media_sync") logger.warning( - f"Media sync job {current.id} is already running. Rejecting new request." + f"Media sync job {current.id if current else 'unknown'} is already running. Rejecting new request." ) return None @@ -146,9 +146,9 @@ def start_media_sync_job( def get_current_media_sync_job() -> Optional[MediaSyncJob]: """Get the current running/queued media sync job, if any.""" - return get_current_job("media_sync") + return cast(Optional[MediaSyncJob], get_current_job("media_sync")) def get_media_sync_job_by_id(job_id: str) -> Optional[MediaSyncJob]: """Get media sync job by ID. Currently only tracks the current job.""" - return get_job_by_id("media_sync", job_id) + return cast(Optional[MediaSyncJob], get_job_by_id("media_sync", job_id)) diff --git a/frigate/jobs/motion_search.py b/frigate/jobs/motion_search.py index d7c8f8fbc..1a90f0bb9 100644 --- a/frigate/jobs/motion_search.py +++ b/frigate/jobs/motion_search.py @@ -6,7 +6,7 @@ import threading from concurrent.futures import Future, ThreadPoolExecutor, as_completed from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Any, Optional +from typing import Any, Optional, cast import cv2 import numpy as np @@ -96,7 +96,7 @@ def create_polygon_mask( dtype=np.int32, ) mask = np.zeros((frame_height, frame_width), dtype=np.uint8) - cv2.fillPoly(mask, [motion_points], 255) + cv2.fillPoly(mask, [motion_points], (255,)) return mask @@ -116,7 +116,7 @@ def compute_roi_bbox_normalized( def heatmap_overlaps_roi( - heatmap: dict[str, int], roi_bbox: tuple[float, float, float, float] + heatmap: object, roi_bbox: tuple[float, float, float, float] ) -> bool: """Check if a sparse motion heatmap has any overlap with the ROI bounding box. @@ -155,9 +155,9 @@ def segment_passes_activity_gate(recording: Recordings) -> bool: Returns True if any of motion, objects, or regions is non-zero/non-null. Returns True if all are null (old segments without data). """ - motion = recording.motion - objects = recording.objects - regions = recording.regions + motion: Any = recording.motion + objects: Any = recording.objects + regions: Any = recording.regions # Old segments without metadata - pass through (conservative) if motion is None and objects is None and regions is None: @@ -278,6 +278,9 @@ class MotionSearchRunner(threading.Thread): frame_width = camera_config.detect.width frame_height = camera_config.detect.height + if frame_width is None or frame_height is None: + raise ValueError(f"Camera {camera_name} detect dimensions not configured") + # Create polygon mask polygon_mask = create_polygon_mask( self.job.polygon_points, frame_width, frame_height @@ -415,11 +418,13 @@ class MotionSearchRunner(threading.Thread): if self._should_stop(): break + rec_start: float = recording.start_time # type: ignore[assignment] + rec_end: float = recording.end_time # type: ignore[assignment] future = executor.submit( self._process_recording_for_motion, - recording.path, - recording.start_time, - recording.end_time, + str(recording.path), + rec_start, + rec_end, self.job.start_time_range, self.job.end_time_range, polygon_mask, @@ -524,10 +529,12 @@ class MotionSearchRunner(threading.Thread): break try: + rec_start: float = recording.start_time # type: ignore[assignment] + rec_end: float = recording.end_time # type: ignore[assignment] results, frames = self._process_recording_for_motion( - recording.path, - recording.start_time, - recording.end_time, + str(recording.path), + rec_start, + rec_end, self.job.start_time_range, self.job.end_time_range, polygon_mask, @@ -672,7 +679,9 @@ class MotionSearchRunner(threading.Thread): # Handle frame dimension changes if gray.shape != polygon_mask.shape: resized_mask = cv2.resize( - polygon_mask, (gray.shape[1], gray.shape[0]), cv2.INTER_NEAREST + polygon_mask, + (gray.shape[1], gray.shape[0]), + interpolation=cv2.INTER_NEAREST, ) current_bbox = cv2.boundingRect(resized_mask) else: @@ -698,7 +707,7 @@ class MotionSearchRunner(threading.Thread): ) if prev_frame_gray is not None: - diff = cv2.absdiff(prev_frame_gray, masked_gray) + diff = cv2.absdiff(prev_frame_gray, masked_gray) # type: ignore[unreachable] diff_blurred = cv2.GaussianBlur(diff, (3, 3), 0) _, thresh = cv2.threshold( diff_blurred, threshold, 255, cv2.THRESH_BINARY @@ -825,7 +834,7 @@ def get_motion_search_job(job_id: str) -> Optional[MotionSearchJob]: if job_entry: return job_entry[0] # Check completed jobs via manager - return get_job_by_id("motion_search", job_id) + return cast(Optional[MotionSearchJob], get_job_by_id("motion_search", job_id)) def cancel_motion_search_job(job_id: str) -> bool: diff --git a/frigate/jobs/vlm_watch.py b/frigate/jobs/vlm_watch.py index dae5e5f41..a66f60dfc 100644 --- a/frigate/jobs/vlm_watch.py +++ b/frigate/jobs/vlm_watch.py @@ -54,9 +54,9 @@ class VLMWatchRunner(threading.Thread): job: VLMWatchJob, config: FrigateConfig, cancel_event: threading.Event, - frame_processor, - genai_manager, - dispatcher, + frame_processor: Any, + genai_manager: Any, + dispatcher: Any, ) -> None: super().__init__(daemon=True, name=f"vlm_watch_{job.id}") self.job = job @@ -226,9 +226,12 @@ class VLMWatchRunner(threading.Thread): remaining = deadline - time.time() if remaining <= 0: break - topic, payload = self.detection_subscriber.check_for_update( + result = self.detection_subscriber.check_for_update( timeout=min(1.0, remaining) ) + if result is None: + continue + topic, payload = result if topic is None or payload is None: continue # payload = (camera, frame_name, frame_time, tracked_objects, motion_boxes, regions) @@ -328,9 +331,9 @@ def start_vlm_watch_job( condition: str, max_duration_minutes: int, config: FrigateConfig, - frame_processor, - genai_manager, - dispatcher, + frame_processor: Any, + genai_manager: Any, + dispatcher: Any, labels: list[str] | None = None, zones: list[str] | None = None, ) -> str: diff --git a/frigate/mypy.ini b/frigate/mypy.ini index 3bec4d439..e1da675be 100644 --- a/frigate/mypy.ini +++ b/frigate/mypy.ini @@ -44,7 +44,7 @@ ignore_errors = false [mypy-frigate.genai.*] ignore_errors = false -[mypy-frigate.jobs] +[mypy-frigate.jobs.*] ignore_errors = false [mypy-frigate.motion]