Compare commits

..
Author SHA1 Message Date
Nicolas Mowen ede06d794d Implement multi-model object detection 2026-07-23 17:51:21 -06:00
78 changed files with 1159 additions and 958 deletions
-1
View File
@@ -82,7 +82,6 @@ frontdoor
fstype fstype
fullchain fullchain
fullscreen fullscreen
gatekeep
genai genai
generativeai generativeai
genpts genpts
@@ -10,11 +10,8 @@ body:
Before submitting, read the [beta documentation][docs]. Before submitting, read the [beta documentation][docs].
By posting here you agree to follow our [AI policy][ai-policy]. Posts that appear to be written by an AI on your behalf may be closed without a response.
[docs]: https://docs-dev.frigate.video/ [docs]: https://docs-dev.frigate.video/
[discussions]: https://github.com/blakeblackshear/frigate/discussions [discussions]: https://github.com/blakeblackshear/frigate/discussions
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
@@ -8,12 +8,9 @@ body:
Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community.
By posting here you agree to follow our [AI policy][ai-policy]. Posts that appear to be written by an AI on your behalf may be closed without a response.
[discussions]: https://www.github.com/blakeblackshear/frigate/discussions [discussions]: https://www.github.com/blakeblackshear/frigate/discussions
[docs]: https://docs.frigate.video [docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724 [faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
@@ -8,12 +8,9 @@ body:
Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community.
By posting here you agree to follow our [AI policy][ai-policy]. Posts that appear to be written by an AI on your behalf may be closed without a response.
[discussions]: https://www.github.com/blakeblackshear/frigate/discussions [discussions]: https://www.github.com/blakeblackshear/frigate/discussions
[docs]: https://docs.frigate.video [docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724 [faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
@@ -8,12 +8,9 @@ body:
Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community.
By posting here you agree to follow our [AI policy][ai-policy]. Posts that appear to be written by an AI on your behalf may be closed without a response.
[discussions]: https://www.github.com/blakeblackshear/frigate/discussions [discussions]: https://www.github.com/blakeblackshear/frigate/discussions
[docs]: https://docs.frigate.video [docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724 [faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
@@ -8,12 +8,9 @@ body:
Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community.
By posting here you agree to follow our [AI policy][ai-policy]. Posts that appear to be written by an AI on your behalf may be closed without a response.
[discussions]: https://www.github.com/blakeblackshear/frigate/discussions [discussions]: https://www.github.com/blakeblackshear/frigate/discussions
[docs]: https://docs.frigate.video [docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724 [faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
@@ -8,12 +8,9 @@ body:
Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community.
By posting here you agree to follow our [AI policy][ai-policy]. Posts that appear to be written by an AI on your behalf may be closed without a response.
[discussions]: https://www.github.com/blakeblackshear/frigate/discussions [discussions]: https://www.github.com/blakeblackshear/frigate/discussions
[docs]: https://docs.frigate.video [docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724 [faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
-3
View File
@@ -10,12 +10,9 @@ body:
**If you are looking for support, start a new discussion and use a support category.** **If you are looking for support, start a new discussion and use a support category.**
By posting here you agree to follow our [AI policy][ai-policy]. Posts that appear to be written by an AI on your behalf may be closed without a response.
[discussions]: https://www.github.com/blakeblackshear/frigate/discussions [discussions]: https://www.github.com/blakeblackshear/frigate/discussions
[docs]: https://docs.frigate.video [docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724 [faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
@@ -12,14 +12,11 @@ body:
**If you are unsure if your issue is actually a bug or not, please submit a support request first.** **If you are unsure if your issue is actually a bug or not, please submit a support request first.**
By posting here you agree to follow our [AI policy][ai-policy]. Posts that appear to be written by an AI on your behalf may be closed without a response.
[discussions]: https://www.github.com/blakeblackshear/frigate/discussions [discussions]: https://www.github.com/blakeblackshear/frigate/discussions
[prs]: https://www.github.com/blakeblackshear/frigate/pulls [prs]: https://www.github.com/blakeblackshear/frigate/pulls
[docs]: https://docs.frigate.video [docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724 [faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai]: https://docs.frigate.video [ai]: https://docs.frigate.video
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Checklist label: Checklist
@@ -7,13 +7,6 @@ assignees: ''
--- ---
<!--
By posting here you agree to follow our AI policy:
https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
Requests that appear to be written by an AI on your behalf may be closed without a response.
-->
**Describe what you are trying to accomplish and why in non technical terms** **Describe what you are trying to accomplish and why in non technical terms**
I want to be able to ... so that I can ... I want to be able to ... so that I can ...
+1 -1
View File
@@ -1,4 +1,4 @@
_Please read the [contributing guidelines](https://github.com/blakeblackshear/frigate/blob/dev/CONTRIBUTING.md) and the [AI policy](https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md) before submitting a PR. Every PR must be read and submitted by a person, and PRs that appear to be unreviewed AI output will be closed without review._ _Please read the [contributing guidelines](https://github.com/blakeblackshear/frigate/blob/dev/CONTRIBUTING.md) before submitting a PR._
## Proposed change ## Proposed change
-126
View File
@@ -1,126 +0,0 @@
# Frigate AI Policy
## TL;DR
- **Use AI tools if they help you.** We do too. This is about what you post, not which tools you use to write it.
- **A person has to read it and send it.** Don't wire a bot or an agent up to post on your behalf.
- **Write your posts yourself.** Your own words, the template filled in, and you answering maintainers rather than your assistant.
- **Don't paste an AI's guess at the cause as though it were a diagnosis.** Tell us what you actually observed.
- **Read your code before you submit it.** Disclose that AI was used, and be ready to explain every line.
- **If we misjudge something you wrote, just say so.** We'll take you at your word.
The rest of this document explains each of these, and why.
## Scope
AI tools are a reality of modern development and we're not opposed to their use. You are responsible for anything you submit, however it was produced, and we are responsible for anything we merge and release. We hold a high bar for both.
This policy applies everywhere this project is discussed: issues, discussions, pull requests, code reviews, and commit comments.
## Why this exists
Frigate is built and supported by a small group of maintainers and a community of volunteers who read every post and review every pull request. Nobody here is paid to do it, and time spent reading a post is time not spent fixing bugs or building features.
We're not opposed to AI tools. We use them too. But content generated by an AI and submitted without review costs a real person real time, and usually gives them less to work with than a few honest sentences would have. That is the problem this policy addresses.
## A person has to be in the loop
Every issue, discussion, comment, and pull request here must be read and submitted by a person. Using an AI tool to help you write is fine. Wiring one up to post on your behalf is not.
Specifically, do not:
- Connect a bot or agent to GitHub that opens issues, discussions, or pull requests without you reading them first
- Post output from a tool you have not read
- Use tooling to file bulk or drive-by contributions across the repository
We will close anything we believe was posted without a person reading it, and we may mark it as spam. Posts that skip the templates are the most common sign of this.
## Issues, discussions, and comments
We do not mind if you use AI tools to help you write. Do not have tools post unreviewed content on your behalf. We may hide any comment we believe to be unreviewed AI output.
Keep posts to what is needed to communicate your point. A long, confidently written, AI-padded post is harder to help with than a short direct one, not easier, and it is usually obvious.
**Describe your actual problem in your own words.** Tell us what you did, what you expected, and what actually happened. That is the information we need, and only you have it.
**Do not paste an AI's guess at the cause as though it were a diagnosis.** It is frequently wrong in ways that send everyone down the wrong path, and it buries the details that would have led to the real answer. We would rather see what you observed than what a model inferred.
**Fill in the template completely.** The templates ask for logs, config, version, and hardware because those are the things needed to help you. An AI cannot supply them for you, and a post missing them cannot be acted on.
**Answer maintainers yourself.** If we ask you a question, we are asking _you_, not your AI assistant. These are the spaces where we build trust and understanding with the community, and that only works if we're talking to each other. Using AI to fix your grammar or clarity is fine, but the substance has to be yours.
This applies to pull request descriptions and review replies as much as it does to bug reports and discussions.
### Quoting AI output
If you want to include something an AI told you, it must be:
- In a quote block, using `>`
- Disclosed as AI output, saying which tool it came from
- Accompanied by your own comment explaining why you think it is relevant
Keep the excerpt short. Do not paste long transcripts.
### Non-native English speakers
AI is genuinely useful for participating in a project that operates in English, and we would rather hear from you through a translation tool than not hear from you at all. Using AI to improve the grammar or clarity of something you wrote yourself is fine.
If you are translating your posts, make sure the translation says what you meant. Including your original text in a `<details>` block helps us verify the translation if something reads oddly, and keeps the thread readable.
## Code contributions
We need to understand your relationship with the code you're submitting. The more AI was involved, the more important it is that you've genuinely reviewed, tested, and understood what it produced.
Because of the long-term maintenance burden every merged change creates, we require a human in the loop who understands the work the AI produced. Pull requests that appear to be unreviewed AI output will be closed without review.
### Requirements when AI is used
If AI is used to generate any portion of the code, contributors must adhere to the following requirements:
1. **Explicitly disclose the manner in which AI was employed.** The PR template asks for this. Be honest, this won't automatically disqualify your PR. We'd rather have an honest disclosure than find out later. Trust matters more than method.
2. **Perform a comprehensive manual review prior to submitting the pull request.** Don't submit code you haven't read carefully and tested locally.
3. **Be prepared to explain every line of code you submitted when asked about it by a maintainer.** If you can't explain why something works the way it does, you're not ready to submit it.
4. **Check for an existing pull request addressing the same change.** If one exists, comment there and work with its author instead of opening a duplicate.
5. **It is strictly prohibited to use AI to write your posts for you** (bug reports, feature requests, pull request descriptions, GitHub discussions, responding to humans, etc.). We need to hear from _you_, not your AI assistant. These are the spaces where we build trust and understanding with contributors, and that only works if we're talking to each other.
### Established contributors
Contributors with a long history of thoughtful, quality contributions to Frigate have earned trust through that track record. The level of scrutiny we apply to AI usage naturally reflects that trust. This isn't a formal exemption, it's just how trust works. If you've been around, we know how you think and how you work. If you're new, we're still getting to know you, and clear disclosure helps build that relationship.
### What this means in practice
We're not trying to gatekeep how you write code. Use whatever tools make you productive. But there's a difference between using AI as a tool to implement something you understand and handing a feature request to an AI and submitting whatever comes back. The former is fine. The latter creates maintenance risk for the project.
Some honest context: when we review a PR, we're not just evaluating whether the code works today. We're evaluating whether we can maintain it, debug it, and extend it long-term, often without the original author's involvement. Code that the author doesn't deeply understand is code that nobody understands, and that's a liability.
One more thing worth saying directly: most maintainers already have access to the same AI tools you do. A PR that's entirely AI-generated, where the author can't explain the design, debug issues independently, or engage substantively in design discussions, doesn't offer something we couldn't produce ourselves. What makes a contribution genuinely valuable is the human judgment and domain understanding behind it, as well as the engagement during review that shapes it into something we can confidently take on long-term.
## Our use of AI
The Frigate documentation site has an "Ask AI" search that answers questions from the docs, and we may use AI tooling to help with triage and project management. Like any automated tooling, it is not always right.
If an AI tool leaves a comment on your contribution, treat it the way you would any other comment. If you think it is wrong, say so, and a brief explanation is enough. Maintainers always have the final say.
## Enforcement
Contributions and posts that do not follow this policy will be closed. Depending on the situation, maintainers may also:
- Hide or delete comments that appear to be unreviewed AI output
- Mark automated content as spam
- Close an issue, discussion, or pull request without further review
- Lock a conversation
- Temporarily or permanently block an account from participating in the project
Repeated violations may result in being blocked from contributing to Frigate.
### When we get it wrong
There is no reliable way to detect this, and we're not going to pretend otherwise. Whether something reads as unreviewed AI output is a judgment call, usually made quickly, by a volunteer with limited time and no way to know for certain. These calls are subjective and we won't always get them right.
If it happens to you, just say so. A short reply telling us you wrote it yourself is enough, and we'll take you at your word and pick the conversation back up. We would much rather occasionally reopen something we misjudged than treat everyone who posts here as a suspect.
We'd ask for some understanding in return. These calls get made quickly because the volume is real, and time spent second-guessing them is time not spent helping the person in the next thread.
## Attribution
Portions of this policy are adapted from the [Open Home Foundation AI Policy](https://developers.home-assistant.io/docs/ai_policy/).
+19 -9
View File
@@ -2,8 +2,6 @@
Thank you for your interest in contributing to Frigate. This document covers the expectations and guidelines for contributions. Please read it before submitting a pull request. Thank you for your interest in contributing to Frigate. This document covers the expectations and guidelines for contributions. Please read it before submitting a pull request.
All participation in this project, including pull requests, issues, and discussions, is covered by our [AI policy](AI_POLICY.md).
## Before you start ## Before you start
### Bugfixes ### Bugfixes
@@ -23,16 +21,28 @@ Before writing code for a new feature:
## AI usage policy ## AI usage policy
AI tools are a reality of modern development and we're not opposed to their use. But we need to understand your relationship with the code you're submitting, and we need to hear from you rather than from your AI assistant. AI tools are a reality of modern development and we're not opposed to their use. But we need to understand your relationship with the code you're submitting. The more AI was involved, the more important it is that you've genuinely reviewed, tested, and understood what it produced.
**Read the [AI policy](AI_POLICY.md) before you open a pull request.** It is short, and it applies to everything you post here. The parts that most often catch people out: ### Requirements when AI is used
- A person has to be in the loop. Don't wire a bot or agent up to open pull requests, issues, or discussions on your behalf. If AI is used to generate any portion of the code, contributors must adhere to the following requirements:
- Disclose how AI was used. The PR template asks for this. Be honest, it won't automatically disqualify your PR.
- Review and test everything you submit, and be prepared to explain every line when asked.
- Don't use AI to write your PR description or your replies to maintainers.
Pull requests that appear to be unreviewed AI output will be closed without review. 1. **Explicitly disclose the manner in which AI was employed.** The PR template asks for this. Be honest — this won't automatically disqualify your PR. We'd rather have an honest disclosure than find out later. Trust matters more than method.
2. **Perform a comprehensive manual review prior to submitting the pull request.** Don't submit code you haven't read carefully and tested locally.
3. **Be prepared to explain every line of code they submitted when asked about it by a maintainer.** If you can't explain why something works the way it does, you're not ready to submit it.
4. **It is strictly prohibited to use AI to write your posts for you** (bug reports, feature requests, pull request descriptions, GitHub discussions, responding to humans, etc.). We need to hear from _you_, not your AI assistant. These are the spaces where we build trust and understanding with contributors, and that only works if we're talking to each other.
### Established contributors
Contributors with a long history of thoughtful, quality contributions to Frigate have earned trust through that track record. The level of scrutiny we apply to AI usage naturally reflects that trust. This isn't a formal exemption — it's just how trust works. If you've been around, we know how you think and how you work. If you're new, we're still getting to know you, and clear disclosure helps build that relationship.
### What this means in practice
We're not trying to gatekeep how you write code. Use whatever tools make you productive. But there's a difference between using AI as a tool to implement something you understand and handing a feature request to an AI and submitting whatever comes back. The former is fine. The latter creates maintenance risk for the project.
Some honest context: when we review a PR, we're not just evaluating whether the code works today. We're evaluating whether we can maintain it, debug it, and extend it long-term — often without the original author's involvement. Code that the author doesn't deeply understand is code that nobody understands, and that's a liability.
One more thing worth saying directly: most maintainers already have access to the same AI tools you do. A PR that's entirely AI-generated — where the author can't explain the design, debug issues independently, or engage substantively in design discussions — doesn't offer something we couldn't produce ourselves. What makes a contribution genuinely valuable is the human judgment and domain understanding behind it, as well as the engagement during review that shapes it into something we can confidently take on long-term.
## Pull request guidelines ## Pull request guidelines
+236 -212
View File
@@ -34,12 +34,13 @@ edgeTPU:
type: edgetpu type: edgetpu
device: usb device: usb
model: models:
model_type: yolo-generic default:
width: 320 # <--- should match the imgsize of the model, typically 320 model_type: yolo-generic
height: 320 # <--- should match the imgsize of the model, typically 320 width: 320 # <--- should match the imgsize of the model, typically 320
path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite height: 320 # <--- should match the imgsize of the model, typically 320
labelmap_path: /config/labels-coco17.txt path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite
labelmap_path: /config/labels-coco17.txt
hailo8l: hailo8l:
title: Hailo-8/Hailo-8L title: Hailo-8/Hailo-8L
models: models:
@@ -67,27 +68,28 @@ hailo8l:
type: hailo8l type: hailo8l
device: PCIe device: PCIe
model: models:
width: 320 default:
height: 320 width: 320
input_tensor: nhwc height: 320
input_pixel_format: rgb input_tensor: nhwc
input_dtype: int input_pixel_format: rgb
model_type: yolo-generic input_dtype: int
labelmap_path: /labelmap/coco-80.txt model_type: yolo-generic
labelmap_path: /labelmap/coco-80.txt
# The detector automatically selects the default model based on your hardware: # The detector automatically selects the default model based on your hardware:
# - For Hailo-8 hardware: YOLOv6n (default: yolov6n.hef) # - For Hailo-8 hardware: YOLOv6n (default: yolov6n.hef)
# - For Hailo-8L hardware: YOLOv6n (default: yolov6n.hef) # - For Hailo-8L hardware: YOLOv6n (default: yolov6n.hef)
# #
# Optionally, you can specify a local model path to override the default. # Optionally, you can specify a local model path to override the default.
# If a local path is provided and the file exists, it will be used instead of downloading. # If a local path is provided and the file exists, it will be used instead of downloading.
# Example: # Example:
# path: /config/model_cache/hailo/yolov6n.hef # path: /config/model_cache/hailo/yolov6n.hef
# #
# You can also override using a custom URL: # You can also override using a custom URL:
# path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/yolov6n.hef # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/yolov6n.hef
# just make sure to give it the write configuration based on the model # just make sure to give it the write configuration based on the model
- key: ssd - key: ssd
label: SSD MobileNet v1 label: SSD MobileNet v1
recommended: false recommended: false
@@ -111,18 +113,19 @@ hailo8l:
type: hailo8l type: hailo8l
device: PCIe device: PCIe
model: models:
width: 300 default:
height: 300 width: 300
input_tensor: nhwc height: 300
input_pixel_format: rgb input_tensor: nhwc
model_type: ssd input_pixel_format: rgb
# Specify the local model path (if available) or URL for SSD MobileNet v1. model_type: ssd
# Example with a local path: # Specify the local model path (if available) or URL for SSD MobileNet v1.
# path: /config/model_cache/h8l_cache/ssd_mobilenet_v1.hef # Example with a local path:
# # path: /config/model_cache/h8l_cache/ssd_mobilenet_v1.hef
# Or override using a custom URL: #
# path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8l/ssd_mobilenet_v1.hef # Or override using a custom URL:
# path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8l/ssd_mobilenet_v1.hef
openvino: openvino:
title: OpenVINO title: OpenVINO
models: models:
@@ -171,14 +174,15 @@ openvino:
type: openvino type: openvino
device: GPU # or NPU device: GPU # or NPU
model: models:
model_type: yolo-generic default:
width: 320 # <--- should match the imgsize set during model export model_type: yolo-generic
height: 320 # <--- should match the imgsize set during model export width: 320 # <--- should match the imgsize set during model export
input_tensor: nchw height: 320 # <--- should match the imgsize set during model export
input_dtype: float input_tensor: nchw
path: /config/model_cache/yolo.onnx # use the filename you generated above input_dtype: float
labelmap_path: /labelmap/coco-80.txt path: /config/model_cache/yolo.onnx # use the filename you generated above
labelmap_path: /labelmap/coco-80.txt
- key: ssd - key: ssd
label: SSDLite MobileNet v2 label: SSDLite MobileNet v2
recommended: false recommended: false
@@ -202,13 +206,14 @@ openvino:
type: openvino type: openvino
device: GPU # Or NPU device: GPU # Or NPU
model: models:
width: 300 default:
height: 300 width: 300
input_tensor: nhwc height: 300
input_pixel_format: bgr input_tensor: nhwc
path: /openvino-model/ssdlite_mobilenet_v2.xml input_pixel_format: bgr
labelmap_path: /openvino-model/coco_91cl_bkgr.txt path: /openvino-model/ssdlite_mobilenet_v2.xml
labelmap_path: /openvino-model/coco_91cl_bkgr.txt
- key: yolo-legacy - key: yolo-legacy
label: YOLO (v3, v4, v7) label: YOLO (v3, v4, v7)
recommended: false recommended: false
@@ -240,14 +245,15 @@ openvino:
type: openvino type: openvino
device: GPU # or NPU device: GPU # or NPU
model: models:
model_type: yolo-generic default:
width: 320 # <--- should match the imgsize set during model export model_type: yolo-generic
height: 320 # <--- should match the imgsize set during model export width: 320 # <--- should match the imgsize set during model export
input_tensor: nchw height: 320 # <--- should match the imgsize set during model export
input_dtype: float input_tensor: nchw
path: /config/model_cache/yolo.onnx # use the filename you generated above input_dtype: float
labelmap_path: /labelmap/coco-80.txt path: /config/model_cache/yolo.onnx # use the filename you generated above
labelmap_path: /labelmap/coco-80.txt
- key: yolonas - key: yolonas
label: YOLO-NAS label: YOLO-NAS
recommended: false recommended: false
@@ -280,14 +286,15 @@ openvino:
type: openvino type: openvino
device: GPU device: GPU
model: models:
model_type: yolonas default:
width: 320 # <--- should match whatever was set in notebook model_type: yolonas
height: 320 # <--- should match whatever was set in notebook width: 320 # <--- should match whatever was set in notebook
input_tensor: nchw height: 320 # <--- should match whatever was set in notebook
input_pixel_format: bgr input_tensor: nchw
path: /config/yolo_nas_s.onnx input_pixel_format: bgr
labelmap_path: /labelmap/coco-80.txt path: /config/yolo_nas_s.onnx
labelmap_path: /labelmap/coco-80.txt
- key: yolox - key: yolox
label: YOLOX label: YOLOX
recommended: false recommended: false
@@ -308,10 +315,11 @@ openvino:
type: openvino type: openvino
device: GPU device: GPU
model: models:
model_type: yolox default:
path: /config/model_cache/yolox.onnx # use the filename you generated above model_type: yolox
labelmap_path: /labelmap/coco-80.txt path: /config/model_cache/yolox.onnx # use the filename you generated above
labelmap_path: /labelmap/coco-80.txt
- key: rfdetr - key: rfdetr
label: RF-DETR label: RF-DETR
recommended: false recommended: false
@@ -350,13 +358,14 @@ openvino:
type: openvino type: openvino
device: GPU device: GPU
model: models:
model_type: rfdetr default:
width: 320 model_type: rfdetr
height: 320 width: 320
input_tensor: nchw height: 320
input_dtype: float input_tensor: nchw
path: /config/model_cache/rfdetr.onnx # use the filename you generated above input_dtype: float
path: /config/model_cache/rfdetr.onnx # use the filename you generated above
- key: dfine - key: dfine
label: D-FINE / DEIMv2 label: D-FINE / DEIMv2
recommended: false recommended: false
@@ -448,14 +457,15 @@ openvino:
type: openvino type: openvino
device: CPU device: CPU
model: models:
model_type: dfine default:
width: 640 model_type: dfine
height: 640 width: 640
input_tensor: nchw height: 640
input_dtype: float input_tensor: nchw
path: /config/model_cache/dfine-s.onnx # use the filename you generated above input_dtype: float
labelmap_path: /labelmap/coco-80.txt path: /config/model_cache/dfine-s.onnx # use the filename you generated above
labelmap_path: /labelmap/coco-80.txt
appleSilicon: appleSilicon:
title: Apple Silicon title: Apple Silicon
models: models:
@@ -504,14 +514,15 @@ appleSilicon:
type: zmq type: zmq
endpoint: tcp://host.docker.internal:5555 endpoint: tcp://host.docker.internal:5555
model: models:
model_type: yolo-generic default:
width: 320 # <--- should match the imgsize set during model export model_type: yolo-generic
height: 320 # <--- should match the imgsize set during model export width: 320 # <--- should match the imgsize set during model export
input_tensor: nchw height: 320 # <--- should match the imgsize set during model export
input_dtype: float input_tensor: nchw
path: /config/model_cache/yolo.onnx # use the filename you generated above input_dtype: float
labelmap_path: /labelmap/coco-80.txt path: /config/model_cache/yolo.onnx # use the filename you generated above
labelmap_path: /labelmap/coco-80.txt
- key: yolo-legacy - key: yolo-legacy
label: YOLO (v3, v4, v7) label: YOLO (v3, v4, v7)
recommended: false recommended: false
@@ -543,14 +554,15 @@ appleSilicon:
type: zmq type: zmq
endpoint: tcp://host.docker.internal:5555 endpoint: tcp://host.docker.internal:5555
model: models:
model_type: yolo-generic default:
width: 320 # <--- should match the imgsize set during model export model_type: yolo-generic
height: 320 # <--- should match the imgsize set during model export width: 320 # <--- should match the imgsize set during model export
input_tensor: nchw height: 320 # <--- should match the imgsize set during model export
input_dtype: float input_tensor: nchw
path: /config/model_cache/yolo.onnx # use the filename you generated above input_dtype: float
labelmap_path: /labelmap/coco-80.txt path: /config/model_cache/yolo.onnx # use the filename you generated above
labelmap_path: /labelmap/coco-80.txt
onnx: onnx:
title: ONNX title: ONNX
models: models:
@@ -598,14 +610,15 @@ onnx:
onnx: onnx:
type: onnx type: onnx
model: models:
model_type: yolo-generic default:
width: 320 # <--- should match the imgsize set during model export model_type: yolo-generic
height: 320 # <--- should match the imgsize set during model export width: 320 # <--- should match the imgsize set during model export
input_tensor: nchw height: 320 # <--- should match the imgsize set during model export
input_dtype: float input_tensor: nchw
path: /config/model_cache/yolo.onnx # use the filename you generated above input_dtype: float
labelmap_path: /labelmap/coco-80.txt path: /config/model_cache/yolo.onnx # use the filename you generated above
labelmap_path: /labelmap/coco-80.txt
- key: rfdetr - key: rfdetr
label: RF-DETR label: RF-DETR
recommended: false recommended: false
@@ -643,13 +656,14 @@ onnx:
onnx: onnx:
type: onnx type: onnx
model: models:
model_type: rfdetr default:
width: 320 model_type: rfdetr
height: 320 width: 320
input_tensor: nchw height: 320
input_dtype: float input_tensor: nchw
path: /config/model_cache/rfdetr.onnx # use the filename you generated above input_dtype: float
path: /config/model_cache/rfdetr.onnx # use the filename you generated above
- key: yolonas - key: yolonas
label: YOLO-NAS label: YOLO-NAS
recommended: false recommended: false
@@ -681,14 +695,15 @@ onnx:
onnx: onnx:
type: onnx type: onnx
model: models:
model_type: yolonas default:
width: 320 # <--- should match whatever was set in notebook model_type: yolonas
height: 320 # <--- should match whatever was set in notebook width: 320 # <--- should match whatever was set in notebook
input_pixel_format: bgr height: 320 # <--- should match whatever was set in notebook
input_tensor: nchw input_pixel_format: bgr
path: /config/yolo_nas_s.onnx input_tensor: nchw
labelmap_path: /labelmap/coco-80.txt path: /config/yolo_nas_s.onnx
labelmap_path: /labelmap/coco-80.txt
- key: yolox - key: yolox
label: YOLOX label: YOLOX
recommended: false recommended: false
@@ -711,14 +726,15 @@ onnx:
onnx: onnx:
type: onnx type: onnx
model: models:
model_type: yolox default:
width: 416 # <--- should match the imgsize set during model export model_type: yolox
height: 416 # <--- should match the imgsize set during model export width: 416 # <--- should match the imgsize set during model export
input_tensor: nchw height: 416 # <--- should match the imgsize set during model export
input_dtype: float_denorm input_tensor: nchw
path: /config/model_cache/yolox_tiny.onnx # use the filename you generated above input_dtype: float_denorm
labelmap_path: /labelmap/coco-80.txt path: /config/model_cache/yolox_tiny.onnx # use the filename you generated above
labelmap_path: /labelmap/coco-80.txt
- key: dfine - key: dfine
label: D-FINE / DEIMv2 label: D-FINE / DEIMv2
recommended: false recommended: false
@@ -809,14 +825,15 @@ onnx:
onnx: onnx:
type: onnx type: onnx
model: models:
model_type: dfine default:
width: 640 model_type: dfine
height: 640 width: 640
input_tensor: nchw height: 640
input_dtype: float input_tensor: nchw
path: /config/model_cache/dfine_m_obj2coco.onnx # use the filename you generated above input_dtype: float
labelmap_path: /labelmap/coco-80.txt path: /config/model_cache/dfine_m_obj2coco.onnx # use the filename you generated above
labelmap_path: /labelmap/coco-80.txt
- key: yolo-legacy - key: yolo-legacy
label: YOLO (v3, v4, v7) label: YOLO (v3, v4, v7)
recommended: false recommended: false
@@ -847,14 +864,15 @@ onnx:
onnx: onnx:
type: onnx type: onnx
model: models:
model_type: yolo-generic default:
width: 320 # <--- should match the imgsize set during model export model_type: yolo-generic
height: 320 # <--- should match the imgsize set during model export width: 320 # <--- should match the imgsize set during model export
input_tensor: nchw height: 320 # <--- should match the imgsize set during model export
input_dtype: float input_tensor: nchw
path: /config/model_cache/yolo.onnx # use the filename you generated above input_dtype: float
labelmap_path: /labelmap/coco-80.txt path: /config/model_cache/yolo.onnx # use the filename you generated above
labelmap_path: /labelmap/coco-80.txt
cpu: cpu:
title: CPU title: CPU
models: models:
@@ -928,18 +946,19 @@ memryx:
type: memryx type: memryx
device: PCIe:0 device: PCIe:0
model: models:
model_type: yolonas default:
width: 320 # (Can be set to 640 for higher resolution) model_type: yolonas
height: 320 # (Can be set to 640 for higher resolution) width: 320 # (Can be set to 640 for higher resolution)
input_tensor: nchw height: 320 # (Can be set to 640 for higher resolution)
input_dtype: float input_tensor: nchw
labelmap_path: /labelmap/coco-80.txt input_dtype: float
# Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. labelmap_path: /labelmap/coco-80.txt
# path: /config/yolonas.zip # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.
# The .zip file must contain: # path: /config/yolonas.zip
# ├── yolonas.dfp (a file ending with .dfp) # The .zip file must contain:
# ── yolonas_post.onnx (optional; only if the model includes a cropped post-processing network) # ── yolonas.dfp (a file ending with .dfp)
# └── yolonas_post.onnx (optional; only if the model includes a cropped post-processing network)
- key: yolov9 - key: yolov9
label: YOLOv9 label: YOLOv9
recommended: false recommended: false
@@ -965,17 +984,18 @@ memryx:
type: memryx type: memryx
device: PCIe:0 device: PCIe:0
model: models:
model_type: yolo-generic default:
width: 320 # (Can be set to 640 for higher resolution) model_type: yolo-generic
height: 320 # (Can be set to 640 for higher resolution) width: 320 # (Can be set to 640 for higher resolution)
input_tensor: nchw height: 320 # (Can be set to 640 for higher resolution)
input_dtype: float input_tensor: nchw
labelmap_path: /labelmap/coco-80.txt input_dtype: float
# Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. labelmap_path: /labelmap/coco-80.txt
# path: /config/yolov9.zip # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.
# The .zip file must contain: # path: /config/yolov9.zip
# ├── yolov9.dfp (a file ending with .dfp) # The .zip file must contain:
# ├── yolov9.dfp (a file ending with .dfp)
- key: yolox - key: yolox
label: YOLOX label: YOLOX
recommended: false recommended: false
@@ -1001,17 +1021,18 @@ memryx:
type: memryx type: memryx
device: PCIe:0 device: PCIe:0
model: models:
model_type: yolox default:
width: 640 model_type: yolox
height: 640 width: 640
input_tensor: nchw height: 640
input_dtype: float_denorm input_tensor: nchw
labelmap_path: /labelmap/coco-80.txt input_dtype: float_denorm
# Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. labelmap_path: /labelmap/coco-80.txt
# path: /config/yolox.zip # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.
# The .zip file must contain: # path: /config/yolox.zip
# ├── yolox.dfp (a file ending with .dfp) # The .zip file must contain:
# ├── yolox.dfp (a file ending with .dfp)
- key: ssd - key: ssd
label: SSDLite MobileNet v2 label: SSDLite MobileNet v2
recommended: false recommended: false
@@ -1037,18 +1058,19 @@ memryx:
type: memryx type: memryx
device: PCIe:0 device: PCIe:0
model: models:
model_type: ssd default:
width: 320 model_type: ssd
height: 320 width: 320
input_tensor: nchw height: 320
input_dtype: float input_tensor: nchw
labelmap_path: /labelmap/coco-80.txt input_dtype: float
# Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. labelmap_path: /labelmap/coco-80.txt
# path: /config/ssdlite_mobilenet.zip # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.
# The .zip file must contain: # path: /config/ssdlite_mobilenet.zip
# ├── ssdlite_mobilenet.dfp (a file ending with .dfp) # The .zip file must contain:
# ── ssdlite_mobilenet_post.onnx (optional; only if the model includes a cropped post-processing network) # ── ssdlite_mobilenet.dfp (a file ending with .dfp)
# └── ssdlite_mobilenet_post.onnx (optional; only if the model includes a cropped post-processing network)
tensorrt: tensorrt:
title: TensorRT title: TensorRT
models: models:
@@ -1087,13 +1109,14 @@ tensorrt:
type: tensorrt type: tensorrt
device: 0 #This is the default, select the first GPU device: 0 #This is the default, select the first GPU
model: models:
path: /config/model_cache/tensorrt/yolov7-320.trt # use the filename you generated above default:
labelmap_path: /labelmap/coco-80.txt path: /config/model_cache/tensorrt/yolov7-320.trt # use the filename you generated above
input_tensor: nchw labelmap_path: /labelmap/coco-80.txt
input_pixel_format: rgb input_tensor: nchw
width: 320 # MUST match the chosen model i.e yolov7-320 -> 320, yolov4-416 -> 416 input_pixel_format: rgb
height: 320 # MUST match the chosen model i.e yolov7-320 -> 320 yolov4-416 -> 416 width: 320 # MUST match the chosen model i.e yolov7-320 -> 320, yolov4-416 -> 416
height: 320 # MUST match the chosen model i.e yolov7-320 -> 320 yolov4-416 -> 416
synaptics: synaptics:
title: Synaptics title: Synaptics
models: models:
@@ -1261,14 +1284,15 @@ axengine:
axengine: axengine:
type: axengine type: axengine
model: models:
path: frigate-yolov9-tiny default:
model_type: yolo-generic path: frigate-yolov9-tiny
width: 320 model_type: yolo-generic
height: 320 width: 320
input_dtype: int height: 320
input_pixel_format: bgr input_dtype: int
labelmap_path: /labelmap/coco-80.txt input_pixel_format: bgr
labelmap_path: /labelmap/coco-80.txt
degirumAiServer: degirumAiServer:
title: DeGirum AI Server title: DeGirum AI Server
models: models:
+46 -36
View File
@@ -157,44 +157,51 @@ auth:
- front_door - front_door
- back_yard - back_yard
# Optional: model modifications # Optional: named object detection models (default: a single model named "default")
# Each entry defines a model; cameras choose which model to use with detect.model.
# Detectors that support multiple models (openvino, onnx, tensorrt, cpu, rknn) run
# one instance per model in use. Detectors that only support a single model
# (edgetpu, hailo8l, memryx, and others) are assigned to models round robin, so at
# least as many of those detectors as models are required when no multi-model
# capable detector is configured.
# NOTE: The default values are for the EdgeTPU detector. # NOTE: The default values are for the EdgeTPU detector.
# Other detectors will require the model config to be set. # Other detectors will require the model config to be set.
model: models:
# Required: path to the model. Frigate+ models use plus://<model_id> (default: automatic based on detector) default:
path: /edgetpu_model.tflite # Required: path to the model. Frigate+ models use plus://<model_id> (default: automatic based on detector)
# Required: path to the labelmap (default: shown below) path: /edgetpu_model.tflite
labelmap_path: /labelmap.txt # Required: path to the labelmap (default: shown below)
# Required: Object detection model input width (default: shown below) labelmap_path: /labelmap.txt
width: 320 # Required: Object detection model input width (default: shown below)
# Required: Object detection model input height (default: shown below) width: 320
height: 320 # Required: Object detection model input height (default: shown below)
# Required: Object detection model input colorspace height: 320
# Valid values are rgb, bgr, or yuv. (default: shown below) # Required: Object detection model input colorspace
input_pixel_format: rgb # Valid values are rgb, bgr, or yuv. (default: shown below)
# Required: Object detection model input tensor format input_pixel_format: rgb
# Valid values are nhwc, nchw, hwnc, or hwcn (default: shown below) # Required: Object detection model input tensor format
input_tensor: nhwc # Valid values are nhwc, nchw, hwnc, or hwcn (default: shown below)
# Optional: Data type of the model input tensor input_tensor: nhwc
# Valid values are float, float_denorm, or int (default: shown below) # Optional: Data type of the model input tensor
input_dtype: int # Valid values are float, float_denorm, or int (default: shown below)
# Required: Object detection model architecture, used by detectors that support more input_dtype: int
# than one model type (openvino, onnx, rknn, memryx, axengine, synaptics, and others) # Required: Object detection model architecture, used by detectors that support more
# Valid values are ssd, yolox, yolonas, yolo-generic, rfdetr, dfine (default: shown below) # than one model type (openvino, onnx, rknn, memryx, axengine, synaptics, and others)
model_type: ssd # Valid values are ssd, yolox, yolonas, yolo-generic, rfdetr, dfine (default: shown below)
# Required: Label name modifications. These are merged into the standard labelmap. model_type: ssd
labelmap: # Required: Label name modifications. These are merged into the standard labelmap.
2: vehicle labelmap:
# Optional: Map of object labels to their attribute labels (default: depends on model) 2: vehicle
attributes_map: # Optional: Map of object labels to their attribute labels (default: depends on model)
person: attributes_map:
- amazon person:
- face - amazon
car: - face
- amazon car:
- fedex - amazon
- license_plate - fedex
- ups - license_plate
- ups
# Optional: Audio Events Configuration # Optional: Audio Events Configuration
# NOTE: Can be overridden at the camera level # NOTE: Can be overridden at the camera level
@@ -302,6 +309,9 @@ ffmpeg:
detect: detect:
# Optional: enables detection for the camera (default: shown below) # Optional: enables detection for the camera (default: shown below)
enabled: False enabled: False
# Optional: name of the model (key under models) used by this camera
# (default: the only defined model, or the model named "default")
model: default
# Optional: width of the frame for the input with the detect role (default: use native stream resolution) # Optional: width of the frame for the input with the detect role (default: use native stream resolution)
width: 1280 width: 1280
# Optional: height of the frame for the input with the detect role (default: use native stream resolution) # Optional: height of the frame for the input with the detect role (default: use native stream resolution)
+17 -15
View File
@@ -192,12 +192,13 @@ Navigate to <NavPath path="Settings > System > Detectors and model" /> and open
```yaml ```yaml
# Optional: model config # Optional: model config
model: models:
path: /path/to/model default:
width: 320 path: /path/to/model
height: 320 width: 320
input_tensor: "nhwc" height: 320
input_pixel_format: "bgr" input_tensor: "nhwc"
input_pixel_format: "bgr"
``` ```
</TabItem> </TabItem>
@@ -214,15 +215,16 @@ If the labelmap is customized then the labels used for alerts will need to be ad
The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. By default, truck is renamed to car because they are often confused. You cannot add new object types, but you can change the names of existing objects in the model. The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. By default, truck is renamed to car because they are often confused. You cannot add new object types, but you can change the names of existing objects in the model.
```yaml ```yaml
model: models:
labelmap: default:
2: vehicle labelmap:
3: vehicle 2: vehicle
5: vehicle 3: vehicle
7: vehicle 5: vehicle
15: animal 7: vehicle
16: animal 15: animal
17: animal 16: animal
17: animal
``` ```
Note that if you rename objects in the labelmap, you will also need to update your `objects -> track` list as well. Note that if you rename objects in the labelmap, you will also need to update your `objects -> track` list as well.
+1 -1
View File
@@ -165,7 +165,7 @@ If available, recommended settings are:
#### Setup via the Add Camera Wizard #### Setup via the Add Camera Wizard
The [Add Camera Wizard](cameras.md#adding-a-camera-with-the-add-camera-wizard) is the recommended way to add a standard Reolink camera. Before starting, make sure [HTTP is enabled](https://support.reolink.com/articles/360003452893-How-to-Access-Reolink-Cameras-NVRs-Home-Hub-Locally-via-Web-Browsers/) in the camera's advanced network settings. The wizard uses the camera's HTTP API to determine its resolution and choose the recommended stream type from the table above. The Add Camera Wizard is the recommended way to add a standard Reolink camera. Before starting, make sure [HTTP is enabled](https://support.reolink.com/articles/360003452893-How-to-Access-Reolink-Cameras-NVRs-Home-Hub-Locally-via-Web-Browsers/) in the camera's advanced network settings. The wizard uses the camera's HTTP API to determine its resolution and choose the recommended stream type from the table above.
1. Click **Add Camera** in <NavPath path="Settings > Global configuration > Camera management" />. 1. Click **Add Camera** in <NavPath path="Settings > Global configuration > Camera management" />.
2. Choose **Manual selection** as the stream detection method and select **Reolink** as the camera brand. 2. Choose **Manual selection** as the stream detection method and select **Reolink** as the camera brand.
+1 -44
View File
@@ -7,49 +7,6 @@ import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem"; import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath"; import NavPath from "@site/src/components/NavPath";
## Adding a camera with the Add Camera Wizard
The Add Camera Wizard is the recommended way to add a camera. Click **Add Camera** in <NavPath path="Settings > Global configuration > Camera management" />. The wizard connects to your camera, tests each stream, and writes the camera's configuration for you, including the [go2rtc](go2rtc.md) restream and the live view stream mapping, so a standard setup needs no hand-written YAML.
### Step 1: Name and connection
Enter a name for the camera along with its host or IP address and credentials, then choose how the wizard should find the camera's streams:
- **Probe camera** queries the camera over ONVIF (the ONVIF port is usually 80 or 8080) and asks it for its stream URLs. Some cameras use a separate ONVIF/service account rather than the device admin user, and some require **Use digest authentication** to be enabled.
- **Manual selection** builds a stream URL from a template for the camera brand you pick (Dahua/Amcrest/EmpireTech, Hikvision/Uniview/Annke, Ubiquiti, Reolink, Axis, TP-Link, or Foscam). Choose **Other** to enter a custom RTSP URL directly. Non-RTSP stream types must be [configured manually](#setting-up-camera-inputs).
The name you enter is lowercased and spaces become underscores. If the result still isn't a valid config key, the wizard generates a safe name and stores what you typed as `friendly_name`.
### Step 2: Probe or snapshot
In probe mode, the wizard reports what the camera returned (manufacturer, model, firmware, profile count, and whether PTZ, presets, and [autotracking](autotracking.md) are supported) along with the RTSP URLs it discovered. Test each candidate to see its resolution, frame rate, and codecs together with a snapshot, then select the one you want to use.
In manual mode, the wizard tests the templated URL and shows the same metadata and snapshot.
If no RTSP URLs are found, the credentials may be wrong or the camera may not support ONVIF. Go back and use manual selection instead.
### Step 3: Stream configuration
Assign [roles](#setting-up-camera-inputs) to the stream, and use **Add Another Stream** to add the camera's other streams, for example a substream for `detect` alongside the main stream for `record`. At least one stream must have the `detect` role before you can continue.
**Reduce connections to camera** routes that input through the go2rtc restream so Frigate and the live view share a single connection to the camera instead of each opening their own. See [restream](restream.md) for more detail.
### Step 4: Validation and testing
Connect each stream to get a live preview, an estimated bandwidth figure, and a list of validation results. The wizard checks for the most common misconfigurations, including:
- A detect resolution that is too high (increased resource usage) or too low for reliable detection, or one it could not probe at all
- A stream marked `record` whose audio codec is not AAC, or that has no audio at all
- A stream marked `audio` that carries no audio stream
- Using a restreamed input for the `record` role
- Brand-specific issues, such as an RTSP stream on a Reolink camera that should use http-flv, or a Dahua/Hikvision substream selected for `detect`
**Use stream compatibility mode** passes the stream through go2rtc's ffmpeg module. Enable it if a stream fails to load after several attempts. Note that this also prevents [two way talk](/configuration/live#two-way-talk) from being detected for that stream.
**Save New Camera** writes the configuration and starts the camera right away. No restart is required.
Other features, including [hardware acceleration](hardware_acceleration_video.md), [two way talk](/configuration/live#two-way-talk), and audio transcoding, is configured after the camera has been added. For camera model specific quirks, see the [camera specific](camera_specific.md) docs.
## Setting Up Camera Inputs ## Setting Up Camera Inputs
Several inputs can be configured for each camera and the role of each input can be mixed and matched based on your needs. This allows you to use a lower resolution stream for object detection, but create recordings from a higher resolution stream, or vice versa. Several inputs can be configured for each camera and the role of each input can be mixed and matched based on your needs. This allows you to use a lower resolution stream for object detection, but create recordings from a higher resolution stream, or vice versa.
@@ -112,7 +69,7 @@ Additional cameras are simply added under the camera configuration section.
<ConfigTabs> <ConfigTabs>
<TabItem value="ui"> <TabItem value="ui">
Navigate to <NavPath path="Settings > Global configuration > Camera management" /> and use the [Add Camera Wizard](#adding-a-camera-with-the-add-camera-wizard) to configure each additional camera. Navigate to <NavPath path="Settings > Global configuration > Camera management" /> and use the add camera button to configure each additional camera.
</TabItem> </TabItem>
<TabItem value="yaml"> <TabItem value="yaml">
+8 -7
View File
@@ -334,13 +334,14 @@ detectors:
type: openvino type: openvino
device: AUTO device: AUTO
model: models:
width: 300 default:
height: 300 width: 300
input_tensor: nhwc height: 300
input_pixel_format: bgr input_tensor: nhwc
path: /openvino-model/ssdlite_mobilenet_v2.xml input_pixel_format: bgr
labelmap_path: /openvino-model/coco_91cl_bkgr.txt path: /openvino-model/ssdlite_mobilenet_v2.xml
labelmap_path: /openvino-model/coco_91cl_bkgr.txt
record: record:
enabled: True enabled: True
+3 -103
View File
@@ -6,7 +6,6 @@ title: Configuring Generative AI
import ConfigTabs from "@site/src/components/ConfigTabs"; import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem"; import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath"; import NavPath from "@site/src/components/NavPath";
import FaqItem from "@site/src/components/FaqItem";
## Configuration ## Configuration
@@ -14,18 +13,6 @@ A Generative AI provider can be configured in the global config, which will make
`genai` is a map of named providers. Each key under `genai` is a name you choose, and its value is that provider's settings: `genai` is a map of named providers. Each key under `genai` is a name you choose, and its value is that provider's settings:
<ConfigTabs>
<TabItem value="ui">
1. Navigate to <NavPath path="Settings > Enrichments > Generative AI" />.
- Click **Add** and enter a **Provider name**. Any name of letters, numbers, hyphens, and underscores is accepted, but it cannot be changed from the UI after the provider is created.
- Set **Provider** to the service you are using (e.g., `ollama`)
- Set **Base URL**, **API key**, and **Model** as required by that provider
- Set **Roles** to the roles this provider should handle.
</TabItem>
<TabItem value="yaml">
```yaml ```yaml
genai: genai:
my_provider: # any name you like my_provider: # any name you like
@@ -38,9 +25,6 @@ genai:
- chat - chat
``` ```
</TabItem>
</ConfigTabs>
The examples on this page all use `my_provider`, but the name is arbitrary and is only used to reference the provider elsewhere in the config (for example, `semantic_search.model`). The examples on this page all use `my_provider`, but the name is arbitrary and is only used to reference the provider elsewhere in the config (for example, `semantic_search.model`).
Each provider handles one or more **roles**: `chat`, `descriptions`, and `embeddings`. A provider handles all three by default, and each role may be assigned to exactly one provider. Define a single provider if you want it to do everything, or split the roles across several providers using the `roles` option. Each provider handles one or more **roles**: `chat`, `descriptions`, and `embeddings`. A provider handles all three by default, and each role may be assigned to exactly one provider. Define a single provider if you want it to do everything, or split the roles across several providers using the `roles` option.
@@ -59,20 +43,15 @@ Running Generative AI models on CPU is not recommended, as high inference times
### Recommended Local Models ### Recommended Local Models
You must use a vision-capable model with Frigate. The following models are recommended for local deployment of the `descriptions` and `chat` roles: You must use a vision-capable model with Frigate. The following models are recommended for local deployment:
| Model | Notes | | Model | Notes |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `qwen3-vl` | Strong visual and situational understanding, enhanced ability to identify smaller objects and interactions with object. | | `qwen3-vl` | Strong visual and situational understanding, enhanced ability to identify smaller objects and interactions with object. |
| `qwen3.6` | Strong situational understanding, but missing DeepStack from qwen3-vl leading to worse performance for identifying objects in people's hand and other small details. | | `qwen3.5` | Strong situational understanding, but missing DeepStack from qwen3-vl leading to worse performance for identifying objects in people's hand and other small details. |
| `qwen3.6` | Strong situational understanding, similar to qwen3-vl |
| `gemma4` | Strong situational understanding, sometimes resorts to more vague terms like 'interacts' instead of assigning a specific action. | | `gemma4` | Strong situational understanding, sometimes resorts to more vague terms like 'interacts' instead of assigning a specific action. |
The `embeddings` role needs a different kind of model. Text queries are matched against the stored image embeddings, so the model must be trained to place images and text into the same vector space. A chat or description model will still return vectors when asked, but those vectors are not trained for retrieval and text searches will return poor matches with no error to indicate why.
| Model | Notes |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `qwen3-vl-embedding` | Multimodal embeddings for [Semantic Search](/configuration/semantic_search#genai-provider). Must be served by llama.cpp started with `--embeddings` and `--mmproj`. |
:::info :::info
Each model is available in multiple parameter sizes (3b, 4b, 8b, etc.). Larger sizes are more capable of complex tasks and understanding of situations, but requires more memory and computational resources. It is recommended to try multiple models and experiment to see which performs best. Each model is available in multiple parameter sizes (3b, 4b, 8b, etc.). Larger sizes are more capable of complex tasks and understanding of situations, but requires more memory and computational resources. It is recommended to try multiple models and experiment to see which performs best.
@@ -437,82 +416,3 @@ genai:
</TabItem> </TabItem>
</ConfigTabs> </ConfigTabs>
## FAQ
<FaqItem id="how-do-i-debug-genai-issues" question="How do I debug GenAI issues?">
Frigate's Generative AI features are configured and enabled separately. [Review descriptions and summaries](/configuration/genai/genai_review) live under `review.genai`, and [object descriptions](/configuration/genai/genai_objects) live under `objects.genai`. Configuring a provider on this page does not enable either feature, and enabling one does not enable the other. Decide which of the two is not working, then work through the steps below.
1. Confirm a provider is available and holds the `descriptions` role.
- Review descriptions, review summaries, and object descriptions all use the provider that has the `descriptions` role assigned in <NavPath path="Settings > Enrichments > Generative AI > Roles" /> (`genai.<provider>.roles`).
- A provider is contacted the first time one of its roles is actually used. A provider holding the `embeddings` role for semantic search is initialized during startup, while a `descriptions` provider is not initialized until the first description is requested, which may be well after boot.
- In <NavPath path="Settings > Enrichments > Generative AI" />, use **Refresh models** next to the model field. It queries the provider for its model list and is a quick way to verify that the base URL, API key, and network path between Frigate and your provider are correct.
2. Confirm the feature you expect is actually enabled.
- Object descriptions are disabled by default. Turn on <NavPath path="Settings > Global configuration > Objects > GenAI object config > Enable GenAI" /> (`objects.genai.enabled`), either globally or per camera. This is the most common reason custom prompts appear to be ignored while review summaries are still being generated.
- Review descriptions are disabled by default. Turn on <NavPath path="Settings > Global configuration > Review > GenAI config > Enable GenAI descriptions" /> (`review.genai.enabled`). Once enabled, alerts are described by default but detections are not, so a detection-only review item will never get a summary unless **Enable GenAI for detections** (`review.genai.detections`) is also on.
3. If object descriptions are never requested, check the filters that skip generation.
- <NavPath path="Settings > Global configuration > Objects > GenAI object config > GenAI objects" /> (`objects.genai.objects`) limits generation to specific labels, and **Required zones** (`objects.genai.required_zones`) requires the object to have entered one of those zones. If either is set and does not match, Frigate skips the request silently.
- Thumbnails are only collected while an object is moving. Objects that go stationary early contribute fewer frames.
- **Use snapshots** (`objects.genai.use_snapshot`) requires snapshots to be enabled for the camera. If the snapshot cannot be read, Frigate logs `Cannot load snapshot for <id>, file not found` and no description is generated.
- **Send on end** (`objects.genai.send_triggers.tracked_object_end`) is on by default. If you have turned it off in favor of **Early GenAI trigger** (`objects.genai.send_triggers.after_significant_updates`), descriptions are only requested once that number of updates is reached.
4. Enable debug logs to see exactly what Frigate is doing. Restart Frigate after this change. The next step also requires a restart, so turn both on at the same time to avoid restarting twice.
```yaml
logger:
default: info
logs:
# highlight-start
frigate.genai: debug
frigate.data_processing.post.object_descriptions: debug
frigate.data_processing.post.review_descriptions: debug
# highlight-end
```
5. Save the exact images and prompts that were sent to your provider.
- Turn on **Save thumbnails** for the feature you are debugging (`review.genai.debug_save_thumbnails` or `objects.genai.debug_save_thumbnails`). Both features write to `/media/frigate/clips/genai-requests/`, and these files are admin-only.
- Review descriptions write `genai-requests/<review_id>/` containing the numbered frames that were sent, plus `prompt.txt` and `response.txt` with the exact prompt and the raw, unparsed model response.
- Review summary reports write `genai-requests/<start_ts>-<end_ts>/prompt.txt` and `response.txt`. No images are involved, since a report summarizes existing review descriptions.
- Object descriptions write `genai-requests/<event_id>/` containing the numbered thumbnails. The prompt for object descriptions is not written to a file, it is only visible in the debug logs from step 4.
- Look at the saved images before blaming the model. If the object is small, blurry, or out of frame, no prompt will fix the result. For object descriptions, consider turning on **Use snapshots** (`objects.genai.use_snapshot`) to send a higher quality image. For review items, consider setting **Review image source** (`review.genai.image_source`) to `recordings` for 480p frames instead of the lower resolution preview frames.
<ConfigTabs>
<TabItem value="ui">
For review descriptions, navigate to <NavPath path="Settings > Global configuration > Review" /> and set **GenAI config > Save thumbnails** to on.
For object descriptions, navigate to <NavPath path="Settings > Global configuration > Objects" />, expand **GenAI object config**, and set **Save thumbnails** to on.
</TabItem>
<TabItem value="yaml">
```yaml
review:
genai:
enabled: true
# highlight-next-line
debug_save_thumbnails: true
objects:
genai:
enabled: true
# highlight-next-line
debug_save_thumbnails: true
```
</TabItem>
</ConfigTabs>
6. Verify the prompt is what you think it is.
- Object description prompts are the ones you control directly. A camera-level <NavPath path="Settings > Camera configuration > Objects > GenAI object config > Caption prompt" /> (`objects.genai.prompt`) overrides the global one, and an entry in **Object prompts** (`objects.genai.object_prompts`) for a label overrides both for that label. Only `{label}`, `{sub_label}`, and `{camera}` are substituted.
- Review description prompts are built by Frigate and request a structured JSON response, so they are not fully replaceable. The parts you control are <NavPath path="Settings > Global configuration > Review > GenAI config > Activity context prompt" /> (`review.genai.activity_context_prompt`) and **Additional concerns** (`review.genai.additional_concerns`). Keep the activity context prompt general, since overly specific rules will sway the model's threat level scoring.
7. If descriptions are generated but the results are poor or inconsistent, look at the model and the context window.
- Empty fields, missing `shortSummary` values, or `Failed to parse review description` errors usually mean the model is not following the requested JSON schema. Smaller models struggle with structured output. Try a larger parameter size or one of the [recommended models](#recommended-local-models).
- Frigate calculates how many frames to send from the context size the provider reports. If your server reports a different value than it is actually running with, frames will be truncated or the request will fail. Pin the value by adding `context_size` under <NavPath path="Settings > Enrichments > Generative AI > Provider options" /> (`genai.<provider>.provider_options`), and for Ollama also confirm `options.num_ctx` there matches the context you have configured.
- Check **Review Description Speed** and **Object Description Speed** in <NavPath path="System metrics > Enrichments" />. If inference takes tens of seconds, requests will queue behind each other and descriptions will appear to stop. For Ollama, review `OLLAMA_NUM_PARALLEL`, `OLLAMA_MAX_QUEUE`, and `OLLAMA_MAX_LOADED_MODELS` so that concurrent requests from Frigate are handled the way you expect.
</FaqItem>
-4
View File
@@ -113,7 +113,3 @@ Many providers also have a public facing chat interface for their models. Downlo
- OpenAI - [ChatGPT](https://chatgpt.com) - OpenAI - [ChatGPT](https://chatgpt.com)
- Gemini - [Google AI Studio](https://aistudio.google.com) - Gemini - [Google AI Studio](https://aistudio.google.com)
- Ollama - [Open WebUI](https://docs.openwebui.com/) - Ollama - [Open WebUI](https://docs.openwebui.com/)
## Troubleshooting
If descriptions are not being generated, or the generated descriptions are not what you expect, see [How do I debug GenAI issues?](/configuration/genai/genai_config#how-do-i-debug-genai-issues).
@@ -201,7 +201,3 @@ Along with individual review item summaries, Generative AI can also produce a si
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. 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. 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.
## Troubleshooting
If summaries are not being generated, or the generated summaries are not what you expect, see [How do I debug GenAI issues?](/configuration/genai/genai_config#how-do-i-debug-genai-issues).
+1 -1
View File
@@ -15,7 +15,7 @@ Frigate uses the bundled go2rtc to power a number of key features:
:::tip[Most users no longer need to configure go2rtc by hand] :::tip[Most users no longer need to configure go2rtc by hand]
The [**camera setup wizard**](cameras.md#adding-a-camera-with-the-add-camera-wizard) is the recommended way to add cameras. Click **Add Camera** in <NavPath path="Settings > Global configuration > Camera management" />, and the wizard probes your camera and writes its configuration for you, including the go2rtc restream and the live stream mapping, so go2rtc is set up automatically. The **camera setup wizard** is the recommended way to add cameras. Click **Add Camera** in <NavPath path="Settings > Global configuration > Camera management" />, and the wizard probes your camera and writes its configuration for you, including the go2rtc restream and the live stream mapping, so go2rtc is set up automatically.
This guide is mainly useful if you are **upgrading from an older version and have existing cameras that don't yet use go2rtc**, or if you want to fine-tune a stream by hand (for example, to transcode a codec your browser can't play). The [go2rtc troubleshooting guide](/troubleshooting/go2rtc) applies regardless of how your cameras were added. This guide is mainly useful if you are **upgrading from an older version and have existing cameras that don't yet use go2rtc**, or if you want to fine-tune a stream by hand (for example, to transcode a codec your browser can't play). The [go2rtc troubleshooting guide](/troubleshooting/go2rtc) applies regardless of how your cameras were added.
+1 -1
View File
@@ -34,7 +34,7 @@ If you are using go2rtc, you should adjust the following settings in your camera
- Video codec: **H.264** - provides the most compatible video codec with all Live view technologies and browsers. Avoid any kind of "smart codec" or "+" codec like _H.264+_ or _H.265+_. as these non-standard codecs remove keyframes (see below). - Video codec: **H.264** - provides the most compatible video codec with all Live view technologies and browsers. Avoid any kind of "smart codec" or "+" codec like _H.264+_ or _H.265+_. as these non-standard codecs remove keyframes (see below).
- Audio codec: **AAC** - provides the most compatible audio codec with all Live view technologies and browsers that support audio. - Audio codec: **AAC** - provides the most compatible audio codec with all Live view technologies and browsers that support audio.
- I-frame interval (sometimes called the keyframe interval, the interframe space, or the GOP length): match your camera's frame rate, or choose "1x" (for interframe space on Reolink cameras). For example, if your stream outputs 20fps, your i-frame interval should be 20 (or 1x on Reolink). Values higher than the frame rate will cause the stream to take longer to begin playback. See [this page](https://web.archive.org/web/20251213190836/https://gardinal.net/understanding-the-keyframe-interval/) for more on keyframes. For many users this may not be an issue, but it should be noted that a 1x i-frame interval will cause more storage utilization if you are using the stream for the `record` role as well. - I-frame interval (sometimes called the keyframe interval, the interframe space, or the GOP length): match your camera's frame rate, or choose "1x" (for interframe space on Reolink cameras). For example, if your stream outputs 20fps, your i-frame interval should be 20 (or 1x on Reolink). Values higher than the frame rate will cause the stream to take longer to begin playback. See [this page](https://gardinal.net/understanding-the-keyframe-interval/) for more on keyframes. For many users this may not be an issue, but it should be noted that a 1x i-frame interval will cause more storage utilization if you are using the stream for the `record` role as well.
The default video and audio codec on your camera may not always be compatible with your browser, which is why setting them to H.264 and AAC is recommended. See the [go2rtc docs](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#codecs-madness) for codec support information. The default video and audio codec on your camera may not always be compatible with your browser, which is why setting them to H.264 and AAC is recommended. See the [go2rtc docs](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#codecs-madness) for codec support information.
+53 -15
View File
@@ -91,6 +91,41 @@ The best detection accuracy comes from a model trained on images that look like
::: :::
### Running multiple models
Models are defined as named entries under `models`, and each camera selects the model it uses with `detect.model`. This makes it possible to run different models for different groups of cameras, for example a dedicated model for indoor cameras, outdoor cameras, or thermal cameras.
```yaml
detectors:
ov:
type: openvino
device: GPU
models:
indoor:
path: /config/model_cache/indoor-model.xml
model_type: yolo-generic
width: 320
height: 320
outdoor:
path: plus://<your_model_id>
cameras:
living_room:
detect:
model: indoor
driveway:
detect:
model: outdoor
```
When only one model is defined, all cameras use it automatically. With multiple models, cameras use the model named `default` unless `detect.model` selects another one; `detect.model` can also be set globally and overridden per camera.
How detectors handle multiple models depends on the hardware:
- **Detectors that support multiple models** (`openvino`, `onnx`, `tensorrt`, `cpu`, `rknn`): a single detector entry is automatically expanded into one instance per model in use. For example, detector `ov` with models `indoor` and `outdoor` runs as `ov_indoor` and `ov_outdoor`, and each instance appears separately in the System Metrics page. Keep in mind that each instance loads its own copy of the model, which increases GPU memory usage.
- **Detectors that only support a single model** (`edgetpu`, `hailo8l`, `memryx`, and other single-session hardware): each detector entry serves exactly one model. Detector entries are assigned to models round robin, so running two models on Coral hardware requires two Corals. If these are the only detectors configured and there are fewer of them than models in use, Frigate will fail to start with an error explaining the options.
# Officially Supported Detectors # Officially Supported Detectors
Frigate provides a number of builtin detector types. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras. Frigate provides a number of builtin detector types. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras.
@@ -789,11 +824,12 @@ You can set it to:
- A path to some model.json. - A path to some model.json.
```yaml ```yaml
model: models:
path: ./mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1 # directory to model .json and file default:
width: 300 # width is in the model name as the first number in the "int"x"int" section path: ./mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1 # directory to model .json and file
height: 300 # height is in the model name as the second number in the "int"x"int" section width: 300 # width is in the model name as the first number in the "int"x"int" section
input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here height: 300 # height is in the model name as the second number in the "int"x"int" section
input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here
``` ```
#### Local Inference #### Local Inference
@@ -809,11 +845,12 @@ It is also possible to eliminate the need for an AI server and run the hardware
Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file. Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file.
```yaml ```yaml
model: models:
path: mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1 default:
width: 300 # width is in the model name as the first number in the "int"x"int" section path: mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1
height: 300 # height is in the model name as the second number in the "int"x"int" section width: 300 # width is in the model name as the first number in the "int"x"int" section
input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here height: 300 # height is in the model name as the second number in the "int"x"int" section
input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here
``` ```
#### AI Hub Cloud Inference #### AI Hub Cloud Inference
@@ -829,11 +866,12 @@ If you do not possess whatever hardware you want to run, there's also the option
Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file. Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file.
```yaml ```yaml
model: models:
path: mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1 default:
width: 300 # width is in the model name as the first number in the "int"x"int" section path: mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1
height: 300 # height is in the model name as the second number in the "int"x"int" section width: 300 # width is in the model name as the first number in the "int"x"int" section
input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here height: 300 # height is in the model name as the second number in the "int"x"int" section
input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here
``` ```
## AXERA ## AXERA
+9 -8
View File
@@ -144,7 +144,7 @@ At this point you should be able to start Frigate and a basic config will be cre
### Step 2: Add a camera ### Step 2: Add a camera
Click the **Add Camera** button in <NavPath path="Settings > Global configuration > Camera management" /> to use the camera setup wizard to get your first camera added into Frigate. See [Adding a camera with the Add Camera Wizard](../configuration/cameras.md#adding-a-camera-with-the-add-camera-wizard) for a walkthrough of each step. Click the **Add Camera** button in <NavPath path="Settings > Global configuration > Camera management" /> to use the camera setup wizard to get your first camera added into Frigate.
### Step 3: Configure hardware acceleration (recommended) ### Step 3: Configure hardware acceleration (recommended)
@@ -228,13 +228,14 @@ detectors: # <---- add detectors
device: GPU device: GPU
# We will use the default MobileNet_v2 model from OpenVINO. # We will use the default MobileNet_v2 model from OpenVINO.
model: models:
width: 300 default:
height: 300 width: 300
input_tensor: nhwc height: 300
input_pixel_format: bgr input_tensor: nhwc
path: /openvino-model/ssdlite_mobilenet_v2.xml input_pixel_format: bgr
labelmap_path: /openvino-model/coco_91cl_bkgr.txt path: /openvino-model/ssdlite_mobilenet_v2.xml
labelmap_path: /openvino-model/coco_91cl_bkgr.txt
cameras: cameras:
name_of_your_camera: name_of_your_camera:
+10 -8
View File
@@ -64,8 +64,9 @@ You can either choose the new model from the <NavPath path="Settings > System >
```yaml ```yaml
detectors: ... detectors: ...
model: models:
path: plus://<your_model_id> default:
path: plus://<your_model_id>
``` ```
:::note :::note
@@ -79,10 +80,11 @@ Models are downloaded into the `/config/model_cache` folder and only downloaded
If needed, you can override the labelmap for Frigate+ models. This is not recommended as renaming labels will break the Submit to Frigate+ feature if the labels are not available in Frigate+. If needed, you can override the labelmap for Frigate+ models. This is not recommended as renaming labels will break the Submit to Frigate+ feature if the labels are not available in Frigate+.
```yaml ```yaml
model: models:
path: plus://<your_model_id> default:
labelmap: path: plus://<your_model_id>
3: animal labelmap:
4: animal 3: animal
5: animal 4: animal
5: animal
``` ```
+3 -2
View File
@@ -38,8 +38,9 @@ Navigate to <NavPath path="Settings > System > Detectors and model" />. In the *
```yaml ```yaml
detectors: ... detectors: ...
model: models:
path: plus://<your_model_id> default:
path: plus://<your_model_id>
``` ```
:::tip :::tip
+3 -7
View File
@@ -34,15 +34,11 @@ The detect FFmpeg process exited on its own. This message is only the notificati
</FaqItem> </FaqItem>
<FaqItem id="non-monotonically-increasing-dts" question="Non-monotonic DTS / non monotonically increasing dts to muxer / Queue input is backward in time"> <FaqItem id="non-monotonically-increasing-dts" question="Application provided invalid, non monotonically increasing dts to muxer">
These are FFmpeg messages indicating the camera sent packets with out-of-order timestamps, either on the video or the audio stream. Timestamp jitter like this is common with WiFi cameras and restreamed or proxied sources; other causes are a camera "Smart Codec" / H.264+ / H.265+ mode or a camera clock that jumps. A sustained flood of these messages usually precedes the stream stalling and the watchdog restarting FFmpeg. An FFmpeg message meaning the camera sent packets with out-of-order timestamps. Because recordings are copied without re-encoding, FFmpeg cannot fix them, and the segment muxer often splits early, producing one-second segments and a cache backlog. The usual cause is a camera "Smart Codec" / H.264+ / H.265+ mode or a camera clock that jumps.
In most cases, the fix is to improve the network, reduce system resource usage, or switch to non-WiFi cameras. In general, WiFi cameras are [not recommended](https://ipcamtalk.com/threads/multiple-cameras-high-bandwidth.77100/#post-861110). See [Recordings: segments are only 1 second long](/troubleshooting/recordings#segments-are-only-1-second-long).
On the video stream, this can affect recordings: because they are copied without re-encoding, FFmpeg cannot fix the timestamps, and the segment muxer often splits early, producing one-second segments and a cache backlog. See [Recordings: segments are only 1 second long](/troubleshooting/recordings#segments-are-only-1-second-long).
On the audio stream, the messages can come from the output's audio encoding. If the audio stream is the problem, it may help to have go2rtc transcode it by adding `#audio=aac` to the camera's go2rtc stream to produce clean timestamps for everything consuming the restream.
</FaqItem> </FaqItem>
+37 -25
View File
@@ -372,31 +372,40 @@ def config(request: Request):
config["go2rtc"]["streams"][stream_name] = cleaned config["go2rtc"]["streams"][stream_name] = cleaned
config["plus"] = {"enabled": request.app.frigate_config.plus_api.is_active()} config["plus"] = {"enabled": request.app.frigate_config.plus_api.is_active()}
config["model"]["colormap"] = config_obj.model.colormap
config["model"]["all_attributes"] = config_obj.model.all_attributes
config["model"]["non_logo_attributes"] = config_obj.model.non_logo_attributes
# Add model plus data if plus is enabled for model_key, model in config_obj.models.items():
if config["plus"]["enabled"]: model_dict = config["models"][model_key]
model_path = config.get("model", {}).get("path") model_dict["colormap"] = model.colormap
if model_path: model_dict["all_attributes"] = model.all_attributes
model_json_path = FilePath(model_path).with_suffix(".json") model_dict["non_logo_attributes"] = model.non_logo_attributes
try:
with open(model_json_path) as f:
model_plus_data = json.load(f)
config["model"]["plus"] = model_plus_data
except FileNotFoundError:
config["model"]["plus"] = None
except json.JSONDecodeError:
config["model"]["plus"] = None
else:
config["model"]["plus"] = None
# use merged labelamp # Add model plus data if plus is enabled
for detector_config in config["detectors"].values(): if config["plus"]["enabled"]:
detector_config["model"]["labelmap"] = ( model_plus_data = None
request.app.frigate_config.model.merged_labelmap
) if model.path:
model_json_path = FilePath(model.path).with_suffix(".json")
try:
with open(model_json_path) as f:
model_plus_data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
model_plus_data = None
model_dict["plus"] = model_plus_data
# legacy single-model block kept for frontend compatibility, remove
# once the UI is fully multi-model aware
default_model_key = (
"default" if "default" in config_obj.models else next(iter(config_obj.models))
)
config["model"] = config["models"][default_model_key]
# use each detector's assigned merged labelmap
for key, detector_config in config["detectors"].items():
if config_obj.detectors[key].model:
detector_config["model"]["labelmap"] = config_obj.detectors[
key
].model.merged_labelmap
return JSONResponse(content=config) return JSONResponse(content=config)
@@ -1323,8 +1332,11 @@ def plusModels(request: Request, filterByCurrentModelDetector: bool = False):
modelList = models["list"] modelList = models["list"]
# current model type # current model type, based on the default model until the UI is
modelType = request.app.frigate_config.model.model_type # fully multi-model aware
config_models = request.app.frigate_config.models
default_model = config_models.get("default") or next(iter(config_models.values()))
modelType = default_model.model_type
# current detectorType for comparing to supportedDetectors # current detectorType for comparing to supportedDetectors
detectorType = list(request.app.frigate_config.detectors.values())[0].type detectorType = list(request.app.frigate_config.detectors.values())[0].type
+1 -1
View File
@@ -813,7 +813,7 @@ async def event_snapshot(
timestamp_style=request.app.frigate_config.cameras[ timestamp_style=request.app.frigate_config.cameras[
event.camera event.camera
].timestamp_style, ].timestamp_style,
colormap=request.app.frigate_config.model.colormap, colormap=request.app.frigate_config.model_for_camera(event.camera).colormap,
) )
except DoesNotExist: except DoesNotExist:
# see if the object is currently being tracked # see if the object is currently being tracked
+19 -17
View File
@@ -97,7 +97,9 @@ class FrigateApp:
self.metrics_manager = manager self.metrics_manager = manager
self.audio_process: mp.Process | None = None self.audio_process: mp.Process | None = None
self.stop_event = stop_event self.stop_event = stop_event
self.detection_queue: Queue = mp.Queue() self.detection_queues: dict[str, Queue] = {
model_key: mp.Queue() for model_key in config.models
}
self.detectors: dict[str, ObjectDetectProcess] = {} self.detectors: dict[str, ObjectDetectProcess] = {}
self.detection_shms: list[mp.shared_memory.SharedMemory] = [] self.detection_shms: list[mp.shared_memory.SharedMemory] = []
self.log_queue: Queue = mp.Queue() self.log_queue: Queue = mp.Queue()
@@ -363,20 +365,14 @@ class FrigateApp:
) )
def start_detectors(self) -> None: def start_detectors(self) -> None:
for name in self.config.cameras.keys(): for name, camera_config in self.config.cameras.items():
camera_model = self.config.models[camera_config.detect.model]
try: try:
largest_frame = max(
[
det.model.height * det.model.width * 3
if det.model is not None
else 320
for det in self.config.detectors.values()
]
)
shm_in = UntrackedSharedMemory( shm_in = UntrackedSharedMemory(
name=name, name=name,
create=True, create=True,
size=largest_frame, size=camera_model.height * camera_model.width * 3,
) )
except FileExistsError: except FileExistsError:
shm_in = UntrackedSharedMemory(name=name) shm_in = UntrackedSharedMemory(name=name)
@@ -391,11 +387,16 @@ class FrigateApp:
self.detection_shms.append(shm_in) self.detection_shms.append(shm_in)
self.detection_shms.append(shm_out) self.detection_shms.append(shm_out)
for name, detector_config in self.config.detectors.items(): for name, detector_config in self.config.detector_instances.items():
cameras_using_model = [
camera_name
for camera_name, camera_config in self.config.cameras.items()
if camera_config.detect.model == detector_config.model_key
]
self.detectors[name] = ObjectDetectProcess( self.detectors[name] = ObjectDetectProcess(
name, name,
self.detection_queue, self.detection_queues[detector_config.model_key],
list(self.config.cameras.keys()), cameras_using_model,
self.config, self.config,
detector_config, detector_config,
self.stop_event, self.stop_event,
@@ -430,7 +431,7 @@ class FrigateApp:
def start_camera_processor(self) -> None: def start_camera_processor(self) -> None:
self.camera_maintainer = CameraMaintainer( self.camera_maintainer = CameraMaintainer(
self.config, self.config,
self.detection_queue, self.detection_queues,
self.detected_frames_queue, self.detected_frames_queue,
self.camera_metrics, self.camera_metrics,
self.ptz_metrics, self.ptz_metrics,
@@ -685,8 +686,9 @@ class FrigateApp:
for detector in self.detectors.values(): for detector in self.detectors.values():
detector.stop() detector.stop()
empty_and_close_queue(self.detection_queue) for detection_queue in self.detection_queues.values():
logger.info("Detection queue closed") empty_and_close_queue(detection_queue)
logger.info("Detection queues closed")
self.detected_frames_processor.join() self.detected_frames_processor.join()
empty_and_close_queue(self.detected_frames_queue) empty_and_close_queue(self.detected_frames_queue)
+1 -1
View File
@@ -178,7 +178,7 @@ class CameraActivityManager:
return return
for label in camera_config.objects.track: for label in camera_config.objects.track:
if label in self.config.model.non_logo_attributes: if label in self.config.model_for_camera(camera).non_logo_attributes:
continue continue
new_count = all_objects[label] new_count = all_objects[label]
+11 -16
View File
@@ -29,7 +29,7 @@ class CameraMaintainer(threading.Thread):
def __init__( def __init__(
self, self,
config: FrigateConfig, config: FrigateConfig,
detection_queue: Queue, detection_queues: dict[str, Queue],
detected_frames_queue: Queue, detected_frames_queue: Queue,
camera_metrics: DictProxy, camera_metrics: DictProxy,
ptz_metrics: dict[str, PTZMetrics], ptz_metrics: dict[str, PTZMetrics],
@@ -38,7 +38,7 @@ class CameraMaintainer(threading.Thread):
): ):
super().__init__(name="camera_processor") super().__init__(name="camera_processor")
self.config = config self.config = config
self.detection_queue = detection_queue self.detection_queues = detection_queues
self.detected_frames_queue = detected_frames_queue self.detected_frames_queue = detected_frames_queue
self.stop_event = stop_event self.stop_event = stop_event
self.camera_metrics = camera_metrics self.camera_metrics = camera_metrics
@@ -79,10 +79,11 @@ class CameraMaintainer(threading.Thread):
# create or update region grids for each camera # create or update region grids for each camera
for camera in self.config.cameras.values(): for camera in self.config.cameras.values():
assert camera.name is not None assert camera.name is not None
camera_model = self.config.models[camera.detect.model]
self.region_grids[camera.name] = get_camera_regions_grid( self.region_grids[camera.name] = get_camera_regions_grid(
camera.name, camera.name,
camera.detect, camera.detect,
max(self.config.model.width, self.config.model.height), max(camera_model.width, camera_model.height),
) )
def __calculate_shm_frame_count(self) -> int: def __calculate_shm_frame_count(self) -> int:
@@ -115,6 +116,8 @@ class CameraMaintainer(threading.Thread):
camera_stop_event = self.__ensure_camera_stop_event(name) camera_stop_event = self.__ensure_camera_stop_event(name)
camera_model = self.config.models[config.detect.model]
if runtime: if runtime:
self.camera_metrics[name] = CameraMetrics(self.metrics_manager) self.camera_metrics[name] = CameraMetrics(self.metrics_manager)
self.ptz_metrics[name] = PTZMetrics( self.ptz_metrics[name] = PTZMetrics(
@@ -123,32 +126,24 @@ class CameraMaintainer(threading.Thread):
self.region_grids[name] = get_camera_regions_grid( self.region_grids[name] = get_camera_regions_grid(
name, name,
config.detect, config.detect,
max(self.config.model.width, self.config.model.height), max(camera_model.width, camera_model.height),
) )
try: try:
largest_frame = max(
[
det.model.height * det.model.width * 3
if det.model is not None
else 320
for det in self.config.detectors.values()
]
)
UntrackedSharedMemory(name=f"out-{name}", create=True, size=20 * 6 * 4) UntrackedSharedMemory(name=f"out-{name}", create=True, size=20 * 6 * 4)
UntrackedSharedMemory( UntrackedSharedMemory(
name=name, name=name,
create=True, create=True,
size=largest_frame, size=camera_model.height * camera_model.width * 3,
) )
except FileExistsError: except FileExistsError:
pass pass
camera_process = CameraTracker( camera_process = CameraTracker(
config, config,
self.config.model, camera_model,
self.config.model.merged_labelmap, camera_model.merged_labelmap,
self.detection_queue, self.detection_queues[config.detect.model],
self.detected_frames_queue, self.detected_frames_queue,
self.camera_metrics[name], self.camera_metrics[name],
self.ptz_metrics[name], self.ptz_metrics[name],
+6 -5
View File
@@ -40,6 +40,7 @@ class CameraState:
self.name = name self.name = name
self.config = config self.config = config
self.camera_config = config.cameras[name] self.camera_config = config.cameras[name]
self.model_config = config.model_for_camera(name)
self.frame_manager = frame_manager self.frame_manager = frame_manager
self.best_objects: dict[str, TrackedObject] = {} self.best_objects: dict[str, TrackedObject] = {}
self.tracked_objects: dict[str, TrackedObject] = {} self.tracked_objects: dict[str, TrackedObject] = {}
@@ -101,7 +102,7 @@ class CameraState:
thickness = 1 thickness = 1
else: else:
thickness = 2 thickness = 2
color = self.config.model.colormap.get( color = self.model_config.colormap.get(
obj["label"], (255, 255, 255) obj["label"], (255, 255, 255)
) )
else: else:
@@ -125,7 +126,7 @@ class CameraState:
and obj["frame_time"] == frame_time and obj["frame_time"] == frame_time
): ):
thickness = 5 thickness = 5
color = self.config.model.colormap.get( color = self.model_config.colormap.get(
obj["label"], (255, 255, 255) obj["label"], (255, 255, 255)
) )
@@ -261,7 +262,7 @@ class CameraState:
if draw_options.get("paths"): if draw_options.get("paths"):
for obj in tracked_objects.values(): for obj in tracked_objects.values():
if obj["frame_time"] == frame_time and obj["path_data"]: if obj["frame_time"] == frame_time and obj["path_data"]:
color = self.config.model.colormap.get( color = self.model_config.colormap.get(
obj["label"], (255, 255, 255) obj["label"], (255, 255, 255)
) )
@@ -366,7 +367,7 @@ class CameraState:
for id in new_ids: for id in new_ids:
logger.debug(f"{self.name}: New tracked object ID: {id}") logger.debug(f"{self.name}: New tracked object ID: {id}")
new_obj = tracked_objects[id] = TrackedObject( new_obj = tracked_objects[id] = TrackedObject(
self.config.model, self.model_config,
self.camera_config, self.camera_config,
self.config.ui, self.config.ui,
self.frame_cache, self.frame_cache,
@@ -510,7 +511,7 @@ class CameraState:
sub_label = None sub_label = None
if obj.obj_data.get("sub_label"): if obj.obj_data.get("sub_label"):
if obj.obj_data["sub_label"][0] in self.config.model.all_attributes: if obj.obj_data["sub_label"][0] in self.model_config.all_attributes:
label = obj.obj_data["sub_label"][0] label = obj.obj_data["sub_label"][0]
else: else:
label = f"{object_type}-verified" label = f"{object_type}-verified"
+2 -1
View File
@@ -156,10 +156,11 @@ class Dispatcher:
if camera not in self.config.cameras: if camera not in self.config.cameras:
return None return None
camera_model = self.config.model_for_camera(camera)
grid = get_camera_regions_grid( grid = get_camera_regions_grid(
camera, camera,
self.config.cameras[camera].detect, self.config.cameras[camera].detect,
max(self.config.model.width, self.config.model.height), max(camera_model.width, camera_model.height),
) )
return grid return grid
+5
View File
@@ -50,6 +50,11 @@ class DetectConfig(FrigateBaseModel):
title="Enable object detection", title="Enable object detection",
description="Enable or disable object detection for all cameras; can be overridden per-camera.", description="Enable or disable object detection for all cameras; can be overridden per-camera.",
) )
model: str | None = Field(
default=None,
title="Detection model name",
description="Name of the model (key under `models`) used by this camera. Defaults to the only defined model, or the model named 'default'.",
)
height: int | None = Field( height: int | None = Field(
default=None, default=None,
title="Detect height", title="Detect height",
+154 -32
View File
@@ -4,6 +4,7 @@ import io
import json import json
import logging import logging
import os import os
import re
from typing import Any, Self from typing import Any, Self
import numpy as np import numpy as np
@@ -11,6 +12,7 @@ from pydantic import (
BaseModel, BaseModel,
ConfigDict, ConfigDict,
Field, Field,
PrivateAttr,
TypeAdapter, TypeAdapter,
ValidationInfo, ValidationInfo,
field_validator, field_validator,
@@ -19,7 +21,11 @@ from pydantic import (
from ruamel.yaml import YAML from ruamel.yaml import YAML
from frigate.const import REGEX_JSON from frigate.const import REGEX_JSON
from frigate.detectors import DetectorConfig, ModelConfig from frigate.detectors import (
DetectorConfig,
ModelConfig,
assign_detector_instances,
)
from frigate.detectors.detector_config import BaseDetectorConfig from frigate.detectors.detector_config import BaseDetectorConfig
from frigate.plus import PlusApi from frigate.plus import PlusApi
from frigate.util.builtin import ( from frigate.util.builtin import (
@@ -109,7 +115,7 @@ DEFAULT_CONFIG = f"""
mqtt: mqtt:
enabled: False enabled: False
{_render_default_yaml({"detectors": NEW_CONFIG_DETECTORS, "model": DEFAULT_MODEL})} {_render_default_yaml({"detectors": NEW_CONFIG_DETECTORS, "models": {"default": DEFAULT_MODEL}})}
cameras: {{}} # No cameras defined, UI wizard should be used cameras: {{}} # No cameras defined, UI wizard should be used
version: {CURRENT_CONFIG_VERSION} version: {CURRENT_CONFIG_VERSION}
""" """
@@ -503,10 +509,10 @@ class FrigateConfig(FrigateBaseModel):
title="Detector hardware", title="Detector hardware",
description="Configuration for object detectors (CPU, GPU, ONNX backends) and any detector-specific model settings.", description="Configuration for object detectors (CPU, GPU, ONNX backends) and any detector-specific model settings.",
) )
model: ModelConfig = Field( models: dict[str, ModelConfig] = Field(
default_factory=ModelConfig, default_factory=lambda: {"default": ModelConfig()},
title="Detection model", title="Detection models",
description="Settings to configure a custom object detection model and its input shape.", description="Named object detection models. Cameras select a model with detect.model; detectors are assigned to models automatically.",
) )
# GenAI config (named provider configs: name -> GenAIConfig) # GenAI config (named provider configs: name -> GenAIConfig)
@@ -621,11 +627,37 @@ class FrigateConfig(FrigateBaseModel):
) )
_plus_api: PlusApi _plus_api: PlusApi
_detector_instances: dict[str, BaseDetectorConfig] = PrivateAttr(
default_factory=dict
)
@property @property
def plus_api(self) -> PlusApi: def plus_api(self) -> PlusApi:
return self._plus_api return self._plus_api
@property
def detector_instances(self) -> dict[str, BaseDetectorConfig]:
"""Runtime detector instances expanded per assigned model."""
return self._detector_instances
def model_for_camera(self, camera_name: str) -> ModelConfig:
"""Return the detection model config used by the given camera."""
return self.models[self.cameras[camera_name].detect.model]
@field_validator("models")
@classmethod
def validate_model_names(cls, v: dict[str, ModelConfig]):
if not v:
raise ValueError("At least one model must be defined under models")
for name in v.keys():
if not re.match(r"^[a-zA-Z0-9_-]+$", name):
raise ValueError(
f"Invalid model name '{name}'. Model names can only contain letters, numbers, underscores, and hyphens"
)
return v
@model_validator(mode="after") @model_validator(mode="after")
def post_validation(self, info: ValidationInfo) -> Self: def post_validation(self, info: ValidationInfo) -> Self:
# Load plus api from context, if possible. # Load plus api from context, if possible.
@@ -671,7 +703,12 @@ class FrigateConfig(FrigateBaseModel):
) )
# set default min_score for object attributes # set default min_score for object attributes
for attribute in self.model.all_attributes: all_model_attributes = {
attribute
for model in self.models.values()
for attribute in model.all_attributes
}
for attribute in sorted(all_model_attributes):
existing = self.objects.filters.get(attribute) existing = self.objects.filters.get(attribute)
if existing is None: if existing is None:
self.objects.filters[attribute] = FilterConfig(min_score=0.7) self.objects.filters[attribute] = FilterConfig(min_score=0.7)
@@ -721,8 +758,18 @@ class FrigateConfig(FrigateBaseModel):
exclude_unset=True, exclude_unset=True,
) )
# capture raw model dumps before plus models are loaded so detector
# instances can run their own detector-specific plus validation
raw_model_dumps = {
name: model.model_dump(exclude_unset=True, warnings="none")
for name, model in self.models.items()
}
for model in self.models.values():
model.check_and_load_plus_model(self.plus_api)
adapter = TypeAdapter(DetectorConfig)
for key, detector in self.detectors.items(): for key, detector in self.detectors.items():
adapter = TypeAdapter(DetectorConfig)
model_dict = ( model_dict = (
detector detector
if isinstance(detector, dict) if isinstance(detector, dict)
@@ -737,27 +784,6 @@ class FrigateConfig(FrigateBaseModel):
) )
detector_config.model = None detector_config.model = None
model_config = self.model.model_dump(exclude_unset=True, warnings="none")
if detector_config.model_path:
model_config["path"] = detector_config.model_path
if "path" not in model_config:
if detector_config.type == "cpu" or detector_config.type.endswith(
"_tfl"
):
model_config["path"] = "/cpu_model.tflite"
elif detector_config.type == "edgetpu":
model_config["path"] = "/edgetpu_model.tflite"
elif detector_config.type == "openvino":
for default_key, default_value in DEFAULT_MODEL.items():
model_config.setdefault(default_key, default_value)
model = ModelConfig.model_validate(model_config)
model.check_and_load_plus_model(self.plus_api, detector_config.type)
model.compute_model_hash()
labelmap_objects = model.merged_labelmap.values()
detector_config.model = model
self.detectors[key] = detector_config self.detectors[key] = detector_config
for name, camera in self.cameras.items(): for name, camera in self.cameras.items():
@@ -785,6 +811,21 @@ class FrigateConfig(FrigateBaseModel):
{"name": name, **merged_config} {"name": name, **merged_config}
) )
# resolve which named model this camera uses
if camera_config.detect.model is not None:
if camera_config.detect.model not in self.models:
raise ValueError(
f"Camera {name} references model '{camera_config.detect.model}' which is not defined under models. Defined models: {', '.join(self.models.keys())}"
)
elif len(self.models) == 1:
camera_config.detect.model = next(iter(self.models))
elif "default" in self.models:
camera_config.detect.model = "default"
else:
raise ValueError(
f"Camera {name} does not specify detect.model and multiple models are defined. Set detect.model on the camera or globally, or name one of the models 'default'."
)
if camera_config.ffmpeg.hwaccel_args == "auto": if camera_config.ffmpeg.hwaccel_args == "auto":
camera_config.ffmpeg.hwaccel_args = self.ffmpeg.hwaccel_args camera_config.ffmpeg.hwaccel_args = self.ffmpeg.hwaccel_args
@@ -1005,7 +1046,10 @@ class FrigateConfig(FrigateBaseModel):
verify_profile_overrides_match_base(camera_config) verify_profile_overrides_match_base(camera_config)
verify_autotrack_zones(camera_config) verify_autotrack_zones(camera_config)
verify_motion_and_detect(camera_config) verify_motion_and_detect(camera_config)
verify_objects_track(camera_config, labelmap_objects) verify_objects_track(
camera_config,
self.models[camera_config.detect.model].merged_labelmap.values(),
)
verify_lpr_and_face(self, camera_config) verify_lpr_and_face(self, camera_config)
# Validate camera profiles reference top-level profile definitions # Validate camera profiles reference top-level profile definitions
@@ -1022,8 +1066,11 @@ class FrigateConfig(FrigateBaseModel):
config.name = name config.name = name
self.objects.parse_all_objects(self.cameras) self.objects.parse_all_objects(self.cameras)
self.model.create_colormap(sorted(self.objects.all_objects)) for model in self.models.values():
self.model.check_and_load_plus_model(self.plus_api) model.create_colormap(sorted(self.objects.all_objects))
# expand detectors into per-model runtime instances
self.__build_detector_instances(raw_model_dumps)
# Check audio transcription and audio detection requirements # Check audio transcription and audio detection requirements
if self.audio_transcription.enabled: if self.audio_transcription.enabled:
@@ -1054,6 +1101,81 @@ class FrigateConfig(FrigateBaseModel):
return self return self
def __build_detector_instances(
self, raw_model_dumps: dict[str, dict[str, Any]]
) -> None:
"""Expand detector entries into runtime instances, one per assigned model."""
used_models = list(
dict.fromkeys(camera.detect.model for camera in self.cameras.values())
) or list(self.models.keys())
unused_models = set(self.models.keys()) - set(used_models)
if unused_models:
logger.warning(
f"Models {', '.join(sorted(unused_models))} are defined but not used by any camera, no detector instances will be created for them"
)
assignments = assign_detector_instances(
{key: detector.type for key, detector in self.detectors.items()},
used_models,
)
models_per_detector: dict[str, int] = {}
for _, detector_key, _ in assignments:
models_per_detector[detector_key] = (
models_per_detector.get(detector_key, 0) + 1
)
instances: dict[str, BaseDetectorConfig] = {}
for instance_name, detector_key, model_key in assignments:
instance = self.detectors[detector_key].model_copy(deep=True)
instance.model_key = model_key
model_dict = raw_model_dumps[model_key].copy()
if instance.model_path:
if models_per_detector[detector_key] > 1:
logger.warning(
f"Detector {detector_key} runs multiple models, its model_path will be ignored"
)
else:
model_dict["path"] = instance.model_path
if "path" not in model_dict:
if instance.type == "cpu" or instance.type.endswith("_tfl"):
model_dict["path"] = "/cpu_model.tflite"
elif instance.type == "edgetpu":
model_dict["path"] = "/edgetpu_model.tflite"
elif instance.type == "openvino":
for default_key, default_value in DEFAULT_MODEL.items():
model_dict.setdefault(default_key, default_value)
model = ModelConfig.model_validate(model_dict)
try:
model.check_and_load_plus_model(self.plus_api, instance.type)
except ValueError as e:
raise ValueError(f"Model '{model_key}': {e}") from e
model.compute_model_hash()
instance.model = model
instances[instance_name] = instance
logger.log(
logging.INFO if len(used_models) > 1 else logging.DEBUG,
f"Detector instance {instance_name} ({instance.type}) will run model '{model_key}'",
)
# populate user-facing detector entries with their first assigned
# model for display purposes
for instance_name, detector_key, model_key in assignments:
detector = self.detectors[detector_key]
if detector.model is None:
detector.model = instances[instance_name].model
detector.model_key = model_key
self._detector_instances = instances
@field_validator("cameras") @field_validator("cameras")
@classmethod @classmethod
def ensure_zones_and_cameras_have_different_names(cls, v: dict[str, CameraConfig]): def ensure_zones_and_cameras_have_different_names(cls, v: dict[str, CameraConfig]):
@@ -72,9 +72,10 @@ class LicensePlateProcessingMixin:
# Object config # Object config
self.lp_objects: list[str] = [] self.lp_objects: list[str] = []
for obj, attributes in self.config.model.attributes_map.items(): for model in self.config.models.values():
if "license_plate" in attributes: for obj, attributes in model.attributes_map.items():
self.lp_objects.append(obj) if "license_plate" in attributes and obj not in self.lp_objects:
self.lp_objects.append(obj)
# Detection specific parameters # Detection specific parameters
self.min_size = 8 self.min_size = 8
@@ -231,8 +231,12 @@ class ReviewDescriptionProcessor(PostProcessorApi):
final_data, final_data,
thumbs, thumbs,
camera_config.review.genai, camera_config.review.genai,
list(self.config.model.merged_labelmap.values()), list(
self.config.model.all_attributes, self.config.model_for_camera(
camera_config.name
).merged_labelmap.values()
),
self.config.model_for_camera(camera_config.name).all_attributes,
), ),
).start() ).start()
+59 -1
View File
@@ -1,11 +1,69 @@
import logging import logging
from .detector_config import InputTensorEnum, ModelConfig, PixelFormatEnum # noqa: F401 from .detector_config import InputTensorEnum, ModelConfig, PixelFormatEnum # noqa: F401
from .detector_types import DetectorConfig, DetectorTypeEnum, api_types # noqa: F401 from .detector_types import ( # noqa: F401
DetectorConfig,
DetectorTypeEnum,
api_types,
detector_supports_multiple_models,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def assign_detector_instances(
detector_types: dict[str, str],
used_models: list[str],
) -> list[tuple[str, str, str]]:
"""Assign detector entries to models.
Detector types that support multiple models get one instance per model.
Single-model detector entries are round-robin assigned across the models,
wrapping around so that every detector entry is assigned.
Args:
detector_types: Detector key to detector type, in config order
used_models: Ordered model keys in use by cameras
Returns:
List of (instance_name, detector_key, model_key) assignments
"""
multi = [
key
for key, type_key in detector_types.items()
if detector_supports_multiple_models(type_key)
]
single = [key for key in detector_types if key not in multi]
if not multi and len(single) < len(used_models):
single_types = sorted({detector_types[key] for key in single})
raise ValueError(
f"Detectors {', '.join(single)} (types: {', '.join(single_types)}) can each only run a single model, "
f"but {len(used_models)} models are in use ({', '.join(used_models)}). "
"Add more detectors, use a detector type that supports multiple models, or reduce the number of models assigned to cameras."
)
assignments: list[tuple[str, str, str]] = []
for key in multi:
for model_key in used_models:
instance_name = key if len(used_models) == 1 else f"{key}_{model_key}"
assignments.append((instance_name, key, model_key))
for i, key in enumerate(single):
assignments.append((key, key, used_models[i % len(used_models)]))
instance_names = [name for name, _, _ in assignments]
duplicates = {name for name in instance_names if instance_names.count(name) > 1}
if duplicates:
raise ValueError(
f"Detector instance names collide: {', '.join(sorted(duplicates))}. "
"Rename the conflicting detectors or models so that expanded instance names (detector_model) are unique."
)
return assignments
def create_detector(detector_config): def create_detector(detector_config):
if detector_config.type == DetectorTypeEnum.cpu: if detector_config.type == DetectorTypeEnum.cpu:
logger.warning( logger.warning(
+4
View File
@@ -11,6 +11,10 @@ logger = logging.getLogger(__name__)
class DetectionApi(ABC): class DetectionApi(ABC):
type_key: str type_key: str
supported_models: list[ModelTypeEnum] supported_models: list[ModelTypeEnum]
# whether this detector type can run multiple model instances concurrently
# on the same hardware (one detector config entry can be expanded to an
# instance per model); single-model detectors serve exactly one model each
supports_multiple_models: bool = False
@abstractmethod @abstractmethod
def __init__(self, detector_config: BaseDetectorConfig): def __init__(self, detector_config: BaseDetectorConfig):
+5
View File
@@ -250,6 +250,11 @@ class BaseDetectorConfig(BaseModel):
title="Detector specific model path", title="Detector specific model path",
description="File path to the detector model binary if required by the chosen detector.", description="File path to the detector model binary if required by the chosen detector.",
) )
model_key: str | None = Field(
default=None,
title="Assigned model name",
description="Name of the model (key under `models`) this detector instance serves. Set automatically at runtime, users should not set this.",
)
model_config = ConfigDict( model_config = ConfigDict(
extra="allow", arbitrary_types_allowed=True, protected_namespaces=() extra="allow", arbitrary_types_allowed=True, protected_namespaces=()
) )
+6
View File
@@ -29,6 +29,12 @@ for _, name, _ in _included_modules:
api_types = {det.type_key: det for det in DetectionApi.__subclasses__()} api_types = {det.type_key: det for det in DetectionApi.__subclasses__()}
def detector_supports_multiple_models(type_key: str) -> bool:
"""Return whether the given detector type can run multiple model instances."""
detector = api_types.get(type_key)
return bool(detector and getattr(detector, "supports_multiple_models", False))
class StrEnum(str, Enum): class StrEnum(str, Enum):
pass pass
+1
View File
@@ -37,6 +37,7 @@ class CpuDetectorConfig(BaseDetectorConfig):
class CpuTfl(DetectionApi): class CpuTfl(DetectionApi):
type_key = DETECTOR_KEY type_key = DETECTOR_KEY
supports_multiple_models = True
def __init__(self, detector_config: CpuDetectorConfig): def __init__(self, detector_config: CpuDetectorConfig):
# Suppress TFLite delegate creation messages that bypass Python logging # Suppress TFLite delegate creation messages that bypass Python logging
+1
View File
@@ -41,6 +41,7 @@ class ONNXDetectorConfig(BaseDetectorConfig):
class ONNXDetector(DetectionApi): class ONNXDetector(DetectionApi):
type_key = DETECTOR_KEY type_key = DETECTOR_KEY
supports_multiple_models = True
def __init__(self, detector_config: ONNXDetectorConfig): def __init__(self, detector_config: ONNXDetectorConfig):
super().__init__(detector_config) super().__init__(detector_config)
+1
View File
@@ -36,6 +36,7 @@ class OvDetectorConfig(BaseDetectorConfig):
class OvDetector(DetectionApi): class OvDetector(DetectionApi):
type_key = DETECTOR_KEY type_key = DETECTOR_KEY
supports_multiple_models = True
supported_models = [ supported_models = [
ModelTypeEnum.dfine, ModelTypeEnum.dfine,
ModelTypeEnum.rfdetr, ModelTypeEnum.rfdetr,
+1
View File
@@ -47,6 +47,7 @@ class RknnDetectorConfig(BaseDetectorConfig):
class Rknn(DetectionApi): class Rknn(DetectionApi):
type_key = DETECTOR_KEY type_key = DETECTOR_KEY
supports_multiple_models = True
def __init__(self, config: RknnDetectorConfig): def __init__(self, config: RknnDetectorConfig):
super().__init__(config) super().__init__(config)
+1
View File
@@ -30,6 +30,7 @@ class TeflonDetectorConfig(BaseDetectorConfig):
class TeflonTfl(DetectionApi): class TeflonTfl(DetectionApi):
type_key = DETECTOR_KEY type_key = DETECTOR_KEY
supports_multiple_models = True
def __init__(self, detector_config: TeflonDetectorConfig): def __init__(self, detector_config: TeflonDetectorConfig):
# Location in Debian's mesa-teflon-delegate # Location in Debian's mesa-teflon-delegate
+1
View File
@@ -82,6 +82,7 @@ class HostDeviceMem:
class TensorRtDetector(DetectionApi): class TensorRtDetector(DetectionApi):
type_key = DETECTOR_KEY type_key = DETECTOR_KEY
supports_multiple_models = True
def _load_engine(self, model_path): def _load_engine(self, model_path):
try: try:
+15 -6
View File
@@ -159,7 +159,16 @@ class EventProcessor(threading.Thread):
if width is None or height is None: if width is None or height is None:
return return
first_detector = list(self.config.detectors.values())[0] # find a detector instance running this camera's model so the
# event records the model that produced it
camera_detector = next(
(
detector
for detector in self.config.detector_instances.values()
if detector.model_key == camera_config.detect.model
),
list(self.config.detectors.values())[0],
)
start_time = event_data["start_time"] start_time = event_data["start_time"]
end_time = ( end_time = (
@@ -229,13 +238,13 @@ class EventProcessor(threading.Thread):
Event.thumbnail: event_data.get("thumbnail"), Event.thumbnail: event_data.get("thumbnail"),
Event.has_clip: event_data["has_clip"], Event.has_clip: event_data["has_clip"],
Event.has_snapshot: event_data["has_snapshot"], Event.has_snapshot: event_data["has_snapshot"],
Event.model_hash: first_detector.model.model_hash Event.model_hash: camera_detector.model.model_hash
if first_detector.model if camera_detector.model
else None, else None,
Event.model_type: first_detector.model.model_type Event.model_type: camera_detector.model.model_type
if first_detector.model if camera_detector.model
else None, else None,
Event.detector_type: first_detector.type, Event.detector_type: camera_detector.type,
Event.data: { Event.data: {
"box": box, "box": box,
"region": region, "region": region,
+12 -20
View File
@@ -115,11 +115,9 @@ class PendingReviewSegment:
if self._frame is not None: if self._frame is not None:
self.thumb_time = datetime.datetime.now().timestamp() self.thumb_time = datetime.datetime.now().timestamp()
self.has_frame = True self.has_frame = True
Path(self.frame_path).parent.mkdir(parents=True, exist_ok=True) cv2.imwrite(
if not cv2.imwrite(
self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60] self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60]
): )
logger.error("Failed to write review thumbnail to %s", self.frame_path)
def save_full_frame(self, camera_config: CameraConfig, frame: np.ndarray) -> None: def save_full_frame(self, camera_config: CameraConfig, frame: np.ndarray) -> None:
color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
@@ -130,11 +128,9 @@ class PendingReviewSegment:
if self._frame is not None: if self._frame is not None:
self.has_frame = True self.has_frame = True
Path(self.frame_path).parent.mkdir(parents=True, exist_ok=True) cv2.imwrite(
if not cv2.imwrite(
self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60] self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60]
): )
logger.error("Failed to write review thumbnail to %s", self.frame_path)
def get_data(self, ended: bool) -> dict: def get_data(self, ended: bool) -> dict:
end_time = None end_time = None
@@ -378,16 +374,6 @@ class ReviewSegmentMaintainer(threading.Thread):
"""Forcibly end the pending segment for a camera.""" """Forcibly end the pending segment for a camera."""
segment = self.active_review_segments.get(camera) segment = self.active_review_segments.get(camera)
if segment: if segment:
if self.indefinite_events.get(camera):
self.indefinite_events[camera] = {}
now = datetime.datetime.now().timestamp()
if segment.last_alert_time == sys.maxsize:
segment.last_alert_time = now
if segment.last_detection_time == sys.maxsize:
segment.last_detection_time = now
prev_data = segment.get_data(False) prev_data = segment.get_data(False)
return self._publish_segment_end(segment, prev_data) return self._publish_segment_end(segment, prev_data)
return None return None
@@ -450,7 +436,10 @@ class ReviewSegmentMaintainer(threading.Thread):
if not object["sub_label"]: if not object["sub_label"]:
segment.detections[object["id"]] = object["label"] segment.detections[object["id"]] = object["label"]
elif object["sub_label"][0] in self.config.model.all_attributes: elif (
object["sub_label"][0]
in self.config.model_for_camera(segment.camera).all_attributes
):
segment.detections[object["id"]] = object["sub_label"][0] segment.detections[object["id"]] = object["sub_label"][0]
else: else:
segment.detections[object["id"]] = f"{object['label']}-verified" segment.detections[object["id"]] = f"{object['label']}-verified"
@@ -588,7 +577,10 @@ class ReviewSegmentMaintainer(threading.Thread):
for object in activity.get_all_objects(): for object in activity.get_all_objects():
if not object["sub_label"]: if not object["sub_label"]:
detections[object["id"]] = object["label"] detections[object["id"]] = object["label"]
elif object["sub_label"][0] in self.config.model.all_attributes: elif (
object["sub_label"][0]
in self.config.model_for_camera(camera).all_attributes
):
detections[object["id"]] = object["sub_label"][0] detections[object["id"]] = object["sub_label"][0]
else: else:
detections[object["id"]] = f"{object['label']}-verified" detections[object["id"]] = f"{object['label']}-verified"
+181 -9
View File
@@ -86,7 +86,7 @@ class TestConfig(unittest.TestCase):
}, },
}, },
# needs to be a file that will exist, doesn't matter what # needs to be a file that will exist, doesn't matter what
"model": {"path": "/etc/hosts", "width": 512}, "models": {"default": {"path": "/etc/hosts", "width": 512}},
} }
frigate_config = FrigateConfig(**(deep_merge(config, self.minimal))) frigate_config = FrigateConfig(**(deep_merge(config, self.minimal)))
@@ -103,7 +103,7 @@ class TestConfig(unittest.TestCase):
assert frigate_config.detectors["edgetpu"].device is None assert frigate_config.detectors["edgetpu"].device is None
assert frigate_config.detectors["openvino"].device is None assert frigate_config.detectors["openvino"].device is None
assert frigate_config.model.path == "/etc/hosts" assert frigate_config.models["default"].path == "/etc/hosts"
assert frigate_config.detectors["cpu"].model.path == "/cpu_model.tflite" assert frigate_config.detectors["cpu"].model.path == "/cpu_model.tflite"
assert frigate_config.detectors["edgetpu"].model.path == "/edgetpu_model.tflite" assert frigate_config.detectors["edgetpu"].model.path == "/edgetpu_model.tflite"
assert frigate_config.detectors["openvino"].model.path == "/etc/hosts" assert frigate_config.detectors["openvino"].model.path == "/etc/hosts"
@@ -956,7 +956,7 @@ class TestConfig(unittest.TestCase):
def test_merge_labelmap(self): def test_merge_labelmap(self):
config = { config = {
"mqtt": {"host": "mqtt"}, "mqtt": {"host": "mqtt"},
"model": {"labelmap": {7: "truck"}}, "models": {"default": {"labelmap": {7: "truck"}}},
"cameras": { "cameras": {
"back": { "back": {
"ffmpeg": { "ffmpeg": {
@@ -977,7 +977,7 @@ class TestConfig(unittest.TestCase):
} }
frigate_config = FrigateConfig(**config) frigate_config = FrigateConfig(**config)
assert frigate_config.model.merged_labelmap[7] == "truck" assert frigate_config.models["default"].merged_labelmap[7] == "truck"
def test_default_labelmap_empty(self): def test_default_labelmap_empty(self):
config = { config = {
@@ -1002,12 +1002,12 @@ class TestConfig(unittest.TestCase):
} }
frigate_config = FrigateConfig(**config) frigate_config = FrigateConfig(**config)
assert frigate_config.model.merged_labelmap[0] == "person" assert frigate_config.models["default"].merged_labelmap[0] == "person"
def test_default_labelmap(self): def test_default_labelmap(self):
config = { config = {
"mqtt": {"host": "mqtt"}, "mqtt": {"host": "mqtt"},
"model": {"width": 320, "height": 320}, "models": {"default": {"width": 320, "height": 320}},
"cameras": { "cameras": {
"back": { "back": {
"ffmpeg": { "ffmpeg": {
@@ -1028,7 +1028,7 @@ class TestConfig(unittest.TestCase):
} }
frigate_config = FrigateConfig(**config) frigate_config = FrigateConfig(**config)
assert frigate_config.model.merged_labelmap[0] == "person" assert frigate_config.models["default"].merged_labelmap[0] == "person"
def test_plus_labelmap(self): def test_plus_labelmap(self):
with open(os.path.join(MODEL_CACHE_DIR, "test"), "w") as f: with open(os.path.join(MODEL_CACHE_DIR, "test"), "w") as f:
@@ -1039,7 +1039,7 @@ class TestConfig(unittest.TestCase):
config = { config = {
"mqtt": {"host": "mqtt"}, "mqtt": {"host": "mqtt"},
"detectors": {"cpu": {"type": "cpu"}}, "detectors": {"cpu": {"type": "cpu"}},
"model": {"path": "plus://test"}, "models": {"default": {"path": "plus://test"}},
"cameras": { "cameras": {
"back": { "back": {
"ffmpeg": { "ffmpeg": {
@@ -1060,7 +1060,7 @@ class TestConfig(unittest.TestCase):
} }
frigate_config = FrigateConfig(**config) frigate_config = FrigateConfig(**config)
assert frigate_config.model.merged_labelmap[0] == "amazon" assert frigate_config.models["default"].merged_labelmap[0] == "amazon"
def test_fails_on_invalid_role(self): def test_fails_on_invalid_role(self):
config = { config = {
@@ -1765,5 +1765,177 @@ class TestAttributeFilterDefaults(unittest.TestCase):
self.assertEqual(face_filter.min_score, 0.3) self.assertEqual(face_filter.min_score, 0.3)
class TestMultiModelConfig(unittest.TestCase):
"""Tests for named models and detector instance assignment."""
def setUp(self):
self.base = {
"mqtt": {"host": "mqtt"},
"models": {
"indoor": {"width": 320, "height": 320},
"outdoor": {"width": 640, "height": 640},
},
"cameras": {
"living_room": self._camera("indoor"),
"driveway": self._camera("outdoor"),
},
}
def _camera(self, model=None):
camera = {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
}
if model:
camera["detect"]["model"] = model
return camera
def test_single_model_resolves_implicitly(self):
config = FrigateConfig(
mqtt={"host": "mqtt"},
models={"custom": {"width": 320, "height": 320}},
cameras={"back": self._camera()},
)
assert config.cameras["back"].detect.model == "custom"
def test_multiple_models_resolve_to_default(self):
config = FrigateConfig(
mqtt={"host": "mqtt"},
models={
"default": {"width": 320, "height": 320},
"outdoor": {"width": 640, "height": 640},
},
cameras={"back": self._camera()},
)
assert config.cameras["back"].detect.model == "default"
def test_multiple_models_without_default_requires_selection(self):
config = self.base.copy()
config["cameras"] = {"back": self._camera()}
self.assertRaises(ValidationError, lambda: FrigateConfig(**config))
def test_camera_references_missing_model(self):
config = self.base.copy()
config["cameras"] = {"back": self._camera("thermal")}
self.assertRaises(ValidationError, lambda: FrigateConfig(**config))
def test_global_detect_model_inherited_and_overridden(self):
config = self.base.copy()
config["detect"] = {"model": "indoor"}
config["cameras"] = {
"living_room": self._camera(),
"driveway": self._camera("outdoor"),
}
frigate_config = FrigateConfig(**config)
assert frigate_config.cameras["living_room"].detect.model == "indoor"
assert frigate_config.cameras["driveway"].detect.model == "outdoor"
def test_invalid_model_name(self):
config = self.base.copy()
config["models"] = {"bad name!": {"width": 320, "height": 320}}
self.assertRaises(ValidationError, lambda: FrigateConfig(**config))
def test_multi_model_detector_expands_instances(self):
config = self.base.copy()
config["detectors"] = {
"ov0": {"type": "openvino", "device": "GPU"},
"ov1": {"type": "openvino", "device": "GPU.1"},
}
frigate_config = FrigateConfig(**config)
assert sorted(frigate_config.detector_instances.keys()) == [
"ov0_indoor",
"ov0_outdoor",
"ov1_indoor",
"ov1_outdoor",
]
assert frigate_config.detector_instances["ov0_indoor"].model_key == "indoor"
assert frigate_config.detector_instances["ov0_indoor"].model.width == 320
assert frigate_config.detector_instances["ov0_outdoor"].model.width == 640
def test_multi_model_detector_single_model_keeps_name(self):
config = self.base.copy()
config["models"] = {"default": {"width": 320, "height": 320}}
config["cameras"] = {"back": self._camera()}
config["detectors"] = {"ov": {"type": "openvino", "device": "GPU"}}
frigate_config = FrigateConfig(**config)
assert list(frigate_config.detector_instances.keys()) == ["ov"]
assert frigate_config.detector_instances["ov"].model_key == "default"
def test_single_model_detectors_round_robin(self):
config = self.base.copy()
config["detectors"] = {
"coral0": {"type": "edgetpu", "device": "usb:0"},
"coral1": {"type": "edgetpu", "device": "usb:1"},
"coral2": {"type": "edgetpu", "device": "usb:2"},
}
frigate_config = FrigateConfig(**config)
assignments = {
key: instance.model_key
for key, instance in frigate_config.detector_instances.items()
}
assert assignments == {
"coral0": "indoor",
"coral1": "outdoor",
"coral2": "indoor",
}
def test_single_model_detectors_insufficient_coverage(self):
config = self.base.copy()
config["detectors"] = {"coral": {"type": "edgetpu", "device": "usb"}}
self.assertRaises(ValidationError, lambda: FrigateConfig(**config))
def test_single_model_detector_with_multi_model_detector(self):
config = self.base.copy()
config["detectors"] = {
"coral": {"type": "edgetpu", "device": "usb"},
"ov": {"type": "openvino", "device": "GPU"},
}
frigate_config = FrigateConfig(**config)
assert frigate_config.detector_instances["coral"].model_key == "indoor"
assert frigate_config.detector_instances["ov_indoor"].model_key == "indoor"
assert frigate_config.detector_instances["ov_outdoor"].model_key == "outdoor"
def test_unused_model_gets_no_instances(self):
config = self.base.copy()
config["models"] = {
**config["models"],
"thermal": {"width": 320, "height": 320},
}
frigate_config = FrigateConfig(**config)
model_keys = {
instance.model_key
for instance in frigate_config.detector_instances.values()
}
assert "thermal" not in model_keys
def test_model_path_ignored_when_detector_runs_multiple_models(self):
config = self.base.copy()
config["detectors"] = {
"cpu": {"type": "cpu", "model_path": "/custom_model.tflite"}
}
frigate_config = FrigateConfig(**config)
assert (
frigate_config.detector_instances["cpu_indoor"].model.path
== "/cpu_model.tflite"
)
def test_model_path_applied_when_detector_runs_one_model(self):
config = self.base.copy()
config["models"] = {"default": {"width": 320, "height": 320}}
config["cameras"] = {"back": self._camera()}
config["detectors"] = {
"cpu": {"type": "cpu", "model_path": "/custom_model.tflite"}
}
frigate_config = FrigateConfig(**config)
assert (
frigate_config.detector_instances["cpu"].model.path
== "/custom_model.tflite"
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main(verbosity=2) unittest.main(verbosity=2)
+42
View File
@@ -0,0 +1,42 @@
"""Tests for config file migration functions."""
import unittest
from frigate.util.config import migrate_019_0
class TestMigrate019(unittest.TestCase):
def test_migrates_model_to_named_models(self):
config = {
"version": "0.18-0",
"mqtt": {"host": "mqtt"},
"model": {"path": "/config/model.tflite", "width": 320, "height": 320},
}
new_config = migrate_019_0(config)
assert "model" not in new_config
assert new_config["models"] == {
"default": {"path": "/config/model.tflite", "width": 320, "height": 320}
}
assert new_config["version"] == "0.19-0"
def test_no_model_defined(self):
config = {"version": "0.18-0", "mqtt": {"host": "mqtt"}}
new_config = migrate_019_0(config)
assert "model" not in new_config
assert "models" not in new_config
assert new_config["version"] == "0.19-0"
def test_existing_models_not_overwritten(self):
config = {
"version": "0.18-0",
"model": {"width": 320},
"models": {"custom": {"width": 640}},
}
new_config = migrate_019_0(config)
assert "model" not in new_config
assert new_config["models"] == {"custom": {"width": 640}}
assert new_config["version"] == "0.19-0"
if __name__ == "__main__":
unittest.main(verbosity=2)
+4 -1
View File
@@ -209,7 +209,10 @@ class TrackedObjectProcessor(threading.Thread):
if obj.obj_data.get("sub_label"): if obj.obj_data.get("sub_label"):
sub_label = obj.obj_data["sub_label"][0] sub_label = obj.obj_data["sub_label"][0]
if sub_label in self.config.model.all_attribute_logos: if (
sub_label
in self.config.model_for_camera(camera).all_attribute_logos
):
self.dispatcher.publish( self.dispatcher.publish(
f"{camera}/{sub_label}/snapshot", f"{camera}/{sub_label}/snapshot",
jpg_bytes, jpg_bytes,
+29 -1
View File
@@ -20,7 +20,7 @@ from frigate.util.services import get_video_properties
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CURRENT_CONFIG_VERSION = "0.18-0" CURRENT_CONFIG_VERSION = "0.19-0"
DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, "config.yml") DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, "config.yml")
@@ -99,6 +99,7 @@ def migrate_frigate_config(config_file: str):
new_config = migrate_014(config) new_config = migrate_014(config)
with open(config_file, "w") as f: with open(config_file, "w") as f:
yaml.dump(new_config, f) yaml.dump(new_config, f)
config = new_config
previous_version = "0.14" previous_version = "0.14"
logger.info("Migrating export file names...") logger.info("Migrating export file names...")
@@ -117,6 +118,7 @@ def migrate_frigate_config(config_file: str):
new_config = migrate_015_0(config) new_config = migrate_015_0(config)
with open(config_file, "w") as f: with open(config_file, "w") as f:
yaml.dump(new_config, f) yaml.dump(new_config, f)
config = new_config
previous_version = "0.15-0" previous_version = "0.15-0"
if previous_version < "0.15-1": if previous_version < "0.15-1":
@@ -124,6 +126,7 @@ def migrate_frigate_config(config_file: str):
new_config = migrate_015_1(config) new_config = migrate_015_1(config)
with open(config_file, "w") as f: with open(config_file, "w") as f:
yaml.dump(new_config, f) yaml.dump(new_config, f)
config = new_config
previous_version = "0.15-1" previous_version = "0.15-1"
if previous_version < "0.16-0": if previous_version < "0.16-0":
@@ -131,6 +134,7 @@ def migrate_frigate_config(config_file: str):
new_config = migrate_016_0(config) new_config = migrate_016_0(config)
with open(config_file, "w") as f: with open(config_file, "w") as f:
yaml.dump(new_config, f) yaml.dump(new_config, f)
config = new_config
previous_version = "0.16-0" previous_version = "0.16-0"
if previous_version < "0.17-0": if previous_version < "0.17-0":
@@ -138,6 +142,7 @@ def migrate_frigate_config(config_file: str):
new_config = migrate_017_0(config) new_config = migrate_017_0(config)
with open(config_file, "w") as f: with open(config_file, "w") as f:
yaml.dump(new_config, f) yaml.dump(new_config, f)
config = new_config
previous_version = "0.17-0" previous_version = "0.17-0"
if previous_version < "0.18-0": if previous_version < "0.18-0":
@@ -145,8 +150,17 @@ def migrate_frigate_config(config_file: str):
new_config = migrate_018_0(config) new_config = migrate_018_0(config)
with open(config_file, "w") as f: with open(config_file, "w") as f:
yaml.dump(new_config, f) yaml.dump(new_config, f)
config = new_config
previous_version = "0.18-0" previous_version = "0.18-0"
if previous_version < "0.19-0":
logger.info(f"Migrating frigate config from {previous_version} to 0.19-0...")
new_config = migrate_019_0(config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
config = new_config
previous_version = "0.19-0"
logger.info("Finished frigate config migration...") logger.info("Finished frigate config migration...")
@@ -658,6 +672,20 @@ def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
return new_config return new_config
def migrate_019_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
"""Handle migrating frigate config to 0.19-0"""
new_config = config.copy()
# Migrate the single model config to named models
model = new_config.pop("model", None)
if model is not None and "models" not in new_config:
new_config["models"] = {"default": model}
new_config["version"] = "0.19-0"
return new_config
def get_relative_coordinates( def get_relative_coordinates(
mask: str | list | None, mask: str | list | None,
frame_shape: tuple[int, int], frame_shape: tuple[int, int],
+1 -1
View File
@@ -1 +1 @@
[{"id": "case-001", "name": "Package Theft Investigation", "description": "Review of suspicious activity near the front porch", "created_at": 1780597809.365581, "updated_at": 1780673409.365581}] [{"id": "case-001", "name": "Package Theft Investigation", "description": "Review of suspicious activity near the front porch", "created_at": 1784761296.1184616, "updated_at": 1784836896.1184616}]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
[{"id": "event-person-001", "label": "person", "sub_label": null, "camera": "front_door", "start_time": 1780677009.365581, "end_time": 1780677039.365581, "false_positive": false, "zones": ["front_yard"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "abc123", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.92, "score": 0.92, "region": [0.1, 0.1, 0.5, 0.8], "box": [0.2, 0.15, 0.45, 0.75], "area": 0.18, "ratio": 0.6, "type": "object", "description": "A person walking toward the front door", "average_estimated_speed": 1.2, "velocity_angle": 45.0, "path_data": [[[0.2, 0.5], 0.0], [[0.3, 0.5], 1.0]]}}, {"id": "event-car-001", "label": "car", "sub_label": null, "camera": "backyard", "start_time": 1780673409.365581, "end_time": 1780673454.365581, "false_positive": false, "zones": ["driveway"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "def456", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.87, "score": 0.87, "region": [0.3, 0.2, 0.9, 0.7], "box": [0.35, 0.25, 0.85, 0.65], "area": 0.2, "ratio": 1.25, "type": "object", "description": "A car parked in the driveway", "average_estimated_speed": 0.0, "velocity_angle": 0.0, "path_data": []}}, {"id": "event-person-002", "label": "person", "sub_label": null, "camera": "garage", "start_time": 1780669809.365581, "end_time": 1780669829.365581, "false_positive": false, "zones": [], "thumbnail": null, "has_clip": false, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "ghi789", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.78, "score": 0.78, "region": [0.0, 0.0, 0.6, 0.9], "box": [0.1, 0.05, 0.5, 0.85], "area": 0.32, "ratio": 0.5, "type": "object", "description": null, "average_estimated_speed": 0.5, "velocity_angle": 90.0, "path_data": [[[0.1, 0.4], 0.0]]}}] [{"id": "event-person-001", "label": "person", "sub_label": null, "camera": "front_door", "start_time": 1784840496.1184616, "end_time": 1784840526.1184616, "false_positive": false, "zones": ["front_yard"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "abc123", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.92, "score": 0.92, "region": [0.1, 0.1, 0.5, 0.8], "box": [0.2, 0.15, 0.45, 0.75], "area": 0.18, "ratio": 0.6, "type": "object", "description": "A person walking toward the front door", "average_estimated_speed": 1.2, "velocity_angle": 45.0, "path_data": [[[0.2, 0.5], 0.0], [[0.3, 0.5], 1.0]]}}, {"id": "event-car-001", "label": "car", "sub_label": null, "camera": "backyard", "start_time": 1784836896.1184616, "end_time": 1784836941.1184616, "false_positive": false, "zones": ["driveway"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "def456", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.87, "score": 0.87, "region": [0.3, 0.2, 0.9, 0.7], "box": [0.35, 0.25, 0.85, 0.65], "area": 0.2, "ratio": 1.25, "type": "object", "description": "A car parked in the driveway", "average_estimated_speed": 0.0, "velocity_angle": 0.0, "path_data": []}}, {"id": "event-person-002", "label": "person", "sub_label": null, "camera": "garage", "start_time": 1784833296.1184616, "end_time": 1784833316.1184616, "false_positive": false, "zones": [], "thumbnail": null, "has_clip": false, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "ghi789", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.78, "score": 0.78, "region": [0.0, 0.0, 0.6, 0.9], "box": [0.1, 0.05, 0.5, 0.85], "area": 0.32, "ratio": 0.5, "type": "object", "description": null, "average_estimated_speed": 0.5, "velocity_angle": 90.0, "path_data": [[[0.1, 0.4], 0.0]]}}]
+1 -1
View File
@@ -1 +1 @@
[{"id": "export-001", "camera": "front_door", "name": "Front Door - Person Alert", "date": 1780680609.365581, "video_path": "/exports/export-001.mp4", "thumb_path": "/exports/export-001-thumb.jpg", "in_progress": false, "export_case_id": null}, {"id": "export-002", "camera": "backyard", "name": "Backyard - Car Detection", "date": 1780673409.365581, "video_path": "/exports/export-002.mp4", "thumb_path": "/exports/export-002-thumb.jpg", "in_progress": false, "export_case_id": "case-001"}, {"id": "export-003", "camera": "garage", "name": "Garage - In Progress", "date": 1780682409.365581, "video_path": "/exports/export-003.mp4", "thumb_path": "/exports/export-003-thumb.jpg", "in_progress": true, "export_case_id": null}] [{"id": "export-001", "camera": "front_door", "name": "Front Door - Person Alert", "date": 1784844096.1184616, "video_path": "/exports/export-001.mp4", "thumb_path": "/exports/export-001-thumb.jpg", "in_progress": false, "export_case_id": null}, {"id": "export-002", "camera": "backyard", "name": "Backyard - Car Detection", "date": 1784836896.1184616, "video_path": "/exports/export-002.mp4", "thumb_path": "/exports/export-002-thumb.jpg", "in_progress": false, "export_case_id": "case-001"}, {"id": "export-003", "camera": "garage", "name": "Garage - In Progress", "date": 1784845896.1184616, "video_path": "/exports/export-003.mp4", "thumb_path": "/exports/export-003-thumb.jpg", "in_progress": true, "export_case_id": null}]
@@ -102,11 +102,17 @@ def generate_config():
snapshot = config.model_dump() snapshot = config.model_dump()
# Runtime-computed fields not in the Pydantic dump # Runtime-computed fields not in the Pydantic dump
all_attrs = set() for model_dict in snapshot.get("models", {}).values():
for attrs in snapshot.get("model", {}).get("attributes_map", {}).values(): all_attrs = set()
all_attrs.update(attrs) for attrs in model_dict.get("attributes_map", {}).values():
snapshot["model"]["all_attributes"] = sorted(all_attrs) all_attrs.update(attrs)
snapshot["model"]["colormap"] = {} model_dict["all_attributes"] = sorted(all_attrs)
model_dict["colormap"] = {}
# legacy single-model block mirrors the default model, matching /api/config
models = snapshot.get("models", {})
default_key = "default" if "default" in models else next(iter(models))
snapshot["model"] = models[default_key]
return snapshot return snapshot
@@ -1 +1 @@
{"2026-06-05": {"day": "2026-06-05", "reviewed_alert": 1, "reviewed_detection": 0, "total_alert": 2, "total_detection": 2}, "2026-06-04": {"day": "2026-06-04", "reviewed_alert": 3, "reviewed_detection": 2, "total_alert": 3, "total_detection": 4}} {"2026-07-23": {"day": "2026-07-23", "reviewed_alert": 1, "reviewed_detection": 0, "total_alert": 2, "total_detection": 2}, "2026-07-22": {"day": "2026-07-22", "reviewed_alert": 3, "reviewed_detection": 2, "total_alert": 3, "total_detection": 4}}
+1 -1
View File
@@ -1 +1 @@
[{"id": "review-alert-001", "camera": "front_door", "start_time": "2026-06-05T11:30:09.365581", "end_time": "2026-06-05T11:30:39.365581", "has_been_reviewed": false, "severity": "alert", "thumb_path": "/clips/front_door/review-alert-001-thumb.jpg", "data": {"audio": [], "detections": ["person-abc123"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}, {"id": "review-alert-002", "camera": "backyard", "start_time": "2026-06-05T10:30:09.365581", "end_time": "2026-06-05T10:30:54.365581", "has_been_reviewed": true, "severity": "alert", "thumb_path": "/clips/backyard/review-alert-002-thumb.jpg", "data": {"audio": [], "detections": ["car-def456"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["driveway"]}}, {"id": "review-detect-001", "camera": "garage", "start_time": "2026-06-05T09:30:09.365581", "end_time": "2026-06-05T09:30:29.365581", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/garage/review-detect-001-thumb.jpg", "data": {"audio": [], "detections": ["person-ghi789"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": []}}, {"id": "review-detect-002", "camera": "front_door", "start_time": "2026-06-05T08:30:09.365581", "end_time": "2026-06-05T08:30:24.365581", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/front_door/review-detect-002-thumb.jpg", "data": {"audio": [], "detections": ["car-jkl012"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}] [{"id": "review-alert-001", "camera": "front_door", "start_time": "2026-07-23T21:01:36.118462", "end_time": "2026-07-23T21:02:06.118462", "has_been_reviewed": false, "severity": "alert", "thumb_path": "/clips/front_door/review-alert-001-thumb.jpg", "data": {"audio": [], "detections": ["person-abc123"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}, {"id": "review-alert-002", "camera": "backyard", "start_time": "2026-07-23T20:01:36.118462", "end_time": "2026-07-23T20:02:21.118462", "has_been_reviewed": true, "severity": "alert", "thumb_path": "/clips/backyard/review-alert-002-thumb.jpg", "data": {"audio": [], "detections": ["car-def456"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["driveway"]}}, {"id": "review-detect-001", "camera": "garage", "start_time": "2026-07-23T19:01:36.118462", "end_time": "2026-07-23T19:01:56.118462", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/garage/review-detect-001-thumb.jpg", "data": {"audio": [], "detections": ["person-ghi789"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": []}}, {"id": "review-detect-002", "camera": "front_door", "start_time": "2026-07-23T18:01:36.118462", "end_time": "2026-07-23T18:01:51.118462", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/front_door/review-detect-002-thumb.jpg", "data": {"audio": [], "detections": ["car-jkl012"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}]
+4 -4
View File
@@ -73,7 +73,7 @@
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-router-dom": "^6.30.3", "react-router-dom": "^6.30.3",
"react-swipeable": "^7.0.2", "react-swipeable": "^7.0.2",
"react-zoom-pan-pinch": "3.4.4", "react-zoom-pan-pinch": "^3.7.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"scroll-into-view-if-needed": "^3.1.0", "scroll-into-view-if-needed": "^3.1.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
@@ -12354,9 +12354,9 @@
} }
}, },
"node_modules/react-zoom-pan-pinch": { "node_modules/react-zoom-pan-pinch": {
"version": "3.4.4", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.4.4.tgz", "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz",
"integrity": "sha512-lGTu7D9lQpYEQ6sH+NSlLA7gicgKRW8j+D/4HO1AbSV2POvKRFzdWQ8eI0r3xmOsl4dYQcY+teV6MhULeg1xBw==", "integrity": "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8", "node": ">=8",
+1 -1
View File
@@ -87,7 +87,7 @@
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-router-dom": "^6.30.3", "react-router-dom": "^6.30.3",
"react-swipeable": "^7.0.2", "react-swipeable": "^7.0.2",
"react-zoom-pan-pinch": "3.4.4", "react-zoom-pan-pinch": "^3.7.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"scroll-into-view-if-needed": "^3.1.0", "scroll-into-view-if-needed": "^3.1.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
@@ -86,6 +86,10 @@
"label": "Enable object detection", "label": "Enable object detection",
"description": "Enable or disable object detection for this camera." "description": "Enable or disable object detection for this camera."
}, },
"model": {
"label": "Detection model name",
"description": "Name of the model (key under `models`) used by this camera. Defaults to the only defined model, or the model named 'default'."
},
"height": { "height": {
"label": "Detect height", "label": "Detect height",
"description": "Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution." "description": "Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution."
+11 -3
View File
@@ -329,6 +329,10 @@
"label": "Detector specific model path", "label": "Detector specific model path",
"description": "File path to the detector model binary if required by the chosen detector." "description": "File path to the detector model binary if required by the chosen detector."
}, },
"model_key": {
"label": "Assigned model name",
"description": "Name of the model (key under `models`) this detector instance serves. Set automatically at runtime, users should not set this."
},
"axengine": { "axengine": {
"label": "AXEngine NPU", "label": "AXEngine NPU",
"description": "AXERA AX650N/AX8850N NPU detector running compiled .axmodel files via the AXEngine runtime." "description": "AXERA AX650N/AX8850N NPU detector running compiled .axmodel files via the AXEngine runtime."
@@ -454,9 +458,9 @@
} }
} }
}, },
"model": { "models": {
"label": "Detection model", "label": "Detection models",
"description": "Settings to configure a custom object detection model and its input shape.", "description": "Named object detection models. Cameras select a model with detect.model; detectors are assigned to models automatically.",
"path": { "path": {
"label": "Custom object detector model path", "label": "Custom object detector model path",
"description": "Path to a custom detection model file (or plus://<model_id> for Frigate+ models)." "description": "Path to a custom detection model file (or plus://<model_id> for Frigate+ models)."
@@ -625,6 +629,10 @@
"label": "Enable object detection", "label": "Enable object detection",
"description": "Enable or disable object detection for all cameras; can be overridden per-camera." "description": "Enable or disable object detection for all cameras; can be overridden per-camera."
}, },
"model": {
"label": "Detection model name",
"description": "Name of the model (key under `models`) used by this camera. Defaults to the only defined model, or the model named 'default'."
},
"height": { "height": {
"label": "Detect height", "label": "Detect height",
"description": "Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution." "description": "Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution."
+1 -3
View File
@@ -125,7 +125,5 @@
"baby": "Baby", "baby": "Baby",
"baby_stroller": "Baby Stroller", "baby_stroller": "Baby Stroller",
"rickshaw": "Rickshaw", "rickshaw": "Rickshaw",
"rodent": "Rodent", "rodent": "Rodent"
"possum": "Possum",
"garbage_truck": "Garbage Truck"
} }
@@ -14,7 +14,6 @@ const logger: SectionConfigOverrides = {
additionalProperties: { additionalProperties: {
"ui:options": { "ui:options": {
enumI18nPrefix: "logger.logLevel", enumI18nPrefix: "logger.logLevel",
additionalPropertyKeySize: "lg",
additionalPropertyKeyLabel: additionalPropertyKeyLabel:
"configForm.additionalProperties.loggerNameLabel", "configForm.additionalProperties.loggerNameLabel",
additionalPropertyKeyPlaceholder: additionalPropertyKeyPlaceholder:
@@ -74,6 +74,7 @@ const SECTIONS_WITHOUT_OVERRIDE_BADGE = new Set([
"birdseye", "birdseye",
"detectors", "detectors",
"model", "model",
"models",
]); ]);
type CameraEntryProps = { type CameraEntryProps = {
@@ -15,14 +15,6 @@ import { useTranslation } from "react-i18next";
import { LuTrash2 } from "react-icons/lu"; import { LuTrash2 } from "react-icons/lu";
import type { ConfigFormContext } from "@/types/configForm"; import type { ConfigFormContext } from "@/types/configForm";
const KEY_SIZE_CLASSES = {
sm: { key: "md:col-span-2", value: "md:col-span-9" },
md: { key: "md:col-span-4", value: "md:col-span-7" },
lg: { key: "md:col-span-7", value: "md:col-span-4" },
} as const;
type AdditionalPropertyKeySize = keyof typeof KEY_SIZE_CLASSES;
export function WrapIfAdditionalTemplate< export function WrapIfAdditionalTemplate<
T = unknown, T = unknown,
S extends StrictRJSFSchema = RJSFSchema, S extends StrictRJSFSchema = RJSFSchema,
@@ -66,14 +58,6 @@ export function WrapIfAdditionalTemplate<
: undefined; : undefined;
const preventKeyRename = uiOptions.preventKeyRename === true; const preventKeyRename = uiOptions.preventKeyRename === true;
const keySize =
typeof uiOptions.additionalPropertyKeySize === "string" &&
uiOptions.additionalPropertyKeySize in KEY_SIZE_CLASSES
? (uiOptions.additionalPropertyKeySize as AdditionalPropertyKeySize)
: "sm";
const keySpanClass = KEY_SIZE_CLASSES[keySize].key;
const valueSpanClass = KEY_SIZE_CLASSES[keySize].value;
const formContext = registry?.formContext as ConfigFormContext | undefined; const formContext = registry?.formContext as ConfigFormContext | undefined;
// optionally, lock the key once it's been saved // optionally, lock the key once it's been saved
@@ -142,7 +126,7 @@ export function WrapIfAdditionalTemplate<
style={style} style={style}
> >
{!keyIsReadonly && ( {!keyIsReadonly && (
<div className={cn("col-span-12 space-y-2", keySpanClass)}> <div className="col-span-12 space-y-2 md:col-span-2">
{displayLabel && <Label htmlFor={keyId}>{keyLabel}</Label>} {displayLabel && <Label htmlFor={keyId}>{keyLabel}</Label>}
{keyLocked ? ( {keyLocked ? (
<div <div
@@ -174,7 +158,7 @@ export function WrapIfAdditionalTemplate<
<div <div
className={cn( className={cn(
"col-span-12 space-y-2", "col-span-12 space-y-2",
!keyIsReadonly && valueSpanClass, !keyIsReadonly && "md:col-span-9",
)} )}
> >
{!keyIsReadonly && displayLabel && ( {!keyIsReadonly && displayLabel && (
-39
View File
@@ -1,39 +0,0 @@
import { SVGProps } from "react";
/**
* Skunk silhouette for the `skunk` object label.
*
* react-icons has no skunk in any of its packs. The usable stand-ins are
* either squirrels, which are indistinguishable from the `squirrel` label, or
* animals such as porcupine and hedgehog that are themselves Frigate+
* candidate labels.
*
* Adapted from "skunk silhouette" by dear_theophilus, published by Openclipart
* and released into the public domain, which permits reproduction,
* distribution and derivative works:
* https://openclipart.org/detail/170808/skunk-silhouette-by-dear_theophilus-170808
*
* Changes from the original: the unused Inkscape text region was dropped, the
* layer translate was folded into the viewBox, the viewBox was padded to give
* the same optical margin as the surrounding react-icons, and the fill was
* switched to currentColor. The white back stripe is negative space in a
* single path under the default nonzero fill rule, so no fill-rule override is
* needed here.
*
* Sized to sit alongside the react-icons set: currentColor fill and a 1em
* default box.
*/
export default function SkunkIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="96.87 93.174 110.58 109.81"
fill="currentColor"
height="1em"
width="1em"
{...props}
>
<path d="m169.58 196.93c-0.18864-0.85889 0.0134-1.3274 0.99603-2.31 0.67796-0.67797 1.7065-1.3304 2.2857-1.4498 1.3854-0.2857 5.2538-1.7692 5.8796-2.2549 0.27087-0.2102 0.4925-0.61406 0.4925-0.89746 0-0.77202-6.6659-6.0863-7.6343-6.0863-0.46161 0-2.0798 0.36441-3.596 0.80981-3.4618 1.0169-10.449 1.4599-16.39 1.0391-7.1408-0.50577-6.6473-0.58559-8.3439 1.3494-0.8327 0.94969-1.9642 2.5979-2.5144 3.6627-1.164 2.2525-4.5952 6.5163-5.8025 7.2106-1.0288 0.59154-4.5445 0.64844-5.9949 0.097-0.71722-0.27269-1.4467-0.28055-2.184-0.0235-2.4986 0.871-1.4597-1.341 1.2885-2.7437 0.78536-0.40084 1.9099-1.3016 2.499-2.0016 1.0163-1.2078 1.0634-1.4452 0.92116-4.6468-0.12622-2.8412-0.38301-3.9062-1.6262-6.7446-2.6443-6.0373-7.3573-11.099-12.678-13.617-1.5889-0.75179-2.62-0.93285-5.3302-0.93595-3.0016-0.004-3.6532-0.14511-6.107-1.3278-5.2109-2.5116-5.8227-4.8841-1.4913-5.7833 1.8591-0.38596 2.618-0.82052 4.4338-2.539 1.2049-1.1403 2.7581-2.3618 3.4516-2.7146 1.9684-1.001 7.7872-0.84521 13.816 0.37 5.7138 1.1517 10.339 1.3571 13.488 0.59905 1.1919-0.28691 4.6492-1.5446 7.683-2.7949 9.8002-4.0389 14.311-4.9373 19.109-3.806 1.4086 0.33208 2.7106 0.4572 2.8935 0.27806 1.0374-1.0164-1.1645-3.1382-6.6365-6.3947-5.6439-3.3588-9.8091-9.1231-10.764-14.897-0.60953-3.6839 0.72362-12.081 1.2923-13.596 0.56871-1.5152 1.4436-3.3754 1.9442-4.1338 2.9223-4.4276 8.177-9.1772 12.256-11.078 6.691-3.1177 17.754-2.3107 25.847 1.8855 3.5353 1.8329 6.7739 4.7994 8.0035 7.3309 1.3563 2.7924 2.3788 9.4802 1.6321 10.675-0.28831 0.46149-1.0187 1.9915-1.6231 3.4001-1.0066 2.3458-2.3392 4.3362-2.9009 4.3326-0.12475-0.00078-0.81827-1.1453-1.5412-2.5434-1.642-3.1756-6.3156-7.6992-7.9544-7.6992-0.58481 0-2.2156 0.52248-3.624 1.1611-3.1804 1.4421-4.0269 2.8569-3.7769 6.3127 0.24247 3.3521 2.1549 6.8946 9.8511 18.247 3.3307 4.9132 4.5143 10.219 3.7106 16.634-0.40898 3.2645-0.77579 4.5016-2.1396 7.2158-0.90527 1.8017-2.1169 3.8932-2.6924 4.6478-1.3686 1.7944-3.6318 10.202-4.0199 14.935-0.34632 4.2221-1.4143 6.7775-3.7479 8.9681l-1.5853 1.4881s-10.87 1.3087-11.076 0.36929zm-0.94988-19.482c5.4569-0.84327 10.316-2.9685 15.305-6.694 2.2799-1.7026 7.2867-9.9628 7.5372-11.17 1.1907-5.7355-0.81743-10.348-8.1457-18.709-1.1818-1.3484-3.251-4.3132-4.5981-6.5886 0 0-1.8246-3.0395-2.2503-4.734-0.4037-1.6068-0.24133-4.9643-0.24133-4.9643-0.0233-3.0587 1.7125-7.8176 2.7258-8.9913 2.4126-2.7945 5.1792-3.93 8.9395-3.6689 2.4539 0.17036 6.2631 1.6358 6.8904 2.6509 0.46463 0.75179 1.7776 0.45185 1.7776-0.40608 0-2.1062-2.1313-4.3793-5.8702-6.261-8.5709-4.3134-18.12-1.6112-22.089 6.2506-0.84198 1.6679-1.9752 7.8544-1.5778 9.9776 0.98137 5.2435 7.314 15.066 15.216 23.602 2.3474 2.5357 2.891 3.3814 3.2505 5.0579 0.62386 2.909 0.53762 3.9625-0.45679 5.5804-2.2494 3.6597-6.0764 5.9532-12.424 7.446-5.4931 1.2918-14.085 0.80251-19.932-1.135-2.8291-0.93745-3.2146-1.1799-3.3204-2.0877-0.26031-2.2347 5.8542-6.1292 12.294-7.8306 1.712-0.45228 3.469-1.013 3.9045-1.2461 2.613-1.3984-5.139-1.3894-10.001 0.0116-1.4244 0.41047-4.1726 1.3784-6.107 2.151-4.0916 1.6341-9.9725 3.2146-13.412 3.6046-1.9171 0.21734-3.2711 0.0654-6.6311-0.74437-6.6813-1.6101-8.8678-1.2892-6.6918 0.98197 1.1258 1.1751 6.0865 4.3046 6.8234 4.3046 0.22537 0 0.58888 0.3347 0.80781 0.74378 0.50815 0.94949 5.6856 4.2866 11.253 7.2529 3.5996 1.918 5.4022 2.5812 10.638 3.9141 3.4672 0.88264 7.7224 1.7278 9.456 1.8781 1.7336 0.15029 3.1675 0.30072 3.1864 0.33427 0.0189 0.0336 1.7032-0.1969 3.743-0.5121zm-55.67-16.07c0.0817-0.43098-0.25131-0.85888-0.89769-1.1534-0.8518-0.38811-1.1373-0.35853-1.6687 0.17289-0.53858 0.53858-0.55739 0.74197-0.11767 1.2718 0.67722 0.816 2.5119 0.61688 2.6841-0.29131zm7.5598-0.11521c0-0.95213-0.94275-1.7372-1.4622-1.2177-0.30143 0.30141 0.67899 2.2151 1.1348 2.2151 0.18008 0 0.32742-0.44882 0.32742-0.99738z" />
</svg>
);
}
@@ -20,26 +20,6 @@ function formatCalendarDay(day: Date): string {
return `${y}-${m}-${d}`; return `${y}-${m}-${d}`;
} }
function getTodayInTimezone(timezone?: string): {
year: number;
month: number;
day: number;
offset: number;
} {
const now = new Date();
const offset = Math.round(getUTCOffset(now, timezone));
// shifting by the offset makes the UTC getters read the timezone's wall clock
const wallClock = new Date(now.getTime() + offset * 60000);
return {
year: wallClock.getUTCFullYear(),
month: wallClock.getUTCMonth(),
day: wallClock.getUTCDate(),
offset,
};
}
type ReviewActivityCalendarProps = { type ReviewActivityCalendarProps = {
reviewSummary?: ReviewSummary; reviewSummary?: ReviewSummary;
recordingsSummary?: RecordingsSummary; recordingsSummary?: RecordingsSummary;
@@ -57,14 +37,12 @@ export default function ReviewActivityCalendar({
const [weekStartsOn] = useUserPersistence("weekStartsOn", 0); const [weekStartsOn] = useUserPersistence("weekStartsOn", 0);
const disabledDates = useMemo(() => { const disabledDates = useMemo(() => {
// day cells are TZDate in `timezone`, so the cutoff must be a real instant const tomorrow = new Date();
const { year, month, day, offset } = getTodayInTimezone(timezone); tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0);
// midday: ranges match by calendar day, so this dodges DST edges const future = new Date();
const from = new Date(Date.UTC(year, month, day + 1, 12) - offset * 60000); future.setFullYear(tomorrow.getFullYear() + 10);
const to = new Date(from); return { from: tomorrow, to: future };
to.setFullYear(from.getFullYear() + 10); }, []);
return { from, to };
}, [timezone]);
const modifiers = useMemo(() => { const modifiers = useMemo(() => {
const recordingsSet = new Set<string>(); const recordingsSet = new Set<string>();
@@ -204,25 +182,48 @@ export function TimezoneAwareCalendar({
}; };
}, [recordingsSummary]); }, [recordingsSummary]);
// callers pre-shift dates so the local clock reads `timezone`, so boundaries const timezoneOffset = useMemo(
// are built in local time rather than as instants () =>
const { year, month, day } = useMemo( timezone ? Math.round(getUTCOffset(new Date(), timezone)) : undefined,
() => getTodayInTimezone(timezone),
[timezone], [timezone],
); );
const disabledDates = useMemo(() => { const disabledDates = useMemo(() => {
// midday: ranges match by calendar day, so this dodges DST edges const tomorrow = new Date();
const from = new Date(year, month, day + 1, 12);
const to = new Date(from);
to.setFullYear(from.getFullYear() + 10);
return { from, to };
}, [year, month, day]);
const today = useMemo( if (timezoneOffset) {
() => new Date(year, month, day, 12), tomorrow.setHours(
[year, month, day], tomorrow.getHours() + 24,
); tomorrow.getMinutes() + timezoneOffset,
0,
0,
);
} else {
tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0);
}
const future = new Date();
future.setFullYear(tomorrow.getFullYear() + 10);
return { from: tomorrow, to: future };
}, [timezoneOffset]);
const today = useMemo(() => {
if (!timezoneOffset) {
return undefined;
}
const date = new Date();
const utc = Date.UTC(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
);
const todayUtc = new Date(utc);
todayUtc.setMinutes(todayUtc.getMinutes() + timezoneOffset, 0, 0);
return todayUtc;
}, [timezoneOffset]);
return ( return (
<Calendar <Calendar
+11 -1
View File
@@ -70,7 +70,17 @@ export function extractSectionSchema(
// For global level, get from root properties // For global level, get from root properties
if (schemaObj.properties) { if (schemaObj.properties) {
const props = schemaObj.properties; const props = schemaObj.properties;
const sectionProp = props[sectionPath]; let sectionProp = props[sectionPath];
// the model editor edits a single entry of the `models` map, so
// resolve the map value schema since there is no root `model` property
if (!sectionProp && sectionPath === "model") {
const modelsProp = props["models"] as SchemaWithDefinitions | undefined;
const additional = modelsProp?.additionalProperties;
if (additional && typeof additional === "object") {
sectionProp = additional as RJSFSchema;
}
}
if (sectionProp && typeof sectionProp === "object") { if (sectionProp && typeof sectionProp === "object") {
if ("$ref" in sectionProp && typeof sectionProp.$ref === "string") { if ("$ref" in sectionProp && typeof sectionProp.$ref === "string") {
+30 -23
View File
@@ -59,6 +59,7 @@ export interface CameraConfig {
height: number; height: number;
max_disappeared: number; max_disappeared: number;
min_initialized: number; min_initialized: number;
model: string;
stationary: { stationary: {
interval: number; interval: number;
max_frames: { max_frames: {
@@ -394,6 +395,30 @@ export type GenAIAgentConfig = {
runtime_options?: Record<string, unknown>; runtime_options?: Record<string, unknown>;
}; };
export interface ModelConfig {
height: number;
input_pixel_format: string;
input_tensor: string;
labelmap: Record<string, unknown>;
labelmap_path: string | null;
model_type: string;
path: string | null;
width: number;
colormap: { [key: string]: [number, number, number] };
attributes_map: { [key: string]: string[] };
all_attributes: string[];
plus?: {
name: string;
id: string;
trainDate: string;
baseModel: string;
isBaseModel: boolean;
supportedDetectors: string[];
width: number;
height: number;
} | null;
}
export interface FrigateConfig { export interface FrigateConfig {
version: string; version: string;
safe_mode: boolean; safe_mode: boolean;
@@ -446,6 +471,7 @@ export interface FrigateConfig {
height: number | null; height: number | null;
max_disappeared: number | null; max_disappeared: number | null;
min_initialized: number | null; min_initialized: number | null;
model: string | null;
stationary: { stationary: {
interval: number | null; interval: number | null;
max_frames: { max_frames: {
@@ -512,29 +538,10 @@ export interface FrigateConfig {
logs: Record<string, string>; logs: Record<string, string>;
}; };
model: { // legacy single-model block, mirrors the default entry of `models`
height: number; model: ModelConfig;
input_pixel_format: string;
input_tensor: string; models: { [modelKey: string]: ModelConfig };
labelmap: Record<string, unknown>;
labelmap_path: string | null;
model_type: string;
path: string | null;
width: number;
colormap: { [key: string]: [number, number, number] };
attributes_map: { [key: string]: string[] };
all_attributes: string[];
plus?: {
name: string;
id: string;
trainDate: string;
baseModel: string;
isBaseModel: boolean;
supportedDetectors: string[];
width: number;
height: number;
} | null;
};
motion: Record<string, unknown> | null; motion: Record<string, unknown> | null;
+2 -24
View File
@@ -1,12 +1,9 @@
import { IconName } from "@/components/icons/IconPicker"; import { IconName } from "@/components/icons/IconPicker";
import SkunkIcon from "@/components/icons/SkunkIcon";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { EventType } from "@/types/search"; import { EventType } from "@/types/search";
import { BsPersonWalking } from "react-icons/bs"; import { BsPersonWalking } from "react-icons/bs";
import { import {
FaAmazon, FaAmazon,
FaBaby,
FaBabyCarriage,
FaBicycle, FaBicycle,
FaBus, FaBus,
FaCarSide, FaCarSide,
@@ -27,8 +24,6 @@ import {
FaUsps, FaUsps,
} from "react-icons/fa"; } from "react-icons/fa";
import { import {
GiBarbecue,
GiCow,
GiDeer, GiDeer,
GiFox, GiFox,
GiGoat, GiGoat,
@@ -37,9 +32,7 @@ import {
GiPostStamp, GiPostStamp,
GiRabbit, GiRabbit,
GiRaccoonHead, GiRaccoonHead,
GiRat,
GiSailboat, GiSailboat,
GiSeatedMouse,
GiSoundWaves, GiSoundWaves,
GiSquirrel, GiSquirrel,
} from "react-icons/gi"; } from "react-icons/gi";
@@ -47,7 +40,6 @@ import { LuBox, LuLassoSelect, LuScanBarcode } from "react-icons/lu";
import * as LuIcons from "react-icons/lu"; import * as LuIcons from "react-icons/lu";
import { MdRecordVoiceOver } from "react-icons/md"; import { MdRecordVoiceOver } from "react-icons/md";
import { PiBirdFill } from "react-icons/pi"; import { PiBirdFill } from "react-icons/pi";
import { HiMiniTruck } from "react-icons/hi2";
export function getAttributeLabels(config?: FrigateConfig) { export function getAttributeLabels(config?: FrigateConfig) {
if (!config) { if (!config) {
@@ -82,12 +74,6 @@ export function getIconForLabel(
switch (label) { switch (label) {
// objects // objects
case "baby":
return <FaBaby key={iconKey} className={className} />;
case "baby_stroller":
return <FaBabyCarriage key={iconKey} className={className} />;
case "bbq_grill":
return <GiBarbecue key={iconKey} className={className} />;
case "bear": case "bear":
return <GiPolarBear key={iconKey} className={className} />; return <GiPolarBear key={iconKey} className={className} />;
case "bicycle": case "bicycle":
@@ -104,8 +90,6 @@ export function getIconForLabel(
return <FaCarSide key={iconKey} className={className} />; return <FaCarSide key={iconKey} className={className} />;
case "cat": case "cat":
return <FaCat key={iconKey} className={className} />; return <FaCat key={iconKey} className={className} />;
case "cow":
return <GiCow key={iconKey} className={className} />;
case "deer": case "deer":
return <GiDeer key={iconKey} className={className} />; return <GiDeer key={iconKey} className={className} />;
case "animal": case "animal":
@@ -114,8 +98,6 @@ export function getIconForLabel(
return <FaDog key={iconKey} className={className} />; return <FaDog key={iconKey} className={className} />;
case "fox": case "fox":
return <GiFox key={iconKey} className={className} />; return <GiFox key={iconKey} className={className} />;
case "garbage_truck":
return <HiMiniTruck key={iconKey} className={className} />;
case "goat": case "goat":
return <GiGoat key={iconKey} className={className} />; return <GiGoat key={iconKey} className={className} />;
case "horse": case "horse":
@@ -132,22 +114,18 @@ export function getIconForLabel(
return <LuBox key={iconKey} className={className} />; return <LuBox key={iconKey} className={className} />;
case "person": case "person":
return <BsPersonWalking key={iconKey} className={className} />; return <BsPersonWalking key={iconKey} className={className} />;
case "possum":
return <GiSeatedMouse key={iconKey} className={className} />;
case "rabbit": case "rabbit":
return <GiRabbit key={iconKey} className={className} />; return <GiRabbit key={iconKey} className={className} />;
case "raccoon": case "raccoon":
return <GiRaccoonHead key={iconKey} className={className} />; return <GiRaccoonHead key={iconKey} className={className} />;
case "robot_lawnmower": case "robot_lawnmower":
return <FaHockeyPuck key={iconKey} className={className} />; return <FaHockeyPuck key={iconKey} className={className} />;
case "rodent":
return <GiRat key={iconKey} className={className} />;
case "sports_ball": case "sports_ball":
return <FaFootballBall key={iconKey} className={className} />; return <FaFootballBall key={iconKey} className={className} />;
case "skunk": case "skunk":
return <SkunkIcon key={iconKey} className={className} />;
case "squirrel":
return <GiSquirrel key={iconKey} className={className} />; return <GiSquirrel key={iconKey} className={className} />;
case "squirrel":
return <LuIcons.LuSquirrel key={iconKey} className={className} />;
case "umbrella": case "umbrella":
return <FaUmbrella key={iconKey} className={className} />; return <FaUmbrella key={iconKey} className={className} />;
case "waste_bin": case "waste_bin":
@@ -115,8 +115,10 @@ const STATUS_BAR_KEY = "detectors_and_model";
const EMPTY_PENDING: Record<string, ConfigSectionData> = {}; const EMPTY_PENDING: Record<string, ConfigSectionData> = {};
const deriveInitialState = (config: FrigateConfig): PageState => { const deriveInitialState = (config: FrigateConfig): PageState => {
const plusModelId = config.model?.plus?.id; // this view edits the default model; other named models are untouched
const modelPath = config.model?.path; const defaultModel = config.models?.default ?? config.model;
const plusModelId = defaultModel?.plus?.id;
const modelPath = defaultModel?.path;
const plusEnabled = Boolean(config.plus?.enabled); const plusEnabled = Boolean(config.plus?.enabled);
// The reliable signal that a Plus model is currently active is the // The reliable signal that a Plus model is currently active is the
@@ -136,10 +138,8 @@ const deriveInitialState = (config: FrigateConfig): PageState => {
modelTab = "custom"; modelTab = "custom";
} }
const { plus: _plus, ...modelWithoutPlus } = (config.model ?? {}) as Record< const { plus: _plus, ...modelWithoutPlus } = (defaultModel ??
string, {}) as unknown as Record<string, unknown>;
unknown
>;
// If a Plus model is active, the resolved `model.path` is auto-derived from // If a Plus model is active, the resolved `model.path` is auto-derived from
// `plus.id` — drop it so the Custom tab starts clean and doesn't silently // `plus.id` — drop it so the Custom tab starts clean and doesn't silently
// re-save the same Plus model when the user thinks they switched modes. // re-save the same Plus model when the user thinks they switched modes.
@@ -476,7 +476,7 @@ export default function DetectorsAndModelSettingsView({
try { try {
await axios.put("config/set", { await axios.put("config/set", {
requires_restart: 0, requires_restart: 0,
config_data: { detectors: null, model: null }, config_data: { detectors: null, models: { default: null } },
}); });
preCleared = true; preCleared = true;
} catch { } catch {
@@ -488,7 +488,7 @@ export default function DetectorsAndModelSettingsView({
requires_restart: 0, requires_restart: 0,
config_data: { config_data: {
detectors: sanitizedDetectors, detectors: sanitizedDetectors,
model: modelPayload, models: { default: modelPayload },
}, },
}); });
@@ -541,7 +541,7 @@ export default function DetectorsAndModelSettingsView({
snapshot.detectors, snapshot.detectors,
detectorHiddenFields, detectorHiddenFields,
), ),
model: restoreModel, models: { default: restoreModel },
}, },
}); });
} catch { } catch {