diff --git a/frigate/app.py b/frigate/app.py index 488f121e6..b6a94ed66 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -428,18 +428,11 @@ class FrigateApp: self.camera_maintainer.start() def start_audio_processor(self) -> None: - audio_cameras = [ - c - for c in self.config.cameras.values() - if c.enabled and c.audio.enabled_in_config - ] - - if audio_cameras: - self.audio_process = AudioProcessor( - self.config, audio_cameras, self.camera_metrics, self.stop_event - ) - self.audio_process.start() - self.processes["audio_detector"] = self.audio_process.pid or 0 + self.audio_process = AudioProcessor( + self.config, self.camera_metrics, self.stop_event + ) + self.audio_process.start() + self.processes["audio_detector"] = self.audio_process.pid or 0 def start_timeline_processor(self) -> None: self.timeline_processor = TimelineProcessor( diff --git a/frigate/data_processing/post/object_descriptions.py b/frigate/data_processing/post/object_descriptions.py index babdb7252..6404b3851 100644 --- a/frigate/data_processing/post/object_descriptions.py +++ b/frigate/data_processing/post/object_descriptions.py @@ -269,7 +269,9 @@ class ObjectDescriptionProcessor(PostProcessorApi): if event.has_snapshot and camera_config.objects.genai.use_snapshot: snapshot_image = self._read_and_crop_snapshot(event) + if not snapshot_image: + self.cleanup_event(event_id) return num_thumbnails = len(self.tracked_events.get(event_id, [])) diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 1a998febe..96a44a8c6 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -60,7 +60,11 @@ from frigate.data_processing.real_time.license_plate import ( ) from frigate.data_processing.types import DataProcessorMetrics, PostProcessDataEnum from frigate.db.sqlitevecq import SqliteVecQueueDatabase -from frigate.events.types import EventTypeEnum, RegenerateDescriptionEnum +from frigate.events.types import ( + EventStateEnum, + EventTypeEnum, + RegenerateDescriptionEnum, +) from frigate.genai import GenAIClientManager from frigate.models import Event, Recordings, ReviewSegment, Trigger from frigate.types import TrackedObjectUpdateTypesEnum @@ -435,7 +439,7 @@ class EmbeddingMaintainer(threading.Thread): if update is None: return - source_type, _, camera, frame_name, data = update + source_type, event_type, camera, frame_name, data = update logger.debug( f"Received update - source_type: {source_type}, camera: {camera}, data label: {data.get('label') if data else 'None'}" @@ -485,6 +489,12 @@ class EmbeddingMaintainer(threading.Thread): for processor in self.post_processors: if isinstance(processor, ObjectDescriptionProcessor): + # skip end events — _process_finalized handles them via event_end_subscriber. + # processing them here can re-create tracked_events entries after cleanup + # when the event_subscriber queue is backlogged behind event_end_subscriber. + if event_type == EventStateEnum.end: + continue + processor.process_data( { "camera": camera, diff --git a/frigate/events/audio.py b/frigate/events/audio.py index 6a22b2251..c5e35a8d5 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -84,7 +84,6 @@ class AudioProcessor(FrigateProcess): def __init__( self, config: FrigateConfig, - cameras: list[CameraConfig], camera_metrics: DictProxy, stop_event: MpEvent, ): @@ -93,12 +92,11 @@ class AudioProcessor(FrigateProcess): ) self.camera_metrics = camera_metrics - self.cameras = cameras self.config = config def run(self) -> None: self.pre_run_setup(self.config.logger) - audio_threads: list[AudioEventMaintainer] = [] + audio_threads: dict[str, AudioEventMaintainer] = {} threading.current_thread().name = "process:audio_manager" @@ -112,32 +110,56 @@ class AudioProcessor(FrigateProcess): else: self.transcription_model_runner = None - if len(self.cameras) == 0: - return + config_subscriber = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.audio, + CameraConfigUpdateEnum.ffmpeg, + ], + ) - for camera in self.cameras: - audio_thread = AudioEventMaintainer( + def spawn_if_needed(camera: CameraConfig) -> None: + name = camera.name + if name is None or name in audio_threads: + return + if not camera.enabled or not camera.audio.enabled: + return + # ffmpeg update may not have arrived yet; wait for next poll + if not any("audio" in i.roles for i in camera.ffmpeg.inputs): + return + thread = AudioEventMaintainer( camera, self.config, self.camera_metrics, self.transcription_model_runner, self.stop_event, # type: ignore[arg-type] ) - audio_threads.append(audio_thread) - audio_thread.start() + audio_threads[name] = thread + thread.start() + self.logger.info(f"Audio maintainer started for {name}") + + for camera in self.config.cameras.values(): + spawn_if_needed(camera) self.logger.info(f"Audio processor started (pid: {self.pid})") - while not self.stop_event.wait(): - pass + # poll for newly added cameras or cameras flipped to audio.enabled at runtime + while not self.stop_event.wait(timeout=1.0): + config_subscriber.check_for_updates() + for camera in self.config.cameras.values(): + spawn_if_needed(camera) - for thread in audio_threads: + config_subscriber.stop() + + for thread in audio_threads.values(): thread.join(1) if thread.is_alive(): self.logger.info(f"Waiting for thread {thread.name:s} to exit") thread.join(10) - for thread in audio_threads: + for thread in audio_threads.values(): if thread.is_alive(): self.logger.warning(f"Thread {thread.name} is still alive") diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index 867db8d97..477e9a0db 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -62,8 +62,10 @@ def get_canvas_shape(width: int, height: int) -> tuple[int, int]: if round(a_w / a_h, 2) != round(width / height, 2): canvas_width = int(width // 4 * 4) canvas_height = int((canvas_width / a_w * a_h) // 4 * 4) - logger.warning( - f"The birdseye resolution is a non-standard aspect ratio, forcing birdseye resolution to {canvas_width} x {canvas_height}" + logger.error( + f"Birdseye resolution {width}x{height} is not a supported aspect ratio " + f"and may cause visual distortion; falling back to {canvas_width}x{canvas_height}. " + f"Set width and height to a supported aspect ratio (16:9, 20:10, 16:6, 32:9, 12:9, 22:15, 9:16, 9:12, 16:3, or 1:1)" ) return (canvas_width, canvas_height) @@ -796,15 +798,18 @@ class Birdseye: websocket_server: Any, ) -> None: self.config = config + canvas_width, canvas_height = get_canvas_shape( + config.birdseye.width, config.birdseye.height + ) self.input: queue.Queue[bytes] = queue.Queue(maxsize=10) self.converter = FFMpegConverter( config.ffmpeg, self.input, stop_event, - config.birdseye.width, - config.birdseye.height, - config.birdseye.width, - config.birdseye.height, + canvas_width, + canvas_height, + canvas_width, + canvas_height, config.birdseye.quality, config.birdseye.restream, ) diff --git a/frigate/util/config.py b/frigate/util/config.py index 60d440339..71e2af809 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -492,7 +492,7 @@ def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any] genai = new_config.get("genai") if genai and genai.get("provider"): - genai["roles"] = ["embeddings", "vision", "tools"] + genai["roles"] = ["embeddings", "descriptions", "chat"] new_config["genai"] = {"default": genai} # Remove deprecated sync_recordings from global record config diff --git a/generate_config_translations.py b/generate_config_translations.py index 032edb232..9bc830855 100644 --- a/generate_config_translations.py +++ b/generate_config_translations.py @@ -150,29 +150,51 @@ def extract_translations_from_schema( # Handle anyOf cases elif "anyOf" in field_schema: for item in field_schema["anyOf"]: + nested = None + if item.get("type") == "null": + continue if "properties" in item: nested = extract_translations_from_schema(item, defs=defs) + elif "$ref" in item: + ref_path = item["$ref"] + if ref_path.startswith("#/$defs/"): + ref_name = ref_path.split("/")[-1] + if ref_name in defs: + nested = extract_translations_from_schema( + defs[ref_name], defs=defs + ) + elif ( + "additionalProperties" in item + and isinstance(item["additionalProperties"], dict) + and "$ref" in item["additionalProperties"] + ): + ref_path = item["additionalProperties"]["$ref"] + if ref_path.startswith("#/$defs/"): + ref_name = ref_path.split("/")[-1] + if ref_name in defs: + nested = extract_translations_from_schema( + defs[ref_name], defs=defs + ) + elif ( + "items" in item + and isinstance(item["items"], dict) + and ("$ref" in item["items"]) + ): + ref_path = item["items"]["$ref"] + if ref_path.startswith("#/$defs/"): + ref_name = ref_path.split("/")[-1] + if ref_name in defs: + nested = extract_translations_from_schema( + defs[ref_name], defs=defs + ) + + if nested: nested_without_root = { k: v for k, v in nested.items() if k not in ("label", "description") } field_translations.update(nested_without_root) - elif "$ref" in item: - ref_path = item["$ref"] - if ref_path.startswith("#/$defs/"): - ref_name = ref_path.split("/")[-1] - if ref_name in defs: - ref_schema = defs[ref_name] - nested = extract_translations_from_schema( - ref_schema, defs=defs - ) - nested_without_root = { - k: v - for k, v in nested.items() - if k not in ("label", "description") - } - field_translations.update(nested_without_root) if field_translations: translations[field_name] = field_translations diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json index 65f981575..4f2c0ea01 100644 --- a/web/public/locales/en/config/cameras.json +++ b/web/public/locales/en/config/cameras.json @@ -33,7 +33,11 @@ }, "filters": { "label": "Audio filters", - "description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives." + "description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives.", + "threshold": { + "label": "Minimum audio confidence", + "description": "Minimum confidence threshold for the audio event to be counted." + } }, "enabled_in_config": { "label": "Original audio state", diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index 8efecd0ce..61f0e41cc 100644 --- a/web/public/locales/en/config/global.json +++ b/web/public/locales/en/config/global.json @@ -559,7 +559,11 @@ }, "filters": { "label": "Audio filters", - "description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives." + "description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives.", + "threshold": { + "label": "Minimum audio confidence", + "description": "Minimum confidence threshold for the audio event to be counted." + } }, "enabled_in_config": { "label": "Original audio state", diff --git a/web/public/locales/en/views/faceLibrary.json b/web/public/locales/en/views/faceLibrary.json index 4f8a9cf2d..27e545460 100644 --- a/web/public/locales/en/views/faceLibrary.json +++ b/web/public/locales/en/views/faceLibrary.json @@ -32,7 +32,11 @@ "title": "Recent Recognitions", "titleShort": "Recent", "aria": "Select recent recognitions", - "empty": "There are no recent face recognition attempts" + "empty": "There are no recent face recognition attempts", + "emptyNoLibrary": { + "title": "Upload a face", + "description": "You must add at least one face to the library for face recognition to function." + } }, "deleteFaceLibrary": { "title": "Delete Name", diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index b47b6ba39..b73534d5d 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -446,6 +446,7 @@ }, "cameraManagement": { "title": "Manage Cameras", + "description": "Add, edit, and delete cameras, control which cameras are enabled, and configure per-profile and camera type overrides. To configure streams, detection, motion, and other camera-specific settings, choose the specific section under Camera Configuration.", "addCamera": "Add New Camera", "deleteCamera": "Delete Camera", "deleteCameraDialog": { @@ -1127,8 +1128,16 @@ "cameras": "Cameras", "loading": "Loading model information…", "error": "Failed to load model information", + "noModelLoaded": "No Frigate+ model is currently loaded.", "availableModels": "Available Models", "loadingAvailableModels": "Loading available models…", + "selectModel": "Select a model", + "noModelsAvailable": "No models available", + "filter": { + "ariaLabel": "Filter models by type", + "baseModels": "Base Models", + "fineTunedModels": "Fine-tuned Models" + }, "modelSelect": "Your available models on Frigate+ can be selected here. Note that only models compatible with your current detector configuration can be selected." }, "unsavedChanges": "Unsaved Frigate+ settings changes", @@ -1744,4 +1753,4 @@ "jinav2SmallModelSize": "The 'small' size with the Jina V2 model has high RAM and inference cost. The 'large' model with a discrete GPU is recommended." } } -} \ No newline at end of file +} diff --git a/web/public/locales/nb_NO/views/chat.json b/web/public/locales/nb-NO/views/chat.json similarity index 100% rename from web/public/locales/nb_NO/views/chat.json rename to web/public/locales/nb-NO/views/chat.json diff --git a/web/public/locales/nb_NO/views/motionSearch.json b/web/public/locales/nb-NO/views/motionSearch.json similarity index 100% rename from web/public/locales/nb_NO/views/motionSearch.json rename to web/public/locales/nb-NO/views/motionSearch.json diff --git a/web/public/locales/nb_NO/views/replay.json b/web/public/locales/nb-NO/views/replay.json similarity index 100% rename from web/public/locales/nb_NO/views/replay.json rename to web/public/locales/nb-NO/views/replay.json diff --git a/web/public/locales/pt_BR/views/chat.json b/web/public/locales/pt-BR/views/chat.json similarity index 100% rename from web/public/locales/pt_BR/views/chat.json rename to web/public/locales/pt-BR/views/chat.json diff --git a/web/public/locales/pt_BR/views/motionSearch.json b/web/public/locales/pt-BR/views/motionSearch.json similarity index 100% rename from web/public/locales/pt_BR/views/motionSearch.json rename to web/public/locales/pt-BR/views/motionSearch.json diff --git a/web/public/locales/pt_BR/views/replay.json b/web/public/locales/pt-BR/views/replay.json similarity index 100% rename from web/public/locales/pt_BR/views/replay.json rename to web/public/locales/pt-BR/views/replay.json diff --git a/web/public/locales/yue_Hant/views/chat.json b/web/public/locales/yue-Hant/views/chat.json similarity index 100% rename from web/public/locales/yue_Hant/views/chat.json rename to web/public/locales/yue-Hant/views/chat.json diff --git a/web/public/locales/yue_Hant/views/motionSearch.json b/web/public/locales/yue-Hant/views/motionSearch.json similarity index 100% rename from web/public/locales/yue_Hant/views/motionSearch.json rename to web/public/locales/yue-Hant/views/motionSearch.json diff --git a/web/public/locales/yue_Hant/views/replay.json b/web/public/locales/yue-Hant/views/replay.json similarity index 100% rename from web/public/locales/yue_Hant/views/replay.json rename to web/public/locales/yue-Hant/views/replay.json diff --git a/web/public/locales/zh_Hans/views/chat.json b/web/public/locales/zh-CN/views/chat.json similarity index 100% rename from web/public/locales/zh_Hans/views/chat.json rename to web/public/locales/zh-CN/views/chat.json diff --git a/web/public/locales/zh-Hans/views/motionSearch.json b/web/public/locales/zh-CN/views/motionSearch.json similarity index 100% rename from web/public/locales/zh-Hans/views/motionSearch.json rename to web/public/locales/zh-CN/views/motionSearch.json diff --git a/web/public/locales/zh-Hans/views/replay.json b/web/public/locales/zh-CN/views/replay.json similarity index 100% rename from web/public/locales/zh-Hans/views/replay.json rename to web/public/locales/zh-CN/views/replay.json diff --git a/web/public/locales/zh_Hant/views/chat.json b/web/public/locales/zh-Hant/views/chat.json similarity index 100% rename from web/public/locales/zh_Hant/views/chat.json rename to web/public/locales/zh-Hant/views/chat.json diff --git a/web/public/locales/zh_Hant/views/motionSearch.json b/web/public/locales/zh-Hant/views/motionSearch.json similarity index 100% rename from web/public/locales/zh_Hant/views/motionSearch.json rename to web/public/locales/zh-Hant/views/motionSearch.json diff --git a/web/public/locales/zh_Hant/views/replay.json b/web/public/locales/zh-Hant/views/replay.json similarity index 100% rename from web/public/locales/zh_Hant/views/replay.json rename to web/public/locales/zh-Hant/views/replay.json diff --git a/web/src/components/card/EmptyCard.tsx b/web/src/components/card/EmptyCard.tsx index b9943b31a..5495a6e50 100644 --- a/web/src/components/card/EmptyCard.tsx +++ b/web/src/components/card/EmptyCard.tsx @@ -12,6 +12,7 @@ type EmptyCardProps = { description?: string; buttonText?: string; link?: string; + onClick?: () => void; }; export function EmptyCard({ className, @@ -21,6 +22,7 @@ export function EmptyCard({ description, buttonText, link, + onClick, }: EmptyCardProps) { let TitleComponent; @@ -39,11 +41,16 @@ export function EmptyCard({ {description} )} - {buttonText?.length && ( - - )} + {buttonText?.length && + (onClick ? ( + + ) : ( + + ))} ); } diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx index 0affaff07..2760550b8 100644 --- a/web/src/components/settings/ZoneEditPane.tsx +++ b/web/src/components/settings/ZoneEditPane.tsx @@ -528,7 +528,7 @@ export default function ZoneEditPane({ ); updateConfig(); // Only publish WS state for base config when zone has a name - if (!editingProfile && zoneName) { + if (!editingProfile && polygon?.name) { sendZoneState(enabled ? "ON" : "OFF"); } } else { diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index 20763261c..a57196327 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -1,5 +1,6 @@ import AddFaceIcon from "@/components/icons/AddFaceIcon"; import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { EmptyCard } from "@/components/card/EmptyCard"; import CreateFaceWizardDialog from "@/components/overlay/detail/FaceCreateWizardDialog"; import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog"; import UploadImageDialog from "@/components/overlay/dialog/UploadImageDialog"; @@ -473,7 +474,9 @@ export default function FaceLibrary() { attemptImages={trainImages} faceNames={faces} selectedFaces={selectedFaces} + isLoading={faceData === undefined} onClickFaces={onClickFaces} + onAddFace={() => setAddFace(true)} onRefresh={refreshFaces} /> ) : ( @@ -691,7 +694,9 @@ type TrainingGridProps = { attemptImages: string[]; faceNames: string[]; selectedFaces: string[]; + isLoading: boolean; onClickFaces: (images: string[], ctrl: boolean) => void; + onAddFace: () => void; onRefresh: ( data?: | FaceLibraryData @@ -708,7 +713,9 @@ function TrainingGrid({ attemptImages, faceNames, selectedFaces, + isLoading, onClickFaces, + onAddFace, onRefresh, }: TrainingGridProps) { const { t } = useTranslation(["views/faceLibrary"]); @@ -762,6 +769,25 @@ function TrainingGrid({ ]); if (attemptImages.length == 0) { + if (isLoading) { + return ( + + ); + } + + if (faceNames.length == 0) { + return ( + } + title={t("train.emptyNoLibrary.title")} + description={t("train.emptyNoLibrary.description")} + buttonText={t("button.addFace")} + onClick={onAddFace} + /> + ); + } + return (
diff --git a/web/src/views/settings/CameraManagementView.tsx b/web/src/views/settings/CameraManagementView.tsx index 157f3d158..6233b232c 100644 --- a/web/src/views/settings/CameraManagementView.tsx +++ b/web/src/views/settings/CameraManagementView.tsx @@ -122,9 +122,12 @@ export default function CameraManagementView({
{viewMode === "settings" ? ( <> - + {t("cameraManagement.title")} +

+ {t("cameraManagement.description")} +

diff --git a/web/src/views/settings/FrigatePlusSettingsView.tsx b/web/src/views/settings/FrigatePlusSettingsView.tsx index 4beeeea36..7bc81fae2 100644 --- a/web/src/views/settings/FrigatePlusSettingsView.tsx +++ b/web/src/views/settings/FrigatePlusSettingsView.tsx @@ -1,5 +1,5 @@ import Heading from "@/components/ui/heading"; -import { useCallback, useContext, useEffect, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { Toaster } from "@/components/ui/sonner"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { toast } from "sonner"; @@ -10,7 +10,7 @@ import { CheckCircle2, XCircle } from "lucide-react"; import { Trans, useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Link } from "react-router-dom"; -import { LuExternalLink } from "react-icons/lu"; +import { LuExternalLink, LuFilter } from "react-icons/lu"; import { StatusBarMessagesContext } from "@/context/statusbar-provider"; import { Select, @@ -19,6 +19,14 @@ import { SelectItem, SelectTrigger, } from "@/components/ui/select"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { cn } from "@/lib/utils"; import { useDocDomain } from "@/hooks/use-doc-domain"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { @@ -26,6 +34,8 @@ import { SplitCardRow, } from "@/components/card/SettingsGroupCard"; import FrigatePlusCurrentModelSummary from "@/views/settings/components/FrigatePlusCurrentModelSummary"; +import { useRestart } from "@/api/ws"; +import RestartDialog from "@/components/overlay/dialog/RestartDialog"; type FrigatePlusModel = { id: string; @@ -58,6 +68,8 @@ export default function FrigatePlusSettingsView({ useSWR("config"); const [changedValue, setChangedValue] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [restartDialogOpen, setRestartDialogOpen] = useState(false); + const { send: sendRestart } = useRestart(); const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; @@ -76,7 +88,7 @@ export default function FrigatePlusSettingsView({ }, ); - const { data: availableModels = {} } = useSWR< + const { data: availableModels = {}, isLoading: isLoadingModels } = useSWR< Record >("/plus/models", { fallbackData: {}, @@ -92,6 +104,19 @@ export default function FrigatePlusSettingsView({ }, }); + const [showBaseModels, setShowBaseModels] = useState(true); + const [showFineTunedModels, setShowFineTunedModels] = useState(true); + + const filteredModelEntries = useMemo( + () => + Object.entries(availableModels || {}).filter(([, model]) => + model.isBaseModel ? showBaseModels : showFineTunedModels, + ), + [availableModels, showBaseModels, showFineTunedModels], + ); + + const isFilterActive = !showBaseModels || !showFineTunedModels; + useEffect(() => { if (config) { if (frigatePlusSettings?.model.id == undefined) { @@ -128,47 +153,60 @@ export default function FrigatePlusSettingsView({ const saveToConfig = useCallback(async () => { setIsLoading(true); - axios - .put(`config/set?model.path=plus://${frigatePlusSettings.model.id}`, { + try { + // Clear the existing model section so only the new path remains + await axios.put("config/set", { requires_restart: 0, - }) - .then((res) => { - if (res.status === 200) { - toast.success(t("frigatePlus.toast.success"), { - position: "top-center", - }); - setChangedValue(false); - updateConfig(); - } else { - toast.error( - t("frigatePlus.toast.error", { errorMessage: res.statusText }), - { - position: "top-center", - }, - ); - } - }) - .catch((error) => { - const errorMessage = - error.response?.data?.message || - error.response?.data?.detail || - "Unknown error"; + config_data: { model: null }, + }); + const res = await axios.put("config/set", { + requires_restart: 0, + config_data: { + model: { path: `plus://${frigatePlusSettings.model.id}` }, + }, + }); + + if (res.status === 200) { + toast.success(t("frigatePlus.toast.success"), { + position: "top-center", + action: ( + setRestartDialogOpen(true)}> + + + ), + }); + setChangedValue(false); + updateConfig(); + } else { toast.error( - t("toast.save.error.title", { errorMessage, ns: "common" }), + t("frigatePlus.toast.error", { errorMessage: res.statusText }), { position: "top-center", }, ); - }) - .finally(() => { - addMessage( - "plus_restart", - t("frigatePlus.restart_required"), - undefined, - "plus_restart", - ); - setIsLoading(false); + } + } catch (error) { + const err = error as { + response?: { data?: { message?: string; detail?: string } }; + }; + const errorMessage = + err.response?.data?.message || + err.response?.data?.detail || + "Unknown error"; + toast.error(t("toast.save.error.title", { errorMessage, ns: "common" }), { + position: "top-center", }); + } finally { + addMessage( + "plus_restart", + t("frigatePlus.restart_required"), + undefined, + "plus_restart", + ); + setIsLoading(false); + } }, [updateConfig, addMessage, frigatePlusSettings, t]); const onCancel = useCallback(() => { @@ -201,274 +239,340 @@ export default function FrigatePlusSettingsView({ } return ( -
+
-
-
-
-
- - {t("frigatePlus.title")} - +
+
+ + {t("frigatePlus.title")} + -

- {t("frigatePlus.description")} -

-
+

+ {t("frigatePlus.description")} +

+
-
- - -

{t("frigatePlus.apiKey.desc")}

- {!config?.model.plus && ( -
- - {t("frigatePlus.apiKey.plusLink")} - - -
- )} - - } - content={ -
- {config?.plus?.enabled ? ( - - ) : ( - - )} - - {config?.plus?.enabled - ? t("frigatePlus.apiKey.validated") - : t("frigatePlus.apiKey.notValidated")} - -
- } - /> -
- - {config?.model.plus && ( - - )} - - {config?.model.plus && ( - - - frigatePlus.modelInfo.modelSelect - - } - content={ - + handleFrigatePlusConfigChange({ + model: { id: value as string }, + }) + } + > + {frigatePlusSettings.model.id && - availableModels?.[frigatePlusSettings.model.id] ? ( - - {new Date( + availableModels?.[frigatePlusSettings.model.id] + ? new Date( availableModels[ frigatePlusSettings.model.id ].trainDate, ).toLocaleString() + - " " + - availableModels[frigatePlusSettings.model.id] - .baseModel + - " (" + - (availableModels[frigatePlusSettings.model.id] - .isBaseModel - ? t( - "frigatePlus.modelInfo.plusModelType.baseModel", + " " + + availableModels[frigatePlusSettings.model.id] + .baseModel + + " (" + + (availableModels[frigatePlusSettings.model.id] + .isBaseModel + ? t( + "frigatePlus.modelInfo.plusModelType.baseModel", + ) + : t( + "frigatePlus.modelInfo.plusModelType.userModel", + )) + + ") " + + availableModels[frigatePlusSettings.model.id].name + + " (" + + availableModels[frigatePlusSettings.model.id] + .width + + "x" + + availableModels[frigatePlusSettings.model.id] + .height + + ")" + : isLoadingModels + ? t("frigatePlus.modelInfo.loadingAvailableModels") + : t("frigatePlus.modelInfo.selectModel")} + + + + + {filteredModelEntries.length === 0 ? ( +
+ {t("frigatePlus.modelInfo.noModelsAvailable")} +
+ ) : ( + filteredModelEntries.map(([id, model]) => ( + - ) : ( - - {t("frigatePlus.modelInfo.loadingAvailableModels")} - - )} - - - - {Object.entries(availableModels || {}).map( - ([id, model]) => ( - - {new Date(model.trainDate).toLocaleString()}{" "} -
- {model.baseModel} {" ("} - {model.isBaseModel - ? t( - "frigatePlus.modelInfo.plusModelType.baseModel", - ) - : t( - "frigatePlus.modelInfo.plusModelType.userModel", - )} - {")"} -
-
- {model.name} ( - {model.width + "x" + model.height}) -
-
- {t( - "frigatePlus.modelInfo.supportedDetectors", - )} - : {model.supportedDetectors.join(", ")} -
-
- {id} -
-
- ), - )} -
-
- - } - /> -
- )} - - - -

- - frigatePlus.snapshotConfig.desc - -

-
- + {new Date(model.trainDate).toLocaleString()}{" "} +
+ {model.baseModel} {" ("} + {model.isBaseModel + ? t( + "frigatePlus.modelInfo.plusModelType.baseModel", + ) + : t( + "frigatePlus.modelInfo.plusModelType.userModel", + )} + {")"} +
+
+ {model.name} ( + {model.width + "x" + model.height}) +
+
+ {t( + "frigatePlus.modelInfo.supportedDetectors", + )} + : {model.supportedDetectors.join(", ")} +
+
+ {id} +
+ + )) + )} + + + + + +
- - } - content={ -
-
- - - - - - - - - {Object.entries(config.cameras).map( - ([name, camera]) => ( - - - - - ), + -
- {t("frigatePlus.snapshotConfig.table.camera")} - - {t( - "frigatePlus.snapshotConfig.table.snapshots", - )} -
- - - {camera.snapshots.enabled ? ( - - ) : ( - - )} -
-
-
- } - /> -
-
-
-
+ /> + + + +
+
+ {t("frigatePlus.modelInfo.filter.ariaLabel")} +
+
+ + +
+
+ + +
+
+
+ +
+ } + /> + + )} -
-
-
- - + + +

+ + frigatePlus.snapshotConfig.desc + +

+
+ + {t("readTheDocumentation", { ns: "common" })} + + +
+ + } + content={ +
+
+ + + + + + + + + {Object.entries(config.cameras).map( + ([name, camera]) => ( + + + + + ), + )} + +
+ {t("frigatePlus.snapshotConfig.table.camera")} + + {t("frigatePlus.snapshotConfig.table.snapshots")} +
+ + + {camera.snapshots.enabled ? ( + + ) : ( + + )} +
+
+
+ } + /> +
+
+
+ +
+
+ {changedValue && ( +
+ + {t("unsavedChanges")} +
+ )} +
+ {changedValue && ( + + )} +
+ setRestartDialogOpen(false)} + onRestart={() => sendRestart("restart")} + />
); } diff --git a/web/src/views/settings/Go2RtcStreamsSettingsView.tsx b/web/src/views/settings/Go2RtcStreamsSettingsView.tsx index fbc55a24b..1d5781ad0 100644 --- a/web/src/views/settings/Go2RtcStreamsSettingsView.tsx +++ b/web/src/views/settings/Go2RtcStreamsSettingsView.tsx @@ -385,7 +385,7 @@ export default function Go2RtcStreamsSettingsView({
)} -
+
{hasChanges && (