Compare commits

...

140 Commits

Author SHA1 Message Date
Nicolas Mowen
7348d177fd Fix import issues 2026-02-14 16:04:41 -07:00
Nicolas Mowen
9187cdc71c Cleanup 2026-02-14 15:54:20 -07:00
Nicolas Mowen
82dc2ab1eb Support getting client via manager 2026-02-14 15:53:56 -07:00
Nicolas Mowen
599638d145 Convert to roles list 2026-02-14 15:49:04 -07:00
Nicolas Mowen
d8d96a6886 Add config migration 2026-02-14 15:48:22 -07:00
Nicolas Mowen
4e4d6e2532 GenAI client manager 2026-02-14 15:37:53 -07:00
nulledy
a153ecb5aa Allow API Events to be Detections or Alerts, depending on the Event Label (#21923)
* - API created events will be alerts OR detections, depending on the event label, defaulting to alerts
- Indefinite API events will extend the recording segment until those events are ended
- API event start time is the actual start time, instead of having a pre-buffer of record.event_pre_capture

* Instead of checking for indefinite events on a camera before deciding if we should end the segment, only update last_detection_time and last_alert_time if frame_time is greater, which should have the same effect

* Add the ability to set a pre_capture number of seconds when creating a manual event via the API. Default behavior unchanged

* Remove unnecessary _publish_segment_start() call

* Formatting

* handle last_alert_time or last_detection_time being None when checking them against the frame_time

* comment manual_info["label"].split(": ")[0] for clarity
2026-02-14 15:22:50 -07:00
Josh Hawkins
196657c75a Improve jsmpeg player websocket handling (#21943)
* improve jsmpeg player websocket handling

prevent websocket console messages from appearing when player is destroyed

* reformat files after ruff upgrade
2026-02-14 15:22:50 -07:00
FL42
4bce1a3035 feat: add X-Frame-Time when returning snapshot (#21932)
Co-authored-by: Florent MORICONI <170678386+fmcloudconsulting@users.noreply.github.com>
2026-02-14 15:22:50 -07:00
Eric Work
4847659f7c Add networking options for configuring listening ports (#21779) 2026-02-14 15:22:49 -07:00
Nicolas Mowen
41123056aa Add live context tool to LLM (#21754)
* Add live context tool

* Improve handling of images in request

* Improve prompt caching
2026-02-14 15:22:30 -07:00
Nicolas Mowen
c893cb639e Update to ROCm 7.2.0 (#21753)
* Update to ROCm 7.2.0

* ROCm now works properly with JinaV1

* Arcface has compilation error
2026-02-14 15:22:29 -07:00
Josh Hawkins
a78d39a320 Offline preview image (#21752)
* use latest preview frame for latest image when camera is offline

* remove frame extraction logic

* tests

* frontend

* add description to api endpoint
2026-02-14 15:22:06 -07:00
Nicolas Mowen
7cb2c11ff9 Implement LLM Chat API with tool calling support (#21731)
* Implement initial tools definiton APIs

* Add initial chat completion API with tool support

* Implement other providers

* Cleanup
2026-02-14 15:22:06 -07:00
John Shaw
8507d917d2 Remove parents in remove_empty_directories (#21726)
The original implementation did a full directory tree walk to find and remove
empty directories, so this implementation should remove the parents as well,
like the original did.
2026-02-14 15:22:06 -07:00
Nicolas Mowen
3788cc27e4 Implement llama.cpp GenAI Provider (#21690)
* Implement llama.cpp GenAI Provider

* Add docs

* Update links

* Fix broken mqtt links

* Fix more broken anchors
2026-02-14 15:22:03 -07:00
John Shaw
0bbf3bb191 Optimize empty directory cleanup for recordings (#21695)
The previous empty directory cleanup did a full recursive directory
walk, which can be extremely slow. This new implementation only removes
directories which have a chance of being empty due to a recent file
deletion.
2026-02-14 15:21:23 -07:00
Nicolas Mowen
08fb45e455 Refactor Time-Lapse Export (#21668)
* refactor time lapse creation to be a separate API call with ability to pass arbitrary ffmpeg args

* Add CPU fallback
2026-02-14 15:21:23 -07:00
Eugeny Tulupov
dd27a28ee2 Update go2rtc to v1.9.13 (#21648)
Co-authored-by: Eugeny Tulupov <eugeny.tulupov@spirent.com>
2026-02-14 15:21:22 -07:00
Josh Hawkins
1d95f66fca Fix incorrect counting in sync_recordings (#21626) 2026-02-14 15:21:22 -07:00
Josh Hawkins
4d8e67bce3 use same logging pattern in sync_recordings as the other sync functions (#21625) 2026-02-14 15:21:22 -07:00
Josh Hawkins
16cff564c5 Media sync API refactor and UI (#21542)
* generic job infrastructure

* types and dispatcher changes for jobs

* save data in memory only for completed jobs

* implement media sync job and endpoints

* change logs to debug

* websocket hook and types

* frontend

* i18n

* docs tweaks

* endpoint descriptions

* tweak docs
2026-02-14 15:21:22 -07:00
Josh Hawkins
a57393fb50 Add media sync API endpoint (#21526)
* add media cleanup functions

* add endpoint

* remove scheduled sync recordings from cleanup

* move to utils dir

* tweak import

* remove sync_recordings and add config migrator

* remove sync_recordings

* docs

* remove key

* clean up docs

* docs fix

* docs tweak
2026-02-14 15:21:22 -07:00
Nicolas Mowen
9003683813 Add API to handle deleting recordings (#21520)
* Add recording delete API

* Re-organize recordings apis

* Fix import

* Consolidate query types
2026-02-14 15:21:22 -07:00
Nicolas Mowen
6fbb6a843c Exports Improvements (#21521)
* Add images to case folder view

* Add ability to select case in export dialog

* Add to mobile review too
2026-02-14 15:21:22 -07:00
Nicolas Mowen
64f5f0b345 Add support for GPU and NPU temperatures (#21495)
* Add rockchip temps

* Add support for GPU and NPU temperatures in the frontend

* Add support for Nvidia temperature

* Improve separation

* Adjust graph scaling
2026-02-14 15:21:22 -07:00
Andrew Roberts
bad0e92dde Camera-specific hwaccel settings for timelapse exports (correct base) (#21386)
* added hwaccel_args to camera.record.export config struct

* populate camera.record.export.hwaccel_args with a cascade up to camera then global if 'auto'

* use new hwaccel args in export

* added documentation for camera-specific hwaccel export

* fix c/p error

* missed an import

* fleshed out the docs and comments a bit

* ruff lint

* separated out the tips in the doc

* fix documentation

* fix and simplify reference config doc
2026-02-14 15:21:22 -07:00
Nicolas Mowen
096cf494ad Refactor temperature reporting for detectors and implement Hailo temp reading (#21395)
* Add Hailo temperature retrieval

* Refactor `get_hailo_temps()` to use ctxmanager

* Show Hailo temps in system UI

* Move hailo_platform import to get_hailo_temps

* Refactor temperatures calculations to use within detector block

* Adjust webUI to handle new location

---------

Co-authored-by: tigattack <10629864+tigattack@users.noreply.github.com>
2026-02-14 15:21:22 -07:00
Nicolas Mowen
6df07a6df7 Export filter UI (#21322)
* Get started on export filters

* implement basic filter

* Implement filtering and adjust api

* Improve filter handling

* Improve navigation

* Cleanup

* handle scrolling
2026-02-14 15:21:22 -07:00
Josh Hawkins
194a3dbbba Camera connection quality indicator (#21297)
* add camera connection quality metrics and indicator

* formatting

* move stall calcs to watchdog

* clean up

* change watchdog to 1s and separately track time for ffmpeg retry_interval

* implement status caching to reduce message volume
2026-02-14 15:21:22 -07:00
Nicolas Mowen
b05b26fe44 Case management UI (#21299)
* Refactor export cards to match existing cards in other UI pages

* Show cases separately from exports

* Add proper filtering and display of cases

* Add ability to edit and select cases for exports

* Cleanup typing

* Hide if no unassigned

* Cleanup hiding logic

* fix scrolling

* Improve layout
2026-02-14 15:21:21 -07:00
Josh Hawkins
de93181444 refactor vainfo to search for first GPU (#21296)
use existing LibvaGpuSelector to pick appropritate libva device
2026-02-14 15:20:26 -07:00
Nicolas Mowen
2009634f1c implement case management for export apis (#21295) 2026-02-14 15:20:26 -07:00
Nicolas Mowen
76755d7708 Create scaffolding for case management (#21293) 2026-02-14 15:20:25 -07:00
Nicolas Mowen
c2833eb8df Update version 2026-02-14 15:20:25 -07:00
Josh Hawkins
4dcd2968b3 consolidate attribute filtering to match non-english and url encoded values (#22002) 2026-02-14 08:33:17 -06:00
Nicolas Mowen
73c1e12faf Fix saving attributes for object to DB (#22000) 2026-02-14 07:40:08 -06:00
Josh Hawkins
5f93cee732 fix tooltips (#21989) 2026-02-13 07:22:28 -06:00
Josh Hawkins
67e3f8eefa Miscellaneous fixes (0.17 beta) (#21934)
* improve chip tooltip display

- use formatList to use i18n separators instead of commas
- ensure the correct event type is used so sublabels are not run through normalization
- remove smart-capitalization classes as translated labels use i18n (which includes capitalization)
- give icons an optional key so that the console doesn't complain about duplication when rendering

* Add grace period for recording segment checks to prevent spurious ffmpeg restarts

* add admin precedence to proxy role_map resolution to prevent downgrade

* clean up

* formatting

* work around radix pointer events issue when dialog is opened from drawer

fixes https://github.com/blakeblackshear/frigate/discussions/21940

* prevent console warnings about missing titles and descriptions

make these invisible with sr-only

* remove duplicate language

* Adjust handling for device sizes

* Cleanup

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2026-02-12 13:42:08 -07:00
GuoQing Liu
e1005ac2a5 fix: fix object classification model not reload (#21982) 2026-02-12 08:56:52 -07:00
Hosted Weblate
6accc38275 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (651 of 651 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (651 of 651 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: OverTheHillsAndFarAway <prosjektx@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/nb_NO/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2026-02-07 10:42:52 -07:00
Hosted Weblate
ff20be58b4 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (651 of 651 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (651 of 651 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (54 of 54 strings)

Co-authored-by: GuoQing Liu <842607283@qq.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/zh_Hans/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2026-02-07 10:42:52 -07:00
Hosted Weblate
fc3f798bd6 Translated using Weblate (Swedish)
Currently translated at 100.0% (651 of 651 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (217 of 217 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/sv/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2026-02-07 10:42:52 -07:00
Hosted Weblate
44e695362a Translated using Weblate (French)
Currently translated at 100.0% (651 of 651 strings)

Translated using Weblate (French)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (French)

Currently translated at 100.0% (217 of 217 strings)

Co-authored-by: Apocoloquintose <bertrand.moreux@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/fr/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2026-02-07 10:42:52 -07:00
Hosted Weblate
9fbc854bf5 Translated using Weblate (Polish)
Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (651 of 651 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (54 of 54 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kamil AvH <kamil.arszagi@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/pl/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2026-02-07 10:42:52 -07:00
Hosted Weblate
334acd6078 Translated using Weblate (Catalan)
Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (651 of 651 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (217 of 217 strings)

Co-authored-by: Eduardo Pastor Fernández <123eduardoneko123@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ca/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2026-02-07 10:42:52 -07:00
Hosted Weblate
92c503070c Translated using Weblate (Japanese)
Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (135 of 136 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (216 of 217 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (54 of 54 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Yusuke, Hirota <hirota.yusuke@jp.fujitsu.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/ja/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-system
2026-02-07 10:42:52 -07:00
Hosted Weblate
ecd7d04228 Translated using Weblate (Romanian)
Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (651 of 651 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (54 of 54 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: lukasig <lukasig@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ro/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2026-02-07 10:42:52 -07:00
Hosted Weblate
11576e9e68 Translated using Weblate (Estonian)
Currently translated at 29.4% (192 of 651 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Estonian)

Currently translated at 33.0% (45 of 136 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/et/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-settings
2026-02-07 10:42:52 -07:00
Hosted Weblate
2cfb118981 Translated using Weblate (Danish)
Currently translated at 39.1% (196 of 501 strings)

Translated using Weblate (Danish)

Currently translated at 56.1% (55 of 98 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (25 of 25 strings)

Co-authored-by: Bjorn Jorgensen <github@bjornz.dk>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/da/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-player
Translation: Frigate NVR/views-live
2026-02-07 10:42:52 -07:00
Hosted Weblate
e1c273be8d Translated using Weblate (German)
Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (German)

Currently translated at 100.0% (651 of 651 strings)

Translated using Weblate (German)

Currently translated at 100.0% (217 of 217 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sebastian Sie <sebastian.neuplanitz@googlemail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/de/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2026-02-07 10:42:52 -07:00
Josh Hawkins
ea1533f456 Miscellaneous Fixes (0.17 beta) (#21912)
* fix display of custom sublabels in review item chip

use "manual" as type so it's not run through translation and normalized, producing "Josh S Car" instead of "Josh's Car"

* use css instead of js for reviewed button hover state in filmstrip
2026-02-07 09:06:55 -07:00
Nicolas Mowen
41b983a133 Set log to debug (#21898) 2026-02-05 12:31:07 -07:00
GuoQing Liu
c9055ea941 fix: fix delete zone type i18n (#21894) 2026-02-05 09:38:09 -06:00
Nicolas Mowen
c9ba851f0d Fix event getting stuck due to only checking current clip / snapshot (#21893)
* Fix event getting stuck due to only checking current clip / snapshot

* formatting
2026-02-05 08:43:19 -06:00
Pops0n
a8ab82937b Update hailo installation instructions and script (#21882)
* Update installation.md for Raspberry Pi and Hailo

Updated Hailo installation instructions to cover both Bookworm and Trixie OS on Raspberry Pi. 


Referenced discussions: #21177, #20621, #20062, #19531

* Update user_installation.sh for Raspberry Pi (Bookworm and Trixie)

Simplified and improved the user installation script for  Hailo to support Raspberry Pi OS Bookworm, Trixie, and x86 platforms.

Referenced discussions: #21177, #20621, #20062, #19531

* Update installation.md

* Update user_installation.sh

* Update installation.md

* Update installation.md

Added optional fix for PCIe descriptor page size error.
Related discussion: #19481

* Update installation.md

Changed kernel driver version check from modinfo to /sys/module for correct post-reboot output
2026-02-05 06:38:34 -07:00
Josh Hawkins
21e4b36c7c Add languages (#21870)
* add persian, croatian, and slovak

* i18n

* fix formatting due to new version of ruff
2026-02-03 13:29:52 -06:00
Hosted Weblate
06141b900e Update translation files
Updated by "Squash Git commits" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/
Translation: Frigate NVR/common
2026-02-03 10:54:04 -07:00
Hosted Weblate
011e7a1ce6 Update translation files
Updated by "Squash Git commits" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/
Translation: Frigate NVR/common
2026-02-03 10:54:04 -07:00
Hosted Weblate
81d5e80dd2 Update translation files
Updated by "Squash Git commits" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/
Translation: Frigate NVR/common
2026-02-03 10:54:04 -07:00
Hosted Weblate
3b79b34c07 Update translation files
Updated by "Squash Git commits" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/
Translation: Frigate NVR/common
2026-02-03 10:54:04 -07:00
Hosted Weblate
7d8d2c5521 Translated using Weblate (French)
Currently translated at 100.0% (654 of 654 strings)

Co-authored-by: Apocoloquintose <bertrand.moreux@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/fr/
Translation: Frigate NVR/views-settings
2026-02-03 10:54:04 -07:00
Hosted Weblate
7fcce67c2a Translated using Weblate (Arabic)
Currently translated at 1.6% (2 of 122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Med Taha Ben Brahim <mohamedtaha.bb@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/ar/
Translation: Frigate NVR/views-classificationmodel
2026-02-03 10:54:04 -07:00
Hosted Weblate
87da12c453 Translated using Weblate (Korean)
Currently translated at 75.4% (40 of 53 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 임도균 <limkwon7@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ko/
Translation: Frigate NVR/views-facelibrary
2026-02-03 10:54:04 -07:00
Hosted Weblate
522630487b Translated using Weblate (Serbian)
Currently translated at 94.5% (52 of 55 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Serbian)

Currently translated at 7.6% (50 of 654 strings)

Translated using Weblate (Serbian)

Currently translated at 55.4% (51 of 92 strings)

Translated using Weblate (Serbian)

Currently translated at 44.0% (52 of 118 strings)

Translated using Weblate (Serbian)

Currently translated at 37.5% (51 of 136 strings)

Translated using Weblate (Serbian)

Currently translated at 18.6% (40 of 215 strings)

Translated using Weblate (Serbian)

Currently translated at 12.9% (65 of 501 strings)

Translated using Weblate (Serbian)

Currently translated at 36.0% (49 of 136 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (46 of 46 strings)

Translated using Weblate (Serbian)

Currently translated at 96.2% (51 of 53 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Serbian)

Currently translated at 42.6% (52 of 122 strings)

Translated using Weblate (Serbian)

Currently translated at 31.9% (39 of 122 strings)

Translated using Weblate (Serbian)

Currently translated at 16.2% (35 of 215 strings)

Translated using Weblate (Serbian)

Currently translated at 10.3% (52 of 501 strings)

Translated using Weblate (Serbian)

Currently translated at 71.6% (38 of 53 strings)

Translated using Weblate (Serbian)

Currently translated at 42.3% (39 of 92 strings)

Translated using Weblate (Serbian)

Currently translated at 27.9% (38 of 136 strings)

Translated using Weblate (Serbian)

Currently translated at 32.2% (38 of 118 strings)

Translated using Weblate (Serbian)

Currently translated at 84.7% (39 of 46 strings)

Translated using Weblate (Serbian)

Currently translated at 70.9% (39 of 55 strings)

Translated using Weblate (Serbian)

Currently translated at 90.6% (39 of 43 strings)

Translated using Weblate (Serbian)

Currently translated at 28.6% (39 of 136 strings)

Translated using Weblate (Serbian)

Currently translated at 79.5% (39 of 49 strings)

Translated using Weblate (Serbian)

Currently translated at 5.5% (36 of 654 strings)

Translated using Weblate (Serbian)

Currently translated at 14.8% (32 of 215 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Serbian)

Currently translated at 56.6% (30 of 53 strings)

Translated using Weblate (Serbian)

Currently translated at 72.0% (31 of 43 strings)

Translated using Weblate (Serbian)

Currently translated at 22.7% (31 of 136 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Serbian)

Currently translated at 67.3% (31 of 46 strings)

Translated using Weblate (Serbian)

Currently translated at 25.4% (31 of 122 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Serbian)

Currently translated at 56.3% (31 of 55 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Serbian)

Currently translated at 8.5% (43 of 501 strings)

Translated using Weblate (Serbian)

Currently translated at 63.2% (31 of 49 strings)

Translated using Weblate (Serbian)

Currently translated at 32.6% (30 of 92 strings)

Translated using Weblate (Serbian)

Currently translated at 26.2% (31 of 118 strings)

Translated using Weblate (Serbian)

Currently translated at 22.0% (30 of 136 strings)

Translated using Weblate (Serbian)

Currently translated at 4.5% (30 of 654 strings)

Co-authored-by: Aleksandar Jevremovic <aleksandar@jevremovic.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/sr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/sr/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-auth
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/components-player
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-configeditor
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-02-03 10:54:04 -07:00
Hosted Weblate
77eb5d6012 Translated using Weblate (Finnish)
Currently translated at 76.9% (10 of 13 strings)

Translated using Weblate (Finnish)

Currently translated at 32.8% (215 of 654 strings)

Translated using Weblate (Finnish)

Currently translated at 4.0% (5 of 122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Niko Järvinen <nbjarvinen@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/fi/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/fi/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/fi/
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-settings
2026-02-03 10:54:04 -07:00
Hosted Weblate
7339961636 Translated using Weblate (Swedish)
Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (216 of 216 strings)

Co-authored-by: Felix Boström <felix.bostrum@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-input/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/sv/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-input
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
2026-02-03 10:54:04 -07:00
Hosted Weblate
c34fd3b0d5 Translated using Weblate (French)
Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (French)

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (French)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (French)

Currently translated at 100.0% (654 of 654 strings)

Co-authored-by: Apocoloquintose <bertrand.moreux@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/fr/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-02-03 10:54:04 -07:00
Hosted Weblate
00c8c407c5 Translated using Weblate (Spanish)
Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (216 of 216 strings)

Co-authored-by: Daniel <danieldiazdelaiglesia@gmail.com>
Co-authored-by: Gerard Ricart Castells <gerard.ricart@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/es/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-configeditor
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-02-03 10:54:04 -07:00
Hosted Weblate
b17e5a3eff Translated using Weblate (Dutch)
Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (122 of 122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marijn <168113859+Marijn0@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/nl/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
2026-02-03 10:54:04 -07:00
Hosted Weblate
8b3c8a0ade Translated using Weblate (Indonesian)
Currently translated at 36.4% (27 of 74 strings)

Translated using Weblate (Indonesian)

Currently translated at 96.0% (24 of 25 strings)

Translated using Weblate (Indonesian)

Currently translated at 12.5% (27 of 216 strings)

Translated using Weblate (Indonesian)

Currently translated at 17.7% (89 of 501 strings)

Translated using Weblate (Indonesian)

Currently translated at 16.1% (22 of 136 strings)

Translated using Weblate (Indonesian)

Currently translated at 58.6% (27 of 46 strings)

Translated using Weblate (Indonesian)

Currently translated at 4.2% (28 of 654 strings)

Translated using Weblate (Indonesian)

Currently translated at 90.6% (39 of 43 strings)

Translated using Weblate (Indonesian)

Currently translated at 23.7% (29 of 122 strings)

Translated using Weblate (Indonesian)

Currently translated at 47.2% (26 of 55 strings)

Translated using Weblate (Indonesian)

Currently translated at 98.1% (52 of 53 strings)

Translated using Weblate (Indonesian)

Currently translated at 25.5% (25 of 98 strings)

Translated using Weblate (Indonesian)

Currently translated at 19.1% (26 of 136 strings)

Translated using Weblate (Indonesian)

Currently translated at 53.0% (26 of 49 strings)

Translated using Weblate (Indonesian)

Currently translated at 23.7% (28 of 118 strings)

Translated using Weblate (Indonesian)

Currently translated at 13.9% (17 of 122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Syarif Hidayat <syarifbl09@gmail.com>
Co-authored-by: ariska <ariska@databisnis.id>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/id/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/components-player
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-02-03 10:54:04 -07:00
Hosted Weblate
2fe5e06fed Translated using Weblate (Arabic)
Currently translated at 1.6% (2 of 122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Med Taha Ben Brahim <mohamedtaha.bb@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/ar/
Translation: Frigate NVR/views-classificationmodel
2026-02-03 10:54:04 -07:00
Hosted Weblate
e9db966097 Translated using Weblate (Italian)
Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (55 of 55 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (216 of 216 strings)

Co-authored-by: Filippo-riccardo Franzin (filippo franzin) <filric01@gmail.com>
Co-authored-by: Gringo <ita.translations@tiscali.it>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/it/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-02-03 10:54:04 -07:00
Hosted Weblate
4d63a74fd6 Translated using Weblate (Polish)
Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Polish)

Currently translated at 95.9% (94 of 98 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (215 of 215 strings)

Co-authored-by: Damian Radecki <damianradecki97@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J P <jpoloczek24@gmail.com>
Co-authored-by: Mateusz Kulis <kulis.matis@gmail.com>
Co-authored-by: przeniek <przeniek@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/pl/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-live
2026-02-03 10:54:04 -07:00
Hosted Weblate
b6e5894650 Translated using Weblate (Hungarian)
Currently translated at 71.3% (97 of 136 strings)

Translated using Weblate (Hungarian)

Currently translated at 25.4% (31 of 122 strings)

Translated using Weblate (Hungarian)

Currently translated at 93.9% (203 of 216 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Hungarian)

Currently translated at 62.3% (408 of 654 strings)

Translated using Weblate (Hungarian)

Currently translated at 96.2% (51 of 53 strings)

Translated using Weblate (Hungarian)

Currently translated at 83.7% (36 of 43 strings)

Translated using Weblate (Hungarian)

Currently translated at 20.4% (25 of 122 strings)

Translated using Weblate (Hungarian)

Currently translated at 88.9% (121 of 136 strings)

Translated using Weblate (Hungarian)

Currently translated at 85.4% (428 of 501 strings)

Translated using Weblate (Hungarian)

Currently translated at 93.9% (202 of 215 strings)

Translated using Weblate (Hungarian)

Currently translated at 68.3% (93 of 136 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Peter Bartfai <pbartfai@stardust.hu>
Co-authored-by: Zsolt Fojtyik <zsozso830316@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/hu/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-02-03 10:54:04 -07:00
Hosted Weblate
62b880a4b2 Translated using Weblate (Croatian)
Currently translated at 100.0% (46 of 46 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (654 of 654 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: stipe-jurkovic <sjurko00@fesb.hr>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/hr/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-player
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-02-03 10:54:04 -07:00
Hosted Weblate
5984346623 Added translation using Weblate (Icelandic)
Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sindris <sindri.sse@gmail.com>
2026-02-03 10:54:04 -07:00
Hosted Weblate
a394d37bfe Translated using Weblate (Portuguese)
Currently translated at 90.5% (48 of 53 strings)

Translated using Weblate (Portuguese)

Currently translated at 76.9% (10 of 13 strings)

Translated using Weblate (Portuguese)

Currently translated at 28.6% (35 of 122 strings)

Co-authored-by: Carlos Santos <c.santos00@hotmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/pt/
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
2026-02-03 10:54:04 -07:00
Hosted Weblate
a407f08db6 Translated using Weblate (Czech)
Currently translated at 65.4% (428 of 654 strings)

Translated using Weblate (Czech)

Currently translated at 45.9% (56 of 122 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Czech)

Currently translated at 91.9% (125 of 136 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (55 of 55 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Czech)

Currently translated at 86.7% (118 of 136 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Czech)

Currently translated at 93.4% (86 of 92 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Czech)

Currently translated at 96.3% (53 of 55 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Czech)

Currently translated at 64.9% (425 of 654 strings)

Translated using Weblate (Czech)

Currently translated at 74.2% (101 of 136 strings)

Translated using Weblate (Czech)

Currently translated at 32.7% (40 of 122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Martin Brož <code@martin-broz.cz>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/cs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/cs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/cs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/cs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/cs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/cs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/cs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/cs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/cs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/cs/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-02-03 10:54:04 -07:00
Hosted Weblate
2cd14341e0 Translated using Weblate (Catalan)
Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (216 of 216 strings)

Co-authored-by: Eduardo Pastor Fernández <123eduardoneko123@gmail.com>
Co-authored-by: Gerard Ricart Castells <gerard.ricart@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ca/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
2026-02-03 10:54:04 -07:00
Hosted Weblate
9285c5e10a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (216 of 216 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/uk/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
2026-02-03 10:54:04 -07:00
Hosted Weblate
74ac45d0af Translated using Weblate (Romanian)
Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (216 of 216 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: lukasig <lukasig@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ro/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-configeditor
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
2026-02-03 10:54:04 -07:00
Hosted Weblate
2f06cfe50c Translated using Weblate (Russian)
Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (98 of 98 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: internetson <sockmancore@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ru/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/ru/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/ru/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/ru/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/ru/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-system
2026-02-03 10:54:04 -07:00
Hosted Weblate
9413f1c46d Translated using Weblate (Estonian)
Currently translated at 80.4% (74 of 92 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (216 of 216 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/et/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-live
2026-02-03 10:54:04 -07:00
Hosted Weblate
fee3050886 Translated using Weblate (Greek)
Currently translated at 6.5% (8 of 122 strings)

Translated using Weblate (Greek)

Currently translated at 53.2% (115 of 216 strings)

Translated using Weblate (Greek)

Currently translated at 48.8% (21 of 43 strings)

Co-authored-by: Apostolos Tsaganos <apostolos94@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/el/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/el/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/el/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
2026-02-03 10:54:04 -07:00
Hosted Weblate
9d572ba8cb Translated using Weblate (Danish)
Currently translated at 72.0% (18 of 25 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Danish)

Currently translated at 18.3% (9 of 49 strings)

Translated using Weblate (Danish)

Currently translated at 92.3% (12 of 13 strings)

Translated using Weblate (Danish)

Currently translated at 39.5% (17 of 43 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (46 of 46 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (118 of 118 strings)

Translated using Weblate (Danish)

Currently translated at 39.1% (196 of 501 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Danish)

Currently translated at 18.8% (10 of 53 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Danish)

Currently translated at 37.2% (16 of 43 strings)

Translated using Weblate (Danish)

Currently translated at 59.8% (73 of 122 strings)

Co-authored-by: Bjorn Jorgensen <github@bjornz.dk>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-icons/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/da/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/da/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-auth
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-icons
Translation: Frigate NVR/components-player
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-configeditor
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-search
2026-02-03 10:54:04 -07:00
Hosted Weblate
9267d006ce Translated using Weblate (German)
Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (German)

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (German)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (German)

Currently translated at 99.8% (653 of 654 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sebastian Sie <sebastian.neuplanitz@googlemail.com>
Co-authored-by: jmtatsch <julian@tatsch.it>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/de/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
2026-02-03 10:54:04 -07:00
Hosted Weblate
e6e2b74034 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 83.8% (114 of 136 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.1% (54 of 55 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.6% (42 of 43 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 47.5% (58 of 122 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 69.1% (452 of 654 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.5% (200 of 216 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.3% (53 of 55 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 68.6% (449 of 654 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 78.6% (107 of 136 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 43.4% (53 of 122 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 77.9% (106 of 136 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.7% (89 of 92 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 40.9% (50 of 122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rhuan Barreto <rhuan@barreto.work>
Co-authored-by: cvroque <carlos.vroque@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/pt_BR/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
2026-02-03 10:54:04 -07:00
Hosted Weblate
0a307edce9 Translated using Weblate (Lithuanian)
Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Lithuanian)

Currently translated at 84.5% (553 of 654 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (501 of 501 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: MaBeniu <runnerm@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/lt/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-02-03 10:54:04 -07:00
Hosted Weblate
59a959430d Translated using Weblate (Turkish)
Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (216 of 216 strings)

Co-authored-by: Emircanos <emircan368@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: pcislocked <git@pcislocked.net>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/tr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/tr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/tr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/tr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/tr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/tr/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-02-03 10:54:04 -07:00
Nicolas Mowen
2d83992284 Miscellaneous fixes (0.17 beta) (#21867)
* Adjust title prompt to have less rigidity

* Improve motion boxes handling for features that don't require motion

* Improve handling of classes starting with digits

* Improve vehicle nuance

* tweak lpr docs

* Improve grammar

* Don't allow # in face name

* add password requirements to new user dialog

* change password requirements

* Clenaup

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2026-02-03 08:31:00 -06:00
Blake Blackshear
e4fe021279 Merge remote-tracking branch 'origin/master' into dev 2026-01-31 18:42:40 +00:00
Josh Hawkins
b4520d9e2f Miscellaneous fixes (0.17 beta) (#21826)
* revert other changes

* fix verified icon overlay in debug view list

* Add classification object debug logs

* Formatting

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2026-01-29 12:42:06 -07:00
Josh Hawkins
3b6814fbc9 Revert "Miscellaneous fixes (0.17 beta) (#21764)" (#21825)
This reverts commit 50ac5a1483.
2026-01-29 11:30:21 -07:00
Marijn0
338d85a9a2 Fix i18n keys (#21814) 2026-01-28 13:55:38 -07:00
Blake Blackshear
4131252a3b Port go2rtc check (#21808)
* version update

* Restrict go2rtc exec sources by default (#21543)

* Restrict go2rtc exec sources by default

* add docs

* check for addon value too

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2026-01-28 06:56:15 -06:00
Josh Hawkins
50ac5a1483 Miscellaneous fixes (0.17 beta) (#21764)
* Add 640x640 Intel NPU stats

* use css instead of js for reviewed button hover state in filmstrip

* update copilot instructions to copy HA's format

* Set json schema for genai

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2026-01-25 18:59:25 -07:00
Josh Hawkins
a75f6945ae Miscellaneous fixes (0.17 beta) (#21737)
* use default stable api version for gemini genai client

* update gemini docs

* remove outdated genai.md and update correct file

* Classification fixes

* Mutate when a date is selected and marked as reviewed

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2026-01-21 17:46:24 -07:00
dependabot[bot]
90b14f1a32 Bump lodash from 4.17.21 to 4.17.23 in /web (#21749)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-21 17:46:06 -07:00
Josh Hawkins
d633c7d966 Miscellaneous fixes (0.17 beta) (#21699)
* tracking details tweaks

- fix 4:3 layout
- get and use aspect of record stream if different from detect stream

* aspect ratio docs tip

* spacing

* fix

* i18n fix

* additional logs on ffmpeg exit

* improve no camera view

instead of showing an "add camera" message, show a specific message for empty camera groups when frigate already has cameras added

* add note about separate onvif accounts in some camera firmware

* clarify review summary report docs

* review settings tweaks

- remove horizontal divider
- update description language for switches
- keep save button disabled until review classification settings change

* use correct Toaster component from shadcn

* clarify support for intel b-series (battlemage) gpus

* add clarifying comment to dummy camera docs
2026-01-20 08:17:58 -07:00
Josh Hawkins
0a8f499640 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
2026-01-18 06:36:27 -07:00
Kirill Kulakov
cfeb86646f fix(recording): handle unexpected filenames in cache maintainer to prevent crash (#21676)
* fix(recording): handle unexpected filenames in cache maintainer to prevent crash

* test(recording): add test for maintainer cache file parsing

* Prevent log spam from unexpected cache files

Addresses PR review feedback: Add deduplication to prevent warning
messages from being logged repeatedly for the same unexpected file
in the cache directory. Each unexpected filename is only logged once
per RecordingMaintainer instance lifecycle.

Also adds test to verify warning is only emitted once per filename.

* Fix code formatting for test_maintainer.py

* fixes + ruff
2026-01-16 19:23:23 -07:00
Nicolas Mowen
bf099c3edd Miscellaneous fixes (0.17 beta) (#21655)
* Fix jetson stats reading

* Return result

* Avoid unknown class for cover image

* fix double encoding of passwords in camera wizard

* formatting

* empty homekit config fixes

* add locks to jina v1 embeddings

protect tokenizer and feature extractor in jina_v1_embedding with per-instance thread lock to avoid the "Already borrowed" RuntimeError during concurrent tokenization

* Capitalize correctly

* replace deprecated google-generativeai with google-genai

update gemini genai provider with new calls from SDK
provider_options specifies any http options
suppress unneeded info logging

* fix attribute area on detail stream hover

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2026-01-15 07:08:49 -07:00
dependabot[bot]
2e1706baa0 Bump @remix-run/router and react-router-dom in /web (#21580)
Bumps [@remix-run/router](https://github.com/remix-run/react-router/tree/HEAD/packages/router) to 1.23.2 and updates ancestor dependency [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom). These dependencies need to be updated together.


Updates `@remix-run/router` from 1.19.0 to 1.23.2
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/@remix-run/router@1.23.2/packages/router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@remix-run/router@1.23.2/packages/router)

Updates `react-router-dom` from 6.26.0 to 6.30.3
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.30.3/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: "@remix-run/router"
  dependency-version: 1.23.2
  dependency-type: indirect
- dependency-name: react-router-dom
  dependency-version: 6.30.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-14 07:26:56 -06:00
dependabot[bot]
43c8f68e44 Bump qs from 6.14.0 to 6.14.1 in /docs (#21504)
Bumps [qs](https://github.com/ljharb/qs) from 6.14.0 to 6.14.1.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.0...v6.14.1)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-14 07:26:36 -06:00
dependabot[bot]
90d857ad6d Bump vite from 6.2.0 to 6.4.1 in /web (#20593)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.0 to 6.4.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-14 07:26:04 -06:00
dependabot[bot]
c222aa0e65 Bump form-data from 4.0.0 to 4.0.4 in /web (#19242)
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.0 to 4.0.4.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.0...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-14 07:12:48 -06:00
Hosted Weblate
cd37af4365 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (136 of 136 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: OverTheHillsAndFarAway <prosjektx@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/nb_NO/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
5e57dbe070 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (118 of 118 strings)

Co-authored-by: GuoQing Liu <842607283@qq.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/zh_Hans/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
bd568ab3b1 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 11.9% (78 of 654 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (55 of 55 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 97.6% (42 of 43 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 99.2% (135 of 136 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (131 of 131 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J P <jpoloczek24@gmail.com>
Co-authored-by: windasd <me@windasd.tw>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/zh_Hant/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
7d02220ec5 Translated using Weblate (Persian)
Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (6 of 6 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (46 of 46 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (92 of 92 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (55 of 55 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (118 of 118 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (55 of 55 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (41 of 41 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Persian)

Currently translated at 99.6% (652 of 654 strings)

Translated using Weblate (Persian)

Currently translated at 98.9% (91 of 92 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Persian)

Currently translated at 84.6% (11 of 13 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (118 of 118 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Persian)

Currently translated at 95.6% (44 of 46 strings)

Translated using Weblate (Persian)

Currently translated at 66.6% (4 of 6 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Persian)

Currently translated at 92.0% (23 of 25 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Persian)

Currently translated at 97.9% (48 of 49 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: حمید ملک محمدی <hmmftg@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-icons/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-recording/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/fa/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-auth
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/components-icons
Translation: Frigate NVR/components-player
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-configeditor
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-recording
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
da75481443 Translated using Weblate (Swedish)
Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (136 of 136 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/sv/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
85bc988c76 Translated using Weblate (French)
Currently translated at 97.7% (133 of 136 strings)

Translated using Weblate (French)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (French)

Currently translated at 100.0% (43 of 43 strings)

Co-authored-by: Apocoloquintose <bertrand.moreux@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/fr/
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
53a592322a Translated using Weblate (Spanish)
Currently translated at 98.5% (134 of 136 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (118 of 118 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (55 of 55 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (92 of 92 strings)

Co-authored-by: Ancor Trujillo <nightsearch@hotmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: José María Díaz <jdiaz.bb@gmail.com>
Co-authored-by: klakiti <alberticobrito@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/es/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
0a24e3ce67 Translated using Weblate (Dutch)
Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (131 of 131 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marijn <168113859+Marijn0@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/nl/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
d1a184d4ac Translated using Weblate (Arabic)
Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Arabic)

Currently translated at 0.8% (1 of 122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Med Taha Ben Brahim <mohamedtaha.bb@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/ar/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ar/
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-facelibrary
2026-01-13 13:02:28 -07:00
Hosted Weblate
311549536c Translated using Weblate (Italian)
Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (43 of 43 strings)

Co-authored-by: Gringo <ita.translations@tiscali.it>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nton <arlatalpa@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/it/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
ace11730bd Translated using Weblate (Polish)
Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (55 of 55 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Polish)

Currently translated at 95.1% (39 of 41 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Polish)

Currently translated at 69.6% (85 of 122 strings)

Translated using Weblate (Polish)

Currently translated at 84.4% (114 of 135 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (53 of 53 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J P <jpoloczek24@gmail.com>
Co-authored-by: Janusz Poloczek <jpoloczek@trafficaisolutions.com>
Co-authored-by: przeniek <przeniek@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/pl/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
2b345bd3f7 Translated using Weblate (Hebrew)
Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (136 of 136 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ronen Atsil <atsil55@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/he/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/he/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/he/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/he/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
e2353e55f3 Translated using Weblate (Hungarian)
Currently translated at 94.3% (50 of 53 strings)

Translated using Weblate (Hungarian)

Currently translated at 62.0% (406 of 654 strings)

Translated using Weblate (Hungarian)

Currently translated at 63.4% (26 of 41 strings)

Translated using Weblate (Hungarian)

Currently translated at 97.9% (48 of 49 strings)

Translated using Weblate (Hungarian)

Currently translated at 19.6% (24 of 122 strings)

Translated using Weblate (Hungarian)

Currently translated at 19.6% (24 of 122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: ZsoltiHUB <zsoltizsolti043@gmail.com>
Co-authored-by: ugfus1630 <katona.ta@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/hu/
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
2026-01-13 13:02:28 -07:00
Hosted Weblate
cd3d0883bc Translated using Weblate (Croatian)
Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (46 of 46 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (55 of 55 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (118 of 118 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Croatian)

Currently translated at 46.7% (43 of 92 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (118 of 118 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Croatian)

Currently translated at 10.9% (55 of 501 strings)

Translated using Weblate (Croatian)

Currently translated at 27.8% (34 of 122 strings)

Translated using Weblate (Croatian)

Currently translated at 15.8% (34 of 215 strings)

Translated using Weblate (Croatian)

Currently translated at 24.5% (29 of 118 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Croatian)

Currently translated at 67.4% (29 of 43 strings)

Translated using Weblate (Croatian)

Currently translated at 39.1% (29 of 74 strings)

Translated using Weblate (Croatian)

Currently translated at 58.4% (31 of 53 strings)

Translated using Weblate (Croatian)

Currently translated at 22.7% (31 of 136 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Croatian)

Currently translated at 63.0% (29 of 46 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Croatian)

Currently translated at 31.5% (29 of 92 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Croatian)

Currently translated at 59.1% (29 of 49 strings)

Translated using Weblate (Croatian)

Currently translated at 7.9% (40 of 501 strings)

Translated using Weblate (Croatian)

Currently translated at 52.7% (29 of 55 strings)

Translated using Weblate (Croatian)

Currently translated at 5.0% (33 of 654 strings)

Translated using Weblate (Croatian)

Currently translated at 26.4% (36 of 136 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: stipe-jurkovic <sjurko00@fesb.hr>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/hr/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-auth
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/components-player
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-configeditor
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
820fc6e9b5 Translated using Weblate (Portuguese)
Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Portuguese)

Currently translated at 27.8% (34 of 122 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Portuguese)

Currently translated at 4.9% (6 of 122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nuno Ponte <nuno.ponte@gmail.com>
Co-authored-by: fabiovalverde <fabio@rvalverde.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/pt/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-auth
Translation: Frigate NVR/views-classificationmodel
2026-01-13 13:02:28 -07:00
Hosted Weblate
a326ecdc9f Translated using Weblate (Catalan)
Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (136 of 136 strings)

Co-authored-by: Eduardo Pastor Fernández <123eduardoneko123@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: anton garcias <isaga.percompartir@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/ca/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
91f9a01df5 Translated using Weblate (Japanese)
Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (118 of 118 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: alpha <etc@alpha-line.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/ja/
Translation: Frigate NVR/audio
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
2026-01-13 13:02:28 -07:00
Hosted Weblate
6572fa8a48 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (136 of 136 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/uk/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
067de06176 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Bulgarian)

Currently translated at 23.2% (10 of 43 strings)

Translated using Weblate (Bulgarian)

Currently translated at 8.1% (4 of 49 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Bulgarian)

Currently translated at 53.2% (267 of 501 strings)

Translated using Weblate (Bulgarian)

Currently translated at 1.6% (2 of 122 strings)

Translated using Weblate (Bulgarian)

Currently translated at 45.6% (21 of 46 strings)

Translated using Weblate (Bulgarian)

Currently translated at 2.9% (4 of 136 strings)

Translated using Weblate (Bulgarian)

Currently translated at 10.9% (6 of 55 strings)

Translated using Weblate (Bulgarian)

Currently translated at 11.3% (6 of 53 strings)

Translated using Weblate (Bulgarian)

Currently translated at 2.9% (4 of 136 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (13 of 13 strings)

Co-authored-by: Borislav <sartheris@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jan Ivanov (Telemaniaka) <telemaniaka@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/bg/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/bg/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/bg/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/bg/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/bg/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/bg/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/bg/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/bg/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/bg/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/bg/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/bg/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/bg/
Translation: Frigate NVR/audio
Translation: Frigate NVR/components-auth
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-configeditor
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
849677c758 Translated using Weblate (Romanian)
Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (136 of 136 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: lukasig <lukasig@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/ro/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
1c7f68bf44 Translated using Weblate (Estonian)
Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Estonian)

Currently translated at 67.3% (62 of 92 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Estonian)

Currently translated at 22.9% (115 of 501 strings)

Translated using Weblate (Estonian)

Currently translated at 26.6% (36 of 135 strings)

Translated using Weblate (Estonian)

Currently translated at 14.7% (18 of 122 strings)

Translated using Weblate (Estonian)

Currently translated at 28.3% (15 of 53 strings)

Translated using Weblate (Estonian)

Currently translated at 26.5% (13 of 49 strings)

Translated using Weblate (Estonian)

Currently translated at 5.3% (7 of 131 strings)

Translated using Weblate (Estonian)

Currently translated at 40.0% (10 of 25 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/et/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-player
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
68fee3ed7b Translated using Weblate (Greek)
Currently translated at 2.4% (3 of 122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Thanasis <than2031995@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/el/
Translation: Frigate NVR/views-classificationmodel
2026-01-13 13:02:28 -07:00
Hosted Weblate
c13a47f30b Translated using Weblate (German)
Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (German)

Currently translated at 98.5% (134 of 136 strings)

Translated using Weblate (German)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (German)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (German)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (German)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (German)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (German)

Currently translated at 100.0% (122 of 122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sebastian Sie <sebastian.neuplanitz@googlemail.com>
Co-authored-by: jmtatsch <julian@tatsch.it>
Co-authored-by: zobe123 <manuel.zobl@gmx.at>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/de/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
8d26d2dca2 Translated using Weblate (Portuguese (Brazil))
Currently translated at 66.2% (433 of 654 strings)

Co-authored-by: Cleiton PEres <cleiton@consultecti.com.br>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/pt_BR/
Translation: Frigate NVR/views-settings
2026-01-13 13:02:28 -07:00
Hosted Weblate
4188eedf3d Translated using Weblate (Thai)
Currently translated at 23.5% (32 of 136 strings)

Translated using Weblate (Thai)

Currently translated at 2.4% (3 of 122 strings)

Translated using Weblate (Thai)

Currently translated at 45.2% (24 of 53 strings)

Translated using Weblate (Thai)

Currently translated at 60.8% (45 of 74 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kongesque <Kongesque@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/th/
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
8a52d83065 Translated using Weblate (Lithuanian)
Currently translated at 92.3% (121 of 131 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (46 of 46 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (55 of 55 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Lithuanian)

Currently translated at 68.9% (451 of 654 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Lithuanian)

Currently translated at 90.4% (123 of 136 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Lithuanian)

Currently translated at 95.3% (204 of 214 strings)

Translated using Weblate (Lithuanian)

Currently translated at 56.5% (69 of 122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: MaBeniu <runnerm@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/lt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/lt/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Hosted Weblate
b2d1fdf7eb Translated using Weblate (Turkish)
Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (136 of 136 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (136 of 136 strings)

Co-authored-by: Emircanos <emircan368@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/tr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/tr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/tr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/tr/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-system
2026-01-13 13:02:28 -07:00
Nicolas Mowen
2c34e1ec10 Miscellaneous fixes (0.17 beta) (#21607)
* Strip model name before training

* Handle options file for go2rtc option

* Make reviewed optional and add null to API call

* Send reviewed for dashboard

* Allow setting context size for openai compatible endpoints

* push empty go2rtc config to avoid homekit error in log

* Add option to set runtime options for LLM providers

* Docs

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2026-01-12 20:36:38 -07:00
GuoQing Liu
91cc6747b6 i18n miscellaneous fixes (#21614)
* fix: fix face library unknown label i18n wrong

* fix: fix review genai threat level i18n

* fix: fix preview unknown label i18n

* fix: fix AM/PM i18n display issue
2026-01-12 09:15:27 -06:00
Blake Blackshear
7b5a1b7284 ensure cloudflare pages are indexed by google (#21606)
* ensure cloudflare pages are indexed by google

* avoid indexing dev-docs as well
2026-01-11 08:48:03 -07:00
Dermot Duffy
7e5d98dbab fix: Correctly apply API filter for "reviewed" (#21600) 2026-01-11 06:42:33 -07:00
Josh Hawkins
d952a97bda reduce gif size for docs assets changes (#21594) 2026-01-10 12:59:15 -07:00
Blake Blackshear
ea39bb3565 update copyright (#21485) 2026-01-01 09:55:46 -06:00
552 changed files with 21557 additions and 2745 deletions

View File

@@ -1,2 +1,385 @@
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.
# GitHub Copilot Instructions for Frigate NVR
This document provides coding guidelines and best practices for contributing to Frigate NVR, a complete and local NVR designed for Home Assistant with AI object detection.
## Project Overview
Frigate NVR is a realtime object detection system for IP cameras that uses:
- **Backend**: Python 3.13+ with FastAPI, OpenCV, TensorFlow/ONNX
- **Frontend**: React with TypeScript, Vite, TailwindCSS
- **Architecture**: Multiprocessing design with ZMQ and MQTT communication
- **Focus**: Minimal resource usage with maximum performance
## Code Review Guidelines
When reviewing code, do NOT comment on:
- Missing imports - Static analysis tooling catches these
- Code formatting - Ruff (Python) and Prettier (TypeScript/React) handle formatting
- Minor style inconsistencies already enforced by linters
## Python Backend Standards
### Python Requirements
- **Compatibility**: Python 3.13+
- **Language Features**: Use modern Python features:
- Pattern matching
- Type hints (comprehensive typing preferred)
- f-strings (preferred over `%` or `.format()`)
- Dataclasses
- Async/await patterns
### Code Quality Standards
- **Formatting**: Ruff (configured in `pyproject.toml`)
- **Linting**: Ruff with rules defined in project config
- **Type Checking**: Use type hints consistently
- **Testing**: unittest framework - use `python3 -u -m unittest` to run tests
- **Language**: American English for all code, comments, and documentation
### Logging Standards
- **Logger Pattern**: Use module-level logger
```python
import logging
logger = logging.getLogger(__name__)
```
- **Format Guidelines**:
- No periods at end of log messages
- No sensitive data (keys, tokens, passwords)
- Use lazy logging: `logger.debug("Message with %s", variable)`
- **Log Levels**:
- `debug`: Development and troubleshooting information
- `info`: Important runtime events (startup, shutdown, state changes)
- `warning`: Recoverable issues that should be addressed
- `error`: Errors that affect functionality but don't crash the app
- `exception`: Use in except blocks to include traceback
### Error Handling
- **Exception Types**: Choose most specific exception available
- **Try/Catch Best Practices**:
- Only wrap code that can throw exceptions
- Keep try blocks minimal - process data after the try/except
- Avoid bare exceptions except in background tasks
Bad pattern:
```python
try:
data = await device.get_data() # Can throw
# ❌ Don't process data inside try block
processed = data.get("value", 0) * 100
result = processed
except DeviceError:
logger.error("Failed to get data")
```
Good pattern:
```python
try:
data = await device.get_data() # Can throw
except DeviceError:
logger.error("Failed to get data")
return
# ✅ Process data outside try block
processed = data.get("value", 0) * 100
result = processed
```
### Async Programming
- **External I/O**: All external I/O operations must be async
- **Best Practices**:
- Avoid sleeping in loops - use `asyncio.sleep()` not `time.sleep()`
- Avoid awaiting in loops - use `asyncio.gather()` instead
- No blocking calls in async functions
- Use `asyncio.create_task()` for background operations
- **Thread Safety**: Use proper synchronization for shared state
### Documentation Standards
- **Module Docstrings**: Concise descriptions at top of files
```python
"""Utilities for motion detection and analysis."""
```
- **Function Docstrings**: Required for public functions and methods
```python
async def process_frame(frame: ndarray, config: Config) -> Detection:
"""Process a video frame for object detection.
Args:
frame: The video frame as numpy array
config: Detection configuration
Returns:
Detection results with bounding boxes
"""
```
- **Comment Style**:
- Explain the "why" not just the "what"
- Keep lines under 88 characters when possible
- Use clear, descriptive comments
### File Organization
- **API Endpoints**: `frigate/api/` - FastAPI route handlers
- **Configuration**: `frigate/config/` - Configuration parsing and validation
- **Detectors**: `frigate/detectors/` - Object detection backends
- **Events**: `frigate/events/` - Event management and storage
- **Utilities**: `frigate/util/` - Shared utility functions
## Frontend (React/TypeScript) Standards
### Internationalization (i18n)
- **CRITICAL**: Never write user-facing strings directly in components
- **Always use react-i18next**: Import and use the `t()` function
```tsx
import { useTranslation } from "react-i18next";
function MyComponent() {
const { t } = useTranslation(["views/live"]);
return <div>{t("camera_not_found")}</div>;
}
```
- **Translation Files**: Add English strings to the appropriate json files in `web/public/locales/en`
- **Namespaces**: Organize translations by feature/view (e.g., `views/live`, `common`, `views/system`)
### Code Quality
- **Linting**: ESLint (see `web/.eslintrc.cjs`)
- **Formatting**: Prettier with Tailwind CSS plugin
- **Type Safety**: TypeScript strict mode enabled
- **Testing**: Vitest for unit tests
### Component Patterns
- **UI Components**: Use Radix UI primitives (in `web/src/components/ui/`)
- **Styling**: TailwindCSS with `cn()` utility for class merging
- **State Management**: React hooks (useState, useEffect, useCallback, useMemo)
- **Data Fetching**: Custom hooks with proper loading and error states
### ESLint Rules
Key rules enforced:
- `react-hooks/rules-of-hooks`: error
- `react-hooks/exhaustive-deps`: error
- `no-console`: error (use proper logging or remove)
- `@typescript-eslint/no-explicit-any`: warn (always use proper types instead of `any`)
- Unused variables must be prefixed with `_`
- Comma dangles required for multiline objects/arrays
### File Organization
- **Pages**: `web/src/pages/` - Route components
- **Views**: `web/src/views/` - Complex view components
- **Components**: `web/src/components/` - Reusable components
- **Hooks**: `web/src/hooks/` - Custom React hooks
- **API**: `web/src/api/` - API client functions
- **Types**: `web/src/types/` - TypeScript type definitions
## Testing Requirements
### Backend Testing
- **Framework**: Python unittest
- **Run Command**: `python3 -u -m unittest`
- **Location**: `frigate/test/`
- **Coverage**: Aim for comprehensive test coverage of core functionality
- **Pattern**: Use `TestCase` classes with descriptive test method names
```python
class TestMotionDetection(unittest.TestCase):
def test_detects_motion_above_threshold(self):
# Test implementation
```
### Test Best Practices
- Always have a way to test your work and confirm your changes
- Write tests for bug fixes to prevent regressions
- Test edge cases and error conditions
- Mock external dependencies (cameras, APIs, hardware)
- Use fixtures for test data
## Development Commands
### Python Backend
```bash
# Run all tests
python3 -u -m unittest
# Run specific test file
python3 -u -m unittest frigate.test.test_ffmpeg_presets
# Check formatting (Ruff)
ruff format --check frigate/
# Apply formatting
ruff format frigate/
# Run linter
ruff check frigate/
```
### Frontend (from web/ directory)
```bash
# Start dev server (AI agents should never run this directly unless asked)
npm run dev
# Build for production
npm run build
# Run linter
npm run lint
# Fix linting issues
npm run lint:fix
# Format code
npm run prettier:write
```
### Docker Development
AI agents should never run these commands directly unless instructed.
```bash
# Build local image
make local
# Build debug image
make debug
```
## Common Patterns
### API Endpoint Pattern
```python
from fastapi import APIRouter, Request
from frigate.api.defs.tags import Tags
router = APIRouter(tags=[Tags.Events])
@router.get("/events")
async def get_events(request: Request, limit: int = 100):
"""Retrieve events from the database."""
# Implementation
```
### Configuration Access
```python
# Access Frigate configuration
config: FrigateConfig = request.app.frigate_config
camera_config = config.cameras["front_door"]
```
### Database Queries
```python
from frigate.models import Event
# Use Peewee ORM for database access
events = (
Event.select()
.where(Event.camera == camera_name)
.order_by(Event.start_time.desc())
.limit(limit)
)
```
## Common Anti-Patterns to Avoid
### ❌ Avoid These
```python
# Blocking operations in async functions
data = requests.get(url) # ❌ Use async HTTP client
time.sleep(5) # ❌ Use asyncio.sleep()
# Hardcoded strings in React components
<div>Camera not found</div> # ❌ Use t("camera_not_found")
# Missing error handling
data = await api.get_data() # ❌ No exception handling
# Bare exceptions in regular code
try:
value = await sensor.read()
except Exception: # ❌ Too broad
logger.error("Failed")
```
### ✅ Use These Instead
```python
# Async operations
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
data = await response.json()
await asyncio.sleep(5) # ✅ Non-blocking
# Translatable strings in React
const { t } = useTranslation();
<div>{t("camera_not_found")}</div> # ✅ Translatable
# Proper error handling
try:
data = await api.get_data()
except ApiException as err:
logger.error("API error: %s", err)
raise
# Specific exceptions
try:
value = await sensor.read()
except SensorException as err: # ✅ Specific
logger.exception("Failed to read sensor")
```
## Project-Specific Conventions
### Configuration Files
- Main config: `config/config.yml`
### Directory Structure
- Backend code: `frigate/`
- Frontend code: `web/`
- Docker files: `docker/`
- Documentation: `docs/`
- Database migrations: `migrations/`
### Code Style Conformance
Always conform new and refactored code to the existing coding style in the project:
- Follow established patterns in similar files
- Match indentation and formatting of surrounding code
- Use consistent naming conventions (snake_case for Python, camelCase for TypeScript)
- Maintain the same level of verbosity in comments and docstrings
## Additional Resources
- Documentation: https://docs.frigate.video
- Main Repository: https://github.com/blakeblackshear/frigate
- Home Assistant Integration: https://github.com/blakeblackshear/frigate-hass-integration

View File

@@ -1,7 +1,7 @@
default_target: local
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
VERSION = 0.17.0
VERSION = 0.18.0
IMAGE_REPO ?= ghcr.io/blakeblackshear/frigate
GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
BOARDS= #Initialized empty
@@ -49,7 +49,8 @@ push: push-boards
--push
run: local
docker run --rm --publish=5000:5000 --volume=${PWD}/config:/config frigate:latest
docker run --rm --publish=5000:5000 --publish=8971:8971 \
--volume=${PWD}/config:/config frigate:latest
run_tests: local
docker run --rm --workdir=/opt/frigate --entrypoint= frigate:latest \

View File

@@ -2,15 +2,19 @@
# Update package list and install dependencies
sudo apt-get update
sudo apt-get install -y build-essential cmake git wget
sudo apt-get install -y build-essential cmake git wget linux-headers-$(uname -r)
hailo_version="4.21.0"
arch=$(uname -m)
if [[ $arch == "x86_64" ]]; then
sudo apt install -y linux-headers-$(uname -r);
else
sudo apt install -y linux-modules-extra-$(uname -r);
if [[ $arch == "aarch64" ]]; then
source /etc/os-release
os_codename=$VERSION_CODENAME
echo "Detected OS codename: $os_codename"
fi
if [ "$os_codename" = "trixie" ]; then
sudo apt install -y dkms
fi
# Clone the HailoRT driver repository
@@ -47,3 +51,4 @@ sudo udevadm control --reload-rules && sudo udevadm trigger
echo "HailoRT driver installation complete."
echo "reboot your system to load the firmware!"
echo "Driver version: $(modinfo -F version hailo_pci)"

View File

@@ -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.10/go2rtc_linux_${TARGETARCH}" go2rtc
ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.13/go2rtc_linux_${TARGETARCH}" go2rtc
FROM wget AS tempio
ARG TARGETARCH

View File

@@ -47,7 +47,7 @@ onnxruntime == 1.22.*
# Embeddings
transformers == 4.45.*
# Generative AI
google-generativeai == 0.8.*
google-genai == 1.58.*
ollama == 0.6.*
openai == 1.65.*
# push notifications

View File

@@ -10,7 +10,8 @@ echo "[INFO] Starting certsync..."
lefile="/etc/letsencrypt/live/frigate/fullchain.pem"
tls_enabled=`python3 /usr/local/nginx/get_listen_settings.py | jq -r .tls.enabled`
tls_enabled=`python3 /usr/local/nginx/get_nginx_settings.py | jq -r .tls.enabled`
listen_external_port=`python3 /usr/local/nginx/get_nginx_settings.py | jq -r .listen.external_port`
while true
do
@@ -34,7 +35,7 @@ do
;;
esac
liveprint=`echo | openssl s_client -showcerts -connect 127.0.0.1:8971 2>&1 | openssl x509 -fingerprint 2>&1 | grep -i fingerprint || echo 'failed'`
liveprint=`echo | openssl s_client -showcerts -connect 127.0.0.1:$listen_external_port 2>&1 | openssl x509 -fingerprint 2>&1 | grep -i fingerprint || echo 'failed'`
case "$liveprint" in
*Fingerprint*)
@@ -55,4 +56,4 @@ do
done
exit 0
exit 0

View File

@@ -54,8 +54,8 @@ function setup_homekit_config() {
local config_path="$1"
if [[ ! -f "${config_path}" ]]; then
echo "[INFO] Creating empty HomeKit config file..."
echo 'homekit: {}' > "${config_path}"
echo "[INFO] Creating empty config file for HomeKit..."
echo '{}' > "${config_path}"
fi
# Convert YAML to JSON for jq processing
@@ -69,15 +69,15 @@ function setup_homekit_config() {
local cleaned_json="/tmp/cache/homekit_cleaned.json"
jq '
# Keep only the homekit section if it exists, otherwise empty object
if has("homekit") then {homekit: .homekit} else {homekit: {}} end
if has("homekit") then {homekit: .homekit} else {} end
' "${temp_json}" > "${cleaned_json}" 2>/dev/null || {
echo '{"homekit": {}}' > "${cleaned_json}"
echo '{}' > "${cleaned_json}"
}
# Convert back to YAML and write to the config file
yq eval -P "${cleaned_json}" > "${config_path}" 2>/dev/null || {
echo "[WARNING] Failed to convert cleaned config to YAML, creating minimal config"
echo 'homekit: {}' > "${config_path}"
echo '{}' > "${config_path}"
}
# Clean up temp files

View File

@@ -80,14 +80,14 @@ if [ ! \( -f "$letsencrypt_path/privkey.pem" -a -f "$letsencrypt_path/fullchain.
fi
# build templates for optional FRIGATE_BASE_PATH environment variable
python3 /usr/local/nginx/get_base_path.py | \
python3 /usr/local/nginx/get_nginx_settings.py | \
tempio -template /usr/local/nginx/templates/base_path.gotmpl \
-out /usr/local/nginx/conf/base_path.conf
-out /usr/local/nginx/conf/base_path.conf
# build templates for optional TLS support
python3 /usr/local/nginx/get_listen_settings.py | \
tempio -template /usr/local/nginx/templates/listen.gotmpl \
-out /usr/local/nginx/conf/listen.conf
# build templates for additional network settings
python3 /usr/local/nginx/get_nginx_settings.py | \
tempio -template /usr/local/nginx/templates/listen.gotmpl \
-out /usr/local/nginx/conf/listen.conf
# Replace the bash process with the NGINX process, redirecting stderr to stdout
exec 2>&1

View File

@@ -23,8 +23,28 @@ sys.path.remove("/opt/frigate")
yaml = YAML()
# Check if arbitrary exec sources are allowed (defaults to False for security)
ALLOW_ARBITRARY_EXEC = os.environ.get(
"GO2RTC_ALLOW_ARBITRARY_EXEC", "false"
allow_arbitrary_exec = None
if "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.environ:
allow_arbitrary_exec = os.environ.get("GO2RTC_ALLOW_ARBITRARY_EXEC")
elif (
os.path.isdir("/run/secrets")
and os.access("/run/secrets", os.R_OK)
and "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.listdir("/run/secrets")
):
allow_arbitrary_exec = (
Path(os.path.join("/run/secrets", "GO2RTC_ALLOW_ARBITRARY_EXEC"))
.read_text()
.strip()
)
# check for the add-on options file
elif os.path.isfile("/data/options.json"):
with open("/data/options.json") as f:
raw_options = f.read()
options = json.loads(raw_options)
allow_arbitrary_exec = options.get("go2rtc_allow_arbitrary_exec")
ALLOW_ARBITRARY_EXEC = allow_arbitrary_exec is not None and str(
allow_arbitrary_exec
).lower() in ("true", "1", "yes")
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}

View File

@@ -1,11 +0,0 @@
"""Prints the base path as json to stdout."""
import json
import os
from typing import Any
base_path = os.environ.get("FRIGATE_BASE_PATH", "")
result: dict[str, Any] = {"base_path": base_path}
print(json.dumps(result))

View File

@@ -1,35 +0,0 @@
"""Prints the tls config as json to stdout."""
import json
import sys
from typing import Any
from ruamel.yaml import YAML
sys.path.insert(0, "/opt/frigate")
from frigate.util.config import find_config_file
sys.path.remove("/opt/frigate")
yaml = YAML()
config_file = find_config_file()
try:
with open(config_file) as f:
raw_config = f.read()
if config_file.endswith((".yaml", ".yml")):
config: dict[str, Any] = yaml.load(raw_config)
elif config_file.endswith(".json"):
config: dict[str, Any] = json.loads(raw_config)
except FileNotFoundError:
config: dict[str, Any] = {}
tls_config: dict[str, any] = config.get("tls", {"enabled": True})
networking_config = config.get("networking", {})
ipv6_config = networking_config.get("ipv6", {"enabled": False})
output = {"tls": tls_config, "ipv6": ipv6_config}
print(json.dumps(output))

View File

@@ -0,0 +1,62 @@
"""Prints the nginx settings as json to stdout."""
import json
import os
import sys
from typing import Any
from ruamel.yaml import YAML
sys.path.insert(0, "/opt/frigate")
from frigate.util.config import find_config_file
sys.path.remove("/opt/frigate")
yaml = YAML()
config_file = find_config_file()
try:
with open(config_file) as f:
raw_config = f.read()
if config_file.endswith((".yaml", ".yml")):
config: dict[str, Any] = yaml.load(raw_config)
elif config_file.endswith(".json"):
config: dict[str, Any] = json.loads(raw_config)
except FileNotFoundError:
config: dict[str, Any] = {}
tls_config: dict[str, Any] = config.get("tls", {})
tls_config.setdefault("enabled", True)
networking_config: dict[str, Any] = config.get("networking", {})
ipv6_config: dict[str, Any] = networking_config.get("ipv6", {})
ipv6_config.setdefault("enabled", False)
listen_config: dict[str, Any] = networking_config.get("listen", {})
listen_config.setdefault("internal", 5000)
listen_config.setdefault("external", 8971)
# handle case where internal port is a string with ip:port
internal_port = listen_config["internal"]
if type(internal_port) is str:
internal_port = int(internal_port.split(":")[-1])
listen_config["internal_port"] = internal_port
# handle case where external port is a string with ip:port
external_port = listen_config["external"]
if type(external_port) is str:
external_port = int(external_port.split(":")[-1])
listen_config["external_port"] = external_port
base_path = os.environ.get("FRIGATE_BASE_PATH", "")
result: dict[str, Any] = {
"tls": tls_config,
"ipv6": ipv6_config,
"listen": listen_config,
"base_path": base_path,
}
print(json.dumps(result))

View File

@@ -7,7 +7,7 @@ location ^~ {{ .base_path }}/ {
# remove base_url from the path before passing upstream
rewrite ^{{ .base_path }}/(.*) /$1 break;
proxy_pass $scheme://127.0.0.1:8971;
proxy_pass $scheme://127.0.0.1:{{ .listen.external_port }};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

View File

@@ -1,45 +1,36 @@
# Internal (IPv4 always; IPv6 optional)
listen 5000;
{{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:5000;{{ end }}{{ end }}
listen {{ .listen.internal }};
{{ if .ipv6.enabled }}listen [::]:{{ .listen.internal_port }};{{ end }}
# intended for external traffic, protected by auth
{{ if .tls }}
{{ if .tls.enabled }}
# external HTTPS (IPv4 always; IPv6 optional)
listen 8971 ssl;
{{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:8971 ssl;{{ end }}{{ end }}
{{ if .tls.enabled }}
# external HTTPS (IPv4 always; IPv6 optional)
listen {{ .listen.external }} ssl;
{{ if .ipv6.enabled }}listen [::]:{{ .listen.external_port }} ssl;{{ end }}
ssl_certificate /etc/letsencrypt/live/frigate/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/frigate/privkey.pem;
ssl_certificate /etc/letsencrypt/live/frigate/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/frigate/privkey.pem;
# generated 2024-06-01, Mozilla Guideline v5.7, nginx 1.25.3, OpenSSL 1.1.1w, modern configuration, no OCSP
# https://ssl-config.mozilla.org/#server=nginx&version=1.25.3&config=modern&openssl=1.1.1w&ocsp=false&guideline=5.7
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
# generated 2024-06-01, Mozilla Guideline v5.7, nginx 1.25.3, OpenSSL 1.1.1w, modern configuration, no OCSP
# https://ssl-config.mozilla.org/#server=nginx&version=1.25.3&config=modern&openssl=1.1.1w&ocsp=false&guideline=5.7
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
# modern configuration
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;
# modern configuration
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;
# ACME challenge location
location /.well-known/acme-challenge/ {
default_type "text/plain";
root /etc/letsencrypt/www;
}
{{ else }}
# external HTTP (IPv4 always; IPv6 optional)
listen 8971;
{{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:8971;{{ end }}{{ end }}
{{ end }}
# ACME challenge location
location /.well-known/acme-challenge/ {
default_type "text/plain";
root /etc/letsencrypt/www;
}
{{ else }}
# (No tls section) default to HTTP (IPv4 always; IPv6 optional)
listen 8971;
{{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:8971;{{ end }}{{ end }}
# (No tls) default to HTTP (IPv4 always; IPv6 optional)
listen {{ .listen.external }};
{{ if .ipv6.enabled }}listen [::]:{{ .listen.external_port }};{{ end }}
{{ end }}

View File

@@ -13,7 +13,7 @@ ARG ROCM
RUN apt update -qq && \
apt install -y wget gpg && \
wget -O rocm.deb https://repo.radeon.com/amdgpu-install/7.1.1/ubuntu/jammy/amdgpu-install_7.1.1.70101-1_all.deb && \
wget -O rocm.deb https://repo.radeon.com/amdgpu-install/7.2/ubuntu/jammy/amdgpu-install_7.2.70200-1_all.deb && \
apt install -y ./rocm.deb && \
apt update && \
apt install -qq -y rocm
@@ -56,6 +56,8 @@ FROM scratch AS rocm-dist
ARG ROCM
# Copy HIP headers required for MIOpen JIT (BuildHip) / HIPRTC at runtime
COPY --from=rocm /opt/rocm-${ROCM}/include/ /opt/rocm-${ROCM}/include/
COPY --from=rocm /opt/rocm-$ROCM/bin/rocminfo /opt/rocm-$ROCM/bin/migraphx-driver /opt/rocm-$ROCM/bin/
# Copy MIOpen database files for gfx10xx and gfx11xx only (RDNA2/RDNA3)
COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx10* /opt/rocm-$ROCM/share/miopen/db/

View File

@@ -1 +1 @@
onnxruntime-migraphx @ https://github.com/NickM-27/frigate-onnxruntime-rocm/releases/download/v7.1.0/onnxruntime_migraphx-1.23.1-cp311-cp311-linux_x86_64.whl
onnxruntime-migraphx @ https://github.com/NickM-27/frigate-onnxruntime-rocm/releases/download/v7.2.0/onnxruntime_migraphx-1.23.1-cp311-cp311-linux_x86_64.whl

View File

@@ -1,5 +1,5 @@
variable "ROCM" {
default = "7.1.1"
default = "7.2.0"
}
variable "HSA_OVERRIDE_GFX_VERSION" {
default = ""

View File

@@ -155,34 +155,33 @@ services:
### Enabling IPv6
IPv6 is disabled by default, to enable IPv6 listen.gotmpl needs to be bind mounted with IPv6 enabled. For example:
IPv6 is disabled by default, to enable IPv6 modify your Frigate configuration as follows:
```
{{ if not .enabled }}
# intended for external traffic, protected by auth
listen 8971;
{{ else }}
# intended for external traffic, protected by auth
listen 8971 ssl;
# intended for internal traffic, not protected by auth
listen 5000;
```yaml
networking:
ipv6:
enabled: True
```
becomes
### Listen on different ports
```
{{ if not .enabled }}
# intended for external traffic, protected by auth
listen [::]:8971 ipv6only=off;
{{ else }}
# intended for external traffic, protected by auth
listen [::]:8971 ipv6only=off ssl;
You can change the ports Nginx uses for listening using Frigate's configuration file. The internal port (unauthenticated) and external port (authenticated) can be changed independently. You can also specify an IP address using the format `ip:port` if you wish to bind the port to a specific interface. This may be useful for example to prevent exposing the internal port outside the container.
# intended for internal traffic, not protected by auth
listen [::]:5000 ipv6only=off;
For example:
```yaml
networking:
listen:
internal: 127.0.0.1:5000
external: 8971
```
:::warning
This setting is for advanced users. For the majority of use cases it's recommended to change the `ports` section of your Docker compose file or use the Docker `run` `--publish` option instead, e.g. `-p 443:8971`. Changing Frigate's ports may break some integrations.
:::
## Base path
By default, Frigate runs at the root path (`/`). However some setups require to run Frigate under a custom path prefix (e.g. `/frigate`), especially when Frigate is located behind a reverse proxy that requires path-based routing.
@@ -234,7 +233,7 @@ To do this:
### Custom go2rtc version
Frigate currently includes go2rtc v1.9.10, there may be certain cases where you want to run a different version of go2rtc.
Frigate currently includes go2rtc v1.9.13, there may be certain cases where you want to run a different version of go2rtc.
To do this:

View File

@@ -29,6 +29,10 @@ auth:
reset_admin_password: true
```
## Password guidance
Constructing secure passwords and managing them properly is important. Frigate requires a minimum length of 12 characters. For guidance on password standards see [NIST SP 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html). To learn what makes a password truly secure, read this [article](https://medium.com/peerio/how-to-build-a-billion-dollar-password-3d92568d9277).
## Login failure rate limiting
In order to limit the risk of brute force attacks, rate limiting is available for login failures. This is implemented with SlowApi, and the string notation for valid values is available in [the documentation](https://limits.readthedocs.io/en/stable/quickstart.html#examples).
@@ -162,6 +166,10 @@ In this example:
- If no mapping matches, Frigate falls back to `default_role` if configured.
- If `role_map` is not defined, Frigate assumes the role header directly contains `admin`, `viewer`, or a custom role name.
**Note on matching semantics:**
- Admin precedence: if the `admin` mapping matches, Frigate resolves the session to `admin` to avoid accidental downgrade when a user belongs to multiple groups (for example both `admin` and `viewer` groups).
#### Port Considerations
**Authenticated Port (8971)**

View File

@@ -244,7 +244,7 @@ go2rtc:
- rtspx://192.168.1.1:7441/abcdefghijk
```
[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#source-rtsp)
[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#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.

View File

@@ -79,6 +79,12 @@ cameras:
If the ONVIF connection is successful, PTZ controls will be available in the camera's WebUI.
:::note
Some cameras use a separate ONVIF/service account that is distinct from the device administrator credentials. If ONVIF authentication fails with the admin account, try creating or using an ONVIF/service user in the camera's firmware. Refer to your camera manufacturer's documentation for more.
:::
:::tip
If your ONVIF camera does not require authentication credentials, you may still need to specify an empty string for `user` and `password`, eg: `user: ""` and `password: ""`.
@@ -95,7 +101,7 @@ The FeatureList on the [ONVIF Conformant Products Database](https://www.onvif.or
| Brand or specific camera | PTZ Controls | Autotracking | Notes |
| ---------------------------- | :----------: | :----------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Amcrest | ✅ | ✅ | ⛔️ Generally, Amcrest should work, but some older models (like the common IP2M-841) don't support autotracking |
| Amcrest | ✅ | ✅ | ⛔️ Generally, Amcrest should work, but some older models (like the common IP2M-841) don't support autotracking |
| Amcrest ASH21 | ✅ | ❌ | ONVIF service port: 80 |
| Amcrest IP4M-S2112EW-AI | ✅ | ❌ | FOV relative movement not supported. |
| Amcrest IP5M-1190EW | ✅ | ❌ | ONVIF Port: 80. FOV relative movement not supported. |

View File

@@ -1,249 +0,0 @@
---
id: genai
title: Generative AI
---
Generative AI can be used to automatically generate descriptive text based on the thumbnails of your tracked objects. This helps with [Semantic Search](/configuration/semantic_search) in Frigate to provide more context about your tracked objects. Descriptions are accessed via the _Explore_ view in the Frigate UI by clicking on a tracked object's thumbnail.
Requests for a description are sent off automatically to your AI provider at the end of the tracked object's lifecycle, or can optionally be sent earlier after a number of significantly changed frames, for example in use in more real-time notifications. Descriptions can also be regenerated manually via the Frigate UI. Note that if you are manually entering a description for tracked objects prior to its end, this will be overwritten by the generated response.
## Configuration
Generative AI can be enabled for all cameras or only for specific cameras. If GenAI is disabled for a camera, you can still manually generate descriptions for events using the HTTP API. There are currently 3 native providers available to integrate with Frigate. Other providers that support the OpenAI standard API can also be used. See the OpenAI section below.
To use Generative AI, you must define a single provider at the global level of your Frigate configuration. If the provider you choose requires an API key, you may either directly paste it in your configuration, or store it in an environment variable prefixed with `FRIGATE_`.
```yaml
genai:
provider: gemini
api_key: "{FRIGATE_GEMINI_API_KEY}"
model: gemini-2.0-flash
cameras:
front_camera:
genai:
enabled: True # <- enable GenAI for your front camera
use_snapshot: True
objects:
- person
required_zones:
- steps
indoor_camera:
objects:
genai:
enabled: False # <- disable GenAI for your indoor camera
```
By default, descriptions will be generated for all tracked objects and all zones. But you can also optionally specify `objects` and `required_zones` to only generate descriptions for certain tracked objects or zones.
Optionally, you can generate the description using a snapshot (if enabled) by setting `use_snapshot` to `True`. By default, this is set to `False`, which sends the uncompressed images from the `detect` stream collected over the object's lifetime to the model. Once the object lifecycle ends, only a single compressed and cropped thumbnail is saved with the tracked object. Using a snapshot might be useful when you want to _regenerate_ a tracked object's description as it will provide the AI with a higher-quality image (typically downscaled by the AI itself) than the cropped/compressed thumbnail. Using a snapshot otherwise has a trade-off in that only a single image is sent to your provider, which will limit the model's ability to determine object movement or direction.
Generative AI can also be toggled dynamically for a camera via MQTT with the topic `frigate/<camera_name>/object_descriptions/set`. See the [MQTT documentation](/integrations/mqtt/#frigatecamera_nameobjectdescriptionsset).
## Ollama
:::warning
Using Ollama on CPU is not recommended, high inference times make using Generative AI impractical.
:::
[Ollama](https://ollama.com/) allows you to self-host large language models and keep everything running locally. It is highly recommended to host this server on a machine with an Nvidia graphics card, or on a Apple silicon Mac for best performance.
Most of the 7b parameter 4-bit vision models will fit inside 8GB of VRAM. There is also a [Docker container](https://hub.docker.com/r/ollama/ollama) available.
Parallel requests also come with some caveats. You will need to set `OLLAMA_NUM_PARALLEL=1` and choose a `OLLAMA_MAX_QUEUE` and `OLLAMA_MAX_LOADED_MODELS` values that are appropriate for your hardware and preferences. See the [Ollama documentation](https://docs.ollama.com/faq#how-does-ollama-handle-concurrent-requests).
### Model Types: Instruct vs Thinking
Most vision-language models are available as **instruct** models, which are fine-tuned to follow instructions and respond concisely to prompts. However, some models (such as certain Qwen-VL or minigpt variants) offer both **instruct** and **thinking** versions.
- **Instruct models** are always recommended for use with Frigate. These models generate direct, relevant, actionable descriptions that best fit Frigate's object and event summary use case.
- **Thinking models** are fine-tuned for more free-form, open-ended, and speculative outputs, which are typically not concise and may not provide the practical summaries Frigate expects. For this reason, Frigate does **not** recommend or support using thinking models.
Some models are labeled as **hybrid** (capable of both thinking and instruct tasks). In these cases, Frigate will always use instruct-style prompts and specifically disables thinking-mode behaviors to ensure concise, useful responses.
**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 providers 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.
:::note
You should have at least 8 GB of RAM available (or VRAM if running on GPU) to run the 7B models, 16 GB to run the 13B models, and 32 GB to run the 33B models.
:::
#### Ollama Cloud models
Ollama also supports [cloud models](https://ollama.com/cloud), where your local Ollama instance handles requests from Frigate, but model inference is performed in the cloud. Set up Ollama locally, sign in with your Ollama account, and specify the cloud model name in your Frigate config. For more details, see the Ollama cloud model [docs](https://docs.ollama.com/cloud).
### Configuration
```yaml
genai:
provider: ollama
base_url: http://localhost:11434
model: qwen3-vl:4b
```
## 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.
### Supported Models
You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://ai.google.dev/gemini-api/docs/models/gemini).
### Get API Key
To start using Gemini, you must first get an API key from [Google AI Studio](https://aistudio.google.com).
1. Accept the Terms of Service
2. Click "Get API Key" from the right hand navigation
3. Click "Create API key in new project"
4. Copy the API key for use in your config
### Configuration
```yaml
genai:
provider: gemini
api_key: "{FRIGATE_GEMINI_API_KEY}"
model: gemini-2.0-flash
```
:::note
To use a different Gemini-compatible API endpoint, set the `GEMINI_BASE_URL` environment variable to your provider's API URL.
:::
## OpenAI
OpenAI does not have a free tier for their API. With the release of gpt-4o, pricing has been reduced and each generation should cost fractions of a cent if you choose to go this route.
### Supported Models
You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://platform.openai.com/docs/models).
### Get API Key
To start using OpenAI, you must first [create an API key](https://platform.openai.com/api-keys) and [configure billing](https://platform.openai.com/settings/organization/billing/overview).
### Configuration
```yaml
genai:
provider: openai
api_key: "{FRIGATE_OPENAI_API_KEY}"
model: gpt-4o
```
:::note
To use a different OpenAI-compatible API endpoint, set the `OPENAI_BASE_URL` environment variable to your provider's API URL.
:::
## Azure OpenAI
Microsoft offers several vision models through Azure OpenAI. A subscription is required.
### Supported Models
You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models).
### Create Resource and Get API Key
To start using Azure OpenAI, you must first [create a resource](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource). You'll need your API key, model name, and resource URL, which must include the `api-version` parameter (see the example below).
### Configuration
```yaml
genai:
provider: azure_openai
base_url: https://instance.cognitiveservices.azure.com/openai/responses?api-version=2025-04-01-preview
model: gpt-5-mini
api_key: "{FRIGATE_OPENAI_API_KEY}"
```
## Usage and Best Practices
Frigate's thumbnail search excels at identifying specific details about tracked objects for example, using an "image caption" approach to find a "person wearing a yellow vest," "a white dog running across the lawn," or "a red car on a residential street." To enhance this further, Frigates default prompts are designed to ask your AI provider about the intent behind the object's actions, rather than just describing its appearance.
While generating simple descriptions of detected objects is useful, understanding intent provides a deeper layer of insight. Instead of just recognizing "what" is in a scene, Frigates default prompts aim to infer "why" it might be there or "what" it could do next. Descriptions tell you whats happening, but intent gives context. For instance, a person walking toward a door might seem like a visitor, but if theyre moving quickly after hours, you can infer a potential break-in attempt. Detecting a person loitering near a door at night can trigger an alert sooner than simply noting "a person standing by the door," helping you respond based on the situations context.
### Using GenAI for notifications
Frigate provides an [MQTT topic](/integrations/mqtt), `frigate/tracked_object_update`, that is updated with a JSON payload containing `event_id` and `description` when your AI provider returns a description for a tracked object. This description could be used directly in notifications, such as sending alerts to your phone or making audio announcements. If additional details from the tracked object are needed, you can query the [HTTP API](/integrations/api/event-events-event-id-get) using the `event_id`, eg: `http://frigate_ip:5000/api/events/<event_id>`.
If looking to get notifications earlier than when an object ceases to be tracked, an additional send trigger can be configured of `after_significant_updates`.
```yaml
genai:
send_triggers:
tracked_object_end: true # default
after_significant_updates: 3 # how many updates to a tracked object before we should send an image
```
## Custom Prompts
Frigate sends multiple frames from the tracked object along with a prompt to your Generative AI provider asking it to generate a description. The default prompt is as follows:
```
Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.
```
:::tip
Prompts can use variable replacements `{label}`, `{sub_label}`, and `{camera}` to substitute information from the tracked object as part of the prompt.
:::
You are also able to define custom prompts in your configuration.
```yaml
genai:
provider: ollama
base_url: http://localhost:11434
model: qwen3-vl:8b-instruct
objects:
prompt: "Analyze the {label} in these images from the {camera} security camera. Focus on the actions, behavior, and potential intent of the {label}, rather than just describing its appearance."
object_prompts:
person: "Examine the main person in these images. What are they doing and what might their actions suggest about their intent (e.g., approaching a door, leaving an area, standing still)? Do not describe the surroundings or static details."
car: "Observe the primary vehicle in these images. Focus on its movement, direction, or purpose (e.g., parking, approaching, circling). If it's a delivery vehicle, mention the company."
```
Prompts can also be overridden at the camera level to provide a more detailed prompt to the model about your specific camera, if you desire.
```yaml
cameras:
front_door:
objects:
genai:
enabled: True
use_snapshot: True
prompt: "Analyze the {label} in these images from the {camera} security camera at the front door. Focus on the actions and potential intent of the {label}."
object_prompts:
person: "Examine the person in these images. What are they doing, and how might their actions suggest their purpose (e.g., delivering something, approaching, leaving)? If they are carrying or interacting with a package, include details about its source or destination."
cat: "Observe the cat in these images. Focus on its movement and intent (e.g., wandering, hunting, interacting with objects). If the cat is near the flower pots or engaging in any specific actions, mention it."
objects:
- person
- cat
required_zones:
- steps
```
### Experiment with prompts
Many providers also have a public facing chat interface for their models. Download a couple of different thumbnails or snapshots from Frigate and try new things in the playground to get descriptions to your liking before updating the prompt in Frigate.
- OpenAI - [ChatGPT](https://chatgpt.com)
- Gemini - [Google AI Studio](https://aistudio.google.com)
- Ollama - [Open WebUI](https://docs.openwebui.com/)

View File

@@ -5,7 +5,7 @@ title: Configuring Generative AI
## Configuration
A Generative AI provider can be configured in the global config, which will make the Generative AI features available for use. There are currently 3 native providers available to integrate with Frigate. Other providers that support the OpenAI standard API can also be used. See the OpenAI section below.
A Generative AI provider can be configured in the global config, which will make the Generative AI features available for use. There are currently 4 native providers available to integrate with Frigate. Other providers that support the OpenAI standard API can also be used. See the OpenAI section below.
To use Generative AI, you must define a single provider at the global level of your Frigate configuration. If the provider you choose requires an API key, you may either directly paste it in your configuration, or store it in an environment variable prefixed with `FRIGATE_`.
@@ -17,11 +17,23 @@ Using Ollama on CPU is not recommended, high inference times make using Generati
:::
[Ollama](https://ollama.com/) allows you to self-host large language models and keep everything running locally. It provides a nice API over [llama.cpp](https://github.com/ggerganov/llama.cpp). It is highly recommended to host this server on a machine with an Nvidia graphics card, or on a Apple silicon Mac for best performance.
[Ollama](https://ollama.com/) allows you to self-host large language models and keep everything running locally. It is highly recommended to host this server on a machine with an Nvidia graphics card, or on a Apple silicon Mac for best performance.
Most of the 7b parameter 4-bit vision models will fit inside 8GB of VRAM. There is also a [Docker container](https://hub.docker.com/r/ollama/ollama) available.
Parallel requests also come with some caveats. You will need to set `OLLAMA_NUM_PARALLEL=1` and choose a `OLLAMA_MAX_QUEUE` and `OLLAMA_MAX_LOADED_MODELS` values that are appropriate for your hardware and preferences. See the [Ollama documentation](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-does-ollama-handle-concurrent-requests).
Parallel requests also come with some caveats. You will need to set `OLLAMA_NUM_PARALLEL=1` and choose a `OLLAMA_MAX_QUEUE` and `OLLAMA_MAX_LOADED_MODELS` values that are appropriate for your hardware and preferences. See the [Ollama documentation](https://docs.ollama.com/faq#how-does-ollama-handle-concurrent-requests).
### Model Types: Instruct vs Thinking
Most vision-language models are available as **instruct** models, which are fine-tuned to follow instructions and respond concisely to prompts. However, some models (such as certain Qwen-VL or minigpt variants) offer both **instruct** and **thinking** versions.
- **Instruct models** are always recommended for use with Frigate. These models generate direct, relevant, actionable descriptions that best fit Frigate's object and event summary use case.
- **Thinking models** are fine-tuned for more free-form, open-ended, and speculative outputs, which are typically not concise and may not provide the practical summaries Frigate expects. For this reason, Frigate does **not** recommend or support using thinking models.
Some models are labeled as **hybrid** (capable of both thinking and instruct tasks). In these cases, Frigate will always use instruct-style prompts and specifically disables thinking-mode behaviors to ensure concise, useful responses.
**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 providers documentation or model library for guidance on the correct model variant to use.
### Supported Models
@@ -41,12 +53,12 @@ If you are trying to use a single model for Frigate and HomeAssistant, it will n
The following models are recommended:
| Model | Notes |
| ----------------- | -------------------------------------------------------------------- |
| `qwen3-vl` | Strong visual and situational understanding, higher vram requirement |
| `Intern3.5VL` | Relatively fast with good vision comprehension |
| `gemma3` | Strong frame-to-frame understanding, slower inference times |
| `qwen2.5-vl` | Fast but capable model with good vision comprehension |
| Model | Notes |
| ------------- | -------------------------------------------------------------------- |
| `qwen3-vl` | Strong visual and situational understanding, higher vram requirement |
| `Intern3.5VL` | Relatively fast with good vision comprehension |
| `gemma3` | Strong frame-to-frame understanding, slower inference times |
| `qwen2.5-vl` | Fast but capable model with good vision comprehension |
:::note
@@ -54,26 +66,64 @@ You should have at least 8 GB of RAM available (or VRAM if running on GPU) to ru
:::
#### Ollama Cloud models
Ollama also supports [cloud models](https://ollama.com/cloud), where your local Ollama instance handles requests from Frigate, but model inference is performed in the cloud. Set up Ollama locally, sign in with your Ollama account, and specify the cloud model name in your Frigate config. For more details, see the Ollama cloud model [docs](https://docs.ollama.com/cloud).
### Configuration
```yaml
genai:
provider: ollama
base_url: http://localhost:11434
model: minicpm-v:8b
provider_options: # other Ollama client options can be defined
model: qwen3-vl:4b
provider_options: # other Ollama client options can be defined
keep_alive: -1
options:
num_ctx: 8192 # make sure the context matches other services that are using ollama
num_ctx: 8192 # make sure the context matches other services that are using ollama
```
## Google Gemini
## llama.cpp
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.
[llama.cpp](https://github.com/ggml-org/llama.cpp) is a C++ implementation of LLaMA that provides a high-performance inference server. Using llama.cpp directly gives you access to all native llama.cpp options and parameters.
:::warning
Using llama.cpp on CPU is not recommended, high inference times make using Generative AI impractical.
:::
It is highly recommended to host the llama.cpp server on a machine with a discrete graphics card, or on an Apple silicon Mac for best performance.
### Supported Models
You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://ai.google.dev/gemini-api/docs/models/gemini). At the time of writing, this includes `gemini-1.5-pro` and `gemini-1.5-flash`.
You must use a vision capable model with Frigate. The llama.cpp server supports various vision models in GGUF format.
### Configuration
```yaml
genai:
provider: llamacpp
base_url: http://localhost:8080
model: your-model-name
provider_options:
temperature: 0.7
repeat_penalty: 1.05
top_p: 0.8
top_k: 40
min_p: 0.05
seed: -1
```
All llama.cpp native options can be passed through `provider_options`, including `temperature`, `top_k`, `top_p`, `min_p`, `repeat_penalty`, `repeat_last_n`, `seed`, `grammar`, and more. See the [llama.cpp server documentation](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md) for a complete list of available parameters.
## Google Gemini
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
You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://ai.google.dev/gemini-api/docs/models/gemini).
### Get API Key
@@ -90,16 +140,32 @@ 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-1.5-flash
model: gemini-2.5-flash
```
:::note
To use a different Gemini-compatible API endpoint, set the `provider_options` with the `base_url` key to your provider's API URL. For example:
```
genai:
provider: gemini
...
provider_options:
base_url: https://...
```
Other HTTP options are available, see the [python-genai documentation](https://github.com/googleapis/python-genai).
:::
## OpenAI
OpenAI does not have a free tier for their API. With the release of gpt-4o, pricing has been reduced and each generation should cost fractions of a cent if you choose to go this route.
### Supported Models
You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://platform.openai.com/docs/models). At the time of writing, this includes `gpt-4o` and `gpt-4-turbo`.
You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://platform.openai.com/docs/models).
### Get API Key
@@ -120,23 +186,41 @@ To use a different OpenAI-compatible API endpoint, set the `OPENAI_BASE_URL` env
:::
:::tip
For OpenAI-compatible servers (such as llama.cpp) that don't expose the configured context size in the API response, you can manually specify the context size in `provider_options`:
```yaml
genai:
provider: openai
base_url: http://your-llama-server
model: your-model-name
provider_options:
context_size: 8192 # Specify the configured context size
```
This ensures Frigate uses the correct context window size when generating prompts.
:::
## Azure OpenAI
Microsoft offers several vision models through Azure OpenAI. A subscription is required.
### Supported Models
You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models). At the time of writing, this includes `gpt-4o` and `gpt-4-turbo`.
You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models).
### Create Resource and Get API Key
To start using Azure OpenAI, you must first [create a resource](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource). You'll need your API key and resource URL, which must include the `api-version` parameter (see the example below). The model field is not required in your configuration as the model is part of the deployment name you chose when deploying the resource.
To start using Azure OpenAI, you must first [create a resource](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource). You'll need your API key, model name, and resource URL, which must include the `api-version` parameter (see the example below).
### Configuration
```yaml
genai:
provider: azure_openai
base_url: https://example-endpoint.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2023-03-15-preview
base_url: https://instance.cognitiveservices.azure.com/openai/responses?api-version=2025-04-01-preview
model: gpt-5-mini
api_key: "{FRIGATE_OPENAI_API_KEY}"
```
```

View File

@@ -11,7 +11,7 @@ By default, descriptions will be generated for all tracked objects and all zones
Optionally, you can generate the description using a snapshot (if enabled) by setting `use_snapshot` to `True`. By default, this is set to `False`, which sends the uncompressed images from the `detect` stream collected over the object's lifetime to the model. Once the object lifecycle ends, only a single compressed and cropped thumbnail is saved with the tracked object. Using a snapshot might be useful when you want to _regenerate_ a tracked object's description as it will provide the AI with a higher-quality image (typically downscaled by the AI itself) than the cropped/compressed thumbnail. Using a snapshot otherwise has a trade-off in that only a single image is sent to your provider, which will limit the model's ability to determine object movement or direction.
Generative AI object descriptions can also be toggled dynamically for a camera via MQTT with the topic `frigate/<camera_name>/object_descriptions/set`. See the [MQTT documentation](/integrations/mqtt/#frigatecamera_nameobjectdescriptionsset).
Generative AI object descriptions can also be toggled dynamically for a camera via MQTT with the topic `frigate/<camera_name>/object_descriptions/set`. See the [MQTT documentation](/integrations/mqtt#frigatecamera_nameobject_descriptionsset).
## Usage and Best Practices
@@ -75,4 +75,4 @@ Many providers also have a public facing chat interface for their models. Downlo
- OpenAI - [ChatGPT](https://chatgpt.com)
- Gemini - [Google AI Studio](https://aistudio.google.com)
- Ollama - [Open WebUI](https://docs.openwebui.com/)
- Ollama - [Open WebUI](https://docs.openwebui.com/)

View File

@@ -7,7 +7,7 @@ Generative AI can be used to automatically generate structured summaries of revi
Requests for a summary are requested automatically to your AI provider for alert review items when the activity has ended, they can also be optionally enabled for detections as well.
Generative AI review summaries can also be toggled dynamically for a [camera via MQTT](/integrations/mqtt/#frigatecamera_namereviewdescriptionsset).
Generative AI review summaries can also be toggled dynamically for a [camera via MQTT](/integrations/mqtt#frigatecamera_namereview_descriptionsset).
## Review Summary Usage and Best Practices
@@ -125,10 +125,10 @@ review:
## Review Reports
Along with individual review item summaries, Generative AI provides the ability to request a report of a given time period. For example, you can get a daily report while on a vacation of any suspicious activity or other concerns that may require review.
Along with individual review item summaries, Generative AI can also produce a single report of review items from all cameras marked "suspicious" over a specified time period (for example, a daily summary of suspicious activity while you're on vacation).
### Requesting Reports Programmatically
Review reports can be requested via the [API](/integrations/api#review-summarization) by sending a POST request to `/api/review/summarize/start/{start_ts}/end/{end_ts}` with Unix timestamps.
Review reports can be requested via the [API](/integrations/api/generate-review-summary-review-summarize-start-start-ts-end-end-ts-post) by sending a POST request to `/api/review/summarize/start/{start_ts}/end/{end_ts}` with Unix timestamps.
For Home Assistant users, there is a built-in service (`frigate.review_summarize`) that makes it easy to request review reports as part of automations or scripts. This allows you to automatically generate daily summaries, vacation reports, or custom time period reports based on your specific needs.

View File

@@ -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`.
@@ -381,6 +381,7 @@ Start with ["Why isn't my license plate being detected and recognized?"](#why-is
```yaml
lpr:
enabled: true
device: CPU
debug_save_plates: true
```
@@ -432,6 +433,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.

View File

@@ -139,7 +139,13 @@ record:
:::tip
When using `hwaccel_args` globally hardware encoding is used for time lapse generation. The encoder determines its own behavior so the resulting file size may be undesirably large.
When using `hwaccel_args`, hardware encoding is used for timelapse generation. This setting can be overridden for a specific camera (e.g., when camera resolution exceeds hardware encoder limits); set `cameras.<camera>.record.export.hwaccel_args` with the appropriate settings. Using an unrecognized value or empty string will fall back to software encoding (libx264).
:::
:::tip
The encoder determines its own behavior so the resulting file size may be undesirably large.
To reduce the output file size the ffmpeg parameter `-qp n` can be utilized (where `n` stands for the value of the quantisation parameter). The value can be adjusted to get an acceptable tradeoff between quality and file size for the given scenario.
:::
@@ -148,19 +154,16 @@ To reduce the output file size the ffmpeg parameter `-qp n` can be utilized (whe
Apple devices running the Safari browser may fail to playback h.265 recordings. The [apple compatibility option](../configuration/camera_specific.md#h265-cameras-via-safari) should be used to ensure seamless playback on Apple devices.
## Syncing Recordings With Disk
## Syncing Media Files With Disk
In some cases the recordings files may be deleted but Frigate will not know this has happened. Recordings sync can be enabled which will tell Frigate to check the file system and delete any db entries for files which don't exist.
Media files (event snapshots, event thumbnails, review thumbnails, previews, exports, and recordings) can become orphaned when database entries are deleted but the corresponding files remain on disk.
```yaml
record:
sync_recordings: True
```
Normal operation may leave small numbers of orphaned files until Frigate's scheduled cleanup, but crashes, configuration changes, or upgrades may cause more orphaned files that Frigate does not clean up. This feature checks the file system for media files and removes any that are not referenced in the database.
This feature is meant to fix variations in files, not completely delete entries in the database. If you delete all of your media, don't use `sync_recordings`, just stop Frigate, delete the `frigate.db` database, and restart.
The Maintenance pane in the Frigate UI or an API endpoint `POST /api/media/sync` can be used to trigger a media sync. When using the API, a job ID is returned and the operation continues on the server. Status can be checked with the `/api/media/sync/status/{job_id}` endpoint.
:::warning
The sync operation uses considerable CPU resources and in most cases is not needed, only enable when necessary.
This operation uses considerable CPU resources and includes a safety threshold that aborts if more than 50% of files would be deleted. Only run when necessary. If you set `force: true` the safety threshold will be bypassed; do not use `force` unless you are certain the deletions are intended.
:::

View File

@@ -73,11 +73,19 @@ tls:
# Optional: Enable TLS for port 8971 (default: shown below)
enabled: True
# Optional: IPv6 configuration
# Optional: Networking configuration
networking:
# Optional: Enable IPv6 on 5000, and 8971 if tls is configured (default: shown below)
ipv6:
enabled: False
# Optional: Override ports Frigate uses for listening (defaults: shown below)
# An IP address may also be provided to bind to a specific interface, e.g. ip:port
# NOTE: This setting is for advanced users and may break some integrations. The majority
# of users should change ports in the docker compose file
# or use the docker run `--publish` option to select a different port.
listen:
internal: 5000
external: 8971
# Optional: Proxy configuration
proxy:
@@ -510,8 +518,6 @@ record:
# Optional: Number of minutes to wait between cleanup runs (default: shown below)
# This can be used to reduce the frequency of deleting recording segments from disk if you want to minimize i/o
expire_interval: 60
# Optional: Two-way sync recordings database with disk on startup and once a day (default: shown below).
sync_recordings: False
# Optional: Continuous retention settings
continuous:
# Optional: Number of days to retain recordings regardless of tracked objects or motion (default: shown below)
@@ -534,6 +540,8 @@ record:
# The -r (framerate) dictates how smooth the output video is.
# So the args would be -vf setpts=0.02*PTS -r 30 in that case.
timelapse_args: "-vf setpts=0.04*PTS -r 30"
# Optional: Global hardware acceleration settings for timelapse exports. (default: inherit)
hwaccel_args: auto
# Optional: Recording Preview Settings
preview:
# Optional: Quality of recording preview (default: shown below).
@@ -696,6 +704,9 @@ genai:
# Optional additional args to pass to the GenAI Provider (default: None)
provider_options:
keep_alive: -1
# Optional: Options to pass during inference calls (default: {})
runtime_options:
temperature: 0.7
# Optional: Configuration for audio transcription
# NOTE: only the enabled option can be overridden at the camera level
@@ -749,7 +760,7 @@ classification:
interval: None
# Optional: Restream configuration
# Uses https://github.com/AlexxIT/go2rtc (v1.9.10)
# Uses https://github.com/AlexxIT/go2rtc (v1.9.13)
# NOTE: The default go2rtc API port (1984) must be used,
# changing this port for the integrated go2rtc instance is not supported.
go2rtc:
@@ -835,6 +846,11 @@ cameras:
# Optional: camera specific output args (default: inherit)
# output_args:
# Optional: camera specific hwaccel args for timelapse export (default: inherit)
# record:
# export:
# hwaccel_args:
# Optional: timeout for highest scoring image before allowing it
# to be replaced by a newer image. (default: shown below)
best_image_timeout: 60

View File

@@ -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://<frigate_host>:8554/<camera_name>`. 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.10) 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.10#configuration) for more advanced configurations and features.
Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.13) 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.13#configuration) for more advanced configurations and features.
:::note
@@ -206,7 +206,13 @@ Enabling arbitrary exec sources allows execution of arbitrary commands through g
## Advanced Restream Configurations
The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#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.13#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below:
:::warning
The `exec:`, `echo:`, and `expr:` sources are disabled by default for security. You must set `GO2RTC_ALLOW_ARBITRARY_EXEC=true` to use them. See [Security: Restricted Stream Sources](#security-restricted-stream-sources) for more information.
:::
:::warning

View File

@@ -11,6 +11,12 @@ Cameras configured to output H.264 video and AAC audio will offer the most compa
- **Stream Viewing**: This stream will be rebroadcast as is to Home Assistant for viewing with the stream component. Setting this resolution too high will use significant bandwidth when viewing streams in Home Assistant, and they may not load reliably over slower connections.
:::tip
For the best experience in Frigate's UI, configure your camera so that the detection and recording streams use the same aspect ratio. For example, if your main stream is 3840x2160 (16:9), set your substream to 640x360 (also 16:9) instead of 640x480 (4:3). While not strictly required, matching aspect ratios helps ensure seamless live stream display and preview/recordings playback.
:::
### Choosing a detect resolution
The ideal resolution for detection is one where the objects you want to detect fit inside the dimensions of the model used by Frigate (320x320). Frigate does not pass the entire camera frame to object detection. It will crop an area of motion from the full frame and look in that portion of the frame. If the area being inspected is larger than 320x320, Frigate must resize it before running object detection. Higher resolutions do not improve the detection accuracy because the additional detail is lost in the resize. Below you can see a reference for how large a 320x320 area is against common resolutions.

View File

@@ -42,7 +42,7 @@ If the EQ13 is out of stock, the link below may take you to a suggested alternat
| ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------- |
| Beelink EQ13 (<a href="https://amzn.to/4jn2qVr" target="_blank" rel="nofollow noopener sponsored">Amazon</a>) | Can run object detection on several 1080p cameras with low-medium activity | Dual gigabit NICs for easy isolated camera network. |
| Intel 1120p ([Amazon](https://www.amazon.com/Beelink-i3-1220P-Computer-Display-Gigabit/dp/B0DDCKT9YP) | Can handle a large number of 1080p cameras with high activity | |
| Intel 125H ([Amazon](https://www.amazon.com/MINISFORUM-Pro-125H-Barebone-Computer-HDMI2-1/dp/B0FH21FSZM) | Can handle a significant number of 1080p cameras with high activity | Includes NPU for more efficient detection in 0.17+ |
| Intel 125H ([Amazon](https://www.amazon.com/MINISFORUM-Pro-125H-Barebone-Computer-HDMI2-1/dp/B0FH21FSZM) | Can handle a significant number of 1080p cameras with high activity | Includes NPU for more efficient detection in 0.17+ |
## Detectors
@@ -55,12 +55,10 @@ Frigate supports multiple different detectors that work on different types of ha
**Most Hardware**
- [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is available in m.2 format with a HAT for RPi devices offering a wide range of compatibility with devices.
- [Supports many model architectures](../../configuration/object_detectors#configuration)
- Runs best with tiny or small size models
- [Google Coral EdgeTPU](#google-coral-tpu): The Google Coral EdgeTPU is available in USB and m.2 format allowing for a wide range of compatibility with devices.
- [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#edge-tpu-detector)
- <CommunityBadge /> [MemryX](#memryx-mx3): The MX3 M.2 accelerator module is available in m.2 format allowing for a wide range of compatibility with devices.
@@ -89,7 +87,6 @@ Frigate supports multiple different detectors that work on different types of ha
**Nvidia**
- [TensortRT](#tensorrt---nvidia-gpu): TensorRT can run on Nvidia GPUs to provide efficient object detection.
- [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx-supported-models)
- Runs well with any size models including large
@@ -152,9 +149,7 @@ The OpenVINO detector type is able to run on:
:::note
Intel NPUs have seen [limited success in community deployments](https://github.com/blakeblackshear/frigate/discussions/13248#discussioncomment-12347357), although they remain officially unsupported.
In testing, the NPU delivered performance that was only comparable to — or in some cases worse than — the integrated GPU.
Intel B-series (Battlemage) GPUs are not officially supported with Frigate 0.17, though a user has [provided steps to rebuild the Frigate container](https://github.com/blakeblackshear/frigate/discussions/21257) with support for them.
:::
@@ -172,7 +167,7 @@ Inference speeds vary greatly depending on the CPU or GPU used, some known examp
| Intel N100 | ~ 15 ms | s-320: 30 ms | 320: ~ 25 ms | | Can only run one detector instance |
| Intel N150 | ~ 15 ms | t-320: 16 ms s-320: 24 ms | | | |
| Intel Iris XE | ~ 10 ms | t-320: 6 ms t-640: 14 ms s-320: 8 ms s-640: 16 ms | 320: ~ 10 ms 640: ~ 20 ms | 320-n: 33 ms | |
| Intel NPU | ~ 6 ms | s-320: 11 ms | 320: ~ 14 ms 640: ~ 34 ms | 320-n: 40 ms | |
| Intel NPU | ~ 6 ms | s-320: 11 ms s-640: 30 ms | 320: ~ 14 ms 640: ~ 34 ms | 320-n: 40 ms | |
| Intel Arc A310 | ~ 5 ms | t-320: 7 ms t-640: 11 ms s-320: 8 ms s-640: 15 ms | 320: ~ 8 ms 640: ~ 14 ms | | |
| Intel Arc A380 | ~ 6 ms | | 320: ~ 10 ms 640: ~ 22 ms | 336: 20 ms 448: 27 ms | |
| Intel Arc A750 | ~ 4 ms | | 320: ~ 8 ms | | |

View File

@@ -112,42 +112,65 @@ The Hailo-8 and Hailo-8L AI accelerators are available in both M.2 and HAT form
:::warning
The Raspberry Pi kernel includes an older version of the Hailo driver that is incompatible with Frigate. You **must** follow the installation steps below to install the correct driver version, and you **must** disable the built-in kernel driver as described in step 1.
On Raspberry Pi OS **Bookworm**, the kernel includes an older version of the Hailo driver that is incompatible with Frigate. You **must** follow the installation steps below to install the correct driver version, and you **must** disable the built-in kernel driver as described in step 1.
On Raspberry Pi OS **Trixie**, the Hailo driver is no longer shipped with the kernel. It is installed via DKMS, and the conflict described below does not apply. You can simply run the installation script.
:::
1. **Disable the built-in Hailo driver (Raspberry Pi only)**:
1. **Disable the built-in Hailo driver (Raspberry Pi Bookworm OS only)**:
:::note
If you are **not** using a Raspberry Pi, skip this step and proceed directly to step 2.
If you are **not** using a Raspberry Pi with **Bookworm OS**, skip this step and proceed directly to step 2.
If you are using Raspberry Pi with **Trixie OS**, also skip this step and proceed directly to step 2.
:::
If you are using a Raspberry Pi, you need to blacklist the built-in kernel Hailo driver to prevent conflicts. First, check if the driver is currently loaded:
First, check if the driver is currently loaded:
```bash
lsmod | grep hailo
```
If it shows `hailo_pci`, unload it:
```bash
sudo rmmod hailo_pci
sudo modprobe -r hailo_pci
```
Now blacklist the driver to prevent it from loading on boot:
Then locate the built-in kernel driver and rename it so it cannot be loaded.
Renaming allows the original driver to be restored later if needed.
First, locate the currently installed kernel module:
```bash
echo "blacklist hailo_pci" | sudo tee /etc/modprobe.d/blacklist-hailo_pci.conf
modinfo -n hailo_pci
```
Update initramfs to ensure the blacklist takes effect:
Example output:
```
/lib/modules/6.6.31+rpt-rpi-2712/kernel/drivers/media/pci/hailo/hailo_pci.ko.xz
```
Save the module path to a variable:
```bash
sudo update-initramfs -u
BUILTIN=$(modinfo -n hailo_pci)
```
And rename the module by appending .bak:
```bash
sudo mv "$BUILTIN" "${BUILTIN}.bak"
```
Now refresh the kernel module map so the system recognizes the change:
```bash
sudo depmod -a
```
Reboot your Raspberry Pi:
```bash
@@ -160,9 +183,9 @@ The Raspberry Pi kernel includes an older version of the Hailo driver that is in
lsmod | grep hailo
```
This command should return no results. If it still shows `hailo_pci`, the blacklist did not take effect properly and you may need to check for other Hailo packages installed via apt that are loading the driver.
This command should return no results.
2. **Run the installation script**:
3. **Run the installation script**:
Download the installation script:
@@ -190,7 +213,7 @@ The Raspberry Pi kernel includes an older version of the Hailo driver that is in
- Download and install the required firmware
- Set up udev rules
3. **Reboot your system**:
4. **Reboot your system**:
After the script completes successfully, reboot to load the firmware:
@@ -198,7 +221,7 @@ The Raspberry Pi kernel includes an older version of the Hailo driver that is in
sudo reboot
```
4. **Verify the installation**:
5. **Verify the installation**:
After rebooting, verify that the Hailo device is available:
@@ -212,6 +235,38 @@ The Raspberry Pi kernel includes an older version of the Hailo driver that is in
lsmod | grep hailo_pci
```
Verify the driver version:
```bash
cat /sys/module/hailo_pci/version
```
Verify that the firmware was installed correctly:
```bash
ls -l /lib/firmware/hailo/hailo8_fw.bin
```
**Optional: Fix PCIe descriptor page size error**
If you encounter the following error:
```
[HailoRT] [error] CHECK failed - max_desc_page_size given 16384 is bigger than hw max desc page size 4096
```
Create a configuration file to force the correct descriptor page size:
```bash
echo 'options hailo_pci force_desc_page_size=4096' | sudo tee /etc/modprobe.d/hailo_pci.conf
```
and reboot:
```bash
sudo reboot
```
#### Setup
To set up Frigate, follow the default installation instructions, for example: `ghcr.io/blakeblackshear/frigate:stable`

View File

@@ -11,7 +11,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.10#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.13#module-streams), not just rtsp.
:::tip
@@ -47,8 +47,8 @@ After adding this to the config, restart Frigate and try to watch the live strea
- 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.10#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.10#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.
- If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#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.13#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:

View File

@@ -37,7 +37,7 @@ cameras:
## Steps
1. Export or copy the clip you want to replay to the Frigate host (e.g., `/media/frigate/` or `debug/clips/`).
1. Export or copy the clip you want to replay to the Frigate host (e.g., `/media/frigate/` or `debug/clips/`). Depending on what you are looking to debug, it is often helpful to add some "pre-capture" time (where the tracked object is not yet visible) to the clip when exporting.
2. Add the temporary camera to `config/config.yml` (example above). Use a unique name such as `test` or `replay_camera` so it's easy to remove later.
- If you're debugging a specific camera, copy the settings from that camera (frame rate, model/enrichment settings, zones, etc.) into the temporary camera so the replay closely matches the original environment. Leave `record` and `snapshots` disabled unless you are specifically debugging recording or snapshot behavior.
3. Restart Frigate.

View File

@@ -18490,9 +18490,9 @@
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"

View File

@@ -28,7 +28,7 @@ const sidebars: SidebarsConfig = {
{
type: "link",
label: "Go2RTC Configuration Reference",
href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.10#configuration",
href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.13#configuration",
} as PropSidebarItemLink,
],
Detectors: [

8
docs/static/_headers vendored Normal file
View File

@@ -0,0 +1,8 @@
https://:project.pages.dev/*
X-Robots-Tag: noindex
https://:version.:project.pages.dev/*
X-Robots-Tag: noindex
https://docs-dev.frigate.video/*
X-Robots-Tag: noindex

View File

@@ -331,6 +331,59 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/HTTPValidationError"
/media/sync:
post:
tags:
- App
summary: Start media sync job
description: |-
Start an asynchronous media sync job to find and (optionally) remove orphaned media files.
Returns 202 with job details when queued, or 409 if a job is already running.
operationId: sync_media_media_sync_post
requestBody:
required: true
content:
application/json:
responses:
"202":
description: Accepted - Job queued
"409":
description: Conflict - Job already running
"422":
description: Validation Error
/media/sync/current:
get:
tags:
- App
summary: Get current media sync job
description: |-
Retrieve the current running media sync job, if any. Returns the job details or null when no job is active.
operationId: get_media_sync_current_media_sync_current_get
responses:
"200":
description: Successful Response
"422":
description: Validation Error
/media/sync/status/{job_id}:
get:
tags:
- App
summary: Get media sync job status
description: |-
Get status and results for the specified media sync job id. Returns 200 with job details including results, or 404 if the job is not found.
operationId: get_media_sync_status_media_sync_status__job_id__get
parameters:
- name: job_id
in: path
responses:
"200":
description: Successful Response
"404":
description: Not Found - Job not found
"422":
description: Validation Error
/faces/train/{name}/classify:
post:
tags:
@@ -3147,6 +3200,7 @@ paths:
duration: 30
include_recording: true
draw: {}
pre_capture: null
responses:
"200":
description: Successful Response
@@ -4949,6 +5003,12 @@ components:
- type: "null"
title: Draw
default: {}
pre_capture:
anyOf:
- type: integer
- type: "null"
title: Pre Capture Seconds
default: null
type: object
title: EventsCreateBody
EventsDeleteBody:

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 MiB

After

Width:  |  Height:  |  Size: 12 MiB

View File

@@ -23,17 +23,30 @@ 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.request.app_body import AppConfigSetBody, MediaSyncBody
from frigate.api.defs.tags import Tags
from frigate.config import FrigateConfig
from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
CameraConfigUpdateTopic,
)
from frigate.ffmpeg_presets import FFMPEG_HWACCEL_VAAPI, _gpu_selector
from frigate.genai import GenAIClientManager
from frigate.jobs.media_sync import (
get_current_media_sync_job,
get_media_sync_job_by_id,
start_media_sync_job,
)
from frigate.models import Event, Timeline
from frigate.stats.prometheus import get_metrics, update_metrics
from frigate.types import JobStatusTypesEnum
from frigate.util.builtin import (
clean_camera_user_pass,
flatten_config_data,
@@ -420,6 +433,7 @@ def config_set(request: Request, body: AppConfigSetBody):
if body.requires_restart == 0 or body.update_topic:
old_config: FrigateConfig = request.app.frigate_config
request.app.frigate_config = config
request.app.genai_manager = GenAIClientManager(config)
if body.update_topic:
if body.update_topic.startswith("config/cameras/"):
@@ -458,7 +472,15 @@ def config_set(request: Request, body: AppConfigSetBody):
@router.get("/vainfo", dependencies=[Depends(allow_any_authenticated())])
def vainfo():
vainfo = vainfo_hwaccel()
# Use LibvaGpuSelector to pick an appropriate libva device (if available)
selected_gpu = ""
try:
selected_gpu = _gpu_selector.get_gpu_arg(FFMPEG_HWACCEL_VAAPI, 0) or ""
except Exception:
selected_gpu = ""
# If selected_gpu is empty, pass None to vainfo_hwaccel to run plain `vainfo`.
vainfo = vainfo_hwaccel(device_name=selected_gpu or None)
return JSONResponse(
content={
"return_code": vainfo.returncode,
@@ -593,6 +615,98 @@ def restart():
)
@router.post(
"/media/sync",
dependencies=[Depends(require_role(["admin"]))],
summary="Start media sync job",
description="""Start an asynchronous media sync job to find and (optionally) remove orphaned media files.
Returns 202 with job details when queued, or 409 if a job is already running.""",
)
def sync_media(body: MediaSyncBody = Body(...)):
"""Start async media sync job - remove orphaned files.
Syncs specified media types: event snapshots, event thumbnails, review thumbnails,
previews, exports, and/or recordings. Job runs in background; use /media/sync/current
or /media/sync/status/{job_id} to check status.
Args:
body: MediaSyncBody with dry_run flag and media_types list.
media_types can include: 'all', 'event_snapshots', 'event_thumbnails',
'review_thumbnails', 'previews', 'exports', 'recordings'
Returns:
202 Accepted with job_id, or 409 Conflict if job already running.
"""
job_id = start_media_sync_job(
dry_run=body.dry_run, media_types=body.media_types, force=body.force
)
if job_id is None:
# A job is already running
current = get_current_media_sync_job()
return JSONResponse(
content={
"error": "A media sync job is already running",
"current_job_id": current.id if current else None,
},
status_code=409,
)
return JSONResponse(
content={
"job": {
"job_type": "media_sync",
"status": JobStatusTypesEnum.queued,
"id": job_id,
}
},
status_code=202,
)
@router.get(
"/media/sync/current",
dependencies=[Depends(require_role(["admin"]))],
summary="Get current media sync job",
description="""Retrieve the current running media sync job, if any. Returns the job details
or null when no job is active.""",
)
def get_media_sync_current():
"""Get the current running media sync job, if any."""
job = get_current_media_sync_job()
if job is None:
return JSONResponse(content={"job": None}, status_code=200)
return JSONResponse(
content={"job": job.to_dict()},
status_code=200,
)
@router.get(
"/media/sync/status/{job_id}",
dependencies=[Depends(require_role(["admin"]))],
summary="Get media sync job status",
description="""Get status and results for the specified media sync job id. Returns 200 with
job details including results, or 404 if the job is not found.""",
)
def get_media_sync_status(job_id: str):
"""Get the status of a specific media sync job."""
job = get_media_sync_job_by_id(job_id)
if job is None:
return JSONResponse(
content={"error": "Job not found"},
status_code=404,
)
return JSONResponse(
content={"job": job.to_dict()},
status_code=200,
)
@router.get("/labels", dependencies=[Depends(allow_any_authenticated())])
def get_labels(camera: str = ""):
try:
@@ -687,13 +801,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()]

View File

@@ -26,7 +26,7 @@ from frigate.api.defs.request.app_body import (
AppPutRoleBody,
)
from frigate.api.defs.tags import Tags
from frigate.config import AuthConfig, ProxyConfig
from frigate.config import AuthConfig, NetworkingConfig, ProxyConfig
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
from frigate.models import User
@@ -41,7 +41,7 @@ def require_admin_by_default():
endpoints require admin access unless explicitly overridden with
allow_public(), allow_any_authenticated(), or require_role().
Port 5000 (internal) always has admin role set by the /auth endpoint,
Internal port always has admin role set by the /auth endpoint,
so this check passes automatically for internal requests.
Certain paths are exempted from the global admin check because they must
@@ -130,7 +130,7 @@ def require_admin_by_default():
pass
# For all other paths, require admin role
# Port 5000 (internal) requests have admin role set automatically
# Internal port requests have admin role set automatically
role = request.headers.get("remote-role")
if role == "admin":
return
@@ -143,6 +143,17 @@ def require_admin_by_default():
return admin_checker
def _is_authenticated(request: Request) -> bool:
"""
Helper to determine if a request is from an authenticated user.
Returns True if the request has a valid authenticated user (not anonymous).
Internal port requests are considered anonymous despite having admin role.
"""
username = request.headers.get("remote-user")
return username is not None and username != "anonymous"
def allow_public():
"""
Override dependency to allow unauthenticated access to an endpoint.
@@ -171,6 +182,7 @@ def allow_any_authenticated():
Rejects:
- Requests with no remote-user header (did not pass through /auth endpoint)
- External port requests with anonymous user (auth disabled, no proxy auth)
Example:
@router.get("/authenticated-endpoint", dependencies=[Depends(allow_any_authenticated())])
@@ -179,8 +191,14 @@ def allow_any_authenticated():
async def auth_checker(request: Request):
# Ensure a remote-user has been set by the /auth endpoint
username = request.headers.get("remote-user")
if username is None:
raise HTTPException(status_code=401, detail="Authentication required")
# Internal port requests have admin role and should be allowed
role = request.headers.get("remote-role")
if role != "admin":
if username is None or not _is_authenticated(request):
raise HTTPException(status_code=401, detail="Authentication required")
return
return auth_checker
@@ -350,21 +368,15 @@ def validate_password_strength(password: str) -> tuple[bool, Optional[str]]:
Validate password strength.
Returns a tuple of (is_valid, error_message).
Longer passwords are harder to crack than shorter complex ones.
https://pages.nist.gov/800-63-3/sp800-63b.html
"""
if not password:
return False, "Password cannot be empty"
if len(password) < 8:
return False, "Password must be at least 8 characters long"
if not any(c.isupper() for c in password):
return False, "Password must contain at least one uppercase letter"
if not any(c.isdigit() for c in password):
return False, "Password must contain at least one digit"
if not any(c in '!@#$%^&*(),.?":{}|<>' for c in password):
return False, "Password must contain at least one special character"
if len(password) < 12:
return False, "Password must be at least 12 characters long"
return True, None
@@ -445,10 +457,11 @@ def resolve_role(
Determine the effective role for a request based on proxy headers and configuration.
Order of resolution:
1. If a role header is defined in proxy_config.header_map.role:
- If a role_map is configured, treat the header as group claims
(split by proxy_config.separator) and map to roles.
- If no role_map is configured, treat the header as role names directly.
1. If a role header is defined in proxy_config.header_map.role:
- If a role_map is configured, treat the header as group claims
(split by proxy_config.separator) and map to roles.
Admin matches short-circuit to admin.
- If no role_map is configured, treat the header as role names directly.
2. If no valid role is found, return proxy_config.default_role if it's valid in config_roles, else 'viewer'.
Args:
@@ -498,6 +511,12 @@ def resolve_role(
}
logger.debug("Matched roles from role_map: %s", matched_roles)
# If admin matches, prioritize it to avoid accidental downgrade when
# users belong to both admin and lower-privilege groups.
if "admin" in matched_roles and "admin" in config_roles:
logger.debug("Resolved role (with role_map) to 'admin'.")
return "admin"
if matched_roles:
resolved = next(
(r for r in config_roles if r in matched_roles), validated_default
@@ -569,12 +588,18 @@ def resolve_role(
def auth(request: Request):
auth_config: AuthConfig = request.app.frigate_config.auth
proxy_config: ProxyConfig = request.app.frigate_config.proxy
networking_config: NetworkingConfig = request.app.frigate_config.networking
success_response = Response("", status_code=202)
# handle case where internal port is a string with ip:port
internal_port = networking_config.listen.internal
if type(internal_port) is str:
internal_port = int(internal_port.split(":")[-1])
# dont require auth if the request is on the internal port
# this header is set by Frigate's nginx proxy, so it cant be spoofed
if int(request.headers.get("x-server-port", default=0)) == 5000:
if int(request.headers.get("x-server-port", default=0)) == internal_port:
success_response.headers["remote-user"] = "anonymous"
success_response.headers["remote-role"] = "admin"
return success_response
@@ -800,7 +825,7 @@ def get_users():
"/users",
dependencies=[Depends(require_role(["admin"]))],
summary="Create new user",
description='Creates a new user with the specified username, password, and role. Requires admin role. Password must meet strength requirements: minimum 8 characters, at least one uppercase letter, at least one digit, and at least one special character (!@#$%^&*(),.?":{} |<>).',
description="Creates a new user with the specified username, password, and role. Requires admin role. Password must be at least 12 characters long.",
)
def create_user(
request: Request,
@@ -817,6 +842,15 @@ def create_user(
content={"message": f"Role must be one of: {', '.join(config_roles)}"},
status_code=400,
)
# Validate password strength
is_valid, error_message = validate_password_strength(body.password)
if not is_valid:
return JSONResponse(
content={"message": error_message},
status_code=400,
)
role = body.role or "viewer"
password_hash = hash_password(body.password, iterations=HASH_ITERATIONS)
User.insert(
@@ -851,7 +885,7 @@ def delete_user(request: Request, username: str):
"/users/{username}/password",
dependencies=[Depends(allow_any_authenticated())],
summary="Update user password",
description="Updates a user's password. Users can only change their own password unless they have admin role. Requires the current password to verify identity for non-admin users. Password must meet strength requirements: minimum 8 characters, at least one uppercase letter, at least one digit, and at least one special character (!@#$%^&*(),.?\":{} |<>). If user changes their own password, a new JWT cookie is automatically issued.",
description="Updates a user's password. Users can only change their own password unless they have admin role. Requires the current password to verify identity for non-admin users. Password must be at least 12 characters long. If user changes their own password, a new JWT cookie is automatically issued.",
)
async def update_password(
request: Request,

View File

@@ -848,9 +848,10 @@ async def onvif_probe(
try:
if isinstance(uri, str) and uri.startswith("rtsp://"):
if username and password and "@" not in uri:
# Inject URL-encoded credentials and add only the
# authenticated version.
cred = f"{quote_plus(username)}:{quote_plus(password)}@"
# Inject raw credentials and add only the
# authenticated version. The credentials will be encoded
# later by ffprobe_stream or the config system.
cred = f"{username}:{password}@"
injected = uri.replace(
"rtsp://", f"rtsp://{cred}", 1
)
@@ -903,12 +904,8 @@ async def onvif_probe(
"/cam/realmonitor?channel=1&subtype=0",
"/11",
]
# Use URL-encoded credentials for pattern fallback URIs when provided
auth_str = (
f"{quote_plus(username)}:{quote_plus(password)}@"
if username and password
else ""
)
# Use raw credentials for pattern fallback URIs when provided
auth_str = f"{username}:{password}@" if username and password else ""
rtsp_port = 554
for path in common_paths:
uri = f"rtsp://{auth_str}{host}:{rtsp_port}{path}"
@@ -930,7 +927,7 @@ async def onvif_probe(
and uri.startswith("rtsp://")
and "@" not in uri
):
cred = f"{quote_plus(username)}:{quote_plus(password)}@"
cred = f"{username}:{password}@"
cred_uri = uri.replace("rtsp://", f"rtsp://{cred}", 1)
if cred_uri not in to_test:
to_test.append(cred_uri)

643
frigate/api/chat.py Normal file
View File

@@ -0,0 +1,643 @@
"""Chat and LLM tool calling APIs."""
import base64
import json
import logging
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
import cv2
from fastapi import APIRouter, Body, Depends, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from frigate.api.auth import (
allow_any_authenticated,
get_allowed_cameras_for_filter,
)
from frigate.api.defs.query.events_query_parameters import EventsQueryParams
from frigate.api.defs.request.chat_body import ChatCompletionRequest
from frigate.api.defs.response.chat_response import (
ChatCompletionResponse,
ChatMessageResponse,
)
from frigate.api.defs.tags import Tags
from frigate.api.event import events
logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.chat])
class ToolExecuteRequest(BaseModel):
"""Request model for tool execution."""
tool_name: str
arguments: Dict[str, Any]
def get_tool_definitions() -> List[Dict[str, Any]]:
"""
Get OpenAI-compatible tool definitions for Frigate.
Returns a list of tool definitions that can be used with OpenAI-compatible
function calling APIs.
"""
return [
{
"type": "function",
"function": {
"name": "search_objects",
"description": (
"Search for detected objects in Frigate by camera, object label, time range, "
"zones, and other filters. Use this to answer questions about when "
"objects were detected, what objects appeared, or to find specific object detections. "
"An 'object' in Frigate represents a tracked detection (e.g., a person, package, car)."
),
"parameters": {
"type": "object",
"properties": {
"camera": {
"type": "string",
"description": "Camera name to filter by (optional). Use 'all' for all cameras.",
},
"label": {
"type": "string",
"description": "Object label to filter by (e.g., 'person', 'package', 'car').",
},
"after": {
"type": "string",
"description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').",
},
"before": {
"type": "string",
"description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').",
},
"zones": {
"type": "array",
"items": {"type": "string"},
"description": "List of zone names to filter by.",
},
"limit": {
"type": "integer",
"description": "Maximum number of objects to return (default: 10).",
"default": 10,
},
},
},
"required": [],
},
},
{
"type": "function",
"function": {
"name": "get_live_context",
"description": (
"Get the current detection information for a camera: objects being tracked, "
"zones, timestamps. Use this to understand what is visible in the live view. "
"Call this when the user has included a live image (via include_live_image) or "
"when answering questions about what is happening right now on a specific camera."
),
"parameters": {
"type": "object",
"properties": {
"camera": {
"type": "string",
"description": "Camera name to get live context for.",
},
},
"required": ["camera"],
},
},
},
]
@router.get(
"/chat/tools",
dependencies=[Depends(allow_any_authenticated())],
summary="Get available tools",
description="Returns OpenAI-compatible tool definitions for function calling.",
)
def get_tools(request: Request) -> JSONResponse:
"""Get list of available tools for LLM function calling."""
tools = get_tool_definitions()
return JSONResponse(content={"tools": tools})
async def _execute_search_objects(
request: Request,
arguments: Dict[str, Any],
allowed_cameras: List[str],
) -> JSONResponse:
"""
Execute the search_objects tool.
This searches for detected objects (events) in Frigate using the same
logic as the events API endpoint.
"""
# Parse ISO 8601 timestamps to Unix timestamps if provided
after = arguments.get("after")
before = arguments.get("before")
if after:
try:
after_dt = datetime.fromisoformat(after.replace("Z", "+00:00"))
after = after_dt.timestamp()
except (ValueError, AttributeError):
logger.warning(f"Invalid 'after' timestamp format: {after}")
after = None
if before:
try:
before_dt = datetime.fromisoformat(before.replace("Z", "+00:00"))
before = before_dt.timestamp()
except (ValueError, AttributeError):
logger.warning(f"Invalid 'before' timestamp format: {before}")
before = None
# Convert zones array to comma-separated string if provided
zones = arguments.get("zones")
if isinstance(zones, list):
zones = ",".join(zones)
elif zones is None:
zones = "all"
# Build query parameters compatible with EventsQueryParams
query_params = EventsQueryParams(
camera=arguments.get("camera", "all"),
cameras=arguments.get("camera", "all"),
label=arguments.get("label", "all"),
labels=arguments.get("label", "all"),
zones=zones,
zone=zones,
after=after,
before=before,
limit=arguments.get("limit", 10),
)
try:
# Call the events endpoint function directly
# The events function is synchronous and takes params and allowed_cameras
response = events(query_params, allowed_cameras)
# The response is already a JSONResponse with event data
# Return it as-is for the LLM
return response
except Exception as e:
logger.error(f"Error executing search_objects: {e}", exc_info=True)
return JSONResponse(
content={
"success": False,
"message": f"Error searching objects: {str(e)}",
},
status_code=500,
)
@router.post(
"/chat/execute",
dependencies=[Depends(allow_any_authenticated())],
summary="Execute a tool",
description="Execute a tool function call from an LLM.",
)
async def execute_tool(
request: Request,
body: ToolExecuteRequest = Body(...),
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
) -> JSONResponse:
"""
Execute a tool function call.
This endpoint receives tool calls from LLMs and executes the corresponding
Frigate operations, returning results in a format the LLM can understand.
"""
tool_name = body.tool_name
arguments = body.arguments
logger.debug(f"Executing tool: {tool_name} with arguments: {arguments}")
if tool_name == "search_objects":
return await _execute_search_objects(request, arguments, allowed_cameras)
return JSONResponse(
content={
"success": False,
"message": f"Unknown tool: {tool_name}",
"tool": tool_name,
},
status_code=400,
)
async def _execute_get_live_context(
request: Request,
camera: str,
allowed_cameras: List[str],
) -> Dict[str, Any]:
if camera not in allowed_cameras:
return {
"error": f"Camera '{camera}' not found or access denied",
}
if camera not in request.app.frigate_config.cameras:
return {
"error": f"Camera '{camera}' not found",
}
try:
frame_processor = request.app.detected_frames_processor
camera_state = frame_processor.camera_states.get(camera)
if camera_state is None:
return {
"error": f"Camera '{camera}' state not available",
}
tracked_objects_dict = {}
with camera_state.current_frame_lock:
tracked_objects = camera_state.tracked_objects.copy()
frame_time = camera_state.current_frame_time
for obj_id, tracked_obj in tracked_objects.items():
obj_dict = tracked_obj.to_dict()
if obj_dict.get("frame_time") == frame_time:
tracked_objects_dict[obj_id] = {
"label": obj_dict.get("label"),
"zones": obj_dict.get("current_zones", []),
"sub_label": obj_dict.get("sub_label"),
"stationary": obj_dict.get("stationary", False),
}
return {
"camera": camera,
"timestamp": frame_time,
"detections": list(tracked_objects_dict.values()),
}
except Exception as e:
logger.error(f"Error executing get_live_context: {e}", exc_info=True)
return {
"error": f"Error getting live context: {str(e)}",
}
async def _get_live_frame_image_url(
request: Request,
camera: str,
allowed_cameras: List[str],
) -> Optional[str]:
"""
Fetch the current live frame for a camera as a base64 data URL.
Returns None if the frame cannot be retrieved. Used when include_live_image
is set to attach the image to the first user message.
"""
if (
camera not in allowed_cameras
or camera not in request.app.frigate_config.cameras
):
return None
try:
frame_processor = request.app.detected_frames_processor
if camera not in frame_processor.camera_states:
return None
frame = frame_processor.get_current_frame(camera, {})
if frame is None:
return None
height, width = frame.shape[:2]
max_dimension = 1024
if height > max_dimension or width > max_dimension:
scale = max_dimension / max(height, width)
frame = cv2.resize(
frame,
(int(width * scale), int(height * scale)),
interpolation=cv2.INTER_AREA,
)
_, img_encoded = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
b64 = base64.b64encode(img_encoded.tobytes()).decode("utf-8")
return f"data:image/jpeg;base64,{b64}"
except Exception as e:
logger.debug("Failed to get live frame for %s: %s", camera, e)
return None
async def _execute_tool_internal(
tool_name: str,
arguments: Dict[str, Any],
request: Request,
allowed_cameras: List[str],
) -> Dict[str, Any]:
"""
Internal helper to execute a tool and return the result as a dict.
This is used by the chat completion endpoint to execute tools.
"""
if tool_name == "search_objects":
response = await _execute_search_objects(request, arguments, allowed_cameras)
try:
if hasattr(response, "body"):
body_str = response.body.decode("utf-8")
return json.loads(body_str)
elif hasattr(response, "content"):
return response.content
else:
return {}
except (json.JSONDecodeError, AttributeError) as e:
logger.warning(f"Failed to extract tool result: {e}")
return {"error": "Failed to parse tool result"}
elif tool_name == "get_live_context":
camera = arguments.get("camera")
if not camera:
return {"error": "Camera parameter is required"}
return await _execute_get_live_context(request, camera, allowed_cameras)
else:
return {"error": f"Unknown tool: {tool_name}"}
@router.post(
"/chat/completion",
response_model=ChatCompletionResponse,
dependencies=[Depends(allow_any_authenticated())],
summary="Chat completion with tool calling",
description=(
"Send a chat message to the configured GenAI provider with tool calling support. "
"The LLM can call Frigate tools to answer questions about your cameras and events."
),
)
async def chat_completion(
request: Request,
body: ChatCompletionRequest = Body(...),
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
) -> JSONResponse:
"""
Chat completion endpoint with tool calling support.
This endpoint:
1. Gets the configured GenAI client
2. Gets tool definitions
3. Sends messages + tools to LLM
4. Handles tool_calls if present
5. Executes tools and sends results back to LLM
6. Repeats until final answer
7. Returns response to user
"""
genai_client = request.app.genai_manager.tool_client
if not genai_client:
return JSONResponse(
content={
"error": "GenAI is not configured. Please configure a GenAI provider in your Frigate config.",
},
status_code=400,
)
tools = get_tool_definitions()
conversation = []
current_datetime = datetime.now(timezone.utc)
current_date_str = current_datetime.strftime("%Y-%m-%d")
current_time_str = current_datetime.strftime("%H:%M:%S %Z")
cameras_info = []
config = request.app.frigate_config
for camera_id in allowed_cameras:
if camera_id not in config.cameras:
continue
camera_config = config.cameras[camera_id]
friendly_name = (
camera_config.friendly_name
if camera_config.friendly_name
else camera_id.replace("_", " ").title()
)
cameras_info.append(f" - {friendly_name} (ID: {camera_id})")
cameras_section = ""
if cameras_info:
cameras_section = (
"\n\nAvailable cameras:\n"
+ "\n".join(cameras_info)
+ "\n\nWhen users refer to cameras by their friendly name (e.g., 'Back Deck Camera'), use the corresponding camera ID (e.g., 'back_deck_cam') in tool calls."
)
live_image_note = ""
if body.include_live_image:
live_image_note = (
f"\n\nThe first user message includes a live image from camera "
f"'{body.include_live_image}'. Use get_live_context for that camera to get "
"current detection details (objects, zones) to aid in understanding the image."
)
system_prompt = f"""You are a helpful assistant for Frigate, a security camera NVR system. You help users answer questions about their cameras, detected objects, and events.
Current date and time: {current_date_str} at {current_time_str} (UTC)
When users ask questions about "today", "yesterday", "this week", etc., use the current date above as reference.
When searching for objects or events, use ISO 8601 format for dates (e.g., {current_date_str}T00:00:00Z for the start of today).
Always be accurate with time calculations based on the current date provided.{cameras_section}{live_image_note}"""
conversation.append(
{
"role": "system",
"content": system_prompt,
}
)
first_user_message_seen = False
for msg in body.messages:
msg_dict = {
"role": msg.role,
"content": msg.content,
}
if msg.tool_call_id:
msg_dict["tool_call_id"] = msg.tool_call_id
if msg.name:
msg_dict["name"] = msg.name
if (
msg.role == "user"
and not first_user_message_seen
and body.include_live_image
):
first_user_message_seen = True
image_url = await _get_live_frame_image_url(
request, body.include_live_image, allowed_cameras
)
if image_url:
msg_dict["content"] = [
{"type": "text", "text": msg.content},
{"type": "image_url", "image_url": {"url": image_url}},
]
conversation.append(msg_dict)
tool_iterations = 0
max_iterations = body.max_tool_iterations
logger.debug(
f"Starting chat completion with {len(conversation)} message(s), "
f"{len(tools)} tool(s) available, max_iterations={max_iterations}"
)
try:
while tool_iterations < max_iterations:
logger.debug(
f"Calling LLM (iteration {tool_iterations + 1}/{max_iterations}) "
f"with {len(conversation)} message(s) in conversation"
)
response = genai_client.chat_with_tools(
messages=conversation,
tools=tools if tools else None,
tool_choice="auto",
)
if response.get("finish_reason") == "error":
logger.error("GenAI client returned an error")
return JSONResponse(
content={
"error": "An error occurred while processing your request.",
},
status_code=500,
)
assistant_message = {
"role": "assistant",
"content": response.get("content"),
}
if response.get("tool_calls"):
assistant_message["tool_calls"] = [
{
"id": tc["id"],
"type": "function",
"function": {
"name": tc["name"],
"arguments": json.dumps(tc["arguments"]),
},
}
for tc in response["tool_calls"]
]
conversation.append(assistant_message)
tool_calls = response.get("tool_calls")
if not tool_calls:
logger.debug(
f"Chat completion finished with final answer (iterations: {tool_iterations})"
)
return JSONResponse(
content=ChatCompletionResponse(
message=ChatMessageResponse(
role="assistant",
content=response.get("content"),
tool_calls=None,
),
finish_reason=response.get("finish_reason", "stop"),
tool_iterations=tool_iterations,
).model_dump(),
)
# Execute tools
tool_iterations += 1
logger.debug(
f"Tool calls detected (iteration {tool_iterations}/{max_iterations}): "
f"{len(tool_calls)} tool(s) to execute"
)
tool_results = []
for tool_call in tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["arguments"]
tool_call_id = tool_call["id"]
logger.debug(
f"Executing tool: {tool_name} (id: {tool_call_id}) with arguments: {json.dumps(tool_args, indent=2)}"
)
try:
tool_result = await _execute_tool_internal(
tool_name, tool_args, request, allowed_cameras
)
if isinstance(tool_result, dict):
result_content = json.dumps(tool_result)
result_summary = tool_result
if isinstance(tool_result, dict) and isinstance(
tool_result.get("content"), list
):
result_count = len(tool_result.get("content", []))
result_summary = {
"count": result_count,
"sample": tool_result.get("content", [])[:2]
if result_count > 0
else [],
}
logger.debug(
f"Tool {tool_name} (id: {tool_call_id}) completed successfully. "
f"Result: {json.dumps(result_summary, indent=2)}"
)
elif isinstance(tool_result, str):
result_content = tool_result
logger.debug(
f"Tool {tool_name} (id: {tool_call_id}) completed successfully. "
f"Result length: {len(result_content)} characters"
)
else:
result_content = str(tool_result)
logger.debug(
f"Tool {tool_name} (id: {tool_call_id}) completed successfully. "
f"Result type: {type(tool_result).__name__}"
)
tool_results.append(
{
"role": "tool",
"tool_call_id": tool_call_id,
"content": result_content,
}
)
except Exception as e:
logger.error(
f"Error executing tool {tool_name} (id: {tool_call_id}): {e}",
exc_info=True,
)
error_content = json.dumps(
{"error": f"Tool execution failed: {str(e)}"}
)
tool_results.append(
{
"role": "tool",
"tool_call_id": tool_call_id,
"content": error_content,
}
)
logger.debug(
f"Tool {tool_name} (id: {tool_call_id}) failed. Error result added to conversation."
)
conversation.extend(tool_results)
logger.debug(
f"Added {len(tool_results)} tool result(s) to conversation. "
f"Continuing with next LLM call..."
)
logger.warning(
f"Max tool iterations ({max_iterations}) reached. Returning partial response."
)
return JSONResponse(
content=ChatCompletionResponse(
message=ChatMessageResponse(
role="assistant",
content="I reached the maximum number of tool call iterations. Please try rephrasing your question.",
tool_calls=None,
),
finish_reason="length",
tool_iterations=tool_iterations,
).model_dump(),
)
except Exception as e:
logger.error(f"Error in chat completion: {e}", exc_info=True)
return JSONResponse(
content={
"error": "An error occurred while processing your request.",
},
status_code=500,
)

View File

@@ -73,7 +73,7 @@ def get_faces():
face_dict[name] = []
for file in filter(
lambda f: (f.lower().endswith((".webp", ".png", ".jpg", ".jpeg"))),
lambda f: f.lower().endswith((".webp", ".png", ".jpg", ".jpeg")),
os.listdir(face_dir),
):
face_dict[name].append(file)
@@ -582,7 +582,7 @@ def get_classification_dataset(name: str):
dataset_dict[category_name] = []
for file in filter(
lambda f: (f.lower().endswith((".webp", ".png", ".jpg", ".jpeg"))),
lambda f: f.lower().endswith((".webp", ".png", ".jpg", ".jpeg")),
os.listdir(category_dir),
):
dataset_dict[category_name].append(file)
@@ -693,7 +693,7 @@ def get_classification_images(name: str):
status_code=200,
content=list(
filter(
lambda f: (f.lower().endswith((".webp", ".png", ".jpg", ".jpeg"))),
lambda f: f.lower().endswith((".webp", ".png", ".jpg", ".jpeg")),
os.listdir(train_dir),
)
),
@@ -759,15 +759,28 @@ def delete_classification_dataset_images(
CLIPS_DIR, sanitize_filename(name), "dataset", sanitize_filename(category)
)
deleted_count = 0
for id in list_of_ids:
file_path = os.path.join(folder, sanitize_filename(id))
if os.path.isfile(file_path):
os.unlink(file_path)
deleted_count += 1
if os.path.exists(folder) and not os.listdir(folder) and category.lower() != "none":
os.rmdir(folder)
# Update training metadata to reflect deleted images
# This ensures the dataset is marked as changed after deletion
# (even if the total count happens to be the same after adding and deleting)
if deleted_count > 0:
sanitized_name = sanitize_filename(name)
metadata = read_training_metadata(sanitized_name)
if metadata:
last_count = metadata.get("last_training_image_count", 0)
updated_count = max(0, last_count - deleted_count)
write_training_metadata(sanitized_name, updated_count)
return JSONResponse(
content=({"success": True, "message": "Successfully deleted images."}),
status_code=200,

View File

@@ -1,8 +1,7 @@
from enum import Enum
from typing import Optional, Union
from typing import Optional
from pydantic import BaseModel
from pydantic.json_schema import SkipJsonSchema
class Extension(str, Enum):
@@ -48,15 +47,3 @@ class MediaMjpegFeedQueryParams(BaseModel):
mask: Optional[int] = None
motion: Optional[int] = None
regions: Optional[int] = None
class MediaRecordingsSummaryQueryParams(BaseModel):
timezone: str = "utc"
cameras: Optional[str] = "all"
class MediaRecordingsAvailabilityQueryParams(BaseModel):
cameras: str = "all"
before: Union[float, SkipJsonSchema[None]] = None
after: Union[float, SkipJsonSchema[None]] = None
scale: int = 30

View File

@@ -0,0 +1,21 @@
from typing import Optional, Union
from pydantic import BaseModel
from pydantic.json_schema import SkipJsonSchema
class MediaRecordingsSummaryQueryParams(BaseModel):
timezone: str = "utc"
cameras: Optional[str] = "all"
class MediaRecordingsAvailabilityQueryParams(BaseModel):
cameras: str = "all"
before: Union[float, SkipJsonSchema[None]] = None
after: Union[float, SkipJsonSchema[None]] = None
scale: int = 30
class RecordingsDeleteQueryParams(BaseModel):
keep: Optional[str] = None
cameras: Optional[str] = "all"

View File

@@ -10,7 +10,7 @@ class ReviewQueryParams(BaseModel):
cameras: str = "all"
labels: str = "all"
zones: str = "all"
reviewed: int = 0
reviewed: Union[int, SkipJsonSchema[None]] = None
limit: Union[int, SkipJsonSchema[None]] = None
severity: Union[SeverityEnum, SkipJsonSchema[None]] = None
before: Union[float, SkipJsonSchema[None]] = None

View File

@@ -1,6 +1,6 @@
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional
from pydantic import BaseModel
from pydantic import BaseModel, Field
class AppConfigSetBody(BaseModel):
@@ -27,3 +27,16 @@ class AppPostLoginBody(BaseModel):
class AppPutRoleBody(BaseModel):
role: str
class MediaSyncBody(BaseModel):
dry_run: bool = Field(
default=True, description="If True, only report orphans without deleting them"
)
media_types: List[str] = Field(
default=["all"],
description="Types of media to sync: 'all', 'event_snapshots', 'event_thumbnails', 'review_thumbnails', 'previews', 'exports', 'recordings'",
)
force: bool = Field(
default=False, description="If True, bypass safety threshold checks"
)

View File

@@ -0,0 +1,41 @@
"""Chat API request models."""
from typing import Optional
from pydantic import BaseModel, Field
class ChatMessage(BaseModel):
"""A single message in a chat conversation."""
role: str = Field(
description="Message role: 'user', 'assistant', 'system', or 'tool'"
)
content: str = Field(description="Message content")
tool_call_id: Optional[str] = Field(
default=None, description="For tool messages, the ID of the tool call"
)
name: Optional[str] = Field(
default=None, description="For tool messages, the tool name"
)
class ChatCompletionRequest(BaseModel):
"""Request for chat completion with tool calling."""
messages: list[ChatMessage] = Field(
description="List of messages in the conversation"
)
max_tool_iterations: int = Field(
default=5,
ge=1,
le=10,
description="Maximum number of tool call iterations (default: 5)",
)
include_live_image: Optional[str] = Field(
default=None,
description=(
"If set, the current live frame from this camera is attached to the first "
"user message as multimodal content. Use with get_live_context for detection info."
),
)

View File

@@ -41,6 +41,7 @@ class EventsCreateBody(BaseModel):
duration: Optional[int] = 30
include_recording: Optional[bool] = True
draw: Optional[dict] = {}
pre_capture: Optional[int] = None
class EventsEndBody(BaseModel):

View File

@@ -0,0 +1,35 @@
from typing import Optional
from pydantic import BaseModel, Field
class ExportCaseCreateBody(BaseModel):
"""Request body for creating a new export case."""
name: str = Field(max_length=100, description="Friendly name of the export case")
description: Optional[str] = Field(
default=None, description="Optional description of the export case"
)
class ExportCaseUpdateBody(BaseModel):
"""Request body for updating an existing export case."""
name: Optional[str] = Field(
default=None,
max_length=100,
description="Updated friendly name of the export case",
)
description: Optional[str] = Field(
default=None, description="Updated description of the export case"
)
class ExportCaseAssignBody(BaseModel):
"""Request body for assigning or unassigning an export to a case."""
export_case_id: Optional[str] = Field(
default=None,
max_length=30,
description="Case ID to assign to the export, or null to unassign",
)

View File

@@ -3,18 +3,47 @@ from typing import Optional, Union
from pydantic import BaseModel, Field
from pydantic.json_schema import SkipJsonSchema
from frigate.record.export import (
PlaybackFactorEnum,
PlaybackSourceEnum,
)
from frigate.record.export import PlaybackSourceEnum
class ExportRecordingsBody(BaseModel):
playback: PlaybackFactorEnum = Field(
default=PlaybackFactorEnum.realtime, title="Playback factor"
)
source: PlaybackSourceEnum = Field(
default=PlaybackSourceEnum.recordings, title="Playback source"
)
name: Optional[str] = Field(title="Friendly name", default=None, max_length=256)
image_path: Union[str, SkipJsonSchema[None]] = None
export_case_id: Optional[str] = Field(
default=None,
title="Export case ID",
max_length=30,
description="ID of the export case to assign this export to",
)
class ExportRecordingsCustomBody(BaseModel):
source: PlaybackSourceEnum = Field(
default=PlaybackSourceEnum.recordings, title="Playback source"
)
name: str = Field(title="Friendly name", default=None, max_length=256)
image_path: Union[str, SkipJsonSchema[None]] = None
export_case_id: Optional[str] = Field(
default=None,
title="Export case ID",
max_length=30,
description="ID of the export case to assign this export to",
)
ffmpeg_input_args: Optional[str] = Field(
default=None,
title="FFmpeg input arguments",
description="Custom FFmpeg input arguments. If not provided, defaults to timelapse input args.",
)
ffmpeg_output_args: Optional[str] = Field(
default=None,
title="FFmpeg output arguments",
description="Custom FFmpeg output arguments. If not provided, defaults to timelapse output args.",
)
cpu_fallback: bool = Field(
default=False,
title="CPU Fallback",
description="If true, retry export without hardware acceleration if the initial export fails.",
)

View File

@@ -0,0 +1,37 @@
"""Chat API response models."""
from typing import Any, Optional
from pydantic import BaseModel, Field
class ToolCall(BaseModel):
"""A tool call from the LLM."""
id: str = Field(description="Unique identifier for this tool call")
name: str = Field(description="Tool name to call")
arguments: dict[str, Any] = Field(description="Arguments for the tool call")
class ChatMessageResponse(BaseModel):
"""A message in the chat response."""
role: str = Field(description="Message role")
content: Optional[str] = Field(
default=None, description="Message content (None if tool calls present)"
)
tool_calls: Optional[list[ToolCall]] = Field(
default=None, description="Tool calls if LLM wants to call tools"
)
class ChatCompletionResponse(BaseModel):
"""Response from chat completion."""
message: ChatMessageResponse = Field(description="The assistant's message")
finish_reason: str = Field(
description="Reason generation stopped: 'stop', 'tool_calls', 'length', 'error'"
)
tool_iterations: int = Field(
default=0, description="Number of tool call iterations performed"
)

View File

@@ -0,0 +1,22 @@
from typing import List, Optional
from pydantic import BaseModel, Field
class ExportCaseModel(BaseModel):
"""Model representing a single export case."""
id: str = Field(description="Unique identifier for the export case")
name: str = Field(description="Friendly name of the export case")
description: Optional[str] = Field(
default=None, description="Optional description of the export case"
)
created_at: float = Field(
description="Unix timestamp when the export case was created"
)
updated_at: float = Field(
description="Unix timestamp when the export case was last updated"
)
ExportCasesResponse = List[ExportCaseModel]

View File

@@ -15,6 +15,9 @@ class ExportModel(BaseModel):
in_progress: bool = Field(
description="Whether the export is currently being processed"
)
export_case_id: Optional[str] = Field(
default=None, description="ID of the export case this export belongs to"
)
class StartExportResponse(BaseModel):

View File

@@ -3,13 +3,15 @@ from enum import Enum
class Tags(Enum):
app = "App"
auth = "Auth"
camera = "Camera"
preview = "Preview"
chat = "Chat"
events = "Events"
export = "Export"
classification = "Classification"
logs = "Logs"
media = "Media"
notifications = "Notifications"
preview = "Preview"
recordings = "Recordings"
review = "Review"
export = "Export"
events = "Events"
classification = "Classification"
auth = "Auth"

View File

@@ -69,6 +69,25 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.events])
def _build_attribute_filter_clause(attributes: str):
filtered_attributes = [
attr.strip() for attr in attributes.split(",") if attr.strip()
]
attribute_clauses = []
for attr in filtered_attributes:
attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*')
escaped_attr = json.dumps(attr, ensure_ascii=True)[1:-1]
if escaped_attr != attr:
attribute_clauses.append(Event.data.cast("text") % f'*:"{escaped_attr}"*')
if not attribute_clauses:
return None
return reduce(operator.or_, attribute_clauses)
@router.get(
"/events",
response_model=list[EventResponse],
@@ -193,14 +212,9 @@ def events(
if attributes != "all":
# Custom classification results are stored as data[model_name] = result_value
filtered_attributes = attributes.split(",")
attribute_clauses = []
for attr in filtered_attributes:
attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*')
attribute_clause = reduce(operator.or_, attribute_clauses)
clauses.append(attribute_clause)
attribute_clause = _build_attribute_filter_clause(attributes)
if attribute_clause is not None:
clauses.append(attribute_clause)
if recognized_license_plate != "all":
filtered_recognized_license_plates = recognized_license_plate.split(",")
@@ -508,7 +522,7 @@ def events_search(
cameras = params.cameras
labels = params.labels
sub_labels = params.sub_labels
attributes = params.attributes
attributes = unquote(params.attributes)
zones = params.zones
after = params.after
before = params.before
@@ -607,13 +621,9 @@ def events_search(
if attributes != "all":
# Custom classification results are stored as data[model_name] = result_value
filtered_attributes = attributes.split(",")
attribute_clauses = []
for attr in filtered_attributes:
attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*')
event_filters.append(reduce(operator.or_, attribute_clauses))
attribute_clause = _build_attribute_filter_clause(attributes)
if attribute_clause is not None:
event_filters.append(attribute_clause)
if zones != "all":
zone_clauses = []
@@ -1772,6 +1782,7 @@ def create_event(
body.duration,
"api",
body.draw,
body.pre_capture,
),
EventMetadataTypeEnum.manual_event_create.value,
)

View File

@@ -4,10 +4,10 @@ import logging
import random
import string
from pathlib import Path
from typing import List
from typing import List, Optional
import psutil
from fastapi import APIRouter, Depends, Request
from fastapi import APIRouter, Depends, Query, Request
from fastapi.responses import JSONResponse
from pathvalidate import sanitize_filepath
from peewee import DoesNotExist
@@ -19,8 +19,20 @@ from frigate.api.auth import (
require_camera_access,
require_role,
)
from frigate.api.defs.request.export_recordings_body import ExportRecordingsBody
from frigate.api.defs.request.export_case_body import (
ExportCaseAssignBody,
ExportCaseCreateBody,
ExportCaseUpdateBody,
)
from frigate.api.defs.request.export_recordings_body import (
ExportRecordingsBody,
ExportRecordingsCustomBody,
)
from frigate.api.defs.request.export_rename_body import ExportRenameBody
from frigate.api.defs.response.export_case_response import (
ExportCaseModel,
ExportCasesResponse,
)
from frigate.api.defs.response.export_response import (
ExportModel,
ExportsResponse,
@@ -29,9 +41,9 @@ from frigate.api.defs.response.export_response import (
from frigate.api.defs.response.generic_response import GenericResponse
from frigate.api.defs.tags import Tags
from frigate.const import CLIPS_DIR, EXPORT_DIR
from frigate.models import Export, Previews, Recordings
from frigate.models import Export, ExportCase, Previews, Recordings
from frigate.record.export import (
PlaybackFactorEnum,
DEFAULT_TIME_LAPSE_FFMPEG_ARGS,
PlaybackSourceEnum,
RecordingExporter,
)
@@ -52,17 +64,182 @@ router = APIRouter(tags=[Tags.export])
)
def get_exports(
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
export_case_id: Optional[str] = None,
cameras: Optional[str] = Query(default="all"),
start_date: Optional[float] = None,
end_date: Optional[float] = None,
):
exports = (
Export.select()
.where(Export.camera << allowed_cameras)
.order_by(Export.date.desc())
.dicts()
.iterator()
)
query = Export.select().where(Export.camera << allowed_cameras)
if export_case_id is not None:
if export_case_id == "unassigned":
query = query.where(Export.export_case.is_null(True))
else:
query = query.where(Export.export_case == export_case_id)
if cameras and cameras != "all":
requested = set(cameras.split(","))
filtered_cameras = list(requested.intersection(allowed_cameras))
if not filtered_cameras:
return JSONResponse(content=[])
query = query.where(Export.camera << filtered_cameras)
if start_date is not None:
query = query.where(Export.date >= start_date)
if end_date is not None:
query = query.where(Export.date <= end_date)
exports = query.order_by(Export.date.desc()).dicts().iterator()
return JSONResponse(content=[e for e in exports])
@router.get(
"/cases",
response_model=ExportCasesResponse,
dependencies=[Depends(allow_any_authenticated())],
summary="Get export cases",
description="Gets all export cases from the database.",
)
def get_export_cases():
cases = (
ExportCase.select().order_by(ExportCase.created_at.desc()).dicts().iterator()
)
return JSONResponse(content=[c for c in cases])
@router.post(
"/cases",
response_model=ExportCaseModel,
dependencies=[Depends(require_role(["admin"]))],
summary="Create export case",
description="Creates a new export case.",
)
def create_export_case(body: ExportCaseCreateBody):
case = ExportCase.create(
id="".join(random.choices(string.ascii_lowercase + string.digits, k=12)),
name=body.name,
description=body.description,
created_at=Path().stat().st_mtime,
updated_at=Path().stat().st_mtime,
)
return JSONResponse(content=model_to_dict(case))
@router.get(
"/cases/{case_id}",
response_model=ExportCaseModel,
dependencies=[Depends(allow_any_authenticated())],
summary="Get a single export case",
description="Gets a specific export case by ID.",
)
def get_export_case(case_id: str):
try:
case = ExportCase.get(ExportCase.id == case_id)
return JSONResponse(content=model_to_dict(case))
except DoesNotExist:
return JSONResponse(
content={"success": False, "message": "Export case not found"},
status_code=404,
)
@router.patch(
"/cases/{case_id}",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Update export case",
description="Updates an existing export case.",
)
def update_export_case(case_id: str, body: ExportCaseUpdateBody):
try:
case = ExportCase.get(ExportCase.id == case_id)
except DoesNotExist:
return JSONResponse(
content={"success": False, "message": "Export case not found"},
status_code=404,
)
if body.name is not None:
case.name = body.name
if body.description is not None:
case.description = body.description
case.save()
return JSONResponse(
content={"success": True, "message": "Successfully updated export case."}
)
@router.delete(
"/cases/{case_id}",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Delete export case",
description="""Deletes an export case.\n Exports that reference this case will have their export_case set to null.\n """,
)
def delete_export_case(case_id: str):
try:
case = ExportCase.get(ExportCase.id == case_id)
except DoesNotExist:
return JSONResponse(
content={"success": False, "message": "Export case not found"},
status_code=404,
)
# Unassign exports from this case but keep the exports themselves
Export.update(export_case=None).where(Export.export_case == case).execute()
case.delete_instance()
return JSONResponse(
content={"success": True, "message": "Successfully deleted export case."}
)
@router.patch(
"/export/{export_id}/case",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Assign export to case",
description=(
"Assigns an export to a case, or unassigns it if export_case_id is null."
),
)
async def assign_export_case(
export_id: str,
body: ExportCaseAssignBody,
request: Request,
):
try:
export: Export = Export.get(Export.id == export_id)
await require_camera_access(export.camera, request=request)
except DoesNotExist:
return JSONResponse(
content={"success": False, "message": "Export not found."},
status_code=404,
)
if body.export_case_id is not None:
try:
ExportCase.get(ExportCase.id == body.export_case_id)
except DoesNotExist:
return JSONResponse(
content={"success": False, "message": "Export case not found."},
status_code=404,
)
export.export_case = body.export_case_id
else:
export.export_case = None
export.save()
return JSONResponse(
content={"success": True, "message": "Successfully updated export case."}
)
@router.post(
"/export/{camera_name}/start/{start_time}/end/{end_time}",
response_model=StartExportResponse,
@@ -88,11 +265,20 @@ def export_recording(
status_code=404,
)
playback_factor = body.playback
playback_source = body.source
friendly_name = body.name
existing_image = sanitize_filepath(body.image_path) if body.image_path else None
export_case_id = body.export_case_id
if export_case_id is not None:
try:
ExportCase.get(ExportCase.id == export_case_id)
except DoesNotExist:
return JSONResponse(
content={"success": False, "message": "Export case not found"},
status_code=404,
)
# Ensure that existing_image is a valid path
if existing_image and not existing_image.startswith(CLIPS_DIR):
return JSONResponse(
@@ -151,16 +337,12 @@ def export_recording(
existing_image,
int(start_time),
int(end_time),
(
PlaybackFactorEnum[playback_factor]
if playback_factor in PlaybackFactorEnum.__members__.values()
else PlaybackFactorEnum.realtime
),
(
PlaybackSourceEnum[playback_source]
if playback_source in PlaybackSourceEnum.__members__.values()
else PlaybackSourceEnum.recordings
),
export_case_id,
)
exporter.start()
return JSONResponse(
@@ -271,6 +453,138 @@ async def export_delete(event_id: str, request: Request):
)
@router.post(
"/export/custom/{camera_name}/start/{start_time}/end/{end_time}",
response_model=StartExportResponse,
dependencies=[Depends(require_camera_access)],
summary="Start custom recording export",
description="""Starts an export of a recording for the specified time range using custom FFmpeg arguments.
The export can be from recordings or preview footage. Returns the export ID if
successful, or an error message if the camera is invalid or no recordings/previews
are found for the time range. If ffmpeg_input_args and ffmpeg_output_args are not provided,
defaults to timelapse export settings.""",
)
def export_recording_custom(
request: Request,
camera_name: str,
start_time: float,
end_time: float,
body: ExportRecordingsCustomBody,
):
if not camera_name or not request.app.frigate_config.cameras.get(camera_name):
return JSONResponse(
content=(
{"success": False, "message": f"{camera_name} is not a valid camera."}
),
status_code=404,
)
playback_source = body.source
friendly_name = body.name
existing_image = sanitize_filepath(body.image_path) if body.image_path else None
ffmpeg_input_args = body.ffmpeg_input_args
ffmpeg_output_args = body.ffmpeg_output_args
cpu_fallback = body.cpu_fallback
export_case_id = body.export_case_id
if export_case_id is not None:
try:
ExportCase.get(ExportCase.id == export_case_id)
except DoesNotExist:
return JSONResponse(
content={"success": False, "message": "Export case not found"},
status_code=404,
)
# Ensure that existing_image is a valid path
if existing_image and not existing_image.startswith(CLIPS_DIR):
return JSONResponse(
content=({"success": False, "message": "Invalid image path"}),
status_code=400,
)
if playback_source == "recordings":
recordings_count = (
Recordings.select()
.where(
Recordings.start_time.between(start_time, end_time)
| Recordings.end_time.between(start_time, end_time)
| (
(start_time > Recordings.start_time)
& (end_time < Recordings.end_time)
)
)
.where(Recordings.camera == camera_name)
.count()
)
if recordings_count <= 0:
return JSONResponse(
content=(
{"success": False, "message": "No recordings found for time range"}
),
status_code=400,
)
else:
previews_count = (
Previews.select()
.where(
Previews.start_time.between(start_time, end_time)
| Previews.end_time.between(start_time, end_time)
| ((start_time > Previews.start_time) & (end_time < Previews.end_time))
)
.where(Previews.camera == camera_name)
.count()
)
if not is_current_hour(start_time) and previews_count <= 0:
return JSONResponse(
content=(
{"success": False, "message": "No previews found for time range"}
),
status_code=400,
)
export_id = f"{camera_name}_{''.join(random.choices(string.ascii_lowercase + string.digits, k=6))}"
# Set default values if not provided (timelapse defaults)
if ffmpeg_input_args is None:
ffmpeg_input_args = ""
if ffmpeg_output_args is None:
ffmpeg_output_args = DEFAULT_TIME_LAPSE_FFMPEG_ARGS
exporter = RecordingExporter(
request.app.frigate_config,
export_id,
camera_name,
friendly_name,
existing_image,
int(start_time),
int(end_time),
(
PlaybackSourceEnum[playback_source]
if playback_source in PlaybackSourceEnum.__members__.values()
else PlaybackSourceEnum.recordings
),
export_case_id,
ffmpeg_input_args,
ffmpeg_output_args,
cpu_fallback,
)
exporter.start()
return JSONResponse(
content=(
{
"success": True,
"message": "Starting export of recording.",
"export_id": export_id,
}
),
status_code=200,
)
@router.get(
"/exports/{export_id}",
response_model=ExportModel,

View File

@@ -16,12 +16,14 @@ from frigate.api import app as main_app
from frigate.api import (
auth,
camera,
chat,
classification,
event,
export,
media,
notification,
preview,
record,
review,
)
from frigate.api.auth import get_jwt_secret, limiter, require_admin_by_default
@@ -31,6 +33,7 @@ from frigate.comms.event_metadata_updater import (
from frigate.config import FrigateConfig
from frigate.config.camera.updater import CameraConfigUpdatePublisher
from frigate.embeddings import EmbeddingsContext
from frigate.genai import GenAIClientManager
from frigate.ptz.onvif import OnvifController
from frigate.stats.emitter import StatsEmitter
from frigate.storage import StorageMaintainer
@@ -120,6 +123,7 @@ def create_fastapi_app(
# Order of include_router matters: https://fastapi.tiangolo.com/tutorial/path-params/#order-matters
app.include_router(auth.router)
app.include_router(camera.router)
app.include_router(chat.router)
app.include_router(classification.router)
app.include_router(review.router)
app.include_router(main_app.router)
@@ -128,8 +132,10 @@ def create_fastapi_app(
app.include_router(export.router)
app.include_router(event.router)
app.include_router(media.router)
app.include_router(record.router)
# App Properties
app.frigate_config = frigate_config
app.genai_manager = GenAIClientManager(frigate_config)
app.embeddings = embeddings
app.detected_frames_processor = detected_frames_processor
app.storage_maintainer = storage_maintainer

View File

@@ -8,9 +8,8 @@ import os
import subprocess as sp
import time
from datetime import datetime, timedelta, timezone
from functools import reduce
from pathlib import Path as FilePath
from typing import Any, List
from typing import Any
from urllib.parse import unquote
import cv2
@@ -19,12 +18,11 @@ import pytz
from fastapi import APIRouter, Depends, Path, Query, Request, Response
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
from pathvalidate import sanitize_filename
from peewee import DoesNotExist, fn, operator
from peewee import DoesNotExist, fn
from tzlocal import get_localzone_name
from frigate.api.auth import (
allow_any_authenticated,
get_allowed_cameras_for_filter,
require_camera_access,
)
from frigate.api.defs.query.media_query_parameters import (
@@ -32,8 +30,6 @@ from frigate.api.defs.query.media_query_parameters import (
MediaEventsSnapshotQueryParams,
MediaLatestFrameQueryParams,
MediaMjpegFeedQueryParams,
MediaRecordingsAvailabilityQueryParams,
MediaRecordingsSummaryQueryParams,
)
from frigate.api.defs.tags import Tags
from frigate.camera.state import CameraState
@@ -44,13 +40,12 @@ from frigate.const import (
INSTALL_DIR,
MAX_SEGMENT_DURATION,
PREVIEW_FRAME_TYPE,
RECORD_DIR,
)
from frigate.models import Event, Previews, Recordings, Regions, ReviewSegment
from frigate.output.preview import get_most_recent_preview_frame
from frigate.track.object_processing import TrackedObjectProcessor
from frigate.util.file import get_event_thumbnail_bytes
from frigate.util.image import get_image_from_recording
from frigate.util.time import get_dst_transitions
logger = logging.getLogger(__name__)
@@ -131,7 +126,9 @@ async def camera_ptz_info(request: Request, camera_name: str):
@router.get(
"/{camera_name}/latest.{extension}", dependencies=[Depends(require_camera_access)]
"/{camera_name}/latest.{extension}",
dependencies=[Depends(require_camera_access)],
description="Returns the latest frame from the specified camera in the requested format (jpg, png, webp). Falls back to preview frames if the camera is offline.",
)
async def latest_frame(
request: Request,
@@ -165,20 +162,37 @@ async def latest_frame(
or 10
)
is_offline = False
if frame is None or datetime.now().timestamp() > (
frame_processor.get_current_frame_time(camera_name) + retry_interval
):
if request.app.camera_error_image is None:
error_image = glob.glob(
os.path.join(INSTALL_DIR, "frigate/images/camera-error.jpg")
)
last_frame_time = frame_processor.get_current_frame_time(camera_name)
preview_path = get_most_recent_preview_frame(
camera_name, before=last_frame_time
)
if len(error_image) > 0:
request.app.camera_error_image = cv2.imread(
error_image[0], cv2.IMREAD_UNCHANGED
if preview_path:
logger.debug(f"Using most recent preview frame for {camera_name}")
frame = cv2.imread(preview_path, cv2.IMREAD_UNCHANGED)
if frame is not None:
is_offline = True
if frame is None or not is_offline:
logger.debug(
f"No live or preview frame available for {camera_name}. Using error image."
)
if request.app.camera_error_image is None:
error_image = glob.glob(
os.path.join(INSTALL_DIR, "frigate/images/camera-error.jpg")
)
frame = request.app.camera_error_image
if len(error_image) > 0:
request.app.camera_error_image = cv2.imread(
error_image[0], cv2.IMREAD_UNCHANGED
)
frame = request.app.camera_error_image
height = int(params.height or str(frame.shape[0]))
width = int(height * frame.shape[1] / frame.shape[0])
@@ -200,14 +214,18 @@ async def latest_frame(
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
_, img = cv2.imencode(f".{extension.value}", frame, quality_params)
headers = {
"Cache-Control": "no-store" if not params.store else "private, max-age=60",
}
if is_offline:
headers["X-Frigate-Offline"] = "true"
return Response(
content=img.tobytes(),
media_type=extension.get_mime_type(),
headers={
"Cache-Control": "no-store"
if not params.store
else "private, max-age=60",
},
headers=headers,
)
elif (
camera_name == "birdseye"
@@ -397,333 +415,6 @@ async def submit_recording_snapshot_to_plus(
)
@router.get("/recordings/storage", dependencies=[Depends(allow_any_authenticated())])
def get_recordings_storage_usage(request: Request):
recording_stats = request.app.stats_emitter.get_latest_stats()["service"][
"storage"
][RECORD_DIR]
if not recording_stats:
return JSONResponse({})
total_mb = recording_stats["total"]
camera_usages: dict[str, dict] = (
request.app.storage_maintainer.calculate_camera_usages()
)
for camera_name in camera_usages.keys():
if camera_usages.get(camera_name, {}).get("usage"):
camera_usages[camera_name]["usage_percent"] = (
camera_usages.get(camera_name, {}).get("usage", 0) / total_mb
) * 100
return JSONResponse(content=camera_usages)
@router.get("/recordings/summary", dependencies=[Depends(allow_any_authenticated())])
def all_recordings_summary(
request: Request,
params: MediaRecordingsSummaryQueryParams = Depends(),
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
):
"""Returns true/false by day indicating if recordings exist"""
cameras = params.cameras
if cameras != "all":
requested = set(unquote(cameras).split(","))
filtered = requested.intersection(allowed_cameras)
if not filtered:
return JSONResponse(content={})
camera_list = list(filtered)
else:
camera_list = allowed_cameras
time_range_query = (
Recordings.select(
fn.MIN(Recordings.start_time).alias("min_time"),
fn.MAX(Recordings.start_time).alias("max_time"),
)
.where(Recordings.camera << camera_list)
.dicts()
.get()
)
min_time = time_range_query.get("min_time")
max_time = time_range_query.get("max_time")
if min_time is None or max_time is None:
return JSONResponse(content={})
dst_periods = get_dst_transitions(params.timezone, min_time, max_time)
days: dict[str, bool] = {}
for period_start, period_end, period_offset in dst_periods:
hours_offset = int(period_offset / 60 / 60)
minutes_offset = int(period_offset / 60 - hours_offset * 60)
period_hour_modifier = f"{hours_offset} hour"
period_minute_modifier = f"{minutes_offset} minute"
period_query = (
Recordings.select(
fn.strftime(
"%Y-%m-%d",
fn.datetime(
Recordings.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
).alias("day")
)
.where(
(Recordings.camera << camera_list)
& (Recordings.end_time >= period_start)
& (Recordings.start_time <= period_end)
)
.group_by(
fn.strftime(
"%Y-%m-%d",
fn.datetime(
Recordings.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
)
)
.order_by(Recordings.start_time.desc())
.namedtuples()
)
for g in period_query:
days[g.day] = True
return JSONResponse(content=dict(sorted(days.items())))
@router.get(
"/{camera_name}/recordings/summary", dependencies=[Depends(require_camera_access)]
)
async def recordings_summary(camera_name: str, timezone: str = "utc"):
"""Returns hourly summary for recordings of given camera"""
time_range_query = (
Recordings.select(
fn.MIN(Recordings.start_time).alias("min_time"),
fn.MAX(Recordings.start_time).alias("max_time"),
)
.where(Recordings.camera == camera_name)
.dicts()
.get()
)
min_time = time_range_query.get("min_time")
max_time = time_range_query.get("max_time")
days: dict[str, dict] = {}
if min_time is None or max_time is None:
return JSONResponse(content=list(days.values()))
dst_periods = get_dst_transitions(timezone, min_time, max_time)
for period_start, period_end, period_offset in dst_periods:
hours_offset = int(period_offset / 60 / 60)
minutes_offset = int(period_offset / 60 - hours_offset * 60)
period_hour_modifier = f"{hours_offset} hour"
period_minute_modifier = f"{minutes_offset} minute"
recording_groups = (
Recordings.select(
fn.strftime(
"%Y-%m-%d %H",
fn.datetime(
Recordings.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
).alias("hour"),
fn.SUM(Recordings.duration).alias("duration"),
fn.SUM(Recordings.motion).alias("motion"),
fn.SUM(Recordings.objects).alias("objects"),
)
.where(
(Recordings.camera == camera_name)
& (Recordings.end_time >= period_start)
& (Recordings.start_time <= period_end)
)
.group_by((Recordings.start_time + period_offset).cast("int") / 3600)
.order_by(Recordings.start_time.desc())
.namedtuples()
)
event_groups = (
Event.select(
fn.strftime(
"%Y-%m-%d %H",
fn.datetime(
Event.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
).alias("hour"),
fn.COUNT(Event.id).alias("count"),
)
.where(Event.camera == camera_name, Event.has_clip)
.where(
(Event.start_time >= period_start) & (Event.start_time <= period_end)
)
.group_by((Event.start_time + period_offset).cast("int") / 3600)
.namedtuples()
)
event_map = {g.hour: g.count for g in event_groups}
for recording_group in recording_groups:
parts = recording_group.hour.split()
hour = parts[1]
day = parts[0]
events_count = event_map.get(recording_group.hour, 0)
hour_data = {
"hour": hour,
"events": events_count,
"motion": recording_group.motion,
"objects": recording_group.objects,
"duration": round(recording_group.duration),
}
if day in days:
# merge counts if already present (edge-case at DST boundary)
days[day]["events"] += events_count or 0
days[day]["hours"].append(hour_data)
else:
days[day] = {
"events": events_count or 0,
"hours": [hour_data],
"day": day,
}
return JSONResponse(content=list(days.values()))
@router.get("/{camera_name}/recordings", dependencies=[Depends(require_camera_access)])
async def recordings(
camera_name: str,
after: float = (datetime.now() - timedelta(hours=1)).timestamp(),
before: float = datetime.now().timestamp(),
):
"""Return specific camera recordings between the given 'after'/'end' times. If not provided the last hour will be used"""
recordings = (
Recordings.select(
Recordings.id,
Recordings.start_time,
Recordings.end_time,
Recordings.segment_size,
Recordings.motion,
Recordings.objects,
Recordings.duration,
)
.where(
Recordings.camera == camera_name,
Recordings.end_time >= after,
Recordings.start_time <= before,
)
.order_by(Recordings.start_time)
.dicts()
.iterator()
)
return JSONResponse(content=list(recordings))
@router.get(
"/recordings/unavailable",
response_model=list[dict],
dependencies=[Depends(allow_any_authenticated())],
)
async def no_recordings(
request: Request,
params: MediaRecordingsAvailabilityQueryParams = Depends(),
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
):
"""Get time ranges with no recordings."""
cameras = params.cameras
if cameras != "all":
requested = set(unquote(cameras).split(","))
filtered = requested.intersection(allowed_cameras)
if not filtered:
return JSONResponse(content=[])
cameras = ",".join(filtered)
else:
cameras = allowed_cameras
before = params.before or datetime.datetime.now().timestamp()
after = (
params.after
or (datetime.datetime.now() - datetime.timedelta(hours=1)).timestamp()
)
scale = params.scale
clauses = [(Recordings.end_time >= after) & (Recordings.start_time <= before)]
if cameras != "all":
camera_list = cameras.split(",")
clauses.append((Recordings.camera << camera_list))
else:
camera_list = allowed_cameras
# Get recording start times
data: list[Recordings] = (
Recordings.select(Recordings.start_time, Recordings.end_time)
.where(reduce(operator.and_, clauses))
.order_by(Recordings.start_time.asc())
.dicts()
.iterator()
)
# Convert recordings to list of (start, end) tuples
recordings = [(r["start_time"], r["end_time"]) for r in data]
# Iterate through time segments and check if each has any recording
no_recording_segments = []
current = after
current_gap_start = None
while current < before:
segment_end = min(current + scale, before)
# Check if this segment overlaps with any recording
has_recording = any(
rec_start < segment_end and rec_end > current
for rec_start, rec_end in recordings
)
if not has_recording:
# This segment has no recordings
if current_gap_start is None:
current_gap_start = current # Start a new gap
else:
# This segment has recordings
if current_gap_start is not None:
# End the current gap and append it
no_recording_segments.append(
{"start_time": int(current_gap_start), "end_time": int(current)}
)
current_gap_start = None
current = segment_end
# Append the last gap if it exists
if current_gap_start is not None:
no_recording_segments.append(
{"start_time": int(current_gap_start), "end_time": int(before)}
)
return JSONResponse(content=no_recording_segments)
@router.get(
"/{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4",
dependencies=[Depends(require_camera_access)],
@@ -1070,7 +761,7 @@ async def event_snapshot(
if event_id in camera_state.tracked_objects:
tracked_obj = camera_state.tracked_objects.get(event_id)
if tracked_obj is not None:
jpg_bytes = tracked_obj.get_img_bytes(
jpg_bytes, frame_time = tracked_obj.get_img_bytes(
ext="jpg",
timestamp=params.timestamp,
bounding_box=params.bbox,
@@ -1099,6 +790,7 @@ async def event_snapshot(
headers = {
"Content-Type": "image/jpeg",
"Cache-Control": "private, max-age=31536000" if event_complete else "no-store",
"X-Frame-Time": frame_time,
}
if params.download:

479
frigate/api/record.py Normal file
View File

@@ -0,0 +1,479 @@
"""Recording APIs."""
import logging
from datetime import datetime, timedelta
from functools import reduce
from pathlib import Path
from typing import List
from urllib.parse import unquote
from fastapi import APIRouter, Depends, Request
from fastapi import Path as PathParam
from fastapi.responses import JSONResponse
from peewee import fn, operator
from frigate.api.auth import (
allow_any_authenticated,
get_allowed_cameras_for_filter,
require_camera_access,
require_role,
)
from frigate.api.defs.query.recordings_query_parameters import (
MediaRecordingsAvailabilityQueryParams,
MediaRecordingsSummaryQueryParams,
RecordingsDeleteQueryParams,
)
from frigate.api.defs.response.generic_response import GenericResponse
from frigate.api.defs.tags import Tags
from frigate.const import RECORD_DIR
from frigate.models import Event, Recordings
from frigate.util.time import get_dst_transitions
logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.recordings])
@router.get("/recordings/storage", dependencies=[Depends(allow_any_authenticated())])
def get_recordings_storage_usage(request: Request):
recording_stats = request.app.stats_emitter.get_latest_stats()["service"][
"storage"
][RECORD_DIR]
if not recording_stats:
return JSONResponse({})
total_mb = recording_stats["total"]
camera_usages: dict[str, dict] = (
request.app.storage_maintainer.calculate_camera_usages()
)
for camera_name in camera_usages.keys():
if camera_usages.get(camera_name, {}).get("usage"):
camera_usages[camera_name]["usage_percent"] = (
camera_usages.get(camera_name, {}).get("usage", 0) / total_mb
) * 100
return JSONResponse(content=camera_usages)
@router.get("/recordings/summary", dependencies=[Depends(allow_any_authenticated())])
def all_recordings_summary(
request: Request,
params: MediaRecordingsSummaryQueryParams = Depends(),
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
):
"""Returns true/false by day indicating if recordings exist"""
cameras = params.cameras
if cameras != "all":
requested = set(unquote(cameras).split(","))
filtered = requested.intersection(allowed_cameras)
if not filtered:
return JSONResponse(content={})
camera_list = list(filtered)
else:
camera_list = allowed_cameras
time_range_query = (
Recordings.select(
fn.MIN(Recordings.start_time).alias("min_time"),
fn.MAX(Recordings.start_time).alias("max_time"),
)
.where(Recordings.camera << camera_list)
.dicts()
.get()
)
min_time = time_range_query.get("min_time")
max_time = time_range_query.get("max_time")
if min_time is None or max_time is None:
return JSONResponse(content={})
dst_periods = get_dst_transitions(params.timezone, min_time, max_time)
days: dict[str, bool] = {}
for period_start, period_end, period_offset in dst_periods:
hours_offset = int(period_offset / 60 / 60)
minutes_offset = int(period_offset / 60 - hours_offset * 60)
period_hour_modifier = f"{hours_offset} hour"
period_minute_modifier = f"{minutes_offset} minute"
period_query = (
Recordings.select(
fn.strftime(
"%Y-%m-%d",
fn.datetime(
Recordings.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
).alias("day")
)
.where(
(Recordings.camera << camera_list)
& (Recordings.end_time >= period_start)
& (Recordings.start_time <= period_end)
)
.group_by(
fn.strftime(
"%Y-%m-%d",
fn.datetime(
Recordings.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
)
)
.order_by(Recordings.start_time.desc())
.namedtuples()
)
for g in period_query:
days[g.day] = True
return JSONResponse(content=dict(sorted(days.items())))
@router.get(
"/{camera_name}/recordings/summary", dependencies=[Depends(require_camera_access)]
)
async def recordings_summary(camera_name: str, timezone: str = "utc"):
"""Returns hourly summary for recordings of given camera"""
time_range_query = (
Recordings.select(
fn.MIN(Recordings.start_time).alias("min_time"),
fn.MAX(Recordings.start_time).alias("max_time"),
)
.where(Recordings.camera == camera_name)
.dicts()
.get()
)
min_time = time_range_query.get("min_time")
max_time = time_range_query.get("max_time")
days: dict[str, dict] = {}
if min_time is None or max_time is None:
return JSONResponse(content=list(days.values()))
dst_periods = get_dst_transitions(timezone, min_time, max_time)
for period_start, period_end, period_offset in dst_periods:
hours_offset = int(period_offset / 60 / 60)
minutes_offset = int(period_offset / 60 - hours_offset * 60)
period_hour_modifier = f"{hours_offset} hour"
period_minute_modifier = f"{minutes_offset} minute"
recording_groups = (
Recordings.select(
fn.strftime(
"%Y-%m-%d %H",
fn.datetime(
Recordings.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
).alias("hour"),
fn.SUM(Recordings.duration).alias("duration"),
fn.SUM(Recordings.motion).alias("motion"),
fn.SUM(Recordings.objects).alias("objects"),
)
.where(
(Recordings.camera == camera_name)
& (Recordings.end_time >= period_start)
& (Recordings.start_time <= period_end)
)
.group_by((Recordings.start_time + period_offset).cast("int") / 3600)
.order_by(Recordings.start_time.desc())
.namedtuples()
)
event_groups = (
Event.select(
fn.strftime(
"%Y-%m-%d %H",
fn.datetime(
Event.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
).alias("hour"),
fn.COUNT(Event.id).alias("count"),
)
.where(Event.camera == camera_name, Event.has_clip)
.where(
(Event.start_time >= period_start) & (Event.start_time <= period_end)
)
.group_by((Event.start_time + period_offset).cast("int") / 3600)
.namedtuples()
)
event_map = {g.hour: g.count for g in event_groups}
for recording_group in recording_groups:
parts = recording_group.hour.split()
hour = parts[1]
day = parts[0]
events_count = event_map.get(recording_group.hour, 0)
hour_data = {
"hour": hour,
"events": events_count,
"motion": recording_group.motion,
"objects": recording_group.objects,
"duration": round(recording_group.duration),
}
if day in days:
# merge counts if already present (edge-case at DST boundary)
days[day]["events"] += events_count or 0
days[day]["hours"].append(hour_data)
else:
days[day] = {
"events": events_count or 0,
"hours": [hour_data],
"day": day,
}
return JSONResponse(content=list(days.values()))
@router.get("/{camera_name}/recordings", dependencies=[Depends(require_camera_access)])
async def recordings(
camera_name: str,
after: float = (datetime.now() - timedelta(hours=1)).timestamp(),
before: float = datetime.now().timestamp(),
):
"""Return specific camera recordings between the given 'after'/'end' times. If not provided the last hour will be used"""
recordings = (
Recordings.select(
Recordings.id,
Recordings.start_time,
Recordings.end_time,
Recordings.segment_size,
Recordings.motion,
Recordings.objects,
Recordings.duration,
)
.where(
Recordings.camera == camera_name,
Recordings.end_time >= after,
Recordings.start_time <= before,
)
.order_by(Recordings.start_time)
.dicts()
.iterator()
)
return JSONResponse(content=list(recordings))
@router.get(
"/recordings/unavailable",
response_model=list[dict],
dependencies=[Depends(allow_any_authenticated())],
)
async def no_recordings(
request: Request,
params: MediaRecordingsAvailabilityQueryParams = Depends(),
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
):
"""Get time ranges with no recordings."""
cameras = params.cameras
if cameras != "all":
requested = set(unquote(cameras).split(","))
filtered = requested.intersection(allowed_cameras)
if not filtered:
return JSONResponse(content=[])
cameras = ",".join(filtered)
else:
cameras = allowed_cameras
before = params.before or datetime.datetime.now().timestamp()
after = (
params.after
or (datetime.datetime.now() - datetime.timedelta(hours=1)).timestamp()
)
scale = params.scale
clauses = [(Recordings.end_time >= after) & (Recordings.start_time <= before)]
if cameras != "all":
camera_list = cameras.split(",")
clauses.append((Recordings.camera << camera_list))
else:
camera_list = allowed_cameras
# Get recording start times
data: list[Recordings] = (
Recordings.select(Recordings.start_time, Recordings.end_time)
.where(reduce(operator.and_, clauses))
.order_by(Recordings.start_time.asc())
.dicts()
.iterator()
)
# Convert recordings to list of (start, end) tuples
recordings = [(r["start_time"], r["end_time"]) for r in data]
# Iterate through time segments and check if each has any recording
no_recording_segments = []
current = after
current_gap_start = None
while current < before:
segment_end = min(current + scale, before)
# Check if this segment overlaps with any recording
has_recording = any(
rec_start < segment_end and rec_end > current
for rec_start, rec_end in recordings
)
if not has_recording:
# This segment has no recordings
if current_gap_start is None:
current_gap_start = current # Start a new gap
else:
# This segment has recordings
if current_gap_start is not None:
# End the current gap and append it
no_recording_segments.append(
{"start_time": int(current_gap_start), "end_time": int(current)}
)
current_gap_start = None
current = segment_end
# Append the last gap if it exists
if current_gap_start is not None:
no_recording_segments.append(
{"start_time": int(current_gap_start), "end_time": int(before)}
)
return JSONResponse(content=no_recording_segments)
@router.delete(
"/recordings/start/{start}/end/{end}",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Delete recordings",
description="""Deletes recordings within the specified time range.
Recordings can be filtered by cameras and kept based on motion, objects, or audio attributes.
""",
)
async def delete_recordings(
start: float = PathParam(..., description="Start timestamp (unix)"),
end: float = PathParam(..., description="End timestamp (unix)"),
params: RecordingsDeleteQueryParams = Depends(),
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
):
"""Delete recordings in the specified time range."""
if start >= end:
return JSONResponse(
content={
"success": False,
"message": "Start time must be less than end time.",
},
status_code=400,
)
cameras = params.cameras
if cameras != "all":
requested = set(cameras.split(","))
filtered = requested.intersection(allowed_cameras)
if not filtered:
return JSONResponse(
content={
"success": False,
"message": "No valid cameras found in the request.",
},
status_code=400,
)
camera_list = list(filtered)
else:
camera_list = allowed_cameras
# Parse keep parameter
keep_set = set()
if params.keep:
keep_set = set(params.keep.split(","))
# Build query to find overlapping recordings
clauses = [
(
Recordings.start_time.between(start, end)
| Recordings.end_time.between(start, end)
| ((start > Recordings.start_time) & (end < Recordings.end_time))
),
(Recordings.camera << camera_list),
]
keep_clauses = []
if "motion" in keep_set:
keep_clauses.append(Recordings.motion.is_null(False) & (Recordings.motion > 0))
if "object" in keep_set:
keep_clauses.append(
Recordings.objects.is_null(False) & (Recordings.objects > 0)
)
if "audio" in keep_set:
keep_clauses.append(Recordings.dBFS.is_null(False))
if keep_clauses:
keep_condition = reduce(operator.or_, keep_clauses)
clauses.append(~keep_condition)
recordings_to_delete = (
Recordings.select(Recordings.id, Recordings.path)
.where(reduce(operator.and_, clauses))
.dicts()
.iterator()
)
recording_ids = []
deleted_count = 0
error_count = 0
for recording in recordings_to_delete:
recording_ids.append(recording["id"])
try:
Path(recording["path"]).unlink(missing_ok=True)
deleted_count += 1
except Exception as e:
logger.error(f"Failed to delete recording file {recording['path']}: {e}")
error_count += 1
if recording_ids:
max_deletes = 100000
recording_ids_list = list(recording_ids)
for i in range(0, len(recording_ids_list), max_deletes):
Recordings.delete().where(
Recordings.id << recording_ids_list[i : i + max_deletes]
).execute()
message = f"Successfully deleted {deleted_count} recording(s)."
if error_count > 0:
message += f" {error_count} file deletion error(s) occurred."
return JSONResponse(
content={"success": True, "message": message},
status_code=200,
)

View File

@@ -33,7 +33,6 @@ from frigate.api.defs.response.review_response import (
ReviewSummaryResponse,
)
from frigate.api.defs.tags import Tags
from frigate.config import FrigateConfig
from frigate.embeddings import EmbeddingsContext
from frigate.models import Recordings, ReviewSegment, UserReviewStatus
from frigate.review.types import SeverityEnum
@@ -144,6 +143,8 @@ async def review(
(UserReviewStatus.has_been_reviewed == False)
| (UserReviewStatus.has_been_reviewed.is_null())
)
elif reviewed == 1:
review_query = review_query.where(UserReviewStatus.has_been_reviewed == True)
# Apply ordering and limit
review_query = (
@@ -745,9 +746,7 @@ async def set_not_reviewed(
description="Use GenAI to summarize review items over a period of time.",
)
def generate_review_summary(request: Request, start_ts: float, end_ts: float):
config: FrigateConfig = request.app.frigate_config
if not config.genai.provider:
if not request.app.genai_manager.vision_client:
return JSONResponse(
content=(
{

View File

@@ -19,6 +19,8 @@ class CameraMetrics:
process_pid: Synchronized
capture_process_pid: Synchronized
ffmpeg_pid: Synchronized
reconnects_last_hour: Synchronized
stalls_last_hour: Synchronized
def __init__(self, manager: SyncManager):
self.camera_fps = manager.Value("d", 0)
@@ -35,6 +37,8 @@ class CameraMetrics:
self.process_pid = manager.Value("i", 0)
self.capture_process_pid = manager.Value("i", 0)
self.ffmpeg_pid = manager.Value("i", 0)
self.reconnects_last_hour = manager.Value("i", 0)
self.stalls_last_hour = manager.Value("i", 0)
class PTZMetrics:

View File

@@ -28,6 +28,7 @@ from frigate.const import (
UPDATE_CAMERA_ACTIVITY,
UPDATE_EMBEDDINGS_REINDEX_PROGRESS,
UPDATE_EVENT_DESCRIPTION,
UPDATE_JOB_STATE,
UPDATE_MODEL_STATE,
UPDATE_REVIEW_DESCRIPTION,
UPSERT_REVIEW_SEGMENT,
@@ -60,6 +61,7 @@ class Dispatcher:
self.camera_activity = CameraActivityManager(config, self.publish)
self.audio_activity = AudioActivityManager(config, self.publish)
self.model_state: dict[str, ModelStatusTypesEnum] = {}
self.job_state: dict[str, dict[str, Any]] = {} # {job_type: job_data}
self.embeddings_reindex: dict[str, Any] = {}
self.birdseye_layout: dict[str, Any] = {}
self.audio_transcription_state: str = "idle"
@@ -180,6 +182,19 @@ class Dispatcher:
def handle_model_state() -> None:
self.publish("model_state", json.dumps(self.model_state.copy()))
def handle_update_job_state() -> None:
if payload and isinstance(payload, dict):
job_type = payload.get("job_type")
if job_type:
self.job_state[job_type] = payload
self.publish(
"job_state",
json.dumps(self.job_state),
)
def handle_job_state() -> None:
self.publish("job_state", json.dumps(self.job_state.copy()))
def handle_update_audio_transcription_state() -> None:
if payload:
self.audio_transcription_state = payload
@@ -277,6 +292,7 @@ class Dispatcher:
UPDATE_EVENT_DESCRIPTION: handle_update_event_description,
UPDATE_REVIEW_DESCRIPTION: handle_update_review_description,
UPDATE_MODEL_STATE: handle_update_model_state,
UPDATE_JOB_STATE: handle_update_job_state,
UPDATE_EMBEDDINGS_REINDEX_PROGRESS: handle_update_embeddings_reindex_progress,
UPDATE_BIRDSEYE_LAYOUT: handle_update_birdseye_layout,
UPDATE_AUDIO_TRANSCRIPTION_STATE: handle_update_audio_transcription_state,
@@ -284,6 +300,7 @@ class Dispatcher:
"restart": handle_restart,
"embeddingsReindexProgress": handle_embeddings_reindex_progress,
"modelState": handle_model_state,
"jobState": handle_job_state,
"audioTranscriptionState": handle_audio_transcription_state,
"birdseyeLayout": handle_birdseye_layout,
"onConnect": handle_on_connect,

View File

@@ -8,6 +8,7 @@ from .config import * # noqa: F403
from .database import * # noqa: F403
from .logger import * # noqa: F403
from .mqtt import * # noqa: F403
from .network import * # noqa: F403
from .proxy import * # noqa: F403
from .telemetry import * # noqa: F403
from .tls import * # noqa: F403

View File

@@ -6,7 +6,7 @@ from pydantic import Field
from ..base import FrigateBaseModel
from ..env import EnvString
__all__ = ["GenAIConfig", "GenAIProviderEnum"]
__all__ = ["GenAIConfig", "GenAIProviderEnum", "GenAIRoleEnum"]
class GenAIProviderEnum(str, Enum):
@@ -14,6 +14,13 @@ class GenAIProviderEnum(str, Enum):
azure_openai = "azure_openai"
gemini = "gemini"
ollama = "ollama"
llamacpp = "llamacpp"
class GenAIRoleEnum(str, Enum):
tools = "tools"
vision = "vision"
embeddings = "embeddings"
class GenAIConfig(FrigateBaseModel):
@@ -23,6 +30,17 @@ class GenAIConfig(FrigateBaseModel):
base_url: Optional[str] = Field(default=None, title="Provider base url.")
model: str = Field(default="gpt-4o", title="GenAI model.")
provider: GenAIProviderEnum | None = Field(default=None, title="GenAI provider.")
roles: list[GenAIRoleEnum] = Field(
default_factory=lambda: [
GenAIRoleEnum.embeddings,
GenAIRoleEnum.vision,
GenAIRoleEnum.tools,
],
title="GenAI roles (tools, vision, embeddings); one provider per role.",
)
provider_options: dict[str, Any] = Field(
default={}, title="GenAI Provider extra options."
)
runtime_options: dict[str, Any] = Field(
default={}, title="Options to pass during inference calls."
)

View File

@@ -1,5 +1,5 @@
from enum import Enum
from typing import Optional
from typing import Optional, Union
from pydantic import Field
@@ -19,8 +19,6 @@ __all__ = [
"RetainModeEnum",
]
DEFAULT_TIME_LAPSE_FFMPEG_ARGS = "-vf setpts=0.04*PTS -r 30"
class RecordRetainConfig(FrigateBaseModel):
days: float = Field(default=0, ge=0, title="Default retention period.")
@@ -67,16 +65,13 @@ class RecordPreviewConfig(FrigateBaseModel):
class RecordExportConfig(FrigateBaseModel):
timelapse_args: str = Field(
default=DEFAULT_TIME_LAPSE_FFMPEG_ARGS, title="Timelapse Args"
hwaccel_args: Union[str, list[str]] = Field(
default="auto", title="Export-specific FFmpeg hardware acceleration arguments."
)
class RecordConfig(FrigateBaseModel):
enabled: bool = Field(default=False, title="Enable record on all cameras.")
sync_recordings: bool = Field(
default=False, title="Sync recordings with disk on startup and once a day."
)
expire_interval: int = Field(
default=60,
title="Number of minutes to wait between cleanup runs.",

View File

@@ -108,12 +108,13 @@ class GenAIReviewConfig(FrigateBaseModel):
default="""### Normal Activity Indicators (Level 0)
- Known/verified people in any zone at any time
- People with pets in residential areas
- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items — normal commute and travel patterns
- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving
- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime
- Activity confined to public areas only (sidewalks, streets) without entering property at any time
### Suspicious Activity Indicators (Level 1)
- **Testing or attempting to open doors/windows/handles on vehicles or buildings** — ALWAYS Level 1 regardless of time or duration
- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools — Level 1
- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** — ALWAYS Level 1 regardless of activity or duration
- Taking items that don't belong to them (packages, objects from porches/driveways)
- Climbing or jumping fences/barriers to access property
@@ -133,8 +134,8 @@ Evaluate in this order:
1. **If person is verified/known** → Level 0 regardless of time or activity
2. **If person is unidentified:**
- Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) → Level 1
- Check actions: If testing doors/handles, taking items, climbing → Level 1
- Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service worker) → Level 0
- Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing → Level 1
- Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) → Level 0
3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)
The mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is.""",

View File

@@ -45,7 +45,7 @@ from .camera.audio import AudioConfig
from .camera.birdseye import BirdseyeConfig
from .camera.detect import DetectConfig
from .camera.ffmpeg import FfmpegConfig
from .camera.genai import GenAIConfig
from .camera.genai import GenAIConfig, GenAIRoleEnum
from .camera.motion import MotionConfig
from .camera.notification import NotificationConfig
from .camera.objects import FilterConfig, ObjectConfig
@@ -347,9 +347,9 @@ class FrigateConfig(FrigateBaseModel):
default_factory=ModelConfig, title="Detection model configuration."
)
# GenAI config
genai: GenAIConfig = Field(
default_factory=GenAIConfig, title="Generative AI configuration."
# GenAI config (named provider configs: name -> GenAIConfig)
genai: Dict[str, GenAIConfig] = Field(
default_factory=dict, title="Generative AI configuration (named providers)."
)
# Camera config
@@ -431,6 +431,18 @@ class FrigateConfig(FrigateBaseModel):
# set notifications state
self.notifications.enabled_in_config = self.notifications.enabled
# validate genai: each role (tools, vision, embeddings) at most once
role_to_name: dict[GenAIRoleEnum, str] = {}
for name, genai_cfg in self.genai.items():
for role in genai_cfg.roles:
if role in role_to_name:
raise ValueError(
f"GenAI role '{role.value}' is assigned to both "
f"'{role_to_name[role]}' and '{name}'; each role must have "
"exactly one provider."
)
role_to_name[role] = name
# set default min_score for object attributes
for attribute in self.model.all_attributes:
if not self.objects.filters.get(attribute):
@@ -525,6 +537,14 @@ class FrigateConfig(FrigateBaseModel):
if camera_config.ffmpeg.hwaccel_args == "auto":
camera_config.ffmpeg.hwaccel_args = self.ffmpeg.hwaccel_args
# Resolve export hwaccel_args: camera export -> camera ffmpeg -> global ffmpeg
# This allows per-camera override for exports (e.g., when camera resolution
# exceeds hardware encoder limits)
if camera_config.record.export.hwaccel_args == "auto":
camera_config.record.export.hwaccel_args = (
camera_config.ffmpeg.hwaccel_args
)
for input in camera_config.ffmpeg.inputs:
need_detect_dimensions = "detect" in input.roles and (
camera_config.detect.height is None
@@ -662,6 +682,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

View File

@@ -1,13 +1,27 @@
from typing import Union
from pydantic import Field
from .base import FrigateBaseModel
__all__ = ["IPv6Config", "NetworkingConfig"]
__all__ = ["IPv6Config", "ListenConfig", "NetworkingConfig"]
class IPv6Config(FrigateBaseModel):
enabled: bool = Field(default=False, title="Enable IPv6 for port 5000 and/or 8971")
class ListenConfig(FrigateBaseModel):
internal: Union[int, str] = Field(
default=5000, title="Internal listening port for Frigate"
)
external: Union[int, str] = Field(
default=8971, title="External listening port for Frigate"
)
class NetworkingConfig(FrigateBaseModel):
ipv6: IPv6Config = Field(default_factory=IPv6Config, title="Network configuration")
ipv6: IPv6Config = Field(default_factory=IPv6Config, title="IPv6 configuration")
listen: ListenConfig = Field(
default_factory=ListenConfig, title="Listening ports configuration"
)

View File

@@ -14,7 +14,6 @@ RECORD_DIR = f"{BASE_DIR}/recordings"
TRIGGER_DIR = f"{CLIPS_DIR}/triggers"
BIRDSEYE_PIPE = "/tmp/cache/birdseye"
CACHE_DIR = "/tmp/cache"
FRIGATE_LOCALHOST = "http://127.0.0.1:5000"
PLUS_ENV_VAR = "PLUS_API_KEY"
PLUS_API_HOST = "https://api.frigate.video"
@@ -122,6 +121,7 @@ UPDATE_REVIEW_DESCRIPTION = "update_review_description"
UPDATE_MODEL_STATE = "update_model_state"
UPDATE_EMBEDDINGS_REINDEX_PROGRESS = "handle_embeddings_reindex_progress"
UPDATE_BIRDSEYE_LAYOUT = "update_birdseye_layout"
UPDATE_JOB_STATE = "update_job_state"
NOTIFICATION_TEST = "notification_test"
# IO Nice Values

View File

@@ -97,7 +97,7 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi):
self.interpreter.allocate_tensors()
self.tensor_input_details = self.interpreter.get_input_details()
self.tensor_output_details = self.interpreter.get_output_details()
self.labelmap = load_labels(labelmap_path, prefill=0)
self.labelmap = load_labels(labelmap_path, prefill=0, indexed=False)
self.classifications_per_second.start()
def __update_metrics(self, duration: float) -> None:
@@ -398,7 +398,7 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
self.interpreter.allocate_tensors()
self.tensor_input_details = self.interpreter.get_input_details()
self.tensor_output_details = self.interpreter.get_output_details()
self.labelmap = load_labels(labelmap_path, prefill=0)
self.labelmap = load_labels(labelmap_path, prefill=0, indexed=False)
def __update_metrics(self, duration: float) -> None:
self.classifications_per_second.update()
@@ -419,14 +419,21 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
"""
if object_id not in self.classification_history:
self.classification_history[object_id] = []
logger.debug(f"Created new classification history for {object_id}")
self.classification_history[object_id].append(
(current_label, current_score, current_time)
)
history = self.classification_history[object_id]
logger.debug(
f"History for {object_id}: {len(history)} entries, latest=({current_label}, {current_score})"
)
if len(history) < 3:
logger.debug(
f"History for {object_id} has {len(history)} entries, need at least 3"
)
return None, 0.0
label_counts = {}
@@ -445,14 +452,27 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
best_count = label_counts[best_label]
consensus_threshold = total_attempts * 0.6
logger.debug(
f"Consensus calc for {object_id}: label_counts={label_counts}, "
f"best_label={best_label}, best_count={best_count}, "
f"total={total_attempts}, threshold={consensus_threshold}"
)
if best_count < consensus_threshold:
logger.debug(
f"No consensus for {object_id}: {best_count} < {consensus_threshold}"
)
return None, 0.0
avg_score = sum(label_scores[best_label]) / len(label_scores[best_label])
if best_label == "none":
logger.debug(f"Filtering 'none' label for {object_id}")
return None, 0.0
logger.debug(
f"Consensus reached for {object_id}: {best_label} with avg_score={avg_score}"
)
return best_label, avg_score
def process_frame(self, obj_data, frame):
@@ -560,17 +580,30 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
)
if score < self.model_config.threshold:
logger.debug(f"Score {score} is less than threshold.")
logger.debug(
f"{self.model_config.name}: Score {score} < threshold {self.model_config.threshold} for {object_id}, skipping"
)
return
sub_label = self.labelmap[best_id]
logger.debug(
f"{self.model_config.name}: Object {object_id} (label={obj_data['label']}) passed threshold with sub_label={sub_label}, score={score}"
)
consensus_label, consensus_score = self.get_weighted_score(
object_id, sub_label, score, now
)
logger.debug(
f"{self.model_config.name}: get_weighted_score returned consensus_label={consensus_label}, consensus_score={consensus_score} for {object_id}"
)
if consensus_label is not None:
camera = obj_data["camera"]
logger.debug(
f"{self.model_config.name}: Publishing sub_label={consensus_label} for {obj_data['label']} object {object_id} on {camera}"
)
if (
self.model_config.object_config.classification_type
@@ -625,6 +658,7 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
def handle_request(self, topic, request_data):
if topic == EmbeddingsRequestEnum.reload_classification_model.value:
if request_data.get("model_name") == self.model_config.name:
self.__build_detector()
logger.info(
f"Successfully loaded updated model for {self.model_config.name}"
)
@@ -662,7 +696,7 @@ def write_classification_attempt(
# delete oldest face image if maximum is reached
try:
files = sorted(
filter(lambda f: (f.endswith(".webp")), os.listdir(folder)),
filter(lambda f: f.endswith(".webp"), os.listdir(folder)),
key=lambda f: os.path.getctime(os.path.join(folder, f)),
reverse=True,
)

View File

@@ -539,7 +539,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
cv2.imwrite(file, frame)
files = sorted(
filter(lambda f: (f.endswith(".webp")), os.listdir(folder)),
filter(lambda f: f.endswith(".webp"), os.listdir(folder)),
key=lambda f: os.path.getctime(os.path.join(folder, f)),
reverse=True,
)

View File

@@ -131,10 +131,8 @@ class ONNXModelRunner(BaseModelRunner):
return model_type in [
EnrichmentModelTypeEnum.paddleocr.value,
EnrichmentModelTypeEnum.yolov9_license_plate.value,
EnrichmentModelTypeEnum.jina_v1.value,
EnrichmentModelTypeEnum.jina_v2.value,
EnrichmentModelTypeEnum.facenet.value,
EnrichmentModelTypeEnum.arcface.value,
ModelTypeEnum.rfdetr.value,
ModelTypeEnum.dfine.value,
]

View File

@@ -59,7 +59,7 @@ from frigate.data_processing.real_time.license_plate import (
from frigate.data_processing.types import DataProcessorMetrics, PostProcessDataEnum
from frigate.db.sqlitevecq import SqliteVecQueueDatabase
from frigate.events.types import EventTypeEnum, RegenerateDescriptionEnum
from frigate.genai import get_genai_client
from frigate.genai import GenAIClientManager
from frigate.models import Event, Recordings, ReviewSegment, Trigger
from frigate.util.builtin import serialize
from frigate.util.file import get_event_thumbnail_bytes
@@ -144,7 +144,7 @@ class EmbeddingMaintainer(threading.Thread):
self.frame_manager = SharedMemoryFrameManager()
self.detected_license_plates: dict[str, dict[str, Any]] = {}
self.genai_client = get_genai_client(config)
self.genai_manager = GenAIClientManager(config)
# model runners to share between realtime and post processors
if self.config.lpr.enabled:
@@ -203,12 +203,15 @@ class EmbeddingMaintainer(threading.Thread):
# post processors
self.post_processors: list[PostProcessorApi] = []
if self.genai_client is not None and any(
if self.genai_manager.vision_client is not None and any(
c.review.genai.enabled_in_config for c in self.config.cameras.values()
):
self.post_processors.append(
ReviewDescriptionProcessor(
self.config, self.requestor, self.metrics, self.genai_client
self.config,
self.requestor,
self.metrics,
self.genai_manager.vision_client,
)
)
@@ -246,7 +249,7 @@ class EmbeddingMaintainer(threading.Thread):
)
self.post_processors.append(semantic_trigger_processor)
if self.genai_client is not None and any(
if self.genai_manager.vision_client is not None and any(
c.objects.genai.enabled_in_config for c in self.config.cameras.values()
):
self.post_processors.append(
@@ -255,7 +258,7 @@ class EmbeddingMaintainer(threading.Thread):
self.embeddings,
self.requestor,
self.metrics,
self.genai_client,
self.genai_manager.vision_client,
semantic_trigger_processor,
)
)
@@ -633,7 +636,7 @@ class EmbeddingMaintainer(threading.Thread):
camera, frame_name, _, _, motion_boxes, _ = data
if not camera or len(motion_boxes) == 0 or camera not in self.config.cameras:
if not camera or camera not in self.config.cameras:
return
camera_config = self.config.cameras[camera]
@@ -660,8 +663,10 @@ class EmbeddingMaintainer(threading.Thread):
return
for processor in self.realtime_processors:
if dedicated_lpr_enabled and isinstance(
processor, LicensePlateRealTimeProcessor
if (
dedicated_lpr_enabled
and len(motion_boxes) > 0
and isinstance(processor, LicensePlateRealTimeProcessor)
):
processor.process_frame(camera, yuv_frame, True)

View File

@@ -2,6 +2,7 @@
import logging
import os
import threading
import warnings
from transformers import AutoFeatureExtractor, AutoTokenizer
@@ -54,6 +55,7 @@ class JinaV1TextEmbedding(BaseEmbedding):
self.tokenizer = None
self.feature_extractor = None
self.runner = None
self._lock = threading.Lock()
files_names = list(self.download_urls.keys()) + [self.tokenizer_file]
if not all(
@@ -134,17 +136,18 @@ class JinaV1TextEmbedding(BaseEmbedding):
)
def _preprocess_inputs(self, raw_inputs):
max_length = max(len(self.tokenizer.encode(text)) for text in raw_inputs)
return [
self.tokenizer(
text,
padding="max_length",
truncation=True,
max_length=max_length,
return_tensors="np",
)
for text in raw_inputs
]
with self._lock:
max_length = max(len(self.tokenizer.encode(text)) for text in raw_inputs)
return [
self.tokenizer(
text,
padding="max_length",
truncation=True,
max_length=max_length,
return_tensors="np",
)
for text in raw_inputs
]
class JinaV1ImageEmbedding(BaseEmbedding):
@@ -174,6 +177,7 @@ class JinaV1ImageEmbedding(BaseEmbedding):
self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name)
self.feature_extractor = None
self.runner: BaseModelRunner | None = None
self._lock = threading.Lock()
files_names = list(self.download_urls.keys())
if not all(
os.path.exists(os.path.join(self.download_path, n)) for n in files_names
@@ -216,8 +220,9 @@ class JinaV1ImageEmbedding(BaseEmbedding):
)
def _preprocess_inputs(self, raw_inputs):
processed_images = [self._process_image(img) for img in raw_inputs]
return [
self.feature_extractor(images=image, return_tensors="np")
for image in processed_images
]
with self._lock:
processed_images = [self._process_image(img) for img in raw_inputs]
return [
self.feature_extractor(images=image, return_tensors="np")
for image in processed_images
]

View File

@@ -6,6 +6,7 @@ from typing import Dict
from frigate.comms.events_updater import EventEndPublisher, EventUpdateSubscriber
from frigate.config import FrigateConfig
from frigate.config.classification import ObjectClassificationType
from frigate.events.types import EventStateEnum, EventTypeEnum
from frigate.models import Event
from frigate.util.builtin import to_relative_box
@@ -15,6 +16,16 @@ logger = logging.getLogger(__name__)
def should_update_db(prev_event: Event, current_event: Event) -> bool:
"""If current_event has updated fields and (clip or snapshot)."""
# If event is ending and was previously saved, always update to set end_time
# This ensures events are properly ended even when alerts/detections are disabled
# mid-event (which can cause has_clip/has_snapshot to become False)
if (
prev_event["end_time"] is None
and current_event["end_time"] is not None
and (prev_event["has_clip"] or prev_event["has_snapshot"])
):
return True
if current_event["has_clip"] or current_event["has_snapshot"]:
# if this is the first time has_clip or has_snapshot turned true
if not prev_event["has_clip"] and not prev_event["has_snapshot"]:
@@ -237,6 +248,18 @@ class EventProcessor(threading.Thread):
"recognized_license_plate"
][1]
# only overwrite attribute-type custom model fields in the database if they're set
for name, model_config in self.config.classification.custom.items():
if (
model_config.object_config
and model_config.object_config.classification_type
== ObjectClassificationType.attribute
):
value = event_data.get(name)
if value is not None:
event[Event.data][name] = value[0]
event[Event.data][f"{name}_score"] = value[1]
(
Event.insert(event)
.on_conflict(

View File

@@ -12,10 +12,21 @@ from playhouse.shortcuts import model_to_dict
from frigate.config import CameraConfig, FrigateConfig, GenAIConfig, GenAIProviderEnum
from frigate.const import CLIPS_DIR
from frigate.data_processing.post.types import ReviewMetadata
from frigate.genai.manager import GenAIClientManager
from frigate.models import Event
logger = logging.getLogger(__name__)
__all__ = [
"GenAIClient",
"GenAIClientManager",
"GenAIConfig",
"GenAIProviderEnum",
"PROVIDERS",
"load_providers",
"register_genai_provider",
]
PROVIDERS = {}
@@ -69,7 +80,7 @@ class GenAIClient:
return "\n- (No objects detected)"
context_prompt = f"""
Your task is to analyze the sequence of images ({len(thumbnails)} total) taken in chronological order from the perspective of the {review_data["camera"]} security camera.
Your task is to analyze a sequence of images taken in chronological order from a security camera.
## Normal Activity Patterns for This Property
@@ -99,8 +110,8 @@ When forming your description:
## Response Format
Your response MUST be a flat JSON object with:
- `title` (string): A concise, direct title that describes the primary action or event in the sequence, not just what you literally see. Use spatial context when available to make titles more meaningful. When multiple objects/actions are present, prioritize whichever is most prominent or occurs first. Use names from "Objects in Scene" based on what you visually observe. If you see both a name and an unidentified object of the same type but visually observe only one person/object, use ONLY the name. Examples: "Joe walking dog", "Person taking out trash", "Vehicle arriving in driveway", "Joe accessing vehicle", "Person leaving porch for driveway".
- `scene` (string): A narrative description of what happens across the sequence from start to finish, in chronological order. Start by describing how the sequence begins, then describe the progression of events. **Describe all significant movements and actions in the order they occur.** For example, if a vehicle arrives and then a person exits, describe both actions sequentially. **Only describe actions you can actually observe happening in the frames provided.** Do not infer or assume actions that aren't visible (e.g., if you see someone walking but never see them sit, don't say they sat down). Include setting, detected objects, and their observable actions. Avoid speculation or filling in assumed behaviors. Your description should align with and support the threat level you assign.
- `title` (string): A concise, grammatically complete title in the format "[Subject] [action verb] [context]" that matches your scene description. Use names from "Objects in Scene" when you visually observe them.
- `shortSummary` (string): A brief 2-sentence summary of the scene, suitable for notifications. Should capture the key activity and context without full detail. This should be a condensed version of the scene description above.
- `confidence` (float): 0-1 confidence in your analysis. Higher confidence when objects/actions are clearly visible and context is unambiguous. Lower confidence when the sequence is unclear, objects are partially obscured, or context is ambiguous.
- `potential_threat_level` (integer): 0, 1, or 2 as defined in "Normal Activity Patterns for This Property" above. Your threat level must be consistent with your scene description and the guidance above.
@@ -108,7 +119,8 @@ Your response MUST be a flat JSON object with:
## Sequence Details
- Frame 1 = earliest, Frame {len(thumbnails)} = latest
- Camera: {review_data["camera"]}
- Total frames: {len(thumbnails)} (Frame 1 = earliest, Frame {len(thumbnails)} = latest)
- Activity started at {review_data["start"]} and lasted {review_data["duration"]} seconds
- Zones involved: {", ".join(review_data["zones"]) if review_data["zones"] else "None"}
@@ -292,18 +304,63 @@ Guidelines:
"""Get the context window size for this provider in tokens."""
return 4096
def chat_with_tools(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
) -> dict[str, Any]:
"""
Send chat messages to LLM with optional tool definitions.
def get_genai_client(config: FrigateConfig) -> Optional[GenAIClient]:
"""Get the GenAI client."""
if not config.genai.provider:
return None
This method handles conversation-style interactions with the LLM,
including function calling/tool usage capabilities.
load_providers()
provider = PROVIDERS.get(config.genai.provider)
if provider:
return provider(config.genai)
Args:
messages: List of message dictionaries. Each message should have:
- 'role': str - One of 'user', 'assistant', 'system', or 'tool'
- 'content': str - The message content
- 'tool_call_id': Optional[str] - For tool responses, the ID of the tool call
- 'name': Optional[str] - For tool messages, the tool name
tools: Optional list of tool definitions in OpenAI-compatible format.
Each tool should have 'type': 'function' and 'function' with:
- 'name': str - Tool name
- 'description': str - Tool description
- 'parameters': dict - JSON schema for parameters
tool_choice: How the model should handle tools:
- 'auto': Model decides whether to call tools
- 'none': Model must not call tools
- 'required': Model must call at least one tool
- Or a dict specifying a specific tool to call
**kwargs: Additional provider-specific parameters.
return None
Returns:
Dictionary with:
- 'content': Optional[str] - The text response from the LLM, None if tool calls
- 'tool_calls': Optional[List[Dict]] - List of tool calls if LLM wants to call tools.
Each tool call dict has:
- 'id': str - Unique identifier for this tool call
- 'name': str - Tool name to call
- 'arguments': dict - Arguments for the tool call (parsed JSON)
- 'finish_reason': str - Reason generation stopped:
- 'stop': Normal completion
- 'tool_calls': LLM wants to call tools
- 'length': Hit token limit
- 'error': An error occurred
Raises:
NotImplementedError: If the provider doesn't implement this method.
"""
# Base implementation - each provider should override this
logger.warning(
f"{self.__class__.__name__} does not support chat_with_tools. "
"This method should be overridden by the provider implementation."
)
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
def load_providers():

View File

@@ -1,8 +1,9 @@
"""Azure OpenAI Provider for Frigate AI."""
import base64
import json
import logging
from typing import Optional
from typing import Any, Optional
from urllib.parse import parse_qs, urlparse
from openai import AzureOpenAI
@@ -64,6 +65,7 @@ class OpenAIClient(GenAIClient):
},
],
timeout=self.timeout,
**self.genai_config.runtime_options,
)
except Exception as e:
logger.warning("Azure OpenAI returned an error: %s", str(e))
@@ -75,3 +77,93 @@ class OpenAIClient(GenAIClient):
def get_context_size(self) -> int:
"""Get the context window size for Azure OpenAI."""
return 128000
def chat_with_tools(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
) -> dict[str, Any]:
try:
openai_tool_choice = None
if tool_choice:
if tool_choice == "none":
openai_tool_choice = "none"
elif tool_choice == "auto":
openai_tool_choice = "auto"
elif tool_choice == "required":
openai_tool_choice = "required"
request_params = {
"model": self.genai_config.model,
"messages": messages,
"timeout": self.timeout,
}
if tools:
request_params["tools"] = tools
if openai_tool_choice is not None:
request_params["tool_choice"] = openai_tool_choice
result = self.provider.chat.completions.create(**request_params)
if (
result is None
or not hasattr(result, "choices")
or len(result.choices) == 0
):
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
choice = result.choices[0]
message = choice.message
content = message.content.strip() if message.content else None
tool_calls = None
if message.tool_calls:
tool_calls = []
for tool_call in message.tool_calls:
try:
arguments = json.loads(tool_call.function.arguments)
except (json.JSONDecodeError, AttributeError) as e:
logger.warning(
f"Failed to parse tool call arguments: {e}, "
f"tool: {tool_call.function.name if hasattr(tool_call.function, 'name') else 'unknown'}"
)
arguments = {}
tool_calls.append(
{
"id": tool_call.id if hasattr(tool_call, "id") else "",
"name": tool_call.function.name
if hasattr(tool_call.function, "name")
else "",
"arguments": arguments,
}
)
finish_reason = "error"
if hasattr(choice, "finish_reason") and choice.finish_reason:
finish_reason = choice.finish_reason
elif tool_calls:
finish_reason = "tool_calls"
elif content:
finish_reason = "stop"
return {
"content": content,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
except Exception as e:
logger.warning("Azure OpenAI returned an error: %s", str(e))
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}

View File

@@ -1,10 +1,11 @@
"""Gemini Provider for Frigate AI."""
import json
import logging
from typing import Optional
from typing import Any, Optional
import google.generativeai as genai
from google.api_core.exceptions import GoogleAPICallError
from google import genai
from google.genai import errors, types
from frigate.config import GenAIProviderEnum
from frigate.genai import GenAIClient, register_genai_provider
@@ -16,40 +17,58 @@ logger = logging.getLogger(__name__)
class GeminiClient(GenAIClient):
"""Generative AI client for Frigate using Gemini."""
provider: genai.GenerativeModel
provider: genai.Client
def _init_provider(self):
"""Initialize the client."""
genai.configure(api_key=self.genai_config.api_key)
return genai.GenerativeModel(
self.genai_config.model, **self.genai_config.provider_options
# Merge provider_options into HttpOptions
http_options_dict = {
"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):
http_options_dict.update(self.genai_config.provider_options)
return genai.Client(
api_key=self.genai_config.api_key,
http_options=types.HttpOptions(**http_options_dict),
)
def _send(self, prompt: str, images: list[bytes]) -> Optional[str]:
"""Submit a request to Gemini."""
data = [
{
"mime_type": "image/jpeg",
"data": img,
}
for img in images
contents = [
types.Part.from_bytes(data=img, mime_type="image/jpeg") for img in images
] + [prompt]
try:
response = self.provider.generate_content(
data,
generation_config=genai.types.GenerationConfig(
candidate_count=1,
),
request_options=genai.types.RequestOptions(
timeout=self.timeout,
# Merge runtime_options into generation_config if provided
generation_config_dict = {"candidate_count": 1}
generation_config_dict.update(self.genai_config.runtime_options)
response = self.provider.models.generate_content(
model=self.genai_config.model,
contents=contents,
config=types.GenerateContentConfig(
**generation_config_dict,
),
)
except GoogleAPICallError as e:
except errors.APIError as e:
logger.warning("Gemini returned an error: %s", str(e))
return None
except Exception as e:
logger.warning("An unexpected error occurred with Gemini: %s", str(e))
return None
try:
description = response.text.strip()
except ValueError:
except (ValueError, AttributeError):
# No description was generated
return None
return description
@@ -58,3 +77,188 @@ class GeminiClient(GenAIClient):
"""Get the context window size for Gemini."""
# Gemini Pro Vision has a 1M token context window
return 1000000
def chat_with_tools(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
) -> dict[str, Any]:
try:
if tools:
function_declarations = []
for tool in tools:
if tool.get("type") == "function":
func_def = tool.get("function", {})
function_declarations.append(
genai.protos.FunctionDeclaration(
name=func_def.get("name"),
description=func_def.get("description"),
parameters=genai.protos.Schema(
type=genai.protos.Type.OBJECT,
properties={
prop_name: genai.protos.Schema(
type=_convert_json_type_to_gemini(
prop.get("type")
),
description=prop.get("description"),
)
for prop_name, prop in func_def.get(
"parameters", {}
)
.get("properties", {})
.items()
},
required=func_def.get("parameters", {}).get(
"required", []
),
),
)
)
tool_config = genai.protos.Tool(
function_declarations=function_declarations
)
if tool_choice == "none":
function_calling_config = genai.protos.FunctionCallingConfig(
mode=genai.protos.FunctionCallingConfig.Mode.NONE
)
elif tool_choice == "required":
function_calling_config = genai.protos.FunctionCallingConfig(
mode=genai.protos.FunctionCallingConfig.Mode.ANY
)
else:
function_calling_config = genai.protos.FunctionCallingConfig(
mode=genai.protos.FunctionCallingConfig.Mode.AUTO
)
else:
tool_config = None
function_calling_config = None
contents = []
for msg in messages:
role = msg.get("role")
content = msg.get("content", "")
if role == "system":
continue
elif role == "user":
contents.append({"role": "user", "parts": [content]})
elif role == "assistant":
parts = [content] if content else []
if "tool_calls" in msg:
for tc in msg["tool_calls"]:
parts.append(
genai.protos.FunctionCall(
name=tc["function"]["name"],
args=json.loads(tc["function"]["arguments"]),
)
)
contents.append({"role": "model", "parts": parts})
elif role == "tool":
tool_name = msg.get("name", "")
tool_result = (
json.loads(content) if isinstance(content, str) else content
)
contents.append(
{
"role": "function",
"parts": [
genai.protos.FunctionResponse(
name=tool_name,
response=tool_result,
)
],
}
)
generation_config = genai.types.GenerationConfig(
candidate_count=1,
)
if function_calling_config:
generation_config.function_calling_config = function_calling_config
response = self.provider.generate_content(
contents,
tools=[tool_config] if tool_config else None,
generation_config=generation_config,
request_options=genai.types.RequestOptions(timeout=self.timeout),
)
content = None
tool_calls = None
if response.candidates and response.candidates[0].content:
parts = response.candidates[0].content.parts
text_parts = [p.text for p in parts if hasattr(p, "text") and p.text]
if text_parts:
content = " ".join(text_parts).strip()
function_calls = [
p.function_call
for p in parts
if hasattr(p, "function_call") and p.function_call
]
if function_calls:
tool_calls = []
for fc in function_calls:
tool_calls.append(
{
"id": f"call_{hash(fc.name)}",
"name": fc.name,
"arguments": dict(fc.args)
if hasattr(fc, "args")
else {},
}
)
finish_reason = "error"
if response.candidates:
finish_reason_map = {
genai.types.FinishReason.STOP: "stop",
genai.types.FinishReason.MAX_TOKENS: "length",
genai.types.FinishReason.SAFETY: "stop",
genai.types.FinishReason.RECITATION: "stop",
genai.types.FinishReason.OTHER: "error",
}
finish_reason = finish_reason_map.get(
response.candidates[0].finish_reason, "error"
)
elif tool_calls:
finish_reason = "tool_calls"
elif content:
finish_reason = "stop"
return {
"content": content,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
except GoogleAPICallError as e:
logger.warning("Gemini returned an error: %s", str(e))
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
except Exception as e:
logger.warning("Unexpected error in Gemini chat_with_tools: %s", str(e))
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
def _convert_json_type_to_gemini(json_type: str) -> genai.protos.Type:
type_map = {
"string": genai.protos.Type.STRING,
"integer": genai.protos.Type.INTEGER,
"number": genai.protos.Type.NUMBER,
"boolean": genai.protos.Type.BOOLEAN,
"array": genai.protos.Type.ARRAY,
"object": genai.protos.Type.OBJECT,
}
return type_map.get(json_type, genai.protos.Type.STRING)

238
frigate/genai/llama_cpp.py Normal file
View File

@@ -0,0 +1,238 @@
"""llama.cpp Provider for Frigate AI."""
import base64
import json
import logging
from typing import Any, Optional
import requests
from frigate.config import GenAIProviderEnum
from frigate.genai import GenAIClient, register_genai_provider
logger = logging.getLogger(__name__)
@register_genai_provider(GenAIProviderEnum.llamacpp)
class LlamaCppClient(GenAIClient):
"""Generative AI client for Frigate using llama.cpp server."""
LOCAL_OPTIMIZED_OPTIONS = {
"temperature": 0.7,
"repeat_penalty": 1.05,
"top_p": 0.8,
}
provider: str # base_url
provider_options: dict[str, Any]
def _init_provider(self):
"""Initialize the client."""
self.provider_options = {
**self.LOCAL_OPTIMIZED_OPTIONS,
**self.genai_config.provider_options,
}
return (
self.genai_config.base_url.rstrip("/")
if self.genai_config.base_url
else None
)
def _send(self, prompt: str, images: list[bytes]) -> Optional[str]:
"""Submit a request to llama.cpp server."""
if self.provider is None:
logger.warning(
"llama.cpp provider has not been initialized, a description will not be generated. Check your llama.cpp configuration."
)
return None
try:
content = []
for image in images:
encoded_image = base64.b64encode(image).decode("utf-8")
content.append(
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{encoded_image}",
},
}
)
content.append(
{
"type": "text",
"text": prompt,
}
)
# Build request payload with llama.cpp native options
payload = {
"messages": [
{
"role": "user",
"content": content,
},
],
**self.provider_options,
}
response = requests.post(
f"{self.provider}/v1/chat/completions",
json=payload,
timeout=self.timeout,
)
response.raise_for_status()
result = response.json()
if (
result is not None
and "choices" in result
and len(result["choices"]) > 0
):
choice = result["choices"][0]
if "message" in choice and "content" in choice["message"]:
return choice["message"]["content"].strip()
return None
except Exception as e:
logger.warning("llama.cpp returned an error: %s", str(e))
return None
def get_context_size(self) -> int:
"""Get the context window size for llama.cpp."""
return self.genai_config.provider_options.get("context_size", 4096)
def chat_with_tools(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
) -> dict[str, Any]:
"""
Send chat messages to llama.cpp server with optional tool definitions.
Uses the OpenAI-compatible endpoint but passes through all native llama.cpp
parameters (like slot_id, temperature, etc.) via provider_options.
"""
if self.provider is None:
logger.warning(
"llama.cpp provider has not been initialized. Check your llama.cpp configuration."
)
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
try:
openai_tool_choice = None
if tool_choice:
if tool_choice == "none":
openai_tool_choice = "none"
elif tool_choice == "auto":
openai_tool_choice = "auto"
elif tool_choice == "required":
openai_tool_choice = "required"
payload = {
"messages": messages,
}
if tools:
payload["tools"] = tools
if openai_tool_choice is not None:
payload["tool_choice"] = openai_tool_choice
provider_opts = {
k: v for k, v in self.provider_options.items() if k != "context_size"
}
payload.update(provider_opts)
response = requests.post(
f"{self.provider}/v1/chat/completions",
json=payload,
timeout=self.timeout,
)
response.raise_for_status()
result = response.json()
if result is None or "choices" not in result or len(result["choices"]) == 0:
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
choice = result["choices"][0]
message = choice.get("message", {})
content = message.get("content")
if content:
content = content.strip()
else:
content = None
tool_calls = None
if "tool_calls" in message and message["tool_calls"]:
tool_calls = []
for tool_call in message["tool_calls"]:
try:
function_data = tool_call.get("function", {})
arguments_str = function_data.get("arguments", "{}")
arguments = json.loads(arguments_str)
except (json.JSONDecodeError, KeyError, TypeError) as e:
logger.warning(
f"Failed to parse tool call arguments: {e}, "
f"tool: {function_data.get('name', 'unknown')}"
)
arguments = {}
tool_calls.append(
{
"id": tool_call.get("id", ""),
"name": function_data.get("name", ""),
"arguments": arguments,
}
)
finish_reason = "error"
if "finish_reason" in choice and choice["finish_reason"]:
finish_reason = choice["finish_reason"]
elif tool_calls:
finish_reason = "tool_calls"
elif content:
finish_reason = "stop"
return {
"content": content,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
except requests.exceptions.Timeout as e:
logger.warning("llama.cpp request timed out: %s", str(e))
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
except requests.exceptions.RequestException as e:
error_detail = str(e)
if hasattr(e, "response") and e.response is not None:
try:
error_body = e.response.text
error_detail = f"{str(e)} - Response: {error_body[:500]}"
except Exception:
pass
logger.warning("llama.cpp returned an error: %s", error_detail)
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
except Exception as e:
logger.warning("Unexpected error in llama.cpp chat_with_tools: %s", str(e))
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}

89
frigate/genai/manager.py Normal file
View File

@@ -0,0 +1,89 @@
"""GenAI client manager for Frigate.
Manages GenAI provider clients from Frigate config. Configuration is read only
in _update_config(); no other code should read config.genai. Exposes clients
by role: tool_client, vision_client, embeddings_client.
"""
import logging
from typing import TYPE_CHECKING, Optional
from frigate.config import FrigateConfig
from frigate.config.camera.genai import GenAIRoleEnum
if TYPE_CHECKING:
from frigate.genai import GenAIClient
logger = logging.getLogger(__name__)
class GenAIClientManager:
"""Manages GenAI provider clients from Frigate config."""
def __init__(self, config: FrigateConfig) -> None:
self._config = config
self._tool_client: Optional[GenAIClient] = None
self._vision_client: Optional[GenAIClient] = None
self._embeddings_client: Optional[GenAIClient] = None
self._update_config()
def _update_config(self) -> None:
"""Build role clients from current Frigate config.genai.
Called from __init__ and can be called again when config is reloaded.
Each role (tools, vision, embeddings) gets the client for the provider
that has that role in its roles list.
"""
from frigate.genai import PROVIDERS, load_providers
self._tool_client = None
self._vision_client = None
self._embeddings_client = None
if not self._config.genai:
return
load_providers()
for _name, genai_cfg in self._config.genai.items():
if not genai_cfg.provider:
continue
provider_cls = PROVIDERS.get(genai_cfg.provider)
if not provider_cls:
logger.warning(
"Unknown GenAI provider %s in config, skipping.",
genai_cfg.provider,
)
continue
try:
client = provider_cls(genai_cfg)
except Exception as e:
logger.exception(
"Failed to create GenAI client for provider %s: %s",
genai_cfg.provider,
e,
)
continue
for role in genai_cfg.roles:
if role == GenAIRoleEnum.tools:
self._tool_client = client
elif role == GenAIRoleEnum.vision:
self._vision_client = client
elif role == GenAIRoleEnum.embeddings:
self._embeddings_client = client
@property
def tool_client(self) -> "Optional[GenAIClient]":
"""Client configured for the tools role (e.g. chat with function calling)."""
return self._tool_client
@property
def vision_client(self) -> "Optional[GenAIClient]":
"""Client configured for the vision role (e.g. review descriptions, object descriptions)."""
return self._vision_client
@property
def embeddings_client(self) -> "Optional[GenAIClient]":
"""Client configured for the embeddings role."""
return self._embeddings_client

View File

@@ -1,5 +1,6 @@
"""Ollama Provider for Frigate AI."""
import json
import logging
from typing import Any, Optional
@@ -58,11 +59,15 @@ class OllamaClient(GenAIClient):
)
return None
try:
ollama_options = {
**self.provider_options,
**self.genai_config.runtime_options,
}
result = self.provider.generate(
self.genai_config.model,
prompt,
images=images if images else None,
**self.provider_options,
**ollama_options,
)
logger.debug(
f"Ollama tokens used: eval_count={result.get('eval_count')}, prompt_eval_count={result.get('prompt_eval_count')}"
@@ -82,3 +87,120 @@ class OllamaClient(GenAIClient):
return self.genai_config.provider_options.get("options", {}).get(
"num_ctx", 4096
)
def chat_with_tools(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
) -> dict[str, Any]:
if self.provider is None:
logger.warning(
"Ollama provider has not been initialized. Check your Ollama configuration."
)
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
try:
request_messages = []
for msg in messages:
msg_dict = {
"role": msg.get("role"),
"content": msg.get("content", ""),
}
if msg.get("tool_call_id"):
msg_dict["tool_call_id"] = msg["tool_call_id"]
if msg.get("name"):
msg_dict["name"] = msg["name"]
if msg.get("tool_calls"):
msg_dict["tool_calls"] = msg["tool_calls"]
request_messages.append(msg_dict)
request_params = {
"model": self.genai_config.model,
"messages": request_messages,
}
if tools:
request_params["tools"] = tools
if tool_choice:
if tool_choice == "none":
request_params["tool_choice"] = "none"
elif tool_choice == "required":
request_params["tool_choice"] = "required"
elif tool_choice == "auto":
request_params["tool_choice"] = "auto"
request_params.update(self.provider_options)
response = self.provider.chat(**request_params)
if not response or "message" not in response:
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
message = response["message"]
content = (
message.get("content", "").strip() if message.get("content") else None
)
tool_calls = None
if "tool_calls" in message and message["tool_calls"]:
tool_calls = []
for tool_call in message["tool_calls"]:
try:
function_data = tool_call.get("function", {})
arguments_str = function_data.get("arguments", "{}")
arguments = json.loads(arguments_str)
except (json.JSONDecodeError, KeyError, TypeError) as e:
logger.warning(
f"Failed to parse tool call arguments: {e}, "
f"tool: {function_data.get('name', 'unknown')}"
)
arguments = {}
tool_calls.append(
{
"id": tool_call.get("id", ""),
"name": function_data.get("name", ""),
"arguments": arguments,
}
)
finish_reason = "error"
if "done" in response and response["done"]:
if tool_calls:
finish_reason = "tool_calls"
elif content:
finish_reason = "stop"
elif tool_calls:
finish_reason = "tool_calls"
elif content:
finish_reason = "stop"
return {
"content": content,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
except (TimeoutException, ResponseError, ConnectionError) as e:
logger.warning("Ollama returned an error: %s", str(e))
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
except Exception as e:
logger.warning("Unexpected error in Ollama chat_with_tools: %s", str(e))
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}

View File

@@ -1,8 +1,9 @@
"""OpenAI Provider for Frigate AI."""
import base64
import json
import logging
from typing import Optional
from typing import Any, Optional
from httpx import TimeoutException
from openai import OpenAI
@@ -22,9 +23,14 @@ class OpenAIClient(GenAIClient):
def _init_provider(self):
"""Initialize the client."""
return OpenAI(
api_key=self.genai_config.api_key, **self.genai_config.provider_options
)
# Extract context_size from provider_options as it's not a valid OpenAI client parameter
# It will be used in get_context_size() instead
provider_opts = {
k: v
for k, v in self.genai_config.provider_options.items()
if k != "context_size"
}
return OpenAI(api_key=self.genai_config.api_key, **provider_opts)
def _send(self, prompt: str, images: list[bytes]) -> Optional[str]:
"""Submit a request to OpenAI."""
@@ -56,6 +62,7 @@ class OpenAIClient(GenAIClient):
},
],
timeout=self.timeout,
**self.genai_config.runtime_options,
)
if (
result is not None
@@ -73,6 +80,16 @@ class OpenAIClient(GenAIClient):
if self.context_size is not None:
return self.context_size
# First check provider_options for manually specified context size
# This is necessary for llama.cpp and other OpenAI-compatible servers
# that don't expose the configured runtime context size in the API response
if "context_size" in self.genai_config.provider_options:
self.context_size = self.genai_config.provider_options["context_size"]
logger.debug(
f"Using context size {self.context_size} from provider_options for model {self.genai_config.model}"
)
return self.context_size
try:
models = self.provider.models.list()
for model in models.data:
@@ -100,3 +117,113 @@ class OpenAIClient(GenAIClient):
f"Using default context size {self.context_size} for model {self.genai_config.model}"
)
return self.context_size
def chat_with_tools(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
) -> dict[str, Any]:
"""
Send chat messages to OpenAI with optional tool definitions.
Implements function calling/tool usage for OpenAI models.
"""
try:
openai_tool_choice = None
if tool_choice:
if tool_choice == "none":
openai_tool_choice = "none"
elif tool_choice == "auto":
openai_tool_choice = "auto"
elif tool_choice == "required":
openai_tool_choice = "required"
request_params = {
"model": self.genai_config.model,
"messages": messages,
"timeout": self.timeout,
}
if tools:
request_params["tools"] = tools
if openai_tool_choice is not None:
request_params["tool_choice"] = openai_tool_choice
if isinstance(self.genai_config.provider_options, dict):
excluded_options = {"context_size"}
provider_opts = {
k: v
for k, v in self.genai_config.provider_options.items()
if k not in excluded_options
}
request_params.update(provider_opts)
result = self.provider.chat.completions.create(**request_params)
if (
result is None
or not hasattr(result, "choices")
or len(result.choices) == 0
):
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
choice = result.choices[0]
message = choice.message
content = message.content.strip() if message.content else None
tool_calls = None
if message.tool_calls:
tool_calls = []
for tool_call in message.tool_calls:
try:
arguments = json.loads(tool_call.function.arguments)
except (json.JSONDecodeError, AttributeError) as e:
logger.warning(
f"Failed to parse tool call arguments: {e}, "
f"tool: {tool_call.function.name if hasattr(tool_call.function, 'name') else 'unknown'}"
)
arguments = {}
tool_calls.append(
{
"id": tool_call.id if hasattr(tool_call, "id") else "",
"name": tool_call.function.name
if hasattr(tool_call.function, "name")
else "",
"arguments": arguments,
}
)
finish_reason = "error"
if hasattr(choice, "finish_reason") and choice.finish_reason:
finish_reason = choice.finish_reason
elif tool_calls:
finish_reason = "tool_calls"
elif content:
finish_reason = "stop"
return {
"content": content,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
except TimeoutException as e:
logger.warning("OpenAI request timed out: %s", str(e))
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
except Exception as e:
logger.warning("OpenAI returned an error: %s", str(e))
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}

0
frigate/jobs/__init__.py Normal file
View File

21
frigate/jobs/job.py Normal file
View File

@@ -0,0 +1,21 @@
"""Generic base class for long-running background jobs."""
from dataclasses import asdict, dataclass, field
from typing import Any, Optional
@dataclass
class Job:
"""Base class for long-running background jobs."""
id: str = field(default_factory=lambda: __import__("uuid").uuid4().__str__()[:12])
job_type: str = "" # Must be set by subclasses
status: str = "queued" # queued, running, success, failed, cancelled
results: Optional[dict[str, Any]] = None
start_time: Optional[float] = None
end_time: Optional[float] = None
error_message: Optional[str] = None
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for WebSocket transmission."""
return asdict(self)

70
frigate/jobs/manager.py Normal file
View File

@@ -0,0 +1,70 @@
"""Generic job management for long-running background tasks."""
import threading
from typing import Optional
from frigate.jobs.job import Job
from frigate.types import JobStatusTypesEnum
# Global state and locks for enforcing single concurrent job per job type
_job_locks: dict[str, threading.Lock] = {}
_current_jobs: dict[str, Optional[Job]] = {}
# Keep completed jobs for retrieval, keyed by (job_type, job_id)
_completed_jobs: dict[tuple[str, str], Job] = {}
def _get_lock(job_type: str) -> threading.Lock:
"""Get or create a lock for the specified job type."""
if job_type not in _job_locks:
_job_locks[job_type] = threading.Lock()
return _job_locks[job_type]
def set_current_job(job: Job) -> None:
"""Set the current job for a given job type."""
lock = _get_lock(job.job_type)
with lock:
# Store the previous job if it was completed
old_job = _current_jobs.get(job.job_type)
if old_job and old_job.status in (
JobStatusTypesEnum.success,
JobStatusTypesEnum.failed,
JobStatusTypesEnum.cancelled,
):
_completed_jobs[(job.job_type, old_job.id)] = old_job
_current_jobs[job.job_type] = job
def clear_current_job(job_type: str, job_id: Optional[str] = None) -> None:
"""Clear the current job for a given job type, optionally checking the ID."""
lock = _get_lock(job_type)
with lock:
if job_type in _current_jobs:
current = _current_jobs[job_type]
if current is None or (job_id is None or current.id == job_id):
_current_jobs[job_type] = None
def get_current_job(job_type: str) -> Optional[Job]:
"""Get the current running/queued job for a given job type, if any."""
lock = _get_lock(job_type)
with lock:
return _current_jobs.get(job_type)
def get_job_by_id(job_type: str, job_id: str) -> Optional[Job]:
"""Get job by ID. Checks current job first, then completed jobs."""
lock = _get_lock(job_type)
with lock:
# Check if it's the current job
current = _current_jobs.get(job_type)
if current and current.id == job_id:
return current
# Check if it's a completed job
return _completed_jobs.get((job_type, job_id))
def job_is_running(job_type: str) -> bool:
"""Check if a job of the given type is currently running or queued."""
job = get_current_job(job_type)
return job is not None and job.status in ("queued", "running")

135
frigate/jobs/media_sync.py Normal file
View File

@@ -0,0 +1,135 @@
"""Media sync job management with background execution."""
import logging
import threading
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
from frigate.comms.inter_process import InterProcessRequestor
from frigate.const import UPDATE_JOB_STATE
from frigate.jobs.job import Job
from frigate.jobs.manager import (
get_current_job,
get_job_by_id,
job_is_running,
set_current_job,
)
from frigate.types import JobStatusTypesEnum
from frigate.util.media import sync_all_media
logger = logging.getLogger(__name__)
@dataclass
class MediaSyncJob(Job):
"""In-memory job state for media sync operations."""
job_type: str = "media_sync"
dry_run: bool = False
media_types: list[str] = field(default_factory=lambda: ["all"])
force: bool = False
class MediaSyncRunner(threading.Thread):
"""Thread-based runner for media sync jobs."""
def __init__(self, job: MediaSyncJob) -> None:
super().__init__(daemon=True, name="media_sync")
self.job = job
self.requestor = InterProcessRequestor()
def run(self) -> None:
"""Execute the media sync job and broadcast status updates."""
try:
# Update job status to running
self.job.status = JobStatusTypesEnum.running
self.job.start_time = datetime.now().timestamp()
self._broadcast_status()
# Execute sync with provided parameters
logger.debug(
f"Starting media sync job {self.job.id}: "
f"media_types={self.job.media_types}, "
f"dry_run={self.job.dry_run}, "
f"force={self.job.force}"
)
results = sync_all_media(
dry_run=self.job.dry_run,
media_types=self.job.media_types,
force=self.job.force,
)
# Store results and mark as complete
self.job.results = results.to_dict()
self.job.status = JobStatusTypesEnum.success
self.job.end_time = datetime.now().timestamp()
logger.debug(f"Media sync job {self.job.id} completed successfully")
self._broadcast_status()
except Exception as e:
logger.error(f"Media sync job {self.job.id} failed: {e}", exc_info=True)
self.job.status = JobStatusTypesEnum.failed
self.job.error_message = str(e)
self.job.end_time = datetime.now().timestamp()
self._broadcast_status()
finally:
if self.requestor:
self.requestor.stop()
def _broadcast_status(self) -> None:
"""Broadcast job status update via IPC to all WebSocket subscribers."""
try:
self.requestor.send_data(
UPDATE_JOB_STATE,
self.job.to_dict(),
)
except Exception as e:
logger.warning(f"Failed to broadcast media sync status: {e}")
def start_media_sync_job(
dry_run: bool = False,
media_types: Optional[list[str]] = None,
force: bool = False,
) -> Optional[str]:
"""Start a new media sync job if none is currently running.
Returns job ID on success, None if job already running.
"""
# Check if a job is already running
if job_is_running("media_sync"):
current = get_current_job("media_sync")
logger.warning(
f"Media sync job {current.id} is already running. Rejecting new request."
)
return None
# Create and start new job
job = MediaSyncJob(
dry_run=dry_run,
media_types=media_types or ["all"],
force=force,
)
logger.debug(f"Creating new media sync job: {job.id}")
set_current_job(job)
# Start the background runner
runner = MediaSyncRunner(job)
runner.start()
return job.id
def get_current_media_sync_job() -> Optional[MediaSyncJob]:
"""Get the current running/queued media sync job, if any."""
return get_current_job("media_sync")
def get_media_sync_job_by_id(job_id: str) -> Optional[MediaSyncJob]:
"""Get media sync job by ID. Currently only tracks the current job."""
return get_job_by_id("media_sync", job_id)

View File

@@ -26,15 +26,16 @@ LOG_HANDLER.setFormatter(
# filter out norfair warning
LOG_HANDLER.addFilter(
lambda record: not record.getMessage().startswith(
"You are using a scalar distance function"
lambda record: (
not record.getMessage().startswith("You are using a scalar distance function")
)
)
# filter out tflite logging
LOG_HANDLER.addFilter(
lambda record: "Created TensorFlow Lite XNNPACK delegate for CPU."
not in record.getMessage()
lambda record: (
"Created TensorFlow Lite XNNPACK delegate for CPU." not in record.getMessage()
)
)
@@ -89,6 +90,7 @@ def apply_log_levels(default: str, log_levels: dict[str, LogLevel]) -> None:
"ws4py": LogLevel.error,
"PIL": LogLevel.warning,
"numba": LogLevel.warning,
"google_genai.models": LogLevel.warning,
**log_levels,
}

View File

@@ -80,6 +80,14 @@ class Recordings(Model):
regions = IntegerField(null=True)
class ExportCase(Model):
id = CharField(null=False, primary_key=True, max_length=30)
name = CharField(index=True, max_length=100)
description = TextField(null=True)
created_at = DateTimeField()
updated_at = DateTimeField()
class Export(Model):
id = CharField(null=False, primary_key=True, max_length=30)
camera = CharField(index=True, max_length=20)
@@ -88,6 +96,12 @@ class Export(Model):
video_path = CharField(unique=True)
thumb_path = CharField(unique=True)
in_progress = BooleanField()
export_case = ForeignKeyField(
ExportCase,
null=True,
backref="exports",
column_name="export_case_id",
)
class ReviewSegment(Model):

View File

@@ -57,6 +57,51 @@ def get_cache_image_name(camera: str, frame_time: float) -> str:
)
def get_most_recent_preview_frame(camera: str, before: float = None) -> str | None:
"""Get the most recent preview frame for a camera."""
if not os.path.exists(PREVIEW_CACHE_DIR):
return None
try:
# files are named preview_{camera}-{timestamp}.webp
# we want the largest timestamp that is less than or equal to before
preview_files = [
f
for f in os.listdir(PREVIEW_CACHE_DIR)
if f.startswith(f"preview_{camera}-")
and f.endswith(f".{PREVIEW_FRAME_TYPE}")
]
if not preview_files:
return None
# sort by timestamp in descending order
# filenames are like preview_front-1712345678.901234.webp
preview_files.sort(reverse=True)
if before is None:
return os.path.join(PREVIEW_CACHE_DIR, preview_files[0])
for file_name in preview_files:
try:
# Extract timestamp: preview_front-1712345678.901234.webp
# Split by dash and extension
timestamp_part = file_name.split("-")[-1].split(
f".{PREVIEW_FRAME_TYPE}"
)[0]
timestamp = float(timestamp_part)
if timestamp <= before:
return os.path.join(PREVIEW_CACHE_DIR, file_name)
except (ValueError, IndexError):
continue
return None
except Exception as e:
logger.error(f"Error searching for most recent preview frame: {e}")
return None
class FFMpegConverter(threading.Thread):
"""Convert a list of still frames into a vfr mp4."""

View File

@@ -13,9 +13,8 @@ from playhouse.sqlite_ext import SqliteExtDatabase
from frigate.config import CameraConfig, FrigateConfig, RetainModeEnum
from frigate.const import CACHE_DIR, CLIPS_DIR, MAX_WAL_SIZE, RECORD_DIR
from frigate.models import Previews, Recordings, ReviewSegment, UserReviewStatus
from frigate.record.util import remove_empty_directories, sync_recordings
from frigate.util.builtin import clear_and_unlink
from frigate.util.time import get_tomorrow_at_time
from frigate.util.media import remove_empty_directories
logger = logging.getLogger(__name__)
@@ -61,7 +60,7 @@ class RecordingCleanup(threading.Thread):
db.execute_sql("PRAGMA wal_checkpoint(TRUNCATE);")
db.close()
def expire_review_segments(self, config: CameraConfig, now: datetime) -> None:
def expire_review_segments(self, config: CameraConfig, now: datetime) -> set[Path]:
"""Delete review segments that are expired"""
alert_expire_date = (
now - datetime.timedelta(days=config.record.alerts.retain.days)
@@ -85,9 +84,12 @@ class RecordingCleanup(threading.Thread):
.namedtuples()
)
maybe_empty_dirs = set()
thumbs_to_delete = list(map(lambda x: x[1], expired_reviews))
for thumb_path in thumbs_to_delete:
Path(thumb_path).unlink(missing_ok=True)
thumb_path = Path(thumb_path)
thumb_path.unlink(missing_ok=True)
maybe_empty_dirs.add(thumb_path.parent)
max_deletes = 100000
deleted_reviews_list = list(map(lambda x: x[0], expired_reviews))
@@ -100,13 +102,15 @@ class RecordingCleanup(threading.Thread):
<< deleted_reviews_list[i : i + max_deletes]
).execute()
return maybe_empty_dirs
def expire_existing_camera_recordings(
self,
continuous_expire_date: float,
motion_expire_date: float,
config: CameraConfig,
reviews: ReviewSegment,
) -> None:
) -> set[Path]:
"""Delete recordings for existing camera based on retention config."""
# Get the timestamp for cutoff of retained days
@@ -137,6 +141,8 @@ class RecordingCleanup(threading.Thread):
.iterator()
)
maybe_empty_dirs = set()
# loop over recordings and see if they overlap with any non-expired reviews
# TODO: expire segments based on segment stats according to config
review_start = 0
@@ -191,8 +197,10 @@ class RecordingCleanup(threading.Thread):
)
or (mode == RetainModeEnum.active_objects and recording.objects == 0)
):
Path(recording.path).unlink(missing_ok=True)
recording_path = Path(recording.path)
recording_path.unlink(missing_ok=True)
deleted_recordings.add(recording.id)
maybe_empty_dirs.add(recording_path.parent)
else:
kept_recordings.append((recording.start_time, recording.end_time))
@@ -253,8 +261,10 @@ class RecordingCleanup(threading.Thread):
# Delete previews without any relevant recordings
if not keep:
Path(preview.path).unlink(missing_ok=True)
preview_path = Path(preview.path)
preview_path.unlink(missing_ok=True)
deleted_previews.add(preview.id)
maybe_empty_dirs.add(preview_path.parent)
# expire previews
logger.debug(f"Expiring {len(deleted_previews)} previews")
@@ -266,7 +276,9 @@ class RecordingCleanup(threading.Thread):
Previews.id << deleted_previews_list[i : i + max_deletes]
).execute()
def expire_recordings(self) -> None:
return maybe_empty_dirs
def expire_recordings(self) -> set[Path]:
"""Delete recordings based on retention config."""
logger.debug("Start expire recordings.")
logger.debug("Start deleted cameras.")
@@ -291,10 +303,14 @@ class RecordingCleanup(threading.Thread):
.iterator()
)
maybe_empty_dirs = set()
deleted_recordings = set()
for recording in no_camera_recordings:
Path(recording.path).unlink(missing_ok=True)
recording_path = Path(recording.path)
recording_path.unlink(missing_ok=True)
deleted_recordings.add(recording.id)
maybe_empty_dirs.add(recording_path.parent)
logger.debug(f"Expiring {len(deleted_recordings)} recordings")
# delete up to 100,000 at a time
@@ -311,7 +327,7 @@ class RecordingCleanup(threading.Thread):
logger.debug(f"Start camera: {camera}.")
now = datetime.datetime.now()
self.expire_review_segments(config, now)
maybe_empty_dirs |= self.expire_review_segments(config, now)
continuous_expire_date = (
now - datetime.timedelta(days=config.record.continuous.days)
).timestamp()
@@ -341,7 +357,7 @@ class RecordingCleanup(threading.Thread):
.namedtuples()
)
self.expire_existing_camera_recordings(
maybe_empty_dirs |= self.expire_existing_camera_recordings(
continuous_expire_date, motion_expire_date, config, reviews
)
logger.debug(f"End camera: {camera}.")
@@ -349,12 +365,9 @@ class RecordingCleanup(threading.Thread):
logger.debug("End all cameras.")
logger.debug("End expire recordings.")
def run(self) -> None:
# on startup sync recordings with disk if enabled
if self.config.record.sync_recordings:
sync_recordings(limited=False)
next_sync = get_tomorrow_at_time(3)
return maybe_empty_dirs
def run(self) -> None:
# Expire tmp clips every minute, recordings and clean directories every hour.
for counter in itertools.cycle(range(self.config.record.expire_interval)):
if self.stop_event.wait(60):
@@ -363,16 +376,8 @@ class RecordingCleanup(threading.Thread):
self.clean_tmp_previews()
if (
self.config.record.sync_recordings
and datetime.datetime.now().astimezone(datetime.timezone.utc)
> next_sync
):
sync_recordings(limited=True)
next_sync = get_tomorrow_at_time(3)
if counter == 0:
self.clean_tmp_clips()
self.expire_recordings()
remove_empty_directories(RECORD_DIR)
maybe_empty_dirs = self.expire_recordings()
remove_empty_directories(Path(RECORD_DIR), maybe_empty_dirs)
self.truncate_wal()

View File

@@ -33,6 +33,7 @@ from frigate.util.time import is_current_hour
logger = logging.getLogger(__name__)
DEFAULT_TIME_LAPSE_FFMPEG_ARGS = "-vf setpts=0.04*PTS -r 30"
TIMELAPSE_DATA_INPUT_ARGS = "-an -skip_frame nokey"
@@ -40,11 +41,6 @@ def lower_priority():
os.nice(PROCESS_PRIORITY_LOW)
class PlaybackFactorEnum(str, Enum):
realtime = "realtime"
timelapse_25x = "timelapse_25x"
class PlaybackSourceEnum(str, Enum):
recordings = "recordings"
preview = "preview"
@@ -62,8 +58,11 @@ class RecordingExporter(threading.Thread):
image: Optional[str],
start_time: int,
end_time: int,
playback_factor: PlaybackFactorEnum,
playback_source: PlaybackSourceEnum,
export_case_id: Optional[str] = None,
ffmpeg_input_args: Optional[str] = None,
ffmpeg_output_args: Optional[str] = None,
cpu_fallback: bool = False,
) -> None:
super().__init__()
self.config = config
@@ -73,8 +72,11 @@ class RecordingExporter(threading.Thread):
self.user_provided_image = image
self.start_time = start_time
self.end_time = end_time
self.playback_factor = playback_factor
self.playback_source = playback_source
self.export_case_id = export_case_id
self.ffmpeg_input_args = ffmpeg_input_args
self.ffmpeg_output_args = ffmpeg_output_args
self.cpu_fallback = cpu_fallback
# ensure export thumb dir
Path(os.path.join(CLIPS_DIR, "export")).mkdir(exist_ok=True)
@@ -179,9 +181,16 @@ class RecordingExporter(threading.Thread):
return thumb_path
def get_record_export_command(self, video_path: str) -> list[str]:
def get_record_export_command(
self, video_path: str, use_hwaccel: bool = True
) -> list[str]:
# handle case where internal port is a string with ip:port
internal_port = self.config.networking.listen.internal
if type(internal_port) is str:
internal_port = int(internal_port.split(":")[-1])
if (self.end_time - self.start_time) <= MAX_PLAYLIST_SECONDS:
playlist_lines = f"http://127.0.0.1:5000/vod/{self.camera}/start/{self.start_time}/end/{self.end_time}/index.m3u8"
playlist_lines = f"http://127.0.0.1:{internal_port}/vod/{self.camera}/start/{self.start_time}/end/{self.end_time}/index.m3u8"
ffmpeg_input = (
f"-y -protocol_whitelist pipe,file,http,tcp -i {playlist_lines}"
)
@@ -213,25 +222,30 @@ class RecordingExporter(threading.Thread):
for page in range(1, num_pages + 1):
playlist = export_recordings.paginate(page, page_size)
playlist_lines.append(
f"file 'http://127.0.0.1:5000/vod/{self.camera}/start/{float(playlist[0].start_time)}/end/{float(playlist[-1].end_time)}/index.m3u8'"
f"file 'http://127.0.0.1:{internal_port}/vod/{self.camera}/start/{float(playlist[0].start_time)}/end/{float(playlist[-1].end_time)}/index.m3u8'"
)
ffmpeg_input = "-y -protocol_whitelist pipe,file,http,tcp -f concat -safe 0 -i /dev/stdin"
if self.playback_factor == PlaybackFactorEnum.realtime:
ffmpeg_cmd = (
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} -c copy -movflags +faststart"
).split(" ")
elif self.playback_factor == PlaybackFactorEnum.timelapse_25x:
if self.ffmpeg_input_args is not None and self.ffmpeg_output_args is not None:
hwaccel_args = (
self.config.cameras[self.camera].record.export.hwaccel_args
if use_hwaccel
else None
)
ffmpeg_cmd = (
parse_preset_hardware_acceleration_encode(
self.config.ffmpeg.ffmpeg_path,
self.config.ffmpeg.hwaccel_args,
f"-an {ffmpeg_input}",
f"{self.config.cameras[self.camera].record.export.timelapse_args} -movflags +faststart",
hwaccel_args,
f"{self.ffmpeg_input_args} -an {ffmpeg_input}".strip(),
f"{self.ffmpeg_output_args} -movflags +faststart".strip(),
EncodeTypeEnum.timelapse,
)
).split(" ")
else:
ffmpeg_cmd = (
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} -c copy -movflags +faststart"
).split(" ")
# add metadata
title = f"Frigate Recording for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}"
@@ -241,7 +255,9 @@ class RecordingExporter(threading.Thread):
return ffmpeg_cmd, playlist_lines
def get_preview_export_command(self, video_path: str) -> list[str]:
def get_preview_export_command(
self, video_path: str, use_hwaccel: bool = True
) -> list[str]:
playlist_lines = []
codec = "-c copy"
@@ -309,20 +325,25 @@ class RecordingExporter(threading.Thread):
"-y -protocol_whitelist pipe,file,tcp -f concat -safe 0 -i /dev/stdin"
)
if self.playback_factor == PlaybackFactorEnum.realtime:
ffmpeg_cmd = (
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart {video_path}"
).split(" ")
elif self.playback_factor == PlaybackFactorEnum.timelapse_25x:
if self.ffmpeg_input_args is not None and self.ffmpeg_output_args is not None:
hwaccel_args = (
self.config.cameras[self.camera].record.export.hwaccel_args
if use_hwaccel
else None
)
ffmpeg_cmd = (
parse_preset_hardware_acceleration_encode(
self.config.ffmpeg.ffmpeg_path,
self.config.ffmpeg.hwaccel_args,
f"{TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}",
f"{self.config.cameras[self.camera].record.export.timelapse_args} -movflags +faststart {video_path}",
hwaccel_args,
f"{self.ffmpeg_input_args} {TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}".strip(),
f"{self.ffmpeg_output_args} -movflags +faststart {video_path}".strip(),
EncodeTypeEnum.timelapse,
)
).split(" ")
else:
ffmpeg_cmd = (
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart {video_path}"
).split(" ")
# add metadata
title = f"Frigate Preview for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}"
@@ -348,17 +369,20 @@ class RecordingExporter(threading.Thread):
video_path = f"{EXPORT_DIR}/{self.camera}_{filename_start_datetime}-{filename_end_datetime}_{cleaned_export_id}.mp4"
thumb_path = self.save_thumbnail(self.export_id)
Export.insert(
{
Export.id: self.export_id,
Export.camera: self.camera,
Export.name: export_name,
Export.date: self.start_time,
Export.video_path: video_path,
Export.thumb_path: thumb_path,
Export.in_progress: True,
}
).execute()
export_values = {
Export.id: self.export_id,
Export.camera: self.camera,
Export.name: export_name,
Export.date: self.start_time,
Export.video_path: video_path,
Export.thumb_path: thumb_path,
Export.in_progress: True,
}
if self.export_case_id is not None:
export_values[Export.export_case] = self.export_case_id
Export.insert(export_values).execute()
try:
if self.playback_source == PlaybackSourceEnum.recordings:
@@ -376,6 +400,34 @@ class RecordingExporter(threading.Thread):
capture_output=True,
)
# If export failed and cpu_fallback is enabled, retry without hwaccel
if (
p.returncode != 0
and self.cpu_fallback
and self.ffmpeg_input_args is not None
and self.ffmpeg_output_args is not None
):
logger.warning(
f"Export with hardware acceleration failed, retrying without hwaccel for {self.export_id}"
)
if self.playback_source == PlaybackSourceEnum.recordings:
ffmpeg_cmd, playlist_lines = self.get_record_export_command(
video_path, use_hwaccel=False
)
else:
ffmpeg_cmd, playlist_lines = self.get_preview_export_command(
video_path, use_hwaccel=False
)
p = sp.run(
ffmpeg_cmd,
input="\n".join(playlist_lines),
encoding="ascii",
preexec_fn=lower_priority,
capture_output=True,
)
if p.returncode != 0:
logger.error(
f"Failed to export {self.playback_source.value} for command {' '.join(ffmpeg_cmd)}"

View File

@@ -97,6 +97,7 @@ class RecordingMaintainer(threading.Thread):
self.object_recordings_info: dict[str, list] = defaultdict(list)
self.audio_recordings_info: dict[str, list] = defaultdict(list)
self.end_time_cache: dict[str, Tuple[datetime.datetime, float]] = {}
self.unexpected_cache_files_logged: bool = False
async def move_files(self) -> None:
cache_files = [
@@ -112,7 +113,14 @@ class RecordingMaintainer(threading.Thread):
for cache in cache_files:
cache_path = os.path.join(CACHE_DIR, cache)
basename = os.path.splitext(cache)[0]
camera, date = basename.rsplit("@", maxsplit=1)
try:
camera, date = basename.rsplit("@", maxsplit=1)
except ValueError:
if not self.unexpected_cache_files_logged:
logger.warning("Skipping unexpected files in cache")
self.unexpected_cache_files_logged = True
continue
start_time = datetime.datetime.strptime(
date, CACHE_SEGMENT_FORMAT
).astimezone(datetime.timezone.utc)
@@ -164,7 +172,13 @@ class RecordingMaintainer(threading.Thread):
cache_path = os.path.join(CACHE_DIR, cache)
basename = os.path.splitext(cache)[0]
camera, date = basename.rsplit("@", maxsplit=1)
try:
camera, date = basename.rsplit("@", maxsplit=1)
except ValueError:
if not self.unexpected_cache_files_logged:
logger.warning("Skipping unexpected files in cache")
self.unexpected_cache_files_logged = True
continue
# important that start_time is utc because recordings are stored and compared in utc
start_time = datetime.datetime.strptime(
@@ -194,8 +208,10 @@ class RecordingMaintainer(threading.Thread):
processed_segment_count = len(
list(
filter(
lambda r: r["start_time"].timestamp()
< most_recently_processed_frame_time,
lambda r: (
r["start_time"].timestamp()
< most_recently_processed_frame_time
),
grouped_recordings[camera],
)
)

View File

@@ -1,147 +0,0 @@
"""Recordings Utilities."""
import datetime
import logging
import os
from peewee import DatabaseError, chunked
from frigate.const import RECORD_DIR
from frigate.models import Recordings, RecordingsToDelete
logger = logging.getLogger(__name__)
def remove_empty_directories(directory: str) -> None:
# list all directories recursively and sort them by path,
# longest first
paths = sorted(
[x[0] for x in os.walk(directory)],
key=lambda p: len(str(p)),
reverse=True,
)
for path in paths:
# don't delete the parent
if path == directory:
continue
if len(os.listdir(path)) == 0:
os.rmdir(path)
def sync_recordings(limited: bool) -> None:
"""Check the db for stale recordings entries that don't exist in the filesystem."""
def delete_db_entries_without_file(check_timestamp: float) -> bool:
"""Delete db entries where file was deleted outside of frigate."""
if limited:
recordings = Recordings.select(Recordings.id, Recordings.path).where(
Recordings.start_time >= check_timestamp
)
else:
# get all recordings in the db
recordings = Recordings.select(Recordings.id, Recordings.path)
# Use pagination to process records in chunks
page_size = 1000
num_pages = (recordings.count() + page_size - 1) // page_size
recordings_to_delete = set()
for page in range(num_pages):
for recording in recordings.paginate(page, page_size):
if not os.path.exists(recording.path):
recordings_to_delete.add(recording.id)
if len(recordings_to_delete) == 0:
return True
logger.info(
f"Deleting {len(recordings_to_delete)} recording DB entries with missing files"
)
# convert back to list of dictionaries for insertion
recordings_to_delete = [
{"id": recording_id} for recording_id in recordings_to_delete
]
if float(len(recordings_to_delete)) / max(1, recordings.count()) > 0.5:
logger.warning(
f"Deleting {(len(recordings_to_delete) / max(1, recordings.count()) * 100):.2f}% of recordings DB entries, could be due to configuration error. Aborting..."
)
return False
# create a temporary table for deletion
RecordingsToDelete.create_table(temporary=True)
# insert ids to the temporary table
max_inserts = 1000
for batch in chunked(recordings_to_delete, max_inserts):
RecordingsToDelete.insert_many(batch).execute()
try:
# delete records in the main table that exist in the temporary table
query = Recordings.delete().where(
Recordings.id.in_(RecordingsToDelete.select(RecordingsToDelete.id))
)
query.execute()
except DatabaseError as e:
logger.error(f"Database error during recordings db cleanup: {e}")
return True
def delete_files_without_db_entry(files_on_disk: list[str]):
"""Delete files where file is not inside frigate db."""
files_to_delete = []
for file in files_on_disk:
if not Recordings.select().where(Recordings.path == file).exists():
files_to_delete.append(file)
if len(files_to_delete) == 0:
return True
logger.info(
f"Deleting {len(files_to_delete)} recordings files with missing DB entries"
)
if float(len(files_to_delete)) / max(1, len(files_on_disk)) > 0.5:
logger.debug(
f"Deleting {(len(files_to_delete) / max(1, len(files_on_disk)) * 100):.2f}% of recordings DB entries, could be due to configuration error. Aborting..."
)
return False
for file in files_to_delete:
os.unlink(file)
return True
logger.debug("Start sync recordings.")
# start checking on the hour 36 hours ago
check_point = datetime.datetime.now().replace(
minute=0, second=0, microsecond=0
).astimezone(datetime.timezone.utc) - datetime.timedelta(hours=36)
db_success = delete_db_entries_without_file(check_point.timestamp())
# only try to cleanup files if db cleanup was successful
if db_success:
if limited:
# get recording files from last 36 hours
hour_check = f"{RECORD_DIR}/{check_point.strftime('%Y-%m-%d/%H')}"
files_on_disk = {
os.path.join(root, file)
for root, _, files in os.walk(RECORD_DIR)
for file in files
if root > hour_check
}
else:
# get all recordings files on disk and put them in a set
files_on_disk = {
os.path.join(root, file)
for root, _, files in os.walk(RECORD_DIR)
for file in files
}
delete_files_without_db_entry(files_on_disk)
logger.debug("End sync recordings.")

View File

@@ -394,7 +394,11 @@ class ReviewSegmentMaintainer(threading.Thread):
if activity.has_activity_category(SeverityEnum.alert):
# update current time for last alert activity
segment.last_alert_time = frame_time
if (
segment.last_alert_time is None
or frame_time > segment.last_alert_time
):
segment.last_alert_time = frame_time
if segment.severity != SeverityEnum.alert:
# if segment is not alert category but current activity is
@@ -404,7 +408,11 @@ class ReviewSegmentMaintainer(threading.Thread):
should_update_image = True
if activity.has_activity_category(SeverityEnum.detection):
segment.last_detection_time = frame_time
if (
segment.last_detection_time is None
or frame_time > segment.last_detection_time
):
segment.last_detection_time = frame_time
for object in activity.get_all_objects():
# Alert-level objects should always be added (they extend/upgrade the segment)
@@ -695,17 +703,28 @@ class ReviewSegmentMaintainer(threading.Thread):
current_segment.detections[manual_info["event_id"]] = (
manual_info["label"]
)
if (
topic == DetectionTypeEnum.api
and self.config.cameras[camera].review.alerts.enabled
):
current_segment.severity = SeverityEnum.alert
if topic == DetectionTypeEnum.api:
# manual_info["label"] contains 'label: sub_label'
# so split out the label without modifying manual_info
if (
self.config.cameras[camera].review.detections.enabled
and manual_info["label"].split(": ")[0]
in self.config.cameras[camera].review.detections.labels
):
current_segment.last_detection_time = manual_info[
"end_time"
]
elif self.config.cameras[camera].review.alerts.enabled:
current_segment.severity = SeverityEnum.alert
current_segment.last_alert_time = manual_info[
"end_time"
]
elif (
topic == DetectionTypeEnum.lpr
and self.config.cameras[camera].review.detections.enabled
):
current_segment.severity = SeverityEnum.detection
current_segment.last_alert_time = manual_info["end_time"]
current_segment.last_alert_time = manual_info["end_time"]
elif manual_info["state"] == ManualEventState.start:
self.indefinite_events[camera][manual_info["event_id"]] = (
manual_info["label"]
@@ -717,7 +736,18 @@ class ReviewSegmentMaintainer(threading.Thread):
topic == DetectionTypeEnum.api
and self.config.cameras[camera].review.alerts.enabled
):
current_segment.severity = SeverityEnum.alert
# manual_info["label"] contains 'label: sub_label'
# so split out the label without modifying manual_info
if (
not self.config.cameras[
camera
].review.detections.enabled
or manual_info["label"].split(": ")[0]
not in self.config.cameras[
camera
].review.detections.labels
):
current_segment.severity = SeverityEnum.alert
elif (
topic == DetectionTypeEnum.lpr
and self.config.cameras[camera].review.detections.enabled
@@ -789,11 +819,23 @@ class ReviewSegmentMaintainer(threading.Thread):
detections,
)
elif topic == DetectionTypeEnum.api:
if self.config.cameras[camera].review.alerts.enabled:
severity = None
# manual_info["label"] contains 'label: sub_label'
# so split out the label without modifying manual_info
if (
self.config.cameras[camera].review.detections.enabled
and manual_info["label"].split(": ")[0]
in self.config.cameras[camera].review.detections.labels
):
severity = SeverityEnum.detection
elif self.config.cameras[camera].review.alerts.enabled:
severity = SeverityEnum.alert
if severity:
self.active_review_segments[camera] = PendingReviewSegment(
camera,
frame_time,
SeverityEnum.alert,
severity,
{manual_info["event_id"]: manual_info["label"]},
{},
[],
@@ -820,7 +862,7 @@ class ReviewSegmentMaintainer(threading.Thread):
].last_detection_time = manual_info["end_time"]
else:
logger.warning(
f"Manual event API has been called for {camera}, but alerts are disabled. This manual event will not appear as an alert."
f"Manual event API has been called for {camera}, but alerts and detections are disabled. This manual event will not appear as an alert or detection."
)
elif topic == DetectionTypeEnum.lpr:
if self.config.cameras[camera].review.detections.enabled:

View File

@@ -22,6 +22,7 @@ from frigate.util.services import (
get_bandwidth_stats,
get_cpu_stats,
get_fs_type,
get_hailo_temps,
get_intel_gpu_stats,
get_jetson_stats,
get_nvidia_gpu_stats,
@@ -90,9 +91,80 @@ def get_temperatures() -> dict[str, float]:
if temp is not None:
temps[apex] = temp
# Get temperatures for Hailo devices
temps.update(get_hailo_temps())
return temps
def get_detector_temperature(
detector_type: str,
detector_index_by_type: dict[str, int],
) -> Optional[float]:
"""Get temperature for a specific detector based on its type."""
if detector_type == "edgetpu":
# Get temperatures for all attached Corals
base = "/sys/class/apex/"
if os.path.isdir(base):
apex_devices = sorted(os.listdir(base))
index = detector_index_by_type.get("edgetpu", 0)
if index < len(apex_devices):
apex_name = apex_devices[index]
temp = read_temperature(os.path.join(base, apex_name, "temp"))
if temp is not None:
return temp
elif detector_type == "hailo8l":
# Get temperatures for Hailo devices
hailo_temps = get_hailo_temps()
if hailo_temps:
hailo_device_names = sorted(hailo_temps.keys())
index = detector_index_by_type.get("hailo8l", 0)
if index < len(hailo_device_names):
device_name = hailo_device_names[index]
return hailo_temps[device_name]
elif detector_type == "rknn":
# Rockchip temperatures are handled by the GPU / NPU stats
# as there are not detector specific temperatures
pass
return None
def get_detector_stats(
stats_tracking: StatsTrackingTypes,
) -> dict[str, dict[str, Any]]:
"""Get stats for all detectors, including temperatures based on detector type."""
detector_stats: dict[str, dict[str, Any]] = {}
detector_type_indices: dict[str, int] = {}
for name, detector in stats_tracking["detectors"].items():
pid = detector.detect_process.pid if detector.detect_process else None
detector_type = detector.detector_config.type
# Keep track of the index for each detector type to match temperatures correctly
current_index = detector_type_indices.get(detector_type, 0)
detector_type_indices[detector_type] = current_index + 1
detector_stat = {
"inference_speed": round(detector.avg_inference_speed.value * 1000, 2), # type: ignore[attr-defined]
# issue https://github.com/python/typeshed/issues/8799
# from mypy 0.981 onwards
"detection_start": detector.detection_start.value, # type: ignore[attr-defined]
# issue https://github.com/python/typeshed/issues/8799
# from mypy 0.981 onwards
"pid": pid,
}
temp = get_detector_temperature(detector_type, {detector_type: current_index})
if temp is not None:
detector_stat["temperature"] = round(temp, 1)
detector_stats[name] = detector_stat
return detector_stats
def get_processing_stats(
config: FrigateConfig, stats: dict[str, str], hwaccel_errors: list[str]
) -> None:
@@ -173,6 +245,7 @@ async def set_gpu_stats(
"mem": str(round(float(nvidia_usage[i]["mem"]), 2)) + "%",
"enc": str(round(float(nvidia_usage[i]["enc"]), 2)) + "%",
"dec": str(round(float(nvidia_usage[i]["dec"]), 2)) + "%",
"temp": str(nvidia_usage[i]["temp"]),
}
else:
@@ -278,6 +351,32 @@ def stats_snapshot(
if camera_stats.capture_process_pid.value
else None
)
# Calculate connection quality based on current state
# This is computed at stats-collection time so offline cameras
# correctly show as unusable rather than excellent
expected_fps = config.cameras[name].detect.fps
current_fps = camera_stats.camera_fps.value
reconnects = camera_stats.reconnects_last_hour.value
stalls = camera_stats.stalls_last_hour.value
if current_fps < 0.1:
quality_str = "unusable"
elif reconnects == 0 and current_fps >= 0.9 * expected_fps and stalls < 5:
quality_str = "excellent"
elif reconnects <= 2 and current_fps >= 0.6 * expected_fps:
quality_str = "fair"
elif reconnects > 10 or current_fps < 1.0 or stalls > 100:
quality_str = "unusable"
else:
quality_str = "poor"
connection_quality = {
"connection_quality": quality_str,
"expected_fps": expected_fps,
"reconnects_last_hour": reconnects,
"stalls_last_hour": stalls,
}
stats["cameras"][name] = {
"camera_fps": round(camera_stats.camera_fps.value, 2),
"process_fps": round(camera_stats.process_fps.value, 2),
@@ -289,20 +388,10 @@ def stats_snapshot(
"ffmpeg_pid": ffmpeg_pid,
"audio_rms": round(camera_stats.audio_rms.value, 4),
"audio_dBFS": round(camera_stats.audio_dBFS.value, 4),
**connection_quality,
}
stats["detectors"] = {}
for name, detector in stats_tracking["detectors"].items():
pid = detector.detect_process.pid if detector.detect_process else None
stats["detectors"][name] = {
"inference_speed": round(detector.avg_inference_speed.value * 1000, 2), # type: ignore[attr-defined]
# issue https://github.com/python/typeshed/issues/8799
# from mypy 0.981 onwards
"detection_start": detector.detection_start.value, # type: ignore[attr-defined]
# issue https://github.com/python/typeshed/issues/8799
# from mypy 0.981 onwards
"pid": pid,
}
stats["detectors"] = get_detector_stats(stats_tracking)
stats["camera_fps"] = round(total_camera_fps, 2)
stats["process_fps"] = round(total_process_fps, 2)
stats["skipped_fps"] = round(total_skipped_fps, 2)
@@ -388,7 +477,6 @@ def stats_snapshot(
"version": VERSION,
"latest_version": stats_tracking["latest_frigate_version"],
"storage": {},
"temperatures": get_temperatures(),
"last_updated": int(time.time()),
}

View File

@@ -171,8 +171,8 @@ class BaseTestHttp(unittest.TestCase):
def insert_mock_event(
self,
id: str,
start_time: float = datetime.datetime.now().timestamp(),
end_time: float = datetime.datetime.now().timestamp() + 20,
start_time: float | None = None,
end_time: float | None = None,
has_clip: bool = True,
top_score: int = 100,
score: int = 0,
@@ -180,6 +180,11 @@ class BaseTestHttp(unittest.TestCase):
camera: str = "front_door",
) -> Event:
"""Inserts a basic event model with a given id."""
if start_time is None:
start_time = datetime.datetime.now().timestamp()
if end_time is None:
end_time = start_time + 20
return Event.insert(
id=id,
label="Mock",
@@ -229,11 +234,16 @@ class BaseTestHttp(unittest.TestCase):
def insert_mock_recording(
self,
id: str,
start_time: float = datetime.datetime.now().timestamp(),
end_time: float = datetime.datetime.now().timestamp() + 20,
start_time: float | None = None,
end_time: float | None = None,
motion: int = 0,
) -> Event:
"""Inserts a recording model with a given id."""
if start_time is None:
start_time = datetime.datetime.now().timestamp()
if end_time is None:
end_time = start_time + 20
return Recordings.insert(
id=id,
path=id,

View File

@@ -96,16 +96,17 @@ class TestHttpApp(BaseTestHttp):
assert len(events) == 0
def test_get_event_list_limit(self):
now = datetime.now().timestamp()
id = "123456.random"
id2 = "54321.random"
with AuthTestClient(self.app) as client:
super().insert_mock_event(id)
super().insert_mock_event(id, start_time=now + 1)
events = client.get("/events").json()
assert len(events) == 1
assert events[0]["id"] == id
super().insert_mock_event(id2)
super().insert_mock_event(id2, start_time=now)
events = client.get("/events").json()
assert len(events) == 2
@@ -144,7 +145,7 @@ class TestHttpApp(BaseTestHttp):
assert events[0]["id"] == id2
assert events[1]["id"] == id
events = client.get("/events", params={"sort": "score_des"}).json()
events = client.get("/events", params={"sort": "score_desc"}).json()
assert len(events) == 2
assert events[0]["id"] == id
assert events[1]["id"] == id2
@@ -167,6 +168,57 @@ class TestHttpApp(BaseTestHttp):
assert events[0]["id"] == id
assert events[1]["id"] == id2
def test_get_event_list_match_multilingual_attribute(self):
event_id = "123456.zh"
attribute = "中文标签"
with AuthTestClient(self.app) as client:
super().insert_mock_event(event_id, data={"custom_attr": attribute})
events = client.get("/events", params={"attributes": attribute}).json()
assert len(events) == 1
assert events[0]["id"] == event_id
events = client.get(
"/events", params={"attributes": "%E4%B8%AD%E6%96%87%E6%A0%87%E7%AD%BE"}
).json()
assert len(events) == 1
assert events[0]["id"] == event_id
def test_events_search_match_multilingual_attribute(self):
event_id = "123456.zh.search"
attribute = "中文标签"
mock_embeddings = Mock()
mock_embeddings.search_thumbnail.return_value = [(event_id, 0.05)]
self.app.frigate_config.semantic_search.enabled = True
self.app.embeddings = mock_embeddings
with AuthTestClient(self.app) as client:
super().insert_mock_event(event_id, data={"custom_attr": attribute})
events = client.get(
"/events/search",
params={
"search_type": "similarity",
"event_id": event_id,
"attributes": attribute,
},
).json()
assert len(events) == 1
assert events[0]["id"] == event_id
events = client.get(
"/events/search",
params={
"search_type": "similarity",
"event_id": event_id,
"attributes": "%E4%B8%AD%E6%96%87%E6%A0%87%E7%AD%BE",
},
).json()
assert len(events) == 1
assert events[0]["id"] == event_id
def test_get_good_event(self):
id = "123456.random"

View File

@@ -0,0 +1,107 @@
import os
import shutil
from unittest.mock import MagicMock
import cv2
import numpy as np
from frigate.output.preview import PREVIEW_CACHE_DIR, PREVIEW_FRAME_TYPE
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
class TestHttpLatestFrame(BaseTestHttp):
def setUp(self):
super().setUp([])
self.app = super().create_app()
self.app.detected_frames_processor = MagicMock()
if os.path.exists(PREVIEW_CACHE_DIR):
shutil.rmtree(PREVIEW_CACHE_DIR)
os.makedirs(PREVIEW_CACHE_DIR)
def tearDown(self):
if os.path.exists(PREVIEW_CACHE_DIR):
shutil.rmtree(PREVIEW_CACHE_DIR)
super().tearDown()
def test_latest_frame_fallback_to_preview(self):
camera = "front_door"
# 1. Mock frame processor to return None (simulating offline/missing frame)
self.app.detected_frames_processor.get_current_frame.return_value = None
# Return a timestamp that is after our dummy preview frame
self.app.detected_frames_processor.get_current_frame_time.return_value = (
1234567891.0
)
# 2. Create a dummy preview file
dummy_frame = np.zeros((180, 320, 3), np.uint8)
cv2.putText(
dummy_frame,
"PREVIEW",
(50, 50),
cv2.FONT_HERSHEY_SIMPLEX,
1,
(255, 255, 255),
2,
)
preview_path = os.path.join(
PREVIEW_CACHE_DIR, f"preview_{camera}-1234567890.0.{PREVIEW_FRAME_TYPE}"
)
cv2.imwrite(preview_path, dummy_frame)
with AuthTestClient(self.app) as client:
response = client.get(f"/{camera}/latest.webp")
assert response.status_code == 200
assert response.headers.get("X-Frigate-Offline") == "true"
# Verify we got an image (webp)
assert response.headers.get("content-type") == "image/webp"
def test_latest_frame_no_fallback_when_live(self):
camera = "front_door"
# 1. Mock frame processor to return a live frame
dummy_frame = np.zeros((180, 320, 3), np.uint8)
self.app.detected_frames_processor.get_current_frame.return_value = dummy_frame
self.app.detected_frames_processor.get_current_frame_time.return_value = (
2000000000.0 # Way in the future
)
with AuthTestClient(self.app) as client:
response = client.get(f"/{camera}/latest.webp")
assert response.status_code == 200
assert "X-Frigate-Offline" not in response.headers
def test_latest_frame_stale_falls_back_to_preview(self):
camera = "front_door"
# 1. Mock frame processor to return a stale frame
dummy_frame = np.zeros((180, 320, 3), np.uint8)
self.app.detected_frames_processor.get_current_frame.return_value = dummy_frame
# Return a timestamp that is after our dummy preview frame, but way in the past
self.app.detected_frames_processor.get_current_frame_time.return_value = 1000.0
# 2. Create a dummy preview file
preview_path = os.path.join(
PREVIEW_CACHE_DIR, f"preview_{camera}-999.0.{PREVIEW_FRAME_TYPE}"
)
cv2.imwrite(preview_path, dummy_frame)
with AuthTestClient(self.app) as client:
response = client.get(f"/{camera}/latest.webp")
assert response.status_code == 200
assert response.headers.get("X-Frigate-Offline") == "true"
def test_latest_frame_no_preview_found(self):
camera = "front_door"
# 1. Mock frame processor to return None
self.app.detected_frames_processor.get_current_frame.return_value = None
# 2. No preview file created
with AuthTestClient(self.app) as client:
response = client.get(f"/{camera}/latest.webp")
# Should fall back to camera-error.jpg (which might not exist in test env, but let's see)
# If camera-error.jpg is not found, it returns 500 "Unable to get valid frame" in latest_frame
# OR it uses request.app.camera_error_image if already loaded.
# Since we didn't provide camera-error.jpg, it might 500 if glob fails or return 500 if frame is None.
assert response.status_code in [200, 500]
assert "X-Frigate-Offline" not in response.headers

Some files were not shown because too many files have changed in this diff Show More