diff --git a/docs/docs/configuration/advanced/system.md b/docs/docs/configuration/advanced/system.md index 14979698c7..28291efff7 100644 --- a/docs/docs/configuration/advanced/system.md +++ b/docs/docs/configuration/advanced/system.md @@ -67,6 +67,12 @@ This section can be used to set environment variables for those unable to modify Variables prefixed with `FRIGATE_` can be referenced in config fields that support environment variable substitution (such as MQTT host and credentials, camera stream URLs, and ONVIF host and credentials) using the `{FRIGATE_VARIABLE_NAME}` syntax. +:::note + +The `go2rtc` section is an exception. go2rtc runs as a separate process, so its stream definitions can only be substituted with variables that exist in the container's environment (set via Docker `-e`, the `environment:` section of `docker-compose.yml`, or Docker secrets). Variables defined in the `environment_vars` block above are not available to go2rtc streams. Home Assistant app users, who cannot set container environment variables, must instead put credentials directly in their go2rtc stream URLs. + +::: + diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index a47c9285e5..5df70844c9 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -6,6 +6,7 @@ title: Live View import ConfigTabs from "@site/src/components/ConfigTabs"; import TabItem from "@theme/TabItem"; import NavPath from "@site/src/components/NavPath"; +import FaqItem from "@site/src/components/FaqItem"; Frigate intelligently displays your camera streams on the Live view dashboard. By default, Frigate employs "smart streaming" where camera images update once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any motion or active objects are detected, cameras seamlessly switch to a live stream. @@ -341,100 +342,155 @@ When your browser runs into problems playing back your camera streams, it will l ## Live view FAQ -1. **Why don't I have audio in my Live view?** +### Getting Live View Working - You must use go2rtc to hear audio in your live streams. If you have go2rtc already configured, you need to ensure your camera is sending PCMA/PCMU or AAC audio. If you can't change your camera's audio codec, you need to [transcode the audio](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#source-ffmpeg) using go2rtc. + - Note that the low bandwidth mode player is a video-only stream. You should not expect to hear audio when in low bandwidth mode, even if you've set up go2rtc. +You must use go2rtc to hear audio in your live streams. If you have go2rtc already configured, you need to ensure your camera is sending PCMA/PCMU or AAC audio. If you can't change your camera's audio codec, you need to [transcode the audio](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#source-ffmpeg) using go2rtc. -2. **Frigate shows that my live stream is in "low bandwidth mode". What does this mean?** +If the audio controls don't appear in the UI at all, verify that the Live view is actually using your go2rtc stream. If your go2rtc stream names don't match your Frigate camera name, you must map them with the `live -> streams` config (see [Setting Streams For Live UI](#setting-streams-for-live-ui) above); otherwise the UI falls back to the video-only jsmpeg player. - Frigate intelligently selects the live streaming technology based on a number of factors (user-selected modes like two-way talk, camera settings, browser capabilities, available bandwidth) and prioritizes showing an actual up-to-date live view of your camera's stream as quickly as possible. +Note that the low bandwidth mode player is a video-only stream. You should not expect to hear audio when in low bandwidth mode, even if you've set up go2rtc. - When you have go2rtc configured, Live view initially attempts to load and play back your stream with a clearer, fluent stream technology (MSE). An initial timeout, a low bandwidth condition that would cause buffering of the stream, or decoding errors in the stream will cause Frigate to switch to the stream defined by the `detect` role, using the jsmpeg format. This is what the UI labels as "low bandwidth mode". On Live dashboards, the mode will automatically reset when smart streaming is configured and activity stops. Continuous streaming mode does not have an automatic reset mechanism, but you can use the _Reset_ option to force a reload of your stream. + - If you are using continuous streaming or you are loading more than a few high resolution streams at once on the dashboard, your browser may struggle to begin playback of your streams before the timeout. Frigate always prioritizes showing a live stream as quickly as possible, even if it is a lower quality jsmpeg stream. You can use the "Reset" link/button to try loading your high resolution stream again. + - Errors in stream playback (e.g., connection failures, codec issues, or buffering timeouts) that cause the fallback to low bandwidth mode (jsmpeg) are logged to the browser console for easier debugging. These errors may include: - - Network issues (e.g., MSE or WebRTC network connection problems). - - Unsupported codecs or stream formats (e.g., H.265 in WebRTC, which is not supported in some browsers). - - Buffering timeouts or low bandwidth conditions causing fallback to jsmpeg. - - Browser compatibility problems (e.g., iOS Safari limitations with MSE). +If your camera is streaming (as indicated by a red dot in the upper right, or if it has been set to continuous streaming mode), your browser may be blocking audio until you interact with the page. This is an intentional browser limitation. See [this article](https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_availability). Many browsers have a whitelist feature to change this behavior. - To view browser console logs: - 1. Open the Frigate Live View in your browser. - 2. Open the browser's Developer Tools (F12 or right-click > Inspect > Console tab). - 3. Reproduce the error (e.g., load a problematic stream or simulate network issues). - 4. Look for messages prefixed with the camera name. + - These logs help identify if the issue is player-specific (MSE vs. WebRTC) or related to camera configuration (e.g., go2rtc streams, codecs). If you see frequent errors: - - Verify your camera's H.264/AAC settings (see [Frigate's camera settings recommendations](#camera-settings-recommendations)). - - Check go2rtc configuration for transcoding (e.g., audio to AAC/OPUS). - - Test with a different stream via the UI dropdown (if `live -> streams` is configured). - - For WebRTC-specific issues, ensure port 8555 is forwarded and candidates are set (see [WebRTC Extra Configuration](#webrtc-extra-configuration)). - - If your cameras are streaming at a high resolution, your browser may be struggling to load all of the streams before the buffering timeout occurs. Frigate prioritizes showing a true live view as quickly as possible. If the fallback occurs often, change your live view settings to use a lower bandwidth substream. + -3. **It doesn't seem like my cameras are streaming on the Live dashboard. Why?** +The debug view plays the `detect` stream processed by Frigate itself, while the Live view plays your go2rtc stream directly in the browser. If the debug view works but the Live view doesn't, your browser usually can't decode what the camera is sending, most often H.265 video or an incompatible audio track. - On the default Live dashboard ("All Cameras"), your camera images will update once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity is detected, cameras seamlessly switch to a full-resolution live stream. If you want to customize this behavior, use a camera group. +Work through the [go2rtc troubleshooting guide](/troubleshooting/go2rtc#live-view-is-black-buffering-or-stuck-in-low-bandwidth-mode) to isolate the problem. Two fixes resolve the majority of cases: -4. **I see a strange diagonal line on my live view, but my recordings look fine. How can I fix it?** +1. Restream through go2rtc's FFmpeg module by prefixing your source with `ffmpeg:`, for example `- ffmpeg:rtsp://user:password@192.168.1.5:554/stream`. +2. If that doesn't help, transcode to compatible codecs: `- ffmpeg:rtsp://user:password@192.168.1.5:554/stream#video=h264#audio=aac#hardware`. - This is caused by incorrect dimensions set in your detect width or height (or incorrectly auto-detected), causing the jsmpeg player's rendering engine to display a slightly distorted image. You should enlarge the width and height of your `detect` resolution up to a standard aspect ratio (example: 640x352 becomes 640x360, and 800x443 becomes 800x450, 2688x1520 becomes 2688x1512, etc). If changing the resolution to match a standard (4:3, 16:9, or 32:9, etc) aspect ratio does not solve the issue, you can enable "compatibility mode" in your camera group dashboard's stream settings. Depending on your browser and device, more than a few cameras in compatibility mode may not be supported, so only use this option if changing your `detect` width and height fails to resolve the color artifacts and diagonal line. + -5. **How does "smart streaming" work?** + - Because a static image of a scene looks exactly the same as a live stream with no motion or activity, smart streaming updates your camera images once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity (motion or object/audio detection) occurs, cameras seamlessly switch to a live stream. +For a full-resolution, low-latency live view in Home Assistant dashboards, use the [Advanced Camera Card](https://card.camera) with the [go2rtc live provider](https://card.camera/#/configuration/cameras/live-provider?id=go2rtc), which streams directly from Frigate's bundled go2rtc. This also supports audio and [two-way talk](#two-way-talk) on capable cameras. See the [Home Assistant integration docs](/integrations/home-assistant) for setup. - This static image is pulled from the stream defined in your config with the `detect` role. When activity is detected, images from the `detect` stream immediately begin updating at ~5 frames per second so you can see the activity until the live player is loaded and begins playing. This usually only takes a second or two. If the live player times out, buffers, or has streaming errors, the jsmpeg player is loaded and plays a video-only stream from the `detect` role. When activity ends, the players are destroyed and a static image is displayed until activity is detected again, and the process repeats. + - Smart streaming depends on having your camera's motion `threshold` and `contour_area` config values dialed in. Use the Motion Tuner in Settings in the UI to tune these values in real-time. +### Streaming Behavior - This is Frigate's default and recommended setting because it results in a significant bandwidth savings, especially for high resolution cameras. + -6. **I have unmuted some cameras on my dashboard, but I do not hear sound. Why?** +Because a static image of a scene looks exactly the same as a live stream with no motion or activity, smart streaming updates your camera images once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity (motion or object/audio detection) occurs, cameras seamlessly switch to a live stream. - If your camera is streaming (as indicated by a red dot in the upper right, or if it has been set to continuous streaming mode), your browser may be blocking audio until you interact with the page. This is an intentional browser limitation. See [this article](https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_availability). Many browsers have a whitelist feature to change this behavior. +This static image is pulled from the stream defined in your config with the `detect` role. When activity is detected, images from the `detect` stream immediately begin updating at ~5 frames per second so you can see the activity until the live player is loaded and begins playing. This usually only takes a second or two. If the live player times out, buffers, or has streaming errors, the jsmpeg player is loaded and plays a video-only stream from the `detect` role. When activity ends, the players are destroyed and a static image is displayed until activity is detected again, and the process repeats. -7. **My camera streams have lots of visual artifacts / distortion.** +Smart streaming depends on having your camera's motion `threshold` and `contour_area` config values dialed in. Use the Motion Tuner in Settings in the UI to tune these values in real-time. - Some cameras don't include the hardware to support multiple connections to the high resolution stream, and this can cause unexpected behavior. In this case it is recommended to [restream](./restream.md) the high resolution stream so that it can be used for live view and recordings. +This is Frigate's default and recommended setting because it results in a significant bandwidth savings, especially for high resolution cameras. -8. **Why does my camera stream switch aspect ratios on the Live dashboard?** + - Your camera may change aspect ratios on the dashboard because Frigate uses different streams for different purposes. With go2rtc and Smart Streaming, Frigate shows a static image from the `detect` stream when no activity is present, and switches to the live stream when motion is detected. The camera image will change size if your streams use different aspect ratios. + - To prevent this, make the `detect` stream match the go2rtc live stream's aspect ratio (resolution does not need to match, just the aspect ratio). You can either adjust the camera's output resolution or set the `width` and `height` values in your config's `detect` section to a resolution with an aspect ratio that matches. +On the default Live dashboard ("All Cameras"), your camera images will update once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity is detected, cameras seamlessly switch to a full-resolution live stream. If you want to customize this behavior, use a camera group. - Example: Resolutions from two streams - - Mismatched (may cause aspect ratio switching on the dashboard): - - Live/go2rtc stream: 1920x1080 (16:9) - - Detect stream: 640x352 (~1.82:1, not 16:9) + - - Matched (prevents switching): - - Live/go2rtc stream: 1920x1080 (16:9) - - Detect stream: 640x360 (16:9) + - You can update the detect settings in your camera config to match the aspect ratio of your go2rtc live stream. For example: +Frigate intelligently selects the live streaming technology based on a number of factors (user-selected modes like two-way talk, camera settings, browser capabilities, available bandwidth) and prioritizes showing an actual up-to-date live view of your camera's stream as quickly as possible. - ```yaml - cameras: - front_door: - detect: - width: 640 - height: 360 # set this to 360 instead of 352 - ffmpeg: - inputs: - - path: rtsp://127.0.0.1:8554/front_door # main stream 1920x1080 - roles: - - record - - path: rtsp://127.0.0.1:8554/front_door_sub # sub stream 640x352 - roles: - - detect - ``` +When you have go2rtc configured, Live view initially attempts to load and play back your stream with a clearer, fluent stream technology (MSE). An initial timeout, a low bandwidth condition that would cause buffering of the stream, or decoding errors in the stream will cause Frigate to switch to the stream defined by the `detect` role, using the jsmpeg format. This is what the UI labels as "low bandwidth mode". On Live dashboards, the mode will automatically reset when smart streaming is configured and activity stops. Continuous streaming mode does not have an automatic reset mechanism, but you can use the _Reset_ option to force a reload of your stream. - The same applies to your `record` stream: if its aspect ratio differs from your `detect` stream, your recordings will appear in a different shape than the live view. For consistent framing across live view and recordings, use the same aspect ratio for all of a camera's streams (the resolution can still differ). +If you are using continuous streaming or you are loading more than a few high resolution streams at once on the dashboard, your browser may struggle to begin playback of your streams before the timeout. Frigate always prioritizes showing a live stream as quickly as possible, even if it is a lower quality jsmpeg stream. You can use the "Reset" link/button to try loading your high resolution stream again. -9. **Why does Frigate prefer MSE over WebRTC for live view?** +Errors in stream playback (e.g., connection failures, codec issues, or buffering timeouts) that cause the fallback to low bandwidth mode (jsmpeg) are logged to the browser console for easier debugging. These errors may include: - Frigate prefers MSE because it delivers a better out-of-the-box experience than WebRTC on nearly every axis that matters for a security camera system. MSE is an open standard optimized and supported by all modern browsers, works without any extra configuration (WebRTC requires port forwarding and candidate setup, and lacks H.265 support in some browsers), and requires no internet access for NAT traversal. More importantly, MSE runs over TCP, so every frame arrives and is decoded in order, so nothing is ever silently skipped. WebRTC optimizes for latency over UDP by discarding late or incomplete frames, which works against you on cellular or spotty Wi-Fi: you can end up with frozen video, visual corruption, or gaps in the feed without ever knowing you missed something. Frigate's enhanced MSE player has adaptive speed playback and has been tuned for latency and connection robustness that meets or exceeds WebRTC, so you get near-real-time playback with a guarantee that when the video plays, every frame is actually there - which, for an NVR whose whole purpose is letting you see what happened, matters more than shaving fractions of a second off a latency number. That's why Frigate defaults to MSE and reserves WebRTC for cases that require it, like two-way talk. +- Network issues (e.g., MSE or WebRTC network connection problems). +- Unsupported codecs or stream formats (e.g., H.265 in WebRTC, which is not supported in some browsers). +- Buffering timeouts or low bandwidth conditions causing fallback to jsmpeg. +- Browser compatibility problems (e.g., iOS Safari limitations with MSE). + +To view browser console logs: + +1. Open the Frigate Live View in your browser. +2. Open the browser's Developer Tools (F12 or right-click > Inspect > Console tab). +3. Reproduce the error (e.g., load a problematic stream or simulate network issues). +4. Look for messages prefixed with the camera name. + +These logs help identify if the issue is player-specific (MSE vs. WebRTC) or related to camera configuration (e.g., go2rtc streams, codecs). If you see frequent errors: + +- Verify your camera's H.264/AAC settings (see [Frigate's camera settings recommendations](#camera-settings-recommendations)). +- Check go2rtc configuration for transcoding (e.g., audio to AAC/OPUS). +- Test with a different stream via the UI dropdown (if `live -> streams` is configured). +- For WebRTC-specific issues, ensure port 8555 is forwarded and candidates are set (see [WebRTC Extra Configuration](#webrtc-extra-configuration)). +- If your cameras are streaming at a high resolution, your browser may be struggling to load all of the streams before the buffering timeout occurs. Frigate prioritizes showing a true live view as quickly as possible. If the fallback occurs often, change your live view settings to use a lower bandwidth substream. + + + + + +A delay when a stream first starts is usually caused by your camera's I-frame (keyframe) interval. Playback cannot begin until a keyframe arrives, so an interval set higher than your camera's frame rate makes the stream take longer to start. Set the I-frame interval to match the frame rate (or "1x" on Reolink) per the [camera settings recommendations](#camera-settings-recommendations). + +A stream that starts on time but falls further behind live is buffering, which is usually the browser struggling to decode too many high-resolution streams at once. Select a lower-bandwidth substream for your dashboards (see [Setting Streams For Live UI](#setting-streams-for-live-ui)), reduce the number of streams open at once, or improve the network connection between your browser and Frigate. Frigate's player automatically speeds up playback to catch up to live after buffering, and falls back to low bandwidth mode if it stalls for too long. The _Reset_ option forces a fresh connection at the live edge. + + + + + +Frigate prefers MSE because it delivers a better out-of-the-box experience than WebRTC on nearly every axis that matters for a security camera system. MSE is an open standard optimized and supported by all modern browsers, works without any extra configuration (WebRTC requires port forwarding and candidate setup, and lacks H.265 support in some browsers), and requires no internet access for NAT traversal. More importantly, MSE runs over TCP, so every frame arrives and is decoded in order, so nothing is ever silently skipped. WebRTC optimizes for latency over UDP by discarding late or incomplete frames, which works against you on cellular or spotty Wi-Fi: you can end up with frozen video, visual corruption, or gaps in the feed without ever knowing you missed something. Frigate's enhanced MSE player has adaptive speed playback and has been tuned for latency and connection robustness that meets or exceeds WebRTC, so you get near-real-time playback with a guarantee that when the video plays, every frame is actually there - which, for an NVR whose whole purpose is letting you see what happened, matters more than shaving fractions of a second off a latency number. That's why Frigate defaults to MSE and reserves WebRTC for cases that require it, like two-way talk. + + + +### Video Quality Issues + + + +This is caused by incorrect dimensions set in your detect width or height (or incorrectly auto-detected), causing the jsmpeg player's rendering engine to display a slightly distorted image. You should enlarge the width and height of your `detect` resolution up to a standard aspect ratio (example: 640x352 becomes 640x360, and 800x443 becomes 800x450, 2688x1520 becomes 2688x1512, etc). If changing the resolution to match a standard (4:3, 16:9, or 32:9, etc) aspect ratio does not solve the issue, you can enable "compatibility mode" in your camera group dashboard's stream settings. Depending on your browser and device, more than a few cameras in compatibility mode may not be supported, so only use this option if changing your `detect` width and height fails to resolve the color artifacts and diagonal line. + + + + + +Some cameras don't include the hardware to support multiple connections to the high resolution stream, and this can cause unexpected behavior. In this case it is recommended to [restream](./restream.md) the high resolution stream so that it can be used for live view and recordings. + + + + + +Your camera may change aspect ratios on the dashboard because Frigate uses different streams for different purposes. With go2rtc and Smart Streaming, Frigate shows a static image from the `detect` stream when no activity is present, and switches to the live stream when motion is detected. The camera image will change size if your streams use different aspect ratios. + +To prevent this, make the `detect` stream match the go2rtc live stream's aspect ratio (resolution does not need to match, just the aspect ratio). You can either adjust the camera's output resolution or set the `width` and `height` values in your config's `detect` section to a resolution with an aspect ratio that matches. + +Example: Resolutions from two streams + +- Mismatched (may cause aspect ratio switching on the dashboard): + - Live/go2rtc stream: 1920x1080 (16:9) + - Detect stream: 640x352 (~1.82:1, not 16:9) + +- Matched (prevents switching): + - Live/go2rtc stream: 1920x1080 (16:9) + - Detect stream: 640x360 (16:9) + +You can update the detect settings in your camera config to match the aspect ratio of your go2rtc live stream. For example: + +```yaml +cameras: + front_door: + detect: + width: 640 + height: 360 # set this to 360 instead of 352 + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/front_door # main stream 1920x1080 + roles: + - record + - path: rtsp://127.0.0.1:8554/front_door_sub # sub stream 640x352 + roles: + - detect +``` + +The same applies to your `record` stream: if its aspect ratio differs from your `detect` stream, your recordings will appear in a different shape than the live view. For consistent framing across live view and recordings, use the same aspect ratio for all of a camera's streams (the resolution can still differ). + + diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 042c2722c2..a9b9b837b7 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -376,20 +376,22 @@ class EmbeddingMaintainer(threading.Thread): logger.info(f"Disabled classification processor for model: {model_name}") return - # Check if processor already exists for processor in self.realtime_processors: - if isinstance( - processor, - ( - CustomStateClassificationProcessor, - CustomObjectClassificationProcessor, - ), + if ( + isinstance( + processor, + ( + CustomStateClassificationProcessor, + CustomObjectClassificationProcessor, + ), + ) + and processor.model_config.name == model_name ): - if processor.model_config.name == model_name: - logger.debug( - f"Classification processor for model {model_name} already exists, skipping" - ) - return + processor.model_config = model_config + logger.debug( + f"Updated config for classification processor: {model_name}" + ) + return if model_config.state_config is not None: processor = CustomStateClassificationProcessor( diff --git a/web/public/locales/en/views/classificationModel.json b/web/public/locales/en/views/classificationModel.json index 3206ad0339..3e881ae345 100644 --- a/web/public/locales/en/views/classificationModel.json +++ b/web/public/locales/en/views/classificationModel.json @@ -1,5 +1,6 @@ { "documentTitle": "Classification Models - Frigate", + "disabled": "Disabled", "details": { "scoreInfo": "Score represents the average classification confidence across all detections of this object.", "none": "None", @@ -64,7 +65,20 @@ "title": "Edit Classification Model", "descriptionState": "Edit the classes for this state classification model. Changes will require retraining the model.", "descriptionObject": "Edit the object type and classification type for this object classification model.", - "stateClassesInfo": "Note: Changing state classes requires retraining the model with the updated classes." + "enabled": "Enabled", + "enabledDesc": "Run this model. When disabled, it stops running and no longer classifies.", + "saveAttempts": "Save Attempts", + "saveAttemptsDesc": "Number of classification attempt images to keep for the recent classifications UI.", + "motion": "Run on Motion", + "motionDesc": "Run classification when motion is detected within the configured crop.", + "interval": "Interval", + "intervalDesc": "Seconds between periodic classification runs. Leave empty to run only on motion.", + "intervalPlaceholder": "No interval", + "stateClassesInfo": "Model updated. Retrain the model for the class changes to take effect.", + "errors": { + "saveAttemptsInvalid": "Save attempts must be a whole number of 0 or greater", + "intervalInvalid": "Interval must be a whole number greater than 0" + } }, "deleteDatasetImages": { "title": "Delete Dataset Images", diff --git a/web/src/components/classification/ClassificationModelEditDialog.tsx b/web/src/components/classification/ClassificationModelEditDialog.tsx index ac49ee4db7..4be2e04f95 100644 --- a/web/src/components/classification/ClassificationModelEditDialog.tsx +++ b/web/src/components/classification/ClassificationModelEditDialog.tsx @@ -9,6 +9,7 @@ import { import { Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -17,6 +18,7 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, @@ -50,14 +52,25 @@ type ClassificationModelEditDialogProps = { type ObjectClassificationType = "sub_label" | "attribute"; type ObjectFormData = { + enabled: boolean; + saveAttempts: number; objectLabel: string; objectType: ObjectClassificationType; }; type StateFormData = { + enabled: boolean; + saveAttempts: number; + motion: boolean; + interval?: number; classes: string[]; }; +const DEFAULT_SAVE_ATTEMPTS = { + object: 200, + state: 100, +} as const; + export default function ClassificationModelEditDialog({ open, model, @@ -71,6 +84,10 @@ export default function ClassificationModelEditDialog({ const isStateModel = model.state_config !== undefined; const isObjectModel = model.object_config !== undefined; + const defaultSaveAttempts = isObjectModel + ? DEFAULT_SAVE_ATTEMPTS.object + : DEFAULT_SAVE_ATTEMPTS.state; + const objectLabels = useMemo(() => { if (!config) return []; @@ -93,8 +110,17 @@ export default function ClassificationModelEditDialog({ // Define form schema based on model type const formSchema = useMemo(() => { + const sharedFields = { + enabled: z.boolean(), + saveAttempts: z.coerce + .number({ message: t("edit.errors.saveAttemptsInvalid") }) + .int(t("edit.errors.saveAttemptsInvalid")) + .min(0, t("edit.errors.saveAttemptsInvalid")), + }; + if (isObjectModel) { return z.object({ + ...sharedFields, objectLabel: z .string() .min(1, t("wizard.step1.errors.objectLabelRequired")), @@ -103,6 +129,17 @@ export default function ClassificationModelEditDialog({ } else { // State model return z.object({ + ...sharedFields, + motion: z.boolean(), + interval: z.preprocess( + (val) => + val === "" || val === null || val === undefined ? undefined : val, + z.coerce + .number({ message: t("edit.errors.intervalInvalid") }) + .int(t("edit.errors.intervalInvalid")) + .positive(t("edit.errors.intervalInvalid")) + .optional(), + ), classes: z .array(z.string()) .min(1, t("wizard.step1.errors.classRequired")) @@ -129,12 +166,18 @@ export default function ClassificationModelEditDialog({ resolver: zodResolver(formSchema), defaultValues: isObjectModel ? ({ + enabled: model.enabled, + saveAttempts: model.save_attempts ?? defaultSaveAttempts, objectLabel: model.object_config?.objects?.[0] || "", objectType: (model.object_config ?.classification_type as ObjectClassificationType) || "sub_label", } as ObjectFormData) : ({ + enabled: model.enabled, + saveAttempts: model.save_attempts ?? defaultSaveAttempts, + motion: model.state_config?.motion ?? false, + interval: model.state_config?.interval, classes: [""], // Will be populated from dataset } as StateFormData), mode: "onChange", @@ -151,6 +194,8 @@ export default function ClassificationModelEditDialog({ if (open) { if (isObjectModel) { form.reset({ + enabled: model.enabled, + saveAttempts: model.save_attempts ?? defaultSaveAttempts, objectLabel: model.object_config?.objects?.[0] || "", objectType: (model.object_config @@ -158,6 +203,10 @@ export default function ClassificationModelEditDialog({ } as ObjectFormData); } else { form.reset({ + enabled: model.enabled, + saveAttempts: model.save_attempts ?? defaultSaveAttempts, + motion: model.state_config?.motion ?? false, + interval: model.state_config?.interval, classes: [""], } as StateFormData); } @@ -166,7 +215,15 @@ export default function ClassificationModelEditDialog({ mutateDataset(); } } - }, [open, isObjectModel, isStateModel, model, form, mutateDataset]); + }, [ + open, + isObjectModel, + isStateModel, + model, + form, + mutateDataset, + defaultSaveAttempts, + ]); // Update form with classes from dataset when loaded useEffect(() => { @@ -233,6 +290,7 @@ export default function ClassificationModelEditDialog({ setIsSaving(true); try { if (isObjectModel) { + // object model save const objectData = data as ObjectFormData; // Update the config @@ -243,9 +301,10 @@ export default function ClassificationModelEditDialog({ classification: { custom: { [model.name]: { - enabled: model.enabled, + enabled: objectData.enabled, name: model.name, threshold: model.threshold, + save_attempts: objectData.saveAttempts, object_config: { objects: [objectData.objectLabel], classification_type: objectData.objectType, @@ -260,7 +319,34 @@ export default function ClassificationModelEditDialog({ position: "top-center", }); } else { + // state model save const stateData = data as StateFormData; + + const stateConfig: { motion: boolean; interval?: number | null } = { + motion: stateData.motion, + }; + if (stateData.interval != null) { + stateConfig.interval = stateData.interval; + } else if (model.state_config?.interval != null) { + stateConfig.interval = null; + } + + await axios.put("/config/set", { + requires_restart: 0, + update_topic: `config/classification/custom/${model.name}`, + config_data: { + classification: { + custom: { + [model.name]: { + enabled: stateData.enabled, + save_attempts: stateData.saveAttempts, + state_config: stateConfig, + }, + }, + }, + }, + }); + const newClasses = stateData.classes.filter( (c) => c.trim().length > 0, ); @@ -307,11 +393,11 @@ export default function ClassificationModelEditDialog({ if (renamePromises.length > 0) { await Promise.all(renamePromises); await mutate(`classification/${model.name}/dataset`); - toast.success(t("toast.success.updatedModel"), { + toast.success(t("edit.stateClassesInfo"), { position: "top-center", }); } else { - toast.info(t("edit.stateClassesInfo"), { + toast.success(t("toast.success.updatedModel"), { position: "top-center", }); } @@ -359,6 +445,29 @@ export default function ClassificationModelEditDialog({
+ ( + +
+ + {t("edit.enabled")} + + + {t("edit.enabledDesc")} + +
+ + + +
+ )} + /> + {isObjectModel && ( <> )} + {isStateModel && ( + <> + ( + +
+ + {t("edit.motion")} + + + {t("edit.motionDesc")} + +
+ + + +
+ )} + /> + + ( + + + {t("edit.interval")} + + + + + + {t("edit.intervalDesc")} + + + + )} + /> + + )} + + ( + + + {t("edit.saveAttempts")} + + + + + + {t("edit.saveAttemptsDesc")} + + + + )} + /> +