diff --git a/docs/docs/configuration/audio_detectors.md b/docs/docs/configuration/audio_detectors.md index bf71f8d81..3bf57b1a7 100644 --- a/docs/docs/configuration/audio_detectors.md +++ b/docs/docs/configuration/audio_detectors.md @@ -144,4 +144,10 @@ In order to use transcription and translation for past events, you must enable a The transcribed/translated speech will appear in the description box in the Tracked Object Details pane. If Semantic Search is enabled, embeddings are generated for the transcription text and are fully searchable using the description search type. -Recorded `speech` events will always use a `whisper` model, regardless of the `model_size` config setting. Without a GPU, generating transcriptions for longer `speech` events may take a fair amount of time, so be patient. +:::note + +Only one `speech` event may be transcribed at a time. Frigate does not automatically transcribe `speech` events or implement a queue for long-running transcription model inference. + +::: + +Recorded `speech` events will always use a `whisper` model, regardless of the `model_size` config setting. Without a supported Nvidia GPU, generating transcriptions for longer `speech` events may take a fair amount of time, so be patient. diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index 907bda21e..f8b49303f 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -700,11 +700,11 @@ genai: # Optional: Configuration for audio transcription # NOTE: only the enabled option can be overridden at the camera level audio_transcription: - # Optional: Enable license plate recognition (default: shown below) + # Optional: Enable live and speech event audio transcription (default: shown below) enabled: False - # Optional: The device to run the models on (default: shown below) + # Optional: The device to run the models on for live transcription. (default: shown below) device: CPU - # Optional: Set the model size used for transcription. (default: shown below) + # Optional: Set the model size used for live transcription. (default: shown below) model_size: small # Optional: Set the language used for transcription translation. (default: shown below) # List of language codes: https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10 diff --git a/frigate/api/classification.py b/frigate/api/classification.py index a2aec6898..9b116be10 100644 --- a/frigate/api/classification.py +++ b/frigate/api/classification.py @@ -542,6 +542,7 @@ def transcribe_audio(request: Request, body: AudioTranscriptionBody): status_code=409, # 409 Conflict ) else: + logger.debug(f"Failed to transcribe audio, response: {response}") return JSONResponse( content={ "success": False, diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 235693c8c..0c2ba5a89 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -23,6 +23,7 @@ from frigate.const import ( NOTIFICATION_TEST, REQUEST_REGION_GRID, UPDATE_AUDIO_ACTIVITY, + UPDATE_AUDIO_TRANSCRIPTION_STATE, UPDATE_BIRDSEYE_LAYOUT, UPDATE_CAMERA_ACTIVITY, UPDATE_EMBEDDINGS_REINDEX_PROGRESS, @@ -61,6 +62,7 @@ class Dispatcher: self.model_state: dict[str, ModelStatusTypesEnum] = {} self.embeddings_reindex: dict[str, Any] = {} self.birdseye_layout: dict[str, Any] = {} + self.audio_transcription_state: str = "idle" self._camera_settings_handlers: dict[str, Callable] = { "audio": self._on_audio_command, "audio_transcription": self._on_audio_transcription_command, @@ -178,6 +180,19 @@ class Dispatcher: def handle_model_state() -> None: self.publish("model_state", json.dumps(self.model_state.copy())) + def handle_update_audio_transcription_state() -> None: + if payload: + self.audio_transcription_state = payload + self.publish( + "audio_transcription_state", + json.dumps(self.audio_transcription_state), + ) + + def handle_audio_transcription_state() -> None: + self.publish( + "audio_transcription_state", json.dumps(self.audio_transcription_state) + ) + def handle_update_embeddings_reindex_progress() -> None: self.embeddings_reindex = payload self.publish( @@ -264,10 +279,12 @@ class Dispatcher: UPDATE_MODEL_STATE: handle_update_model_state, UPDATE_EMBEDDINGS_REINDEX_PROGRESS: handle_update_embeddings_reindex_progress, UPDATE_BIRDSEYE_LAYOUT: handle_update_birdseye_layout, + UPDATE_AUDIO_TRANSCRIPTION_STATE: handle_update_audio_transcription_state, NOTIFICATION_TEST: handle_notification_test, "restart": handle_restart, "embeddingsReindexProgress": handle_embeddings_reindex_progress, "modelState": handle_model_state, + "audioTranscriptionState": handle_audio_transcription_state, "birdseyeLayout": handle_birdseye_layout, "onConnect": handle_on_connect, } diff --git a/frigate/const.py b/frigate/const.py index 5710966bf..11e89886f 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -113,6 +113,7 @@ CLEAR_ONGOING_REVIEW_SEGMENTS = "clear_ongoing_review_segments" UPDATE_CAMERA_ACTIVITY = "update_camera_activity" UPDATE_AUDIO_ACTIVITY = "update_audio_activity" EXPIRE_AUDIO_ACTIVITY = "expire_audio_activity" +UPDATE_AUDIO_TRANSCRIPTION_STATE = "update_audio_transcription_state" UPDATE_EVENT_DESCRIPTION = "update_event_description" UPDATE_REVIEW_DESCRIPTION = "update_review_description" UPDATE_MODEL_STATE = "update_model_state" diff --git a/frigate/data_processing/post/audio_transcription.py b/frigate/data_processing/post/audio_transcription.py index 870c34068..b7b6cb021 100644 --- a/frigate/data_processing/post/audio_transcription.py +++ b/frigate/data_processing/post/audio_transcription.py @@ -13,6 +13,7 @@ from frigate.config import FrigateConfig from frigate.const import ( CACHE_DIR, MODEL_CACHE_DIR, + UPDATE_AUDIO_TRANSCRIPTION_STATE, UPDATE_EVENT_DESCRIPTION, ) from frigate.data_processing.types import PostProcessDataEnum @@ -190,6 +191,8 @@ class AudioTranscriptionPostProcessor(PostProcessorApi): self.transcription_running = False self.transcription_thread = None + self.requestor.send_data(UPDATE_AUDIO_TRANSCRIPTION_STATE, "idle") + def handle_request(self, topic: str, request_data: dict[str, any]) -> str | None: if topic == "transcribe_audio": event = request_data["event"] @@ -203,6 +206,8 @@ class AudioTranscriptionPostProcessor(PostProcessorApi): # Mark as running and start the thread self.transcription_running = True + self.requestor.send_data(UPDATE_AUDIO_TRANSCRIPTION_STATE, "processing") + self.transcription_thread = threading.Thread( target=self._transcription_wrapper, args=(event,), daemon=True ) diff --git a/frigate/timeline.py b/frigate/timeline.py index 8e6aedc67..a2d59b88e 100644 --- a/frigate/timeline.py +++ b/frigate/timeline.py @@ -109,6 +109,7 @@ class TimelineProcessor(threading.Thread): event_data["region"], ), "attribute": "", + "score": event_data["score"], }, } diff --git a/web/src/api/ws.tsx b/web/src/api/ws.tsx index 302f3f263..44d45ea2f 100644 --- a/web/src/api/ws.tsx +++ b/web/src/api/ws.tsx @@ -461,6 +461,40 @@ export function useEmbeddingsReindexProgress( return { payload: data }; } +export function useAudioTranscriptionProcessState( + revalidateOnFocus: boolean = true, +): { payload: string } { + const { + value: { payload }, + send: sendCommand, + } = useWs("audio_transcription_state", "audioTranscriptionState"); + + const data = useDeepMemo( + payload ? (JSON.parse(payload as string) as string) : "idle", + ); + + useEffect(() => { + let listener = undefined; + if (revalidateOnFocus) { + sendCommand("audioTranscriptionState"); + listener = () => { + if (document.visibilityState == "visible") { + sendCommand("audioTranscriptionState"); + } + }; + addEventListener("visibilitychange", listener); + } + return () => { + if (listener) { + removeEventListener("visibilitychange", listener); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [revalidateOnFocus]); + + return { payload: data || "idle" }; +} + export function useBirdseyeLayout(revalidateOnFocus: boolean = true): { payload: string; } { diff --git a/web/src/components/overlay/ObjectTrackOverlay.tsx b/web/src/components/overlay/ObjectTrackOverlay.tsx index 7e548af2e..8f78adcd7 100644 --- a/web/src/components/overlay/ObjectTrackOverlay.tsx +++ b/web/src/components/overlay/ObjectTrackOverlay.tsx @@ -42,6 +42,7 @@ type ObjectData = { pathPoints: PathPoint[]; currentZones: string[]; currentBox?: number[]; + currentAttributeBox?: number[]; }; export default function ObjectTrackOverlay({ @@ -105,6 +106,12 @@ export default function ObjectTrackOverlay({ selectedObjectIds.length > 0 ? ["event_ids", { ids: selectedObjectIds.join(",") }] : null, + null, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 30000, + }, ); // Fetch timeline data for each object ID using fixed number of hooks @@ -112,7 +119,12 @@ export default function ObjectTrackOverlay({ selectedObjectIds.length > 0 ? `timeline?source_id=${selectedObjectIds.join(",")}&limit=1000` : null, - { revalidateOnFocus: false }, + null, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 30000, + }, ); const getZonesFriendlyNames = (zones: string[], config: FrigateConfig) => { @@ -270,6 +282,7 @@ export default function ObjectTrackOverlay({ ); const currentBox = nearbyTimelineEvent?.data?.box; + const currentAttributeBox = nearbyTimelineEvent?.data?.attribute_box; return { objectId, @@ -278,6 +291,7 @@ export default function ObjectTrackOverlay({ pathPoints: combinedPoints, currentZones, currentBox, + currentAttributeBox, }; }) .filter((obj: ObjectData) => obj.pathPoints.length > 0); // Only include objects with path data @@ -482,6 +496,20 @@ export default function ObjectTrackOverlay({ /> )} + {objData.currentAttributeBox && showBoundingBoxes && ( + + + + )} ); })} diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 6b716a563..467008e92 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -92,6 +92,7 @@ import { DialogPortal } from "@radix-ui/react-dialog"; import { useDetailStream } from "@/context/detail-stream-context"; import { PiSlidersHorizontalBold } from "react-icons/pi"; import { HiSparkles } from "react-icons/hi"; +import { useAudioTranscriptionProcessState } from "@/api/ws"; const SEARCH_TABS = ["snapshot", "tracking_details"] as const; export type SearchTab = (typeof SEARCH_TABS)[number]; @@ -1076,6 +1077,11 @@ function ObjectDetailsTab({ }); }, [search, t]); + // audio transcription processing state + + const { payload: audioTranscriptionProcessState } = + useAudioTranscriptionProcessState(); + // frigate+ submission type SubmissionState = "reviewing" | "uploading" | "submitted"; @@ -1431,10 +1437,20 @@ function ObjectDetailsTab({ diff --git a/web/src/components/overlay/detail/TrackingDetails.tsx b/web/src/components/overlay/detail/TrackingDetails.tsx index 727dd4552..c6e10f8c2 100644 --- a/web/src/components/overlay/detail/TrackingDetails.tsx +++ b/web/src/components/overlay/detail/TrackingDetails.tsx @@ -75,12 +75,15 @@ export function TrackingDetails({ setIsVideoLoading(true); }, [event.id]); - const { data: eventSequence } = useSWR([ - "timeline", + const { data: eventSequence } = useSWR( + ["timeline", { source_id: event.id }], + null, { - source_id: event.id, + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 30000, }, - ]); + ); const { data: config } = useSWR("config"); @@ -104,6 +107,12 @@ export function TrackingDetails({ }, ] : null, + null, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 30000, + }, ); // Convert a timeline timestamp to actual video player time, accounting for @@ -714,53 +723,6 @@ export function TrackingDetails({ )}
{eventSequence.map((item, idx) => { - const isActive = - Math.abs( - (effectiveTime ?? 0) - (item.timestamp ?? 0), - ) <= 0.5; - const formattedEventTimestamp = config - ? formatUnixTimestampToDateTime(item.timestamp ?? 0, { - timezone: config.ui.timezone, - date_format: - config.ui.time_format == "24hour" - ? t( - "time.formattedTimestampHourMinuteSecond.24hour", - { ns: "common" }, - ) - : t( - "time.formattedTimestampHourMinuteSecond.12hour", - { ns: "common" }, - ), - time_style: "medium", - date_style: "medium", - }) - : ""; - - const ratio = - Array.isArray(item.data.box) && - item.data.box.length >= 4 - ? ( - aspectRatio * - (item.data.box[2] / item.data.box[3]) - ).toFixed(2) - : "N/A"; - const areaPx = - Array.isArray(item.data.box) && - item.data.box.length >= 4 - ? Math.round( - (config.cameras[event.camera]?.detect?.width ?? - 0) * - (config.cameras[event.camera]?.detect - ?.height ?? 0) * - (item.data.box[2] * item.data.box[3]), - ) - : undefined; - const areaPct = - Array.isArray(item.data.box) && - item.data.box.length >= 4 - ? (item.data.box[2] * item.data.box[3]).toFixed(4) - : undefined; - return (
handleLifecycleClick(item)} setSelectedZone={setSelectedZone} getZoneColor={getZoneColor} @@ -798,11 +756,7 @@ export function TrackingDetails({ type LifecycleIconRowProps = { item: TrackingDetailsSequence; - isActive?: boolean; - formattedEventTimestamp: string; - ratio: string; - areaPx?: number; - areaPct?: string; + event: Event; onClick: () => void; setSelectedZone: (z: string) => void; getZoneColor: (zoneName: string) => number[] | undefined; @@ -812,11 +766,7 @@ type LifecycleIconRowProps = { function LifecycleIconRow({ item, - isActive, - formattedEventTimestamp, - ratio, - areaPx, - areaPct, + event, onClick, setSelectedZone, getZoneColor, @@ -826,9 +776,101 @@ function LifecycleIconRow({ const { t } = useTranslation(["views/explore", "components/player"]); const { data: config } = useSWR("config"); const [isOpen, setIsOpen] = useState(false); - const navigate = useNavigate(); + const aspectRatio = useMemo(() => { + if (!config) { + return 16 / 9; + } + + return ( + config.cameras[event.camera].detect.width / + config.cameras[event.camera].detect.height + ); + }, [config, event]); + + const isActive = useMemo( + () => Math.abs((effectiveTime ?? 0) - (item.timestamp ?? 0)) <= 0.5, + [effectiveTime, item.timestamp], + ); + + const formattedEventTimestamp = useMemo( + () => + config + ? formatUnixTimestampToDateTime(item.timestamp ?? 0, { + timezone: config.ui.timezone, + date_format: + config.ui.time_format == "24hour" + ? t("time.formattedTimestampHourMinuteSecond.24hour", { + ns: "common", + }) + : t("time.formattedTimestampHourMinuteSecond.12hour", { + ns: "common", + }), + time_style: "medium", + date_style: "medium", + }) + : "", + [config, item.timestamp, t], + ); + + const ratio = useMemo( + () => + Array.isArray(item.data.box) && item.data.box.length >= 4 + ? (aspectRatio * (item.data.box[2] / item.data.box[3])).toFixed(2) + : "N/A", + [aspectRatio, item.data.box], + ); + + const areaPx = useMemo( + () => + Array.isArray(item.data.box) && item.data.box.length >= 4 + ? Math.round( + (config?.cameras[event.camera]?.detect?.width ?? 0) * + (config?.cameras[event.camera]?.detect?.height ?? 0) * + (item.data.box[2] * item.data.box[3]), + ) + : undefined, + [config, event.camera, item.data.box], + ); + + const attributeAreaPx = useMemo( + () => + Array.isArray(item.data.attribute_box) && + item.data.attribute_box.length >= 4 + ? Math.round( + (config?.cameras[event.camera]?.detect?.width ?? 0) * + (config?.cameras[event.camera]?.detect?.height ?? 0) * + (item.data.attribute_box[2] * item.data.attribute_box[3]), + ) + : undefined, + [config, event.camera, item.data.attribute_box], + ); + + const attributeAreaPct = useMemo( + () => + Array.isArray(item.data.attribute_box) && + item.data.attribute_box.length >= 4 + ? (item.data.attribute_box[2] * item.data.attribute_box[3]).toFixed(4) + : undefined, + [item.data.attribute_box], + ); + + const areaPct = useMemo( + () => + Array.isArray(item.data.box) && item.data.box.length >= 4 + ? (item.data.box[2] * item.data.box[3]).toFixed(4) + : undefined, + [item.data.box], + ); + + const score = useMemo(() => { + if (item.data.score !== undefined) { + return (item.data.score * 100).toFixed(0) + "%"; + } + return "N/A"; + }, [item.data.score]); + return (
{getLifecycleItemDescription(item)}
-
-
+
+
+ + {t("trackingDetails.lifecycleItemDesc.header.score")} + + {score} +
+
{t("trackingDetails.lifecycleItemDesc.header.ratio")} {ratio}
-
+
- {t("trackingDetails.lifecycleItemDesc.header.area")} + {t("trackingDetails.lifecycleItemDesc.header.area")}{" "} + {attributeAreaPx !== undefined && + attributeAreaPct !== undefined && ( + + ({getTranslatedLabel(item.data.label)}) + + )} {areaPx !== undefined && areaPct !== undefined ? ( @@ -876,9 +930,25 @@ function LifecycleIconRow({ N/A )}
+ {attributeAreaPx !== undefined && + attributeAreaPct !== undefined && ( +
+ + {t("trackingDetails.lifecycleItemDesc.header.area")} ( + {getTranslatedLabel(item.data.attribute)}) + + + {t("information.pixels", { + ns: "common", + area: attributeAreaPx, + })}{" "} + ยท {attributeAreaPct}% + +
+ )} {item.data?.zones && item.data.zones.length > 0 && ( -
+
{item.data.zones.map((zone, zidx) => { const color = getZoneColor(zone)?.join(",") ?? "0,0,0"; return ( diff --git a/web/src/components/player/MsePlayer.tsx b/web/src/components/player/MsePlayer.tsx index 7b78b53bc..576fc93d6 100644 --- a/web/src/components/player/MsePlayer.tsx +++ b/web/src/components/player/MsePlayer.tsx @@ -82,6 +82,7 @@ function MSEPlayer({ [key: string]: (msg: { value: string; type: string }) => void; }>({}); const msRef = useRef(null); + const mseCodecRef = useRef(null); const wsURL = useMemo(() => { return `${baseUrl.replace(/^http/, "ws")}live/mse/api/ws?src=${camera}`; @@ -93,6 +94,10 @@ function MSEPlayer({ console.error( `${camera} - MSE error '${error}': ${description} See the documentation: https://docs.frigate.video/configuration/live/#live-player-error-messages`, ); + if (mseCodecRef.current) { + // eslint-disable-next-line no-console + console.error(`${camera} - MSE codec in use: ${mseCodecRef.current}`); + } onError?.(error); }, [camera, onError], @@ -299,6 +304,9 @@ function MSEPlayer({ onmessageRef.current["mse"] = (msg) => { if (msg.type !== "mse") return; + // Store the codec value for error logging + mseCodecRef.current = msg.value; + let sb: SourceBuffer | undefined; try { sb = msRef.current?.addSourceBuffer(msg.value); diff --git a/web/src/types/timeline.ts b/web/src/types/timeline.ts index c8e5f7543..0de067406 100644 --- a/web/src/types/timeline.ts +++ b/web/src/types/timeline.ts @@ -16,6 +16,7 @@ export type TrackingDetailsSequence = { data: { camera: string; label: string; + score: number; sub_label: string; box?: [number, number, number, number]; region: [number, number, number, number]; diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index 65257326f..ada72bee3 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -1376,329 +1376,343 @@ function FrigateCameraFeatures({ title={t("cameraSettings.title", { camera })} /> - -
- {isAdmin && ( - <> - - sendEnabled(enabledState == "ON" ? "OFF" : "ON") - } - /> - - sendDetect(detectState == "ON" ? "OFF" : "ON") - } - /> - {recordingEnabled && ( + +
+ <> + {isAdmin && ( + <> - sendRecord(recordState == "ON" ? "OFF" : "ON") + sendEnabled(enabledState == "ON" ? "OFF" : "ON") } /> - )} - - sendSnapshot(snapshotState == "ON" ? "OFF" : "ON") - } - /> - {audioDetectEnabled && ( - sendAudio(audioState == "ON" ? "OFF" : "ON") + sendDetect(detectState == "ON" ? "OFF" : "ON") } /> - )} - {audioDetectEnabled && transcriptionEnabled && ( + {recordingEnabled && ( + + sendRecord(recordState == "ON" ? "OFF" : "ON") + } + /> + )} - sendTranscription(transcriptionState == "ON" ? "OFF" : "ON") + sendSnapshot(snapshotState == "ON" ? "OFF" : "ON") } /> - )} - {autotrackingEnabled && ( - - sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON") - } - /> - )} - - )} -
+ {audioDetectEnabled && ( + + sendAudio(audioState == "ON" ? "OFF" : "ON") + } + /> + )} + {audioDetectEnabled && transcriptionEnabled && ( + + sendTranscription( + transcriptionState == "ON" ? "OFF" : "ON", + ) + } + /> + )} + {autotrackingEnabled && ( + + sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON") + } + /> + )} + + )} -
- {!isRestreamed && ( -
- -
- -
- {t("streaming.restreaming.disabled", { - ns: "components/dialog", - })} -
- - -
- - - {t("button.info", { ns: "common" })} - -
-
- - {t("streaming.restreaming.desc.title", { - ns: "components/dialog", - })} -
- - {t("readTheDocumentation", { ns: "common" })} - - -
-
-
-
-
- )} - {isRestreamed && Object.values(camera.live.streams).length > 0 && ( -
-
{t("stream.title")}
- - - {debug && ( -
- <> - -
{t("stream.debug.picker")}
- -
- )} - - {preferredLiveMode != "jsmpeg" && !debug && isRestreamed && ( -
- {supportsAudioOutput ? ( - <> - -
{t("stream.audio.available")}
- - ) : ( - <> - -
{t("stream.audio.unavailable")}
- - -
- - - {t("button.info", { ns: "common" })} - -
-
- - {t("stream.audio.tips.title")} -
- - {t("readTheDocumentation", { ns: "common" })} - - -
-
-
- - )} -
- )} - {preferredLiveMode != "jsmpeg" && - !debug && - isRestreamed && - supportsAudioOutput && ( +
+ {!isRestreamed && ( +
+
- {supports2WayTalk ? ( - <> - -
{t("stream.twoWayTalk.available")}
- - ) : ( - <> - -
{t("stream.twoWayTalk.unavailable")}
- - -
- - - {t("button.info", { ns: "common" })} - -
-
- - {t("stream.twoWayTalk.tips")} -
- +
+ {t("streaming.restreaming.disabled", { + ns: "components/dialog", + })} +
+ + +
+ + + {t("button.info", { ns: "common" })} + +
+
+ + {t("streaming.restreaming.desc.title", { + ns: "components/dialog", + })} +
+ + {t("readTheDocumentation", { ns: "common" })} + + +
+
+
+
+
+ )} + {isRestreamed && + Object.values(camera.live.streams).length > 0 && ( +
+
{t("stream.title")}
+ + + {debug && ( +
+ <> + +
{t("stream.debug.picker")}
+ +
+ )} + + {preferredLiveMode != "jsmpeg" && + !debug && + isRestreamed && ( +
+ {supportsAudioOutput ? ( + <> + +
{t("stream.audio.available")}
+ + ) : ( + <> + +
{t("stream.audio.unavailable")}
+ + +
+ + + {t("button.info", { ns: "common" })} + +
+
+ + {t("stream.audio.tips.title")} +
+ + {t("readTheDocumentation", { + ns: "common", + })} + + +
+
+
+ + )} +
+ )} + {preferredLiveMode != "jsmpeg" && + !debug && + isRestreamed && + supportsAudioOutput && ( +
+ {supports2WayTalk ? ( + <> + +
{t("stream.twoWayTalk.available")}
+ + ) : ( + <> + +
{t("stream.twoWayTalk.unavailable")}
+ + +
+ + + {t("button.info", { ns: "common" })} + +
+
+ + {t("stream.twoWayTalk.tips")} +
+ + {t("readTheDocumentation", { + ns: "common", + })} + + +
+
+
+ + )} +
+ )} + {preferredLiveMode == "jsmpeg" && isRestreamed && ( +
+
+ +

+ {t("stream.lowBandwidth.tips")} +

+
+ +
)}
)} - {preferredLiveMode == "jsmpeg" && isRestreamed && ( -
-
- -

{t("stream.lowBandwidth.tips")}

-
+
+
+ {t("manualRecording.title")} +
+
+
+

+ {t("manualRecording.tips")} +

+
+ {isRestreamed && ( + <> +
+ { + setPlayInBackground(checked); + }} + disabled={debug} + /> +

+ {t("manualRecording.playInBackground.desc")} +

+
+
+ { + setShowStats(checked); + }} + disabled={debug} + /> +

+ {t("manualRecording.showStats.desc")} +

+
+ )} -
- )} -
-
- {t("manualRecording.title")} -
-
- - -
-

- {t("manualRecording.tips")} -

-
- {isRestreamed && ( - <> -
+
{ - setPlayInBackground(checked); - }} - disabled={debug} + label={t("streaming.debugView", { ns: "components/dialog" })} + isChecked={debug} + onCheckedChange={(checked) => setDebug(checked)} /> -

- {t("manualRecording.playInBackground.desc")} -

-
- { - setShowStats(checked); - }} - disabled={debug} - /> -

- {t("manualRecording.showStats.desc")} -

-
- - )} -
- setDebug(checked)} - /> -
+
+
diff --git a/web/src/views/settings/AuthenticationView.tsx b/web/src/views/settings/AuthenticationView.tsx index 5c11d8914..19f157b46 100644 --- a/web/src/views/settings/AuthenticationView.tsx +++ b/web/src/views/settings/AuthenticationView.tsx @@ -784,7 +784,7 @@ export default function AuthenticationView({ return (
-
+
{section === "users" && UsersSection} {section === "roles" && RolesSection} {!section && ( diff --git a/web/src/views/settings/CameraManagementView.tsx b/web/src/views/settings/CameraManagementView.tsx index 1a626fa02..8f1b5eae5 100644 --- a/web/src/views/settings/CameraManagementView.tsx +++ b/web/src/views/settings/CameraManagementView.tsx @@ -65,7 +65,7 @@ export default function CameraManagementView({ closeButton />
-
+
{viewMode === "settings" ? ( <> diff --git a/web/src/views/settings/CameraReviewSettingsView.tsx b/web/src/views/settings/CameraReviewSettingsView.tsx index 47ea5c22a..7a7b92e4e 100644 --- a/web/src/views/settings/CameraReviewSettingsView.tsx +++ b/web/src/views/settings/CameraReviewSettingsView.tsx @@ -298,7 +298,7 @@ export default function CameraReviewSettingsView({ <>
-
+
{t("cameraReview.title")} diff --git a/web/src/views/settings/EnrichmentsSettingsView.tsx b/web/src/views/settings/EnrichmentsSettingsView.tsx index e3b0626b9..6aba50dd3 100644 --- a/web/src/views/settings/EnrichmentsSettingsView.tsx +++ b/web/src/views/settings/EnrichmentsSettingsView.tsx @@ -244,7 +244,7 @@ export default function EnrichmentsSettingsView({ return (
-
+
{t("enrichments.title")} diff --git a/web/src/views/settings/FrigatePlusSettingsView.tsx b/web/src/views/settings/FrigatePlusSettingsView.tsx index 52af94354..80d98b197 100644 --- a/web/src/views/settings/FrigatePlusSettingsView.tsx +++ b/web/src/views/settings/FrigatePlusSettingsView.tsx @@ -211,7 +211,7 @@ export default function FrigatePlusSettingsView({ <>
-
+
{t("frigatePlus.title")} diff --git a/web/src/views/settings/MasksAndZonesView.tsx b/web/src/views/settings/MasksAndZonesView.tsx index 27c542e87..efeaa9be0 100644 --- a/web/src/views/settings/MasksAndZonesView.tsx +++ b/web/src/views/settings/MasksAndZonesView.tsx @@ -434,7 +434,7 @@ export default function MasksAndZonesView({ {cameraConfig && editingPolygons && (
-
+
{editPane == "zone" && ( -
+
{t("motionDetectionTuner.title")} diff --git a/web/src/views/settings/NotificationsSettingsView.tsx b/web/src/views/settings/NotificationsSettingsView.tsx index 6280ca6a8..77da16386 100644 --- a/web/src/views/settings/NotificationsSettingsView.tsx +++ b/web/src/views/settings/NotificationsSettingsView.tsx @@ -331,7 +331,7 @@ export default function NotificationView({ if (!("Notification" in window) || !window.isSecureContext) { return ( -
+
@@ -385,7 +385,7 @@ export default function NotificationView({ <>
-
+
-
+
{t("debug.title")} @@ -434,7 +434,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) { {t("debug.objectShapeFilterDrawing.area")}

{obj.area ? ( - <> +
px: {obj.area.toString()}
@@ -448,7 +448,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) { .toFixed(4) .toString()}
- +
) : ( "-" )} diff --git a/web/src/views/settings/TriggerView.tsx b/web/src/views/settings/TriggerView.tsx index 0b004fd82..a0e19f5b2 100644 --- a/web/src/views/settings/TriggerView.tsx +++ b/web/src/views/settings/TriggerView.tsx @@ -440,7 +440,7 @@ export default function TriggerView({ return (
-
+
{!isSemanticSearchEnabled ? (
diff --git a/web/src/views/settings/UiSettingsView.tsx b/web/src/views/settings/UiSettingsView.tsx index 8ec484aa3..34df0ddc8 100644 --- a/web/src/views/settings/UiSettingsView.tsx +++ b/web/src/views/settings/UiSettingsView.tsx @@ -108,7 +108,7 @@ export default function UiSettingsView() { <>
-
+
{t("general.title")}