diff --git a/frigate/embeddings/__init__.py b/frigate/embeddings/__init__.py index 0a854fcfa..5e14d0d8c 100644 --- a/frigate/embeddings/__init__.py +++ b/frigate/embeddings/__init__.py @@ -205,14 +205,14 @@ class EmbeddingsContext: ) def get_face_ids(self, name: str) -> list[str]: - sql_query = f""" + sql_query = """ SELECT id FROM vec_descriptions - WHERE id LIKE '%{name}%' + WHERE id LIKE ? """ - return self.db.execute_sql(sql_query).fetchall() + return self.db.execute_sql(sql_query, (f"%{name}%",)).fetchall() def reprocess_face(self, face_file: str) -> dict[str, Any]: return self.requestor.send_data( diff --git a/frigate/embeddings/embeddings.py b/frigate/embeddings/embeddings.py index d31d1b058..91144c3fa 100644 --- a/frigate/embeddings/embeddings.py +++ b/frigate/embeddings/embeddings.py @@ -266,7 +266,7 @@ class Embeddings: ) duration = datetime.datetime.now().timestamp() - start - self.text_inference_speed.update(duration / len(valid_ids)) + self.image_inference_speed.update(duration / len(valid_ids)) return embeddings diff --git a/frigate/events/cleanup.py b/frigate/events/cleanup.py index 1ac03b2ed..1b9e1a673 100644 --- a/frigate/events/cleanup.py +++ b/frigate/events/cleanup.py @@ -95,7 +95,8 @@ class EventCleanup(threading.Thread): .namedtuples() .iterator() ) - logger.debug(f"{len(list(expired_events))} events can be expired") + expired_events = list(expired_events) + logger.debug(f"{len(expired_events)} events can be expired") # delete the media from disk for expired in expired_events: @@ -220,7 +221,8 @@ class EventCleanup(threading.Thread): .namedtuples() .iterator() ) - logger.debug(f"{len(list(expired_events))} events can be expired") + expired_events = list(expired_events) + logger.debug(f"{len(expired_events)} events can be expired") # delete the media from disk for expired in expired_events: media_name = f"{expired.camera}-{expired.id}" diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index 43272a6d1..ec40bf0c5 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -63,7 +63,7 @@ class LibvaGpuSelector: if not self._valid_gpus: return "" - if gpu <= len(self._valid_gpus): + if gpu < len(self._valid_gpus): return self._valid_gpus[gpu] else: logger.warning(f"Invalid GPU index {gpu}, using first valid GPU") @@ -278,7 +278,7 @@ def parse_preset_hardware_acceleration_encode( arg_map = PRESETS_HW_ACCEL_ENCODE_TIMELAPSE if not isinstance(arg, str): - return arg_map["default"].format(input, output) + return arg_map["default"].format(ffmpeg_path, input, output) # Not all jetsons have HW encoders, so fall back to default SW encoder if not if arg.startswith("preset-jetson-") and not os.path.exists("/dev/nvhost-msenc"): @@ -436,7 +436,7 @@ def parse_preset_input(arg: Any, detect_fps: int) -> list[str]: if arg == "preset-http-jpeg-generic": input = PRESETS_INPUT[arg].copy() - input[len(_user_agent_args) + 1] = str(detect_fps) + input[1] = str(detect_fps) return input return PRESETS_INPUT.get(arg, None) diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index 272420dab..3e939d28d 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -153,6 +153,9 @@ Each line represents a detection state, not necessarily unique individuals. Pare if "other_concerns" in schema.get("required", []): schema["required"].remove("other_concerns") + # OpenAI strict mode requires additionalProperties: false on all objects + schema["additionalProperties"] = False + response_format = { "type": "json_schema", "json_schema": { diff --git a/frigate/genai/llama_cpp.py b/frigate/genai/llama_cpp.py index 87443ac4f..ac698b3b6 100644 --- a/frigate/genai/llama_cpp.py +++ b/frigate/genai/llama_cpp.py @@ -36,19 +36,12 @@ def _to_jpeg(img_bytes: bytes) -> bytes | None: class LlamaCppClient(GenAIClient): """Generative AI client for Frigate using llama.cpp server.""" - LOCAL_OPTIMIZED_OPTIONS = { - "temperature": 0.7, - "repeat_penalty": 1.05, - "top_p": 0.8, - } - provider: str # base_url provider_options: dict[str, Any] def _init_provider(self): """Initialize the client.""" self.provider_options = { - **self.LOCAL_OPTIMIZED_OPTIONS, **self.genai_config.provider_options, } return ( diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index cdadafe71..5d80de33c 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -303,12 +303,20 @@ class BirdsEyeFrameManager: birdseye_logo = cv2.imread(logo_files[0], cv2.IMREAD_UNCHANGED) if birdseye_logo is not None: - transparent_layer = birdseye_logo[:, :, 3] + if birdseye_logo.ndim == 2: + # Grayscale image (no channels) — use directly as luminance + transparent_layer = birdseye_logo + elif birdseye_logo.shape[2] >= 4: + # RGBA — use alpha channel as luminance + transparent_layer = birdseye_logo[:, :, 3] + else: + # RGB or other format without alpha — convert to grayscale + transparent_layer = cv2.cvtColor(birdseye_logo, cv2.COLOR_BGR2GRAY) y_offset = height // 2 - transparent_layer.shape[0] // 2 x_offset = width // 2 - transparent_layer.shape[1] // 2 self.blank_frame[ - y_offset : y_offset + transparent_layer.shape[1], - x_offset : x_offset + transparent_layer.shape[0], + y_offset : y_offset + transparent_layer.shape[0], + x_offset : x_offset + transparent_layer.shape[1], ] = transparent_layer else: logger.warning("Unable to read Frigate logo") @@ -753,7 +761,7 @@ class BirdsEyeFrameManager: frame_changed, layout_changed = self.update_frame(frame) except Exception: frame_changed, layout_changed = False, False - self.active_cameras = [] + self.active_cameras = set() self.camera_layout = [] print(traceback.format_exc()) diff --git a/frigate/plus.py b/frigate/plus.py index 197b6e48d..2870d2ae5 100644 --- a/frigate/plus.py +++ b/frigate/plus.py @@ -105,9 +105,9 @@ class PlusApi: def upload_image(self, image: ndarray, camera: str) -> str: r = self._get("image/signed_urls") - presigned_urls = r.json() if not r.ok: raise Exception("Unable to get signed urls") + presigned_urls = r.json() # resize and submit original files = {"file": get_jpg_bytes(image, 1920, 85)} diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index eb2d16940..1a45f619c 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -901,7 +901,7 @@ class PtzAutoTracker: # Check direction difference velocities = np.round(velocities) invalid_dirs = False - if not np.any(np.linalg.norm(velocities, axis=1)): + if np.all(np.linalg.norm(velocities, axis=1)): cosine_sim = np.dot(velocities[0], velocities[1]) / ( np.linalg.norm(velocities[0]) * np.linalg.norm(velocities[1]) ) @@ -1067,7 +1067,7 @@ class PtzAutoTracker: f"{camera}: Zoom test: below dimension threshold: {below_dimension_threshold} width: {bb_right - bb_left}, max width: {camera_width * (self.zoom_factor[camera] + 0.1)}, height: {bb_bottom - bb_top}, max height: {camera_height * (self.zoom_factor[camera] + 0.1)}" ) logger.debug( - f"{camera}: Zoom test: below velocity threshold: {below_velocity_threshold} velocity x: {abs(average_velocity[0])}, x threshold: {velocity_threshold_x}, velocity y: {abs(average_velocity[0])}, y threshold: {velocity_threshold_y}" + f"{camera}: Zoom test: below velocity threshold: {below_velocity_threshold} velocity x: {abs(average_velocity[0])}, x threshold: {velocity_threshold_x}, velocity y: {abs(average_velocity[1])}, y threshold: {velocity_threshold_y}" ) logger.debug(f"{camera}: Zoom test: at max zoom: {at_max_zoom}") logger.debug(f"{camera}: Zoom test: at min zoom: {at_min_zoom}") diff --git a/frigate/util/builtin.py b/frigate/util/builtin.py index aa2417a5c..42aa18c0a 100644 --- a/frigate/util/builtin.py +++ b/frigate/util/builtin.py @@ -116,7 +116,7 @@ def clean_camera_user_pass(line: str) -> str: def escape_special_characters(path: str) -> str: """Cleans reserved characters to encodings for ffmpeg.""" if len(path) > 1000: - return ValueError("Input too long to check") + raise ValueError("Input too long to check") try: found = re.search(REGEX_RTSP_CAMERA_USER_PASS, path).group(0)[3:-1] diff --git a/web/src/components/player/WebRTCPlayer.tsx b/web/src/components/player/WebRTCPlayer.tsx index 147af43ea..d78c7dd80 100644 --- a/web/src/components/player/WebRTCPlayer.tsx +++ b/web/src/components/player/WebRTCPlayer.tsx @@ -52,6 +52,7 @@ export default function WebRtcPlayer({ // camera states const pcRef = useRef(undefined); + const wsRef = useRef(null); const videoRef = useRef(null); const [bufferTimeout, setBufferTimeout] = useState(); const videoLoadTimeoutRef = useRef(undefined); @@ -129,7 +130,8 @@ export default function WebRtcPlayer({ } pcRef.current = await aPc; - const ws = new WebSocket(wsURL); + wsRef.current = new WebSocket(wsURL); + const ws = wsRef.current; ws.addEventListener("open", () => { pcRef.current?.addEventListener("icecandidate", (ev) => { @@ -183,6 +185,10 @@ export default function WebRtcPlayer({ connect(aPc); return () => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } if (pcRef.current) { pcRef.current.close(); pcRef.current = undefined; diff --git a/web/src/hooks/use-camera-activity.ts b/web/src/hooks/use-camera-activity.ts index 71507c2af..76a982725 100644 --- a/web/src/hooks/use-camera-activity.ts +++ b/web/src/hooks/use-camera-activity.ts @@ -125,8 +125,6 @@ export function useCameraActivity( newObjects = [...(objects ?? []), newActiveObject]; } } else { - const newObjects = [...(objects ?? [])]; - let label = updatedEvent.after.label; if (updatedEvent.after.sub_label) { diff --git a/web/src/hooks/use-date-locale.ts b/web/src/hooks/use-date-locale.ts index 7a8085666..901162fe4 100644 --- a/web/src/hooks/use-date-locale.ts +++ b/web/src/hooks/use-date-locale.ts @@ -38,6 +38,7 @@ const localeMap: Record Promise> = { th: () => import("date-fns/locale/th").then((module) => module.th), ca: () => import("date-fns/locale/ca").then((module) => module.ca), hr: () => import("date-fns/locale/hr").then((module) => module.hr), + sl: () => import("date-fns/locale/sl").then((module) => module.sl), }; export function useDateLocale(): Locale { diff --git a/web/src/hooks/use-stats.ts b/web/src/hooks/use-stats.ts index 5bddb75ac..bfb3bafd8 100644 --- a/web/src/hooks/use-stats.ts +++ b/web/src/hooks/use-stats.ts @@ -106,13 +106,11 @@ export default function useStats(stats: FrigateStats | undefined) { const cameraName = config?.cameras?.[name]?.friendly_name ?? name; - // Skip ffmpeg warnings for replay cameras when debug replay is active + // Skip ffmpeg warnings for replay cameras if ( !isNaN(ffmpegAvg) && ffmpegAvg >= CameraFfmpegThreshold.error && - !( - debugReplayStatus?.active && debugReplayStatus?.replay_camera === name - ) + !isReplayCamera(name) ) { problems.push({ text: t("stats.ffmpegHighCpuUsage", { diff --git a/web/src/lib/const.ts b/web/src/lib/const.ts index 5000d7a0b..a11239613 100644 --- a/web/src/lib/const.ts +++ b/web/src/lib/const.ts @@ -26,6 +26,7 @@ export const supportedLanguageKeys = [ "pl", "hr", "sk", + "sl", "lt", "uk", "cs", diff --git a/web/src/pages/Replay.tsx b/web/src/pages/Replay.tsx index 187a9a76b..50927d1dd 100644 --- a/web/src/pages/Replay.tsx +++ b/web/src/pages/Replay.tsx @@ -381,7 +381,7 @@ export default function Replay() { {/* Side panel */} -
+
{t("title")} @@ -399,7 +399,10 @@ export default function Replay() {

{t("description")}

- + {t("debug.debugging", { ns: "views/settings" })} @@ -409,7 +412,10 @@ export default function Replay() { {t("websocket_messages")} - +
{DEBUG_OPTION_KEYS.map((key) => { @@ -554,7 +560,10 @@ export default function Replay() {
- + | null>(null); + + useEffect(() => { + return () => { + if (compatIntervalRef.current) { + clearInterval(compatIntervalRef.current); + } + }; + }, []); + const resetPlayback = useCallback(() => { if (!videoRef.current || !preview) { return; } + if (compatIntervalRef.current) { + clearInterval(compatIntervalRef.current); + compatIntervalRef.current = null; + } + videoRef.current.currentTime = clipStart; - videoRef.current.playbackRate = playbackRate; - }, [clipStart, playbackRate, preview]); - useEffect(() => { - if (!videoRef.current || !preview) { - return; - } - - if (!isVisible) { + if (isSafari || (isFirefox && isMobile)) { + // Safari / iOS can't play at speeds > 2x, so manually step through frames videoRef.current.pause(); - videoRef.current.currentTime = clipStart; - return; - } + compatIntervalRef.current = setInterval(() => { + if (!videoRef.current) { + return; + } - if (videoRef.current.readyState >= 2) { - resetPlayback(); - void videoRef.current.play().catch(() => undefined); + videoRef.current.currentTime += 1; + + if (videoRef.current.currentTime >= clipEnd) { + videoRef.current.currentTime = clipStart; + } + }, 1000 / playbackRate); + } else { + videoRef.current.playbackRate = playbackRate; } - }, [clipStart, isVisible, preview, resetPlayback]); + }, [clipStart, clipEnd, playbackRate, preview]); const drawDimOverlay = useCallback(() => { if (!dimOverlayCanvasRef.current) { @@ -463,15 +479,17 @@ function MotionPreviewClip({ {showLoadingIndicator && ( )} - {preview ? ( + {preview && isVisible ? ( <> {motionHeatmap && ( { if (config && selectedCamera) { return config.cameras[selectedCamera]; @@ -70,6 +72,7 @@ export default function MotionTunerView({ }, [config, selectedCamera]); useEffect(() => { + userInteractedRef.current = false; if (cameraConfig) { setMotionSettings({ threshold: cameraConfig.motion.threshold, @@ -87,24 +90,29 @@ export default function MotionTunerView({ }, [selectedCamera]); useEffect(() => { - if (!motionSettings.threshold) return; + if (!motionSettings.threshold || !userInteractedRef.current) return; sendMotionThreshold(motionSettings.threshold); }, [motionSettings.threshold, sendMotionThreshold]); useEffect(() => { - if (!motionSettings.contour_area) return; + if (!motionSettings.contour_area || !userInteractedRef.current) return; sendMotionContourArea(motionSettings.contour_area); }, [motionSettings.contour_area, sendMotionContourArea]); useEffect(() => { - if (motionSettings.improve_contrast === undefined) return; + if ( + motionSettings.improve_contrast === undefined || + !userInteractedRef.current + ) + return; sendImproveContrast(motionSettings.improve_contrast ? "ON" : "OFF"); }, [motionSettings.improve_contrast, sendImproveContrast]); const handleMotionConfigChange = (newConfig: Partial) => { + userInteractedRef.current = true; setMotionSettings((prevConfig) => ({ ...prevConfig, ...newConfig })); setUnsavedChanges(true); setChangedValue(true);