mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-03 09:38:15 +03:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
130dc76a01 | ||
|
|
87e67d22e2 | ||
|
|
f25f1f689b | ||
|
|
7450e0a276 | ||
|
|
49be4a53cd | ||
|
|
b71223b79c | ||
|
|
a07d392678 | ||
|
|
a75c2d2abb | ||
|
|
f6a5e9819e | ||
|
|
f2ce5572bd | ||
|
|
e473117705 |
@@ -22,7 +22,6 @@ autotrack
|
|||||||
autotracked
|
autotracked
|
||||||
autotracker
|
autotracker
|
||||||
autotracking
|
autotracking
|
||||||
backchannel
|
|
||||||
balena
|
balena
|
||||||
Beelink
|
Beelink
|
||||||
BGRA
|
BGRA
|
||||||
@@ -317,4 +316,4 @@ yolo
|
|||||||
yolonas
|
yolonas
|
||||||
yolox
|
yolox
|
||||||
zeep
|
zeep
|
||||||
zerolatency
|
zerolatency
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
title: "[Beta Support]: "
|
|
||||||
labels: ["support", "triage", "beta"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thank you for testing Frigate beta versions! Use this form for support with beta releases.
|
|
||||||
|
|
||||||
**Note:** Beta versions may have incomplete features, known issues, or unexpected behavior. Please check the [release notes](https://github.com/blakeblackshear/frigate/releases) and [recent discussions][discussions] for known beta issues before submitting.
|
|
||||||
|
|
||||||
Before submitting, read the [beta documentation][docs].
|
|
||||||
|
|
||||||
[docs]: https://deploy-preview-19787--frigate-docs.netlify.app/
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: Describe the problem you are having
|
|
||||||
description: Please be as detailed as possible. Include what you expected to happen vs what actually happened.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: Beta Version
|
|
||||||
description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.17.0-beta1)
|
|
||||||
placeholder: "0.17.0-beta1"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: issue-category
|
|
||||||
attributes:
|
|
||||||
label: Issue Category
|
|
||||||
description: What area is your issue related to? This helps us understand the context.
|
|
||||||
options:
|
|
||||||
- Object Detection / Detectors
|
|
||||||
- Hardware Acceleration
|
|
||||||
- Configuration / Setup
|
|
||||||
- WebUI / Frontend
|
|
||||||
- Recordings / Storage
|
|
||||||
- Notifications / Events
|
|
||||||
- Integration (Home Assistant, etc)
|
|
||||||
- Performance / Stability
|
|
||||||
- Installation / Updates
|
|
||||||
- Other
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: config
|
|
||||||
attributes:
|
|
||||||
label: Frigate config file
|
|
||||||
description: This will be automatically formatted into code, so no need for backticks. Remove any sensitive information like passwords or URLs.
|
|
||||||
render: yaml
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: frigatelogs
|
|
||||||
attributes:
|
|
||||||
label: Relevant Frigate log output
|
|
||||||
description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks.
|
|
||||||
render: shell
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: go2rtclogs
|
|
||||||
attributes:
|
|
||||||
label: Relevant go2rtc log output (if applicable)
|
|
||||||
description: If your issue involves cameras, streams, or playback, please include go2rtc logs. Logs can be viewed via the Frigate UI, Docker, or the go2rtc dashboard. This will be automatically formatted into code, so no need for backticks.
|
|
||||||
render: shell
|
|
||||||
- type: dropdown
|
|
||||||
id: install-method
|
|
||||||
attributes:
|
|
||||||
label: Install method
|
|
||||||
options:
|
|
||||||
- Home Assistant Add-on
|
|
||||||
- Docker Compose
|
|
||||||
- Docker CLI
|
|
||||||
- Proxmox via Docker
|
|
||||||
- Proxmox via TTeck Script
|
|
||||||
- Windows WSL2
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: docker
|
|
||||||
attributes:
|
|
||||||
label: docker-compose file or Docker CLI command
|
|
||||||
description: This will be automatically formatted into code, so no need for backticks. Include relevant environment variables and device mappings.
|
|
||||||
render: yaml
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: os
|
|
||||||
attributes:
|
|
||||||
label: Operating system
|
|
||||||
options:
|
|
||||||
- Home Assistant OS
|
|
||||||
- Debian
|
|
||||||
- Ubuntu
|
|
||||||
- Other Linux
|
|
||||||
- Proxmox
|
|
||||||
- UNRAID
|
|
||||||
- Windows
|
|
||||||
- Other
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: hardware
|
|
||||||
attributes:
|
|
||||||
label: CPU / GPU / Hardware
|
|
||||||
description: Provide details about your hardware (e.g., Intel i5-9400, NVIDIA RTX 3060, Raspberry Pi 4, etc)
|
|
||||||
placeholder: "Intel i7-10700, NVIDIA GTX 1660"
|
|
||||||
- type: textarea
|
|
||||||
id: screenshots
|
|
||||||
attributes:
|
|
||||||
label: Screenshots
|
|
||||||
description: Screenshots of the issue, System metrics pages, or any relevant UI. Drag and drop or paste images directly.
|
|
||||||
- type: textarea
|
|
||||||
id: steps-to-reproduce
|
|
||||||
attributes:
|
|
||||||
label: Steps to reproduce
|
|
||||||
description: If applicable, provide detailed steps to reproduce the issue
|
|
||||||
placeholder: |
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '...'
|
|
||||||
3. See error
|
|
||||||
- type: textarea
|
|
||||||
id: other
|
|
||||||
attributes:
|
|
||||||
label: Any other information that may be helpful
|
|
||||||
description: Additional context, related issues, when the problem started appearing, etc.
|
|
||||||
@@ -6,8 +6,6 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
Use this form to submit a reproducible bug in Frigate or Frigate's UI.
|
Use this form to submit a reproducible bug in Frigate or Frigate's UI.
|
||||||
|
|
||||||
**⚠️ If you are running a beta version (0.17.0-beta or similar), please use the [Beta Support template](https://github.com/blakeblackshear/frigate/discussions/new?category=beta-support) instead.**
|
|
||||||
|
|
||||||
Before submitting your bug report, please ask the AI with the "Ask AI" button on the [official documentation site][ai] about your issue, [search the discussions][discussions], look at recent open and closed [pull requests][prs], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your bug has already been fixed by the developers or reported by the community.
|
Before submitting your bug report, please ask the AI with the "Ask AI" button on the [official documentation site][ai] about your issue, [search the discussions][discussions], look at recent open and closed [pull requests][prs], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your bug has already been fixed by the developers or reported by the community.
|
||||||
|
|
||||||
**If you are unsure if your issue is actually a bug or not, please submit a support request first.**
|
**If you are unsure if your issue is actually a bug or not, please submit a support request first.**
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
Never write strings in the frontend directly, always write to and reference the relevant translations file.
|
|
||||||
Always conform new and refactored code to the existing coding style in the project.
|
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
name: AMD64 Build
|
name: AMD64 Build
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU and Buildx
|
- name: Set up QEMU and Buildx
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
name: ARM Build
|
name: ARM Build
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU and Buildx
|
- name: Set up QEMU and Buildx
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
name: Jetson Jetpack 6
|
name: Jetson Jetpack 6
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU and Buildx
|
- name: Set up QEMU and Buildx
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
- amd64_build
|
- amd64_build
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU and Buildx
|
- name: Set up QEMU and Buildx
|
||||||
@@ -154,7 +154,7 @@ jobs:
|
|||||||
- arm64_build
|
- arm64_build
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU and Buildx
|
- name: Set up QEMU and Buildx
|
||||||
@@ -179,7 +179,7 @@ jobs:
|
|||||||
- arm64_build
|
- arm64_build
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU and Buildx
|
- name: Set up QEMU and Buildx
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
name: Web - Lint
|
name: Web - Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@master
|
- uses: actions/setup-node@master
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
name: Web - Test
|
name: Web - Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@master
|
- uses: actions/setup-node@master
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
name: Python Checks
|
name: Python Checks
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repository
|
- name: Check out the repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
@@ -75,7 +75,7 @@ jobs:
|
|||||||
name: Python Tests
|
name: Python Tests
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@master
|
- uses: actions/setup-node@master
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- id: lowercaseRepo
|
- id: lowercaseRepo
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ onvif-zeep-async == 4.0.*
|
|||||||
paho-mqtt == 2.1.*
|
paho-mqtt == 2.1.*
|
||||||
pandas == 2.2.*
|
pandas == 2.2.*
|
||||||
peewee == 3.17.*
|
peewee == 3.17.*
|
||||||
peewee_migrate == 1.14.*
|
peewee_migrate == 1.13.*
|
||||||
psutil == 7.1.*
|
psutil == 7.1.*
|
||||||
pydantic == 2.10.*
|
pydantic == 2.10.*
|
||||||
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
||||||
|
|||||||
@@ -116,4 +116,4 @@ Along with individual review item summaries, Generative AI provides the ability
|
|||||||
|
|
||||||
Review reports can be requested via the [API](/integrations/api#review-summarization) by sending a POST request to `/api/review/summarize/start/{start_ts}/end/{end_ts}` with Unix timestamps.
|
Review reports can be requested via the [API](/integrations/api#review-summarization) by sending a POST request to `/api/review/summarize/start/{start_ts}/end/{end_ts}` with Unix timestamps.
|
||||||
|
|
||||||
For Home Assistant users, there is a built-in service (`frigate.review_summarize`) that makes it easy to request review reports as part of automations or scripts. This allows you to automatically generate daily summaries, vacation reports, or custom time period reports based on your specific needs.
|
For Home Assistant users, there is a built-in service (`frigate.generate_review_summary`) 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.
|
||||||
|
|||||||
@@ -107,23 +107,23 @@ Fine-tune the LPR feature using these optional parameters at the global level of
|
|||||||
|
|
||||||
### Normalization Rules
|
### Normalization Rules
|
||||||
|
|
||||||
- **`replace_rules`**: List of regex replacement rules to normalize detected plates. These rules are applied sequentially and are applied _before_ the `format` regex, if specified. Each rule must have a `pattern` (which can be a string or a regex) and `replacement` (a string, which also supports [backrefs](https://docs.python.org/3/library/re.html#re.sub) like `\1`). These rules are useful for dealing with common OCR issues like noise characters, separators, or confusions (e.g., 'O'→'0').
|
- **`replace_rules`**: List of regex replacement rules to normalize detected plates. These rules are applied sequentially and are applied _before_ the `format` regex, if specified. Each rule must have a `pattern` (which can be a string or a regex, prepended by `r`) and `replacement` (a string, which also supports [backrefs](https://docs.python.org/3/library/re.html#re.sub) like `\1`). These rules are useful for dealing with common OCR issues like noise characters, separators, or confusions (e.g., 'O'→'0').
|
||||||
|
|
||||||
These rules must be defined at the global level of your `lpr` config.
|
These rules must be defined at the global level of your `lpr` config.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
lpr:
|
lpr:
|
||||||
replace_rules:
|
replace_rules:
|
||||||
- pattern: "[%#*?]" # Remove noise symbols
|
- pattern: r'[%#*?]' # Remove noise symbols
|
||||||
replacement: ""
|
replacement: ""
|
||||||
- pattern: "[= ]" # Normalize = or space to dash
|
- pattern: r'[= ]' # Normalize = or space to dash
|
||||||
replacement: "-"
|
replacement: "-"
|
||||||
- pattern: "O" # Swap 'O' to '0' (common OCR error)
|
- pattern: "O" # Swap 'O' to '0' (common OCR error)
|
||||||
replacement: "0"
|
replacement: "0"
|
||||||
- pattern: "I" # Swap 'I' to '1'
|
- pattern: r'I' # Swap 'I' to '1'
|
||||||
replacement: "1"
|
replacement: "1"
|
||||||
- pattern: '(\w{3})(\w{3})' # Split 6 chars into groups (e.g., ABC123 → ABC-123) - use single quotes to preserve backslashes
|
- pattern: r'(\w{3})(\w{3})' # Split 6 chars into groups (e.g., ABC123 → ABC-123)
|
||||||
replacement: '\1-\2'
|
replacement: r'\1-\2'
|
||||||
```
|
```
|
||||||
|
|
||||||
- Rules fire in order: In the example above: clean noise first, then separators, then swaps, then splits.
|
- Rules fire in order: In the example above: clean noise first, then separators, then swaps, then splits.
|
||||||
@@ -374,19 +374,9 @@ Use `match_distance` to allow small character mismatches. Alternatively, define
|
|||||||
|
|
||||||
Start with ["Why isn't my license plate being detected and recognized?"](#why-isnt-my-license-plate-being-detected-and-recognized). If you are still having issues, work through these steps.
|
Start with ["Why isn't my license plate being detected and recognized?"](#why-isnt-my-license-plate-being-detected-and-recognized). If you are still having issues, work through these steps.
|
||||||
|
|
||||||
1. Start with a simplified LPR config.
|
1. Enable debug logs to see exactly what Frigate is doing.
|
||||||
|
|
||||||
- Remove or comment out everything in your LPR config, including `min_area`, `min_plate_length`, `format`, `known_plates`, or `enhancement` values so that the only values left are `enabled` and `debug_save_plates`. This will run LPR with Frigate's default values.
|
- Enable debug logs for LPR by adding `frigate.data_processing.common.license_plate: debug` to your `logger` configuration. These logs are _very_ verbose, so only keep this enabled when necessary.
|
||||||
|
|
||||||
```yaml
|
|
||||||
lpr:
|
|
||||||
enabled: true
|
|
||||||
debug_save_plates: true
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Enable debug logs to see exactly what Frigate is doing.
|
|
||||||
|
|
||||||
- Enable debug logs for LPR by adding `frigate.data_processing.common.license_plate: debug` to your `logger` configuration. These logs are _very_ verbose, so only keep this enabled when necessary. Restart Frigate after this change.
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
logger:
|
logger:
|
||||||
@@ -395,7 +385,7 @@ Start with ["Why isn't my license plate being detected and recognized?"](#why-is
|
|||||||
frigate.data_processing.common.license_plate: debug
|
frigate.data_processing.common.license_plate: debug
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Ensure your plates are being _detected_.
|
2. Ensure your plates are being _detected_.
|
||||||
|
|
||||||
If you are using a Frigate+ or `license_plate` detecting model:
|
If you are using a Frigate+ or `license_plate` detecting model:
|
||||||
|
|
||||||
@@ -408,7 +398,7 @@ Start with ["Why isn't my license plate being detected and recognized?"](#why-is
|
|||||||
- Watch the debug logs for messages from the YOLOv9 plate detector.
|
- Watch the debug logs for messages from the YOLOv9 plate detector.
|
||||||
- You may need to adjust your `detection_threshold` if your plates are not being detected.
|
- You may need to adjust your `detection_threshold` if your plates are not being detected.
|
||||||
|
|
||||||
4. Ensure the characters on detected plates are being _recognized_.
|
3. Ensure the characters on detected plates are being _recognized_.
|
||||||
|
|
||||||
- Enable `debug_save_plates` to save images of detected text on plates to the clips directory (`/media/frigate/clips/lpr`). Ensure these images are readable and the text is clear.
|
- Enable `debug_save_plates` to save images of detected text on plates to the clips directory (`/media/frigate/clips/lpr`). Ensure these images are readable and the text is clear.
|
||||||
- Watch the debug view to see plates recognized in real-time. For non-dedicated LPR cameras, the `car` or `motorcycle` label will change to the recognized plate when LPR is enabled and working.
|
- Watch the debug view to see plates recognized in real-time. For non-dedicated LPR cameras, the `car` or `motorcycle` label will change to the recognized plate when LPR is enabled and working.
|
||||||
|
|||||||
@@ -178,8 +178,6 @@ To use the Reolink Doorbell with two way talk, you should use the [recommended R
|
|||||||
|
|
||||||
As a starting point to check compatibility for your camera, view the list of cameras supported for two-way talk on the [go2rtc repository](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#two-way-audio). For cameras in the category `ONVIF Profile T`, you can use the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/)'s FeatureList to check for the presence of `AudioOutput`. A camera that supports `ONVIF Profile T` _usually_ supports this, but due to inconsistent support, a camera that explicitly lists this feature may still not work. If no entry for your camera exists on the database, it is recommended not to buy it or to consult with the manufacturer's support on the feature availability.
|
As a starting point to check compatibility for your camera, view the list of cameras supported for two-way talk on the [go2rtc repository](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#two-way-audio). For cameras in the category `ONVIF Profile T`, you can use the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/)'s FeatureList to check for the presence of `AudioOutput`. A camera that supports `ONVIF Profile T` _usually_ supports this, but due to inconsistent support, a camera that explicitly lists this feature may still not work. If no entry for your camera exists on the database, it is recommended not to buy it or to consult with the manufacturer's support on the feature availability.
|
||||||
|
|
||||||
To prevent go2rtc from blocking other applications from accessing your camera's two-way audio, you must configure your stream with `#backchannel=0`. See [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream) in the restream documentation.
|
|
||||||
|
|
||||||
### Streaming options on camera group dashboards
|
### Streaming options on camera group dashboards
|
||||||
|
|
||||||
Frigate provides a dialog in the Camera Group Edit pane with several options for streaming on a camera group's dashboard. These settings are _per device_ and are saved in your device's local storage.
|
Frigate provides a dialog in the Camera Group Edit pane with several options for streaming on a camera group's dashboard. These settings are _per device_ and are saved in your device's local storage.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ To create a poly mask:
|
|||||||
5. Click the plus icon under the type of mask or zone you would like to create
|
5. Click the plus icon under the type of mask or zone you would like to create
|
||||||
6. Click on the camera's latest image to create the points for a masked area. Click the first point again to close the polygon.
|
6. Click on the camera's latest image to create the points for a masked area. Click the first point again to close the polygon.
|
||||||
7. When you've finished creating your mask, press Save.
|
7. When you've finished creating your mask, press Save.
|
||||||
|
8. Restart Frigate to apply your changes.
|
||||||
|
|
||||||
Your config file will be updated with the relative coordinates of the mask/zone:
|
Your config file will be updated with the relative coordinates of the mask/zone:
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,8 @@ detectors:
|
|||||||
|
|
||||||
A TensorFlow Lite model is provided in the container at `/edgetpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`.
|
A TensorFlow Lite model is provided in the container at `/edgetpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`.
|
||||||
|
|
||||||
|
A Tensorflow Lite is provided in the container at `/openvino-model/ssdlite_mobilenet_v2.xml` and is used by this detector type by default. The model comes from Intel's Open Model Zoo [SSDLite MobileNet V2](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/ssdlite_mobilenet_v2) and is converted to an INT8 precision model.
|
||||||
|
|
||||||
#### YOLO v9
|
#### YOLO v9
|
||||||
|
|
||||||
[YOLOv9](https://github.com/dbro/frigate-detector-edgetpu-yolo9/releases/download/v1.0/yolov9-s-relu6-best_320_int8_edgetpu.tflite) models that are compiled for Tensorflow Lite and properly quantized are supported, but not included by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`. Note that the model may require a custom label file (eg. [use this 17 label file](https://raw.githubusercontent.com/dbro/frigate-detector-edgetpu-yolo9/refs/heads/main/labels-coco17.txt) for the model linked above.)
|
[YOLOv9](https://github.com/dbro/frigate-detector-edgetpu-yolo9/releases/download/v1.0/yolov9-s-relu6-best_320_int8_edgetpu.tflite) models that are compiled for Tensorflow Lite and properly quantized are supported, but not included by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`. Note that the model may require a custom label file (eg. [use this 17 label file](https://raw.githubusercontent.com/dbro/frigate-detector-edgetpu-yolo9/refs/heads/main/labels-coco17.txt) for the model linked above.)
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ auth:
|
|||||||
# Optional: Refresh time in seconds (default: shown below)
|
# Optional: Refresh time in seconds (default: shown below)
|
||||||
# When the session is going to expire in less time than this setting,
|
# When the session is going to expire in less time than this setting,
|
||||||
# it will be refreshed back to the session_length.
|
# it will be refreshed back to the session_length.
|
||||||
refresh_time: 1800 # 30 minutes
|
refresh_time: 43200 # 12 hours
|
||||||
# Optional: Rate limiting for login failures to help prevent brute force
|
# Optional: Rate limiting for login failures to help prevent brute force
|
||||||
# login attacks (default: shown below)
|
# login attacks (default: shown below)
|
||||||
# See the docs for more information on valid values
|
# See the docs for more information on valid values
|
||||||
@@ -911,7 +911,7 @@ cameras:
|
|||||||
user: admin
|
user: admin
|
||||||
# Optional: password for login.
|
# Optional: password for login.
|
||||||
password: admin
|
password: admin
|
||||||
# Optional: Skip TLS verification and disable digest authentication for the ONVIF server (default: shown below)
|
# Optional: Skip TLS verification from the ONVIF server (default: shown below)
|
||||||
tls_insecure: False
|
tls_insecure: False
|
||||||
# Optional: Ignores time synchronization mismatches between the camera and the server during authentication.
|
# Optional: Ignores time synchronization mismatches between the camera and the server during authentication.
|
||||||
# Using NTP on both ends is recommended and this should only be set to True in a "safe" environment due to the security risk it represents.
|
# Using NTP on both ends is recommended and this should only be set to True in a "safe" environment due to the security risk it represents.
|
||||||
@@ -1002,6 +1002,10 @@ ui:
|
|||||||
# full: 8:15:22 PM Mountain Standard Time
|
# full: 8:15:22 PM Mountain Standard Time
|
||||||
# (default: shown below).
|
# (default: shown below).
|
||||||
time_style: medium
|
time_style: medium
|
||||||
|
# Optional: Ability to manually override the date / time styling to use strftime format
|
||||||
|
# https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html
|
||||||
|
# possible values are shown above (default: not set)
|
||||||
|
strftime_fmt: "%Y/%m/%d %H:%M"
|
||||||
# Optional: Set the unit system to either "imperial" or "metric" (default: metric)
|
# Optional: Set the unit system to either "imperial" or "metric" (default: metric)
|
||||||
# Used in the UI and in MQTT topics
|
# Used in the UI and in MQTT topics
|
||||||
unit_system: metric
|
unit_system: metric
|
||||||
|
|||||||
@@ -24,12 +24,11 @@ birdseye:
|
|||||||
restream: True
|
restream: True
|
||||||
```
|
```
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
|
|
||||||
To improve connection speed when using Birdseye via restream you can enable a small idle heartbeat by setting `birdseye.idle_heartbeat_fps` to a low value (e.g. `1–2`). This makes Frigate periodically push the last frame even when no motion is detected, reducing initial connection latency.
|
To improve connection speed when using Birdseye via restream you can enable a small idle heartbeat by setting `birdseye.idle_heartbeat_fps` to a low value (e.g. `1–2`). This makes Frigate periodically push the last frame even when no motion is detected, reducing initial connection latency.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Securing Restream With Authentication
|
### Securing Restream With Authentication
|
||||||
|
|
||||||
The go2rtc restream can be secured with RTSP based username / password authentication. Ex:
|
The go2rtc restream can be secured with RTSP based username / password authentication. Ex:
|
||||||
@@ -160,31 +159,6 @@ go2rtc:
|
|||||||
|
|
||||||
See [this comment](https://github.com/AlexxIT/go2rtc/issues/1217#issuecomment-2242296489) for more information.
|
See [this comment](https://github.com/AlexxIT/go2rtc/issues/1217#issuecomment-2242296489) for more information.
|
||||||
|
|
||||||
## Preventing go2rtc from blocking two-way audio {#two-way-talk-restream}
|
|
||||||
|
|
||||||
For cameras that support two-way talk, go2rtc will automatically establish an audio output backchannel when connecting to an RTSP stream. This backchannel blocks access to the camera's audio output for two-way talk functionality, preventing both Frigate and other applications from using it.
|
|
||||||
|
|
||||||
To prevent this, you must configure two separate stream instances:
|
|
||||||
|
|
||||||
1. One stream instance with `#backchannel=0` for Frigate's viewing, recording, and detection (prevents go2rtc from establishing the blocking backchannel)
|
|
||||||
2. A second stream instance without `#backchannel=0` for two-way talk functionality (can be used by Frigate's WebRTC viewer or other applications)
|
|
||||||
|
|
||||||
Configuration example:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
go2rtc:
|
|
||||||
streams:
|
|
||||||
front_door:
|
|
||||||
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2#backchannel=0
|
|
||||||
front_door_twoway:
|
|
||||||
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
|
||||||
```
|
|
||||||
|
|
||||||
In this configuration:
|
|
||||||
|
|
||||||
- `front_door` stream is used by Frigate for viewing, recording, and detection. The `#backchannel=0` parameter prevents go2rtc from establishing the audio output backchannel, so it won't block two-way talk access.
|
|
||||||
- `front_door_twoway` stream is used for two-way talk functionality. This stream can be used by Frigate's WebRTC viewer when two-way talk is enabled, or by other applications (like Home Assistant Advanced Camera Card) that need access to the camera's audio output channel.
|
|
||||||
|
|
||||||
## Advanced Restream Configurations
|
## Advanced Restream Configurations
|
||||||
|
|
||||||
The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below:
|
The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below:
|
||||||
@@ -195,4 +169,4 @@ NOTE: The output will need to be passed with two curly braces `{{output}}`
|
|||||||
go2rtc:
|
go2rtc:
|
||||||
streams:
|
streams:
|
||||||
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {{output}}
|
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {{output}}
|
||||||
```
|
```
|
||||||
@@ -135,7 +135,6 @@ Finally, configure [hardware object detection](/configuration/object_detectors#h
|
|||||||
### MemryX MX3
|
### MemryX MX3
|
||||||
|
|
||||||
The MemryX MX3 Accelerator is available in the M.2 2280 form factor (like an NVMe SSD), and supports a variety of configurations:
|
The MemryX MX3 Accelerator is available in the M.2 2280 form factor (like an NVMe SSD), and supports a variety of configurations:
|
||||||
|
|
||||||
- x86 (Intel/AMD) PCs
|
- x86 (Intel/AMD) PCs
|
||||||
- Raspberry Pi 5
|
- Raspberry Pi 5
|
||||||
- Orange Pi 5 Plus/Max
|
- Orange Pi 5 Plus/Max
|
||||||
@@ -143,6 +142,7 @@ The MemryX MX3 Accelerator is available in the M.2 2280 form factor (like an NVM
|
|||||||
|
|
||||||
#### Configuration
|
#### Configuration
|
||||||
|
|
||||||
|
|
||||||
#### Installation
|
#### Installation
|
||||||
|
|
||||||
To get started with MX3 hardware setup for your system, refer to the [Hardware Setup Guide](https://developer.memryx.com/get_started/hardware_setup.html).
|
To get started with MX3 hardware setup for your system, refer to the [Hardware Setup Guide](https://developer.memryx.com/get_started/hardware_setup.html).
|
||||||
@@ -156,7 +156,7 @@ Then follow these steps for installing the correct driver/runtime configuration:
|
|||||||
|
|
||||||
#### Setup
|
#### Setup
|
||||||
|
|
||||||
To set up Frigate, follow the default installation instructions, for example: `ghcr.io/blakeblackshear/frigate:stable`
|
To set up Frigate, follow the default installation instructions, for example: `ghcr.io/blakeblackshear/frigate:stable`
|
||||||
|
|
||||||
Next, grant Docker permissions to access your hardware by adding the following lines to your `docker-compose.yml` file:
|
Next, grant Docker permissions to access your hardware by adding the following lines to your `docker-compose.yml` file:
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ In your `docker-compose.yml`, also add:
|
|||||||
privileged: true
|
privileged: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- /run/mxa_manager:/run/mxa_manager
|
/run/mxa_manager:/run/mxa_manager
|
||||||
```
|
```
|
||||||
|
|
||||||
If you can't use Docker Compose, you can run the container with something similar to this:
|
If you can't use Docker Compose, you can run the container with something similar to this:
|
||||||
@@ -411,7 +411,7 @@ To install make sure you have the [community app plugin here](https://forums.unr
|
|||||||
|
|
||||||
## Proxmox
|
## Proxmox
|
||||||
|
|
||||||
[According to Proxmox documentation](https://pve.proxmox.com/pve-docs/pve-admin-guide.html#chapter_pct) it is recommended that you run application containers like Frigate inside a Proxmox QEMU VM. This will give you all the advantages of application containerization, while also providing the benefits that VMs offer, such as strong isolation from the host and the ability to live-migrate, which otherwise isn’t possible with containers. Ensure that ballooning is **disabled**, especially if you are passing through a GPU to the VM.
|
[According to Proxmox documentation](https://pve.proxmox.com/pve-docs/pve-admin-guide.html#chapter_pct) it is recommended that you run application containers like Frigate inside a Proxmox QEMU VM. This will give you all the advantages of application containerization, while also providing the benefits that VMs offer, such as strong isolation from the host and the ability to live-migrate, which otherwise isn’t possible with containers.
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
|
|
||||||
|
|||||||
@@ -113,8 +113,7 @@ section.
|
|||||||
|
|
||||||
1. If the stream you added to go2rtc is also used by Frigate for the `record` or `detect` role, you can migrate your config to pull from the RTSP restream to reduce the number of connections to your camera as shown [here](/configuration/restream#reduce-connections-to-camera).
|
1. If the stream you added to go2rtc is also used by Frigate for the `record` or `detect` role, you can migrate your config to pull from the RTSP restream to reduce the number of connections to your camera as shown [here](/configuration/restream#reduce-connections-to-camera).
|
||||||
2. You can [set up WebRTC](/configuration/live#webrtc-extra-configuration) if your camera supports two-way talk. Note that WebRTC only supports specific audio formats and may require opening ports on your router.
|
2. You can [set up WebRTC](/configuration/live#webrtc-extra-configuration) if your camera supports two-way talk. Note that WebRTC only supports specific audio formats and may require opening ports on your router.
|
||||||
3. If your camera supports two-way talk, you must configure your stream with `#backchannel=0` to prevent go2rtc from blocking other applications from accessing the camera's audio output. See [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream) in the restream documentation.
|
|
||||||
|
|
||||||
## Homekit Configuration
|
## Homekit Configuration
|
||||||
|
|
||||||
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 share export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`.
|
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 share export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`.
|
||||||
Generated
+1409
-1790
File diff suppressed because it is too large
Load Diff
+7
-7
@@ -18,14 +18,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "^3.7.0",
|
"@docusaurus/core": "^3.7.0",
|
||||||
"@docusaurus/plugin-content-docs": "^3.7.0",
|
"@docusaurus/plugin-content-docs": "^3.6.3",
|
||||||
"@docusaurus/preset-classic": "^3.7.0",
|
"@docusaurus/preset-classic": "^3.7.0",
|
||||||
"@docusaurus/theme-mermaid": "^3.7.0",
|
"@docusaurus/theme-mermaid": "^3.6.3",
|
||||||
"@inkeep/docusaurus": "^2.0.16",
|
"@inkeep/docusaurus": "^2.0.16",
|
||||||
"@mdx-js/react": "^3.1.0",
|
"@mdx-js/react": "^3.1.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"docusaurus-plugin-openapi-docs": "^4.5.1",
|
"docusaurus-plugin-openapi-docs": "^4.3.1",
|
||||||
"docusaurus-theme-openapi-docs": "^4.5.1",
|
"docusaurus-theme-openapi-docs": "^4.3.1",
|
||||||
"prism-react-renderer": "^2.4.1",
|
"prism-react-renderer": "^2.4.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -44,9 +44,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "^3.7.0",
|
"@docusaurus/module-type-aliases": "^3.4.0",
|
||||||
"@docusaurus/types": "^3.7.0",
|
"@docusaurus/types": "^3.4.0",
|
||||||
"@types/react": "^18.3.27"
|
"@types/react": "^18.3.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0"
|
"node": ">=18.0"
|
||||||
|
|||||||
+9
-105
@@ -55,8 +55,8 @@ def require_admin_by_default():
|
|||||||
"/auth",
|
"/auth",
|
||||||
"/auth/first_time_login",
|
"/auth/first_time_login",
|
||||||
"/login",
|
"/login",
|
||||||
"/logout",
|
|
||||||
# Authenticated user endpoints (allow_any_authenticated)
|
# Authenticated user endpoints (allow_any_authenticated)
|
||||||
|
"/logout",
|
||||||
"/profile",
|
"/profile",
|
||||||
# Public info endpoints (allow_public)
|
# Public info endpoints (allow_public)
|
||||||
"/",
|
"/",
|
||||||
@@ -311,10 +311,7 @@ def get_jwt_secret() -> str:
|
|||||||
)
|
)
|
||||||
jwt_secret = secrets.token_hex(64)
|
jwt_secret = secrets.token_hex(64)
|
||||||
try:
|
try:
|
||||||
fd = os.open(
|
with open(jwt_secret_file, "w") as f:
|
||||||
jwt_secret_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600
|
|
||||||
)
|
|
||||||
with os.fdopen(fd, "w") as f:
|
|
||||||
f.write(str(jwt_secret))
|
f.write(str(jwt_secret))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -359,35 +356,9 @@ def verify_password(password, password_hash):
|
|||||||
return secrets.compare_digest(password_hash, compare_hash)
|
return secrets.compare_digest(password_hash, compare_hash)
|
||||||
|
|
||||||
|
|
||||||
def validate_password_strength(password: str) -> tuple[bool, Optional[str]]:
|
|
||||||
"""
|
|
||||||
Validate password strength.
|
|
||||||
|
|
||||||
Returns a tuple of (is_valid, error_message).
|
|
||||||
"""
|
|
||||||
if not password:
|
|
||||||
return False, "Password cannot be empty"
|
|
||||||
|
|
||||||
if len(password) < 8:
|
|
||||||
return False, "Password must be at least 8 characters long"
|
|
||||||
|
|
||||||
if not any(c.isupper() for c in password):
|
|
||||||
return False, "Password must contain at least one uppercase letter"
|
|
||||||
|
|
||||||
if not any(c.isdigit() for c in password):
|
|
||||||
return False, "Password must contain at least one digit"
|
|
||||||
|
|
||||||
if not any(c in '!@#$%^&*(),.?":{}|<>' for c in password):
|
|
||||||
return False, "Password must contain at least one special character"
|
|
||||||
|
|
||||||
return True, None
|
|
||||||
|
|
||||||
|
|
||||||
def create_encoded_jwt(user, role, expiration, secret):
|
def create_encoded_jwt(user, role, expiration, secret):
|
||||||
return jwt.encode(
|
return jwt.encode(
|
||||||
{"alg": "HS256"},
|
{"alg": "HS256"}, {"sub": user, "role": role, "exp": expiration}, secret
|
||||||
{"sub": user, "role": role, "exp": expiration, "iat": int(time.time())},
|
|
||||||
secret,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -648,27 +619,13 @@ def auth(request: Request):
|
|||||||
return fail_response
|
return fail_response
|
||||||
|
|
||||||
# if the jwt cookie is expiring soon
|
# if the jwt cookie is expiring soon
|
||||||
if jwt_source == "cookie" and expiration - JWT_REFRESH <= current_time:
|
elif jwt_source == "cookie" and expiration - JWT_REFRESH <= current_time:
|
||||||
logger.debug("jwt token expiring soon, refreshing cookie")
|
logger.debug("jwt token expiring soon, refreshing cookie")
|
||||||
|
# ensure the user hasn't been deleted
|
||||||
# Check if password has been changed since token was issued
|
|
||||||
# If so, force re-login by rejecting the refresh
|
|
||||||
try:
|
try:
|
||||||
user_obj = User.get_by_id(user)
|
User.get_by_id(user)
|
||||||
if user_obj.password_changed_at is not None:
|
|
||||||
token_iat = int(token.claims.get("iat", 0))
|
|
||||||
password_changed_timestamp = int(
|
|
||||||
user_obj.password_changed_at.timestamp()
|
|
||||||
)
|
|
||||||
if token_iat < password_changed_timestamp:
|
|
||||||
logger.debug(
|
|
||||||
"jwt token issued before password change, rejecting refresh"
|
|
||||||
)
|
|
||||||
return fail_response
|
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
logger.debug("user not found")
|
|
||||||
return fail_response
|
return fail_response
|
||||||
|
|
||||||
new_expiration = current_time + JWT_SESSION_LENGTH
|
new_expiration = current_time + JWT_SESSION_LENGTH
|
||||||
new_encoded_jwt = create_encoded_jwt(
|
new_encoded_jwt = create_encoded_jwt(
|
||||||
user, role, new_expiration, request.app.jwt_token
|
user, role, new_expiration, request.app.jwt_token
|
||||||
@@ -703,7 +660,7 @@ def profile(request: Request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/logout", dependencies=[Depends(allow_public())])
|
@router.get("/logout", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def logout(request: Request):
|
def logout(request: Request):
|
||||||
auth_config: AuthConfig = request.app.frigate_config.auth
|
auth_config: AuthConfig = request.app.frigate_config.auth
|
||||||
response = RedirectResponse("/login", status_code=303)
|
response = RedirectResponse("/login", status_code=303)
|
||||||
@@ -825,63 +782,10 @@ async def update_password(
|
|||||||
|
|
||||||
HASH_ITERATIONS = request.app.frigate_config.auth.hash_iterations
|
HASH_ITERATIONS = request.app.frigate_config.auth.hash_iterations
|
||||||
|
|
||||||
try:
|
|
||||||
user = User.get_by_id(username)
|
|
||||||
except DoesNotExist:
|
|
||||||
return JSONResponse(content={"message": "User not found"}, status_code=404)
|
|
||||||
|
|
||||||
# Require old_password when:
|
|
||||||
# 1. Non-admin user is changing another user's password (admin only action)
|
|
||||||
# 2. Any user is changing their own password
|
|
||||||
is_changing_own_password = current_username == username
|
|
||||||
is_non_admin = current_role != "admin"
|
|
||||||
|
|
||||||
if is_changing_own_password or is_non_admin:
|
|
||||||
if not body.old_password:
|
|
||||||
return JSONResponse(
|
|
||||||
content={"message": "Current password is required"},
|
|
||||||
status_code=400,
|
|
||||||
)
|
|
||||||
if not verify_password(body.old_password, user.password_hash):
|
|
||||||
return JSONResponse(
|
|
||||||
content={"message": "Current password is incorrect"},
|
|
||||||
status_code=401,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate new password strength
|
|
||||||
is_valid, error_message = validate_password_strength(body.password)
|
|
||||||
if not is_valid:
|
|
||||||
return JSONResponse(
|
|
||||||
content={"message": error_message},
|
|
||||||
status_code=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
password_hash = hash_password(body.password, iterations=HASH_ITERATIONS)
|
password_hash = hash_password(body.password, iterations=HASH_ITERATIONS)
|
||||||
User.update(
|
User.set_by_id(username, {User.password_hash: password_hash})
|
||||||
{
|
|
||||||
User.password_hash: password_hash,
|
|
||||||
User.password_changed_at: datetime.now(),
|
|
||||||
}
|
|
||||||
).where(User.username == username).execute()
|
|
||||||
|
|
||||||
response = JSONResponse(content={"success": True})
|
return JSONResponse(content={"success": True})
|
||||||
|
|
||||||
# If user changed their own password, issue a new JWT to keep them logged in
|
|
||||||
if current_username == username:
|
|
||||||
JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name
|
|
||||||
JWT_COOKIE_SECURE = request.app.frigate_config.auth.cookie_secure
|
|
||||||
JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length
|
|
||||||
|
|
||||||
expiration = int(time.time()) + JWT_SESSION_LENGTH
|
|
||||||
encoded_jwt = create_encoded_jwt(
|
|
||||||
username, current_role, expiration, request.app.jwt_token
|
|
||||||
)
|
|
||||||
# Set new JWT cookie on response
|
|
||||||
set_jwt_cookie(
|
|
||||||
response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
|
|||||||
@@ -710,7 +710,7 @@ def delete_classification_dataset_images(
|
|||||||
if os.path.isfile(file_path):
|
if os.path.isfile(file_path):
|
||||||
os.unlink(file_path)
|
os.unlink(file_path)
|
||||||
|
|
||||||
if os.path.exists(folder) and not os.listdir(folder) and category.lower() != "none":
|
if os.path.exists(folder) and not os.listdir(folder):
|
||||||
os.rmdir(folder)
|
os.rmdir(folder)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ class AppConfigSetBody(BaseModel):
|
|||||||
|
|
||||||
class AppPutPasswordBody(BaseModel):
|
class AppPutPasswordBody(BaseModel):
|
||||||
password: str
|
password: str
|
||||||
old_password: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class AppPostUsersBody(BaseModel):
|
class AppPostUsersBody(BaseModel):
|
||||||
|
|||||||
+2
-51
@@ -837,19 +837,7 @@ async def recording_clip(
|
|||||||
dependencies=[Depends(require_camera_access)],
|
dependencies=[Depends(require_camera_access)],
|
||||||
description="Returns an HLS playlist for the specified timestamp-range on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.",
|
description="Returns an HLS playlist for the specified timestamp-range on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.",
|
||||||
)
|
)
|
||||||
async def vod_ts(
|
async def vod_ts(camera_name: str, start_ts: float, end_ts: float):
|
||||||
camera_name: str,
|
|
||||||
start_ts: float,
|
|
||||||
end_ts: float,
|
|
||||||
force_discontinuity: bool = False,
|
|
||||||
):
|
|
||||||
logger.debug(
|
|
||||||
"VOD: Generating VOD for %s from %s to %s with force_discontinuity=%s",
|
|
||||||
camera_name,
|
|
||||||
start_ts,
|
|
||||||
end_ts,
|
|
||||||
force_discontinuity,
|
|
||||||
)
|
|
||||||
recordings = (
|
recordings = (
|
||||||
Recordings.select(
|
Recordings.select(
|
||||||
Recordings.path,
|
Recordings.path,
|
||||||
@@ -874,14 +862,6 @@ async def vod_ts(
|
|||||||
|
|
||||||
recording: Recordings
|
recording: Recordings
|
||||||
for recording in recordings:
|
for recording in recordings:
|
||||||
logger.debug(
|
|
||||||
"VOD: processing recording: %s start=%s end=%s duration=%s",
|
|
||||||
recording.path,
|
|
||||||
recording.start_time,
|
|
||||||
recording.end_time,
|
|
||||||
recording.duration,
|
|
||||||
)
|
|
||||||
|
|
||||||
clip = {"type": "source", "path": recording.path}
|
clip = {"type": "source", "path": recording.path}
|
||||||
duration = int(recording.duration * 1000)
|
duration = int(recording.duration * 1000)
|
||||||
|
|
||||||
@@ -890,11 +870,6 @@ async def vod_ts(
|
|||||||
inpoint = int((start_ts - recording.start_time) * 1000)
|
inpoint = int((start_ts - recording.start_time) * 1000)
|
||||||
clip["clipFrom"] = inpoint
|
clip["clipFrom"] = inpoint
|
||||||
duration -= inpoint
|
duration -= inpoint
|
||||||
logger.debug(
|
|
||||||
"VOD: applied clipFrom %sms to %s",
|
|
||||||
inpoint,
|
|
||||||
recording.path,
|
|
||||||
)
|
|
||||||
|
|
||||||
# adjust end if recording.end_time is after end_ts
|
# adjust end if recording.end_time is after end_ts
|
||||||
if recording.end_time > end_ts:
|
if recording.end_time > end_ts:
|
||||||
@@ -902,23 +877,12 @@ async def vod_ts(
|
|||||||
|
|
||||||
if duration < min_duration_ms:
|
if duration < min_duration_ms:
|
||||||
# skip if the clip has no valid duration (too short to contain frames)
|
# skip if the clip has no valid duration (too short to contain frames)
|
||||||
logger.debug(
|
|
||||||
"VOD: skipping recording %s - resulting duration %sms too short",
|
|
||||||
recording.path,
|
|
||||||
duration,
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if min_duration_ms <= duration < max_duration_ms:
|
if min_duration_ms <= duration < max_duration_ms:
|
||||||
clip["keyFrameDurations"] = [duration]
|
clip["keyFrameDurations"] = [duration]
|
||||||
clips.append(clip)
|
clips.append(clip)
|
||||||
durations.append(duration)
|
durations.append(duration)
|
||||||
logger.debug(
|
|
||||||
"VOD: added clip %s duration_ms=%s clipFrom=%s",
|
|
||||||
recording.path,
|
|
||||||
duration,
|
|
||||||
clip.get("clipFrom"),
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Recording clip is missing or empty: {recording.path}")
|
logger.warning(f"Recording clip is missing or empty: {recording.path}")
|
||||||
|
|
||||||
@@ -938,7 +902,7 @@ async def vod_ts(
|
|||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
"cache": hour_ago.timestamp() > start_ts,
|
"cache": hour_ago.timestamp() > start_ts,
|
||||||
"discontinuity": force_discontinuity,
|
"discontinuity": False,
|
||||||
"consistentSequenceMediaInfo": True,
|
"consistentSequenceMediaInfo": True,
|
||||||
"durations": durations,
|
"durations": durations,
|
||||||
"segment_duration": max(durations),
|
"segment_duration": max(durations),
|
||||||
@@ -1022,19 +986,6 @@ async def vod_event(
|
|||||||
return vod_response
|
return vod_response
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/vod/clip/{camera_name}/start/{start_ts}/end/{end_ts}",
|
|
||||||
dependencies=[Depends(require_camera_access)],
|
|
||||||
description="Returns an HLS playlist for a timestamp range with HLS discontinuity enabled. Append /master.m3u8 or /index.m3u8 for HLS playback.",
|
|
||||||
)
|
|
||||||
async def vod_clip(
|
|
||||||
camera_name: str,
|
|
||||||
start_ts: float,
|
|
||||||
end_ts: float,
|
|
||||||
):
|
|
||||||
return await vod_ts(camera_name, start_ts, end_ts, force_discontinuity=True)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/events/{event_id}/snapshot.jpg",
|
"/events/{event_id}/snapshot.jpg",
|
||||||
description="Returns a snapshot image for the specified object id. NOTE: The query params only take affect while the event is in-progress. Once the event has ended the snapshot configuration is used.",
|
description="Returns a snapshot image for the specified object id. NOTE: The query params only take affect while the event is in-progress. Once the event has ended the snapshot configuration is used.",
|
||||||
|
|||||||
+10
-34
@@ -5,14 +5,10 @@ import os
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from frigate.api.auth import (
|
from frigate.api.auth import require_camera_access
|
||||||
allow_any_authenticated,
|
|
||||||
get_allowed_cameras_for_filter,
|
|
||||||
require_camera_access,
|
|
||||||
)
|
|
||||||
from frigate.api.defs.response.preview_response import (
|
from frigate.api.defs.response.preview_response import (
|
||||||
PreviewFramesResponse,
|
PreviewFramesResponse,
|
||||||
PreviewsResponse,
|
PreviewsResponse,
|
||||||
@@ -30,32 +26,19 @@ router = APIRouter(tags=[Tags.preview])
|
|||||||
@router.get(
|
@router.get(
|
||||||
"/preview/{camera_name}/start/{start_ts}/end/{end_ts}",
|
"/preview/{camera_name}/start/{start_ts}/end/{end_ts}",
|
||||||
response_model=PreviewsResponse,
|
response_model=PreviewsResponse,
|
||||||
dependencies=[Depends(allow_any_authenticated())],
|
dependencies=[Depends(require_camera_access)],
|
||||||
summary="Get preview clips for time range",
|
summary="Get preview clips for time range",
|
||||||
description="""Gets all preview clips for a specified camera and time range.
|
description="""Gets all preview clips for a specified camera and time range.
|
||||||
Returns a list of preview video clips that overlap with the requested time period,
|
Returns a list of preview video clips that overlap with the requested time period,
|
||||||
ordered by start time. Use camera_name='all' to get previews from all cameras.
|
ordered by start time. Use camera_name='all' to get previews from all cameras.
|
||||||
Returns an error if no previews are found.""",
|
Returns an error if no previews are found.""",
|
||||||
)
|
)
|
||||||
def preview_ts(
|
def preview_ts(camera_name: str, start_ts: float, end_ts: float):
|
||||||
camera_name: str,
|
|
||||||
start_ts: float,
|
|
||||||
end_ts: float,
|
|
||||||
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
|
||||||
):
|
|
||||||
"""Get all mp4 previews relevant for time period."""
|
"""Get all mp4 previews relevant for time period."""
|
||||||
if camera_name != "all":
|
if camera_name != "all":
|
||||||
if camera_name not in allowed_cameras:
|
camera_clause = Previews.camera == camera_name
|
||||||
raise HTTPException(status_code=403, detail="Access denied for camera")
|
|
||||||
camera_list = [camera_name]
|
|
||||||
else:
|
else:
|
||||||
camera_list = allowed_cameras
|
camera_clause = True
|
||||||
|
|
||||||
if not camera_list:
|
|
||||||
return JSONResponse(
|
|
||||||
content={"success": False, "message": "No previews found."},
|
|
||||||
status_code=404,
|
|
||||||
)
|
|
||||||
|
|
||||||
previews = (
|
previews = (
|
||||||
Previews.select(
|
Previews.select(
|
||||||
@@ -70,7 +53,7 @@ def preview_ts(
|
|||||||
| Previews.end_time.between(start_ts, end_ts)
|
| Previews.end_time.between(start_ts, end_ts)
|
||||||
| ((start_ts > Previews.start_time) & (end_ts < Previews.end_time))
|
| ((start_ts > Previews.start_time) & (end_ts < Previews.end_time))
|
||||||
)
|
)
|
||||||
.where(Previews.camera << camera_list)
|
.where(camera_clause)
|
||||||
.order_by(Previews.start_time.asc())
|
.order_by(Previews.start_time.asc())
|
||||||
.dicts()
|
.dicts()
|
||||||
.iterator()
|
.iterator()
|
||||||
@@ -105,21 +88,14 @@ def preview_ts(
|
|||||||
@router.get(
|
@router.get(
|
||||||
"/preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}",
|
"/preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}",
|
||||||
response_model=PreviewsResponse,
|
response_model=PreviewsResponse,
|
||||||
dependencies=[Depends(allow_any_authenticated())],
|
dependencies=[Depends(require_camera_access)],
|
||||||
summary="Get preview clips for specific hour",
|
summary="Get preview clips for specific hour",
|
||||||
description="""Gets all preview clips for a specific hour in a given timezone.
|
description="""Gets all preview clips for a specific hour in a given timezone.
|
||||||
Converts the provided date/time from the specified timezone to UTC and retrieves
|
Converts the provided date/time from the specified timezone to UTC and retrieves
|
||||||
all preview clips for that hour. Use camera_name='all' to get previews from all cameras.
|
all preview clips for that hour. Use camera_name='all' to get previews from all cameras.
|
||||||
The tz_name should be a timezone like 'America/New_York' (use commas instead of slashes).""",
|
The tz_name should be a timezone like 'America/New_York' (use commas instead of slashes).""",
|
||||||
)
|
)
|
||||||
def preview_hour(
|
def preview_hour(year_month: str, day: int, hour: int, camera_name: str, tz_name: str):
|
||||||
year_month: str,
|
|
||||||
day: int,
|
|
||||||
hour: int,
|
|
||||||
camera_name: str,
|
|
||||||
tz_name: str,
|
|
||||||
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
|
||||||
):
|
|
||||||
"""Get all mp4 previews relevant for time period given the timezone"""
|
"""Get all mp4 previews relevant for time period given the timezone"""
|
||||||
parts = year_month.split("-")
|
parts = year_month.split("-")
|
||||||
start_date = (
|
start_date = (
|
||||||
@@ -130,7 +106,7 @@ def preview_hour(
|
|||||||
start_ts = start_date.timestamp()
|
start_ts = start_date.timestamp()
|
||||||
end_ts = end_date.timestamp()
|
end_ts = end_date.timestamp()
|
||||||
|
|
||||||
return preview_ts(camera_name, start_ts, end_ts, allowed_cameras)
|
return preview_ts(camera_name, start_ts, end_ts)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
|||||||
@@ -607,27 +607,23 @@ class Dispatcher:
|
|||||||
)
|
)
|
||||||
self.publish(f"{camera_name}/snapshots/state", payload, retain=True)
|
self.publish(f"{camera_name}/snapshots/state", payload, retain=True)
|
||||||
|
|
||||||
def _on_ptz_command(self, camera_name: str, payload: str | bytes) -> None:
|
def _on_ptz_command(self, camera_name: str, payload: str) -> None:
|
||||||
"""Callback for ptz topic."""
|
"""Callback for ptz topic."""
|
||||||
try:
|
try:
|
||||||
preset: str = (
|
if "preset" in payload.lower():
|
||||||
payload.decode("utf-8") if isinstance(payload, bytes) else payload
|
|
||||||
).lower()
|
|
||||||
|
|
||||||
if "preset" in preset:
|
|
||||||
command = OnvifCommandEnum.preset
|
command = OnvifCommandEnum.preset
|
||||||
param = preset[preset.index("_") + 1 :]
|
param = payload.lower()[payload.index("_") + 1 :]
|
||||||
elif "move_relative" in preset:
|
elif "move_relative" in payload.lower():
|
||||||
command = OnvifCommandEnum.move_relative
|
command = OnvifCommandEnum.move_relative
|
||||||
param = preset[preset.index("_") + 1 :]
|
param = payload.lower()[payload.index("_") + 1 :]
|
||||||
else:
|
else:
|
||||||
command = OnvifCommandEnum[preset]
|
command = OnvifCommandEnum[payload.lower()]
|
||||||
param = ""
|
param = ""
|
||||||
|
|
||||||
self.onvif.handle_command(camera_name, command, param)
|
self.onvif.handle_command(camera_name, command, param)
|
||||||
logger.info(f"Setting ptz command to {command} for {camera_name}")
|
logger.info(f"Setting ptz command to {command} for {camera_name}")
|
||||||
except KeyError as k:
|
except KeyError as k:
|
||||||
logger.error(f"Invalid PTZ command {preset}: {k}")
|
logger.error(f"Invalid PTZ command {payload}: {k}")
|
||||||
|
|
||||||
def _on_birdseye_command(self, camera_name: str, payload: str) -> None:
|
def _on_birdseye_command(self, camera_name: str, payload: str) -> None:
|
||||||
"""Callback for birdseye topic."""
|
"""Callback for birdseye topic."""
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from frigate.config.camera.updater import (
|
|||||||
CameraConfigUpdateEnum,
|
CameraConfigUpdateEnum,
|
||||||
CameraConfigUpdateSubscriber,
|
CameraConfigUpdateSubscriber,
|
||||||
)
|
)
|
||||||
from frigate.const import BASE_DIR, CONFIG_DIR
|
from frigate.const import CONFIG_DIR
|
||||||
from frigate.models import User
|
from frigate.models import User
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -371,7 +371,7 @@ class WebPushClient(Communicator):
|
|||||||
|
|
||||||
sorted_objects.update(payload["after"]["data"]["sub_labels"])
|
sorted_objects.update(payload["after"]["data"]["sub_labels"])
|
||||||
|
|
||||||
image = f"{payload['after']['thumb_path'].replace(BASE_DIR, '')}"
|
image = f"{payload['after']['thumb_path'].replace('/media/frigate', '')}"
|
||||||
ended = state == "end" or state == "genai"
|
ended = state == "end" or state == "genai"
|
||||||
|
|
||||||
if state == "genai" and payload["after"]["data"]["metadata"]:
|
if state == "genai" and payload["after"]["data"]["metadata"]:
|
||||||
@@ -390,20 +390,7 @@ class WebPushClient(Communicator):
|
|||||||
|
|
||||||
message = payload["after"]["data"]["metadata"]["scene"]
|
message = payload["after"]["data"]["metadata"]["scene"]
|
||||||
else:
|
else:
|
||||||
zone_names = payload["after"]["data"]["zones"]
|
title = f"{titlecase(', '.join(sorted_objects).replace('_', ' '))}{' was' if state == 'end' else ''} detected in {titlecase(', '.join(payload['after']['data']['zones']).replace('_', ' '))}"
|
||||||
formatted_zone_names = []
|
|
||||||
|
|
||||||
for zone_name in zone_names:
|
|
||||||
if zone_name in self.config.cameras[camera].zones:
|
|
||||||
formatted_zone_names.append(
|
|
||||||
self.config.cameras[camera]
|
|
||||||
.zones[zone_name]
|
|
||||||
.get_formatted_name(zone_name)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
formatted_zone_names.append(titlecase(zone_name.replace("_", " ")))
|
|
||||||
|
|
||||||
title = f"{titlecase(', '.join(sorted_objects).replace('_', ' '))}{' was' if state == 'end' else ''} detected in {', '.join(formatted_zone_names)}"
|
|
||||||
message = f"Detected on {camera_name}"
|
message = f"Detected on {camera_name}"
|
||||||
|
|
||||||
if ended:
|
if ended:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class AuthConfig(FrigateBaseModel):
|
|||||||
default=86400, title="Session length for jwt session tokens", ge=60
|
default=86400, title="Session length for jwt session tokens", ge=60
|
||||||
)
|
)
|
||||||
refresh_time: int = Field(
|
refresh_time: int = Field(
|
||||||
default=1800,
|
default=43200,
|
||||||
title="Refresh the session if it is going to expire in this many seconds",
|
title="Refresh the session if it is going to expire in this many seconds",
|
||||||
ge=30,
|
ge=30,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ class UIConfig(FrigateBaseModel):
|
|||||||
time_style: DateTimeStyleEnum = Field(
|
time_style: DateTimeStyleEnum = Field(
|
||||||
default=DateTimeStyleEnum.medium, title="Override UI timeStyle."
|
default=DateTimeStyleEnum.medium, title="Override UI timeStyle."
|
||||||
)
|
)
|
||||||
|
strftime_fmt: Optional[str] = Field(
|
||||||
|
default=None, title="Override date and time format using strftime syntax."
|
||||||
|
)
|
||||||
unit_system: UnitSystemEnum = Field(
|
unit_system: UnitSystemEnum = Field(
|
||||||
default=UnitSystemEnum.metric, title="The unit system to use for measurements."
|
default=UnitSystemEnum.metric, title="The unit system to use for measurements."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -209,22 +209,10 @@ class ReviewDescriptionProcessor(PostProcessorApi):
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
f"Found GenAI Review Summary request for {start_ts} to {end_ts}"
|
f"Found GenAI Review Summary request for {start_ts} to {end_ts}"
|
||||||
)
|
)
|
||||||
|
items: list[dict[str, Any]] = [
|
||||||
# Query all review segments with camera and time information
|
r["data"]["metadata"]
|
||||||
segments: list[dict[str, Any]] = [
|
|
||||||
{
|
|
||||||
"camera": r["camera"].replace("_", " ").title(),
|
|
||||||
"start_time": r["start_time"],
|
|
||||||
"end_time": r["end_time"],
|
|
||||||
"metadata": r["data"]["metadata"],
|
|
||||||
}
|
|
||||||
for r in (
|
for r in (
|
||||||
ReviewSegment.select(
|
ReviewSegment.select(ReviewSegment.data)
|
||||||
ReviewSegment.camera,
|
|
||||||
ReviewSegment.start_time,
|
|
||||||
ReviewSegment.end_time,
|
|
||||||
ReviewSegment.data,
|
|
||||||
)
|
|
||||||
.where(
|
.where(
|
||||||
(ReviewSegment.data["metadata"].is_null(False))
|
(ReviewSegment.data["metadata"].is_null(False))
|
||||||
& (ReviewSegment.start_time < end_ts)
|
& (ReviewSegment.start_time < end_ts)
|
||||||
@@ -236,72 +224,21 @@ class ReviewDescriptionProcessor(PostProcessorApi):
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
if len(segments) == 0:
|
if len(items) == 0:
|
||||||
logger.debug("No review items with metadata found during time period")
|
logger.debug("No review items with metadata found during time period")
|
||||||
return "No activity was found during this time period."
|
return "No activity was found during this time."
|
||||||
|
|
||||||
# Identify primary items (important items that need review)
|
important_items = list(
|
||||||
primary_segments = [
|
filter(
|
||||||
seg
|
lambda item: item.get("potential_threat_level", 0) > 0
|
||||||
for seg in segments
|
or item.get("other_concerns"),
|
||||||
if seg["metadata"].get("potential_threat_level", 0) > 0
|
items,
|
||||||
or seg["metadata"].get("other_concerns")
|
)
|
||||||
]
|
)
|
||||||
|
|
||||||
if not primary_segments:
|
if not important_items:
|
||||||
return "No concerns were found during this time period."
|
return "No concerns were found during this time period."
|
||||||
|
|
||||||
# Build hierarchical structure: each primary event with its contextual items
|
|
||||||
events_with_context = []
|
|
||||||
|
|
||||||
for primary_seg in primary_segments:
|
|
||||||
# Start building the primary event structure
|
|
||||||
primary_item = copy.deepcopy(primary_seg["metadata"])
|
|
||||||
primary_item["camera"] = primary_seg["camera"]
|
|
||||||
primary_item["start_time"] = primary_seg["start_time"]
|
|
||||||
primary_item["end_time"] = primary_seg["end_time"]
|
|
||||||
|
|
||||||
# Find overlapping contextual items from other cameras
|
|
||||||
primary_start = primary_seg["start_time"]
|
|
||||||
primary_end = primary_seg["end_time"]
|
|
||||||
primary_camera = primary_seg["camera"]
|
|
||||||
contextual_items = []
|
|
||||||
seen_contextual_cameras = set()
|
|
||||||
|
|
||||||
for seg in segments:
|
|
||||||
seg_camera = seg["camera"]
|
|
||||||
|
|
||||||
if seg_camera == primary_camera:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if seg in primary_segments:
|
|
||||||
continue
|
|
||||||
|
|
||||||
seg_start = seg["start_time"]
|
|
||||||
seg_end = seg["end_time"]
|
|
||||||
|
|
||||||
if seg_start < primary_end and primary_start < seg_end:
|
|
||||||
# Avoid duplicates if same camera has multiple overlapping segments
|
|
||||||
if seg_camera not in seen_contextual_cameras:
|
|
||||||
contextual_item = copy.deepcopy(seg["metadata"])
|
|
||||||
contextual_item["camera"] = seg_camera
|
|
||||||
contextual_item["start_time"] = seg_start
|
|
||||||
contextual_item["end_time"] = seg_end
|
|
||||||
contextual_items.append(contextual_item)
|
|
||||||
seen_contextual_cameras.add(seg_camera)
|
|
||||||
|
|
||||||
# Add context array to primary item
|
|
||||||
primary_item["context"] = contextual_items
|
|
||||||
events_with_context.append(primary_item)
|
|
||||||
|
|
||||||
total_context_items = sum(
|
|
||||||
len(event.get("context", [])) for event in events_with_context
|
|
||||||
)
|
|
||||||
logger.debug(
|
|
||||||
f"Summary includes {len(events_with_context)} primary events with "
|
|
||||||
f"{total_context_items} total contextual items"
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.config.review.genai.debug_save_thumbnails:
|
if self.config.review.genai.debug_save_thumbnails:
|
||||||
Path(
|
Path(
|
||||||
os.path.join(CLIPS_DIR, "genai-requests", f"{start_ts}-{end_ts}")
|
os.path.join(CLIPS_DIR, "genai-requests", f"{start_ts}-{end_ts}")
|
||||||
@@ -310,7 +247,7 @@ class ReviewDescriptionProcessor(PostProcessorApi):
|
|||||||
return self.genai_client.generate_review_summary(
|
return self.genai_client.generate_review_summary(
|
||||||
start_ts,
|
start_ts,
|
||||||
end_ts,
|
end_ts,
|
||||||
events_with_context,
|
important_items,
|
||||||
self.config.review.genai.debug_save_thumbnails,
|
self.config.review.genai.debug_save_thumbnails,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -639,14 +639,14 @@ def write_classification_attempt(
|
|||||||
os.makedirs(folder, exist_ok=True)
|
os.makedirs(folder, exist_ok=True)
|
||||||
cv2.imwrite(file, frame)
|
cv2.imwrite(file, frame)
|
||||||
|
|
||||||
|
files = sorted(
|
||||||
|
filter(lambda f: (f.endswith(".webp")), os.listdir(folder)),
|
||||||
|
key=lambda f: os.path.getctime(os.path.join(folder, f)),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
|
||||||
# delete oldest face image if maximum is reached
|
# delete oldest face image if maximum is reached
|
||||||
try:
|
try:
|
||||||
files = sorted(
|
|
||||||
filter(lambda f: (f.endswith(".webp")), os.listdir(folder)),
|
|
||||||
key=lambda f: os.path.getctime(os.path.join(folder, f)),
|
|
||||||
reverse=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(files) > max_files:
|
if len(files) > max_files:
|
||||||
os.unlink(os.path.join(folder, files[-1]))
|
os.unlink(os.path.join(folder, files[-1]))
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ from peewee import DoesNotExist, IntegrityError
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
from playhouse.shortcuts import model_to_dict
|
from playhouse.shortcuts import model_to_dict
|
||||||
|
|
||||||
|
from frigate.comms.embeddings_updater import (
|
||||||
|
EmbeddingsRequestEnum,
|
||||||
|
)
|
||||||
from frigate.comms.inter_process import InterProcessRequestor
|
from frigate.comms.inter_process import InterProcessRequestor
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.config.classification import SemanticSearchModelEnum
|
from frigate.config.classification import SemanticSearchModelEnum
|
||||||
@@ -492,49 +495,44 @@ class Embeddings:
|
|||||||
or thumbnail_missing
|
or thumbnail_missing
|
||||||
):
|
):
|
||||||
existing_trigger.embedding = self._calculate_trigger_embedding(
|
existing_trigger.embedding = self._calculate_trigger_embedding(
|
||||||
trigger, trigger_name, camera.name
|
trigger
|
||||||
)
|
)
|
||||||
needs_embedding_update = True
|
needs_embedding_update = True
|
||||||
|
|
||||||
if needs_embedding_update:
|
if needs_embedding_update:
|
||||||
existing_trigger.save()
|
existing_trigger.save()
|
||||||
continue
|
|
||||||
else:
|
else:
|
||||||
# Create new trigger
|
# Create new trigger
|
||||||
try:
|
try:
|
||||||
# For thumbnail triggers, validate the event exists
|
try:
|
||||||
if trigger.type == "thumbnail":
|
event: Event = Event.get(Event.id == trigger.data)
|
||||||
try:
|
except DoesNotExist:
|
||||||
event: Event = Event.get(Event.id == trigger.data)
|
logger.warning(
|
||||||
except DoesNotExist:
|
f"Event ID {trigger.data} for trigger {trigger_name} does not exist."
|
||||||
logger.warning(
|
|
||||||
f"Event ID {trigger.data} for trigger {trigger_name} does not exist."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip the event if not an object
|
|
||||||
if event.data.get("type") != "object":
|
|
||||||
logger.warning(
|
|
||||||
f"Event ID {trigger.data} for trigger {trigger_name} is not a tracked object."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
thumbnail = get_event_thumbnail_bytes(event)
|
|
||||||
|
|
||||||
if not thumbnail:
|
|
||||||
logger.warning(
|
|
||||||
f"Unable to retrieve thumbnail for event ID {trigger.data} for {trigger_name}."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.write_trigger_thumbnail(
|
|
||||||
camera.name, trigger.data, thumbnail
|
|
||||||
)
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip the event if not an object
|
||||||
|
if event.data.get("type") != "object":
|
||||||
|
logger.warning(
|
||||||
|
f"Event ID {trigger.data} for trigger {trigger_name} is not a tracked object."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
thumbnail = get_event_thumbnail_bytes(event)
|
||||||
|
|
||||||
|
if not thumbnail:
|
||||||
|
logger.warning(
|
||||||
|
f"Unable to retrieve thumbnail for event ID {trigger.data} for {trigger_name}."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.write_trigger_thumbnail(
|
||||||
|
camera.name, trigger.data, thumbnail
|
||||||
|
)
|
||||||
|
|
||||||
# Calculate embedding for new trigger
|
# Calculate embedding for new trigger
|
||||||
embedding = self._calculate_trigger_embedding(
|
embedding = self._calculate_trigger_embedding(trigger)
|
||||||
trigger, trigger_name, camera.name
|
|
||||||
)
|
|
||||||
|
|
||||||
Trigger.create(
|
Trigger.create(
|
||||||
camera=camera.name,
|
camera=camera.name,
|
||||||
@@ -560,11 +558,7 @@ class Embeddings:
|
|||||||
Trigger.camera == camera.name, Trigger.name.in_(triggers_to_remove)
|
Trigger.camera == camera.name, Trigger.name.in_(triggers_to_remove)
|
||||||
).execute()
|
).execute()
|
||||||
for trigger_name in triggers_to_remove:
|
for trigger_name in triggers_to_remove:
|
||||||
# Only remove thumbnail files for thumbnail triggers
|
self.remove_trigger_thumbnail(camera.name, trigger_name)
|
||||||
if existing_triggers[trigger_name].type == "thumbnail":
|
|
||||||
self.remove_trigger_thumbnail(
|
|
||||||
camera.name, existing_triggers[trigger_name].data
|
|
||||||
)
|
|
||||||
|
|
||||||
def write_trigger_thumbnail(
|
def write_trigger_thumbnail(
|
||||||
self, camera: str, event_id: str, thumbnail: bytes
|
self, camera: str, event_id: str, thumbnail: bytes
|
||||||
@@ -594,13 +588,14 @@ class Embeddings:
|
|||||||
f"Failed to delete thumbnail for trigger with data {event_id} in {camera}: {e}"
|
f"Failed to delete thumbnail for trigger with data {event_id} in {camera}: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _calculate_trigger_embedding(
|
def _calculate_trigger_embedding(self, trigger) -> bytes:
|
||||||
self, trigger, trigger_name: str, camera_name: str
|
|
||||||
) -> bytes:
|
|
||||||
"""Calculate embedding for a trigger based on its type and data."""
|
"""Calculate embedding for a trigger based on its type and data."""
|
||||||
if trigger.type == "description":
|
if trigger.type == "description":
|
||||||
logger.debug(f"Generating embedding for trigger description {trigger_name}")
|
logger.debug(f"Generating embedding for trigger description {trigger.name}")
|
||||||
embedding = self.embed_description(None, trigger.data, upsert=False)
|
embedding = self.requestor.send_data(
|
||||||
|
EmbeddingsRequestEnum.embed_description.value,
|
||||||
|
{"id": None, "description": trigger.data, "upsert": False},
|
||||||
|
)
|
||||||
return embedding.astype(np.float32).tobytes()
|
return embedding.astype(np.float32).tobytes()
|
||||||
|
|
||||||
elif trigger.type == "thumbnail":
|
elif trigger.type == "thumbnail":
|
||||||
@@ -620,21 +615,28 @@ class Embeddings:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with open(
|
with open(
|
||||||
os.path.join(TRIGGER_DIR, camera_name, f"{trigger.data}.webp"),
|
os.path.join(
|
||||||
|
TRIGGER_DIR, trigger.camera, f"{trigger.data}.webp"
|
||||||
|
),
|
||||||
"rb",
|
"rb",
|
||||||
) as f:
|
) as f:
|
||||||
thumbnail = f.read()
|
thumbnail = f.read()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Failed to read thumbnail for trigger {trigger_name} with ID {trigger.data}: {e}"
|
f"Failed to read thumbnail for trigger {trigger.name} with ID {trigger.data}: {e}"
|
||||||
)
|
)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Generating embedding for trigger thumbnail {trigger_name} with ID {trigger.data}"
|
f"Generating embedding for trigger thumbnail {trigger.name} with ID {trigger.data}"
|
||||||
)
|
)
|
||||||
embedding = self.embed_thumbnail(
|
embedding = self.requestor.send_data(
|
||||||
str(trigger.data), thumbnail, upsert=False
|
EmbeddingsRequestEnum.embed_thumbnail.value,
|
||||||
|
{
|
||||||
|
"id": str(trigger.data),
|
||||||
|
"thumbnail": str(thumbnail),
|
||||||
|
"upsert": False,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return embedding.astype(np.float32).tobytes()
|
return embedding.astype(np.float32).tobytes()
|
||||||
|
|
||||||
|
|||||||
+36
-40
@@ -177,60 +177,56 @@ Each line represents a detection state, not necessarily unique individuals. Pare
|
|||||||
self,
|
self,
|
||||||
start_ts: float,
|
start_ts: float,
|
||||||
end_ts: float,
|
end_ts: float,
|
||||||
events: list[dict[str, Any]],
|
segments: list[dict[str, Any]],
|
||||||
debug_save: bool,
|
debug_save: bool,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Generate a summary of review item descriptions over a period of time."""
|
"""Generate a summary of review item descriptions over a period of time."""
|
||||||
time_range = f"{datetime.datetime.fromtimestamp(start_ts).strftime('%B %d, %Y at %I:%M %p')} to {datetime.datetime.fromtimestamp(end_ts).strftime('%B %d, %Y at %I:%M %p')}"
|
time_range = f"{datetime.datetime.fromtimestamp(start_ts).strftime('%B %d, %Y at %I:%M %p')} to {datetime.datetime.fromtimestamp(end_ts).strftime('%B %d, %Y at %I:%M %p')}"
|
||||||
timeline_summary_prompt = f"""
|
timeline_summary_prompt = f"""
|
||||||
You are a security officer writing a concise security report.
|
You are a security officer.
|
||||||
|
Time range: {time_range}.
|
||||||
|
Input: JSON list with "title", "scene", "confidence", "potential_threat_level" (1-2), "other_concerns".
|
||||||
|
|
||||||
Time range: {time_range}
|
Task: Write a concise, human-presentable security report in markdown format.
|
||||||
|
|
||||||
Input format: Each event is a JSON object with:
|
Rules for the report:
|
||||||
- "title", "scene", "confidence", "potential_threat_level" (0-2), "other_concerns", "camera", "time", "start_time", "end_time"
|
|
||||||
- "context": array of related events from other cameras that occurred during overlapping time periods
|
|
||||||
|
|
||||||
Report Structure - Use this EXACT format:
|
- Title & overview
|
||||||
|
- Start with:
|
||||||
|
# Security Summary - {time_range}
|
||||||
|
- Write a 1-2 sentence situational overview capturing the general pattern of the period.
|
||||||
|
|
||||||
# Security Summary - {time_range}
|
- Event details
|
||||||
|
- Present events in chronological order as a bullet list.
|
||||||
|
- **If multiple events occur within the same minute or overlapping time range, COMBINE them into a single bullet.**
|
||||||
|
- Summarize the distinct activities as sub-points under the shared timestamp.
|
||||||
|
- If no timestamp is given, preserve order but label as “Time not specified.”
|
||||||
|
- Use bold timestamps for clarity.
|
||||||
|
- Group bullets under subheadings when multiple events fall into the same category (e.g., Vehicle Activity, Porch Activity, Unusual Behavior).
|
||||||
|
|
||||||
## Overview
|
- Threat levels
|
||||||
[Write 1-2 sentences summarizing the overall activity pattern during this period.]
|
- Always show the threat level for each event using these labels:
|
||||||
|
- Threat level 0: "Normal"
|
||||||
|
- Threat level 1: "Needs review"
|
||||||
|
- Threat level 2: "Security concern"
|
||||||
|
- Format as (threat level: Normal), (threat level: Needs review), or (threat level: Security concern).
|
||||||
|
- If multiple events at the same time share the same threat level, only state it once.
|
||||||
|
|
||||||
---
|
- Final assessment
|
||||||
|
- End with a Final Assessment section.
|
||||||
|
- If all events are threat level 0:
|
||||||
|
Final assessment: Only normal residential activity observed during this period.
|
||||||
|
- If threat level 1 events are present:
|
||||||
|
Final assessment: Some activity requires review but no security concerns identified.
|
||||||
|
- If threat level 2 events are present, clearly summarize them as Security concerns requiring immediate attention.
|
||||||
|
|
||||||
## Timeline
|
- Conciseness
|
||||||
|
- Do not repeat benign clothing/appearance details unless they distinguish individuals.
|
||||||
[Group events by time periods (e.g., "Morning (6:00 AM - 12:00 PM)", "Afternoon (12:00 PM - 5:00 PM)", "Evening (5:00 PM - 9:00 PM)", "Night (9:00 PM - 6:00 AM)"). Use appropriate time blocks based on when events occurred.]
|
- Summarize similar routine events instead of restating full scene descriptions.
|
||||||
|
|
||||||
### [Time Block Name]
|
|
||||||
|
|
||||||
**HH:MM AM/PM** | [Camera Name] | [Threat Level Indicator]
|
|
||||||
- [Event title]: [Clear description incorporating contextual information from the "context" array]
|
|
||||||
- Context: [If context array has items, mention them here, e.g., "Delivery truck present on Front Driveway Cam (HH:MM AM/PM)"]
|
|
||||||
- Assessment: [Brief assessment incorporating context - if context explains the event, note it here]
|
|
||||||
|
|
||||||
[Repeat for each event in chronological order within the time block]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
[One sentence summarizing the period. If all events are normal/explained: "Routine activity observed." If review needed: "Some activity requires review but no security concerns." If security concerns: "Security concerns requiring immediate attention."]
|
|
||||||
|
|
||||||
Guidelines:
|
|
||||||
- List ALL events in chronological order, grouped by time blocks
|
|
||||||
- Threat level indicators: ✓ Normal, ⚠️ Needs review, 🔴 Security concern
|
|
||||||
- Integrate contextual information naturally - use the "context" array to enrich each event's description
|
|
||||||
- If context explains the event (e.g., delivery truck explains person at door), describe it accordingly (e.g., "delivery person" not "unidentified person")
|
|
||||||
- Be concise but informative - focus on what happened and what it means
|
|
||||||
- If contextual information makes an event clearly normal, reflect that in your assessment
|
|
||||||
- Only create time blocks that have events - don't create empty sections
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
timeline_summary_prompt += "\n\nEvents:\n"
|
for item in segments:
|
||||||
for event in events:
|
timeline_summary_prompt += f"\n{item}"
|
||||||
timeline_summary_prompt += f"\n{event}\n"
|
|
||||||
|
|
||||||
if debug_save:
|
if debug_save:
|
||||||
with open(
|
with open(
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ class User(Model):
|
|||||||
default="admin",
|
default="admin",
|
||||||
)
|
)
|
||||||
password_hash = CharField(null=False, max_length=120)
|
password_hash = CharField(null=False, max_length=120)
|
||||||
password_changed_at = DateTimeField(null=True)
|
|
||||||
notification_tokens = JSONField()
|
notification_tokens = JSONField()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -239,12 +239,6 @@ class ImprovedMotionDetector(MotionDetector):
|
|||||||
)
|
)
|
||||||
self.mask = np.where(resized_mask == [0])
|
self.mask = np.where(resized_mask == [0])
|
||||||
|
|
||||||
# Reset motion detection state when mask changes
|
|
||||||
# so motion detection can quickly recalibrate with the new mask
|
|
||||||
self.avg_frame = np.zeros(self.motion_frame_size, np.float32)
|
|
||||||
self.calibrating = True
|
|
||||||
self.motion_frame_count = 0
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"""stop the motion detector."""
|
"""stop the motion detector."""
|
||||||
pass
|
pass
|
||||||
|
|||||||
+5
-25
@@ -95,21 +95,12 @@ class OnvifController:
|
|||||||
|
|
||||||
cam = self.camera_configs[cam_name]
|
cam = self.camera_configs[cam_name]
|
||||||
try:
|
try:
|
||||||
user = cam.onvif.user
|
|
||||||
password = cam.onvif.password
|
|
||||||
|
|
||||||
if user is not None and isinstance(user, bytes):
|
|
||||||
user = user.decode("utf-8")
|
|
||||||
|
|
||||||
if password is not None and isinstance(password, bytes):
|
|
||||||
password = password.decode("utf-8")
|
|
||||||
|
|
||||||
self.cams[cam_name] = {
|
self.cams[cam_name] = {
|
||||||
"onvif": ONVIFCamera(
|
"onvif": ONVIFCamera(
|
||||||
cam.onvif.host,
|
cam.onvif.host,
|
||||||
cam.onvif.port,
|
cam.onvif.port,
|
||||||
user,
|
cam.onvif.user,
|
||||||
password,
|
cam.onvif.password,
|
||||||
wsdl_dir=str(Path(find_spec("onvif").origin).parent / "wsdl"),
|
wsdl_dir=str(Path(find_spec("onvif").origin).parent / "wsdl"),
|
||||||
adjust_time=cam.onvif.ignore_time_mismatch,
|
adjust_time=cam.onvif.ignore_time_mismatch,
|
||||||
encrypt=not cam.onvif.tls_insecure,
|
encrypt=not cam.onvif.tls_insecure,
|
||||||
@@ -334,15 +325,9 @@ class OnvifController:
|
|||||||
presets = []
|
presets = []
|
||||||
|
|
||||||
for preset in presets:
|
for preset in presets:
|
||||||
# Ensure preset name is a Unicode string and handle UTF-8 characters correctly
|
self.cams[camera_name]["presets"][
|
||||||
preset_name = getattr(preset, "Name") or f"preset {preset['token']}"
|
(getattr(preset, "Name") or f"preset {preset['token']}").lower()
|
||||||
|
] = preset["token"]
|
||||||
if isinstance(preset_name, bytes):
|
|
||||||
preset_name = preset_name.decode("utf-8")
|
|
||||||
|
|
||||||
# Convert to lowercase while preserving UTF-8 characters
|
|
||||||
preset_name_lower = preset_name.lower()
|
|
||||||
self.cams[camera_name]["presets"][preset_name_lower] = preset["token"]
|
|
||||||
|
|
||||||
# get list of supported features
|
# get list of supported features
|
||||||
supported_features = []
|
supported_features = []
|
||||||
@@ -578,11 +563,6 @@ class OnvifController:
|
|||||||
self.cams[camera_name]["active"] = False
|
self.cams[camera_name]["active"] = False
|
||||||
|
|
||||||
async def _move_to_preset(self, camera_name: str, preset: str) -> None:
|
async def _move_to_preset(self, camera_name: str, preset: str) -> None:
|
||||||
if isinstance(preset, bytes):
|
|
||||||
preset = preset.decode("utf-8")
|
|
||||||
|
|
||||||
preset = preset.lower()
|
|
||||||
|
|
||||||
if preset not in self.cams[camera_name]["presets"]:
|
if preset not in self.cams[camera_name]["presets"]:
|
||||||
logger.error(f"{preset} is not a valid preset for {camera_name}")
|
logger.error(f"{preset} is not a valid preset for {camera_name}")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -78,8 +78,6 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
[
|
[
|
||||||
CameraConfigUpdateEnum.add,
|
CameraConfigUpdateEnum.add,
|
||||||
CameraConfigUpdateEnum.enabled,
|
CameraConfigUpdateEnum.enabled,
|
||||||
CameraConfigUpdateEnum.motion,
|
|
||||||
CameraConfigUpdateEnum.objects,
|
|
||||||
CameraConfigUpdateEnum.remove,
|
CameraConfigUpdateEnum.remove,
|
||||||
CameraConfigUpdateEnum.zones,
|
CameraConfigUpdateEnum.zones,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from setproctitle import setproctitle
|
|||||||
|
|
||||||
import frigate.log
|
import frigate.log
|
||||||
from frigate.config.logger import LoggerConfig
|
from frigate.config.logger import LoggerConfig
|
||||||
from frigate.const import CONFIG_DIR
|
|
||||||
|
|
||||||
|
|
||||||
class BaseProcess(mp.Process):
|
class BaseProcess(mp.Process):
|
||||||
@@ -93,7 +92,7 @@ class FrigateProcess(BaseProcess):
|
|||||||
try:
|
try:
|
||||||
import memray
|
import memray
|
||||||
|
|
||||||
reports_dir = pathlib.Path(CONFIG_DIR) / "memray_reports"
|
reports_dir = pathlib.Path("/config/memray_reports")
|
||||||
reports_dir.mkdir(parents=True, exist_ok=True)
|
reports_dir.mkdir(parents=True, exist_ok=True)
|
||||||
safe_name = (
|
safe_name = (
|
||||||
process_name.replace(":", "_").replace("/", "_").replace("\\", "_")
|
process_name.replace(":", "_").replace("/", "_").replace("\\", "_")
|
||||||
@@ -127,7 +126,7 @@ class FrigateProcess(BaseProcess):
|
|||||||
self.__memray_tracker.__exit__(None, None, None)
|
self.__memray_tracker.__exit__(None, None, None)
|
||||||
self.__memray_tracker = None
|
self.__memray_tracker = None
|
||||||
|
|
||||||
reports_dir = pathlib.Path(CONFIG_DIR) / "memray_reports"
|
reports_dir = pathlib.Path("/config/memray_reports")
|
||||||
html_file = reports_dir / f"{safe_name}.html"
|
html_file = reports_dir / f"{safe_name}.html"
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
|
|||||||
@@ -771,7 +771,6 @@ def process_frames(
|
|||||||
camera_enabled = camera_config.enabled
|
camera_enabled = camera_config.enabled
|
||||||
|
|
||||||
if "motion" in updated_configs:
|
if "motion" in updated_configs:
|
||||||
motion_detector.config = camera_config.motion
|
|
||||||
motion_detector.update_mask()
|
motion_detector.update_mask()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from frigate.models import Event
|
|||||||
|
|
||||||
|
|
||||||
def migrate(migrator, database, fake=False, **kwargs):
|
def migrate(migrator, database, fake=False, **kwargs):
|
||||||
migrator.change_fields(Event, sub_label=pw.CharField(max_length=100, null=True))
|
migrator.change_columns(Event, sub_label=pw.CharField(max_length=100, null=True))
|
||||||
|
|
||||||
|
|
||||||
def rollback(migrator, database, fake=False, **kwargs):
|
def rollback(migrator, database, fake=False, **kwargs):
|
||||||
migrator.change_fields(Event, sub_label=pw.CharField(max_length=20, null=True))
|
migrator.change_columns(Event, sub_label=pw.CharField(max_length=20, null=True))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Some examples (model - class or model name)::
|
|||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Some examples (model - class or model_name)::
|
|||||||
|
|
||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
@@ -54,9 +54,7 @@ def migrate(migrator, database, fake=False, **kwargs):
|
|||||||
|
|
||||||
# Migrate existing has_been_reviewed data to UserReviewStatus for all users
|
# Migrate existing has_been_reviewed data to UserReviewStatus for all users
|
||||||
def migrate_data():
|
def migrate_data():
|
||||||
# Use raw SQL to avoid ORM issues with columns that don't exist yet
|
all_users = list(User.select())
|
||||||
cursor = database.execute_sql('SELECT "username" FROM "user"')
|
|
||||||
all_users = cursor.fetchall()
|
|
||||||
if not all_users:
|
if not all_users:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -65,7 +63,7 @@ def migrate(migrator, database, fake=False, **kwargs):
|
|||||||
)
|
)
|
||||||
reviewed_segment_ids = [row[0] for row in cursor.fetchall()]
|
reviewed_segment_ids = [row[0] for row in cursor.fetchall()]
|
||||||
# also migrate for anonymous (unauthenticated users)
|
# also migrate for anonymous (unauthenticated users)
|
||||||
usernames = [user[0] for user in all_users] + ["anonymous"]
|
usernames = [user.username for user in all_users] + ["anonymous"]
|
||||||
|
|
||||||
for segment_id in reviewed_segment_ids:
|
for segment_id in reviewed_segment_ids:
|
||||||
for username in usernames:
|
for username in usernames:
|
||||||
@@ -76,7 +74,7 @@ def migrate(migrator, database, fake=False, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not fake: # Only run data migration if not faking
|
if not fake: # Only run data migration if not faking
|
||||||
migrator.run(migrate_data)
|
migrator.python(migrate_data)
|
||||||
|
|
||||||
migrator.sql('ALTER TABLE "reviewsegment" DROP COLUMN "has_been_reviewed"')
|
migrator.sql('ALTER TABLE "reviewsegment" DROP COLUMN "has_been_reviewed"')
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Some examples (model - class or model_name)::
|
|||||||
|
|
||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
> migrator.sql(sql) # Run custom SQL
|
> migrator.sql(sql) # Run custom SQL
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
"""Peewee migrations -- 032_add_password_changed_at.py.
|
|
||||||
|
|
||||||
Some examples (model - class or model name)::
|
|
||||||
|
|
||||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
|
||||||
> migrator.run(func, *args, **kwargs) # Run python code
|
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
|
||||||
> migrator.change_fields(model, **fields) # Change fields
|
|
||||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
|
||||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
|
||||||
> migrator.rename_table(model, new_table_name)
|
|
||||||
> migrator.add_index(model, *col_names, unique=False)
|
|
||||||
> migrator.drop_index(model, *col_names)
|
|
||||||
> migrator.add_not_null(model, *field_names)
|
|
||||||
> migrator.drop_not_null(model, *field_names)
|
|
||||||
> migrator.add_default(model, field_name, default)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import peewee as pw
|
|
||||||
|
|
||||||
SQL = pw.SQL
|
|
||||||
|
|
||||||
|
|
||||||
def migrate(migrator, database, fake=False, **kwargs):
|
|
||||||
migrator.sql(
|
|
||||||
"""
|
|
||||||
ALTER TABLE user ADD COLUMN password_changed_at DATETIME NULL
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def rollback(migrator, database, fake=False, **kwargs):
|
|
||||||
migrator.sql(
|
|
||||||
"""
|
|
||||||
ALTER TABLE user DROP COLUMN password_changed_at
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"form": {
|
"form": {
|
||||||
"user": "Потребителско име",
|
"user": "Потребителско име"
|
||||||
"password": "Парола"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
"select": "Избери"
|
"select": "Избери"
|
||||||
},
|
},
|
||||||
"restart": {
|
"restart": {
|
||||||
"title": "Сигурен ли сте, че искате да рестартирате Frigate?",
|
"title": "Сигурен ли сте, че искате да рестартирате Frigate?"
|
||||||
"button": "Рестартирай"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
{
|
{
|
||||||
"iconPicker": {
|
"iconPicker": {
|
||||||
"selectIcon": "Изберете иконка",
|
"selectIcon": "Изберете иконка"
|
||||||
"search": {
|
|
||||||
"placeholder": "Потърси за икона…"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
{
|
{
|
||||||
"button": {
|
"button": {
|
||||||
"downloadVideo": {
|
"downloadVideo": {
|
||||||
"label": "Свали видео",
|
"label": "Свали видео"
|
||||||
"toast": {
|
|
||||||
"success": "Вашето видео за преглеждане почна да се изтегля."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
{
|
{}
|
||||||
"documentTitle": "Модели за класификация"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1 @@
|
|||||||
{
|
{}
|
||||||
"documentTitle": "Настройки на конфигурацията - Фригейт",
|
|
||||||
"configEditor": "Настройки на конфигурацията"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,7 +9,5 @@
|
|||||||
"aria": "Избери събития",
|
"aria": "Избери събития",
|
||||||
"noFoundForTimePeriod": "Няма намерени събития за този времеви период."
|
"noFoundForTimePeriod": "Няма намерени събития за този времеви период."
|
||||||
},
|
},
|
||||||
"allCameras": "Всички камери",
|
"allCameras": "Всички камери"
|
||||||
"alerts": "Известия",
|
|
||||||
"detections": "Засичания"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,5 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"trackedObjectsCount_one": "{{count}} проследен обект ",
|
"trackedObjectsCount_one": "{{count}} проследен обект ",
|
||||||
"trackedObjectsCount_other": "{{count}} проследени обекта ",
|
"trackedObjectsCount_other": "{{count}} проследени обекта "
|
||||||
"documentTitle": "Разгледай - Фригейт",
|
|
||||||
"generativeAI": "Генериращ Изкъствен Интелект"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"documentTitle": "Експорт - Frigate",
|
"documentTitle": "Експорт - Frigate"
|
||||||
"search": "Търси"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,5 @@
|
|||||||
"deletedName_one": "{{count}} лице бе изтрито успешно.",
|
"deletedName_one": "{{count}} лице бе изтрито успешно.",
|
||||||
"deletedName_other": "{{count}} лица бяха изтрити успешно."
|
"deletedName_other": "{{count}} лица бяха изтрити успешно."
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"addFace": "Добавете нова колекция във библиотеката за лица при качването на първата ви снимка.",
|
|
||||||
"placeholder": "Напишете име за тази колекция"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,5 @@
|
|||||||
"cameraSettings": {
|
"cameraSettings": {
|
||||||
"cameraEnabled": "Камерата е включена"
|
"cameraEnabled": "Камерата е включена"
|
||||||
},
|
},
|
||||||
"documentTitle": "Наживо - Frigate",
|
"documentTitle": "Наживо - Frigate"
|
||||||
"documentTitle.withCamera": "{{camera}} - На живо - Фригейт"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"button": {
|
"button": {
|
||||||
"save": "Запазване на търсенето"
|
"save": "Запазване на търсенето"
|
||||||
},
|
}
|
||||||
"search": "Търси",
|
|
||||||
"savedSearches": "Запазени търсения"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,5 @@
|
|||||||
"point_one": "{{count}} точка",
|
"point_one": "{{count}} точка",
|
||||||
"point_other": "{{count}} точки"
|
"point_other": "{{count}} точки"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"documentTitle": {
|
|
||||||
"default": "Настройки - Фригейт",
|
|
||||||
"authentication": "Настройки за сигурността - Фругейт"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
{
|
{
|
||||||
"stats": {
|
"stats": {
|
||||||
"healthy": "Системата е изправна"
|
"healthy": "Системата е изправна"
|
||||||
},
|
|
||||||
"documentTitle": {
|
|
||||||
"cameras": "Статистики за Камери - Фригейт",
|
|
||||||
"storage": "Статистика за паметта - Фригейт"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,8 +58,7 @@
|
|||||||
"endTimeMustAfterStartTime": "L'hora de finalització ha de ser posterior a l'hora d'inici",
|
"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",
|
"noVaildTimeSelected": "No s'ha seleccionat un rang de temps vàlid",
|
||||||
"failed": "No s'ha pogut inciar l'exportació: {{error}}"
|
"failed": "No s'ha pogut inciar l'exportació: {{error}}"
|
||||||
},
|
}
|
||||||
"view": "Vista"
|
|
||||||
},
|
},
|
||||||
"fromTimeline": {
|
"fromTimeline": {
|
||||||
"saveExport": "Guardar exportació",
|
"saveExport": "Guardar exportació",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"documentTitle": "Models de classificació - Frigate",
|
"documentTitle": "Models de classificació",
|
||||||
"button": {
|
"button": {
|
||||||
"deleteClassificationAttempts": "Suprimeix les imatges de classificació",
|
"deleteClassificationAttempts": "Suprimeix les imatges de classificació",
|
||||||
"renameCategory": "Reanomena la classe",
|
"renameCategory": "Reanomena la classe",
|
||||||
@@ -152,12 +152,7 @@
|
|||||||
"generateSuccess": "Imatges de mostra generades amb èxit",
|
"generateSuccess": "Imatges de mostra generades amb èxit",
|
||||||
"allImagesRequired_one": "Classifiqueu totes les imatges. Queda {{count}} imatge.",
|
"allImagesRequired_one": "Classifiqueu totes les imatges. Queda {{count}} imatge.",
|
||||||
"allImagesRequired_many": "Classifiqueu totes les imatges. Queden {{count}} imatges.",
|
"allImagesRequired_many": "Classifiqueu totes les imatges. Queden {{count}} imatges.",
|
||||||
"allImagesRequired_other": "Classifiqueu totes les imatges. Queden {{count}} imatges.",
|
"allImagesRequired_other": "Classifiqueu totes les imatges. Queden {{count}} imatges."
|
||||||
"modelCreated": "El model s'ha creat correctament. Utilitzeu la vista Classificacions recents per a afegir imatges per als estats que falten i, a continuació, entrenar el model.",
|
|
||||||
"missingStatesWarning": {
|
|
||||||
"title": "Falten exemples d'estat",
|
|
||||||
"description": "Es recomana seleccionar exemples per a tots els estats per obtenir els millors resultats. Podeu continuar sense seleccionar tots els estats, però el model no serà entrenat fins que tots els estats tinguin imatges. Després de continuar, utilitzeu la vista Classificacions recents per classificar imatges per als estats que falten, i després entrenar el model."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deleteModel": {
|
"deleteModel": {
|
||||||
@@ -185,6 +180,5 @@
|
|||||||
"noNewImages": "Sense noves imatges per entrenar. Classifica més imatges primer.",
|
"noNewImages": "Sense noves imatges per entrenar. Classifica més imatges primer.",
|
||||||
"modelNotReady": "El model no está preparat per entrenar",
|
"modelNotReady": "El model no está preparat per entrenar",
|
||||||
"noChanges": "No hi ha canvis al conjunt de dades des de l'última formació."
|
"noChanges": "No hi ha canvis al conjunt de dades des de l'última formació."
|
||||||
},
|
}
|
||||||
"none": "Cap"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,9 +55,5 @@
|
|||||||
"trackedPoint": "Punt de seguiment"
|
"trackedPoint": "Punt de seguiment"
|
||||||
},
|
},
|
||||||
"zoomIn": "Amplia",
|
"zoomIn": "Amplia",
|
||||||
"zoomOut": "Redueix",
|
"zoomOut": "Redueix"
|
||||||
"normalActivity": "Normal",
|
|
||||||
"needsReview": "Necessita revisió",
|
|
||||||
"securityConcern": "Preocupació per la seguretat",
|
|
||||||
"select_all": "Tots"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,10 +220,6 @@
|
|||||||
"viewTrackingDetails": {
|
"viewTrackingDetails": {
|
||||||
"label": "Veure detalls de seguiment",
|
"label": "Veure detalls de seguiment",
|
||||||
"aria": "Mostra els detalls de seguiment"
|
"aria": "Mostra els detalls de seguiment"
|
||||||
},
|
|
||||||
"downloadCleanSnapshot": {
|
|
||||||
"label": "Descarrega la instantània neta",
|
|
||||||
"aria": "Descarrega la instantània neta"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"noTrackedObjects": "No s'han trobat objectes rastrejats",
|
"noTrackedObjects": "No s'han trobat objectes rastrejats",
|
||||||
@@ -267,8 +263,7 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"zones": "Zones",
|
"zones": "Zones",
|
||||||
"ratio": "Ràtio",
|
"ratio": "Ràtio",
|
||||||
"area": "Àrea",
|
"area": "Àrea"
|
||||||
"score": "Puntuació"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"annotationSettings": {
|
"annotationSettings": {
|
||||||
@@ -283,7 +278,7 @@
|
|||||||
"millisecondsToOffset": "Millisegons per l'òfset de detecció d'anotacions per. <em>Per defecte: 0</em>",
|
"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.",
|
"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": {
|
"toast": {
|
||||||
"success": "El desplaçament de l'anotació per {{camera}} s'ha desat al fitxer de configuració."
|
"success": "L'Òfset d'anotació per a {{camera}} s'ha desat al fitxer de configuració. Reinicieu Frigate per aplicar els canvis."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,8 +13,7 @@
|
|||||||
"train": {
|
"train": {
|
||||||
"empty": "No hi ha intents recents de reconeixement de rostres",
|
"empty": "No hi ha intents recents de reconeixement de rostres",
|
||||||
"title": "Reconeixements recents",
|
"title": "Reconeixements recents",
|
||||||
"aria": "Selecciona els reconeixements recents",
|
"aria": "Selecciona els reconeixements recents"
|
||||||
"titleShort": "Recent"
|
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"addFace": "Afegiu una col·lecció nova a la biblioteca de cares pujant la vostra primera imatge.",
|
"addFace": "Afegiu una col·lecció nova a la biblioteca de cares pujant la vostra primera imatge.",
|
||||||
|
|||||||
@@ -172,7 +172,7 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"inputPlaceHolder": "Introduïu un nom…",
|
"inputPlaceHolder": "Introduïu un nom…",
|
||||||
"title": "Nom",
|
"title": "Nom",
|
||||||
"tips": "El nom ha de tenir almenys 2 caràcters, ha de tenir almenys una lletra, i no ha de ser el nom d'una càmera o una altra zona en aquesta càmera."
|
"tips": "El nom ha de tenir almenys 2 caràcters, ha de tenir almenys una lletra, i no ha de ser el nom d'una càmera o una altra zona."
|
||||||
},
|
},
|
||||||
"label": "Zones",
|
"label": "Zones",
|
||||||
"desc": {
|
"desc": {
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
},
|
},
|
||||||
"clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.",
|
"clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.",
|
||||||
"toast": {
|
"toast": {
|
||||||
"success": "S'ha desat la zona ({{zoneName}})."
|
"success": "La zona {{zoneName}} ha estat desada. Reinicia Frigate per a aplicar els canvis."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
@@ -229,8 +229,8 @@
|
|||||||
"clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.",
|
"clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.",
|
||||||
"toast": {
|
"toast": {
|
||||||
"success": {
|
"success": {
|
||||||
"title": "{{polygonName}} s'ha desat.",
|
"title": "{{polygonName}} s'ha desat. Reinicia Frigate per a aplicar els canvis.",
|
||||||
"noName": "La màscara de moviment ha estat desada."
|
"noName": "La màscara de moviment ha estat desada. Reinicia Frigate per aplicar els canvis."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -254,8 +254,8 @@
|
|||||||
"clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.",
|
"clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.",
|
||||||
"toast": {
|
"toast": {
|
||||||
"success": {
|
"success": {
|
||||||
"title": "{{polygonName}} s'ha desat.",
|
"title": "{{polygonName}} s'ha desat. Reinicia Frigate per a aplicar els canvis.",
|
||||||
"noName": "La màscara d'objectes ha estat desada."
|
"noName": "La màscara d'objectes ha estat desada. Reincia Frigate per a aplicar els canvis."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"context": "Les màscares de filtratge d’objectes s’utilitzen per descartar falsos positius d’un tipus d’objecte concret segons la seva ubicació."
|
"context": "Les màscares de filtratge d’objectes s’utilitzen per descartar falsos positius d’un tipus d’objecte concret segons la seva ubicació."
|
||||||
@@ -527,16 +527,7 @@
|
|||||||
"notMatch": "Les contrasenyes no coincideixen",
|
"notMatch": "Les contrasenyes no coincideixen",
|
||||||
"match": "Les contrasenyes coincideixen",
|
"match": "Les contrasenyes coincideixen",
|
||||||
"placeholder": "Introdueix la contrasenya",
|
"placeholder": "Introdueix la contrasenya",
|
||||||
"title": "Contrasenya",
|
"title": "Contrasenya"
|
||||||
"show": "Mostra contrasenya",
|
|
||||||
"hide": "Amaga contrasenya",
|
|
||||||
"requirements": {
|
|
||||||
"title": "Requisits contrasenya:",
|
|
||||||
"length": "Com a mínim 8 carácters",
|
|
||||||
"uppercase": "Com a mínim una majúscula",
|
|
||||||
"digit": "Com a mínim un digit",
|
|
||||||
"special": "Com a mínim un carácter especial (!@#$%^&*(),.?\":{}|<>)"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"newPassword": {
|
"newPassword": {
|
||||||
"title": "Nova contrasenya",
|
"title": "Nova contrasenya",
|
||||||
@@ -546,23 +537,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"usernameIsRequired": "El nom d'usuari és obligatori",
|
"usernameIsRequired": "El nom d'usuari és obligatori",
|
||||||
"passwordIsRequired": "La contrasenya és obligatoria",
|
"passwordIsRequired": "La contrasenya és obligatoria"
|
||||||
"currentPassword": {
|
|
||||||
"title": "Constrasenya actual",
|
|
||||||
"placeholder": "Entra l'actual contrasenya"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"passwordSetting": {
|
"passwordSetting": {
|
||||||
"updatePassword": "Contrasenya actualitzada per {{username}}",
|
"updatePassword": "Contrasenya actualitzada per {{username}}",
|
||||||
"setPassword": "Estableix Contrasenya",
|
"setPassword": "Estableix Contrasenya",
|
||||||
"cannotBeEmpty": "La contrasenya no pot ser buida",
|
"cannotBeEmpty": "La contrasenya no pot ser buida",
|
||||||
"doNotMatch": "Les contrasenyes no coincideixen",
|
"doNotMatch": "Les contrasenyes no coincideixen",
|
||||||
"desc": "Crea un nova contrasenya segura per protegir aquest compte.",
|
"desc": "Crea un nova contrasenya segura per protegir aquest compte."
|
||||||
"currentPasswordRequired": "L'actual contrasenya es requerida",
|
|
||||||
"incorrectCurrentPassword": "L'actual contrasenya es incorrecte",
|
|
||||||
"passwordVerificationFailed": "Falla en la verificació de la contrasenya",
|
|
||||||
"multiDeviceWarning": "Serà necesari loguejarte en qualsevol altre dispositiu en que estiguis loguejat en {{refresh_time}}.",
|
|
||||||
"multiDeviceAdmin": "També pots forçar a tots els usuaris a tornar a autenticar-se immediatament rotant el teu secret JWT."
|
|
||||||
},
|
},
|
||||||
"deleteUser": {
|
"deleteUser": {
|
||||||
"title": "Suprimir usuari",
|
"title": "Suprimir usuari",
|
||||||
@@ -1156,8 +1138,7 @@
|
|||||||
"audioCodecRequired": "Es requereix un flux d'àudio per admetre la detecció d'àudio.",
|
"audioCodecRequired": "Es requereix un flux d'àudio per admetre la detecció d'àudio.",
|
||||||
"restreamingWarning": "Reduir les connexions a la càmera per al flux de registre pot augmentar lleugerament l'ús de la CPU.",
|
"restreamingWarning": "Reduir les connexions a la càmera per al flux de registre pot augmentar lleugerament l'ús de la CPU.",
|
||||||
"brands": {
|
"brands": {
|
||||||
"reolink-rtsp": "No és racomana utilitzar Reolink RSTP. Activeu HTTP a la configuració del microprogramari de la càmera i reinicieu l'assistent.",
|
"reolink-rtsp": "No és racomana utilitzar Reolink RSTP. Activeu HTTP a la configuració del microprogramari de la càmera i reinicieu l'assistent."
|
||||||
"reolink-http": "Els fluxos HTTP de reenllaç haurien d'utilitzar FFmpeg per a una millor compatibilitat. Habilita «Utilitza el mode de compatibilitat del flux» per a aquest flux."
|
|
||||||
},
|
},
|
||||||
"dahua": {
|
"dahua": {
|
||||||
"substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Dahua / Amcrest / EmpireTech suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles."
|
"substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Dahua / Amcrest / EmpireTech suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles."
|
||||||
|
|||||||
@@ -25,10 +25,7 @@
|
|||||||
"deletedModel_one": "Úspešne odstranený {{count}} model",
|
"deletedModel_one": "Úspešne odstranený {{count}} model",
|
||||||
"deletedModel_few": "Úspešne odstranené {{count}} modely",
|
"deletedModel_few": "Úspešne odstranené {{count}} modely",
|
||||||
"deletedModel_other": "Úspěšne ostranených {{count}} modelov",
|
"deletedModel_other": "Úspěšne ostranených {{count}} modelov",
|
||||||
"deletedCategory": "Zmazať triedu",
|
"deletedCategory": "Zmazať triedu"
|
||||||
"categorizedImage": "Obrázek úspěšně klasifikován",
|
|
||||||
"trainedModel": "Úspěšně vytrénovaný model.",
|
|
||||||
"trainingModel": "Trénování modelu bylo úspěšně zahájeno."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,6 @@
|
|||||||
"zoomOut": "Oddálit",
|
"zoomOut": "Oddálit",
|
||||||
"detail": {
|
"detail": {
|
||||||
"label": "Detail",
|
"label": "Detail",
|
||||||
"noDataFound": "Žádná detailní data k prohlédnutí",
|
"noDataFound": "Žádná detailní data k prohlédnutí"
|
||||||
"aria": "Přepnout detailní zobrazení",
|
|
||||||
"trackedObject_other": "{{count}} objektů"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
"desc": "Skutečně chcete vymazat kolekci {{name}}? Toto trvale vymaže všechny přiřazené obličeje."
|
"desc": "Skutečně chcete vymazat kolekci {{name}}? Toto trvale vymaže všechny přiřazené obličeje."
|
||||||
},
|
},
|
||||||
"train": {
|
"train": {
|
||||||
"title": "Nedávná rozpoznání",
|
"title": "Trénovat",
|
||||||
"empty": "Nejsou zde žádné předchozí pokusy o rozpoznání obličeje",
|
"empty": "Nejsou zde žádné předchozí pokusy o rozpoznání obličeje",
|
||||||
"aria": "Vybrat trénink"
|
"aria": "Vybrat trénink"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"masksAndZones": "Editor masky a zón - Frigate",
|
"masksAndZones": "Editor masky a zón - Frigate",
|
||||||
"motionTuner": "Ladění detekce pohybu - Frigate",
|
"motionTuner": "Ladění detekce pohybu - Frigate",
|
||||||
"object": "Ladění - Frigate",
|
"object": "Ladění - Frigate",
|
||||||
"general": "Nastavení rozhraní- Frigate",
|
"general": "Obecné nastavení - Frigate",
|
||||||
"frigatePlus": "Frigate+ nastavení - Frigate",
|
"frigatePlus": "Frigate+ nastavení - Frigate",
|
||||||
"enrichments": "Nastavení obohacení - Frigate",
|
"enrichments": "Nastavení obohacení - Frigate",
|
||||||
"cameraManagement": "Správa kamer - Frigate",
|
"cameraManagement": "Správa kamer - Frigate",
|
||||||
@@ -307,9 +307,7 @@
|
|||||||
"frigateplus": "Frigate+",
|
"frigateplus": "Frigate+",
|
||||||
"enrichments": "Obohacení",
|
"enrichments": "Obohacení",
|
||||||
"triggers": "Spouštěče",
|
"triggers": "Spouštěče",
|
||||||
"cameraManagement": "Správa",
|
"cameraManagement": "Správa"
|
||||||
"cameraReview": "Kontrola",
|
|
||||||
"roles": "Role"
|
|
||||||
},
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"unsavedChanges": {
|
"unsavedChanges": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"time": {
|
"time": {
|
||||||
"untilForTime": "Indtil {{time}}",
|
"untilForTime": "Indtil{{time}}",
|
||||||
"untilForRestart": "Indtil Frigate genstarter.",
|
"untilForRestart": "Indtil Frigate genstarter.",
|
||||||
"untilRestart": "Indtil genstart",
|
"untilRestart": "Indtil genstart",
|
||||||
"ago": "{{timeAgo}} siden",
|
"ago": "{{timeAgo}} siden",
|
||||||
|
|||||||
@@ -15,36 +15,5 @@
|
|||||||
},
|
},
|
||||||
"count_one": "{{count}} Label",
|
"count_one": "{{count}} Label",
|
||||||
"label": "Etiketter"
|
"label": "Etiketter"
|
||||||
},
|
|
||||||
"zones": {
|
|
||||||
"label": "Zoner",
|
|
||||||
"all": {
|
|
||||||
"title": "Alle zoner",
|
|
||||||
"short": "Zoner"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"more": "Flere filtre",
|
|
||||||
"sort": {
|
|
||||||
"label": "Sortér",
|
|
||||||
"dateAsc": "Dato (Stigende)",
|
|
||||||
"dateDesc": "Dato (Faldende)",
|
|
||||||
"speedAsc": "Anslået hastighed (Stigende)",
|
|
||||||
"speedDesc": "Anslået hastighed (Faldende)",
|
|
||||||
"relevance": "Relevans"
|
|
||||||
},
|
|
||||||
"dates": {
|
|
||||||
"selectPreset": "Vælg en forudindstilling…",
|
|
||||||
"all": {
|
|
||||||
"title": "Alle datoer",
|
|
||||||
"short": "Datoer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reset": {
|
|
||||||
"label": "Nulstille filtre til standardværdier"
|
|
||||||
},
|
|
||||||
"timeRange": "Tidsinterval",
|
|
||||||
"estimatedSpeed": "Anslået hastighed ({{unit}})",
|
|
||||||
"features": {
|
|
||||||
"hasVideoClip": "Har et videoklip"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,7 +296,7 @@
|
|||||||
"doorbell": "Türklingel",
|
"doorbell": "Türklingel",
|
||||||
"ding-dong": "BimBam",
|
"ding-dong": "BimBam",
|
||||||
"sliding_door": "Schiebetür",
|
"sliding_door": "Schiebetür",
|
||||||
"slam": "zuknallen",
|
"slam": "Knall",
|
||||||
"knock": "Klopfen",
|
"knock": "Klopfen",
|
||||||
"tap": "Schlag",
|
"tap": "Schlag",
|
||||||
"squeak": "Quietschen",
|
"squeak": "Quietschen",
|
||||||
@@ -355,7 +355,7 @@
|
|||||||
"shatter": "Zerspringen",
|
"shatter": "Zerspringen",
|
||||||
"silence": "Stille",
|
"silence": "Stille",
|
||||||
"environmental_noise": "Umgebungsgeräusch",
|
"environmental_noise": "Umgebungsgeräusch",
|
||||||
"static": "Statisch",
|
"static": "Rauschen",
|
||||||
"pink_noise": "Rosa Rauschen",
|
"pink_noise": "Rosa Rauschen",
|
||||||
"television": "Fernsehgerät",
|
"television": "Fernsehgerät",
|
||||||
"radio": "Radio",
|
"radio": "Radio",
|
||||||
@@ -441,63 +441,5 @@
|
|||||||
"arrow": "Pfeil",
|
"arrow": "Pfeil",
|
||||||
"electronic_tuner": "Elektronischer Tuner",
|
"electronic_tuner": "Elektronischer Tuner",
|
||||||
"effects_unit": "Effekteinheit",
|
"effects_unit": "Effekteinheit",
|
||||||
"chorus_effect": "Chorus-Effekt",
|
"chorus_effect": "Chorus-Effekt"
|
||||||
"sodeling": "Verfilzen",
|
|
||||||
"chird": "Akkord",
|
|
||||||
"change_ringing": "Wechsle RingRing",
|
|
||||||
"shofar": "Schofar",
|
|
||||||
"gush": "sprudeln",
|
|
||||||
"sonar": "Sonar",
|
|
||||||
"whoosh": "Rauschen",
|
|
||||||
"thump": "Ruck",
|
|
||||||
"basketball_bounce": "Basketball Abbraller",
|
|
||||||
"bang": "Knall",
|
|
||||||
"slap": "Ohrfeige",
|
|
||||||
"whack": "verhauen",
|
|
||||||
"smash": "zerschlagen",
|
|
||||||
"breaking": "zerbrechen",
|
|
||||||
"bouncing": "Abbraller",
|
|
||||||
"whip": "Peitsche",
|
|
||||||
"flap": "Lasche",
|
|
||||||
"scratch": "Kratzer",
|
|
||||||
"scrape": "Abfall",
|
|
||||||
"rub": "scheuern",
|
|
||||||
"roll": "rollen",
|
|
||||||
"crushing": "Stauchen",
|
|
||||||
"crumpling": "zerknüllen",
|
|
||||||
"tearing": "Reißen",
|
|
||||||
"beep": "Piep",
|
|
||||||
"ping": "Ping",
|
|
||||||
"ding": "klingeln",
|
|
||||||
"thunk": "dumpfes Geräusch",
|
|
||||||
"clang": "Geklirr",
|
|
||||||
"squeal": "Ausruf",
|
|
||||||
"creak": "Knarren",
|
|
||||||
"rustle": "Geknister",
|
|
||||||
"whir": "schwirren",
|
|
||||||
"clatter": "Geratter",
|
|
||||||
"sizzle": "brutzeln",
|
|
||||||
"clicking": "Klicken",
|
|
||||||
"clickety_clack": "Klappergeräuschen",
|
|
||||||
"rumble": "Grollen",
|
|
||||||
"plop": "plumpsen",
|
|
||||||
"hum": "Brummen",
|
|
||||||
"zing": "Schwung",
|
|
||||||
"boing": "ferderndes Geräusch",
|
|
||||||
"crunch": "knirschendes",
|
|
||||||
"sine_wave": "Sinus Kurve",
|
|
||||||
"harmonic": "harmonisch",
|
|
||||||
"chirp_tone": "Frequenzwobbelung",
|
|
||||||
"pulse": "Takt",
|
|
||||||
"inside": "drinnen",
|
|
||||||
"outside": "draußen",
|
|
||||||
"reverberation": "Widerhall",
|
|
||||||
"echo": "Echo",
|
|
||||||
"noise": "Lärm",
|
|
||||||
"mains_hum": "Netzbrummen",
|
|
||||||
"distortion": "Verzerrung",
|
|
||||||
"sidetone": "Nebengeräusch",
|
|
||||||
"cacophony": "Dissonanz",
|
|
||||||
"throbbing": "Pochen",
|
|
||||||
"vibration": "Vibration"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,10 +81,7 @@
|
|||||||
"formattedTimestampMonthDayYear": {
|
"formattedTimestampMonthDayYear": {
|
||||||
"12hour": "d. MMM yyyy",
|
"12hour": "d. MMM yyyy",
|
||||||
"24hour": "d. MMM yyyy"
|
"24hour": "d. MMM yyyy"
|
||||||
},
|
}
|
||||||
"inProgress": "In Bearbeitung",
|
|
||||||
"invalidStartTime": "Ungültige Startzeit",
|
|
||||||
"invalidEndTime": "Ungültige Endzeit"
|
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
@@ -121,8 +118,7 @@
|
|||||||
"pictureInPicture": "Bild in Bild",
|
"pictureInPicture": "Bild in Bild",
|
||||||
"on": "AN",
|
"on": "AN",
|
||||||
"suspended": "Pausierte",
|
"suspended": "Pausierte",
|
||||||
"unsuspended": "fortsetzen",
|
"unsuspended": "fortsetzen"
|
||||||
"continue": "Weiter"
|
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"back": "Zurück",
|
"back": "Zurück",
|
||||||
@@ -231,8 +227,7 @@
|
|||||||
"logout": "Abmelden"
|
"logout": "Abmelden"
|
||||||
},
|
},
|
||||||
"uiPlayground": "Testgebiet für Benutzeroberfläche",
|
"uiPlayground": "Testgebiet für Benutzeroberfläche",
|
||||||
"export": "Exportieren",
|
"export": "Exportieren"
|
||||||
"classification": "Klassifizierung"
|
|
||||||
},
|
},
|
||||||
"unit": {
|
"unit": {
|
||||||
"speed": {
|
"speed": {
|
||||||
|
|||||||
@@ -11,6 +11,6 @@
|
|||||||
},
|
},
|
||||||
"user": "Benutzername",
|
"user": "Benutzername",
|
||||||
"password": "Kennwort",
|
"password": "Kennwort",
|
||||||
"firstTimeLogin": "Ist dies der erste Loginversuch? Die Zugangsdaten werden in den Frigate Logs angezeigt."
|
"firstTimeLogin": "Versuchen Sie sich zum ersten Mal anzumelden? Die Anmeldedaten sind in den Frigate-Logs aufgeführt."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,8 +66,7 @@
|
|||||||
"failed": "Fehler beim Starten des Exports: {{error}}",
|
"failed": "Fehler beim Starten des Exports: {{error}}",
|
||||||
"noVaildTimeSelected": "Kein gültiger Zeitraum ausgewählt"
|
"noVaildTimeSelected": "Kein gültiger Zeitraum ausgewählt"
|
||||||
},
|
},
|
||||||
"success": "Export erfolgreich gestartet. Die Datei befindet sich auf der Exportseite.",
|
"success": "Export erfolgreich gestartet. Die Datei befindet sich auf der Exportseite."
|
||||||
"view": "Ansicht"
|
|
||||||
},
|
},
|
||||||
"fromTimeline": {
|
"fromTimeline": {
|
||||||
"saveExport": "Export speichern",
|
"saveExport": "Export speichern",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user