Compare commits

..
Author SHA1 Message Date
dependabot[bot]andGitHub 286bf68529 Bump fast-uri from 3.1.2 to 3.1.4 in /docs
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.2 to 3.1.4.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.4)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 13:21:10 +00:00
243 changed files with 1863 additions and 10782 deletions
-3
View File
@@ -8,7 +8,6 @@ amdgpu
analyzeduration
Annke
apexcharts
Aqara
arange
argmax
argmin
@@ -65,7 +64,6 @@ dsize
dtype
ECONNRESET
edgetpu
Eufy
facenet
fastapi
faststart
@@ -84,7 +82,6 @@ frontdoor
fstype
fullchain
fullscreen
gatekeep
genai
generativeai
genpts
@@ -10,11 +10,8 @@ body:
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/
[discussions]: https://github.com/blakeblackshear/frigate/discussions
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea
id: description
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.
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
[docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea
id: description
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.
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
[docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea
id: description
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.
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
[docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea
id: description
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.
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
[docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea
id: description
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.
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
[docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea
id: description
attributes:
-3
View File
@@ -10,12 +10,9 @@ body:
**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
[docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: textarea
id: description
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.**
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
[prs]: https://www.github.com/blakeblackshear/frigate/pulls
[docs]: https://docs.frigate.video
[faq]: https://github.com/blakeblackshear/frigate/discussions/12724
[ai]: https://docs.frigate.video
[ai-policy]: https://github.com/blakeblackshear/frigate/blob/dev/AI_POLICY.md
- type: checkboxes
attributes:
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**
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
-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.
All participation in this project, including pull requests, issues, and discussions, is covered by our [AI policy](AI_POLICY.md).
## Before you start
### Bugfixes
@@ -23,16 +21,28 @@ Before writing code for a new feature:
## 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.
- 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.
If AI is used to generate any portion of the code, contributors must adhere to the following requirements:
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
+19 -83
View File
@@ -1,14 +1,10 @@
"""Convert the default SSDLite MobileNet v2 model to OpenVINO IR.
Replaces the legacy openvino-dev Model Optimizer conversion. The TensorFlow
frontend translates the Object Detection API pre and post processors literally,
producing per-class NonMaxSuppression, NonZero ops and map loops with data
dependent shapes that the GPU plugin handles very badly. Both are cut out the
way ssd_v2_support.json used to do it: the preprocessor is an identity at the
native 300x300 input, and the postprocessor becomes a single fused
DetectionOutput. The result is the [1, 1, 100, 7] tensor that Frigate's
OpenVINO detector expects, with the input flipped to BGR to match the legacy
reverse_input_channels behavior.
frontend converts the Object Detection API frozen graph natively; the four TF
outputs are then repacked into the single [1, 1, 100, 7] DetectionOutput-style
tensor that Frigate's OpenVINO detector expects, and the input is flipped to
BGR to match the legacy reverse_input_channels behavior.
"""
import numpy as np
@@ -16,91 +12,31 @@ import openvino as ov
from openvino import opset8 as ops
from openvino.preprocess import PrePostProcessor
MODEL_DIR = "/models/ssdlite_mobilenet_v2_coco_2018_05_09"
OUTPUT_PATH = "/models/ssdlite_mobilenet_v2.xml"
INPUT_SHAPE = [1, 300, 300, 3]
# faster_rcnn_box_coder divides the deltas by pipeline.config's y/x/height/width
# scales of 10/10/5/5, which DetectionOutput expresses as per-prior variances.
BOX_VARIANCES = np.float32([0.1, 0.1, 0.2, 0.2])
model = ov.convert_model(
f"{MODEL_DIR}/frozen_inference_graph.pb",
input=[("image_tensor:0", INPUT_SHAPE)],
"/models/ssdlite_mobilenet_v2_coco_2018_05_09/frozen_inference_graph.pb",
input=[("image_tensor:0", [1, 300, 300, 3])],
)
nodes = {op.get_friendly_name(): op for op in model.get_ordered_ops()}
parameter = model.get_parameters()[0]
# rows of (image_id, class_id, score, xmin, ymin, xmax, ymax)
boxes = model.output("detection_boxes:0").get_node().input_value(0)
classes = model.output("detection_classes:0").get_node().input_value(0)
scores = model.output("detection_scores:0").get_node().input_value(0)
preprocessor = nodes["Preprocessor/map/TensorArrayStack/TensorArrayGatherV3"]
box_deltas = nodes["Postprocessor/Reshape_1"].output(0)
class_scores = nodes["Postprocessor/convert_scores"].output(0)
anchors_output = nodes["Postprocessor/Reshape"].output(0)
# (ymin,xmin,ymax,xmax) -> (xmin,ymin,xmax,ymax)
boxes = ops.gather(boxes, [1, 0, 3, 2], 2)
classes = ops.unsqueeze(classes, 2)
scores = ops.unsqueeze(scores, 2)
image_id = ops.multiply(scores, np.float32(0.0))
# The anchors only depend on the static input shape, so fold them into a
# constant and drop the generator subgraph with the rest of the postprocessor.
probe = ov.Core().compile_model(
ov.Model([anchors_output, preprocessor.output(0)], [parameter], "probe"), "CPU"
)
probe_input = np.random.default_rng(0).integers(0, 255, INPUT_SHAPE, dtype=np.uint8)
anchors, resized = (out.copy() for out in probe([probe_input]).values())
assert np.allclose(resized, probe_input, atol=1e-3), (
"preprocessor is not an identity at 300x300, it cannot be bypassed"
)
image = ops.convert(parameter, "f32")
for consumer in list(preprocessor.output(0).get_target_inputs()):
consumer.replace_source_output(image.output(0))
# (ymin, xmin, ymax, xmax) -> (xmin, ymin, xmax, ymax)
priors = anchors[:, [1, 0, 3, 2]].astype(np.float32).reshape(-1)
variances = np.tile(BOX_VARIANCES, len(anchors))
proposals = ops.constant(np.stack([priors, variances])[np.newaxis])
# (ty, tx, th, tw) -> (dx, dy, dw, dh) for the CENTER_SIZE decode
box_logits = ops.reshape(ops.gather(box_deltas, [1, 0, 3, 2], 1), [1, -1], False)
class_preds = ops.reshape(class_scores, [1, -1], False)
detections = ops.detection_output(
box_logits,
class_preds,
proposals,
{
"background_label_id": 0,
"top_k": 100,
"keep_top_k": [100],
"nms_threshold": 0.6,
"confidence_threshold": 0.3,
"code_type": "caffe.PriorBoxParameter.CENTER_SIZE",
"share_location": True,
"variance_encoded_in_target": False,
"normalized": True,
"clip_before_nms": False,
"clip_after_nms": True,
"decrease_label_id": False,
},
)
detections = ops.concat([image_id, classes, scores, boxes], 2)
detections = ops.unsqueeze(detections, 1)
detections.output(0).get_tensor().set_names({"detection_out"})
model = ov.Model([detections], [parameter], "ssdlite_mobilenet_v2")
model = ov.Model([detections], model.get_parameters(), "ssdlite_mobilenet_v2")
ppp = PrePostProcessor(model)
ppp.input().tensor().set_layout(ov.Layout("NHWC"))
ppp.input().preprocess().reverse_channels()
model = ppp.build()
# Fail the build rather than silently ship the dynamically shaped graph again.
op_types = [op.get_type_name() for op in model.get_ordered_ops()]
assert op_types.count("DetectionOutput") == 1, "postprocessor was not fused"
for dynamic_op in ("NonMaxSuppression", "NonZero", "Loop", "TensorIterator"):
assert dynamic_op not in op_types, f"{dynamic_op} left in the graph"
output_shape = model.outputs[0].get_partial_shape()
assert output_shape.is_static and list(output_shape) == [1, 1, 100, 7], (
f"unexpected detector output shape {output_shape}"
)
ov.save_model(model, OUTPUT_PATH, compress_to_fp16=True)
ov.save_model(model, "/models/ssdlite_mobilenet_v2.xml", compress_to_fp16=True)
+2
View File
@@ -79,5 +79,7 @@ sherpa-onnx==1.12.*
faster-whisper==1.1.*
librosa==0.11.*
soundfile==0.13.*
# DeGirum detector
degirum == 0.16.*
# Memory profiling
memray == 1.15.*
+75
View File
@@ -1269,3 +1269,78 @@ axengine:
input_dtype: int
input_pixel_format: bgr
labelmap_path: /labelmap/coco-80.txt
degirumAiServer:
title: DeGirum AI Server
models:
- key: ai-server-inference
label: AI Server Inference
recommended: true
download: |-
Launch a DeGirum AI server as a Docker container, then point the detector at it. Add this to your `docker-compose.yml`:
```yaml
degirum_detector:
container_name: degirum
image: degirum/aiserver:latest
privileged: true
ports:
- "8778:8778"
```
Set `location` to the server's service name, container name, or `host:port`.
ui: |
Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**.
| Field | Value |
| --- | --- |
| **Location** | `degirum` |
| **Zoo** | `degirum/public` |
| **Token** | your AI Hub token (optional for the public zoo) |
yaml: |
degirum_detector:
type: degirum
location: degirum
zoo: degirum/public
token: dg_example_token
degirumLocal:
title: DeGirum Local
models:
- key: local-inference
label: Local Inference
recommended: true
download: Run hardware directly inside the Frigate container with `@local`, removing the AI server hop. The matching device runtime (e.g. the Hailo runtime) must be installed in the container; confirm it with `degirum sys-info`.
ui: |
Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**.
| Field | Value |
| --- | --- |
| **Location** | `@local` |
| **Zoo** | `degirum/public` |
| **Token** | your AI Hub token (optional for the public zoo) |
yaml: |
degirum_detector:
type: degirum
location: @local
zoo: degirum/public
token: dg_example_token
degirumCloud:
title: DeGirum AI Hub Cloud
models:
- key: ai-hub-cloud-inference
label: AI Hub Cloud Inference
recommended: true
download: Run inferences on DeGirum's [AI Hub](https://hub.degirum.com) cloud with `@cloud`. Sign up, create an access token, and set it as `token`. Network latency may require lowering your detection fps.
ui: |
Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**.
| Field | Value |
| --- | --- |
| **Location** | `@cloud` |
| **Zoo** | `degirum/public` |
| **Token** | your AI Hub token (optional for the public zoo) |
yaml: |
degirum_detector:
type: degirum
location: @cloud
zoo: degirum/public
token: dg_example_token
@@ -981,9 +981,7 @@ cameras:
# Optional: Adjust sort order of cameras in the UI. Larger numbers come later (default: shown below)
# By default the cameras are sorted alphabetically.
order: 0
# Optional: Whether or not to show the camera on the default All Cameras live dashboard.
# The camera is still available everywhere else, including camera groups and settings
# (default: shown below)
# Optional: Whether or not to show the camera in the Frigate UI (default: shown below)
dashboard: True
# Optional: Whether this camera is visible in review (the review page and its camera
# filter, motion review, and the history view) (default: shown below)
@@ -293,10 +293,6 @@ networking:
This setting is for advanced users. For the majority of use cases it's recommended to change the `ports` section of your Docker compose file or use the Docker `run` `--publish` option instead, e.g. `-p 443:8971`. Changing Frigate's ports may break some integrations.
The internal and external ports must be different port numbers, and Frigate will refuse to start otherwise. Requests arriving on the internal port are treated as authenticated admins, so pointing both at the same port would remove authentication from the external one.
Nginx binds these ports when it starts, so port changes only take effect after Frigate restarts.
:::
### Customizing the Nginx configuration
+1 -1
View File
@@ -256,7 +256,7 @@ The only field that is valid at the camera level is `enabled`.
#### Live transcription
The single camera Live view in the Frigate UI supports live transcription of audio for streams defined with the `audio` role. Use the Enable/Disable Live Audio Transcription button/switch to toggle transcription processing, or toggle it outside of the UI with the [`frigate/<camera_name>/audio_transcription/set`](/integrations/mqtt#frigatecamera_nameaudio_transcriptionset) MQTT topic or the HTTP API. When speech is heard, the UI will display a black box over the top of the camera stream with text. The MQTT topic `frigate/<camera_name>/audio/transcription` will also be updated in real-time with transcribed text.
The single camera Live view in the Frigate UI supports live transcription of audio for streams defined with the `audio` role. Use the Enable/Disable Live Audio Transcription button/switch to toggle transcription processing. When speech is heard, the UI will display a black box over the top of the camera stream with text. The MQTT topic `frigate/<camera_name>/audio/transcription` will also be updated in real-time with transcribed text.
Results can be error-prone due to a number of factors, including:
@@ -11,7 +11,7 @@ Object classification allows you to train a custom MobileNetV2 classification mo
:::info
Training a custom object classification model requires an internet connection to download MobileNetV2 base weights. By default these weights are not cached in `/config/`, so they are downloaded again after the container is recreated. Once trained, the model runs fully offline. See [Network Requirements](/frigate/network_requirements#one-time-model-downloads) for details.
Training a custom object classification model requires a one-time internet connection to download MobileNetV2 base weights. Once trained, the model runs fully offline. See [Network Requirements](/frigate/network_requirements#one-time-model-downloads) for details.
:::
@@ -11,7 +11,7 @@ State classification allows you to train a custom MobileNetV2 classification mod
:::info
Training a custom state classification model requires an internet connection to download MobileNetV2 base weights. By default these weights are not cached in `/config/`, so they are downloaded again after the container is recreated. Once trained, the model runs fully offline. See [Network Requirements](/frigate/network_requirements#one-time-model-downloads) for details.
Training a custom state classification model requires a one-time internet connection to download MobileNetV2 base weights. Once trained, the model runs fully offline. See [Network Requirements](/frigate/network_requirements#one-time-model-downloads) for details.
:::
+3 -103
View File
@@ -6,7 +6,6 @@ title: Configuring Generative AI
import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
import FaqItem from "@site/src/components/FaqItem";
## 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:
<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
genai:
my_provider: # any name you like
@@ -38,9 +25,6 @@ genai:
- 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`).
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
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 |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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. |
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
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>
</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)
- Gemini - [Google AI Studio](https://aistudio.google.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.
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 -3
View File
@@ -67,6 +67,4 @@ If your stream won't play, has no audio, uses excessive CPU, or otherwise misbeh
## Homekit Configuration
To export camera streams to HomeKit, Frigate must be configured in docker to use `host` networking mode. HomeKit settings are stored in `/config/go2rtc_homekit.yml` rather than in your Frigate config, and are edited through the go2rtc config editor at `http://<frigate_host>:1984/editor.html`. Pairings are saved back to that file automatically.
See the [HomeKit integration docs](/integrations/homekit) for the full setup, including the video and audio requirements HomeKit places on the stream.
To add camera streams to Homekit Frigate must be configured in docker to use `host` networking mode. Once that is done, you can use the go2rtc WebUI (accessed via port 1984, which is disabled by default) to export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`.
+1 -1
View File
@@ -334,7 +334,7 @@ When your browser runs into problems playing back your camera streams, it will l
- **stalled**
- What it means: Playback has stalled because the player has fallen too far behind live (extended buffering or no data arriving).
- What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval: shorter intervals make playback start and recover faster. You can also try increasing the timeout value in <NavPath path="Settings > UI" /> .
- What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval: shorter intervals make playback start and recover faster. You can also try increasing the timeout value in the UI pane of Frigate's settings.
- Possible console messages from the player code:
- `Buffer time (10 seconds) exceeded, browser may not be playing media correctly.`
+82 -8
View File
@@ -24,6 +24,7 @@ Frigate supports multiple different detectors that work on different types of ha
- [Coral EdgeTPU](#edge-tpu-detector): The Google Coral EdgeTPU is available in USB, Mini PCIe, and m.2 formats allowing for a wide range of compatibility with devices.
- [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is available in m.2 format with a HAT for RPi devices, offering a wide range of compatibility with devices.
- <CommunityBadge /> [MemryX](#memryx-mx3): The MX3 Acceleration module is available in m.2 format, offering broad compatibility across various platforms.
- <CommunityBadge /> [DeGirum](#degirum): Service for using hardware devices in the cloud or locally. Hardware and models provided on the cloud on [their website](https://hub.degirum.com).
**AMD**
@@ -297,14 +298,6 @@ detectors:
:::
### Intel NPU host requirements {#intel-npu-requirements}
The NPU firmware is loaded by the host kernel and is not part of the Frigate image. Everything else the NPU needs is bundled in the container, so host NPU libraries should never be mounted in.
Frigate bundles a specific version of Intel's [linux-npu-driver](https://github.com/intel/linux-npu-driver/releases), and the host firmware must come from that release or a newer one. Firmware older than the bundled driver may fail with `MAPPED_INFERENCE_VERSION is NOT compatible with the ELF`, where `Expected` is the version the firmware supports and `received` is the version the bundled compiler produced. Distributions often package older firmware than the driver Frigate ships, so check the build date on the host with `sudo dmesg | grep -i vpu` and update it there if needed.
Intel NPUs cannot be used under Home Assistant OS, which does not include the NPU firmware.
### Configuration {#configuration-openvino}
<ModelConfigDropdown detectorTitle="OpenVINO" models={objectDetectorsModels.openvino.models} />
@@ -762,6 +755,87 @@ Explanation of the parameters:
- **example**: Specifying `output_name = "frigate-{quant}-{input_basename}-{soc}-v{tk_version}"` could result in a model called `frigate-i8-my_model-rk3588-v2.3.0.rknn`.
- `config`: Configuration passed to `rknn-toolkit2` for model conversion. For an explanation of all available parameters have a look at section "2.2. Model configuration" of [this manual](https://github.com/MarcA711/rknn-toolkit2/releases/download/v2.3.2/03_Rockchip_RKNPU_API_Reference_RKNN_Toolkit2_V2.3.2_EN.pdf).
## DeGirum
DeGirum is a detector that can use any type of hardware listed on [their website](https://hub.degirum.com). DeGirum can be used with local hardware through a DeGirum AI Server, or through the use of `@local`. You can also connect directly to DeGirum's AI Hub to run inferences. **Please Note:** This detector _cannot_ be used for commercial purposes.
### Configuration {#configuration-degirum}
#### AI Server Inference
Before starting with the config file for this section, you must first launch an AI server. DeGirum has an AI server ready to use as a docker container. Add this to your `docker-compose.yml` to get started:
```yaml
degirum_detector:
container_name: degirum
image: degirum/aiserver:latest
privileged: true
ports:
- "8778:8778"
```
All supported hardware will automatically be found on your AI server host as long as relevant runtimes and drivers are properly installed on your machine. Refer to [DeGirum's docs site](https://docs.degirum.com/pysdk/runtimes-and-drivers) if you have any trouble.
Once completed, configure the detector as follows:
<ModelConfigDropdown detectorTitle="DeGirum" models={objectDetectorsModels.degirumAiServer.models} />
Setting up a model in the `config.yml` is similar to setting up an AI server.
You can set it to:
- A model listed on the [AI Hub](https://hub.degirum.com), given that the correct zoo name is listed in your detector
- If this is what you choose to do, the correct model will be downloaded onto your machine before running.
- A local directory acting as a zoo. See DeGirum's docs site [for more information](https://docs.degirum.com/pysdk/user-guide-pysdk/organizing-models#model-zoo-directory-structure).
- A path to some model.json.
```yaml
model:
path: ./mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1 # directory to model .json and file
width: 300 # width is in the model name as the first number in the "int"x"int" section
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
It is also possible to eliminate the need for an AI server and run the hardware directly. The benefit of this approach is that you eliminate any bottlenecks that occur when transferring prediction results from the AI server docker container to the frigate one. However, the method of implementing local inference is different for every device and hardware combination, so it's usually more trouble than it's worth. A general guideline to achieve this would be:
1. Ensuring that the frigate docker container has the runtime you want to use. So for instance, running `@local` for Hailo means making sure the container you're using has the Hailo runtime installed.
2. To double check the runtime is detected by the DeGirum detector, make sure the `degirum sys-info` command properly shows whatever runtimes you mean to install.
3. Create a DeGirum detector in your configuration.
<ModelConfigDropdown detectorTitle="DeGirum" models={objectDetectorsModels.degirumLocal.models} />
Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file.
```yaml
model:
path: mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1
width: 300 # width is in the model name as the first number in the "int"x"int" section
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
If you do not possess whatever hardware you want to run, there's also the option to run cloud inferences. Do note that your detection fps might need to be lowered as network latency does significantly slow down this method of detection. For use with Frigate, we highly recommend using a local AI server as described above. To set up cloud inferences,
1. Sign up at [DeGirum's AI Hub](https://hub.degirum.com).
2. Get an access token.
3. Create a DeGirum detector in your configuration.
<ModelConfigDropdown detectorTitle="DeGirum" models={objectDetectorsModels.degirumCloud.models} />
Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file.
```yaml
model:
path: mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1
width: 300 # width is in the model name as the first number in the "int"x"int" section
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
Hardware accelerated object detection is supported on the following SoCs:
+1 -1
View File
@@ -126,7 +126,7 @@ Only the fields you explicitly set in a profile override are applied. All other
## Activating Profiles
Profiles can be activated and deactivated via the Frigate UI, [MQTT](/integrations/mqtt#frigateprofileset), the [HTTP API](../integrations/api/camera-set-camera-camera-name-set-feature-sub-command-put.api.mdx), or the Home Assistant integration.
Profiles can be activated and deactivated via the Frigate UI, [MQTT](/integrations/mqtt#frigateprofileset), or the Home Assistant integration.
In the Frigate UI, open the Settings cog and select **Profiles** from the submenu to see all defined profiles. From there you can activate any profile or deactivate the current one. The active profile is indicated in the UI so you always know which profile is in effect.
+5 -25
View File
@@ -34,12 +34,6 @@ The following models are downloaded automatically the first time their associate
| [Custom classification](/configuration/custom_classification/state_classification) (training) | MobileNetV2 ImageNet base weights (via Keras) | Google storage |
| [Audio transcription](/configuration/advanced/system) | Whisper or Sherpa-ONNX streaming model | HuggingFace / OpenAI |
:::note
The MobileNetV2 base weights are the one exception to the `/config/model_cache/` rule. They are also the only entry that is not downloaded when the feature is enabled: Frigate fetches them when a training run actually starts.
:::
### Hardware-Specific Detector Models
If you are using one of the following hardware detectors and have not provided your own model file, a default model will be downloaded on first startup:
@@ -81,7 +75,7 @@ If your Frigate instance has restricted internet access, you can point model dow
| `HF_ENDPOINT` | `https://huggingface.co` | Semantic search, Sherpa-ONNX, AXEngine models |
| `GITHUB_ENDPOINT` | `https://github.com` | Face recognition, LPR, RKNN models |
| `GITHUB_RAW_ENDPOINT` | `https://raw.githubusercontent.com` | Bird classification |
| `TF_KERAS_MOBILENET_V2_WEIGHTS_URL` | Unset (Keras uses its own default) | Custom classification training |
| `TF_KERAS_MOBILENET_V2_WEIGHTS_URL` | Google storage (Keras default) | Custom classification training |
## Optional Cloud Services
@@ -153,23 +147,9 @@ When running as a Home Assistant App, the go2rtc startup script queries the loca
To run Frigate in an air-gapped or offline environment:
1. **Pre-download models**: Start Frigate with internet access once with all desired features enabled. Models will be cached in `/config/model_cache/`.
2. **Pre-download the training base weights**: If you plan to train custom classification models, set `TF_KERAS_MOBILENET_V2_WEIGHTS_URL` before training, then run one training job while online. Without this variable the base weights are cached outside `/config/` and are lost whenever the container is recreated, so a later training run will fail offline. If the machine never has internet access, copy the weights in manually as described below.
3. **Disable version check**: Set `telemetry.version_check: false` in your configuration.
4. **Block outbound model requests**: Set the `HF_HUB_OFFLINE=1` and `TRANSFORMERS_OFFLINE=1` environment variables to prevent HuggingFace and Transformers from attempting any network requests.
5. **Avoid cloud features**: Do not configure Frigate+, Generative AI providers that require internet, or cloud MQTT brokers.
6. **Use local model mirrors**: If limited internet is available, set the `HF_ENDPOINT`, `GITHUB_ENDPOINT`, `GITHUB_RAW_ENDPOINT`, and `TF_KERAS_MOBILENET_V2_WEIGHTS_URL` environment variables to point to local mirrors.
2. **Disable version check**: Set `telemetry.version_check: false` in your configuration.
3. **Block outbound model requests**: Set the `HF_HUB_OFFLINE=1` and `TRANSFORMERS_OFFLINE=1` environment variables to prevent HuggingFace and Transformers from attempting any network requests.
4. **Avoid cloud features**: Do not configure Frigate+, Generative AI providers that require internet, or cloud MQTT brokers.
5. **Use local model mirrors**: If limited internet is available, set the `HF_ENDPOINT`, `GITHUB_ENDPOINT`, and `GITHUB_RAW_ENDPOINT` environment variables to point to local mirrors.
After these steps, Frigate will operate with no outbound internet connections.
### Manually Copying the Training Base Weights
On a machine with internet access, download the weights:
```bash
curl -L -o mobilenet_v2_weights.h5 \
"https://storage.googleapis.com/tensorflow/keras-applications/mobilenet_v2/mobilenet_v2_weights_tf_dim_ordering_tf_kernels_0.35_224_no_top.h5"
```
Copy the file into your Frigate config volume as `/config/model_cache/MobileNet/mobilenet_v2_weights.h5`, keeping that exact filename, then set the environment variable `TF_KERAS_MOBILENET_V2_WEIGHTS_URL` in your Docker compose file to the URL above and restart Frigate.
The variable must be set even though the URL is never contacted. If it is unset, Frigate ignores the copied file and asks Keras to download the weights instead.
+23 -88
View File
@@ -3,100 +3,35 @@ id: homekit
title: HomeKit
---
Frigate cameras can be exported to Apple HomeKit through go2rtc. Each exported camera appears as an accessory in the Apple Home app on your iOS, iPadOS, macOS, and tvOS devices.
Frigate cameras can be integrated with Apple HomeKit through go2rtc. This allows you to view your camera streams directly in the Apple Home app on your iOS, iPadOS, macOS, and tvOS devices.
## Overview
Exporting cameras is handled entirely through go2rtc, which is embedded in Frigate. go2rtc provides the necessary HomeKit Accessory Protocol (HAP) server, so your camera is published to HomeKit as an accessory in its own right.
HomeKit integration is handled entirely through go2rtc, which is embedded in Frigate. go2rtc provides the necessary HomeKit Accessory Protocol (HAP) server to expose your cameras to HomeKit.
:::note
## Setup
This is the opposite of importing a HomeKit camera. go2rtc can also pair with an existing HomeKit camera (Aqara, Eve, Eufy, and similar) and use it as a stream source, which is what the `add` page of the go2rtc WebUI is for. That page discovers HomeKit accessories on your network and will not list your Frigate cameras. It is not used for exporting.
All HomeKit configuration and pairing should be done through the **go2rtc WebUI**.
:::
### Accessing the go2rtc WebUI
The go2rtc WebUI is available at:
```
http://<frigate_host>:1984
```
Replace `<frigate_host>` with the IP address or hostname of your Frigate server.
### Pairing Cameras
1. Navigate to the go2rtc WebUI at `http://<frigate_host>:1984`
2. Use the `add` section to add a new camera to HomeKit
3. Follow the on-screen instructions to generate pairing codes for your cameras
## Requirements
- Frigate must be running with `network_mode: host` so that HomeKit can discover your cameras over mDNS
- Your Apple device must be on the same network as Frigate
- Port 1984 must be accessible so you can reach the go2rtc WebUI
HomeKit also places strict limits on the stream itself. go2rtc passes your stream through without resizing or re-encoding it, so the stream you export must already meet these requirements:
- **Video:** H.264 at 1920x1080, 1280x720, or 320x240
- **Audio:** Opus, mono, 16 kHz
A camera's full resolution stream usually does not qualify. See [Exporting a compatible stream](#exporting-a-compatible-stream) below.
## Configuration
HomeKit settings are stored in `/config/go2rtc_homekit.yml`. This is a separate file from your Frigate config, because go2rtc needs to write your pairings back to it when you pair a device.
Edit it using the go2rtc config editor, which writes to that file directly:
```
http://<frigate_host>:1984/editor.html
```
Replace `<frigate_host>` with the IP address or hostname of your Frigate server. The editor will be empty until you add a HomeKit section, since this file holds only your HomeKit settings and not the rest of your go2rtc config.
:::warning
Do not put the `homekit:` section in the `go2rtc:` section of your Frigate config.
Frigate regenerates that config on every startup, so go2rtc cannot save your pairings to it. Pairing will appear to succeed and then fail after the next restart with `PairVerify with unknown client_id`. If the section exists in both places, your saved pairings are erased on every restart.
:::
Add an entry for each camera you want to export. The key must match the name of a go2rtc stream, and the pin must be 8 digits. This is the number the Home app calls the setup code:
```yaml
homekit:
front_door:
name: Front Door
pin: "12345678"
```
If the key does not match a go2rtc stream, go2rtc logs `[homekit] missing stream:` at startup and the camera will not appear in the Home app.
:::note
go2rtc derives each accessory's HomeKit identity from this key, so renaming it later means the camera appears as a new accessory and has to be paired again. Settle on the name before you pair.
:::
Frigate keeps only the `homekit:` section of this file when it starts, so do not store streams or other go2rtc settings in it.
### Exporting a compatible stream
If a camera's stream does not meet the requirements listed above, define a scaled restream in your Frigate config and point HomeKit at that stream instead of the original:
```yaml
go2rtc:
streams:
front_door:
- rtsp://user:password@192.168.1.50:554/stream
front_door_homekit:
- "ffmpeg:front_door#video=h264#width=1280#height=720#audio=opus/16000"
```
```yaml
# /config/go2rtc_homekit.yml
homekit:
front_door_homekit:
name: Front Door
pin: "12345678"
```
Add `#hardware=cuda`, `#hardware=vaapi`, or the appropriate value for your system to transcode using your GPU. Note that NVENC cannot encode H.264 wider than 4096 pixels, so very wide streams must be scaled down as shown above rather than only re-encoded.
## Pairing Cameras
1. Restart Frigate after adding the `homekit:` section
2. In the Apple Home app, choose **Add Accessory**, then **More options** to enter a code manually
3. Select your camera and enter the pin you configured as the setup code
4. Confirm that a `pairings:` list now appears under the camera in `/config/go2rtc_homekit.yml`
Pairings are saved back to that file automatically. If step 4 shows no `pairings:` list, check the Frigate log for `[homekit] can't save`, which means the `homekit:` section is missing from `/config/go2rtc_homekit.yml`.
For detailed go2rtc configuration options, refer to the [go2rtc documentation](https://github.com/AlexxIT/go2rtc).
- Frigate must be accessible on your local network using host network_mode
- Your iOS device must be on the same network as Frigate
- Port 1984 must be accessible for the go2rtc WebUI
- For detailed go2rtc configuration options, refer to the [go2rtc documentation](https://github.com/AlexxIT/go2rtc)
-12
View File
@@ -390,18 +390,6 @@ Topic to turn audio detection for a camera on and off. Expected values are `ON`
Topic with current state of audio detection for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/audio_transcription/set`
Topic to turn [live audio transcription](/configuration/audio_detectors#live-transcription) for a camera on and off. Expected values are `ON` and `OFF`. Transcribed text is published to `frigate/<camera_name>/audio/transcription`.
`ON` is ignored unless audio transcription is enabled in the config for the camera. Unlike the other camera toggles, this one is not persisted across Frigate restarts.
**NOTE:** Requires audio detection and transcription to be enabled
### `frigate/<camera_name>/audio_transcription/state`
Topic with current state of live audio transcription for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/recordings/set`
Topic to turn recordings for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)).
+3 -7
View File
@@ -34,15 +34,11 @@ The detect FFmpeg process exited on its own. This message is only the notificati
</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).
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.
See [Recordings: segments are only 1 second long](/troubleshooting/recordings#segments-are-only-1-second-long).
</FaqItem>
+1 -1
View File
@@ -39,7 +39,7 @@ The per-clip variation is typically quite low and is mostly an artifact of keyfr
Debug Replay lets you re-run Frigate's detection pipeline against a section of recorded video without manually configuring a dummy camera. It automatically extracts the recording, creates a temporary camera with the same detection settings as the original, and loops the clip through the pipeline so you can observe detections in real time.
The replay camera behaves like a live camera feed rather than History's video player: it loops the clip continuously as Frigate analyzes it and has no playback controls, so you cannot pause, scrub, or step through it frame by frame. The Debug Replay camera does not save recordings or snapshots or surface anything in Explore, but it otherwise behaves like a regular camera, including running enrichments such as Face Recognition, LPR, and custom classification.
The replay camera behaves like a live camera feed rather than History's video player: it loops the clip continuously as Frigate analyzes it and has no playback controls, so you cannot pause, scrub, or step through it frame by frame.
Debug Replay isn't intended to be a one-stop pane for all Frigate diagnostics or a comprehensive debugging environment for every Frigate feature. It merely makes it easier to spin up a "dummy camera" and perform some common adjustments in real time. You'll still need to use the normal tools (logs, an MQTT client, etc) to debug your feature.
+4 -3
View File
@@ -19,6 +19,7 @@
"docusaurus-plugin-openapi-docs": "^4.5.1",
"docusaurus-theme-openapi-docs": "^4.5.1",
"js-yaml": "^4.1.1",
"marked": "^16.4.2",
"prism-react-renderer": "^2.4.1",
"raw-loader": "^4.0.2",
"react": "^18.3.1",
@@ -10971,9 +10972,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"funding": [
{
"type": "github",
-74
View File
@@ -693,43 +693,6 @@ paths:
**Access:** Admin role required.
Set a camera feature state. Use camera_name='*' to target all cameras.
The value to set is sent in the request body as `{"value": "<value>"}`.
| Feature | Accepted values |
| --- | --- |
| `enabled` | `ON`, `OFF` |
| `detect` | `ON`, `OFF` |
| `motion` | `ON`, `OFF` |
| `recordings` | `ON`, `OFF` |
| `snapshots` | `ON`, `OFF` |
| `audio` | `ON`, `OFF` |
| `audio_transcription` | `ON`, `OFF` |
| `notifications` | `ON`, `OFF` |
| `review_alerts` | `ON`, `OFF` |
| `review_detections` | `ON`, `OFF` |
| `object_descriptions` | `ON`, `OFF` |
| `review_descriptions` | `ON`, `OFF` |
| `improve_contrast` | `ON`, `OFF` |
| `ptz_autotracker` | `ON`, `OFF` |
| `birdseye` | `ON`, `OFF` |
| `birdseye_mode` | `CONTINUOUS`, `MOTION`, `OBJECTS` |
| `motion_contour_area` | integer |
| `motion_threshold` | integer |
| `motion_mask` | `ON`, `OFF` |
| `object_mask` | `ON`, `OFF` |
| `zone` | `ON`, `OFF` |
| `profile` | a profile name, or `none` to deactivate |
`motion_mask`, `object_mask`, and `zone` require the `sub_command` path
parameter to be set to the name of the mask or zone. All other features
reject a sub-command.
`profile` applies globally rather than per camera, so it requires
`camera_name` to be `*`.
These features map to the equivalent MQTT topics, which document the
behavior of each value in more detail.
operationId:
camera_set_camera__camera_name__set__feature___sub_command__put
parameters:
@@ -783,43 +746,6 @@ paths:
**Access:** Admin role required.
Set a camera feature state. Use camera_name='*' to target all cameras.
The value to set is sent in the request body as `{"value": "<value>"}`.
| Feature | Accepted values |
| --- | --- |
| `enabled` | `ON`, `OFF` |
| `detect` | `ON`, `OFF` |
| `motion` | `ON`, `OFF` |
| `recordings` | `ON`, `OFF` |
| `snapshots` | `ON`, `OFF` |
| `audio` | `ON`, `OFF` |
| `audio_transcription` | `ON`, `OFF` |
| `notifications` | `ON`, `OFF` |
| `review_alerts` | `ON`, `OFF` |
| `review_detections` | `ON`, `OFF` |
| `object_descriptions` | `ON`, `OFF` |
| `review_descriptions` | `ON`, `OFF` |
| `improve_contrast` | `ON`, `OFF` |
| `ptz_autotracker` | `ON`, `OFF` |
| `birdseye` | `ON`, `OFF` |
| `birdseye_mode` | `CONTINUOUS`, `MOTION`, `OBJECTS` |
| `motion_contour_area` | integer |
| `motion_threshold` | integer |
| `motion_mask` | `ON`, `OFF` |
| `object_mask` | `ON`, `OFF` |
| `zone` | `ON`, `OFF` |
| `profile` | a profile name, or `none` to deactivate |
`motion_mask`, `object_mask`, and `zone` require the `sub_command` path
parameter to be set to the name of the mask or zone. All other features
reject a sub-command.
`profile` applies globally rather than per camera, so it requires
`camera_name` to be `*`.
These features map to the equivalent MQTT topics, which document the
behavior of each value in more detail.
operationId: camera_set_camera__camera_name__set__feature__put
parameters:
- name: camera_name
+1 -15
View File
@@ -31,10 +31,7 @@ from frigate.api.auth import (
get_allowed_cameras_for_filter,
require_role,
)
from frigate.api.config_util import (
publish_camera_section_updates,
swap_runtime_config,
)
from frigate.api.config_util import swap_runtime_config
from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters
from frigate.api.defs.request.app_body import (
AppConfigSetBody,
@@ -966,17 +963,6 @@ def config_set(request: Request, body: AppConfigSetBody):
body.update_topic, settings
)
# a config/cameras/* topic publishes camera copies, a
# global topic the global object. FrigateConfig.parse
# folds some global sections down into every camera,
# and workers read both objects, so any such section
# needs its camera copies sent alongside the global
# publish above.
if body.update_topic == "config/birdseye":
publish_camera_section_updates(
request.app, config, CameraConfigUpdateEnum.birdseye
)
return JSONResponse(
content=(
{
+9 -9
View File
@@ -31,7 +31,7 @@ from frigate.api.media_auth import (
deny_response_for_media_uri,
is_role_restricted,
)
from frigate.config import AuthConfig, ProxyConfig
from frigate.config import AuthConfig, NetworkingConfig, ProxyConfig
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
from frigate.models import User
@@ -620,18 +620,18 @@ def resolve_role(
def auth(request: Request):
auth_config: AuthConfig = request.app.frigate_config.auth
proxy_config: ProxyConfig = request.app.frigate_config.proxy
networking_config: NetworkingConfig = request.app.frigate_config.networking
success_response = Response("", status_code=202)
# handle case where internal port is a string with ip:port
internal_port = networking_config.listen.internal
if type(internal_port) is str:
internal_port = int(internal_port.split(":")[-1])
# dont require auth if the request is on the internal port
# this header is set by Frigate's nginx proxy, so it cant be spoofed.
# the port is the boot-time snapshot rather than the live config value:
# nginx's listeners are fixed at container start, so an in-memory config
# change must never move the port that is trusted here
if (
int(request.headers.get("x-server-port", default=0))
== request.app.auth_internal_port
):
# this header is set by Frigate's nginx proxy, so it cant be spoofed
if int(request.headers.get("x-server-port", default=0)) == internal_port:
success_response.headers["remote-user"] = "anonymous"
success_response.headers["remote-role"] = "admin"
return success_response
+1 -39
View File
@@ -1328,45 +1328,7 @@ def camera_set(
body: CameraSetBody,
sub_command: str | None = None,
):
"""Set a camera feature state. Use camera_name='*' to target all cameras.
The value to set is sent in the request body as `{"value": "<value>"}`.
| Feature | Accepted values |
| --- | --- |
| `enabled` | `ON`, `OFF` |
| `detect` | `ON`, `OFF` |
| `motion` | `ON`, `OFF` |
| `recordings` | `ON`, `OFF` |
| `snapshots` | `ON`, `OFF` |
| `audio` | `ON`, `OFF` |
| `audio_transcription` | `ON`, `OFF` |
| `notifications` | `ON`, `OFF` |
| `review_alerts` | `ON`, `OFF` |
| `review_detections` | `ON`, `OFF` |
| `object_descriptions` | `ON`, `OFF` |
| `review_descriptions` | `ON`, `OFF` |
| `improve_contrast` | `ON`, `OFF` |
| `ptz_autotracker` | `ON`, `OFF` |
| `birdseye` | `ON`, `OFF` |
| `birdseye_mode` | `CONTINUOUS`, `MOTION`, `OBJECTS` |
| `motion_contour_area` | integer |
| `motion_threshold` | integer |
| `motion_mask` | `ON`, `OFF` |
| `object_mask` | `ON`, `OFF` |
| `zone` | `ON`, `OFF` |
| `profile` | a profile name, or `none` to deactivate |
`motion_mask`, `object_mask`, and `zone` require the `sub_command` path
parameter to be set to the name of the mask or zone. All other features
reject a sub-command.
`profile` applies globally rather than per camera, so it requires
`camera_name` to be `*`.
These features map to the equivalent MQTT topics, which document the
behavior of each value in more detail.
"""
"""Set a camera feature state. Use camera_name='*' to target all cameras."""
dispatcher = request.app.dispatcher
frigate_config: FrigateConfig = request.app.frigate_config
-28
View File
@@ -3,30 +3,6 @@
from fastapi import FastAPI
from frigate.config import FrigateConfig
from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
CameraConfigUpdateTopic,
)
def publish_camera_section_updates(
app: FastAPI, config: FrigateConfig, update_type: CameraConfigUpdateEnum
) -> None:
"""Broadcast every camera's re-resolved value for a global section.
Global sections are folded into each camera at parse time and the camera
copies are what workers read, so send them rather than leave a worker to
guess which cameras were inheriting.
"""
for camera_name, camera_config in config.cameras.items():
settings = getattr(camera_config, update_type.name, None)
if settings is None:
continue
app.config_publisher.publish_update(
CameraConfigUpdateTopic(update_type, camera_name), settings
)
def swap_runtime_config(app: FastAPI, config: FrigateConfig) -> None:
@@ -40,10 +16,6 @@ def swap_runtime_config(app: FastAPI, config: FrigateConfig) -> None:
camera the user turned off would silently come back on.
"""
app.frigate_config = config
if app.config_holder is not None:
app.config_holder.set(config)
app.genai_manager.update_config(config)
if app.profile_manager is not None:
-5
View File
@@ -35,7 +35,6 @@ from frigate.comms.event_metadata_updater import (
)
from frigate.config import FrigateConfig
from frigate.config.camera.updater import CameraConfigUpdatePublisher
from frigate.config.holder import ConfigHolder
from frigate.config.profile_manager import ProfileManager
from frigate.debug_replay import DebugReplayManager, debug_replay_auto_stop_watchdog
from frigate.embeddings import EmbeddingsContext
@@ -75,7 +74,6 @@ def create_fastapi_app(
dispatcher: Dispatcher | None = None,
profile_manager: ProfileManager | None = None,
enforce_default_admin: bool = True,
config_holder: ConfigHolder | None = None,
):
logger.info("Starting FastAPI app")
app = FastAPI(
@@ -152,8 +150,6 @@ def create_fastapi_app(
app.include_router(debug_replay.router)
# App Properties
app.frigate_config = frigate_config
# snapshot the port nginx bound at startup, the live config can be swapped
app.auth_internal_port = frigate_config.networking.listen.internal_port
app.genai_manager = GenAIClientManager(frigate_config)
app.embeddings = embeddings
app.detected_frames_processor = detected_frames_processor
@@ -166,7 +162,6 @@ def create_fastapi_app(
app.replay_manager = replay_manager
app.dispatcher = dispatcher
app.profile_manager = profile_manager
app.config_holder = config_holder
if frigate_config.auth.enabled:
secret = get_jwt_secret()
+1 -16
View File
@@ -53,7 +53,6 @@ from frigate.util.file import (
)
from frigate.util.image import get_image_from_recording, get_image_quality_params
from frigate.util.media import get_keyframe_before
from frigate.util.object import create_empty_regions_grid
logger = logging.getLogger(__name__)
@@ -1084,21 +1083,7 @@ def clear_region_grid(request: Request, camera_name: str):
status_code=404,
)
# store an empty grid instead of deleting the row so the grid is
# rebuilt from newly tracked objects and not from all past history
region = {
Regions.camera: camera_name,
Regions.grid: create_empty_regions_grid(),
Regions.last_update: datetime.now().timestamp(),
}
(
Regions.insert(region)
.on_conflict(
conflict_target=[Regions.camera],
update=region,
)
.execute()
)
Regions.delete().where(Regions.camera == camera_name).execute()
return JSONResponse(
content={"success": True, "message": "Region grid cleared"},
)
+2 -2
View File
@@ -182,7 +182,7 @@ async def get_motion_search_status_endpoint(
)
job = get_motion_search_job(job_id)
if not job or job.camera != camera_name:
if not job:
return JSONResponse(
content={"success": False, "message": "Job not found"},
status_code=404,
@@ -253,7 +253,7 @@ async def cancel_motion_search_endpoint(
)
job = get_motion_search_job(job_id)
if not job or job.camera != camera_name:
if not job:
return JSONResponse(
content={"success": False, "message": "Job not found"},
status_code=404,
+23 -24
View File
@@ -30,7 +30,6 @@ from frigate.comms.ws import WebSocketClient
from frigate.comms.zmq_proxy import ZmqProxy
from frigate.config.camera.updater import CameraConfigUpdatePublisher
from frigate.config.config import FrigateConfig
from frigate.config.holder import ConfigHolder
from frigate.config.profile_manager import ProfileManager
from frigate.const import (
CACHE_DIR,
@@ -122,18 +121,8 @@ class FrigateApp:
self.ptz_metrics: dict[str, PTZMetrics] = {}
self.processes: dict[str, int] = {}
self.embeddings: EmbeddingsContext | None = None
self.config_holder = ConfigHolder(config)
@property
def config(self) -> FrigateConfig:
"""The current config, not the one Frigate booted with.
Read through the holder so the deferred watchdog factories below build
a replacement process from the config as it is now. There is no setter
on purpose: a plain attribute would let a caller pin this back to a
single object and reintroduce the staleness.
"""
return self.config_holder.config
self.profile_manager: ProfileManager | None = None
self.config = config
def ensure_dirs(self) -> None:
dirs = [
@@ -354,6 +343,25 @@ class FrigateApp:
)
self.dispatcher.profile_manager = self.profile_manager
def restore_active_profile(self) -> None:
"""Re-activate the persisted profile after subscribers are connected.
ZMQ PUB/SUB drops messages with no subscribers, so activation must
run after every config_updater subscriber is up.
"""
if self.profile_manager is None:
return
persisted = ProfileManager.load_persisted_profile()
if persisted and any(
persisted in cam.profiles for cam in self.config.cameras.values()
):
logger.info("Restoring persisted profile '%s'", persisted)
# runtime overrides are layered on top via restore_runtime_state()
self.profile_manager.activate_profile(
persisted, clear_runtime_overrides=False
)
def start_detectors(self) -> None:
for name in self.config.cameras.keys():
try:
@@ -602,13 +610,6 @@ class FrigateApp:
self.start_detectors()
self.init_dispatcher()
self.init_profile_manager()
# workers get a copy of the config and can miss the broadcast below, so
# apply both layers here. must stay after init_profile_manager(), which
# snapshots the base config that profile deactivation resets to
self.profile_manager.restore_persisted_profile_to_config()
self.dispatcher.reapply_runtime_state_to_config()
self.init_embeddings_client()
self.start_video_output_processor()
self.start_ptz_autotracker()
@@ -623,9 +624,8 @@ class FrigateApp:
self.start_record_cleanup()
self.start_watchdog()
# publish for the recording/review/embeddings processes, which start
# before the config can be corrected, and for the retained MQTT states
self.profile_manager.restore_persisted_profile()
# restore persisted runtime overrides on top of config
self.restore_active_profile()
self.dispatcher.restore_runtime_state()
self.init_auth()
@@ -645,7 +645,6 @@ class FrigateApp:
self.replay_manager,
self.dispatcher,
self.profile_manager,
config_holder=self.config_holder,
),
host="127.0.0.1",
port=5001,
-6
View File
@@ -77,11 +77,6 @@ class MqttClient(Communicator):
"ON" if camera.audio.enabled_in_config else "OFF",
retain=True,
)
self.publish(
f"{camera_name}/audio_transcription/state",
"ON" if camera.audio_transcription.live_enabled else "OFF",
retain=True,
)
self.publish(
f"{camera_name}/detect/state",
"ON" if camera.detect.enabled else "OFF",
@@ -263,7 +258,6 @@ class MqttClient(Communicator):
"snapshots",
"detect",
"audio",
"audio_transcription",
"motion",
"improve_contrast",
"ptz_autotracker",
+2 -2
View File
@@ -13,8 +13,8 @@ class CameraUiConfig(FrigateBaseModel):
)
dashboard: bool = Field(
default=True,
title="Show on Live dashboard",
description="Toggle whether this camera is visible on the default All Cameras live dashboard. The camera remains available everywhere else in the UI, including camera groups and settings.",
title="Show in UI",
description="Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again.",
)
review: bool = Field(
default=True,
-34
View File
@@ -1,34 +0,0 @@
"""Shared handle on the config object that is current for this instance."""
from .config import FrigateConfig
__all__ = ["ConfigHolder"]
class ConfigHolder:
"""Indirection for the most recently parsed config.
/api/config/set re-parses yaml into a brand new FrigateConfig instead of
mutating the old one, so any reference captured during startup goes stale
the first time a user saves. Anything that has to build something after
startup, most importantly the watchdog factories that rebuild a crashed
process, must read through a holder rather than close over a config
object, or the rebuilt process comes back with the config as it was at
boot and silently discards every change made since.
There is deliberately no setter on the read side: the swap runs in exactly
one place (frigate.api.config_util.swap_runtime_config) and everyone else
only reads.
"""
def __init__(self, config: FrigateConfig) -> None:
self._config = config
@property
def config(self) -> FrigateConfig:
"""The config as of the most recent successful save."""
return self._config
def set(self, config: FrigateConfig) -> None:
"""Install a freshly parsed config as the current one."""
self._config = config
+1 -24
View File
@@ -1,18 +1,10 @@
from pydantic import Field, model_validator
from pydantic import Field
from .base import FrigateBaseModel
__all__ = ["IPv6Config", "ListenConfig", "NetworkingConfig"]
def parse_listen_port(value: int | str) -> int:
"""Return the port number from a bare port or an "address:port" value."""
if isinstance(value, str):
return int(value.split(":")[-1])
return value
class IPv6Config(FrigateBaseModel):
enabled: bool = Field(
default=False,
@@ -33,21 +25,6 @@ class ListenConfig(FrigateBaseModel):
description="External listening port for Frigate (default 8971).",
)
@property
def internal_port(self) -> int:
return parse_listen_port(self.internal)
@property
def external_port(self) -> int:
return parse_listen_port(self.external)
@model_validator(mode="after")
def validate_distinct_ports(self) -> "ListenConfig":
if self.internal_port == self.external_port:
raise ValueError("internal and external must listen on different ports")
return self
class NetworkingConfig(FrigateBaseModel):
ipv6: IPv6Config = Field(
+14 -94
View File
@@ -169,93 +169,6 @@ class ProfileManager:
self.config.active_profile = None
self._persist_active_profile(None)
def _validate_profile_name(self, profile_name: str | None) -> str | None:
"""Return an error message if the name is not a defined profile."""
if profile_name is not None and profile_name not in self.config.profiles:
return f"Profile '{profile_name}' is not defined in the profiles section"
return None
def _apply_to_config(
self, profile_name: str | None
) -> tuple[dict[str, set[str]], str | None]:
"""Reset every camera to base, then apply the named profile on top.
Returns the changed camera/section pairs, plus an error message if
applying the profile failed partway through.
"""
changed: dict[str, set[str]] = {}
self._reset_to_base(changed)
if profile_name is not None:
err = self._apply_profile_overrides(profile_name, changed)
if err:
return changed, err
return changed, None
def apply_profile_to_config(self, profile_name: str | None) -> str | None:
"""Apply a profile to the in-memory config, without publishing it.
Safe to call ahead of activate_profile: both reset to the base config
first, so the later call re-derives the same state and still reports
every section as changed.
Returns:
None on success, or an error message string on failure.
"""
err = self._validate_profile_name(profile_name)
if err:
return err
return self._apply_to_config(profile_name)[1]
def _persisted_profile_to_restore(self) -> str | None:
"""Return the persisted profile name, if it still applies to a camera."""
persisted = self.load_persisted_profile()
if not persisted or not any(
persisted in cam.profiles for cam in self.config.cameras.values()
):
return None
return persisted
def restore_persisted_profile_to_config(self) -> None:
"""Restore the persisted profile into the config, without publishing.
Called before worker processes start, so they are handed a config that
already carries the profile rather than relying on the broadcast that
restore_persisted_profile() sends later.
"""
persisted = self._persisted_profile_to_restore()
if persisted is None:
return
err = self.apply_profile_to_config(persisted)
if err:
logger.error("Failed to apply persisted profile '%s': %s", persisted, err)
def restore_persisted_profile(self) -> None:
"""Re-activate the persisted profile once subscribers are connected.
The config already carries the profile; this pass publishes it for the
processes that start before the config can be corrected, and for the
retained MQTT states.
"""
persisted = self._persisted_profile_to_restore()
if persisted is None:
return
logger.info("Restoring persisted profile '%s'", persisted)
# runtime overrides are layered on top by the dispatcher's replay
self.activate_profile(persisted, clear_runtime_overrides=False)
def activate_profile(
self,
profile_name: str | None,
@@ -274,16 +187,23 @@ class ProfileManager:
Returns:
None on success, or an error message string on failure.
"""
err = self._validate_profile_name(profile_name)
if err:
return err
if profile_name is not None:
if profile_name not in self.config.profiles:
return (
f"Profile '{profile_name}' is not defined in the profiles section"
)
# Track which camera/section pairs get changed for ZMQ publishing
changed, err = self._apply_to_config(profile_name)
changed: dict[str, set[str]] = {}
if err:
return err
# Reset all cameras to base config
self._reset_to_base(changed)
# Apply new profile overrides if activating
if profile_name is not None:
err = self._apply_profile_overrides(profile_name, changed)
if err:
return err
# Publish ZMQ updates only for sections that actually changed
self._publish_updates(changed)
+157
View File
@@ -0,0 +1,157 @@
import logging
import queue
from typing import Literal
import numpy as np
from pydantic import ConfigDict, Field
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import BaseDetectorConfig
logger = logging.getLogger(__name__)
DETECTOR_KEY = "degirum"
### DETECTOR CONFIG ###
class DGDetectorConfig(BaseDetectorConfig):
"""DeGirum detector for running models via DeGirum cloud or local inference services."""
model_config = ConfigDict(
title="DeGirum",
)
type: Literal[DETECTOR_KEY]
location: str = Field(
default=None,
title="Inference Location",
description="Location of the DeGirim inference engine (e.g. '@cloud', '127.0.0.1').",
)
zoo: str = Field(
default=None,
title="Model Zoo",
description="Path or URL to the DeGirum model zoo.",
)
token: str = Field(
default=None,
title="DeGirum Cloud Token",
description="Token for DeGirum Cloud access.",
)
### ACTUAL DETECTOR ###
class DGDetector(DetectionApi):
type_key = DETECTOR_KEY
def __init__(self, detector_config: DGDetectorConfig):
try:
import degirum as dg
except ModuleNotFoundError:
raise ImportError("Unable to import DeGirum detector.") from None
self._queue = queue.Queue()
self._zoo = dg.connect(
detector_config.location, detector_config.zoo, detector_config.token
)
logger.debug(f"Models in zoo: {self._zoo.list_models()}")
self.dg_model = self._zoo.load_model(
detector_config.model.path,
)
# Setting input image format to raw reduces preprocessing time
self.dg_model.input_image_format = "RAW"
# Prioritize the most powerful hardware available
self.select_best_device_type()
# Frigate handles pre processing as long as these are all set
input_shape = self.dg_model.input_shape[0]
self.model_height = input_shape[1]
self.model_width = input_shape[2]
# Passing in dummy frame so initial connection latency happens in
# init function and not during actual prediction
frame = np.zeros(
(detector_config.model.width, detector_config.model.height, 3),
dtype=np.uint8,
)
# Pass in frame to overcome first frame latency
self.dg_model(frame)
self.prediction = self.prediction_generator()
def select_best_device_type(self):
"""
Helper function that selects fastest hardware available per model runtime
"""
types = self.dg_model.supported_device_types
device_map = {
"OPENVINO": ["GPU", "NPU", "CPU"],
"HAILORT": ["HAILO8L", "HAILO8"],
"N2X": ["ORCA1", "CPU"],
"ONNX": ["VITIS_NPU", "CPU"],
"RKNN": ["RK3566", "RK3568", "RK3588"],
"TENSORRT": ["DLA", "GPU", "DLA_ONLY"],
"TFLITE": ["ARMNN", "EDGETPU", "CPU"],
}
runtime = types[0].split("/")[0]
# Just create an array of format {runtime}/{hardware} for every hardware
# in the value for appropriate key in device_map
self.dg_model.device_type = [
f"{runtime}/{hardware}" for hardware in device_map[runtime]
]
def prediction_generator(self):
"""
Generator for all incoming frames. By using this generator, we don't have to keep
reconnecting our websocket on every "predict" call.
"""
logger.debug("Prediction generator was called")
with self.dg_model as model:
while 1:
logger.info(f"q size before calling get: {self._queue.qsize()}")
data = self._queue.get(block=True)
logger.info(f"q size after calling get: {self._queue.qsize()}")
logger.debug(
f"Data we're passing into model predict: {data}, shape of data: {data.shape}"
)
result = model.predict(data)
logger.debug(f"Prediction result: {result}")
yield result
def detect_raw(self, tensor_input):
# Reshaping tensor to work with pysdk
truncated_input = tensor_input.reshape(tensor_input.shape[1:])
logger.debug(f"Detect raw was called for tensor input: {tensor_input}")
# add tensor_input to input queue
self._queue.put(truncated_input)
logger.debug(f"Queue size after adding truncated input: {self._queue.qsize()}")
# define empty detection result
detections = np.zeros((20, 6), np.float32)
# grab prediction
res = next(self.prediction)
# If we have an empty prediction, return immediately
if len(res.results) == 0 or len(res.results[0]) == 0:
return detections
i = 0
for result in res.results:
if i >= 20:
break
detections[i] = [
result["category_id"],
float(result["score"]),
result["bbox"][1] / self.model_height,
result["bbox"][0] / self.model_width,
result["bbox"][3] / self.model_height,
result["bbox"][2] / self.model_width,
]
i += 1
logger.debug(f"Detections output: {detections}")
return detections
+1 -2
View File
@@ -9,7 +9,6 @@ from pydantic import ConfigDict, Field
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum
from frigate.util.model import xyxy_to_xywh_for_nms
try:
from tflite_runtime.interpreter import Interpreter, load_delegate
@@ -298,7 +297,7 @@ class EdgeTpuTfl(DetectionApi):
# until after filtering out redundant boxes
# Shift the logit scores to be non-negative (required by cv2)
indices = cv2.dnn.NMSBoxes(
bboxes=xyxy_to_xywh_for_nms(boxes_filtered_decoded),
bboxes=boxes_filtered_decoded,
scores=max_scores_filtered_shiftedpositive,
score_threshold=(
self.min_logit_value + self.logit_shift_to_positive_values
+2 -3
View File
@@ -17,7 +17,6 @@ from frigate.detectors.detector_config import (
ModelTypeEnum,
)
from frigate.util.file import FileLock
from frigate.util.model import xyxy_to_xywh_for_nms
logger = logging.getLogger(__name__)
@@ -582,7 +581,7 @@ class MemryXDetector(DetectionApi):
# Convert coordinates to integers
x_min, y_min, x_max, y_max = map(int, [x_min, y_min, x_max, y_max])
# Append valid detections [class_id, confidence, x_min, y_min, x_max, y_max]
# Append valid detections [class_id, confidence, x, y, width, height]
detections.append([class_id, confidence, x_min, y_min, x_max, y_max])
final_detections = np.zeros((20, 6), np.float32)
@@ -596,7 +595,7 @@ class MemryXDetector(DetectionApi):
detections = np.array(detections, dtype=np.float32)
# Apply Non-Maximum Suppression (NMS)
bboxes = xyxy_to_xywh_for_nms(detections[:, 2:6])
bboxes = detections[:, 2:6].tolist() # (x_min, y_min, width, height)
scores = detections[:, 1].tolist() # Confidence scores
indices = cv2.dnn.NMSBoxes(bboxes, scores, 0.45, 0.5)
+3 -3
View File
@@ -226,12 +226,12 @@ class OvDetector(DetectionApi):
conf_mask = (image_pred[:, 4] * class_conf.squeeze() >= 0.3).squeeze()
# Detections ordered as (x1, y1, x2, y2, obj_conf, class_conf, class_pred)
predictions = np.concatenate(
detections = np.concatenate(
(image_pred[:, :5], class_conf, class_pred), axis=1
)
predictions = predictions[conf_mask]
detections = detections[conf_mask]
ordered = predictions[predictions[:, 5].argsort()[::-1]][:20]
ordered = detections[detections[:, 5].argsort()[::-1]][:20]
for i, object_detected in enumerate(ordered):
detections[i] = self.process_yolo(
+2 -2
View File
@@ -12,7 +12,7 @@ from frigate.const import MODEL_CACHE_DIR, SUPPORTED_RK_SOCS
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detection_runners import RKNNModelRunner
from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum
from frigate.util.model import post_process_yolo, xyxy_to_xywh_for_nms
from frigate.util.model import post_process_yolo
from frigate.util.rknn_converter import auto_convert_model
logger = logging.getLogger(__name__)
@@ -285,7 +285,7 @@ class Rknn(DetectionApi):
# run nms
indices = cv2.dnn.NMSBoxes(
bboxes=xyxy_to_xywh_for_nms(boxes),
bboxes=boxes,
scores=scores,
score_threshold=0.4,
nms_threshold=0.4,
-10
View File
@@ -23,7 +23,6 @@ from frigate.genai.prompts import (
build_review_summary_prompt,
)
from frigate.models import Event
from frigate.util.builtin import has_non_finite_number
logger = logging.getLogger(__name__)
@@ -165,15 +164,6 @@ class GenAIClient:
except json.JSONDecodeError as je:
logger.error("Failed to parse review description JSON: %s", je)
return None
# model_construct skips validation, so non-finite numbers that
# the validated path would have rejected have to be caught here
if has_non_finite_number(raw):
logger.error(
"Discarding review description containing non-finite numbers."
)
return None
# observations and confidence are required on the model; fill an empty default
# if the response omitted it so attribute access stays safe.
raw.setdefault("observations", [])
+6 -3
View File
@@ -178,10 +178,13 @@ class OutputProcess(FrigateProcess):
)
if update_topic is not None and birdseye_config is not None:
# only the global-only fields are applied here; the per-camera
# enabled and mode arrive on config/cameras/<name>/birdseye,
# already resolved against yaml by the config parse
previous_global_mode = self.config.birdseye.mode
self.config.birdseye = birdseye_config
for camera_config in self.config.cameras.values():
if camera_config.birdseye.mode == previous_global_mode:
camera_config.birdseye.mode = birdseye_config.mode
logger.debug("Applied dynamic birdseye config update")
# check if there is an updated config
+8 -12
View File
@@ -625,18 +625,14 @@ class OnvifController:
return
self.cams[camera_name]["active"] = True
# only track start_time for autotracking
if self.ptz_metrics[camera_name].autotracker_enabled.value:
self.ptz_metrics[camera_name].motor_stopped.clear()
logger.debug(
f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name].frame_time.value}"
)
self.ptz_metrics[camera_name].start_time.value = self.ptz_metrics[
camera_name
].frame_time.value
self.ptz_metrics[camera_name].stop_time.value = 0
self.ptz_metrics[camera_name].motor_stopped.clear()
logger.debug(
f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name].frame_time.value}"
)
self.ptz_metrics[camera_name].start_time.value = self.ptz_metrics[
camera_name
].frame_time.value
self.ptz_metrics[camera_name].stop_time.value = 0
move_request = self.cams[camera_name]["relative_move_request"]
# function takes in -1 to 1 for pan and tilt, interpolate to the values of the camera.
@@ -1,174 +0,0 @@
"""Tests that the internal port trusted by /auth cannot be moved at runtime."""
import os
import tempfile
import unittest
from unittest.mock import MagicMock, Mock, patch
import ruamel.yaml
from fastapi import Request
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
from frigate.api.fastapi_app import create_fastapi_app
from frigate.config import FrigateConfig
from frigate.config.camera.updater import CameraConfigUpdatePublisher
from frigate.const import JWT_SECRET_ENV_VAR
from frigate.models import Event, Recordings, ReviewSegment
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
@patch.dict(os.environ, {JWT_SECRET_ENV_VAR: "test-secret"})
class TestAuthInternalPort(BaseTestHttp):
"""/auth grants anonymous admin by port, so that port must stay put.
nginx binds its listeners once at container start and never reloads them,
but /api/config/set can swap the live config object mid-process. If /auth
read the port off the live config, saving networking.listen.internal would
hand unauthenticated admin to whoever can reach the external port.
"""
def setUp(self):
super().setUp(models=[Event, Recordings, ReviewSegment])
self.minimal_config = {
"mqtt": {"host": "mqtt"},
"auth": {"enabled": True},
"networking": {"listen": {"internal": 5000, "external": 8971}},
"cameras": {
"front_door": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
}
},
}
def _create_app(self):
mock_publisher = Mock(spec=CameraConfigUpdatePublisher)
mock_publisher.publisher = MagicMock()
app = create_fastapi_app(
FrigateConfig(**self.minimal_config),
self.db,
None,
None,
None,
None,
None,
None,
mock_publisher,
None,
enforce_default_admin=False,
)
async def mock_get_current_user(request: Request):
return {
"username": request.headers.get("remote-user"),
"role": request.headers.get("remote-role"),
}
async def mock_get_allowed_cameras_for_filter(request: Request):
return list(self.minimal_config.get("cameras", {}).keys())
app.dependency_overrides[get_current_user] = mock_get_current_user
app.dependency_overrides[get_allowed_cameras_for_filter] = (
mock_get_allowed_cameras_for_filter
)
return app
def _write_config_file(self):
"""Write the minimal config to a temp YAML file and return the path."""
yaml = ruamel.yaml.YAML()
f = tempfile.NamedTemporaryFile(mode="w", suffix=".yml", delete=False)
yaml.dump(self.minimal_config, f)
f.close()
return f.name
def test_internal_port_is_anonymous_admin(self):
app = self._create_app()
with AuthTestClient(app) as client:
resp = client.get("/auth", headers={"x-server-port": "5000"})
self.assertEqual(resp.status_code, 202)
self.assertEqual(resp.headers["remote-user"], "anonymous")
self.assertEqual(resp.headers["remote-role"], "admin")
def test_external_port_requires_auth(self):
app = self._create_app()
with AuthTestClient(app) as client:
resp = client.get("/auth", headers={"x-server-port": "8971"})
self.assertEqual(resp.status_code, 401)
def test_swapped_config_does_not_move_the_trusted_port(self):
"""The live config is not what /auth trusts.
Stands in for every path that can rebind app.frigate_config while the
process runs, whatever restart flag the caller claimed.
"""
app = self._create_app()
swapped = FrigateConfig(
**{
**self.minimal_config,
"networking": {"listen": {"internal": 8971, "external": 5000}},
}
)
app.frigate_config = swapped
with AuthTestClient(app) as client:
resp = client.get("/auth", headers={"x-server-port": "8971"})
self.assertEqual(resp.status_code, 401)
# nginx is still listening where it was told to at boot
resp = client.get("/auth", headers={"x-server-port": "5000"})
self.assertEqual(resp.status_code, 202)
self.assertEqual(resp.headers["remote-role"], "admin")
@patch("frigate.api.app.find_config_file")
def test_config_set_rejects_internal_matching_external(self, mock_find_config):
"""Saving the internal port onto the external one is refused outright."""
config_path = self._write_config_file()
mock_find_config.return_value = config_path
try:
app = self._create_app()
with AuthTestClient(app) as client:
resp = client.put(
"/config/set",
json={
"config_data": {"networking": {"listen": {"internal": 8971}}},
"update_topic": "config/networking",
"requires_restart": 1,
},
)
self.assertEqual(resp.status_code, 400)
self.assertFalse(resp.json()["success"])
# the rejected save must not have reached the live config
self.assertEqual(
app.frigate_config.networking.listen.internal_port, 5000
)
resp = client.get("/auth", headers={"x-server-port": "8971"})
self.assertEqual(resp.status_code, 401)
with open(config_path) as f:
self.assertNotIn("8971", f.read().split("external")[0])
finally:
os.unlink(config_path)
if __name__ == "__main__":
unittest.main(verbosity=2)
@@ -13,7 +13,6 @@ from frigate.config.camera.updater import (
CameraConfigUpdatePublisher,
CameraConfigUpdateTopic,
)
from frigate.config.holder import ConfigHolder
from frigate.models import Event, Recordings, ReviewSegment
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
@@ -374,128 +373,6 @@ class TestConfigSetWildcardPropagation(BaseTestHttp):
finally:
os.unlink(config_path)
@patch("frigate.api.app.find_config_file")
def test_global_birdseye_save_fans_out_resolved_camera_configs(
self, mock_find_config
):
"""A global birdseye save must also publish the per-camera values.
Global birdseye only seeds enabled and mode; the camera copies are what
the output process actually reads. Sending just the global object makes
a worker guess which cameras were inheriting, and the only available
guess (mode still equals the previous global) wrongly claims a camera
whose explicit yaml mode happens to match.
"""
self.minimal_config["birdseye"] = {"enabled": True, "mode": "motion"}
# explicit override that matches the global value being replaced
self.minimal_config["cameras"]["front_door"]["birdseye"] = {"mode": "motion"}
config_path = self._write_config_file()
mock_find_config.return_value = config_path
try:
app, mock_publisher = self._create_app_with_publisher()
with AuthTestClient(app) as client:
resp = client.put(
"/config/set",
json={
"config_data": {"birdseye": {"mode": "continuous"}},
"update_topic": "config/birdseye",
"requires_restart": 0,
},
)
self.assertEqual(resp.status_code, 200)
# the global object still goes out on its own topic
mock_publisher.publisher.publish.assert_called_once()
topic, settings = mock_publisher.publisher.publish.call_args[0]
self.assertEqual(topic, "config/birdseye")
self.assertEqual(settings.mode.value, "continuous")
published = {
call[0][0].camera: call[0][1]
for call in mock_publisher.publish_update.call_args_list
}
self.assertEqual(set(published), {"front_door", "back_yard"})
for call in mock_publisher.publish_update.call_args_list:
self.assertEqual(
call[0][0].update_type, CameraConfigUpdateEnum.birdseye
)
# the override survives, the inheriting camera follows global
self.assertEqual(published["front_door"].mode.value, "motion")
self.assertEqual(published["back_yard"].mode.value, "continuous")
finally:
os.unlink(config_path)
@patch("frigate.api.app.find_config_file")
def test_save_updates_the_config_holder(self, mock_find_config):
"""A save must move the holder onto the freshly parsed config.
FrigateApp reads the holder when the watchdog rebuilds a crashed
process; if the save leaves it on the boot config, that process comes
back having lost every change made since Frigate started.
"""
from fastapi import Request
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
from frigate.api.fastapi_app import create_fastapi_app
config_path = self._write_config_file()
mock_find_config.return_value = config_path
mock_publisher = Mock(spec=CameraConfigUpdatePublisher)
mock_publisher.publisher = MagicMock()
boot_config = FrigateConfig(**self.minimal_config)
holder = ConfigHolder(boot_config)
try:
app = create_fastapi_app(
boot_config,
self.db,
None,
None,
None,
None,
None,
None,
mock_publisher,
None,
enforce_default_admin=False,
config_holder=holder,
)
async def mock_get_current_user(request: Request):
return {"username": "admin", "role": "admin"}
async def mock_get_allowed_cameras_for_filter(request: Request):
return list(self.minimal_config.get("cameras", {}).keys())
app.dependency_overrides[get_current_user] = mock_get_current_user
app.dependency_overrides[get_allowed_cameras_for_filter] = (
mock_get_allowed_cameras_for_filter
)
with AuthTestClient(app) as client:
resp = client.put(
"/config/set",
json={
"config_data": {"birdseye": {"inactivity_threshold": 5}},
"update_topic": "config/birdseye",
"requires_restart": 0,
},
)
self.assertEqual(resp.status_code, 200)
self.assertIsNot(holder.config, boot_config)
self.assertIs(holder.config, app.frigate_config)
self.assertEqual(holder.config.birdseye.inactivity_threshold, 5)
finally:
os.unlink(config_path)
if __name__ == "__main__":
unittest.main()
-31
View File
@@ -4,7 +4,6 @@ import unittest
from unittest.mock import MagicMock
from frigate.api.config_util import swap_runtime_config
from frigate.config.holder import ConfigHolder
class TestSwapRuntimeConfig(unittest.TestCase):
@@ -13,7 +12,6 @@ class TestSwapRuntimeConfig(unittest.TestCase):
def _make_app(self) -> MagicMock:
app = MagicMock()
app.dispatcher.comms = [MagicMock(), MagicMock()]
app.config_holder = ConfigHolder(MagicMock(name="boot_config"))
return app
def test_rebinds_all_references(self) -> None:
@@ -39,40 +37,11 @@ class TestSwapRuntimeConfig(unittest.TestCase):
# the swap rebuilds cameras from yaml, so overrides must be re-layered
app.dispatcher.reapply_runtime_state_to_config.assert_called_once_with()
def test_updates_the_config_holder(self) -> None:
app = self._make_app()
holder = app.config_holder
config = MagicMock(name="new_config")
swap_runtime_config(app, config)
self.assertIs(holder.config, config)
def test_deferred_factory_builds_from_the_swapped_config(self) -> None:
"""A watchdog-style factory must not rebuild from the boot config.
The factories in FrigateApp are lambdas evaluated when a process is
restarted, long after a user may have saved. Reading through the
holder is what keeps a rebuilt process from reverting every change
made since Frigate started.
"""
app = self._make_app()
holder = app.config_holder
boot_config = holder.config
factory = lambda: holder.config # noqa: E731
self.assertIs(factory(), boot_config)
config = MagicMock(name="new_config")
swap_runtime_config(app, config)
self.assertIs(factory(), config)
def test_tolerates_missing_optional_collaborators(self) -> None:
app = MagicMock()
app.profile_manager = None
app.stats_emitter = None
app.dispatcher = None
app.config_holder = None
config = MagicMock(name="new_config")
# must not raise when the optional collaborators are absent
@@ -5,7 +5,6 @@ import tempfile
import unittest
from unittest.mock import MagicMock, patch
from frigate.app import FrigateApp
from frigate.comms.dispatcher import Dispatcher
from frigate.comms.runtime_state import RuntimeStatePersistence
@@ -364,94 +363,5 @@ class TestReapplyRuntimeStateToConfig(unittest.TestCase):
dispatcher.reapply_runtime_state_to_config()
class TestStartupAppliesConfigLayersBeforeWorkersStart(unittest.TestCase):
"""Both layers must reach the config before config-carrying workers start.
A worker started before a layer is applied keeps the yaml value for the
rest of the session: the config_updater broadcast sent later is dropped
for subscribers that have not connected yet, and nothing re-sends it.
"""
CONFIG_LAYERS = (
"profile_manager.restore_persisted_profile_to_config",
"dispatcher.reapply_runtime_state_to_config",
)
# started with a copy of the camera config
CONFIG_CARRYING_WORKERS = (
"start_video_output_processor",
"start_ptz_autotracker",
"start_detected_frames_processor",
"start_camera_processor",
"start_audio_processor",
)
def _start_call_order(self) -> list[str]:
"""Return the names FrigateApp.start() calls, in order."""
app = MagicMock()
with (
patch("frigate.app.set_file_limit"),
patch("frigate.app.cleanup_replay_cameras"),
patch("frigate.app.reap_stale_exports"),
patch("frigate.app.create_fastapi_app"),
patch("frigate.app.uvicorn"),
):
FrigateApp.start(app)
return [name for name, _, _ in app.mock_calls]
def test_applied_before_any_config_carrying_worker(self) -> None:
order = self._start_call_order()
for layer in self.CONFIG_LAYERS:
for worker in self.CONFIG_CARRYING_WORKERS:
self.assertLess(order.index(layer), order.index(worker))
def test_applied_after_the_dispatcher_exists(self) -> None:
order = self._start_call_order()
for layer in self.CONFIG_LAYERS:
self.assertLess(order.index("init_dispatcher"), order.index(layer))
def test_applied_after_the_profile_base_is_snapshotted(self) -> None:
# ProfileManager snapshots the config as the "no profile" base that
# deactivation resets to, so neither layer may be in the config yet
order = self._start_call_order()
for layer in self.CONFIG_LAYERS:
self.assertLess(order.index("init_profile_manager"), order.index(layer))
def test_layers_applied_in_order(self) -> None:
# a runtime toggle is the layer the user set last, so it goes on top
order = self._start_call_order()
self.assertLess(
order.index("profile_manager.restore_persisted_profile_to_config"),
order.index("dispatcher.reapply_runtime_state_to_config"),
)
def test_overrides_still_re_applied_after_the_profile_is_restored(self) -> None:
# activation resets the sections it owns to the base first, so the
# overrides have to land on top again
order = self._start_call_order()
self.assertLess(
order.index("profile_manager.restore_persisted_profile"),
order.index("dispatcher.restore_runtime_state"),
)
def test_broadcast_replay_still_runs_at_the_end(self) -> None:
# the broadcast is the only channel for the recording, review, and
# embeddings processes, which start before the config can be corrected
order = self._start_call_order()
for replay in (
"profile_manager.restore_persisted_profile",
"dispatcher.restore_runtime_state",
):
self.assertLess(order.index("start_audio_processor"), order.index(replay))
if __name__ == "__main__":
unittest.main()
-41
View File
@@ -1,41 +0,0 @@
"""Tests for networking config validation."""
import unittest
from pydantic import ValidationError
from frigate.config.network import ListenConfig
class TestListenConfig(unittest.TestCase):
def test_defaults_are_distinct(self):
listen = ListenConfig()
self.assertEqual(listen.internal_port, 5000)
self.assertEqual(listen.external_port, 8971)
def test_address_and_port_string_is_parsed(self):
listen = ListenConfig(internal="127.0.0.1:5000", external="0.0.0.0:8971")
self.assertEqual(listen.internal_port, 5000)
self.assertEqual(listen.external_port, 8971)
def test_identical_ports_rejected(self):
with self.assertRaises(ValidationError):
ListenConfig(internal=8971, external=8971)
def test_same_port_on_different_addresses_rejected(self):
# nginx would accept these as distinct listeners, but /auth decides on
# the port alone, so the external one would inherit anonymous admin
with self.assertRaises(ValidationError):
ListenConfig(internal="127.0.0.1:8971", external="0.0.0.0:8971")
def test_distinct_ports_accepted(self):
listen = ListenConfig(internal=5001, external="0.0.0.0:8971")
self.assertEqual(listen.internal_port, 5001)
self.assertEqual(listen.external_port, 8971)
if __name__ == "__main__":
unittest.main(verbosity=2)
-226
View File
@@ -1,226 +0,0 @@
"""Tests for detector post-processing NMS box format handling.
cv2.dnn.NMSBoxes expects boxes as [x, y, width, height]. Passing corner
coordinates [x1, y1, x2, y2] makes OpenCV treat x2/y2 as width/height,
inflating every box toward the bottom-right by its distance from the origin.
Two well separated objects far from the origin then appear to overlap and the
lower scoring one is silently suppressed.
The regression geometry used throughout: two boxes with zero true overlap,
A = (393, 499, 484, 620) and B = (527, 499, 618, 620) in a 640x640 input
(43 px gap). Misread as [x, y, w, h] their IoU is 0.465, above the 0.4 NMS
threshold, so the buggy format drops the lower scoring box while correct
conversion keeps both.
"""
import math
import unittest
from queue import Queue
import numpy as np
from frigate.detectors.plugins.memryx import MemryXDetector
from frigate.util.model import (
post_process_dfine,
post_process_rfdetr,
post_process_yolo,
post_process_yolox,
)
WIDTH = 640
HEIGHT = 640
# box A: xyxy (393, 499, 484, 620) as center format
A_CX, A_CY, A_W, A_H = 438.5, 559.5, 91.0, 121.0
# box B: xyxy (527, 499, 618, 620) as center format
B_CX, B_CY, B_W, B_H = 572.5, 559.5, 91.0, 121.0
# expected normalized output rows: [class_id, conf, y1, x1, y2, x2]
A_ROW = [499 / 640, 393 / 640, 620 / 640, 484 / 640]
B_ROW = [499 / 640, 527 / 640, 620 / 640, 618 / 640]
def kept(detections: np.ndarray) -> np.ndarray:
"""Rows of the padded (20, 6) output that hold real detections."""
return detections[detections[:, 1] > 0]
class TestYoloNmsPostProcess(unittest.TestCase):
def _single_output(self, rows: list[list[float]]) -> list[np.ndarray]:
"""Build a single-tensor YOLO output (1, attrs, anchors) from
[cx, cy, w, h, class scores...] rows, padded with empty anchors."""
anchors = np.zeros((10, len(rows[0])), dtype=np.float32)
anchors[: len(rows)] = np.array(rows, dtype=np.float32)
return [anchors.T[np.newaxis, ...]]
def test_keeps_separated_objects_far_from_origin(self):
output = self._single_output(
[
[A_CX, A_CY, A_W, A_H, 0.90, 0.0],
[B_CX, B_CY, B_W, B_H, 0.0, 0.85],
]
)
detections = kept(post_process_yolo(output, WIDTH, HEIGHT))
self.assertEqual(len(detections), 2)
np.testing.assert_allclose(detections[0], [0, 0.90, *A_ROW], atol=2e-3)
np.testing.assert_allclose(detections[1], [1, 0.85, *B_ROW], atol=2e-3)
def test_still_suppresses_true_duplicates(self):
# same object twice, shifted 4 px: true IoU 0.92, must dedupe to one
output = self._single_output(
[
[A_CX, A_CY, A_W, A_H, 0.90, 0.0],
[A_CX + 4, A_CY, A_W, A_H, 0.85, 0.0],
]
)
detections = kept(post_process_yolo(output, WIDTH, HEIGHT))
self.assertEqual(len(detections), 1)
np.testing.assert_allclose(detections[0], [0, 0.90, *A_ROW], atol=2e-3)
class TestMultipartYoloPostProcess(unittest.TestCase):
def _multipart_output(self) -> list[np.ndarray]:
"""Build a 3-scale anchor-based YOLO output containing boxes A and B,
both decoded through anchor 0 of the stride-32 scale."""
outputs = [
np.zeros((1, 255, 80, 80), dtype=np.float32),
np.zeros((1, 255, 40, 40), dtype=np.float32),
np.zeros((1, 255, 20, 20), dtype=np.float32),
]
stride, (anchor_w, anchor_h) = 32, (142, 110)
for cx, cy, w, h, conf, class_channel in [
(A_CX, A_CY, A_W, A_H, 0.95, 5), # class 0
(B_CX, B_CY, B_W, B_H, 0.90, 6), # class 1
]:
cell_x, cell_y = int(cx // stride), int(cy // stride)
dx = (cx / stride - cell_x + 0.5) / 2
dy = (cy / stride - cell_y + 0.5) / 2
dw = math.sqrt(w / anchor_w) / 2
dh = math.sqrt(h / anchor_h) / 2
# anchor 0 occupies channels 0-84 of the 255 channel tensor
outputs[2][0, 0:4, cell_y, cell_x] = [dx, dy, dw, dh]
outputs[2][0, 4, cell_y, cell_x] = conf
outputs[2][0, class_channel, cell_y, cell_x] = 1.0
return outputs
def test_keeps_separated_objects_far_from_origin(self):
detections = kept(post_process_yolo(self._multipart_output(), WIDTH, HEIGHT))
self.assertEqual(len(detections), 2)
np.testing.assert_allclose(detections[0], [0, 0.95, *A_ROW], atol=2e-3)
np.testing.assert_allclose(detections[1], [1, 0.90, *B_ROW], atol=2e-3)
def test_empty_output_returns_no_detections(self):
outputs = [
np.zeros((1, 255, 80, 80), dtype=np.float32),
np.zeros((1, 255, 40, 40), dtype=np.float32),
np.zeros((1, 255, 20, 20), dtype=np.float32),
]
detections = kept(post_process_yolo(outputs, WIDTH, HEIGHT))
self.assertEqual(len(detections), 0)
class TestYoloxPostProcess(unittest.TestCase):
def test_keeps_separated_objects_far_from_origin(self):
# with zero grids and unit strides the decode reduces to
# cx = raw cx and w = exp(raw w)
rows = np.zeros((10, 7), dtype=np.float32)
rows[0] = [A_CX, A_CY, math.log(A_W), math.log(A_H), 1.0, 0.90, 0.0]
rows[1] = [B_CX, B_CY, math.log(B_W), math.log(B_H), 1.0, 0.0, 0.85]
predictions = rows[np.newaxis, ...]
grids = np.zeros((1, 10, 2), dtype=np.float32)
expanded_strides = np.ones((1, 10, 1), dtype=np.float32)
detections = kept(
post_process_yolox(predictions, WIDTH, HEIGHT, grids, expanded_strides)
)
self.assertEqual(len(detections), 2)
np.testing.assert_allclose(detections[0], [0, 0.90, *A_ROW], atol=2e-3)
np.testing.assert_allclose(detections[1], [1, 0.85, *B_ROW], atol=2e-3)
class TestDfinePostProcess(unittest.TestCase):
def test_keeps_separated_objects_far_from_origin(self):
# D-FINE emits absolute pixel xyxy boxes alongside labels and scores
labels = np.zeros((1, 10), dtype=np.int64)
labels[0, 1] = 1
boxes = np.zeros((1, 10, 4), dtype=np.float32)
boxes[0, 0] = [393, 499, 484, 620]
boxes[0, 1] = [527, 499, 618, 620]
scores = np.zeros((1, 10), dtype=np.float32)
scores[0, 0] = 0.90
scores[0, 1] = 0.85
detections = kept(post_process_dfine([labels, boxes, scores], WIDTH, HEIGHT))
self.assertEqual(len(detections), 2)
np.testing.assert_allclose(detections[0], [0, 0.90, *A_ROW], atol=2e-3)
np.testing.assert_allclose(detections[1], [1, 0.85, *B_ROW], atol=2e-3)
class TestRfdetrPostProcess(unittest.TestCase):
def test_keeps_separated_objects_far_from_origin(self):
# RF-DETR emits normalized center format boxes and class logits where
# logit index 0 is the background class
boxes = np.zeros((1, 10, 4), dtype=np.float32)
boxes[0, 0] = [A_CX / WIDTH, A_CY / HEIGHT, A_W / WIDTH, A_H / HEIGHT]
boxes[0, 1] = [B_CX / WIDTH, B_CY / HEIGHT, B_W / WIDTH, B_H / HEIGHT]
# background heavy logits everywhere, then two confident objects
logits = np.tile(np.array([10.0, 0.0, 0.0], dtype=np.float32), (1, 10, 1))
logits[0, 0] = [0.0, 4.0, 0.0] # class 0 after background offset
logits[0, 1] = [0.0, 0.0, 3.5] # class 1 after background offset
detections = kept(post_process_rfdetr([boxes, logits]))
conf_a = math.exp(4.0) / (math.exp(4.0) + 2)
conf_b = math.exp(3.5) / (math.exp(3.5) + 2)
self.assertEqual(len(detections), 2)
np.testing.assert_allclose(detections[0], [0, conf_a, *A_ROW], atol=2e-3)
np.testing.assert_allclose(detections[1], [1, conf_b, *B_ROW], atol=2e-3)
class TestMemryxSsdlitePostProcess(unittest.TestCase):
def test_keeps_separated_objects_far_from_origin(self):
# the NMS math runs on the host CPU, so the real method is testable
# without MemryX hardware; it only needs the model dimensions and
# the output queue
detector = object.__new__(MemryXDetector)
detector.memx_model_width = WIDTH
detector.memx_model_height = HEIGHT
detector.output_queue = Queue()
# this path uses a 0.5 NMS threshold, so use a tighter pair: zero
# true overlap (10 px gap), IoU 0.69 when misread as [x, y, w, h]
dets = np.zeros((1, 10, 5), dtype=np.float32)
dets[0, 0] = [480, 500, 540, 620, 0.90]
dets[0, 1] = [550, 500, 610, 620, 0.85]
labels = np.zeros((1, 10), dtype=np.float32)
labels[0, 1] = 1
detector.post_process_ssdlite([dets, labels])
detections = kept(detector.output_queue.get())
self.assertEqual(len(detections), 2)
np.testing.assert_allclose(
detections[0],
[0, 0.90, 500 / 640, 480 / 640, 620 / 640, 540 / 640],
atol=2e-3,
)
np.testing.assert_allclose(
detections[1],
[1, 0.85, 500 / 640, 550 / 640, 620 / 640, 610 / 640],
atol=2e-3,
)
if __name__ == "__main__":
unittest.main()
-92
View File
@@ -785,98 +785,6 @@ class TestProfileManager(unittest.TestCase):
manager.activate_profile("armed", clear_runtime_overrides=False)
dispatcher.clear_runtime_state.assert_not_called()
def test_apply_profile_to_config_mutates_the_config(self):
"""The config-only half applies the same overrides as activation."""
err = self.manager.apply_profile_to_config("armed")
assert err is None
front = self.config.cameras["front"]
assert front.notifications.enabled is True
assert front.objects.track == ["person", "car", "package"]
def test_apply_profile_to_config_makes_no_zmq_mqtt_or_disk_writes(self):
"""Workers are started with the values, so nothing is published yet."""
dispatcher = MagicMock()
manager = ProfileManager(self.config, self.mock_updater, dispatcher)
with patch.object(ProfileManager, "_persist_active_profile") as mock_persist:
manager.apply_profile_to_config("armed")
self.mock_updater.publish_update.assert_not_called()
dispatcher.publish.assert_not_called()
mock_persist.assert_not_called()
# bookkeeping stays with activate_profile
assert self.config.active_profile is None
def test_apply_profile_to_config_rejects_an_unknown_profile(self):
err = self.manager.apply_profile_to_config("nonexistent")
assert err is not None
assert "not defined" in err
def test_restore_persisted_profile_to_config_applies_it(self):
"""The startup config pass restores what was persisted."""
with patch.object(
ProfileManager, "load_persisted_profile", return_value="armed"
):
self.manager.restore_persisted_profile_to_config()
assert self.config.cameras["front"].notifications.enabled is True
# still the config-only half, so nothing is published or persisted
self.mock_updater.publish_update.assert_not_called()
assert self.config.active_profile is None
def test_restore_persisted_profile_to_config_no_op_when_none_persisted(self):
with patch.object(ProfileManager, "load_persisted_profile", return_value=None):
self.manager.restore_persisted_profile_to_config()
assert self.config.cameras["front"].notifications.enabled is False
def test_restore_persisted_profile_to_config_ignores_a_stale_name(self):
"""A profile no longer offered by any camera must not be applied."""
with patch.object(
ProfileManager, "load_persisted_profile", return_value="ghost"
):
self.manager.restore_persisted_profile_to_config()
assert self.config.cameras["front"].notifications.enabled is False
@patch.object(ProfileManager, "_persist_active_profile")
def test_restore_persisted_profile_activates_and_publishes(self, mock_persist):
"""The startup publish pass runs a full activation."""
dispatcher = MagicMock()
manager = ProfileManager(self.config, self.mock_updater, dispatcher)
with patch.object(
ProfileManager, "load_persisted_profile", return_value="armed"
):
manager.restore_persisted_profile()
assert self.config.active_profile == "armed"
self.mock_updater.publish_update.assert_called()
# a startup replay must not wipe the runtime overrides layered on top
dispatcher.clear_runtime_state.assert_not_called()
@patch.object(ProfileManager, "_persist_active_profile")
def test_activation_after_apply_still_publishes_every_section(self, mock_persist):
"""Re-deriving the same state must not skip the broadcast.
The processes that started before the config was corrected have no
other channel.
"""
self.manager.apply_profile_to_config("armed")
self.mock_updater.publish_update.reset_mock()
err = self.manager.activate_profile("armed", clear_runtime_overrides=False)
assert err is None
published = {
call.args[0].update_type.name
for call in self.mock_updater.publish_update.call_args_list
}
assert "notifications" in published
assert "objects" in published
assert self.config.active_profile == "armed"
@patch.object(ProfileManager, "_persist_active_profile")
def test_update_config_preserves_runtime_state_with_active_profile(
self, mock_persist
+1 -92
View File
@@ -1,4 +1,4 @@
"""Tests for ONVIF state that must not depend on the autotracking config.
"""Tests for ONVIF init state that must not depend on the autotracking config.
Regression coverage for a camera that is initialized while autotracking is off and
has it enabled later, which is the normal wizard flow: set the camera up first,
@@ -10,17 +10,12 @@ the tracking thread.
The request objects are built from the locally parsed WSDL and cost no network, so
they are always created and init=True now implies they exist.
Also covers the inverse direction: the ptz movement timestamps must not be written
for a camera that has autotracking off, because nothing clears them back out.
"""
import unittest
from unittest.mock import AsyncMock, MagicMock
from frigate.camera import PTZMetrics
from frigate.config import FrigateConfig
from frigate.ptz.autotrack import ptz_moving_at_frame_time
from frigate.ptz.onvif import OnvifController
CAMERA = "ptz_cam"
@@ -102,36 +97,6 @@ def _make_controller(autotracking_enabled: bool) -> OnvifController:
return controller
def _make_move_controller(autotracking_enabled: bool) -> OnvifController:
"""Build an already initialized controller for a camera that supports relative
FOV movement, with real metrics so the timestamp writes can be asserted on."""
config = _config(autotracking_enabled)
controller = OnvifController.__new__(OnvifController)
controller.config = config
controller.camera_configs = {CAMERA: config.cameras[CAMERA]}
controller.failed_cams = {}
ptz = MagicMock()
ptz.RelativeMove = AsyncMock()
controller.cams = {
CAMERA: {
"init": True,
"active": False,
"ptz": ptz,
"features": ["pt", "pt-r-fov"],
"relative_move_request": MagicMock(),
"relative_fov_range": {
"XRange": {"Min": -1.0, "Max": 1.0},
"YRange": {"Min": -1.0, "Max": 1.0},
},
}
}
controller.ptz_metrics = {
CAMERA: PTZMetrics(autotracker_enabled=autotracking_enabled)
}
return controller
class TestOnvifInitRequests(unittest.IsolatedAsyncioTestCase):
async def test_status_request_created_when_autotracking_disabled(self) -> None:
# the wizard flow: onvif configured first, autotracking enabled later
@@ -178,61 +143,5 @@ class TestOnvifInitRequests(unittest.IsolatedAsyncioTestCase):
ptz.GetStatus.assert_not_called()
class TestManualRelativeMoveMetrics(unittest.IsolatedAsyncioTestCase):
"""A manual move from the UI (click to move, drag to zoom) sends move_relative
for any camera that advertises pt-r-fov, autotracking or not."""
async def test_metrics_untouched_when_autotracking_disabled(self) -> None:
# only camera_maintenance polls get_camera_status, and only for autotracking
# cameras, so a manual move that starts the clock here is never stopped
controller = _make_move_controller(autotracking_enabled=False)
metrics = controller.ptz_metrics[CAMERA]
metrics.frame_time.value = 1000.0
await controller._move_relative(CAMERA, 0.25, -0.25, 0, 1)
controller.cams[CAMERA]["ptz"].RelativeMove.assert_awaited_once()
self.assertEqual(metrics.start_time.value, 0)
self.assertEqual(metrics.stop_time.value, 0)
self.assertTrue(metrics.motor_stopped.is_set())
async def test_detection_regions_not_suppressed_after_manual_move(self) -> None:
# the symptom of the bug: object detection stops entirely because motion
# boxes are never promoted to detection regions again
controller = _make_move_controller(autotracking_enabled=False)
metrics = controller.ptz_metrics[CAMERA]
metrics.frame_time.value = 1000.0
await controller._move_relative(CAMERA, 0.25, -0.25, 0, 1)
for later_frame_time in (1001.0, 1060.0, 4600.0):
with self.subTest(frame_time=later_frame_time):
self.assertFalse(
ptz_moving_at_frame_time(
later_frame_time,
metrics.start_time.value,
metrics.stop_time.value,
)
)
async def test_metrics_written_when_autotracking_enabled(self) -> None:
# get_camera_status resets stop_time once the camera reports IDLE, so the
# autotracking path keeps its motion estimation timestamps
controller = _make_move_controller(autotracking_enabled=True)
metrics = controller.ptz_metrics[CAMERA]
metrics.frame_time.value = 1000.0
await controller._move_relative(CAMERA, 0.25, -0.25, 0, 1)
self.assertEqual(metrics.start_time.value, 1000.0)
self.assertEqual(metrics.stop_time.value, 0)
self.assertFalse(metrics.motor_stopped.is_set())
self.assertTrue(
ptz_moving_at_frame_time(
1001.0, metrics.start_time.value, metrics.stop_time.value
)
)
if __name__ == "__main__":
unittest.main()
-12
View File
@@ -472,18 +472,6 @@ def sanitize_float(value):
return value
def has_non_finite_number(value: Any) -> bool:
"""Return True if any number in a parsed JSON value is NaN or infinite."""
if isinstance(value, float):
return not math.isfinite(value)
if isinstance(value, dict):
return any(has_non_finite_number(v) for v in value.values())
if isinstance(value, list):
return any(has_non_finite_number(v) for v in value)
return False
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
return 1 - cosine_distance(a, b)
+5 -37
View File
@@ -16,31 +16,6 @@ logger = logging.getLogger(__name__)
### Post Processing
def xyxy_to_xywh_for_nms(boxes: np.ndarray | list) -> np.ndarray:
"""Convert [x1, y1, x2, y2] boxes to the [x, y, width, height] format
that cv2.dnn.NMSBoxes expects.
Passing corner coordinates directly makes OpenCV treat x2/y2 as the box
size, inflating every box toward the bottom-right by its distance from
the origin, which suppresses valid detections near other objects.
Args:
boxes: Array-like of shape (N, 4) in corner format.
Returns:
Float32 array of shape (N, 4) in top-left plus size format.
"""
boxes = np.asarray(boxes, dtype=np.float32)
if boxes.size == 0:
return np.zeros((0, 4), dtype=np.float32)
xywh = boxes.copy()
xywh[:, 2] -= xywh[:, 0]
xywh[:, 3] -= xywh[:, 1]
return xywh
def post_process_dfine(
tensor_output: np.ndarray, width: int, height: int
) -> np.ndarray:
@@ -50,9 +25,7 @@ def post_process_dfine(
input_shape = np.array([height, width, height, width])
boxes = np.divide(boxes, input_shape, dtype=np.float32)
indices = cv2.dnn.NMSBoxes(
xyxy_to_xywh_for_nms(boxes), scores, score_threshold=0.4, nms_threshold=0.4
)
indices = cv2.dnn.NMSBoxes(boxes, scores, score_threshold=0.4, nms_threshold=0.4)
detections = np.zeros((20, 6), np.float32)
for i, (bbox, confidence, class_id) in enumerate(
@@ -105,10 +78,7 @@ def post_process_rfdetr(tensor_output: list[np.ndarray, np.ndarray]) -> np.ndarr
# apply nms
indices = cv2.dnn.NMSBoxes(
xyxy_to_xywh_for_nms(filtered_boxes),
filtered_scores,
score_threshold=0.4,
nms_threshold=0.4,
filtered_boxes, filtered_scores, score_threshold=0.4, nms_threshold=0.4
)
detections = np.zeros((20, 6), np.float32)
@@ -189,7 +159,7 @@ def __post_process_multipart_yolo(
all_class_ids.append(class_id)
indices = cv2.dnn.NMSBoxes(
bboxes=xyxy_to_xywh_for_nms(all_boxes),
bboxes=all_boxes,
scores=all_scores,
score_threshold=0.4,
nms_threshold=0.4,
@@ -236,9 +206,7 @@ def __post_process_nms_yolo(predictions: np.ndarray, width, height) -> np.ndarra
boxes = boxes_xyxy
# run NMS
indices = cv2.dnn.NMSBoxes(
xyxy_to_xywh_for_nms(boxes), scores, score_threshold=0.4, nms_threshold=0.4
)
indices = cv2.dnn.NMSBoxes(boxes, scores, score_threshold=0.4, nms_threshold=0.4)
detections = np.zeros((20, 6), np.float32)
for i, (bbox, confidence, class_id) in enumerate(
zip(boxes[indices], scores[indices], class_ids[indices])
@@ -290,7 +258,7 @@ def post_process_yolox(
scores = scores[np.arange(len(cls_inds)), cls_inds]
indices = cv2.dnn.NMSBoxes(
xyxy_to_xywh_for_nms(boxes_xyxy), scores, score_threshold=0.4, nms_threshold=0.4
boxes_xyxy, scores, score_threshold=0.4, nms_threshold=0.4
)
detections = np.zeros((20, 6), np.float32)
+6 -6
View File
@@ -35,11 +35,6 @@ logger = logging.getLogger(__name__)
GRID_SIZE = 8
def create_empty_regions_grid() -> list[list[dict[str, Any]]]:
"""Create a region grid with no learned sizes."""
return [[{"sizes": []} for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
def get_camera_regions_grid(
name: str,
detect: DetectConfig,
@@ -52,7 +47,12 @@ def get_camera_regions_grid(
grid = regions.grid
last_update = regions.last_update
except DoesNotExist:
grid = create_empty_regions_grid()
grid = []
for x in range(GRID_SIZE):
row = []
for y in range(GRID_SIZE):
row.append({"sizes": []})
grid.append(row)
last_update = 0
# get events for timeline entries
+6 -11
View File
@@ -358,17 +358,12 @@ def process_frames(
]
# only add in the motion boxes when not calibrating and a ptz is not moving via autotracking
# the ptz timestamps are only maintained while autotracking is on, so gate
# on the metric rather than trusting them to be reset otherwise
ptz_moving = ptz_metrics.autotracker_enabled.value and (
ptz_moving_at_frame_time(
frame_time,
ptz_metrics.start_time.value,
ptz_metrics.stop_time.value,
)
)
if not motion_detector.is_calibrating() and not ptz_moving:
# ptz_moving_at_frame_time() always returns False for non-autotracking cameras
if not motion_detector.is_calibrating() and not ptz_moving_at_frame_time(
frame_time,
ptz_metrics.start_time.value,
ptz_metrics.stop_time.value,
):
# find motion boxes that are not inside tracked object regions
standalone_motion_boxes = [
b for b in motion_boxes if not inside_any(b, regions)
+38 -30
View File
@@ -36,8 +36,8 @@ from __future__ import annotations
import argparse
import os
import sys
from collections.abc import Iterable
from dataclasses import dataclass
from typing import Iterable
import cv2
import numpy as np
@@ -53,31 +53,21 @@ ARCFACE_INPUT_SIZE = 112
# ---------------------------------------------------------------------------
def _bgr_to_rgb(frame: np.ndarray) -> np.ndarray:
"""Mirror BaseEmbedding._bgr_to_rgb."""
if isinstance(frame, np.ndarray) and frame.ndim == 3:
return np.ascontiguousarray(frame[:, :, ::-1])
return frame
def _process_image_frigate(image: np.ndarray) -> Image.Image:
"""Mirror BaseEmbedding._process_image for an ndarray input.
`Image.fromarray` does not reorder channels, so whatever order it is
handed is what reaches the model. Callers swap to RGB first, exactly as
ArcfaceEmbedding._preprocess_inputs does.
NOTE: Frigate passes the output of `cv2.imread` (BGR) directly in. PIL's
`Image.fromarray` does NOT reorder channels, so the embedder effectively
receives a BGR-ordered tensor. We replicate that faithfully here. (Tested
swapping to RGB produces near-identical embeddings; this model is
robust to channel order.)
"""
return Image.fromarray(image)
def arcface_preprocess(image_bgr: np.ndarray) -> np.ndarray:
"""Mirror ArcfaceEmbedding._preprocess_inputs.
Face crops arrive BGR from cv2 and #23712 added the swap to RGB before
embedding, so this script has to do it too.
"""
pil = _process_image_frigate(_bgr_to_rgb(image_bgr))
"""Mirror ArcfaceEmbedding._preprocess_inputs."""
pil = _process_image_frigate(image_bgr)
width, height = pil.size
if width != ARCFACE_INPUT_SIZE or height != ARCFACE_INPUT_SIZE:
@@ -148,7 +138,9 @@ class LandmarkAligner:
M[0, 2] += tX - eyesCenter[0]
M[1, 2] += tY - eyesCenter[1]
aligned = cv2.warpAffine(image, M, (out_w, out_h), flags=cv2.INTER_CUBIC)
aligned = cv2.warpAffine(
image, M, (out_w, out_h), flags=cv2.INTER_CUBIC
)
info = dict(
angle=float(angle),
eye_dist_px=dist,
@@ -441,7 +433,9 @@ def vector_outlier_test(
if neg
else np.array([])
)
baseline_conf_neg = np.array([similarity_to_confidence(c) for c in baseline_neg])
baseline_conf_neg = np.array(
[similarity_to_confidence(c) for c in baseline_neg]
)
print(
f"\nBaseline (trim_mean only, {len(pos)} images):"
@@ -471,7 +465,9 @@ def vector_outlier_test(
mean, keep = iterative_mean(all_embs, T)
pos_sims = np.array([cosine(p.embedding, mean) for p in pos])
neg_sims = (
np.array([cosine(n.embedding, mean) for n in neg]) if neg else np.array([])
np.array([cosine(n.embedding, mean) for n in neg])
if neg
else np.array([])
)
neg_conf = np.array([similarity_to_confidence(c) for c in neg_sims])
margin = pos_sims.min() - (neg_sims.max() if len(neg_sims) else 0)
@@ -487,7 +483,9 @@ def vector_outlier_test(
# Show which images get dropped at the shipped threshold + neighbors
for T_show in (0.25, 0.30, 0.33):
_, keep = iterative_mean(all_embs, T_show)
print(f"\nAt T={T_show}, the {int((~keep).sum())} dropped positives are:")
print(
f"\nAt T={T_show}, the {int((~keep).sum())} dropped positives are:"
)
final_mean = stats.trim_mean(all_embs[keep], base_trim, axis=0)
m_n = final_mean / (np.linalg.norm(final_mean) + 1e-9)
for i, (p, k) in enumerate(zip(pos, keep)):
@@ -503,7 +501,9 @@ def vector_outlier_test(
)
def degenerate_embedding_test(pos: list[FaceSample], neg: list[FaceSample]) -> None:
def degenerate_embedding_test(
pos: list[FaceSample], neg: list[FaceSample]
) -> None:
"""Detect whether negatives and low-quality positives share a degenerate
'tiny/noisy face' region of the embedding space.
@@ -533,7 +533,8 @@ def degenerate_embedding_test(pos: list[FaceSample], neg: list[FaceSample]) -> N
f"(how tightly negatives cluster together)"
)
print(
f" pos<->pos mean cos : {np.nanmean(pp):.3f} (how tightly positives cluster)"
f" pos<->pos mean cos : {np.nanmean(pp):.3f} "
f"(how tightly positives cluster)"
)
print(
f" pos<->neg mean cos : {pn.mean():.3f} "
@@ -557,7 +558,11 @@ def degenerate_embedding_test(pos: list[FaceSample], neg: list[FaceSample]) -> N
neg_scores = np.array([cosine(n.embedding, clean_mean) for n in neg])
neg_confs = np.array([similarity_to_confidence(c) for c in neg_scores])
pos_scores = np.array(
[cosine(pos[i].embedding, clean_mean) for i in range(len(pos)) if keep[i]]
[
cosine(pos[i].embedding, clean_mean)
for i in range(len(pos))
if keep[i]
]
)
print(
f"\n mean_intra >= {thresh}: keeping {int(keep.sum())}/{len(pos)} positives"
@@ -580,7 +585,9 @@ def degenerate_embedding_test(pos: list[FaceSample], neg: list[FaceSample]) -> N
)
def contamination_analysis(pos: list[FaceSample], neg: list[FaceSample]) -> None:
def contamination_analysis(
pos: list[FaceSample], neg: list[FaceSample]
) -> None:
"""Check whether the positive collection contains a second identity.
Two signals:
@@ -610,7 +617,10 @@ def contamination_analysis(pos: list[FaceSample], neg: list[FaceSample]) -> None
"\nPositives closer to a negative than to their own class avg"
"\n(these are candidates for mislabeled images):"
)
print(f"\n{'max_neg':>7} {'mean_neg':>8} {'mean_intra':>10} {'delta':>6} name")
print(
f"\n{'max_neg':>7} {'mean_neg':>8} {'mean_intra':>10} "
f"{'delta':>6} name"
)
rows = list(zip(pos_names, max_to_neg, mean_to_neg, mean_intra))
rows.sort(key=lambda r: -(r[1] - r[3]))
for nm, mxn, mnn, mi in rows[:15]:
@@ -694,9 +704,7 @@ def main() -> int:
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
ap.add_argument(
"--positive", required=True, help="Training folder for one identity"
)
ap.add_argument("--positive", required=True, help="Training folder for one identity")
ap.add_argument(
"--negative",
default=None,
@@ -1,204 +0,0 @@
/**
* Camera live playback stream settings tests -- MEDIUM tier.
*
* The live streams field maps a display name to a go2rtc stream. Switching
* cameras from the selector keeps the form mounted and only swaps its data, so
* the stream name input has to follow the newly selected camera. It used to be
* an uncontrolled input, which left the previous camera's stream name on screen
* and renamed the wrong key if the stale text was ever committed.
*
* Renames are committed per keystroke so the section is marked as modified
* right away, except while the typed name belongs to another stream, since
* renaming onto an existing name merges the two entries.
*/
import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { test, expect } from "../../fixtures/frigate-test";
import type { Page } from "@playwright/test";
import { configFactory } from "../../fixtures/mock-data/config";
const __dirname = dirname(fileURLToPath(import.meta.url));
const CONFIG_SCHEMA = JSON.parse(
readFileSync(
resolve(__dirname, "../../fixtures/mock-data/config-schema.json"),
"utf-8",
),
);
const GO2RTC_STREAMS = {
front_door_main: ["rtsp://user:pass@192.168.0.20:554/Stream1"],
backyard_main: ["rtsp://user:pass@192.168.0.21:554/Stream1"],
};
const CAMERA_LIVE_STREAMS = {
front_door: { front_door: "front_door_main" },
backyard: { backyard: "backyard_main" },
};
const SETTINGS_URL = "/settings?page=cameraLivePlayback&camera=front_door";
async function installRoutes(
page: Page,
frontDoorStreams: Record<string, string> = CAMERA_LIVE_STREAMS.front_door,
) {
const config = configFactory({
go2rtc: { streams: GO2RTC_STREAMS },
cameras: {
front_door: { live: { streams: frontDoorStreams } },
backyard: { live: { streams: CAMERA_LIVE_STREAMS.backyard } },
},
});
let lastSavedConfig: unknown = null;
await page.route("**/api/config/schema.json", (route) =>
route.fulfill({ json: CONFIG_SCHEMA }),
);
await page.route("**/api/config", (route) => {
if (route.request().method() === "GET") {
return route.fulfill({ json: config });
}
return route.fulfill({ json: { success: true } });
});
await page.route("**/api/config/raw_paths", (route) =>
route.fulfill({
json: {
go2rtc: { streams: GO2RTC_STREAMS },
cameras: {
front_door: { live: { streams: frontDoorStreams } },
backyard: { live: { streams: CAMERA_LIVE_STREAMS.backyard } },
},
},
}),
);
await page.route("**/api/config/set", async (route) => {
lastSavedConfig = route.request().postDataJSON();
await route.fulfill({ json: { success: true, require_restart: false } });
});
return { capturedConfig: () => lastSavedConfig };
}
async function selectCamera(page: Page, friendlyName: string) {
await page.getByRole("button", { name: "Select a camera" }).click();
await page.getByRole("switch", { name: friendlyName }).click();
}
function streamNameInputs(page: Page) {
return page.getByRole("textbox", { name: "Stream name" });
}
function streamNames(page: Page) {
return streamNameInputs(page).evaluateAll((inputs) =>
inputs.map((input) => (input as HTMLInputElement).value),
);
}
/** Rows render in config order, which is not the order they were declared in. */
async function streamNameRow(page: Page, name: string) {
await expect.poll(() => streamNames(page)).toContain(name);
const names = await streamNames(page);
return streamNameInputs(page).nth(names.indexOf(name));
}
test.describe("camera live playback streams @medium", () => {
test("switching cameras updates the stream name field", async ({
frigateApp,
}) => {
await installRoutes(frigateApp.page);
await frigateApp.goto(SETTINGS_URL);
const streamName = frigateApp.page.getByRole("textbox", {
name: "Stream name",
});
await expect(streamName).toHaveValue("front_door");
await expect(
frigateApp.page.getByRole("combobox", { name: "go2rtc stream" }),
).toContainText("front_door_main");
await selectCamera(frigateApp.page, "Backyard");
await expect(streamName).toHaveValue("backyard");
await expect(
frigateApp.page.getByRole("combobox", { name: "go2rtc stream" }),
).toContainText("backyard_main");
});
test("typing a new name enables Save without leaving the field", async ({
frigateApp,
}) => {
await installRoutes(frigateApp.page);
await frigateApp.goto(SETTINGS_URL);
const save = frigateApp.page.getByRole("button", { name: "Save" });
await expect(save).toBeDisabled();
const streamName = await streamNameRow(frigateApp.page, "front_door");
await streamName.click();
await frigateApp.page.keyboard.press("End");
await frigateApp.page.keyboard.type("_hd");
// Still focused: the rename is committed per keystroke, not on blur.
await expect(save).toBeEnabled();
await expect(streamName).toBeFocused();
await expect(streamName).toHaveValue("front_door_hd");
});
test("typing through another stream's name keeps both streams", async ({
frigateApp,
}) => {
await installRoutes(frigateApp.page, {
front: "front_door_main",
front_door: "backyard_main",
});
await frigateApp.goto(SETTINGS_URL);
const streamName = await streamNameRow(frigateApp.page, "front_door");
await streamName.click();
await frigateApp.page.keyboard.press("End");
// "front_door" passes through "front", which the other row already uses.
await frigateApp.page.keyboard.press("Backspace");
await frigateApp.page.keyboard.press("Backspace");
await frigateApp.page.keyboard.press("Backspace");
await frigateApp.page.keyboard.press("Backspace");
await frigateApp.page.keyboard.press("Backspace");
await expect(streamName).toHaveValue("front");
await frigateApp.page.keyboard.type("yard");
await streamName.blur();
expect(await streamNames(frigateApp.page)).toEqual(["frontyard", "front"]);
});
test("renaming a stream saves the new name for the selected camera", async ({
frigateApp,
}) => {
const capture = await installRoutes(frigateApp.page);
await frigateApp.goto(SETTINGS_URL);
await selectCamera(frigateApp.page, "Backyard");
const streamName = frigateApp.page.getByRole("textbox", {
name: "Stream name",
});
await expect(streamName).toHaveValue("backyard");
await streamName.fill("Backyard HD");
// The rename is committed on blur, not on every keystroke.
await streamName.blur();
await frigateApp.page.getByRole("button", { name: "Save" }).click();
await expect
.poll(() => capture.capturedConfig(), { timeout: 5_000 })
.toMatchObject({
config_data: {
cameras: {
backyard: {
live: { streams: { "Backyard HD": "backyard_main" } },
},
},
},
});
});
});
+4 -4
View File
@@ -73,7 +73,7 @@
"react-markdown": "^9.0.1",
"react-router-dom": "^6.30.3",
"react-swipeable": "^7.0.2",
"react-zoom-pan-pinch": "3.4.4",
"react-zoom-pan-pinch": "3.6.1",
"remark-gfm": "^4.0.0",
"scroll-into-view-if-needed": "^3.1.0",
"sonner": "^2.0.7",
@@ -12354,9 +12354,9 @@
}
},
"node_modules/react-zoom-pan-pinch": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.4.4.tgz",
"integrity": "sha512-lGTu7D9lQpYEQ6sH+NSlLA7gicgKRW8j+D/4HO1AbSV2POvKRFzdWQ8eI0r3xmOsl4dYQcY+teV6MhULeg1xBw==",
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.6.1.tgz",
"integrity": "sha512-SdPqdk7QDSV7u/WulkFOi+cnza8rEZ0XX4ZpeH7vx3UZEg7DoyuAy3MCmm+BWv/idPQL2Oe73VoC0EhfCN+sZQ==",
"license": "MIT",
"engines": {
"node": ">=8",
+1 -1
View File
@@ -87,7 +87,7 @@
"react-markdown": "^9.0.1",
"react-router-dom": "^6.30.3",
"react-swipeable": "^7.0.2",
"react-zoom-pan-pinch": "3.4.4",
"react-zoom-pan-pinch": "3.6.1",
"remark-gfm": "^4.0.0",
"scroll-into-view-if-needed": "^3.1.0",
"sonner": "^2.0.7",
+1 -2
View File
@@ -61,8 +61,7 @@
"error": {
"endTimeMustAfterStartTime": "L'hora de finalització ha de ser posterior a l'hora d'inici",
"noVaildTimeSelected": "No s'ha seleccionat un rang de temps vàlid",
"failed": "No s'ha pogut inciar l'exportació: {{error}}",
"noValidTimeSelected": "No s'ha seleccionat cap interval de temps vàlid"
"failed": "No s'ha pogut inciar l'exportació: {{error}}"
},
"view": "Vista",
"queued": "Exporta a la cua. Mostra el progrés a la pàgina d'exportacions.",
+2 -5
View File
@@ -493,9 +493,6 @@
"max_concurrent": {
"label": "Màxim d'exportacions concurrents",
"description": "Nombre màxim de treballs d'exportació a processar al mateix temps."
},
"chapters": {
"label": "Metadades de capítol per incrustar en els enregistraments exportats"
}
},
"preview": {
@@ -867,8 +864,8 @@
"description": "Ordre numèric utilitzat per ordenar la càmera a la interfície d'usuari (taulell de control i llistes per defecte); els nombres més grans apareixen més tard."
},
"dashboard": {
"label": "Mostra al tauler en directe",
"description": "Alterna si aquesta càmera és visible al tauler de control en directe de totes les càmeres per defecte. La càmera roman disponible a tot arreu a la interfície d'usuari, inclosos els grups i la configuració de la càmera."
"label": "Mostra a l'interfície d'usuari",
"description": "Estableix si aquesta càmera és visible a tot arreu a la interfície d'usuari de la Frigate. Desactivar això requerirà editar manualment la configuració per tornar a veure aquesta càmera a la interfície d'usuari."
},
"review": {
"label": "Mostra en la revisió",
+4 -7
View File
@@ -380,9 +380,6 @@
"max_concurrent": {
"label": "Màxim d'exportacions concurrents",
"description": "Nombre màxim de treballs d'exportació a processar al mateix temps."
},
"chapters": {
"label": "Metadades de capítol per incrustar en els enregistraments exportats"
}
},
"preview": {
@@ -1920,7 +1917,7 @@
},
"model_type": {
"label": "Tipus de Model de detecció d'objecte",
"description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas, yolo-generic, rfdetr, dfine) utilitzat per alguns detectors per a l'optimització"
"description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) usat per l'optimització d'alguns detectors."
}
},
"model_path": {
@@ -1969,7 +1966,7 @@
},
"model_type": {
"label": "Tipus de model de detecció d'objectes",
"description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas, yolo-generic, rfdetr, dfine) utilitzat per alguns detectors per a l'optimització."
"description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització."
}
},
"genai": {
@@ -2338,8 +2335,8 @@
"description": "Ordre numèric utilitzat per ordenar la càmera a la interfície d'usuari (taulell de control i llistes per defecte); els nombres més grans apareixen més tard."
},
"dashboard": {
"label": "Mostra al tauler en directe",
"description": "Alterna si aquesta càmera és visible al tauler de control en directe de totes les càmeres per defecte. La càmera roman disponible a tot arreu a la interfície d'usuari, inclosos els grups i la configuració de la càmera."
"label": "Mostra a la interfície",
"description": "Estableix si aquesta càmera és visible a tot arreu a la interfície d'usuari de Frigate. Desactivar això requerirà editar manualment la configuració per tornar a veure aquesta càmera a la interfície d'usuari."
},
"review": {
"label": "Mostra en la revisió",
+1 -3
View File
@@ -126,7 +126,5 @@
"baby_stroller": "Cotxet",
"rickshaw": "Ricksaw",
"Rodent": "Rosegador",
"rodent": "Rosegador",
"possum": "Possum",
"garbage_truck": "Camió de brossa"
"rodent": "Rosegador"
}
@@ -192,20 +192,7 @@
"title": "Edita el model de classificació",
"descriptionState": "Edita les classes per a aquest model de classificació d'estats. Els canvis requeriran tornar a entrenar el model.",
"descriptionObject": "Edita el tipus d'objecte i el tipus de classificació per a aquest model de classificació d'objectes.",
"stateClassesInfo": "S'ha actualitzat el model. Restringeix el model perquè els canvis de classe tinguin efecte.",
"enabled": "Habilitat",
"enabledDesc": "Executa aquest model. Quan està desactivat, deixa d'executar-se i ja no classifica.",
"saveAttempts": "Desa els intents",
"saveAttemptsDesc": "Nombre d'imatges de classificació que s'intenten mantenir per a les classificacions recents UI.",
"motion": "Executa en moviment",
"motionDesc": "Executa la classificació quan es detecta el moviment dins de l'escapçat configurat.",
"interval": "Interval",
"intervalDesc": "Segons entre les classificacions periòdiques. Deixeu-ho buit per a executar-se només en moviment.",
"intervalPlaceholder": "Sense interval",
"errors": {
"saveAttemptsInvalid": "Els intents de desar han de ser un nombre sencer de 0 o més",
"intervalInvalid": "L'interval ha de ser un nombre sencer més gran que 0"
}
"stateClassesInfo": "Nota: Canviar les classes d'estat requereix tornar a entrenar el model amb les classes actualitzades."
},
"tooltip": {
"trainingInProgress": "El model s'està entrenant actualment",
@@ -215,6 +202,5 @@
},
"none": "Cap",
"reclassifyImageAs": "Reclassifica la imatge com a:",
"reclassifyImage": "Reclassifica la imatge",
"disabled": "Desactivat"
"reclassifyImage": "Reclassifica la imatge"
}
+2 -5
View File
@@ -76,7 +76,7 @@
"menuItem": "Visualitza les vistes prèvies del moviment",
"title": "Vista prèvia del moviment: {{camera}}",
"mobileSettingsTitle": "Configuració de la vista prèvia del moviment",
"mobileSettingsDesc": "Ajusteu la velocitat de reproducció, l'enfosquiment i l'escapçament, i trieu una data per a revisar clips només en moviment.",
"mobileSettingsDesc": "Ajusteu la velocitat de reproducció i l'enfosquiment, i trieu una data per a revisar clips només en moviment.",
"dim": "Atenuar",
"dimAria": "Ajusta la intensitat de l'enfosquiment",
"dimDesc": "Incrementa l'enfosquiment per augmentar la visibilitat de l'àrea de moviment.",
@@ -89,9 +89,6 @@
"seekAria": "Cerca el reproductor {{camera}} a {{time}}",
"filter": "Filtre",
"filterDesc": "Seleccioneu àrees per a mostrar només clips amb moviment en aquestes regions.",
"filterClear": "Neteja",
"crop": "Escapça per filtrar",
"cropAria": "Commuta la vista prèvia d'escapçament a les àrees filtrades",
"cropDesc": "Amplia les vistes prèvies a les àrees de filtre seleccionades en lloc de mostrar el fotograma complet."
"filterClear": "Neteja"
}
}
+1 -1
View File
@@ -303,7 +303,7 @@
},
"offset": {
"label": "Òfset d'Anotació",
"desc": "Aquestes dades provenen del canal de detecció de la càmera, però estan sobreposades a les imatges del canal de registre. És poc probable que els dos corrents estiguin perfectament sincronitzats. Com a resultat, la caixa contenidora i les imatges no s'alinearan perfectament. Podeu utilitzar aquest paràmetre per a compensar les anotacions cap endavant o cap enrere en el temps per a alinear-les millor amb el metratge gravat.",
"desc": "Aquestes dades provenen del flux de detecció de la càmera, però se superposen a les imatges del flux de gravació. És poc probable que els dos fluxos estiguin perfectament sincronitzats. Com a resultat, el quadre delimitador i les imatges no s'alinearan perfectament. Tanmateix, es pot utilitzar el camp <code>annotation_offset</code> per ajustar-ho.",
"millisecondsToOffset": "Millisegons per l'òfset de detecció d'anotacions per. <em>Per defecte: 0</em>",
"tips": "Reduïu el valor si la reproducció del vídeo es troba per davant dels quadres i els punts de ruta, i augmenteu-lo si es troba per darrere. Aquest valor pot ser negatiu.",
"toast": {
+7 -15
View File
@@ -698,7 +698,7 @@
"title": "Crear un nou usuari",
"confirmPassword": "Siusplau, confirma la contrasenya",
"usernameOnlyInclude": "El nom d'usuari només pot contenir lletres, números, . o _",
"desc": "Afegeix un compte d'usuari nou i especifica un rol per a l'accés a les àrees de la interfície d'usuari de Frigate."
"desc": "Afegeix un nou compte d'usuari i especifica un rol per accedir a àrees de la interfície de Frigate."
}
},
"title": "Usuaris",
@@ -1323,14 +1323,14 @@
"details": {
"edit": "Edita els detalls de la càmera",
"title": "Edita els detalls de la càmera",
"description": "Actualitza el nom de visualització, l'URL extern i la visibilitat utilitzada per a aquesta càmera a tota la interfície d'usuari de Frigate.",
"description": "Actualitza el nom de visualització, l'URL extern i la visibilitat utilitzada per a aquesta càmera a tota la interfície d'usuari de la Fragata.",
"friendlyNameLabel": "Nom a mostrar",
"friendlyNameHelp": "Nom amistós que es mostra per a aquesta càmera a tota la interfície d'usuari de Frigate. Deixeu-ho en blanc per utilitzar l'ID de la càmera.",
"webuiUrlLabel": "URL de la interfície web de la càmera",
"webuiUrlHelp": "URL per a visitar la interfície d'usuari web de la càmera directament des de la vista de depuració. Deixeu-ho en blanc per desactivar l'enllaç.",
"webuiUrlInvalid": "Ha de ser un URL vàlid (p. ex., https://example.com).",
"dashboardLabel": "Mostra al tauler en directe",
"dashboardHelp": "Mostra aquesta càmera al tauler de control en directe predeterminat de totes les càmeres. Es manté disponible a tot arreu, inclosos els grups de càmeres.",
"dashboardHelp": "Mostra aquesta càmera al Tauler en viu.",
"reviewLabel": "Mostra a la ressenya",
"reviewHelp": "Mostra aquesta càmera a Revisió, incloent el filtre de càmera, la revisió de moviment i la vista de l'historial."
},
@@ -1377,7 +1377,7 @@
"deleteCameraDialog": {
"title": "Suprimeix la càmera",
"description": "Suprimir una càmera eliminarà permanentment tots els enregistraments, els objectes rastrejats i la configuració d'aquesta càmera. Qualsevol flux go2rtc associat amb aquesta càmera encara pot haver de ser eliminat manualment.",
"selectPlaceholder": "Trieu la càmera",
"selectPlaceholder": "Trieu la càmera...",
"confirmTitle": "N'estàs segur?",
"confirmWarning": "Suprimir <strong>{{cameraName}}</strong> no es pot desfer.",
"deleteExports": "Elimina també les exportacions d'aquesta càmera",
@@ -1484,7 +1484,7 @@
"successMulti_other": "Configuració copiada a {{count}} càmeres",
"successMultiWithRestart_one": "Configuració copiada a la càmera {{count}}. Reinicia Frigate per aplicar tots els canvis.",
"successMultiWithRestart_many": "Configuració copiada a {{count}} càmeres. Reinicia Frigate per aplicar tots els canvis.",
"successMultiWithRestart_other": "Configuració copiada a {{count}} càmeres. Reinicia Frigate per aplicar tots els canvis.",
"successMultiWithRestart_other": "Configuració copiada a {{count}} càmeres. Reinicia la fragata per aplicar tots els canvis.",
"partialFailure": "{{successCount}} seccions aplicades; «{{failedSection}}» ha fallat: {{errorMessage}}",
"partialFailureMulti": "S'ha copiat a {{successCount}} càmera(es); ha fallat {{failed}}: {{errorMessage}}",
"newCameraPartialFailure": "S'ha creat la càmera {{cameraName}} però no s'han pogut copiar alguns paràmetres: {{errorMessage}}",
@@ -1641,14 +1641,7 @@
"keyLabel": "Clau",
"valueLabel": "Valor",
"keyPlaceholder": "Nou valor",
"remove": "Elimina",
"providerNameLabel": "Nom del proveïdor",
"providerNamePlaceholder": "p. ex., openai",
"variableNameLabel": "Nom de la variable",
"variableNamePlaceholder": ". ex., La_Meva_Variable",
"loggerNameLabel": "Nom del registrador",
"loggerNamePlaceholder": "p. ex., friagte.registre",
"keyPatternError": "Utilitza només lletres, números, guions i guions baixos (sense espais)"
"remove": "Elimina"
},
"timezone": {
"defaultOption": "Utilitza la zona horària del navegador"
@@ -2103,8 +2096,7 @@
"autotrackingNoZones": "Autotraquejar requereix al menys una zona. Defineix una zona per aquesta cámera a Mascares/Zones, després usa'l com a requerit a la part inferior."
},
"ffmpeg": {
"hwaccelManualNotRecommended": "No es recomanen arguments manuals d'acceleració de maquinari. Tret que existeixi un requisit específic, seleccioneu el predefinit que coincideixi amb el vostre maquinari.",
"inputsMissingGo2rtcStream": "Una entrada a sota apunta a un restream go2rtc que ja no existeix. Seleccioneu un restream existent o introduïu manualment l'URL de la càmera, en cas contrari aquesta càmera no es connectarà."
"hwaccelManualNotRecommended": "No es recomanen arguments manuals d'acceleració de maquinari. Tret que existeixi un requisit específic, seleccioneu el predefinit que coincideixi amb el vostre maquinari."
},
"model": {
"optimizedFor320": "Frigate està optimitzada per a un model 320x320, que és la millor opció per a la majoria de configuracions. Un model 640x640 és més lent i només ajuda en escenaris específics.",
+1 -74
View File
@@ -425,78 +425,5 @@
"chop": "Sekání",
"crack": "Prasknutí",
"chink": "Cinknutí",
"field_recording": "Nahrávka z terénu",
"change_ringing": "Změnit vyzvánění",
"liquid": "Tekutina",
"splash": "Šplouchnutí",
"squish": "Zmáčknout",
"drip": "Kapat",
"pour": "Lít",
"trickle": "Stékat",
"fill": "Naplnit",
"stir": "Míchat",
"boiling": "Vařící",
"sonar": "Sonar",
"arrow": "Šíp",
"electronic_tuner": "Elektronický Ladič",
"bang": "Rána",
"slap": "Plácnout",
"smash": "Rozmlátit",
"bouncing": "Odrážející",
"scratch": "Škrábat",
"spray": "Sprej",
"pulse": "Pulz",
"inside": "Uvnitř",
"outside": "Venku",
"reverberation": "Dozvuk",
"echo": "Ozvěna",
"noise": "Hluk",
"mains_hum": "Síťový brum",
"distortion": "Zkreslení",
"sidetone": "Příposlech",
"cacophony": "Kakofonie",
"throbbing": "Pulzování",
"vibration": "Vibrace",
"sodeling": "Jódlování",
"shofar": "Šofar",
"slosh": "Šplouchání",
"gush": "Příval",
"whoosh": "Svištění",
"thump": "Tlumená rána",
"thunk": "Dutá rána",
"effects_unit": "Efektová jednotka",
"chorus_effect": "Chorus",
"whack": "Úder",
"breaking": "Rozbíjení",
"whip": "Švihnutí",
"flap": "Třepotání",
"scrape": "Drhnutí",
"rub": "Tření",
"roll": "Kutálení",
"crushing": "Drcení",
"crumpling": "Mačkání",
"tearing": "Trhání",
"beep": "Pípnutí",
"ping": "Ping",
"ding": "Cinknutí",
"clang": "Řinčení",
"squeal": "Skřípění",
"creak": "Vrzání",
"rustle": "Šustění",
"whir": "Hučení",
"clatter": "Rachocení",
"sizzle": "Prskání",
"clicking": "Klikání",
"clickety_clack": "Klapot",
"rumble": "Dunění",
"plop": "Žbluňknutí",
"hum": "Brum",
"zing": "Zvonivý tón",
"boing": "Pružinový zvuk",
"crunch": "Křupání",
"sine_wave": "Sinusový tón",
"harmonic": "Harmonický tón",
"chirp_tone": "Klouzavý tón",
"pump": "Pumpa",
"basketball_bounce": "Basketbalový odraz"
"field_recording": "Nahrávka z terénu"
}
+4 -28
View File
@@ -120,19 +120,7 @@
"deleteNow": "Smazat hned",
"next": "Další",
"export": "Exportovat",
"continue": "Pokračovat",
"add": "Přidat",
"applying": "Aplikuje se…",
"undo": "Vrátit",
"copiedToClipboard": "Zkopírováno do schránky",
"modified": "Upraveno",
"overridden": "Přepsáno",
"resetToGlobal": "Obnovit globální nastavení",
"resetToDefault": "Obnovit výchozí nastavení",
"saveAll": "Uložit vše",
"savingAll": "Ukládání…",
"undoAll": "Vrátit vše",
"retry": "Zkusit znovu"
"continue": "Pokračovat"
},
"label": {
"back": "Jdi zpět",
@@ -225,9 +213,7 @@
"gl": "Galego (Galicijština)",
"id": "Bahasa Indonesia (Indonéština)",
"ur": "اردو (Urdština)",
"hr": "Hrvatski (Chorvatština)",
"zhHant": "繁體中文 (Tradiční čínština)",
"bs": "Bosanski (Bosenština)"
"hr": "Hrvatski (Chorvatština)"
},
"theme": {
"highcontrast": "Vysoký kontrast",
@@ -265,11 +251,7 @@
"faceLibrary": "Knihovna Obličejů",
"configurationEditor": "Editor Konfigurace",
"withSystem": "Systém",
"classification": "Klasifikace",
"profiles": "Profily",
"actions": "Akce",
"features": "Funkce",
"chat": "Chat"
"classification": "Klasifikace"
},
"pagination": {
"previous": {
@@ -300,8 +282,7 @@
"error": {
"title": "Chyba při ukládání změn konfigurace: {{errorMessage}}",
"noMessage": "Chyba při ukládání změn konfigurace"
},
"success": "Změny konfigurace byly úspěšně uloženy."
}
}
},
"role": {
@@ -322,10 +303,5 @@
},
"information": {
"pixels": "{{area}}px"
},
"no_items": "Žádné položky",
"validation_errors": "Chyby ověření",
"credentialField": {
"savedPlaceholder": "Uloženo ponechte prázdné pro zachování aktuální hodnoty"
}
}
+2 -6
View File
@@ -68,10 +68,7 @@
"desc": "Vyberte kamery pro tuto skupinu."
},
"icon": "Ikona",
"success": "Skupina kamer {{name}} byla uložena.",
"showAll": "Zobrazit všechny skupiny kamer",
"showLess": "Zobrazit méně",
"editGroups": "Upravit skupiny kamer"
"success": "Skupina kamer {{name}} byla uložena."
},
"debug": {
"options": {
@@ -85,7 +82,6 @@
"regions": "Kraje",
"timestamp": "Časové razítko",
"boundingBox": "Ohraničení",
"mask": "Maska",
"paths": "Cesty"
"mask": "Maska"
}
}
+2 -69
View File
@@ -6,8 +6,7 @@
"title": "Frigate restartuje",
"content": "Tato stránka bude obnovena za {{countdown}} sekund.",
"button": "Vynutit opětovné načtení"
},
"description": "Frigate bude během restartu krátce nedostupný."
}
},
"explore": {
"plus": {
@@ -20,9 +19,6 @@
},
"state": {
"submitted": "Odesláno"
},
"toast": {
"error": "Odeslání do Frigate+ se nezdařilo. Zkontrolujte své síťové připojení a zkuste to znovu."
}
},
"submitToPlus": {
@@ -78,11 +74,7 @@
"endTimeMustAfterStartTime": "Čas konce musí být po čase začátku",
"noVaildTimeSelected": "Není vybráno žádné platné časové období"
},
"view": "Zobrazení",
"queued": "Export zařazen do fronty. Průběh zobrazíte na stránce Exporty.",
"batchSuccess_one": "Začal 1 export. Nyní se otevírá případ.",
"batchSuccess_few": "Začaly {{count}} exporty. Nyní se otevírá případ.",
"batchSuccess_other": "Začalo {{count}} exportů. Nyní se otevírá případ."
"view": "Zobrazení"
},
"fromTimeline": {
"saveExport": "Uložit export",
@@ -90,65 +82,6 @@
},
"name": {
"placeholder": "Jméno exportu"
},
"case": {
"newCaseOption": "Vytvořit nový případ",
"newCaseNamePlaceholder": "Název nového případu",
"newCaseDescriptionPlaceholder": "Popis případu",
"label": "Případ",
"nonAdminHelp": "Pro tyto exporty bude vytvořen nový případ.",
"placeholder": "Vyberte případ"
},
"queueing": "Přidávání exportu do fronty…",
"tabs": {
"export": "Jedna kamera",
"multiCamera": "Vícekamerový"
},
"multiCamera": {
"timeRange": "Časový rozsah",
"selectFromTimeline": "Vybrat z časové osy",
"cameraSelection": "Kamery",
"cameraSelectionHelp": "Kamery se sledovanými objekty v tomto časovém rozsahu jsou předem vybrány",
"searchOrSelectGroup": "Vyhledejte nebo vyberte skupinu kamer…",
"selectAll": "Vybrat všechny kamery",
"clearSelection": "Zrušit výběr",
"selectWithActivity": "Kamery se sledovanými objekty",
"selectGroup": "Vybrat skupinu",
"noMatchingCameras": "Žádná kamera neodpovídá vašemu hledání",
"selectedCount": "{{selected}} / {{total}} vybráno",
"checkingActivity": "Kontrola aktivity kamer…",
"noCameras": "Nejsou k dispozici žádné kamery",
"detectionCount_one": "1 sledovaný objekt",
"detectionCount_few": "{{count}} sledované objekty",
"detectionCount_other": "{{count}} sledovaných objektů",
"nameLabel": "Název exportu",
"namePlaceholder": "Volitelný základní název pro tyto exporty",
"queueingButton": "Příprava exportů…",
"exportButton_one": "Exportovat 1 kameru",
"exportButton_few": "Exportovat {{count}} kamery",
"exportButton_other": "Exportovat {{count}} kamer"
},
"multi": {
"title_one": "Exportovat 1 kontrolu",
"title_few": "Exportovat {{count}} kontroly",
"title_other": "Exportovat {{count}} kontrol",
"description": "Exportujte každou vybranou kontrolu. Všechny exporty budou seskupeny do jednoho případu.",
"descriptionNoCase": "Exportujte každou vybranou kontrolu.",
"caseNamePlaceholder": "Export kontroly {{date}}",
"exportButton_one": "Exportovat 1 kontrolu",
"exportButton_few": "Exportovat {{count}} kontroly",
"exportButton_other": "Exportovat {{count}} kontrol",
"exportingButton": "Exportování…",
"toast": {
"started_one": "Spuštěn 1 export. Otevírá se případ.",
"started_few": "Spuštěny {{count}} exporty. Otevírá se případ.",
"started_other": "Spuštěno {{count}} exportů. Otevírá se případ.",
"startedNoCase_one": "Spuštěn 1 export.",
"startedNoCase_few": "Spuštěny {{count}} exporty.",
"startedNoCase_other": "Spuštěno {{count}} exportů.",
"partial": "Spuštěno {{successful}} z {{total}} exportů. Selhalo: {{failedItems}}",
"failed": "Nepodařilo se spustit {{total}} exportů. Selhalo: {{failedItems}}"
}
}
},
"streaming": {
+1 -2
View File
@@ -309,8 +309,7 @@
"triggers": "Spouštěče",
"cameraManagement": "Správa",
"cameraReview": "Kontrola",
"roles": "Role",
"profiles": "Profily"
"roles": "Role"
},
"dialog": {
"unsavedChanges": {
+1 -136
View File
@@ -206,140 +206,5 @@
"whale_vocalization": "Hvallyde",
"music": "Musik",
"musical_instrument": "Musikinstrument",
"plucked_string_instrument": "Strengeinstrument",
"zither": "Citar",
"ukulele": "Ukulele",
"piano": "Piano",
"electric_piano": "Elektrisk Piano",
"organ": "orgel",
"electronic_organ": "Elektrisk orgel",
"hammond_organ": "Hammond orgel",
"synthesizer": "Synthesizer",
"sampler": "Sampler",
"harpsichord": "Mundharmonika",
"percussion": "Percussion",
"drum_kit": "Trommesæt",
"drum_machine": "Trommemaskine",
"drum": "Tromme",
"timpani": "Pauker",
"tabla": "Tabla",
"cymbal": "Bækken",
"hi_hat": "Hi-Hat",
"wood_block": "Woodblock",
"maraca": "Maraca",
"gong": "Gong",
"tubular_bells": "Rørklokker",
"mallet_percussion": "Mallet percussion",
"glockenspiel": "Klokkespil",
"vibraphone": "Vibrafon",
"steelpan": "olietønde",
"orchestra": "Orkester",
"brass_instrument": "Messingblæser",
"french_horn": "Valdhorn",
"bowed_string_instrument": "Stryger",
"string_section": "Strygerdel",
"pizzicato": "Pizzicato",
"cello": "Cello",
"double_bass": "Kontrabas",
"wind_instrument": "Blæseinstrument",
"church_bell": "Kirkeklokke",
"jingle_bell": "Bjældeklang",
"bicycle_bell": "Ringeklokke",
"tuning_fork": "Stemmegaffel",
"chime": "Kimen",
"wind_chime": "Vindklokke",
"accordion": "Harmonika",
"theremin": "Theremin",
"singing_bowl": "Syngeskål",
"scratching": "Kradse",
"pop_music": "Popmusik",
"hip_hop_music": "Hip-Hopmusik",
"beatboxing": "Beatboxing",
"rock_music": "Rockmusik",
"heavy_metal": "Heavymetal",
"punk_rock": "Punkrock",
"grunge": "Grunge",
"progressive_rock": "Progressiv rock",
"rock_and_roll": "Rock and Roll",
"psychedelic_rock": "Psykedelisk rock",
"rhythm_and_blues": "Rhythm and Blues",
"soul_music": "Soulmusik",
"reggae": "Reggae",
"country": "Country",
"disco": "Disko",
"classical_music": "Klassisk musik",
"electronic_music": "Elektronisk musik",
"house_music": "Housemusik",
"drum_and_bass": "Drum and Bass",
"electronica": "Elektronisk musik",
"electronic_dance_music": "Elektronisk dancemusik",
"ambient_music": "Ambient musik",
"trance_music": "Trancemusik",
"music_of_latin_america": "Musik fra Latin Amerika",
"salsa_music": "Salsamusik",
"flamenco": "Flamenco",
"music_for_children": "Musik for børn",
"new-age_music": "New Age musik",
"vocal_music": "Vokalmusik",
"a_capella": "A Capella",
"music_of_africa": "Musik fra Afrika",
"afrobeat": "Afrobeat",
"christian_music": "Kristen musik",
"gospel_music": "Gospelmusik",
"music_of_asia": "Musik fra Asian",
"ska": "Ska",
"traditional_music": "Traditionel musik",
"independent_music": "Individual musik",
"background_music": "Baggrundsmusik",
"theme_music": "Temamusik",
"jingle": "Jingle",
"soundtrack_music": "Soundtrack musik",
"video_game_music": "Videospil musik",
"christmas_music": "Julemusik",
"dance_music": "Dance musik",
"wedding_music": "Bryllupsmusik",
"happy_music": "Glad musik",
"sad_music": "Trist musik",
"tender_music": "Kærlighedsmusik",
"exciting_music": "Spændende musik",
"angry_music": "Vred musik",
"scary_music": "Skræmmende musik",
"rustling_leaves": "Raslende blade",
"wind_noise": "Vindstøj",
"rain_on_surface": "Regn på en overflade",
"stream": "Strøm",
"ocean": "Verdenshav",
"steam": "Damp",
"gurgling": "Gurgle",
"crackle": "Knitren",
"motor_vehicle": "Motoriseret køretøj",
"toot": "Dytte",
"car_alarm": "Bilalarm",
"power_windows": "Elruder",
"skidding": "Udskridning",
"tire_squeal": "Dækhvin",
"car_passing_by": "Forbikørende bil",
"race_car": "Racerbil",
"truck": "Lastbil",
"air_brake": "Trykluftsbremse",
"air_horn": "Trykluftshorn",
"reversing_beeps": "Bakalarm",
"ice_cream_truck": "Isbil",
"emergency_vehicle": "Redningskøretøj",
"police_car": "Politibil",
"fire_engine": "Brandbil",
"traffic_noise": "Trafikstøj",
"rail_transport": "Godstogstransport",
"train_whistle": "Togfløjt",
"train_horn": "Toghorn",
"railroad_car": "Togvogn",
"train_wheels_squealing": "Skinneskrig",
"subway": "Undergrund",
"aircraft": "Fly",
"aircraft_engine": "Flymotor",
"jet_engine": "Jetmotor",
"propeller": "Probel",
"fixed-wing_aircraft": "Fastvingefly",
"engine": "Motor",
"light_engine": "Lille motor"
"plucked_string_instrument": "Strengeinstrument"
}
+1 -5
View File
@@ -30,9 +30,6 @@
"ask_a": "Ist dieses Objekt ein <code>{{label}}</code>?",
"ask_an": "Ist dieses Objekt ein <code>{{label}}</code>?",
"ask_full": "Ist dieses Objekt ein <code>{{untranslatedLabel}}</code> ({{translatedLabel}})?"
},
"toast": {
"error": "Übermittlung an Frigate+ fehlgeschlagen. Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut."
}
},
"submitToPlus": {
@@ -68,8 +65,7 @@
"error": {
"endTimeMustAfterStartTime": "Die Endzeit darf nicht vor der Startzeit liegen",
"failed": "Fehler beim Export in die Warteschlange: {{error}}",
"noVaildTimeSelected": "Kein gültiger Zeitraum ausgewählt",
"noValidTimeSelected": "Kein gültiger Zeitraum ausgewählt"
"noVaildTimeSelected": "Kein gültiger Zeitraum ausgewählt"
},
"success": "Export erfolgreich gestartet. Die Datei befindet sich auf der Exportseite.",
"view": "Ansicht",
+2 -5
View File
@@ -545,9 +545,6 @@
"max_concurrent": {
"label": "Maximale Anzahl gleichzeitiger Exporte",
"description": "Maximale Anzahl der gleichzeitig zu verarbeitenden Exportaufträge."
},
"chapters": {
"label": "Kapitel-Metadaten zur Einbettung in exportierte Aufzeichnungen"
}
},
"preview": {
@@ -771,8 +768,8 @@
"description": "Numerische Reihenfolge, nach der die Kamera in der Benutzeroberfläche sortiert wird (Standard-Dashboard und Listen); höhere Zahlen erscheinen später."
},
"dashboard": {
"label": "Im Live-Dashboard anzeigen",
"description": "Legt fest, ob diese Kamera im Live-Dashboard Alle Kameras angezeigt wird. Die Kamera bleibt in allen anderen Bereichen der Benutzeroberfläche verfügbar, einschließlich Kamera-Gruppen und Einstellungen."
"label": "In der Benutzeroberfläche anzeigen",
"description": "Schalte ein, ob diese Kamera überall in der Benutzeroberfläche von „Frigate“ sichtbar ist. Wenn du diese Option deaktivierst, musst du die Konfiguration manuell bearbeiten, um diese Kamera wieder in der Benutzeroberfläche anzuzeigen."
},
"review": {
"label": "In der Überprüfung anzeigen",
+4 -7
View File
@@ -1123,7 +1123,7 @@
},
"model_type": {
"label": "Typ des Objekterkennungsmodells",
"description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas, yolo-generic, rfdetr, dfine), der von einigen Detektoren zur Optimierung verwendet wird."
"description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas), der von einigen Detektoren zur Optimierung verwendet wird."
}
},
"model_path": {
@@ -1400,9 +1400,6 @@
"max_concurrent": {
"label": "Maximale Anzahl gleichzeitiger Exporte",
"description": "Maximale Anzahl der gleichzeitig zu verarbeitenden Exportaufträge."
},
"chapters": {
"label": "Kapitel-Metadaten zur Einbettung in exportierte Aufzeichnungen"
}
},
"preview": {
@@ -1732,7 +1729,7 @@
},
"model_type": {
"label": "Typ des Objekterkennungsmodells",
"description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas, yolo-generic, rfdetr, dfine), der von einigen Detektoren zur Optimierung verwendet wird."
"description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas), der von einigen Detektoren zur Optimierung verwendet wird."
}
},
"genai": {
@@ -1935,8 +1932,8 @@
"description": "Numerische Reihenfolge, nach der die Kamera in der Benutzeroberfläche sortiert wird (Standard-Dashboard und Listen); höhere Zahlen erscheinen später."
},
"dashboard": {
"label": "Im Live-Dashboard anzeigen",
"description": "Legt fest, ob diese Kamera im Live-Dashboard Alle Kameras angezeigt wird. Die Kamera bleibt in allen anderen Bereichen der Benutzeroberfläche verfügbar, einschließlich Kamera-Gruppen und Einstellungen."
"label": "In der Benutzeroberfläche anzeigen",
"description": "Schalte ein, ob diese Kamera überall in der Benutzeroberfläche von „Frigate“ sichtbar ist. Wenn du diese Option deaktivierst, musst du die Konfiguration manuell bearbeiten, um diese Kamera wieder in der Benutzeroberfläche anzuzeigen."
},
"review": {
"label": "In der Überprüfung anzeigen",
+1 -3
View File
@@ -125,7 +125,5 @@
"baby": "Baby",
"baby_stroller": "Kinderwagen",
"rickshaw": "Rikscha",
"rodent": "Nagetier",
"garbage_truck": "Müllfahrzeug",
"possum": "Possum"
"rodent": "Nagetier"
}
@@ -64,20 +64,7 @@
"title": "Klassifikationsmodell bearbeiten",
"descriptionState": "Bearbeite die Klassen für dieses Zustandsklassifikationsmodell. Änderungen erfordern ein erneutes Trainieren des Modells.",
"descriptionObject": "Bearbeite den Objekttyp und Klassifizierungstyp für dieses Objektklassifikationsmodell.",
"stateClassesInfo": "Modell aktualisiert. Trainieren Sie das Modell neu, damit die Klassenänderungen wirksam werden.",
"enabled": "Aktiviert",
"enabledDesc": "Dieses Modell ausführen. Bei Deaktivierung wird es nicht mehr ausgeführt und führt keine Klassifizierung mehr durch.",
"saveAttempts": "Versuche speichern",
"saveAttemptsDesc": "Anzahl der für die Übersicht der letzten Klassifizierungen zu speichernden Bilder.",
"motion": "Bei Bewegung ausführen",
"motionDesc": "Klassifizierung bei Bewegung im konfigurierten Bildausschnitt ausführen.",
"interval": "Intervall",
"intervalDesc": "Sekundenabstand zwischen regelmäßigen Klassifizierungsläufen. Leer lassen, um die Klassifizierung nur bei Bewegung auszuführen.",
"intervalPlaceholder": "Kein Intervall",
"errors": {
"saveAttemptsInvalid": "Sparversuche müssen eine ganze Zahl von 0 oder mehr sein",
"intervalInvalid": "Intervall muss eine ganze Nummer sein größer als 0"
}
"stateClassesInfo": "Hinweis: Die Änderung der Statusklassen erfordert ein erneutes Trainieren des Modells mit den aktualisierten Klassen."
},
"deleteDatasetImages": {
"title": "Datensatz Bilder löschen",
@@ -208,6 +195,5 @@
},
"none": "Keiner",
"reclassifyImageAs": "Bild neu klassifizieren als:",
"reclassifyImage": "Bild neu klassifizieren",
"disabled": "Deaktiviert"
"reclassifyImage": "Bild neu klassifizieren"
}
+4 -11
View File
@@ -570,7 +570,7 @@
"openCameraWebUI": "Web-Benutzeroberfläche von {{camera}} öffnen",
"audio": {
"title": "Audio",
"noAudioDetections": "Kein Audio-Ereignis erkannt",
"noAudioDetections": "Keine Audioerkennungen",
"score": "Punktzahl",
"currentRMS": "Aktueller Effektivwert",
"currentdbFS": "Aktuelle dbFS"
@@ -1383,7 +1383,7 @@
"webuiUrlHelp": "URL, um die Web-Benutzeroberfläche der Kamera direkt aus der Debug-Ansicht aufzurufen. Lassen Sie das Feld leer, um den Link zu deaktivieren.",
"webuiUrlInvalid": "Es muss sich um eine gültige URL handeln (z. B. https://example.com).",
"dashboardLabel": "Im Live-Dashboard anzeigen",
"dashboardHelp": "Zeige diese Kamera auf dem Standard Alle Kameras live dashboard. Es bleibt überall verfügbar, einschließlich Kameragruppen.",
"dashboardHelp": "Diese Kamera im Live-Dashboard anzeigen.",
"reviewLabel": "In der Überprüfung anzeigen",
"reviewHelp": "Zeige diese Kamera in der Übersicht an, einschließlich des Kamerafilters, der Bewegungsübersicht und der Verlaufsansicht."
},
@@ -1430,7 +1430,7 @@
"deleteCameraDialog": {
"title": "Kamera löschen",
"description": "Durch das Löschen einer Kamera werden alle Aufzeichnungen, erfassten Objekte und Konfigurationseinstellungen für diese Kamera endgültig entfernt. Alle mit dieser Kamera verbundenen go2rtc-Streams müssen möglicherweise noch manuell entfernt werden.",
"selectPlaceholder": "Kamera auswählen",
"selectPlaceholder": "Kamera auswählen...",
"confirmTitle": "Bist du dir sicher?",
"confirmWarning": "Das Löschen von <strong>{{cameraName}}</strong> kann nicht rückgängig gemacht werden.",
"deleteExports": "Lösche auch die Exporte für diese Kamera",
@@ -1720,14 +1720,7 @@
"keyLabel": "Schlüssel",
"valueLabel": "Wert",
"keyPlaceholder": "Neuer Schlüssel",
"remove": "Entfernen",
"providerNameLabel": "Name des Anbieters",
"providerNamePlaceholder": "z.B. openai",
"variableNameLabel": "Variabler Name",
"variableNamePlaceholder": "z.B. MY_VARIABLE",
"loggerNameLabel": "Name des Loggers",
"loggerNamePlaceholder": "z.B. frigate.record",
"keyPatternError": "Verwenden Sie nur Buchstaben, Zahlen, Bindestriche und Unterstriche (keine Leerzeichen)"
"remove": "Entfernen"
},
"timezone": {
"defaultOption": "Zeitzone des Browsers verwenden"
+2 -2
View File
@@ -859,8 +859,8 @@
"description": "Numeric order used to sort the camera in the UI (default dashboard and lists); larger numbers appear later."
},
"dashboard": {
"label": "Show on Live dashboard",
"description": "Toggle whether this camera is visible on the default All Cameras live dashboard. The camera remains available everywhere else in the UI, including camera groups and settings."
"label": "Show in UI",
"description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again."
},
"review": {
"label": "Show in review",
+18 -2
View File
@@ -357,6 +357,22 @@
"description": "Optional API key for authenticated DeepStack services."
}
},
"degirum": {
"label": "DeGirum",
"description": "DeGirum detector for running models via DeGirum cloud or local inference services.",
"location": {
"label": "Inference Location",
"description": "Location of the DeGirim inference engine (e.g. '@cloud', '127.0.0.1')."
},
"zoo": {
"label": "Model Zoo",
"description": "Path or URL to the DeGirum model zoo."
},
"token": {
"label": "DeGirum Cloud Token",
"description": "Token for DeGirum Cloud access."
}
},
"edgetpu": {
"label": "EdgeTPU",
"description": "EdgeTPU detector that runs TensorFlow Lite models compiled for Coral EdgeTPU using the EdgeTPU delegate.",
@@ -1527,8 +1543,8 @@
"description": "Numeric order used to sort the camera in the UI (default dashboard and lists); larger numbers appear later."
},
"dashboard": {
"label": "Show on Live dashboard",
"description": "Toggle whether this camera is visible on the default All Cameras live dashboard. The camera remains available everywhere else in the UI, including camera groups and settings."
"label": "Show in UI",
"description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again."
},
"review": {
"label": "Show in review",
+2 -5
View File
@@ -74,7 +74,7 @@
"menuItem": "View motion previews",
"title": "Motion previews: {{camera}}",
"mobileSettingsTitle": "Motion Preview Settings",
"mobileSettingsDesc": "Adjust playback speed, dimming, and cropping, and choose a date to review motion-only clips.",
"mobileSettingsDesc": "Adjust playback speed and dimming, and choose a date to review motion-only clips.",
"dim": "Dim",
"dimAria": "Adjust dimming intensity",
"dimDesc": "Increase dimming to increase motion area visibility.",
@@ -87,9 +87,6 @@
"seekAria": "Seek {{camera}} player to {{time}}",
"filter": "Filter",
"filterDesc": "Select areas to only show clips with motion in those regions.",
"filterClear": "Clear",
"crop": "Crop to filter",
"cropAria": "Toggle cropping previews to the filtered areas",
"cropDesc": "Zoom previews into the selected filter areas instead of showing the full frame."
"filterClear": "Clear"
}
}
+3 -2
View File
@@ -463,7 +463,7 @@
"deleteCameraDialog": {
"title": "Delete Camera",
"description": "Deleting a camera will permanently remove all recordings, tracked objects, and configuration for that camera. Any go2rtc streams associated with this camera may still need to be manually removed.",
"selectPlaceholder": "Choose camera",
"selectPlaceholder": "Choose camera...",
"confirmTitle": "Are you sure?",
"confirmWarning": "Deleting <strong>{{cameraName}}</strong> cannot be undone.",
"deleteExports": "Also delete exports for this camera",
@@ -499,7 +499,7 @@
"webuiUrlHelp": "URL to visit the camera's web UI directly from the Debug view. Leave blank to disable the link.",
"webuiUrlInvalid": "Must be a valid URL (e.g., https://example.com).",
"dashboardLabel": "Show on Live dashboard",
"dashboardHelp": "Show this camera on the default All Cameras live dashboard. It remains available everywhere else, including camera groups.",
"dashboardHelp": "Show this camera on the Live dashboard.",
"reviewLabel": "Show in Review",
"reviewHelp": "Show this camera in Review, including the camera filter, motion review, and the history view."
}
@@ -613,6 +613,7 @@
}
},
"footer": {
"changeCount_zero": "No changes selected",
"changeCount_one": "{{count}} change will be applied",
"changeCount_other": "{{count}} changes will be applied",
"restartNeeded": "Restart will be required for some changes.",
+1 -5
View File
@@ -36,9 +36,6 @@
"ask_a": "¿Es este objeto un <code>{{label}}</code>?",
"ask_an": "¿Es este objeto un <code>{{label}}</code>?",
"ask_full": "¿Es este objeto un <code>{{untranslatedLabel}}</code> ({{translatedLabel}})?"
},
"toast": {
"error": "Error al enviarlo a Frigate+. Por favor, comprueba tu conexión a internet y prueba de nuevo."
}
}
},
@@ -71,8 +68,7 @@
"error": {
"failed": "No se pudo iniciar la exportación: {{error}}",
"noVaildTimeSelected": "No se seleccionó un rango de tiempo válido",
"endTimeMustAfterStartTime": "La hora de finalización debe ser posterior a la hora de inicio",
"noValidTimeSelected": "Rango de tiempo seleccionado no valido"
"endTimeMustAfterStartTime": "La hora de finalización debe ser posterior a la hora de inicio"
},
"success": "Exportación iniciada con éxito. Ver el archivo en la página exportaciones.",
"view": "Vista",
+1 -65
View File
@@ -273,69 +273,5 @@
"sailboat": "Purjekas",
"soundtrack_music": "Filmimuusika",
"jingle": "Kõlisemine/tilisemine",
"theme_music": "Tunnusmuusika",
"steel_guitar": "Steel Kitarr",
"tapping": "Koputamine",
"strum": "Klimberdus",
"drum_machine": "Trummimasin",
"drum": "Trumm",
"maraca": "Marakas",
"bowed_string_instrument": "Poogenkeelpill",
"singing_bowl": "Helikauss",
"wind_noise": "Tuulemüra",
"rustling_leaves": "Sahisevad lehed",
"waves": "Lained",
"steam": "Aur",
"ship": "Laev",
"motor_vehicle": "Mootorsõiduk",
"rowboat": "Sõudepaat",
"motorboat": "Mootorpaat",
"waterfall": "Kosk",
"ocean": "Ookean",
"rain_on_surface": "Vihm pinnal",
"stream": "Oja",
"fire": "Tuli",
"crackle": "Praksumine",
"car_alarm": "Auto alarm",
"truck": "Veoauto",
"police_car": "Politseiauto",
"ambulance": "Kiirabi",
"fire_engine": "Tuletõrjeauto",
"aircraft": "Lennuk",
"aircraft_engine": "Lennukimootor",
"jet_engine": "Reaktiivmootor",
"propeller": "Propeller",
"helicopter": "Helikopter",
"fixed-wing_aircraft": "Fikseeritud tiivaga õhusõiduk",
"train_horn": "Rongisignaal",
"railroad_car": "Kaubavagun",
"lawn_mower": "Muruniiduk",
"chainsaw": "mootorsaag",
"engine": "Mootor",
"knock": "Koputus",
"alarm": "Häire",
"siren": "Sireen",
"fire_alarm": "Tulekahjuhäire",
"telephone": "Telefon",
"telephone_bell_ringing": "Helisev telefon",
"ringtone": "Telefonihelin",
"smoke_detector": "Suitsuandur",
"foghorn": "Udupasun",
"whistle": "Vile",
"printer": "Printer",
"drill": "Puur",
"explosion": "Plahvatus",
"hammer": "Haamer",
"air_conditioning": "Õhukonditsioneer",
"gunshot": "Püssilask",
"glass": "Klaas",
"boom": "Pauk",
"fireworks": "Ilutulestik",
"static": "Staatiline",
"white_noise": "Valge müra",
"radio": "Raadio",
"television": "Televiisor",
"scream": "Karjumine",
"pour": "Valamine",
"drip": "Tilkumine"
"theme_music": "Tunnusmuusika"
}
+5 -77
View File
@@ -37,9 +37,6 @@
"ask_an": "Kas see objekt on <code>{{label}}</code>?",
"ask_full": "Kas see objekt on <code>{{untranslatedLabel}}</code> ({{translatedLabel}})?",
"label": "Kinnita see silt Frigate+ teenuse jaoks"
},
"toast": {
"error": "Frigate+ teenusesse saatmine ebaõnnestus. Palun kontrolli oma võrguühendust ja proovi uuesti."
}
},
"submitToPlus": {
@@ -73,79 +70,18 @@
"success": "Eksportimise käivitamine õnnestus. Faili leiad eksportimise lehelt.",
"view": "Vaata",
"error": {
"failed": "Eksportimise järjekorda lisamine ei õnnestunud: {{error}}",
"failed": "Eksportimise käivitamine ei õnnestunud: {{error}}",
"endTimeMustAfterStartTime": "Ajavahemiku lõpp peab olema peale algust",
"noVaildTimeSelected": "Ühtegi kehtivat ajavahemikku pole valitud",
"noValidTimeSelected": "Ühtegi korrektset ajavahemikku pole valitud"
},
"queued": "Eksport on järjekorda lisatud. Vaata progressi eksportide lehelt.",
"batchSuccess_one": "Alustasin 1 ekspordiga. Avan juhtumi kohe.",
"batchSuccess_other": "Alustasin {{count}} ekspordiga. Avan juhtumi kohe.",
"batchPartial": "Alustasin {{successful}}/{{total}} ekspordiga. Ebaõnnestunud kaamerad: {{failedCameras}}",
"batchFailed": "{{total}} eksporti ebaõnnestus algatada. Ebaõnnestunud kaamerad: {{failedCameras}}",
"batchQueuedSuccess_one": "1 eksport järjekorda lisatud. Avan juhtumi kohe.",
"batchQueuedSuccess_other": "{{count}} eksporti järjekorda lisatud. Avan juhtumi kohe.",
"batchQueuedPartial": "{{successful}}/{{total}} ekspordist on järjekorda lisatud. Ebaõnnestunud kaamerad: {{failedCameras}}",
"batchQueueFailed": "{{total}} eksporti ebaõnnestus järjekorda lisada. Ebaõnnestunud kaamerad: {{failedCameras}}"
"noVaildTimeSelected": "Ühtegi kehtivat ajavahemikku pole valitud"
}
},
"fromTimeline": {
"saveExport": "Salvesta eksporditud sisu",
"previewExport": "Eksporditud sisu eelvaade",
"queueingExport": "Ekspordi järjekorda lisamine...",
"useThisRange": "Kasuta seda vahemikku"
"previewExport": "Eksporditud sisu eelvaade"
},
"case": {
"label": "Juhtum",
"placeholder": "Vali juhtum",
"newCaseOption": "Ava uus juhtum",
"newCaseNamePlaceholder": "Uue juhtumi nimi",
"newCaseDescriptionPlaceholder": "Juhtumi kirjeldus",
"nonAdminHelp": "Uus juhtum avatakse järnevatele eksportidele."
},
"queueing": "Ekspordi järjekorda lisamine...",
"tabs": {
"export": "Üksik kaamera",
"multiCamera": "Mitu kaamerat"
},
"multiCamera": {
"timeRange": "Ajavahemik",
"selectFromTimeline": "Vali ajajoonelt",
"cameraSelection": "Kaamerad",
"cameraSelectionHelp": "Selles ajavahemikus tuvastatud objektidega kaamerad on eelvalitud",
"searchOrSelectGroup": "Otsi või vali kaamera grupp...",
"selectAll": "Vali kõik kaamerad",
"clearSelection": "Tühista valik",
"selectWithActivity": "Jälgitud objektidega kaamerad",
"selectGroup": "Vali grupp",
"noMatchingCameras": "Ükski kaamera ei sobitunud otsinguga",
"selectedCount": "{{selected}} / {{total}} valitud",
"checkingActivity": "Kaamera aktiivsuse kontrollimine...",
"noCameras": "Ühtegi kaamerat pole saadaval",
"detectionCount_one": "1 jälgitav objekt",
"detectionCount_other": "{{count}} jälgitavat objekti",
"nameLabel": "Ekspordi nimi",
"namePlaceholder": "Valikuline baasnimi nendele eksportidele",
"queueingButton": "Ekspordi järjekorda lisamine...",
"exportButton_one": "Ekspordi 1 kaamera",
"exportButton_other": "Ekspordi {{count}} kaamerat"
},
"multi": {
"title_one": "Ekspordi 1 ülevaade",
"title_other": "Espordi {{count}} ülevaadet",
"description": "Ekspordi valitud ülevaated. Kõik ekspordid on grupeeritud ühte juhtumisse.",
"descriptionNoCase": "Ekspordi kõik valitud juhtumid.",
"caseNamePlaceholder": "Ülevaate eksport - {{date}}",
"exportButton_one": "Ekspordi 1 ülevaade",
"exportButton_other": "Ekspordi {{count}} ülevaadet",
"exportingButton": "Ekspordin...",
"toast": {
"started_one": "Alustasin 1 eksporti. Avan juhtumi kohe.",
"started_other": "Alustasin {{count}} ekspordiga. Avan juhtumi kohe.",
"startedNoCase_one": "Alustasin 1 ekspordiga.",
"startedNoCase_other": "Alustasin {{count}} ekspordiga.",
"partial": "Alustasin {{successful}}/{{total}} ekspordiga. Ebaõnnestusid: {{failedItems}}",
"failed": "{{total}} ekspordi algatamine ebaõnnestus. Ebaõnnestunud: {{failedItems}}"
}
"placeholder": "Vali juhtum"
}
},
"streaming": {
@@ -178,14 +114,6 @@
"success": "Selle ülevaadatava objektiga seotud videosisu on kustutatud.",
"error": "Kustutamine ei õnnestunud: {{error}}"
}
},
"shareTimestamp": {
"label": "Jaotise ajatempel",
"title": "Jaotise ajatempel",
"description": "Jaga praeguse esituskoha ajatempliga URL-i või vali kohandatud ajatempel. Arvesta, et see ei ole avalik jagamislink ja on kättesaadav ainult kasutajatele, kellel on ligipääs Frigate'ile ja sellele kaamerale.",
"custom": "Kohandatud ajatempel",
"button": "Jaga ajatempliga linki",
"shareTitle": "Frigate ülevaate ajatempel: {{camera}}"
}
},
"imagePicker": {
+2 -177
View File
@@ -12,20 +12,11 @@
"description": "Kasutusel"
},
"audio": {
"label": "Heli tuvastus",
"min_volume": {
"label": "Minimaalne helitase"
},
"filters": {
"label": "Audio filtrid"
}
"label": "Helisündmused"
},
"birdseye": {
"mode": {
"label": "Jälgimisrežiim"
},
"order": {
"label": "Positsioon"
}
},
"label": "Kaameraseadistus",
@@ -34,8 +25,7 @@
"threshold": {
"description": "Minimaalne sarnasuse punktiskoor (0-1), mis on vajalik selle päästiku käivitamiseks."
}
},
"label": "Semantiline otsing"
}
},
"lpr": {
"label": "Sõidukite numbrimärkide tuvastus",
@@ -44,171 +34,6 @@
"review": {
"genai": {
"description": "Kontrollib generatiivse tehisaru kasutamist kirjelduste ja kokkuvõtete koostamiseks ülevaatamisele kuuluvate objektide jaoks."
},
"alerts": {
"enabled": {
"label": "Luba häired"
}
}
},
"audio_transcription": {
"label": "Audio transkriptsioon",
"live_enabled": {
"label": "Reaalajas transkriptsioon"
},
"enabled": {
"label": "Luba heli üleskirjutamine tekstina"
}
},
"detect": {
"label": "Objekti tuvastus",
"enabled": {
"label": "Luba objektituvastus"
},
"height": {
"label": "Tuvastamise kõrgus"
},
"width": {
"label": "Tuvastamise laius"
},
"fps": {
"label": "Tuvastamise kaadrisagedus"
},
"stationary": {
"label": "Püsivate objektide sätted"
}
},
"face_recognition": {
"label": "Näotuvastus"
},
"ffmpeg": {
"path": {
"label": "FFmpeg asukoht"
},
"gpu": {
"label": "GPU indeks"
},
"inputs": {
"global_args": {
"label": "FFmpeg globaalsed argumendid"
}
}
},
"live": {
"label": "Reaalajas mahamängimine",
"height": {
"label": "Otseülekande kõrgus"
},
"quality": {
"label": "Otseülekande kvaliteet"
},
"streams": {
"label": "Otseülekande voo nimed"
}
},
"motion": {
"label": "Liikumistuvastus",
"frame_height": {
"label": "Kaadri kõrgus"
},
"enabled": {
"label": "Luba liikumistuvastus"
}
},
"objects": {
"label": "Objektid",
"filters": {
"min_area": {
"label": "Minimaalne objekti ala"
},
"max_area": {
"label": "Maksimaalne objekti ala"
}
},
"genai": {
"label": "GenAI objekti konfiguratsioon",
"required_zones": {
"label": "Nõutud tsoonid"
},
"debug_save_thumbnails": {
"label": "Salvesta pisipildid"
},
"enabled": {
"label": "Luba GenAI"
},
"use_snapshot": {
"label": "Kasuta hetktõmmiseid"
}
}
},
"mqtt": {
"label": "MQTT"
},
"notifications": {
"enabled": {
"label": "Luba teavitused"
},
"email": {
"label": "Teavituste email"
},
"label": "Teavitused"
},
"ui": {
"label": "Kaamera kasutajaliides"
},
"record": {
"label": "Salvestus",
"enabled": {
"label": "Luba salvestamine"
}
},
"snapshots": {
"enabled": {
"label": "Luba hetktõmmised"
}
},
"timestamp_style": {
"color": {
"red": {
"label": "Punane",
"description": "Punase komponent (0255) ajatempli värvi jaoks."
},
"green": {
"label": "Roheline",
"description": "Rohelise komponent (0255) ajatempli värvi jaoks."
},
"blue": {
"label": "Sinine",
"description": "Sinise komponent (0255) ajatempli värvi jaoks."
},
"label": "Ajatempli värv",
"description": "Ajatempli teksti RGB värviväärtused (kõik väärtused 0255)."
},
"thickness": {
"label": "Ajatempli paksus",
"description": "Ajatempli teksti joone paksus."
},
"effect": {
"label": "Ajatempli efekt",
"description": "Ajatempli teksti visuaalne efekt (puudub, ühtlane, vari)."
}
},
"onvif": {
"user": {
"label": "ONVIF kasutajanimi"
},
"password": {
"label": "ONVIF parool"
},
"port": {
"label": "ONVIF port"
},
"label": "ONVIF",
"host": {
"label": "ONVIF host"
}
},
"profiles": {
"label": "Profiilid"
}
}

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