From 9e70bddc9d640becf9d058470e52d9afbb6647ec Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 12 Mar 2025 12:54:20 -0600 Subject: [PATCH 1/5] Update go2rtc version (#17119) --- docker/main/Dockerfile | 2 +- .../rootfs/usr/local/go2rtc/create_config.py | 13 -- docs/docs/configuration/advanced.md | 2 +- docs/docs/configuration/camera_specific.md | 2 +- docs/docs/configuration/reference.md | 2 +- docs/docs/configuration/restream.md | 6 +- docs/docs/guides/configuring_go2rtc.md | 107 +++++++------- docs/sidebars.ts | 138 +++++++++--------- 8 files changed, 132 insertions(+), 140 deletions(-) diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index 7a0351240..2a7d388bc 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -55,7 +55,7 @@ RUN --mount=type=tmpfs,target=/tmp --mount=type=tmpfs,target=/var/cache/apt \ FROM scratch AS go2rtc ARG TARGETARCH WORKDIR /rootfs/usr/local/go2rtc/bin -ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.2/go2rtc_linux_${TARGETARCH}" go2rtc +ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.9/go2rtc_linux_${TARGETARCH}" go2rtc FROM wget AS tempio ARG TARGETARCH diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index 4fe26775e..30b78e1e1 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -69,10 +69,6 @@ elif go2rtc_config["log"].get("format") is None: if go2rtc_config.get("webrtc") is None: go2rtc_config["webrtc"] = {} -# go2rtc should listen on 8555 tcp & udp by default -if go2rtc_config["webrtc"].get("listen") is None: - go2rtc_config["webrtc"]["listen"] = ":8555" - if go2rtc_config["webrtc"].get("candidates") is None: default_candidates = [] # use internal candidate if it was discovered when running through the add-on @@ -84,15 +80,6 @@ if go2rtc_config["webrtc"].get("candidates") is None: go2rtc_config["webrtc"]["candidates"] = default_candidates -# This prevents WebRTC from attempting to establish a connection to the internal -# docker IPs which are not accessible from outside the container itself and just -# wastes time during negotiation. Note that this is only necessary because -# Frigate container doesn't run in host network mode. -if go2rtc_config["webrtc"].get("filter") is None: - go2rtc_config["webrtc"]["filter"] = {"candidates": []} -elif go2rtc_config["webrtc"]["filter"].get("candidates") is None: - go2rtc_config["webrtc"]["filter"]["candidates"] = [] - # sets default RTSP response to be equivalent to ?video=h264,h265&audio=aac # this means user does not need to specify audio codec when using restream # as source for frigate and the integration supports HLS playback diff --git a/docs/docs/configuration/advanced.md b/docs/docs/configuration/advanced.md index b037c0768..c889d2d26 100644 --- a/docs/docs/configuration/advanced.md +++ b/docs/docs/configuration/advanced.md @@ -186,7 +186,7 @@ To do this: ### Custom go2rtc version -Frigate currently includes go2rtc v1.9.2, there may be certain cases where you want to run a different version of go2rtc. +Frigate currently includes go2rtc v1.9.9, there may be certain cases where you want to run a different version of go2rtc. To do this: diff --git a/docs/docs/configuration/camera_specific.md b/docs/docs/configuration/camera_specific.md index fb4ce5714..0cd1efd47 100644 --- a/docs/docs/configuration/camera_specific.md +++ b/docs/docs/configuration/camera_specific.md @@ -219,7 +219,7 @@ go2rtc: - rtspx://192.168.1.1:7441/abcdefghijk ``` -[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#source-rtsp) +[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#source-rtsp) In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record if used directly with unifi protect. diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index 37884259a..9a880aade 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -591,7 +591,7 @@ genai: person: "My special person prompt." # Optional: Restream configuration -# Uses https://github.com/AlexxIT/go2rtc (v1.9.2) +# Uses https://github.com/AlexxIT/go2rtc (v1.9.9) # NOTE: The default go2rtc API port (1984) must be used, # changing this port for the integrated go2rtc instance is not supported. go2rtc: diff --git a/docs/docs/configuration/restream.md b/docs/docs/configuration/restream.md index 0db4ded80..288dc8fd9 100644 --- a/docs/docs/configuration/restream.md +++ b/docs/docs/configuration/restream.md @@ -7,7 +7,7 @@ title: Restream Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://:8554/`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate. -Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.2) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#configuration) for more advanced configurations and features. +Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.9) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#configuration) for more advanced configurations and features. :::note @@ -134,7 +134,7 @@ cameras: ## Handling Complex Passwords -go2rtc expects URL-encoded passwords in the config, [urlencoder.org](https://urlencoder.org) can be used for this purpose. +go2rtc expects URL-encoded passwords in the config, [urlencoder.org](https://urlencoder.org) can be used for this purpose. For example: @@ -156,7 +156,7 @@ See [this comment(https://github.com/AlexxIT/go2rtc/issues/1217#issuecomment-224 ## Advanced Restream Configurations -The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below: +The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below: NOTE: The output will need to be passed with two curly braces `{{output}}` diff --git a/docs/docs/guides/configuring_go2rtc.md b/docs/docs/guides/configuring_go2rtc.md index 1a61fd0c5..3b8a8af1f 100644 --- a/docs/docs/guides/configuring_go2rtc.md +++ b/docs/docs/guides/configuring_go2rtc.md @@ -13,7 +13,7 @@ Use of the bundled go2rtc is optional. You can still configure FFmpeg to connect # Setup a go2rtc stream -First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#module-streams), not just rtsp. +First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#module-streams), not just rtsp. :::tip @@ -32,69 +32,74 @@ go2rtc: After adding this to the config, restart Frigate and try to watch the live stream for a single camera by clicking on it from the dashboard. It should look much clearer and more fluent than the original jsmpeg stream. - ### What if my video doesn't play? - Check Logs: - - Access the go2rtc logs in the Frigate UI under Logs in the sidebar. - - If go2rtc is having difficulty connecting to your camera, you should see some error messages in the log. + + - Access the go2rtc logs in the Frigate UI under Logs in the sidebar. + - If go2rtc is having difficulty connecting to your camera, you should see some error messages in the log. - Check go2rtc Web Interface: if you don't see any errors in the logs, try viewing the camera through go2rtc's web interface. - - Navigate to port 1984 in your browser to access go2rtc's web interface. - - If using Frigate through Home Assistant, enable the web interface at port 1984. - - If using Docker, forward port 1984 before accessing the web interface. - - Click `stream` for the specific camera to see if the camera's stream is being received. + + - Navigate to port 1984 in your browser to access go2rtc's web interface. + - If using Frigate through Home Assistant, enable the web interface at port 1984. + - If using Docker, forward port 1984 before accessing the web interface. + - Click `stream` for the specific camera to see if the camera's stream is being received. - Check Video Codec: - - If the camera stream works in go2rtc but not in your browser, the video codec might be unsupported. - - If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#codecs-madness) in go2rtc documentation. - - If unable to switch from H265 to H264, or if the stream format is different (e.g., MJPEG), re-encode the video using [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view. - ```yaml - go2rtc: - streams: - back: - - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 - - "ffmpeg:back#video=h264#hardware" - ``` -- Switch to FFmpeg if needed: - - Some camera streams may need to use the ffmpeg module in go2rtc. This has the downside of slower startup times, but has compatibility with more stream types. - ```yaml - go2rtc: - streams: - back: - - ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 - ``` + - If the camera stream works in go2rtc but not in your browser, the video codec might be unsupported. + - If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#codecs-madness) in go2rtc documentation. + - If unable to switch from H265 to H264, or if the stream format is different (e.g., MJPEG), re-encode the video using [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view. + ```yaml + go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + - "ffmpeg:back#video=h264#hardware" + ``` - - If you can see the video but do not have audio, this is most likely because your camera's audio stream codec is not AAC. - - If possible, update your camera's audio settings to AAC in your camera's firmware. - - If your cameras do not support AAC audio, you will need to tell go2rtc to re-encode the audio to AAC on demand if you want audio. This will use additional CPU and add some latency. To add AAC audio on demand, you can update your go2rtc config as follows: - ```yaml - go2rtc: - streams: - back: - - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 - - "ffmpeg:back#audio=aac" - ``` +- Switch to FFmpeg if needed: - If you need to convert **both** the audio and video streams, you can use the following: + - Some camera streams may need to use the ffmpeg module in go2rtc. This has the downside of slower startup times, but has compatibility with more stream types. - ```yaml - go2rtc: - streams: - back: - - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 - - "ffmpeg:back#video=h264#audio=aac#hardware" - ``` + ```yaml + go2rtc: + streams: + back: + - ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + ``` - When using the ffmpeg module, you would add AAC audio like this: + - If you can see the video but do not have audio, this is most likely because your camera's audio stream codec is not AAC. + - If possible, update your camera's audio settings to AAC in your camera's firmware. + - If your cameras do not support AAC audio, you will need to tell go2rtc to re-encode the audio to AAC on demand if you want audio. This will use additional CPU and add some latency. To add AAC audio on demand, you can update your go2rtc config as follows: - ```yaml - go2rtc: - streams: - back: - - "ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2#video=copy#audio=copy#audio=aac#hardware" - ``` + ```yaml + go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + - "ffmpeg:back#audio=aac" + ``` + + If you need to convert **both** the audio and video streams, you can use the following: + + ```yaml + go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + - "ffmpeg:back#video=h264#audio=aac#hardware" + ``` + + When using the ffmpeg module, you would add AAC audio like this: + + ```yaml + go2rtc: + streams: + back: + - "ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2#video=copy#audio=copy#audio=aac#hardware" + ``` :::warning diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 0c25e4eb7..bffb54349 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -1,106 +1,106 @@ -import type { SidebarsConfig, } from '@docusaurus/plugin-content-docs'; -import { PropSidebarItemLink } from '@docusaurus/plugin-content-docs'; -import frigateHttpApiSidebar from './docs/integrations/api/sidebar'; +import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; +import { PropSidebarItemLink } from "@docusaurus/plugin-content-docs"; +import frigateHttpApiSidebar from "./docs/integrations/api/sidebar"; const sidebars: SidebarsConfig = { docs: { Frigate: [ - 'frigate/index', - 'frigate/hardware', - 'frigate/installation', - 'frigate/camera_setup', - 'frigate/video_pipeline', - 'frigate/glossary', + "frigate/index", + "frigate/hardware", + "frigate/installation", + "frigate/camera_setup", + "frigate/video_pipeline", + "frigate/glossary", ], Guides: [ - 'guides/getting_started', - 'guides/configuring_go2rtc', - 'guides/ha_notifications', - 'guides/ha_network_storage', - 'guides/reverse_proxy', + "guides/getting_started", + "guides/configuring_go2rtc", + "guides/ha_notifications", + "guides/ha_network_storage", + "guides/reverse_proxy", ], Configuration: { - 'Configuration Files': [ - 'configuration/index', - 'configuration/reference', + "Configuration Files": [ + "configuration/index", + "configuration/reference", { - type: 'link', - label: 'Go2RTC Configuration Reference', - href: 'https://github.com/AlexxIT/go2rtc/tree/v1.9.2#configuration', + type: "link", + label: "Go2RTC Configuration Reference", + href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.9#configuration", } as PropSidebarItemLink, ], Detectors: [ - 'configuration/object_detectors', - 'configuration/audio_detectors', + "configuration/object_detectors", + "configuration/audio_detectors", ], Classifiers: [ - 'configuration/semantic_search', - 'configuration/genai', - 'configuration/face_recognition', - 'configuration/license_plate_recognition', + "configuration/semantic_search", + "configuration/genai", + "configuration/face_recognition", + "configuration/license_plate_recognition", ], Cameras: [ - 'configuration/cameras', - 'configuration/review', - 'configuration/record', - 'configuration/snapshots', - 'configuration/motion_detection', - 'configuration/birdseye', - 'configuration/live', - 'configuration/restream', - 'configuration/autotracking', - 'configuration/camera_specific', + "configuration/cameras", + "configuration/review", + "configuration/record", + "configuration/snapshots", + "configuration/motion_detection", + "configuration/birdseye", + "configuration/live", + "configuration/restream", + "configuration/autotracking", + "configuration/camera_specific", ], Objects: [ - 'configuration/object_filters', - 'configuration/masks', - 'configuration/zones', - 'configuration/objects', - 'configuration/stationary_objects', + "configuration/object_filters", + "configuration/masks", + "configuration/zones", + "configuration/objects", + "configuration/stationary_objects", ], - 'Extra Configuration': [ - 'configuration/authentication', - 'configuration/notifications', - 'configuration/hardware_acceleration', - 'configuration/ffmpeg_presets', + "Extra Configuration": [ + "configuration/authentication", + "configuration/notifications", + "configuration/hardware_acceleration", + "configuration/ffmpeg_presets", "configuration/pwa", - 'configuration/tls', - 'configuration/advanced', + "configuration/tls", + "configuration/advanced", ], }, Integrations: [ - 'integrations/plus', - 'integrations/home-assistant', + "integrations/plus", + "integrations/home-assistant", // This is the HTTP API generated by OpenAPI { - type: 'category', - label: 'HTTP API', + type: "category", + label: "HTTP API", link: { - type: 'generated-index', - title: 'Frigate HTTP API', - description: 'HTTP API', - slug: '/integrations/api/frigate-http-api', + type: "generated-index", + title: "Frigate HTTP API", + description: "HTTP API", + slug: "/integrations/api/frigate-http-api", }, items: frigateHttpApiSidebar, }, - 'integrations/mqtt', - 'configuration/metrics', - 'integrations/third_party_extensions', + "integrations/mqtt", + "configuration/metrics", + "integrations/third_party_extensions", ], - 'Frigate+': [ - 'plus/index', - 'plus/first_model', - 'plus/improving_model', - 'plus/faq', + "Frigate+": [ + "plus/index", + "plus/first_model", + "plus/improving_model", + "plus/faq", ], Troubleshooting: [ - 'troubleshooting/faqs', - 'troubleshooting/recordings', - 'troubleshooting/edgetpu', + "troubleshooting/faqs", + "troubleshooting/recordings", + "troubleshooting/edgetpu", ], Development: [ - 'development/contributing', - 'development/contributing-boards', + "development/contributing", + "development/contributing-boards", ], }, }; From 636080261243d7e0103d9b4e40ac332fe4eed4a5 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:38:28 -0500 Subject: [PATCH 2/5] Use identifier field for unknown license plates (#17123) * backend * backend fixes * api for search queries * frontend * docs * add filterable scroll list to more filters pane for identifiers * always publish identifier --- .../license_plate_recognition.md | 9 +- docs/docs/integrations/mqtt.md | 8 +- frigate/api/app.py | 33 +++++ .../api/defs/query/events_query_parameters.py | 2 + frigate/api/event.py | 60 ++++++++ frigate/camera/state.py | 9 +- frigate/comms/event_metadata_updater.py | 1 + .../common/license_plate/mixin.py | 12 +- frigate/events/maintainer.py | 6 + frigate/track/object_processing.py | 38 +++++ frigate/track/tracked_object.py | 3 + .../overlay/detail/SearchDetailDialog.tsx | 23 +++ .../overlay/dialog/SearchFilterDialog.tsx | 133 +++++++++++++++++- web/src/pages/Explore.tsx | 2 + web/src/types/search.ts | 4 + web/src/views/search/SearchView.tsx | 4 +- 16 files changed, 332 insertions(+), 15 deletions(-) diff --git a/docs/docs/configuration/license_plate_recognition.md b/docs/docs/configuration/license_plate_recognition.md index 3fe1ee852..f8b9030ff 100644 --- a/docs/docs/configuration/license_plate_recognition.md +++ b/docs/docs/configuration/license_plate_recognition.md @@ -3,16 +3,16 @@ id: license_plate_recognition title: License Plate Recognition (LPR) --- -Frigate can recognize license plates on vehicles and automatically add the detected characters or recognized name as a `sub_label` to objects that are of type `car`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street. +Frigate can recognize license plates on vehicles and automatically add the detected characters to the `identifier` field or a known name as a `sub_label` to objects that are of type `car`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street. LPR works best when the license plate is clearly visible to the camera. For moving vehicles, Frigate continuously refines the recognition process, keeping the most confident result. However, LPR does not run on stationary vehicles. -When a plate is recognized, the detected characters or recognized name is: +When a plate is recognized, the recognized name is: -- Added as a `sub_label` to the `car` tracked object. +- Added to the `car` tracked object as a `sub_label` (if known) or the `identifier` field (if unknown) - Viewable in the Review Item Details pane in Review and the Tracked Object Details pane in Explore. - Filterable through the More Filters menu in Explore. -- Published via the `frigate/events` MQTT topic as a `sub_label` for the tracked object. +- Published via the `frigate/events` MQTT topic as a `sub_label` (known) or `identifier` (unknown) for the tracked object. ## Model Requirements @@ -71,6 +71,7 @@ Fine-tune the LPR feature using these optional parameters: - **`known_plates`**: List of strings or regular expressions that assign custom a `sub_label` to `car` objects when a recognized plate matches a known value. - These labels appear in the UI, filters, and notifications. + - Unknown plates are still saved but are added to the `identifier` field rather than the `sub_label`. - **`match_distance`**: Allows for minor variations (missing/incorrect characters) when matching a detected plate to a known plate. - For example, setting `match_distance: 1` allows a plate `ABCDE` to match `ABCBE` or `ABCD`. - This parameter will _not_ operate on known plates that are defined as regular expressions. You should define the full string of your plate in `known_plates` in order to use `match_distance`. diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index fc8888e40..bcdbe7046 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -54,7 +54,9 @@ Message published for each changed tracked object. The first message is publishe }, // attributes with top score that have been identified on the object at any point "current_attributes": [], // detailed data about the current attributes in this frame "current_estimated_speed": 0.71, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled - "velocity_angle": 180 // direction of travel relative to the frame for objects moving through zones with speed estimation enabled + "velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled + "identifier": "ABC12345", // an identifier for this object - in this case, an unrecognized license plate + "identifier_score": 0.933451 }, "after": { "id": "1607123955.475377-mxklsc", @@ -93,7 +95,9 @@ Message published for each changed tracked object. The first message is publishe } ], "current_estimated_speed": 0.77, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled - "velocity_angle": 180 // direction of travel relative to the frame for objects moving through zones with speed estimation enabled + "velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled + "identifier": "ABC12345", // an identifier for this object - in this case, an unrecognized license plate + "identifier_score": 0.933451 } } ``` diff --git a/frigate/api/app.py b/frigate/api/app.py index 5ce90130f..d9a57a3c1 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -619,6 +619,39 @@ def get_sub_labels(split_joined: Optional[int] = None): return JSONResponse(content=sub_labels) +@router.get("/identifiers") +def get_identifiers(split_joined: Optional[int] = None): + try: + events = Event.select(Event.data).distinct() + except Exception: + return JSONResponse( + content=({"success": False, "message": "Failed to get identifiers"}), + status_code=404, + ) + + identifiers = [] + for e in events: + if e.data is not None and "identifier" in e.data: + identifiers.append(e.data["identifier"]) + + while None in identifiers: + identifiers.remove(None) + + if split_joined: + original_identifiers = identifiers.copy() + for identifier in original_identifiers: + if identifier and "," in identifier: + identifiers.remove(identifier) + parts = identifier.split(",") + for part in parts: + if part.strip() not in identifiers: + identifiers.append(part.strip()) + + identifiers = list(set(identifiers)) + identifiers.sort() + return JSONResponse(content=identifiers) + + @router.get("/timeline") def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None): clauses = [] diff --git a/frigate/api/defs/query/events_query_parameters.py b/frigate/api/defs/query/events_query_parameters.py index 01c79abb0..9f73d8583 100644 --- a/frigate/api/defs/query/events_query_parameters.py +++ b/frigate/api/defs/query/events_query_parameters.py @@ -27,6 +27,7 @@ class EventsQueryParams(BaseModel): max_score: Optional[float] = None min_speed: Optional[float] = None max_speed: Optional[float] = None + identifier: Optional[str] = "all" is_submitted: Optional[int] = None min_length: Optional[float] = None max_length: Optional[float] = None @@ -55,6 +56,7 @@ class EventsSearchQueryParams(BaseModel): max_score: Optional[float] = None min_speed: Optional[float] = None max_speed: Optional[float] = None + identifier: Optional[str] = "all" sort: Optional[str] = None diff --git a/frigate/api/event.py b/frigate/api/event.py index 91651313d..e9cf2fea4 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -101,6 +101,7 @@ def events(params: EventsQueryParams = Depends()): min_length = params.min_length max_length = params.max_length event_id = params.event_id + identifier = params.identifier sort = params.sort @@ -158,6 +159,32 @@ def events(params: EventsQueryParams = Depends()): sub_label_clause = reduce(operator.or_, sub_label_clauses) clauses.append((sub_label_clause)) + if identifier != "all": + # use matching so joined identifiers are included + # for example an identifier 'ABC123' would get events + # with identifiers 'ABC123' and 'ABC123, XYZ789' + identifier_clauses = [] + filtered_identifiers = identifier.split(",") + + if "None" in filtered_identifiers: + filtered_identifiers.remove("None") + identifier_clauses.append((Event.data["identifier"].is_null())) + + for identifier in filtered_identifiers: + # Exact matching plus list inclusion + identifier_clauses.append( + (Event.data["identifier"].cast("text") == identifier) + ) + identifier_clauses.append( + (Event.data["identifier"].cast("text") % f"*{identifier},*") + ) + identifier_clauses.append( + (Event.data["identifier"].cast("text") % f"*, {identifier}*") + ) + + identifier_clause = reduce(operator.or_, identifier_clauses) + clauses.append((identifier_clause)) + if zones != "all": # use matching so events with multiple zones # still match on a search where any zone matches @@ -340,6 +367,8 @@ def events_explore(limit: int = 10): "average_estimated_speed", "velocity_angle", "path_data", + "identifier", + "identifier_score", ] }, "event_count": label_counts[event.label], @@ -397,6 +426,7 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) has_clip = params.has_clip has_snapshot = params.has_snapshot is_submitted = params.is_submitted + identifier = params.identifier # for similarity search event_id = params.event_id @@ -466,6 +496,32 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) event_filters.append((reduce(operator.or_, zone_clauses))) + if identifier != "all": + # use matching so joined identifiers are included + # for example an identifier 'ABC123' would get events + # with identifiers 'ABC123' and 'ABC123, XYZ789' + identifier_clauses = [] + filtered_identifiers = identifier.split(",") + + if "None" in filtered_identifiers: + filtered_identifiers.remove("None") + identifier_clauses.append((Event.data["identifier"].is_null())) + + for identifier in filtered_identifiers: + # Exact matching plus list inclusion + identifier_clauses.append( + (Event.data["identifier"].cast("text") == identifier) + ) + identifier_clauses.append( + (Event.data["identifier"].cast("text") % f"*{identifier},*") + ) + identifier_clauses.append( + (Event.data["identifier"].cast("text") % f"*, {identifier}*") + ) + + identifier_clause = reduce(operator.or_, identifier_clauses) + event_filters.append((identifier_clause)) + if after: event_filters.append((Event.start_time > after)) @@ -627,6 +683,8 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) "average_estimated_speed", "velocity_angle", "path_data", + "identifier", + "identifier_score", ] } @@ -681,6 +739,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()): Event.camera, Event.label, Event.sub_label, + Event.data, fn.strftime( "%Y-%m-%d", fn.datetime( @@ -695,6 +754,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()): Event.camera, Event.label, Event.sub_label, + Event.data, (Event.start_time + seconds_offset).cast("int") / (3600 * 24), Event.zones, ) diff --git a/frigate/camera/state.py b/frigate/camera/state.py index dfd6744e2..2ffa54798 100644 --- a/frigate/camera/state.py +++ b/frigate/camera/state.py @@ -137,12 +137,13 @@ class CameraState: # draw the bounding boxes on the frame box = obj["box"] text = ( - obj["label"] + obj["sub_label"][0] if ( - not obj.get("sub_label") - or not is_label_printable(obj["sub_label"][0]) + obj.get("sub_label") and is_label_printable(obj["sub_label"][0]) ) - else obj["sub_label"][0] + else obj.get("identifier", [None])[0] + if (obj.get("identifier") and obj["identifier"][0]) + else obj["label"] ) draw_box_with_label( frame_copy, diff --git a/frigate/comms/event_metadata_updater.py b/frigate/comms/event_metadata_updater.py index 3342182c3..c5881e686 100644 --- a/frigate/comms/event_metadata_updater.py +++ b/frigate/comms/event_metadata_updater.py @@ -14,6 +14,7 @@ class EventMetadataTypeEnum(str, Enum): manual_event_end = "manual_event_end" regenerate_description = "regenerate_description" sub_label = "sub_label" + identifier = "identifier" class EventMetadataPublisher(Publisher): diff --git a/frigate/data_processing/common/license_plate/mixin.py b/frigate/data_processing/common/license_plate/mixin.py index c74949d9c..37530b205 100644 --- a/frigate/data_processing/common/license_plate/mixin.py +++ b/frigate/data_processing/common/license_plate/mixin.py @@ -1054,13 +1054,19 @@ class LicensePlateProcessingMixin: for plate in plates ) ), - top_plate, + None, ) - # Send the result to the API + # If it's a known plate, publish to sub_label + if sub_label is not None: + self.sub_label_publisher.publish( + EventMetadataTypeEnum.sub_label, (id, sub_label, avg_confidence) + ) + self.sub_label_publisher.publish( - EventMetadataTypeEnum.sub_label, (id, sub_label, avg_confidence) + EventMetadataTypeEnum.identifier, (id, top_plate, avg_confidence) ) + self.detected_license_plates[id] = { "plate": top_plate, "char_confidences": top_char_confidences, diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py index 5cfa7c716..947763f40 100644 --- a/frigate/events/maintainer.py +++ b/frigate/events/maintainer.py @@ -27,6 +27,7 @@ def should_update_db(prev_event: Event, current_event: Event) -> bool: or prev_event["average_estimated_speed"] != current_event["average_estimated_speed"] or prev_event["velocity_angle"] != current_event["velocity_angle"] + or prev_event["identifier"] != current_event["identifier"] or prev_event["path_data"] != current_event["path_data"] ): return True @@ -226,6 +227,11 @@ class EventProcessor(threading.Thread): event[Event.sub_label] = event_data["sub_label"][0] event[Event.data]["sub_label_score"] = event_data["sub_label"][1] + # only overwrite the identifier in the database if it's set + if event_data.get("identifier") is not None: + event[Event.data]["identifier"] = event_data["identifier"][0] + event[Event.data]["identifier_score"] = event_data["identifier"][1] + ( Event.insert(event) .on_conflict( diff --git a/frigate/track/object_processing.py b/frigate/track/object_processing.py index ddac2b588..56dd59110 100644 --- a/frigate/track/object_processing.py +++ b/frigate/track/object_processing.py @@ -346,6 +346,41 @@ class TrackedObjectProcessor(threading.Thread): return True + def set_identifier( + self, event_id: str, identifier: str | None, score: float | None + ) -> None: + """Update identifier for given event id.""" + tracked_obj: TrackedObject = None + + for state in self.camera_states.values(): + tracked_obj = state.tracked_objects.get(event_id) + + if tracked_obj is not None: + break + + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + event = None + + if not tracked_obj and not event: + return + + if tracked_obj: + tracked_obj.obj_data["identifier"] = (identifier, score) + + if event: + data = event.data + data["identifier"] = identifier + if identifier is None: + data["identifier_score"] = None + elif score is not None: + data["identifier_score"] = score + event.data = data + event.save() + + return True + def create_manual_event(self, payload: tuple) -> None: ( frame_time, @@ -507,6 +542,9 @@ class TrackedObjectProcessor(threading.Thread): if topic.endswith(EventMetadataTypeEnum.sub_label.value): (event_id, sub_label, score) = payload self.set_sub_label(event_id, sub_label, score) + if topic.endswith(EventMetadataTypeEnum.identifier.value): + (event_id, identifier, score) = payload + self.set_identifier(event_id, identifier, score) elif topic.endswith(EventMetadataTypeEnum.manual_event_create.value): self.create_manual_event(payload) elif topic.endswith(EventMetadataTypeEnum.manual_event_end.value): diff --git a/frigate/track/tracked_object.py b/frigate/track/tracked_object.py index f1eb29328..9c19595b5 100644 --- a/frigate/track/tracked_object.py +++ b/frigate/track/tracked_object.py @@ -153,6 +153,8 @@ class TrackedObject: "current_estimated_speed": self.current_estimated_speed, "velocity_angle": self.velocity_angle, "path_data": self.path_data, + "identifier": obj_data.get("identifier"), + "identifier_score": obj_data.get("identifier_score"), } thumb_update = True @@ -365,6 +367,7 @@ class TrackedObject: "average_estimated_speed": self.average_estimated_speed, "velocity_angle": self.velocity_angle, "path_data": self.path_data, + "identifier": self.obj_data.get("identifier"), } return event diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index c94c2cd2d..7f69f2ca2 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -333,6 +333,18 @@ function ObjectDetailsTab({ } }, [search]); + const identifierScore = useMemo(() => { + if (!search) { + return undefined; + } + + if (search.data.identifier && search.data?.identifier_score) { + return Math.round((search.data?.identifier_score ?? 0) * 100); + } else { + return undefined; + } + }, [search]); + const averageEstimatedSpeed = useMemo(() => { if (!search || !search.data?.average_estimated_speed) { return undefined; @@ -538,6 +550,17 @@ function ObjectDetailsTab({ + {search?.data.identifier && ( +
+
Identifier
+
+
+ {search.data.identifier}{" "} + {identifierScore && ` (${identifierScore}%)`} +
+
+
+ )}
diff --git a/web/src/components/overlay/dialog/SearchFilterDialog.tsx b/web/src/components/overlay/dialog/SearchFilterDialog.tsx index 23deee531..59b30f82f 100644 --- a/web/src/components/overlay/dialog/SearchFilterDialog.tsx +++ b/web/src/components/overlay/dialog/SearchFilterDialog.tsx @@ -33,6 +33,14 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { + Command, + CommandEmpty, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { LuCheck } from "react-icons/lu"; type SearchFilterDialogProps = { config?: FrigateConfig; @@ -77,7 +85,8 @@ export default function SearchFilterDialog({ (currentFilter.max_score ?? 1) < 1 || (currentFilter.max_speed ?? 150) < 150 || (currentFilter.zones?.length ?? 0) > 0 || - (currentFilter.sub_labels?.length ?? 0) > 0), + (currentFilter.sub_labels?.length ?? 0) > 0 || + (currentFilter.identifier?.length ?? 0) > 0), [currentFilter], ); @@ -119,6 +128,12 @@ export default function SearchFilterDialog({ setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels }) } /> + + setCurrentFilter({ ...currentFilter, identifier: identifiers }) + } + /> @@ -830,3 +846,118 @@ export function SnapshotClipFilterContent({
); } + +type IdentifierFilterContentProps = { + identifiers: string[] | undefined; + setIdentifiers: (identifiers: string[] | undefined) => void; +}; + +export function IdentifierFilterContent({ + identifiers, + setIdentifiers, +}: IdentifierFilterContentProps) { + const { data: allIdentifiers, error } = useSWR("identifiers", { + revalidateOnFocus: false, + }); + + const [selectedIdentifiers, setSelectedIdentifiers] = useState( + identifiers || [], + ); + const [inputValue, setInputValue] = useState(""); + + useEffect(() => { + if (identifiers) { + setSelectedIdentifiers(identifiers); + } else { + setSelectedIdentifiers([]); + } + }, [identifiers]); + + const handleSelect = (identifier: string) => { + const newSelected = selectedIdentifiers.includes(identifier) + ? selectedIdentifiers.filter((id) => id !== identifier) // Deselect + : [...selectedIdentifiers, identifier]; // Select + + setSelectedIdentifiers(newSelected); + if (newSelected.length === 0) { + setIdentifiers(undefined); // Clear filter if no identifiers selected + } else { + setIdentifiers(newSelected); + } + }; + + if (!allIdentifiers || allIdentifiers.length === 0) { + return null; + } + + const filteredIdentifiers = + allIdentifiers?.filter((id) => + id.toLowerCase().includes(inputValue.toLowerCase()), + ) || []; + + return ( +
+ +
Identifiers
+ {error ? ( +

Failed to load identifiers

+ ) : !allIdentifiers ? ( +

Loading identifiers...

+ ) : ( + <> + + + + {filteredIdentifiers.length === 0 && inputValue && ( + No identifiers found. + )} + {filteredIdentifiers.map((identifier) => ( + handleSelect(identifier)} + className="cursor-pointer" + > + + {identifier} + + ))} + + + {selectedIdentifiers.length > 0 && ( +
+ {selectedIdentifiers.map((id) => ( + + {id} + + + ))} +
+ )} + + )} +

+ Select one or more identifiers from the list. +

+
+ ); +} diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index af23c18f4..cf24ff70d 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -105,6 +105,7 @@ export default function Explore() { cameras: searchSearchParams["cameras"], labels: searchSearchParams["labels"], sub_labels: searchSearchParams["sub_labels"], + identifier: searchSearchParams["identifier"], zones: searchSearchParams["zones"], before: searchSearchParams["before"], after: searchSearchParams["after"], @@ -140,6 +141,7 @@ export default function Explore() { cameras: searchSearchParams["cameras"], labels: searchSearchParams["labels"], sub_labels: searchSearchParams["sub_labels"], + identifier: searchSearchParams["identifier"], zones: searchSearchParams["zones"], before: searchSearchParams["before"], after: searchSearchParams["after"], diff --git a/web/src/types/search.ts b/web/src/types/search.ts index cef7f6aff..90bcd54d7 100644 --- a/web/src/types/search.ts +++ b/web/src/types/search.ts @@ -58,6 +58,8 @@ export type SearchResult = { average_estimated_speed: number; velocity_angle: number; path_data: [number[], number][]; + identifier?: string; + identifier_score?: number; }; }; @@ -66,6 +68,7 @@ export type SearchFilter = { cameras?: string[]; labels?: string[]; sub_labels?: string[]; + identifier?: string[]; zones?: string[]; before?: number; after?: number; @@ -89,6 +92,7 @@ export type SearchQueryParams = { cameras?: string[]; labels?: string[]; sub_labels?: string[]; + identifier?: string[]; zones?: string[]; before?: string; after?: string; diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index adbc96413..7f1b1e4a1 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -121,6 +121,7 @@ export default function SearchView({ }, [config, searchFilter]); const { data: allSubLabels } = useSWR("sub_labels"); + const { data: allIdentifiers } = useSWR("identifiers"); const allZones = useMemo(() => { if (!config) { @@ -160,12 +161,13 @@ export default function SearchView({ max_score: ["100"], min_speed: ["1"], max_speed: ["150"], + identifier: allIdentifiers, has_clip: ["yes", "no"], has_snapshot: ["yes", "no"], ...(config?.plus?.enabled && searchFilter?.has_snapshot && { is_submitted: ["yes", "no"] }), }), - [config, allLabels, allZones, allSubLabels, searchFilter], + [config, allLabels, allZones, allSubLabels, allIdentifiers, searchFilter], ); // remove duplicate event ids From b7333557a15053df3372f0a6793a95b4753f8aa9 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 12 Mar 2025 15:54:28 -0600 Subject: [PATCH 3/5] Enable audio by default (#17125) * Remove mp4 query, allowing go2rtc to send any audio * Add audio transcoding by default --- .../rootfs/usr/local/go2rtc/create_config.py | 25 ++++++------------- frigate/config/camera/ffmpeg.py | 2 +- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index 30b78e1e1..5d8e80f9d 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -80,24 +80,15 @@ if go2rtc_config["webrtc"].get("candidates") is None: go2rtc_config["webrtc"]["candidates"] = default_candidates -# sets default RTSP response to be equivalent to ?video=h264,h265&audio=aac -# this means user does not need to specify audio codec when using restream -# as source for frigate and the integration supports HLS playback -if go2rtc_config.get("rtsp") is None: - go2rtc_config["rtsp"] = {"default_query": "mp4"} -else: - if go2rtc_config["rtsp"].get("default_query") is None: - go2rtc_config["rtsp"]["default_query"] = "mp4" +if go2rtc_config["rtsp"].get("username") is not None: + go2rtc_config["rtsp"]["username"] = go2rtc_config["rtsp"]["username"].format( + **FRIGATE_ENV_VARS + ) - if go2rtc_config["rtsp"].get("username") is not None: - go2rtc_config["rtsp"]["username"] = go2rtc_config["rtsp"]["username"].format( - **FRIGATE_ENV_VARS - ) - - if go2rtc_config["rtsp"].get("password") is not None: - go2rtc_config["rtsp"]["password"] = go2rtc_config["rtsp"]["password"].format( - **FRIGATE_ENV_VARS - ) +if go2rtc_config["rtsp"].get("password") is not None: + go2rtc_config["rtsp"]["password"] = go2rtc_config["rtsp"]["password"].format( + **FRIGATE_ENV_VARS + ) # ensure ffmpeg path is set correctly path = config.get("ffmpeg", {}).get("path", "default") diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py index 0b1ec2331..04bbfac7b 100644 --- a/frigate/config/camera/ffmpeg.py +++ b/frigate/config/camera/ffmpeg.py @@ -21,7 +21,7 @@ __all__ = [ FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning", "-threads", "2"] FFMPEG_INPUT_ARGS_DEFAULT = "preset-rtsp-generic" -RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = "preset-record-generic" +RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = "preset-record-generic-audio-aac" DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = [ "-threads", "2", From 124cc4c9cce02c22d87361f56a4ce4566c640c64 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 12 Mar 2025 18:45:16 -0500 Subject: [PATCH 4/5] Rename identifier field (#17128) * backend rename * frontend * docs * fix api path --- .../license_plate_recognition.md | 8 +- docs/docs/integrations/mqtt.md | 8 +- frigate/api/app.py | 38 +++--- .../api/defs/query/events_query_parameters.py | 4 +- frigate/api/event.py | 110 ++++++++++------- frigate/camera/state.py | 7 +- frigate/comms/event_metadata_updater.py | 2 +- .../common/license_plate/mixin.py | 3 +- frigate/events/maintainer.py | 15 ++- frigate/track/object_processing.py | 27 +++-- frigate/track/tracked_object.py | 10 +- .../overlay/detail/SearchDetailDialog.tsx | 22 ++-- .../overlay/dialog/SearchFilterDialog.tsx | 113 ++++++++++-------- web/src/pages/Explore.tsx | 6 +- web/src/types/search.ts | 8 +- web/src/views/search/SearchView.tsx | 15 ++- 16 files changed, 238 insertions(+), 158 deletions(-) diff --git a/docs/docs/configuration/license_plate_recognition.md b/docs/docs/configuration/license_plate_recognition.md index f8b9030ff..776f30cf9 100644 --- a/docs/docs/configuration/license_plate_recognition.md +++ b/docs/docs/configuration/license_plate_recognition.md @@ -3,16 +3,16 @@ id: license_plate_recognition title: License Plate Recognition (LPR) --- -Frigate can recognize license plates on vehicles and automatically add the detected characters to the `identifier` field or a known name as a `sub_label` to objects that are of type `car`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street. +Frigate can recognize license plates on vehicles and automatically add the detected characters to the `recognized_license_plate` field or a known name as a `sub_label` to objects that are of type `car`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street. LPR works best when the license plate is clearly visible to the camera. For moving vehicles, Frigate continuously refines the recognition process, keeping the most confident result. However, LPR does not run on stationary vehicles. When a plate is recognized, the recognized name is: -- Added to the `car` tracked object as a `sub_label` (if known) or the `identifier` field (if unknown) +- Added to the `car` tracked object as a `sub_label` (if known) or the `recognized_license_plate` field (if unknown) - Viewable in the Review Item Details pane in Review and the Tracked Object Details pane in Explore. - Filterable through the More Filters menu in Explore. -- Published via the `frigate/events` MQTT topic as a `sub_label` (known) or `identifier` (unknown) for the tracked object. +- Published via the `frigate/events` MQTT topic as a `sub_label` (known) or `recognized_license_plate` (unknown) for the tracked object. ## Model Requirements @@ -71,7 +71,7 @@ Fine-tune the LPR feature using these optional parameters: - **`known_plates`**: List of strings or regular expressions that assign custom a `sub_label` to `car` objects when a recognized plate matches a known value. - These labels appear in the UI, filters, and notifications. - - Unknown plates are still saved but are added to the `identifier` field rather than the `sub_label`. + - Unknown plates are still saved but are added to the `recognized_license_plate` field rather than the `sub_label`. - **`match_distance`**: Allows for minor variations (missing/incorrect characters) when matching a detected plate to a known plate. - For example, setting `match_distance: 1` allows a plate `ABCDE` to match `ABCBE` or `ABCD`. - This parameter will _not_ operate on known plates that are defined as regular expressions. You should define the full string of your plate in `known_plates` in order to use `match_distance`. diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index bcdbe7046..abbc12974 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -55,8 +55,8 @@ Message published for each changed tracked object. The first message is publishe "current_attributes": [], // detailed data about the current attributes in this frame "current_estimated_speed": 0.71, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled "velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled - "identifier": "ABC12345", // an identifier for this object - in this case, an unrecognized license plate - "identifier_score": 0.933451 + "recognized_license_plate": "ABC12345", // a recognized license plate for car objects + "recognized_license_plate_score": 0.933451 }, "after": { "id": "1607123955.475377-mxklsc", @@ -96,8 +96,8 @@ Message published for each changed tracked object. The first message is publishe ], "current_estimated_speed": 0.77, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled "velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled - "identifier": "ABC12345", // an identifier for this object - in this case, an unrecognized license plate - "identifier_score": 0.933451 + "recognized_license_plate": "ABC12345", // a recognized license plate for car objects + "recognized_license_plate_score": 0.933451 } } ``` diff --git a/frigate/api/app.py b/frigate/api/app.py index d9a57a3c1..05013ed12 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -619,37 +619,39 @@ def get_sub_labels(split_joined: Optional[int] = None): return JSONResponse(content=sub_labels) -@router.get("/identifiers") -def get_identifiers(split_joined: Optional[int] = None): +@router.get("/recognized_license_plates") +def get_recognized_license_plates(split_joined: Optional[int] = None): try: events = Event.select(Event.data).distinct() except Exception: return JSONResponse( - content=({"success": False, "message": "Failed to get identifiers"}), + content=( + {"success": False, "message": "Failed to get recognized license plates"} + ), status_code=404, ) - identifiers = [] + recognized_license_plates = [] for e in events: - if e.data is not None and "identifier" in e.data: - identifiers.append(e.data["identifier"]) + if e.data is not None and "recognized_license_plate" in e.data: + recognized_license_plates.append(e.data["recognized_license_plate"]) - while None in identifiers: - identifiers.remove(None) + while None in recognized_license_plates: + recognized_license_plates.remove(None) if split_joined: - original_identifiers = identifiers.copy() - for identifier in original_identifiers: - if identifier and "," in identifier: - identifiers.remove(identifier) - parts = identifier.split(",") + original_recognized_license_plates = recognized_license_plates.copy() + for recognized_license_plate in original_recognized_license_plates: + if recognized_license_plate and "," in recognized_license_plate: + recognized_license_plates.remove(recognized_license_plate) + parts = recognized_license_plate.split(",") for part in parts: - if part.strip() not in identifiers: - identifiers.append(part.strip()) + if part.strip() not in recognized_license_plates: + recognized_license_plates.append(part.strip()) - identifiers = list(set(identifiers)) - identifiers.sort() - return JSONResponse(content=identifiers) + recognized_license_plates = list(set(recognized_license_plates)) + recognized_license_plates.sort() + return JSONResponse(content=recognized_license_plates) @router.get("/timeline") diff --git a/frigate/api/defs/query/events_query_parameters.py b/frigate/api/defs/query/events_query_parameters.py index 9f73d8583..d707ba8cc 100644 --- a/frigate/api/defs/query/events_query_parameters.py +++ b/frigate/api/defs/query/events_query_parameters.py @@ -27,7 +27,7 @@ class EventsQueryParams(BaseModel): max_score: Optional[float] = None min_speed: Optional[float] = None max_speed: Optional[float] = None - identifier: Optional[str] = "all" + recognized_license_plate: Optional[str] = "all" is_submitted: Optional[int] = None min_length: Optional[float] = None max_length: Optional[float] = None @@ -56,7 +56,7 @@ class EventsSearchQueryParams(BaseModel): max_score: Optional[float] = None min_speed: Optional[float] = None max_speed: Optional[float] = None - identifier: Optional[str] = "all" + recognized_license_plate: Optional[str] = "all" sort: Optional[str] = None diff --git a/frigate/api/event.py b/frigate/api/event.py index e9cf2fea4..88a865318 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -101,7 +101,7 @@ def events(params: EventsQueryParams = Depends()): min_length = params.min_length max_length = params.max_length event_id = params.event_id - identifier = params.identifier + recognized_license_plate = params.recognized_license_plate sort = params.sort @@ -159,31 +159,44 @@ def events(params: EventsQueryParams = Depends()): sub_label_clause = reduce(operator.or_, sub_label_clauses) clauses.append((sub_label_clause)) - if identifier != "all": - # use matching so joined identifiers are included - # for example an identifier 'ABC123' would get events - # with identifiers 'ABC123' and 'ABC123, XYZ789' - identifier_clauses = [] - filtered_identifiers = identifier.split(",") + if recognized_license_plate != "all": + # use matching so joined recognized_license_plates are included + # for example a recognized license plate 'ABC123' would get events + # with recognized license plates 'ABC123' and 'ABC123, XYZ789' + recognized_license_plate_clauses = [] + filtered_recognized_license_plates = recognized_license_plate.split(",") - if "None" in filtered_identifiers: - filtered_identifiers.remove("None") - identifier_clauses.append((Event.data["identifier"].is_null())) + if "None" in filtered_recognized_license_plates: + filtered_recognized_license_plates.remove("None") + recognized_license_plate_clauses.append( + (Event.data["recognized_license_plate"].is_null()) + ) - for identifier in filtered_identifiers: + for recognized_license_plate in filtered_recognized_license_plates: # Exact matching plus list inclusion - identifier_clauses.append( - (Event.data["identifier"].cast("text") == identifier) + recognized_license_plate_clauses.append( + ( + Event.data["recognized_license_plate"].cast("text") + == recognized_license_plate + ) ) - identifier_clauses.append( - (Event.data["identifier"].cast("text") % f"*{identifier},*") + recognized_license_plate_clauses.append( + ( + Event.data["recognized_license_plate"].cast("text") + % f"*{recognized_license_plate},*" + ) ) - identifier_clauses.append( - (Event.data["identifier"].cast("text") % f"*, {identifier}*") + recognized_license_plate_clauses.append( + ( + Event.data["recognized_license_plate"].cast("text") + % f"*, {recognized_license_plate}*" + ) ) - identifier_clause = reduce(operator.or_, identifier_clauses) - clauses.append((identifier_clause)) + recognized_license_plate_clause = reduce( + operator.or_, recognized_license_plate_clauses + ) + clauses.append((recognized_license_plate_clause)) if zones != "all": # use matching so events with multiple zones @@ -367,8 +380,8 @@ def events_explore(limit: int = 10): "average_estimated_speed", "velocity_angle", "path_data", - "identifier", - "identifier_score", + "recognized_license_plate", + "recognized_license_plate_score", ] }, "event_count": label_counts[event.label], @@ -426,7 +439,7 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) has_clip = params.has_clip has_snapshot = params.has_snapshot is_submitted = params.is_submitted - identifier = params.identifier + recognized_license_plate = params.recognized_license_plate # for similarity search event_id = params.event_id @@ -496,31 +509,44 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) event_filters.append((reduce(operator.or_, zone_clauses))) - if identifier != "all": - # use matching so joined identifiers are included - # for example an identifier 'ABC123' would get events - # with identifiers 'ABC123' and 'ABC123, XYZ789' - identifier_clauses = [] - filtered_identifiers = identifier.split(",") + if recognized_license_plate != "all": + # use matching so joined recognized_license_plates are included + # for example an recognized_license_plate 'ABC123' would get events + # with recognized_license_plates 'ABC123' and 'ABC123, XYZ789' + recognized_license_plate_clauses = [] + filtered_recognized_license_plates = recognized_license_plate.split(",") - if "None" in filtered_identifiers: - filtered_identifiers.remove("None") - identifier_clauses.append((Event.data["identifier"].is_null())) + if "None" in filtered_recognized_license_plates: + filtered_recognized_license_plates.remove("None") + recognized_license_plate_clauses.append( + (Event.data["recognized_license_plate"].is_null()) + ) - for identifier in filtered_identifiers: + for recognized_license_plate in filtered_recognized_license_plates: # Exact matching plus list inclusion - identifier_clauses.append( - (Event.data["identifier"].cast("text") == identifier) + recognized_license_plate_clauses.append( + ( + Event.data["recognized_license_plate"].cast("text") + == recognized_license_plate + ) ) - identifier_clauses.append( - (Event.data["identifier"].cast("text") % f"*{identifier},*") + recognized_license_plate_clauses.append( + ( + Event.data["recognized_license_plate"].cast("text") + % f"*{recognized_license_plate},*" + ) ) - identifier_clauses.append( - (Event.data["identifier"].cast("text") % f"*, {identifier}*") + recognized_license_plate_clauses.append( + ( + Event.data["recognized_license_plate"].cast("text") + % f"*, {recognized_license_plate}*" + ) ) - identifier_clause = reduce(operator.or_, identifier_clauses) - event_filters.append((identifier_clause)) + recognized_license_plate_clause = reduce( + operator.or_, recognized_license_plate_clauses + ) + event_filters.append((recognized_license_plate_clause)) if after: event_filters.append((Event.start_time > after)) @@ -683,8 +709,8 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) "average_estimated_speed", "velocity_angle", "path_data", - "identifier", - "identifier_score", + "recognized_license_plate", + "recognized_license_plate_score", ] } diff --git a/frigate/camera/state.py b/frigate/camera/state.py index 2ffa54798..0e02c6c14 100644 --- a/frigate/camera/state.py +++ b/frigate/camera/state.py @@ -141,8 +141,11 @@ class CameraState: if ( obj.get("sub_label") and is_label_printable(obj["sub_label"][0]) ) - else obj.get("identifier", [None])[0] - if (obj.get("identifier") and obj["identifier"][0]) + else obj.get("recognized_license_plate", [None])[0] + if ( + obj.get("recognized_license_plate") + and obj["recognized_license_plate"][0] + ) else obj["label"] ) draw_box_with_label( diff --git a/frigate/comms/event_metadata_updater.py b/frigate/comms/event_metadata_updater.py index c5881e686..c702208bc 100644 --- a/frigate/comms/event_metadata_updater.py +++ b/frigate/comms/event_metadata_updater.py @@ -14,7 +14,7 @@ class EventMetadataTypeEnum(str, Enum): manual_event_end = "manual_event_end" regenerate_description = "regenerate_description" sub_label = "sub_label" - identifier = "identifier" + recognized_license_plate = "recognized_license_plate" class EventMetadataPublisher(Publisher): diff --git a/frigate/data_processing/common/license_plate/mixin.py b/frigate/data_processing/common/license_plate/mixin.py index 37530b205..751a674f5 100644 --- a/frigate/data_processing/common/license_plate/mixin.py +++ b/frigate/data_processing/common/license_plate/mixin.py @@ -1064,7 +1064,8 @@ class LicensePlateProcessingMixin: ) self.sub_label_publisher.publish( - EventMetadataTypeEnum.identifier, (id, top_plate, avg_confidence) + EventMetadataTypeEnum.recognized_license_plate, + (id, top_plate, avg_confidence), ) self.detected_license_plates[id] = { diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py index 947763f40..7788c83e9 100644 --- a/frigate/events/maintainer.py +++ b/frigate/events/maintainer.py @@ -27,7 +27,8 @@ def should_update_db(prev_event: Event, current_event: Event) -> bool: or prev_event["average_estimated_speed"] != current_event["average_estimated_speed"] or prev_event["velocity_angle"] != current_event["velocity_angle"] - or prev_event["identifier"] != current_event["identifier"] + or prev_event["recognized_license_plate"] + != current_event["recognized_license_plate"] or prev_event["path_data"] != current_event["path_data"] ): return True @@ -227,10 +228,14 @@ class EventProcessor(threading.Thread): event[Event.sub_label] = event_data["sub_label"][0] event[Event.data]["sub_label_score"] = event_data["sub_label"][1] - # only overwrite the identifier in the database if it's set - if event_data.get("identifier") is not None: - event[Event.data]["identifier"] = event_data["identifier"][0] - event[Event.data]["identifier_score"] = event_data["identifier"][1] + # only overwrite the recognized_license_plate in the database if it's set + if event_data.get("recognized_license_plate") is not None: + event[Event.data]["recognized_license_plate"] = event_data[ + "recognized_license_plate" + ][0] + event[Event.data]["recognized_license_plate_score"] = event_data[ + "recognized_license_plate" + ][1] ( Event.insert(event) diff --git a/frigate/track/object_processing.py b/frigate/track/object_processing.py index 56dd59110..b18ad97fa 100644 --- a/frigate/track/object_processing.py +++ b/frigate/track/object_processing.py @@ -346,10 +346,10 @@ class TrackedObjectProcessor(threading.Thread): return True - def set_identifier( - self, event_id: str, identifier: str | None, score: float | None + def set_recognized_license_plate( + self, event_id: str, recognized_license_plate: str | None, score: float | None ) -> None: - """Update identifier for given event id.""" + """Update recognized license plate for given event id.""" tracked_obj: TrackedObject = None for state in self.camera_states.values(): @@ -367,15 +367,18 @@ class TrackedObjectProcessor(threading.Thread): return if tracked_obj: - tracked_obj.obj_data["identifier"] = (identifier, score) + tracked_obj.obj_data["recognized_license_plate"] = ( + recognized_license_plate, + score, + ) if event: data = event.data - data["identifier"] = identifier - if identifier is None: - data["identifier_score"] = None + data["recognized_license_plate"] = recognized_license_plate + if recognized_license_plate is None: + data["recognized_license_plate_score"] = None elif score is not None: - data["identifier_score"] = score + data["recognized_license_plate_score"] = score event.data = data event.save() @@ -542,9 +545,11 @@ class TrackedObjectProcessor(threading.Thread): if topic.endswith(EventMetadataTypeEnum.sub_label.value): (event_id, sub_label, score) = payload self.set_sub_label(event_id, sub_label, score) - if topic.endswith(EventMetadataTypeEnum.identifier.value): - (event_id, identifier, score) = payload - self.set_identifier(event_id, identifier, score) + if topic.endswith(EventMetadataTypeEnum.recognized_license_plate.value): + (event_id, recognized_license_plate, score) = payload + self.set_recognized_license_plate( + event_id, recognized_license_plate, score + ) elif topic.endswith(EventMetadataTypeEnum.manual_event_create.value): self.create_manual_event(payload) elif topic.endswith(EventMetadataTypeEnum.manual_event_end.value): diff --git a/frigate/track/tracked_object.py b/frigate/track/tracked_object.py index 9c19595b5..7a4829c2a 100644 --- a/frigate/track/tracked_object.py +++ b/frigate/track/tracked_object.py @@ -153,8 +153,12 @@ class TrackedObject: "current_estimated_speed": self.current_estimated_speed, "velocity_angle": self.velocity_angle, "path_data": self.path_data, - "identifier": obj_data.get("identifier"), - "identifier_score": obj_data.get("identifier_score"), + "recognized_license_plate": obj_data.get( + "recognized_license_plate" + ), + "recognized_license_plate_score": obj_data.get( + "recognized_license_plate_score" + ), } thumb_update = True @@ -367,7 +371,7 @@ class TrackedObject: "average_estimated_speed": self.average_estimated_speed, "velocity_angle": self.velocity_angle, "path_data": self.path_data, - "identifier": self.obj_data.get("identifier"), + "recognized_license_plate": self.obj_data.get("recognized_license_plate"), } return event diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 7f69f2ca2..ed472c742 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -333,13 +333,18 @@ function ObjectDetailsTab({ } }, [search]); - const identifierScore = useMemo(() => { + const recognizedLicensePlateScore = useMemo(() => { if (!search) { return undefined; } - if (search.data.identifier && search.data?.identifier_score) { - return Math.round((search.data?.identifier_score ?? 0) * 100); + if ( + search.data.recognized_license_plate && + search.data?.recognized_license_plate_score + ) { + return Math.round( + (search.data?.recognized_license_plate_score ?? 0) * 100, + ); } else { return undefined; } @@ -550,13 +555,16 @@ function ObjectDetailsTab({
- {search?.data.identifier && ( + {search?.data.recognized_license_plate && (
-
Identifier
+
+ Recognized License Plate +
- {search.data.identifier}{" "} - {identifierScore && ` (${identifierScore}%)`} + {search.data.recognized_license_plate}{" "} + {recognizedLicensePlateScore && + ` (${recognizedLicensePlateScore}%)`}
diff --git a/web/src/components/overlay/dialog/SearchFilterDialog.tsx b/web/src/components/overlay/dialog/SearchFilterDialog.tsx index 59b30f82f..2768f8859 100644 --- a/web/src/components/overlay/dialog/SearchFilterDialog.tsx +++ b/web/src/components/overlay/dialog/SearchFilterDialog.tsx @@ -86,7 +86,7 @@ export default function SearchFilterDialog({ (currentFilter.max_speed ?? 150) < 150 || (currentFilter.zones?.length ?? 0) > 0 || (currentFilter.sub_labels?.length ?? 0) > 0 || - (currentFilter.identifier?.length ?? 0) > 0), + (currentFilter.recognized_license_plate?.length ?? 0) > 0), [currentFilter], ); @@ -128,10 +128,13 @@ export default function SearchFilterDialog({ setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels }) } /> - - setCurrentFilter({ ...currentFilter, identifier: identifiers }) + + setCurrentFilter({ + ...currentFilter, + recognized_license_plate: plate, + }) } /> @@ -847,97 +850,109 @@ export function SnapshotClipFilterContent({ ); } -type IdentifierFilterContentProps = { - identifiers: string[] | undefined; - setIdentifiers: (identifiers: string[] | undefined) => void; +type RecognizedLicensePlatesFilterContentProps = { + recognizedLicensePlates: string[] | undefined; + setRecognizedLicensePlates: ( + recognizedLicensePlates: string[] | undefined, + ) => void; }; -export function IdentifierFilterContent({ - identifiers, - setIdentifiers, -}: IdentifierFilterContentProps) { - const { data: allIdentifiers, error } = useSWR("identifiers", { - revalidateOnFocus: false, - }); - - const [selectedIdentifiers, setSelectedIdentifiers] = useState( - identifiers || [], +export function RecognizedLicensePlatesFilterContent({ + recognizedLicensePlates, + setRecognizedLicensePlates, +}: RecognizedLicensePlatesFilterContentProps) { + const { data: allRecognizedLicensePlates, error } = useSWR( + "recognized_license_plates", + { + revalidateOnFocus: false, + }, ); + + const [selectedRecognizedLicensePlates, setSelectedRecognizedLicensePlates] = + useState(recognizedLicensePlates || []); const [inputValue, setInputValue] = useState(""); useEffect(() => { - if (identifiers) { - setSelectedIdentifiers(identifiers); + if (recognizedLicensePlates) { + setSelectedRecognizedLicensePlates(recognizedLicensePlates); } else { - setSelectedIdentifiers([]); + setSelectedRecognizedLicensePlates([]); } - }, [identifiers]); + }, [recognizedLicensePlates]); - const handleSelect = (identifier: string) => { - const newSelected = selectedIdentifiers.includes(identifier) - ? selectedIdentifiers.filter((id) => id !== identifier) // Deselect - : [...selectedIdentifiers, identifier]; // Select + const handleSelect = (recognizedLicensePlate: string) => { + const newSelected = selectedRecognizedLicensePlates.includes( + recognizedLicensePlate, + ) + ? selectedRecognizedLicensePlates.filter( + (id) => id !== recognizedLicensePlate, + ) // Deselect + : [...selectedRecognizedLicensePlates, recognizedLicensePlate]; // Select - setSelectedIdentifiers(newSelected); + setSelectedRecognizedLicensePlates(newSelected); if (newSelected.length === 0) { - setIdentifiers(undefined); // Clear filter if no identifiers selected + setRecognizedLicensePlates(undefined); // Clear filter if no plates selected } else { - setIdentifiers(newSelected); + setRecognizedLicensePlates(newSelected); } }; - if (!allIdentifiers || allIdentifiers.length === 0) { + if (!allRecognizedLicensePlates || allRecognizedLicensePlates.length === 0) { return null; } - const filteredIdentifiers = - allIdentifiers?.filter((id) => + const filteredRecognizedLicensePlates = + allRecognizedLicensePlates?.filter((id) => id.toLowerCase().includes(inputValue.toLowerCase()), ) || []; return (
-
Identifiers
+
Recognized License Plates
{error ? ( -

Failed to load identifiers

- ) : !allIdentifiers ? ( -

Loading identifiers...

+

+ Failed to load recognized license plates. +

+ ) : !allRecognizedLicensePlates ? ( +

+ Loading recognized license plates... +

) : ( <> - {filteredIdentifiers.length === 0 && inputValue && ( - No identifiers found. + {filteredRecognizedLicensePlates.length === 0 && inputValue && ( + No license plates found. )} - {filteredIdentifiers.map((identifier) => ( + {filteredRecognizedLicensePlates.map((plate) => ( handleSelect(identifier)} + key={plate} + value={plate} + onSelect={() => handleSelect(plate)} className="cursor-pointer" > - {identifier} + {plate} ))} - {selectedIdentifiers.length > 0 && ( + {selectedRecognizedLicensePlates.length > 0 && (
- {selectedIdentifiers.map((id) => ( + {selectedRecognizedLicensePlates.map((id) => ( )}

- Select one or more identifiers from the list. + Select one or more plates from the list.

); diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index cf24ff70d..31fe81d04 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -105,7 +105,8 @@ export default function Explore() { cameras: searchSearchParams["cameras"], labels: searchSearchParams["labels"], sub_labels: searchSearchParams["sub_labels"], - identifier: searchSearchParams["identifier"], + recognized_license_plate: + searchSearchParams["recognized_license_plate"], zones: searchSearchParams["zones"], before: searchSearchParams["before"], after: searchSearchParams["after"], @@ -141,7 +142,8 @@ export default function Explore() { cameras: searchSearchParams["cameras"], labels: searchSearchParams["labels"], sub_labels: searchSearchParams["sub_labels"], - identifier: searchSearchParams["identifier"], + recognized_license_plate: + searchSearchParams["recognized_license_plate"], zones: searchSearchParams["zones"], before: searchSearchParams["before"], after: searchSearchParams["after"], diff --git a/web/src/types/search.ts b/web/src/types/search.ts index 90bcd54d7..5dca11973 100644 --- a/web/src/types/search.ts +++ b/web/src/types/search.ts @@ -58,8 +58,8 @@ export type SearchResult = { average_estimated_speed: number; velocity_angle: number; path_data: [number[], number][]; - identifier?: string; - identifier_score?: number; + recognized_license_plate?: string; + recognized_license_plate_score?: number; }; }; @@ -68,7 +68,7 @@ export type SearchFilter = { cameras?: string[]; labels?: string[]; sub_labels?: string[]; - identifier?: string[]; + recognized_license_plate?: string[]; zones?: string[]; before?: number; after?: number; @@ -92,7 +92,7 @@ export type SearchQueryParams = { cameras?: string[]; labels?: string[]; sub_labels?: string[]; - identifier?: string[]; + recognized_license_plate?: string[]; zones?: string[]; before?: string; after?: string; diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index 7f1b1e4a1..a8d241c00 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -121,7 +121,9 @@ export default function SearchView({ }, [config, searchFilter]); const { data: allSubLabels } = useSWR("sub_labels"); - const { data: allIdentifiers } = useSWR("identifiers"); + const { data: allRecognizedLicensePlates } = useSWR( + "recognized_license_plates", + ); const allZones = useMemo(() => { if (!config) { @@ -161,13 +163,20 @@ export default function SearchView({ max_score: ["100"], min_speed: ["1"], max_speed: ["150"], - identifier: allIdentifiers, + recognized_license_plate: allRecognizedLicensePlates, has_clip: ["yes", "no"], has_snapshot: ["yes", "no"], ...(config?.plus?.enabled && searchFilter?.has_snapshot && { is_submitted: ["yes", "no"] }), }), - [config, allLabels, allZones, allSubLabels, allIdentifiers, searchFilter], + [ + config, + allLabels, + allZones, + allSubLabels, + allRecognizedLicensePlates, + searchFilter, + ], ); // remove duplicate event ids From b8f2d8fb0c94ab4ebfac51858a19f989972c0011 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 12 Mar 2025 20:23:06 -0600 Subject: [PATCH 5/5] Fix rtsp config access (#17129) --- docker/main/rootfs/usr/local/go2rtc/create_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index 5d8e80f9d..d7c21c7f7 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -80,12 +80,12 @@ if go2rtc_config["webrtc"].get("candidates") is None: go2rtc_config["webrtc"]["candidates"] = default_candidates -if go2rtc_config["rtsp"].get("username") is not None: +if go2rtc_config.get("rtsp", {}).get("username") is not None: go2rtc_config["rtsp"]["username"] = go2rtc_config["rtsp"]["username"].format( **FRIGATE_ENV_VARS ) -if go2rtc_config["rtsp"].get("password") is not None: +if go2rtc_config.get("rtsp", {}).get("password") is not None: go2rtc_config["rtsp"]["password"] = go2rtc_config["rtsp"]["password"].format( **FRIGATE_ENV_VARS )