From 004581caf0f548ce32d2412c3c6534b3b724a5e8 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 25 Nov 2025 09:17:53 -0700 Subject: [PATCH 1/6] Don't add to history when opening search dialog --- web/src/components/overlay/detail/SearchDetailDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 467008e92..333c1bc9d 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -538,7 +538,7 @@ export default function SearchDetailDialog({ {isDesktop && onPrevious && onNext && ( From 036a5d84cc0d1e002107a592b2cc4ede9386a741 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 25 Nov 2025 10:07:13 -0700 Subject: [PATCH 2/6] Update caniuse --- web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 371defaaa..8df3356c5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -4702,9 +4702,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", "dev": true, "funding": [ { From 36da8db6d07d1483d8e27eefe987d0869d3cef24 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 25 Nov 2025 10:24:07 -0700 Subject: [PATCH 3/6] Revamp the history handling for dialog components --- web/src/components/mobile/MobilePage.tsx | 38 +++---------- .../overlay/detail/SearchDetailDialog.tsx | 2 +- .../overlay/dialog/PlatformAwareDialog.tsx | 7 ++- web/src/components/ui/dialog.tsx | 52 +++++------------ web/src/components/ui/sheet.tsx | 52 +++++------------ web/src/hooks/use-history-back.ts | 57 +++++++++++++++++++ 6 files changed, 103 insertions(+), 105 deletions(-) create mode 100644 web/src/hooks/use-history-back.ts diff --git a/web/src/components/mobile/MobilePage.tsx b/web/src/components/mobile/MobilePage.tsx index a4b80b2cb..4b6c41c7a 100644 --- a/web/src/components/mobile/MobilePage.tsx +++ b/web/src/components/mobile/MobilePage.tsx @@ -13,7 +13,7 @@ import { cn } from "@/lib/utils"; import { isPWA } from "@/utils/isPWA"; import { Button } from "@/components/ui/button"; import { useTranslation } from "react-i18next"; -import { useLocation } from "react-router-dom"; +import { useHistoryBack } from "@/hooks/use-history-back"; const MobilePageContext = createContext<{ open: boolean; @@ -24,15 +24,16 @@ type MobilePageProps = { children: React.ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void; + enableHistoryBack?: boolean; }; export function MobilePage({ children, open: controlledOpen, onOpenChange, + enableHistoryBack = true, }: MobilePageProps) { const [uncontrolledOpen, setUncontrolledOpen] = useState(false); - const location = useLocation(); const open = controlledOpen ?? uncontrolledOpen; const setOpen = useCallback( @@ -46,33 +47,12 @@ export function MobilePage({ [onOpenChange, setUncontrolledOpen], ); - useEffect(() => { - let isActive = true; - - if (open && isActive) { - window.history.pushState({ isMobilePage: true }, "", location.pathname); - } - - const handlePopState = (event: PopStateEvent) => { - if (open && isActive) { - event.preventDefault(); - setOpen(false); - // Delay replaceState to ensure state updates are processed - setTimeout(() => { - if (isActive) { - window.history.replaceState(null, "", location.pathname); - } - }, 0); - } - }; - - window.addEventListener("popstate", handlePopState); - - return () => { - isActive = false; - window.removeEventListener("popstate", handlePopState); - }; - }, [open, setOpen, location.pathname]); + // Handle browser back button to close mobile page + useHistoryBack({ + enabled: enableHistoryBack, + open, + onClose: () => setOpen(false), + }); return ( diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 333c1bc9d..467008e92 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -538,7 +538,7 @@ export default function SearchDetailDialog({ {isDesktop && onPrevious && onNext && ( diff --git a/web/src/components/overlay/dialog/PlatformAwareDialog.tsx b/web/src/components/overlay/dialog/PlatformAwareDialog.tsx index e0ec6efa9..5782ba149 100644 --- a/web/src/components/overlay/dialog/PlatformAwareDialog.tsx +++ b/web/src/components/overlay/dialog/PlatformAwareDialog.tsx @@ -113,7 +113,12 @@ export function PlatformAwareSheet({ } return ( - + {trigger} diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx index 65a861012..f65adbff0 100644 --- a/web/src/components/ui/dialog.tsx +++ b/web/src/components/ui/dialog.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import { X } from "lucide-react"; import { cn } from "@/lib/utils"; +import { useHistoryBack } from "@/hooks/use-history-back"; // Enhanced Dialog with History Support interface HistoryDialogProps extends DialogPrimitive.DialogProps { @@ -15,51 +16,28 @@ const Dialog = ({ ...props }: HistoryDialogProps) => { const [internalOpen, setInternalOpen] = React.useState(open || false); - const historyStateRef = React.useRef void; - }>(null); + // Sync internal state with controlled open prop React.useEffect(() => { if (open !== undefined) { setInternalOpen(open); } }, [open]); - React.useEffect(() => { - if (enableHistoryBack) { - if (internalOpen) { - window.history.pushState({ dialogOpen: true }, ""); + const handleOpenChange = React.useCallback( + (newOpen: boolean) => { + setInternalOpen(newOpen); + onOpenChange?.(newOpen); + }, + [onOpenChange], + ); - const listener = () => { - setInternalOpen(false); - if (onOpenChange) onOpenChange(false); - }; - - historyStateRef.current = { listener }; - window.addEventListener("popstate", listener); - - return () => { - if (internalOpen) { - window.removeEventListener("popstate", listener); - historyStateRef.current = null; - } - }; - } else if (historyStateRef.current) { - window.removeEventListener( - "popstate", - historyStateRef.current.listener, - ); - historyStateRef.current = null; - } - } - }, [enableHistoryBack, internalOpen, onOpenChange]); - - const handleOpenChange = (open: boolean) => { - setInternalOpen(open); - if (onOpenChange) { - onOpenChange(open); - } - }; + // Handle browser back button to close dialog + useHistoryBack({ + enabled: enableHistoryBack, + open: internalOpen, + onClose: () => handleOpenChange(false), + }); return ( { const [internalOpen, setInternalOpen] = React.useState(open || false); - const historyStateRef = React.useRef void; - }>(null); + // Sync internal state with controlled open prop React.useEffect(() => { if (open !== undefined) { setInternalOpen(open); } }, [open]); - React.useEffect(() => { - if (enableHistoryBack) { - if (internalOpen) { - window.history.pushState({ sheetOpen: true }, ""); + const handleOpenChange = React.useCallback( + (newOpen: boolean) => { + setInternalOpen(newOpen); + onOpenChange?.(newOpen); + }, + [onOpenChange], + ); - const listener = () => { - setInternalOpen(false); - if (onOpenChange) onOpenChange(false); - }; - - historyStateRef.current = { listener }; - window.addEventListener("popstate", listener); - - return () => { - if (internalOpen) { - window.removeEventListener("popstate", listener); - historyStateRef.current = null; - } - }; - } else if (historyStateRef.current) { - window.removeEventListener( - "popstate", - historyStateRef.current.listener, - ); - historyStateRef.current = null; - } - } - }, [enableHistoryBack, internalOpen, onOpenChange]); - - const handleOpenChange = (open: boolean) => { - setInternalOpen(open); - if (onOpenChange) { - onOpenChange(open); - } - }; + // Handle browser back button to close sheet + useHistoryBack({ + enabled: enableHistoryBack, + open: internalOpen, + onClose: () => handleOpenChange(false), + }); return ( void; +} + +/** + * Hook that manages browser history for overlay components (dialogs, sheets, etc.) + * When enabled, pressing the browser back button will close the overlay instead of navigating away. + */ +export function useHistoryBack({ + enabled, + open, + onClose, +}: UseHistoryBackOptions): void { + const historyPushedRef = React.useRef(false); + const closedByBackRef = React.useRef(false); + + // Keep onClose in a ref to avoid effect re-runs that cause multiple history pushes + const onCloseRef = React.useRef(onClose); + React.useLayoutEffect(() => { + onCloseRef.current = onClose; + }); + + React.useEffect(() => { + if (!enabled) return; + + if (open) { + // Only push history state if we haven't already (prevents duplicates in strict mode) + if (!historyPushedRef.current) { + window.history.pushState({ overlayOpen: true }, ""); + historyPushedRef.current = true; + } + + const handlePopState = () => { + closedByBackRef.current = true; + historyPushedRef.current = false; + onCloseRef.current(); + }; + + window.addEventListener("popstate", handlePopState); + + return () => { + window.removeEventListener("popstate", handlePopState); + }; + } else { + // Overlay is closing - clean up history if we pushed and it wasn't via back button + if (historyPushedRef.current && !closedByBackRef.current) { + window.history.back(); + } + historyPushedRef.current = false; + closedByBackRef.current = false; + } + }, [enabled, open]); +} From 71ae40217cd38ebed062c653c9c1bc45e5bb14a6 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:22:34 -0600 Subject: [PATCH 4/6] clarify audio transcription docs --- docs/docs/configuration/audio_detectors.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/docs/configuration/audio_detectors.md b/docs/docs/configuration/audio_detectors.md index 3bf57b1a7..245ce703c 100644 --- a/docs/docs/configuration/audio_detectors.md +++ b/docs/docs/configuration/audio_detectors.md @@ -75,7 +75,13 @@ audio: ### Audio Transcription -Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAI’s open-source Whisper models via `faster-whisper`. To enable transcription, enable it in your config. Note that audio detection must also be enabled as described above in order to use audio transcription features. +Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAI’s open-source Whisper models via `faster-whisper`. The goal of this feature is to support Semantic Search for `speech` audio events. Frigate is not intended to act as a continuous, fully-automatic speech transcription service — automatically transcribing all speech (or queuing many audio events for transcription) requires substantial CPU (or GPU) resources and is impractical on most systems. For this reason, transcriptions for events are initiated manually from the UI or the API rather than being run continuously in the background. + +Transcription accuracy also depends heavily on the quality of your camera's microphone and recording conditions. Many cameras use inexpensive microphones, and distance to the speaker, low audio bitrate, or background noise can significantly reduce transcription quality. If you need higher accuracy, more robust long-running queues, or large-scale automatic transcription, consider using the HTTP API in combination with an automation platform and a cloud transcription service. + +#### Configuration + +To enable transcription, enable it in your config. Note that audio detection must also be enabled as described above in order to use audio transcription features. ```yaml audio_transcription: From 8e64f912f4919ec4d0bd6e909f7ef0a77d33dab1 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 25 Nov 2025 14:26:30 -0700 Subject: [PATCH 5/6] Use titlecase helper --- frigate/data_processing/post/review_descriptions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frigate/data_processing/post/review_descriptions.py b/frigate/data_processing/post/review_descriptions.py index fadc483c3..60788e504 100644 --- a/frigate/data_processing/post/review_descriptions.py +++ b/frigate/data_processing/post/review_descriptions.py @@ -12,6 +12,7 @@ from typing import Any import cv2 from peewee import DoesNotExist +from titlecase import titlecase from frigate.comms.embeddings_updater import EmbeddingsRequestEnum from frigate.comms.inter_process import InterProcessRequestor @@ -455,14 +456,14 @@ def run_analysis( for i, verified_label in enumerate(final_data["data"]["verified_objects"]): object_type = verified_label.replace("-verified", "").replace("_", " ") - name = sub_labels_list[i].replace("_", " ").title() + name = titlecase(sub_labels_list[i].replace("_", " ")) unified_objects.append(f"{name} ({object_type})") for label in objects_list: if "-verified" in label: continue elif label in labelmap_objects: - object_type = label.replace("_", " ").title() + object_type = titlecase(label.replace("_", " ")) if label in attribute_labels: unified_objects.append(f"{object_type} (delivery/service)") From c30b912458ef3b060e0c3b0ee4d93b80173c34e2 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 25 Nov 2025 16:59:39 -0700 Subject: [PATCH 6/6] Allow running object clasasification on stationary objects --- frigate/data_processing/real_time/custom_classification.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/frigate/data_processing/real_time/custom_classification.py b/frigate/data_processing/real_time/custom_classification.py index b5e8b1e35..cae3087ff 100644 --- a/frigate/data_processing/real_time/custom_classification.py +++ b/frigate/data_processing/real_time/custom_classification.py @@ -405,9 +405,6 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi): if obj_data.get("end_time") is not None: return - if obj_data.get("stationary"): - return - object_id = obj_data["id"] if (