From 0a8f499640c753cf4b8f66b3821519764a8183d4 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 18 Jan 2026 07:36:27 -0600 Subject: [PATCH] Miscellaneous fixes (0.17 beta) (#21683) * misc triggers tweaks i18n fixes fix toaster color fix clicking on labels selecting incorrect checkbox * update copilot instructions * lpr docs tweaks * add retry params to gemini * i18n fix * ensure users only see recognized plates from accessible cameras in explore * ensure all zone filters are converted to pixels zone-level filters were never converted from percentage area to pixels. RuntimeFilterConfig was only applied to filters at the camera level, not zone.filters. Fixes https://github.com/blakeblackshear/frigate/discussions/21694 * add test for percentage based zone filters * use export id for key instead of name * update gemini docs --- .github/copilot-instructions.md | 5 ++- docs/docs/configuration/genai.md | 6 +-- .../license_plate_recognition.md | 6 +-- frigate/api/app.py | 17 ++++++-- frigate/config/config.py | 7 +++ frigate/genai/gemini.py | 8 ++++ frigate/test/test_config.py | 43 +++++++++++++++++++ web/public/locales/en/common.json | 1 + .../overlay/CreateTriggerDialog.tsx | 13 +++--- .../wizard/Step3ThresholdAndActions.tsx | 15 ++++--- web/src/pages/Exports.tsx | 2 +- web/src/views/settings/TriggerView.tsx | 11 +++-- 12 files changed, 105 insertions(+), 29 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6c14bb166..89acd8a9b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,2 +1,3 @@ -Never write strings in the frontend directly, always write to and reference the relevant translations file. -Always conform new and refactored code to the existing coding style in the project. +- For Frigate NVR, never write strings in the frontend directly. Since the project uses `react-i18next`, use `t()` and write the English string in the relevant translations file in `web/public/locales/en`. +- Always conform new and refactored code to the existing coding style in the project. +- Always have a way to test your work and confirm your changes. When running backend tests, use `python3 -u -m unittest`. diff --git a/docs/docs/configuration/genai.md b/docs/docs/configuration/genai.md index 875608766..292bf437a 100644 --- a/docs/docs/configuration/genai.md +++ b/docs/docs/configuration/genai.md @@ -66,8 +66,6 @@ Some models are labeled as **hybrid** (capable of both thinking and instruct tas **Recommendation:** Always select the `-instruct` or documented instruct/tagged variant of any model you use in your Frigate configuration. If in doubt, refer to your model provider’s documentation or model library for guidance on the correct model variant to use. - - ### Supported Models You must use a vision capable model with Frigate. Current model variants can be found [in their model library](https://ollama.com/search?c=vision). Note that Frigate will not automatically download the model you specify in your config, you must download the model to your local instance of Ollama first i.e. by running `ollama pull qwen3-vl:2b-instruct` on your Ollama server/Docker container. Note that the model specified in Frigate's config must match the downloaded model tag. @@ -93,7 +91,7 @@ genai: ## Google Gemini -Google Gemini has a free tier allowing [15 queries per minute](https://ai.google.dev/pricing) to the API, which is more than sufficient for standard Frigate usage. +Google Gemini has a [free tier](https://ai.google.dev/pricing) for the API, however the limits may not be sufficient for standard Frigate usage. Choose a plan appropriate for your installation. ### Supported Models @@ -114,7 +112,7 @@ To start using Gemini, you must first get an API key from [Google AI Studio](htt genai: provider: gemini api_key: "{FRIGATE_GEMINI_API_KEY}" - model: gemini-2.0-flash + model: gemini-2.5-flash ``` :::note diff --git a/docs/docs/configuration/license_plate_recognition.md b/docs/docs/configuration/license_plate_recognition.md index a18c822f5..5f70dd9a0 100644 --- a/docs/docs/configuration/license_plate_recognition.md +++ b/docs/docs/configuration/license_plate_recognition.md @@ -68,8 +68,8 @@ Fine-tune the LPR feature using these optional parameters at the global level of - Default: `1000` pixels. Note: this is intentionally set very low as it is an _area_ measurement (length x width). For reference, 1000 pixels represents a ~32x32 pixel square in your camera image. - Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant plates. - **`device`**: Device to use to run license plate detection _and_ recognition models. - - Default: `CPU` - - This can be `CPU`, `GPU`, or the GPU's device number. For users without a model that detects license plates natively, using a GPU may increase performance of the YOLOv9 license plate detector model. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. However, for users who run a model that detects `license_plate` natively, there is little to no performance gain reported with running LPR on GPU compared to the CPU. + - Default: `None` + - This is auto-selected by Frigate and can be `CPU`, `GPU`, or the GPU's device number. For users without a model that detects license plates natively, using a GPU may increase performance of the YOLOv9 license plate detector model. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. However, for users who run a model that detects `license_plate` natively, there is little to no performance gain reported with running LPR on GPU compared to the CPU. - **`model_size`**: The size of the model used to identify regions of text on plates. - Default: `small` - This can be `small` or `large`. @@ -432,6 +432,6 @@ If you are using a model that natively detects `license_plate`, add an _object m If you are not using a model that natively detects `license_plate` or you are using dedicated LPR camera mode, only a _motion mask_ over your text is required. -### I see "Error running ... model" in my logs. How can I fix this? +### I see "Error running ... model" in my logs, or my inference time is very high. How can I fix this? This usually happens when your GPU is unable to compile or use one of the LPR models. Set your `device` to `CPU` and try again. GPU acceleration only provides a slight performance increase, and the models are lightweight enough to run without issue on most CPUs. diff --git a/frigate/api/app.py b/frigate/api/app.py index c87e929a6..440adfce4 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -23,7 +23,12 @@ from markupsafe import escape from peewee import SQL, fn, operator from pydantic import ValidationError -from frigate.api.auth import allow_any_authenticated, allow_public, require_role +from frigate.api.auth import ( + allow_any_authenticated, + allow_public, + get_allowed_cameras_for_filter, + require_role, +) from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters from frigate.api.defs.request.app_body import AppConfigSetBody from frigate.api.defs.tags import Tags @@ -687,13 +692,19 @@ def plusModels(request: Request, filterByCurrentModelDetector: bool = False): @router.get( "/recognized_license_plates", dependencies=[Depends(allow_any_authenticated())] ) -def get_recognized_license_plates(split_joined: Optional[int] = None): +def get_recognized_license_plates( + split_joined: Optional[int] = None, + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): try: query = ( Event.select( SQL("json_extract(data, '$.recognized_license_plate') AS plate") ) - .where(SQL("json_extract(data, '$.recognized_license_plate') IS NOT NULL")) + .where( + (SQL("json_extract(data, '$.recognized_license_plate') IS NOT NULL")) + & (Event.camera << allowed_cameras) + ) .distinct() ) recognized_license_plates = [row[0] for row in query.tuples()] diff --git a/frigate/config/config.py b/frigate/config/config.py index a9c54976e..a26d4c50e 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -662,6 +662,13 @@ class FrigateConfig(FrigateBaseModel): # generate zone contours if len(camera_config.zones) > 0: for zone in camera_config.zones.values(): + if zone.filters: + for object_name, filter_config in zone.filters.items(): + zone.filters[object_name] = RuntimeFilterConfig( + frame_shape=camera_config.frame_shape, + **filter_config.model_dump(exclude_unset=True), + ) + zone.generate_contour(camera_config.frame_shape) # Set live view stream if none is set diff --git a/frigate/genai/gemini.py b/frigate/genai/gemini.py index 36e708594..83bd3340d 100644 --- a/frigate/genai/gemini.py +++ b/frigate/genai/gemini.py @@ -24,6 +24,14 @@ class GeminiClient(GenAIClient): http_options_dict = { "api_version": "v1", "timeout": int(self.timeout * 1000), # requires milliseconds + "retry_options": types.HttpRetryOptions( + attempts=3, + initial_delay=1.0, + max_delay=60.0, + exp_base=2.0, + jitter=1.0, + http_status_codes=[429, 500, 502, 503, 504], + ), } if isinstance(self.genai_config.provider_options, dict): diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py index 4bafe7369..afe577f2f 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -632,6 +632,49 @@ class TestConfig(unittest.TestCase): ) assert frigate_config.cameras["back"].zones["test"].color != (0, 0, 0) + def test_zone_filter_area_percent_converts_to_pixels(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": { + "alerts": { + "retain": { + "days": 20, + } + } + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "zones": { + "notification": { + "coordinates": "0.03,1,0.025,0,0.626,0,0.643,1", + "objects": ["person"], + "filters": {"person": {"min_area": 0.1}}, + } + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + expected_min_area = int(1080 * 1920 * 0.1) + assert ( + frigate_config.cameras["back"] + .zones["notification"] + .filters["person"] + .min_area + == expected_min_area + ) + def test_zone_relative_matches_explicit(self): config = { "mqtt": {"host": "mqtt"}, diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json index 2ae6297a1..8bf13ca61 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -3,6 +3,7 @@ "untilForTime": "Until {{time}}", "untilForRestart": "Until Frigate restarts.", "untilRestart": "Until restart", + "never": "Never", "ago": "{{timeAgo}} ago", "justNow": "Just now", "today": "Today", diff --git a/web/src/components/overlay/CreateTriggerDialog.tsx b/web/src/components/overlay/CreateTriggerDialog.tsx index 11734acaf..ef30c649d 100644 --- a/web/src/components/overlay/CreateTriggerDialog.tsx +++ b/web/src/components/overlay/CreateTriggerDialog.tsx @@ -268,7 +268,7 @@ export default function CreateTriggerDialog({
- {t("enabled", { ns: "common" })} + {t("button.enabled", { ns: "common" })}
{t("triggers.dialog.form.enabled.description")} @@ -394,7 +394,10 @@ export default function CreateTriggerDialog({
{availableActions.map((action) => ( -
+
+ + ))}
diff --git a/web/src/components/trigger/wizard/Step3ThresholdAndActions.tsx b/web/src/components/trigger/wizard/Step3ThresholdAndActions.tsx index 33568dca7..2b560ded3 100644 --- a/web/src/components/trigger/wizard/Step3ThresholdAndActions.tsx +++ b/web/src/components/trigger/wizard/Step3ThresholdAndActions.tsx @@ -142,7 +142,10 @@ export default function Step3ThresholdAndActions({ {t("triggers.dialog.form.actions.title")}
{availableActions.map((action) => ( -
+
+ + ))}
@@ -197,9 +200,7 @@ export default function Step3ThresholdAndActions({ {isLoading && } {isLoading ? t("button.saving", { ns: "common" }) - : t("triggers.dialog.form.save", { - defaultValue: "Save Trigger", - })} + : t("button.save", { ns: "common" })}
diff --git a/web/src/pages/Exports.tsx b/web/src/pages/Exports.tsx index 69e9dc0e7..26a75801a 100644 --- a/web/src/pages/Exports.tsx +++ b/web/src/pages/Exports.tsx @@ -206,7 +206,7 @@ function Exports() { > {Object.values(exports).map((item) => ( {trigger_status?.triggers[trigger.name] ?.triggering_event_id && ( @@ -663,7 +664,9 @@ export default function TriggerView({ - {t("name", { ns: "common" })} + + {t("name", { ns: "triggers.table.name" })} + {t("triggers.table.type")} {t("triggers.table.lastTriggered")} @@ -759,7 +762,7 @@ export default function TriggerView({ date_style: "medium", }, ) - : "Never"} + : t("time.never", { ns: "common" })} {trigger_status?.triggers[trigger.name] ?.triggering_event_id && (