Cleanup jobs

This commit is contained in:
Nicolas Mowen 2026-03-25 09:14:06 -06:00
parent 7e41e1ecef
commit 34d732383c
5 changed files with 67 additions and 35 deletions

View File

@ -113,7 +113,11 @@ class GeminiClient(GenAIClient):
# Map roles to Gemini format # Map roles to Gemini format
if role == "system": if role == "system":
# Gemini doesn't have system role, prepend to first user message # 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[ gemini_messages[0].parts[
0 0
].text = f"{content}\n\n{gemini_messages[0].parts[0].text}" ].text = f"{content}\n\n{gemini_messages[0].parts[0].text}"
@ -174,15 +178,21 @@ class GeminiClient(GenAIClient):
if tool_choice: if tool_choice:
if tool_choice == "none": if tool_choice == "none":
tool_config = types.ToolConfig( tool_config = types.ToolConfig(
function_calling_config=types.FunctionCallingConfig(mode=FunctionCallingConfigMode.NONE) function_calling_config=types.FunctionCallingConfig(
mode=FunctionCallingConfigMode.NONE
)
) )
elif tool_choice == "auto": elif tool_choice == "auto":
tool_config = types.ToolConfig( tool_config = types.ToolConfig(
function_calling_config=types.FunctionCallingConfig(mode=FunctionCallingConfigMode.AUTO) function_calling_config=types.FunctionCallingConfig(
mode=FunctionCallingConfigMode.AUTO
)
) )
elif tool_choice == "required": elif tool_choice == "required":
tool_config = types.ToolConfig( tool_config = types.ToolConfig(
function_calling_config=types.FunctionCallingConfig(mode=FunctionCallingConfigMode.ANY) function_calling_config=types.FunctionCallingConfig(
mode=FunctionCallingConfigMode.ANY
)
) )
# Build request config # Build request config
@ -310,7 +320,11 @@ class GeminiClient(GenAIClient):
# Map roles to Gemini format # Map roles to Gemini format
if role == "system": if role == "system":
# Gemini doesn't have system role, prepend to first user message # 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[ gemini_messages[0].parts[
0 0
].text = f"{content}\n\n{gemini_messages[0].parts[0].text}" ].text = f"{content}\n\n{gemini_messages[0].parts[0].text}"
@ -371,15 +385,21 @@ class GeminiClient(GenAIClient):
if tool_choice: if tool_choice:
if tool_choice == "none": if tool_choice == "none":
tool_config = types.ToolConfig( tool_config = types.ToolConfig(
function_calling_config=types.FunctionCallingConfig(mode=FunctionCallingConfigMode.NONE) function_calling_config=types.FunctionCallingConfig(
mode=FunctionCallingConfigMode.NONE
)
) )
elif tool_choice == "auto": elif tool_choice == "auto":
tool_config = types.ToolConfig( tool_config = types.ToolConfig(
function_calling_config=types.FunctionCallingConfig(mode=FunctionCallingConfigMode.AUTO) function_calling_config=types.FunctionCallingConfig(
mode=FunctionCallingConfigMode.AUTO
)
) )
elif tool_choice == "required": elif tool_choice == "required":
tool_config = types.ToolConfig( tool_config = types.ToolConfig(
function_calling_config=types.FunctionCallingConfig(mode=FunctionCallingConfigMode.ANY) function_calling_config=types.FunctionCallingConfig(
mode=FunctionCallingConfigMode.ANY
)
) )
# Build request config # Build request config

View File

@ -5,7 +5,7 @@ import os
import threading import threading
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional, cast
from frigate.comms.inter_process import InterProcessRequestor from frigate.comms.inter_process import InterProcessRequestor
from frigate.const import CONFIG_DIR, UPDATE_JOB_STATE from frigate.const import CONFIG_DIR, UPDATE_JOB_STATE
@ -122,7 +122,7 @@ def start_media_sync_job(
if job_is_running("media_sync"): if job_is_running("media_sync"):
current = get_current_job("media_sync") current = get_current_job("media_sync")
logger.warning( 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 return None
@ -146,9 +146,9 @@ def start_media_sync_job(
def get_current_media_sync_job() -> Optional[MediaSyncJob]: def get_current_media_sync_job() -> Optional[MediaSyncJob]:
"""Get the current running/queued media sync job, if any.""" """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]: def get_media_sync_job_by_id(job_id: str) -> Optional[MediaSyncJob]:
"""Get media sync job by ID. Currently only tracks the current job.""" """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))

View File

@ -6,7 +6,7 @@ import threading
from concurrent.futures import Future, ThreadPoolExecutor, as_completed from concurrent.futures import Future, ThreadPoolExecutor, as_completed
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from datetime import datetime from datetime import datetime
from typing import Any, Optional from typing import Any, Optional, cast
import cv2 import cv2
import numpy as np import numpy as np
@ -96,7 +96,7 @@ def create_polygon_mask(
dtype=np.int32, dtype=np.int32,
) )
mask = np.zeros((frame_height, frame_width), dtype=np.uint8) mask = np.zeros((frame_height, frame_width), dtype=np.uint8)
cv2.fillPoly(mask, [motion_points], 255) cv2.fillPoly(mask, [motion_points], (255,))
return mask return mask
@ -116,7 +116,7 @@ def compute_roi_bbox_normalized(
def heatmap_overlaps_roi( 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: ) -> bool:
"""Check if a sparse motion heatmap has any overlap with the ROI bounding box. """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 any of motion, objects, or regions is non-zero/non-null.
Returns True if all are null (old segments without data). Returns True if all are null (old segments without data).
""" """
motion = recording.motion motion: Any = recording.motion
objects = recording.objects objects: Any = recording.objects
regions = recording.regions regions: Any = recording.regions
# Old segments without metadata - pass through (conservative) # Old segments without metadata - pass through (conservative)
if motion is None and objects is None and regions is None: 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_width = camera_config.detect.width
frame_height = camera_config.detect.height 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 # Create polygon mask
polygon_mask = create_polygon_mask( polygon_mask = create_polygon_mask(
self.job.polygon_points, frame_width, frame_height self.job.polygon_points, frame_width, frame_height
@ -415,11 +418,13 @@ class MotionSearchRunner(threading.Thread):
if self._should_stop(): if self._should_stop():
break break
rec_start: float = recording.start_time # type: ignore[assignment]
rec_end: float = recording.end_time # type: ignore[assignment]
future = executor.submit( future = executor.submit(
self._process_recording_for_motion, self._process_recording_for_motion,
recording.path, str(recording.path),
recording.start_time, rec_start,
recording.end_time, rec_end,
self.job.start_time_range, self.job.start_time_range,
self.job.end_time_range, self.job.end_time_range,
polygon_mask, polygon_mask,
@ -524,10 +529,12 @@ class MotionSearchRunner(threading.Thread):
break break
try: 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( results, frames = self._process_recording_for_motion(
recording.path, str(recording.path),
recording.start_time, rec_start,
recording.end_time, rec_end,
self.job.start_time_range, self.job.start_time_range,
self.job.end_time_range, self.job.end_time_range,
polygon_mask, polygon_mask,
@ -672,7 +679,9 @@ class MotionSearchRunner(threading.Thread):
# Handle frame dimension changes # Handle frame dimension changes
if gray.shape != polygon_mask.shape: if gray.shape != polygon_mask.shape:
resized_mask = cv2.resize( 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) current_bbox = cv2.boundingRect(resized_mask)
else: else:
@ -698,7 +707,7 @@ class MotionSearchRunner(threading.Thread):
) )
if prev_frame_gray is not None: 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) diff_blurred = cv2.GaussianBlur(diff, (3, 3), 0)
_, thresh = cv2.threshold( _, thresh = cv2.threshold(
diff_blurred, threshold, 255, cv2.THRESH_BINARY diff_blurred, threshold, 255, cv2.THRESH_BINARY
@ -825,7 +834,7 @@ def get_motion_search_job(job_id: str) -> Optional[MotionSearchJob]:
if job_entry: if job_entry:
return job_entry[0] return job_entry[0]
# Check completed jobs via manager # 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: def cancel_motion_search_job(job_id: str) -> bool:

View File

@ -54,9 +54,9 @@ class VLMWatchRunner(threading.Thread):
job: VLMWatchJob, job: VLMWatchJob,
config: FrigateConfig, config: FrigateConfig,
cancel_event: threading.Event, cancel_event: threading.Event,
frame_processor, frame_processor: Any,
genai_manager, genai_manager: Any,
dispatcher, dispatcher: Any,
) -> None: ) -> None:
super().__init__(daemon=True, name=f"vlm_watch_{job.id}") super().__init__(daemon=True, name=f"vlm_watch_{job.id}")
self.job = job self.job = job
@ -226,9 +226,12 @@ class VLMWatchRunner(threading.Thread):
remaining = deadline - time.time() remaining = deadline - time.time()
if remaining <= 0: if remaining <= 0:
break break
topic, payload = self.detection_subscriber.check_for_update( result = self.detection_subscriber.check_for_update(
timeout=min(1.0, remaining) timeout=min(1.0, remaining)
) )
if result is None:
continue
topic, payload = result
if topic is None or payload is None: if topic is None or payload is None:
continue continue
# payload = (camera, frame_name, frame_time, tracked_objects, motion_boxes, regions) # payload = (camera, frame_name, frame_time, tracked_objects, motion_boxes, regions)
@ -328,9 +331,9 @@ def start_vlm_watch_job(
condition: str, condition: str,
max_duration_minutes: int, max_duration_minutes: int,
config: FrigateConfig, config: FrigateConfig,
frame_processor, frame_processor: Any,
genai_manager, genai_manager: Any,
dispatcher, dispatcher: Any,
labels: list[str] | None = None, labels: list[str] | None = None,
zones: list[str] | None = None, zones: list[str] | None = None,
) -> str: ) -> str:

View File

@ -44,7 +44,7 @@ ignore_errors = false
[mypy-frigate.genai.*] [mypy-frigate.genai.*]
ignore_errors = false ignore_errors = false
[mypy-frigate.jobs] [mypy-frigate.jobs.*]
ignore_errors = false ignore_errors = false
[mypy-frigate.motion] [mypy-frigate.motion]