Compare commits

..
Author SHA1 Message Date
Nicolas Mowen ff7b324f5c Optimize review summaries for consistency 2026-11-01 08:38:02 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0f00cae567 Bump follow-redirects (#24847)
CI / AMD64 Build (push) Waiting to run
CI / AMD64 Smoke Test (push) Blocked by required conditions
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
Bumps the web-security group with 1 update in the /web directory: [follow-redirects](https://github.com/follow-redirects/follow-redirects).


Updates `follow-redirects` from 1.16.1 to 1.16.2
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.16.1...v1.16.2)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-version: 1.16.2
  dependency-type: indirect
  dependency-group: web-security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 08:07:57 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Josh Hawkins
63c12cf135 Bump the web-minor-patch group in /web with 18 updates (#24844)
* Bump the web-minor-patch group in /web with 18 updates

Bumps the web-minor-patch group in /web with 18 updates:

| Package | From | To |
| --- | --- | --- |
| [@cycjimmy/jsmpeg-player](https://github.com/cycjimmy/jsmpeg-player) | `6.1.2` | `6.1.6` |
| [@hookform/resolvers](https://github.com/react-hook-form/resolvers) | `6.0.0` | `6.0.2` |
| [apexcharts](https://github.com/apexcharts/apexcharts.js) | `8.4.1` | `8.5.0` |
| [axios](https://github.com/axios/axios) | `1.20.0` | `1.21.0` |
| [framer-motion](https://github.com/motiondivision/motion) | `14.6.0` | `14.7.1` |
| [hls.js](https://github.com/video-dev/hls.js) | `1.7.3` | `1.7.4` |
| [i18next](https://github.com/i18next/i18next) | `26.5.5` | `26.5.11` |
| [i18next-http-backend](https://github.com/i18next/i18next-http-backend) | `4.0.4` | `4.0.6` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.61.0` | `1.64.0` |
| [react-apexcharts](https://github.com/apexcharts/react-apexcharts) | `2.1.1` | `2.1.2` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.90.0` | `7.91.0` |
| [react-i18next](https://github.com/i18next/react-i18next) | `17.1.1` | `17.1.9` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.18.4` | `7.18.5` |
| [virtua](https://github.com/inokawa/virtua) | `0.53.6` | `0.54.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.6.5` | `26.6.6` |
| [autoprefixer](https://github.com/postcss/autoprefixer) | `10.6.1` | `10.6.2` |
| [postcss](https://github.com/postcss/postcss) | `8.5.31` | `8.5.32` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.72.0` | `8.72.1` |


Updates `@cycjimmy/jsmpeg-player` from 6.1.2 to 6.1.6
- [Release notes](https://github.com/cycjimmy/jsmpeg-player/releases)
- [Changelog](https://github.com/cycjimmy/jsmpeg-player/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/cycjimmy/jsmpeg-player/compare/v6.1.2...v6.1.6)

Updates `@hookform/resolvers` from 6.0.0 to 6.0.2
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v6.0.0...v6.0.2)

Updates `apexcharts` from 8.4.1 to 8.5.0
- [Release notes](https://github.com/apexcharts/apexcharts.js/releases)
- [Commits](https://github.com/apexcharts/apexcharts.js/compare/v8.4.1...v8.5.0)

Updates `axios` from 1.20.0 to 1.21.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.20.0...v1.21.0)

Updates `framer-motion` from 14.6.0 to 14.7.1
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v14.6.0...v14.7.1)

Updates `hls.js` from 1.7.3 to 1.7.4
- [Release notes](https://github.com/video-dev/hls.js/releases)
- [Changelog](https://github.com/video-dev/hls.js/blob/master/docs/release-process.md)
- [Commits](https://github.com/video-dev/hls.js/compare/v1.7.3...v1.7.4)

Updates `i18next` from 26.5.5 to 26.5.11
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.5.5...v26.5.11)

Updates `i18next-http-backend` from 4.0.4 to 4.0.6
- [Changelog](https://github.com/i18next/i18next-http-backend/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-http-backend/compare/v4.0.4...v4.0.6)

Updates `lucide-react` from 1.61.0 to 1.64.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.64.0/packages/lucide-react)

Updates `react-apexcharts` from 2.1.1 to 2.1.2
- [Release notes](https://github.com/apexcharts/react-apexcharts/releases)
- [Commits](https://github.com/apexcharts/react-apexcharts/commits)

Updates `react-hook-form` from 7.90.0 to 7.91.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.90.0...v7.91.0)

Updates `react-i18next` from 17.1.1 to 17.1.9
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.1.1...v17.1.9)

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

Updates `virtua` from 0.53.6 to 0.54.0
- [Release notes](https://github.com/inokawa/virtua/releases)
- [Commits](https://github.com/inokawa/virtua/compare/0.53.6...0.54.0)

Updates `@types/node` from 26.6.5 to 26.6.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `autoprefixer` from 10.6.1 to 10.6.2
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.6.1...10.6.2)

Updates `postcss` from 8.5.31 to 8.5.32
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.31...8.5.32)

Updates `typescript-eslint` from 8.72.0 to 8.72.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.72.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@cycjimmy/jsmpeg-player"
  dependency-version: 6.1.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: "@hookform/resolvers"
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: apexcharts
  dependency-version: 8.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
- dependency-name: axios
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
- dependency-name: framer-motion
  dependency-version: 14.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
- dependency-name: hls.js
  dependency-version: 1.7.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: i18next
  dependency-version: 26.5.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: i18next-http-backend
  dependency-version: 4.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: lucide-react
  dependency-version: 1.64.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
- dependency-name: react-apexcharts
  dependency-version: 2.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: react-hook-form
  dependency-version: 7.91.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
- dependency-name: react-i18next
  dependency-version: 17.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: react-router-dom
  dependency-version: 7.18.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: virtua
  dependency-version: 0.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
- dependency-name: "@types/node"
  dependency-version: 26.6.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: autoprefixer
  dependency-version: 10.6.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: postcss
  dependency-version: 8.5.32
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: typescript-eslint
  dependency-version: 8.72.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Wait for wizard requests in setup specs

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2026-11-01 07:55:52 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Josh Hawkins
e82dc7ad13 Update google-genai requirement from ==2.32.* to ==2.34.* in /docker/main in the pydantic-genai group (#24839)
* Update google-genai requirement

Updates the requirements on [google-genai](https://github.com/googleapis/python-genai) to permit the latest version.

Updates `google-genai` to 2.34.0
- [Release notes](https://github.com/googleapis/python-genai/releases)
- [Changelog](https://github.com/googleapis/python-genai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-genai/compare/v2.32.0...v2.34.0)

---
updated-dependencies:
- dependency-name: google-genai
  dependency-version: 2.34.0
  dependency-type: direct:production
  dependency-group: pydantic-genai
...

Signed-off-by: dependabot[bot] <support@github.com>

* Remove type ignores google-genai 2.34 no longer needs

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2026-11-01 07:55:33 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
689a5b59ac Update sherpa-onnx requirement from ==1.13.* to ==2.0.* in /docker/main (#24842)
---
updated-dependencies:
- dependency-name: sherpa-onnx
  dependency-version: 2.0.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 07:41:48 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
69061d17f1 Update transformers requirement in /docker/main (#24841)
Updates the requirements on [transformers](https://github.com/huggingface/transformers) to permit the latest version.
- [Release notes](https://github.com/huggingface/transformers/releases)
- [Commits](https://github.com/huggingface/transformers/compare/v5.20.0...v5.21.1)

---
updated-dependencies:
- dependency-name: transformers
  dependency-version: 5.21.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 07:41:40 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
efb890e6e0 Bump the pip-main-minor-patch group in /docker/main with 3 updates (#24840)
Bumps the pip-main-minor-patch group in /docker/main with 3 updates: [ruff](https://github.com/astral-sh/ruff), [mypy](https://github.com/python/mypy) and [tzdata](https://github.com/python/tzdata).


Updates `ruff` from 0.17.1 to 0.17.3
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.17.1...0.17.3)

Updates `mypy` from 2.4.0 to 2.4.1
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v2.4.0...v2.4.1)

Updates `tzdata` from 2026.5 to 2026.6
- [Release notes](https://github.com/python/tzdata/releases)
- [Changelog](https://github.com/python/tzdata/blob/master/NEWS.md)
- [Commits](https://github.com/python/tzdata/compare/2026.5...2026.6)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.17.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: pip-main-minor-patch
- dependency-name: mypy
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pip-main-minor-patch
- dependency-name: tzdata
  dependency-version: '2026.6'
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: pip-main-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 07:41:30 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ea7b3e2336 Bump the actions group across 2 directories with 1 update (#24845)
Bumps the actions group with 1 update in the / directory: [docker/login-action](https://github.com/docker/login-action).
Bumps the actions group with 1 update in the /.github/actions/setup directory: [docker/login-action](https://github.com/docker/login-action).


Updates `docker/login-action` from 4.6.0 to 4.7.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/dbcb813823bdd20940b903addbd779551569679f...81c6f93d4bc991e510b342a00b419b3bb4efc5c6)

Updates `docker/login-action` from 4.6.0 to 4.7.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/dbcb813823bdd20940b903addbd779551569679f...81c6f93d4bc991e510b342a00b419b3bb4efc5c6)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/login-action
  dependency-version: 4.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 07:41:08 -06:00
37 changed files with 492 additions and 389 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ runs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to the Container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
with:
registry: ghcr.io
username: ${{ github.actor }}
-3
View File
@@ -55,9 +55,6 @@ updates:
# Needs Python 3.12 and numpy 2
- dependency-name: "librosa"
versions: [">=1.0"]
# pandas 3.1 requires numpy 2
- dependency-name: "pandas"
versions: [">=3.1"]
# Needs Python 3.12
- dependency-name: "scipy"
versions: [">=1.18"]
+1 -1
View File
@@ -590,7 +590,7 @@ jobs:
with:
string: ${{ github.repository }}
- name: Log in to the Container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
with:
registry: ghcr.io
username: ${{ github.actor }}
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
with:
string: ${{ github.repository }}
- name: Log in to the Container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
with:
registry: ghcr.io
username: ${{ github.actor }}
+1 -1
View File
@@ -1,4 +1,4 @@
ruff == 0.17.1
ruff == 0.17.3
# types
types-peewee == 4.0.*
+5 -5
View File
@@ -16,7 +16,7 @@ python-multipart == 0.0.32
tensorflow == 2.19.* ; platform_machine == 'aarch64'
tensorflow-cpu == 2.21.* ; platform_machine == 'x86_64'
# General
mypy == 2.4.0
mypy == 2.4.1
onvif-zeep-async == 4.3.*
paho-mqtt == 2.1.*
pandas == 3.0.*
@@ -25,7 +25,7 @@ peewee_migrate == 2.3.*
psutil == 7.2.*
pydantic == 2.14.*
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
tzdata == 2026.5
tzdata == 2026.6
pyzmq == 27.2.*
ruamel.yaml == 0.19.*
tzlocal == 5.4.5
@@ -45,9 +45,9 @@ scipy == 1.17.*
openvino == 2026.4.*
onnxruntime == 1.31.*
# Embeddings
transformers == 5.20.*
transformers == 5.21.*
# Generative AI
google-genai == 2.32.*
google-genai == 2.34.*
ollama == 0.6.*
openai == 3.36.*
# push notifications
@@ -68,7 +68,7 @@ prometheus-client == 0.26.*
tflite_runtime @ https://github.com/frigate-nvr/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_x86_64.whl; platform_machine == 'x86_64'
tflite_runtime @ https://github.com/feranick/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_aarch64.whl; platform_machine == 'aarch64'
# audio transcription
sherpa-onnx==1.13.*
sherpa-onnx==2.0.*
faster-whisper==1.2.*
librosa==0.11.*
soundfile==0.14.*
-2
View File
@@ -6371,7 +6371,6 @@ paths:
required: false
schema:
type: integer
minimum: 0
description: Padding to apply to the vod.
default: 0
title: Padding
@@ -7577,7 +7576,6 @@ paths:
required: false
schema:
type: integer
minimum: 1
default: 30
title: Scale
responses:
-4
View File
@@ -966,10 +966,6 @@ def login(request: Request, body: AppPostLoginBody):
try:
db_user: User = User.get_by_id(user)
except DoesNotExist:
# Hash anyway so response timing doesn't reveal which usernames exist
hash_password(
password, iterations=request.app.frigate_config.auth.hash_iterations
)
logger.warning(f"Login failed for unknown user '{user}' from {remote_addr}")
failed_logins.record(user, time.time())
return JSONResponse(content={"message": "Login failed"}, status_code=401)
@@ -1,4 +1,4 @@
from pydantic import BaseModel, Field
from pydantic import BaseModel
from pydantic.json_schema import SkipJsonSchema
@@ -11,7 +11,7 @@ class MediaRecordingsAvailabilityQueryParams(BaseModel):
cameras: str = "all"
before: float | SkipJsonSchema[None] = None
after: float | SkipJsonSchema[None] = None
scale: int = Field(default=30, ge=1)
scale: int = 30
class RecordingsDeleteQueryParams(BaseModel):
+4 -4
View File
@@ -291,12 +291,12 @@ def events(
clauses.append(
reduce(
operator.or_,
[(start_hour_fun >= time_after), (start_hour_fun < time_before)],
[(start_hour_fun > time_after), (start_hour_fun < time_before)],
)
)
# all other cases should be and operator
else:
clauses.append(start_hour_fun >= time_after)
clauses.append(start_hour_fun > time_after)
clauses.append(start_hour_fun < time_before)
if has_clip is not None:
@@ -749,12 +749,12 @@ def events_search(
event_filters.append(
reduce(
operator.or_,
[(start_hour_fun >= time_after), (start_hour_fun < time_before)],
[(start_hour_fun > time_after), (start_hour_fun < time_before)],
)
)
# all other cases should be and operator
else:
event_filters.append(start_hour_fun >= time_after)
event_filters.append(start_hour_fun > time_after)
event_filters.append(start_hour_fun < time_before)
# Perform semantic search
+1 -1
View File
@@ -1227,7 +1227,7 @@ def bulk_delete_exports(body: ExportBulkDeleteBody):
files_in_use = _get_files_in_use()
for export in exports:
if export.in_progress or export.video_path.split("/")[-1] in files_in_use:
if export.video_path.split("/")[-1] in files_in_use:
return JSONResponse(
content={
"success": False,
+5 -9
View File
@@ -862,7 +862,7 @@ async def vod_hour(
async def vod_event(
request: Request,
event_id: str,
padding: int = Query(0, ge=0, description="Padding to apply to the vod."),
padding: int = Query(0, description="Padding to apply to the vod."),
):
try:
event: Event = Event.get(Event.id == event_id)
@@ -1521,7 +1521,6 @@ async def preview_gif(
| ((start_ts > Previews.start_time) & (end_ts < Previews.end_time))
)
.where(Previews.camera == camera_name)
.order_by(Previews.start_time.asc())
.limit(1)
.get()
)
@@ -1531,8 +1530,7 @@ async def preview_gif(
status_code=404,
)
clip_start = max(start_ts, preview.start_time)
diff = clip_start - preview.start_time
diff = start_ts - preview.start_time
minutes = int(diff / 60)
seconds = int(diff % 60)
config: FrigateConfig = request.app.frigate_config
@@ -1544,7 +1542,7 @@ async def preview_gif(
"-ss",
f"00:{minutes}:{seconds}",
"-t",
f"{end_ts - clip_start}",
f"{end_ts - start_ts}",
"-i",
preview.path,
"-r",
@@ -1711,7 +1709,6 @@ async def preview_mp4(
| ((start_ts > Previews.start_time) & (end_ts < Previews.end_time))
)
.where(Previews.camera == camera_name)
.order_by(Previews.start_time.asc())
.limit(1)
.get()
)
@@ -1724,8 +1721,7 @@ async def preview_mp4(
status_code=404,
)
clip_start = max(start_ts, preview.start_time)
diff = clip_start - preview.start_time
diff = start_ts - preview.start_time
minutes = int(diff / 60)
seconds = int(diff % 60)
config: FrigateConfig = request.app.frigate_config
@@ -1738,7 +1734,7 @@ async def preview_mp4(
"-ss",
f"00:{minutes}:{seconds}",
"-t",
f"{end_ts - clip_start}",
f"{end_ts - start_ts}",
"-i",
preview.path,
"-r",
+2 -1
View File
@@ -163,7 +163,8 @@ async def review(
# Apply ordering and limit
review_query = (
review_query.order_by(ReviewSegment.start_time.desc())
review_query.order_by(ReviewSegment.severity.asc())
.order_by(ReviewSegment.start_time.desc())
.limit(limit)
.dicts()
.iterator()
@@ -12,6 +12,8 @@ import math
from collections.abc import Sequence
from typing import Any
from titlecase import titlecase
from frigate.models import Event, Timeline
logger = logging.getLogger(__name__)
@@ -84,12 +86,16 @@ def event_name(event: dict[str, Any]) -> str:
tracked object whenever a subject is re-detected, so the tracking data
cannot say whether two entries are the same subject, and the notes stay
ambiguous rather than implying either answer.
Names are formatted the same way as in the prompt's "Objects in Scene"
list. Small models otherwise copy the raw lowercase sub label into their
response instead of the name they were told to use.
"""
label = str(event["label"]).replace("_", " ").replace("-verified", "")
sub_label = event.get("sub_label")
if sub_label:
return f'{label} "{sub_label}"'
return f'{label} "{titlecase(str(sub_label).replace("_", " "))}"'
article = "an" if label[:1].lower() in "aeiou" else "a"
return f"{article} {label}"
@@ -213,6 +219,7 @@ def build_timeline(
events: list[dict[str, Any]],
span_end: float,
state_changes: Sequence[dict[str, Any]] = (),
span_start: float | None = None,
) -> list[tuple[float, str]]:
"""All annotated moments across every event, in time order.
@@ -222,6 +229,12 @@ def build_timeline(
describe nothing the model can see. A track ending means the object
stopped being detected, which may or may not mean it left the frame.
`span_start` is the timestamp of the first sampled frame. An object
tracked since before it gets a single "already in view" note instead of
its earlier history: those moments would otherwise land on the first
frame and read as happening during the clip, turning a car that parked
minutes ago into one pulling in.
`state_changes` are timeline rows (timestamp, source_id, class_type); the
stationary and active ones become "has stopped moving" / "starts moving
again". Frigate only marks an object stationary after it has been still
@@ -249,6 +262,13 @@ def build_timeline(
if event["start_time"] > span_end:
continue
if (
span_start is not None
and event["end_time"]
and event["end_time"] < span_start
):
continue
points = path_points(event.get("path_data") or [])
legs = path_legs(points) if len(points) >= 2 else []
name = event_name(event)
@@ -259,8 +279,33 @@ def build_timeline(
)
remaining = list(enumerate(legs))
moments: list[tuple[float, str]] = []
event_changes = sorted(
(timestamp, phrase)
for timestamp, phrase in changes_by_event.get(event["id"], [])
if timestamp >= detected_at
)
if merges:
if span_start is not None and detected_at < span_start:
# Only whether it had come to rest is noted; where it is and which
# way it is heading are visible in the first frame.
earlier = [phrase for t, phrase in event_changes if t < span_start]
not_moving = (
bool(earlier) and earlier[-1] == STATE_CHANGE_PHRASES["stationary"]
)
timeline.append(
(
span_start,
f"{name} already in view when the sequence begins"
+ (", not moving" if not_moving else ""),
)
)
remaining = [
(index, leg)
for index, leg in remaining
if leg_start_time(points, leg) >= span_start
]
event_changes = [(t, p) for t, p in event_changes if t >= span_start]
elif merges:
heading = leg_heading(points, legs[0])
timeline.append(
(detected_at, f"{name} first detected at {where}, moving {heading}")
@@ -277,11 +322,7 @@ def build_timeline(
)
)
moments.extend(
(timestamp, phrase)
for timestamp, phrase in changes_by_event.get(event["id"], [])
if timestamp >= detected_at
)
moments.extend(event_changes)
for timestamp, phrase in moments:
if timestamp <= span_end:
@@ -390,7 +431,10 @@ def build_frame_captions(
timeline.extend(
(timestamp, f"[tracker] {note}")
for timestamp, note in build_timeline(
events, span_end, get_state_changes(detection_ids)
events,
span_end,
get_state_changes(detection_ids),
span_start=frame_times[0],
)
)
+2 -2
View File
@@ -386,7 +386,7 @@ class GeminiClient(GenAIClient):
response = self.provider.models.generate_content(
model=self.genai_config.model,
contents=gemini_messages, # type: ignore[arg-type]
contents=gemini_messages,
config=types.GenerateContentConfig(**config_params),
)
@@ -648,7 +648,7 @@ class GeminiClient(GenAIClient):
stream = await self.provider.aio.models.generate_content_stream(
model=self.genai_config.model,
contents=gemini_messages, # type: ignore[arg-type]
contents=gemini_messages,
config=types.GenerateContentConfig(**config_params),
)
+20 -11
View File
@@ -18,11 +18,17 @@ from frigate.models import Event
# Base guidance per response field. `observations` is a reasoning scaffold,
# not user-facing, so style presets never override it.
#
# Every scene and shortSummary guidance states its length. The response
# schema caps both fields, and grammar-constrained providers such as llama.cpp
# enforce the cap by cutting the text off, so a model that is never told the
# limit ends mid-sentence. Every user-facing field also repeats the name rule:
# models follow per-field guidance far more reliably than a single global rule.
REVIEW_DESCRIPTION_FIELD_GUIDELINES: dict[str, str] = {
"observations": "Include the very start of the activity — for example, a vehicle entering the frame or pulling into the driveway — even if it lasts only a few frames and the rest of the clip is dominated by a longer activity. Include each arrival, departure, object handled, and notable change in position or state. Each item is a single concrete fact written as a complete sentence.",
"scene": 'Describe how the sequence begins, then the progression of events — all significant movements and actions in order. For example, if a vehicle arrives and then a person exits, describe both sequentially. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name — do not replace them with generic terms. For unnamed objects (e.g., "person", "car"), refer to them naturally with articles (e.g., "a person", "the car"). Your description should align with and support the threat level you assign.',
"observations": "List what happens in chronological order, starting from the first frame. Include each arrival, departure, object handled, and notable change in position or state, including brief moments at the start or end of the sequence that a longer activity could overshadow. Each item is a single concrete fact written as a complete sentence.",
"scene": 'Describe how the sequence begins, then the progression of events: all significant movements and actions in order. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name; do not replace them with generic terms. For unnamed objects (e.g., "person", "car"), refer to them naturally with articles (e.g., "a person", "the car"). Your description should align with and support the threat level you assign. Use no more than five sentences.',
"title": "Name the primary activity across the observations, together with the location. An activity is what is being done with objects, tools, or surfaces; locomotion through the scene qualifies as the activity only when no other interaction is observed. For named subjects, always use their name. For unnamed objects, refer to them naturally with articles.",
"shortSummary": "Briefly summarize the primary activity across the observations.",
"shortSummary": "Briefly summarize the primary activity across the observations in one sentence. For named subjects, always use their name.",
"potential_threat_level": "Must be consistent with your scene description and the activity patterns above.",
}
@@ -31,19 +37,19 @@ REVIEW_DESCRIPTION_FIELD_GUIDELINES: dict[str, str] = {
# competing style instructions.
REVIEW_RESPONSE_STYLES: dict[str, dict[str, str]] = {
"natural": {
"scene": 'Recount what happened the way a person would describe it to a neighbor, in plain everyday language: how the sequence begins, then each significant movement and action in the order it happens. Use flowing sentences that connect related actions, written the way people actually talk rather than like a surveillance report. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name rather than a generic term. Refer to unnamed objects naturally with articles. Stay factual, and keep the description consistent with the threat level you assign.',
"scene": 'Recount what happened the way a person would describe it to a neighbor, in plain everyday language: how the sequence begins, then each significant movement and action in the order it happens. Use flowing sentences that connect related actions, written the way people actually talk rather than like a surveillance report. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name rather than a generic term. Refer to unnamed objects naturally with articles. Stay factual, and keep the description consistent with the threat level you assign. Use no more than five sentences.',
"title": 'Write the title as a short, sentence-case headline in present tense: the subject, then what they do, phrased the way you would text it to the homeowner. Describe only the action you see; do not assign the person a role or purpose that is not visibly indicated by a uniform, a marked vehicle, or a "(delivery/service)" tag in Objects in Scene. Name the main thing done, not just movement through the scene, unless movement is all that happens. For named subjects, always use their name.',
"shortSummary": "Sum up the primary activity in one short, natural sentence, as if mentioning it to someone in passing.",
"shortSummary": "Sum up the primary activity in one short, natural sentence, as if mentioning it to someone in passing. For named subjects, always use their name.",
},
"concise": {
"scene": 'Cover each significant movement and action in order using as few short, direct sentences as possible, omitting environmental and cosmetic detail unless it affects the assessment. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name. For unnamed objects, refer to them naturally with articles. Your description should align with and support the threat level you assign.',
"scene": 'Cover each significant movement and action in order using as few short, direct sentences as possible, omitting environmental and cosmetic detail unless it affects the assessment. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name. For unnamed objects, refer to them naturally with articles. Your description should align with and support the threat level you assign. Use no more than five sentences.',
"title": 'Write the title as a terse label of two to four words naming the specific activity observed and where it happened. Do not assign a role or purpose that is not visibly indicated by a uniform, a marked vehicle, or a "(delivery/service)" tag in Objects in Scene. For named subjects, always use their name.',
"shortSummary": "Summarize the primary activity in one short sentence.",
"shortSummary": "Summarize the primary activity in one short sentence. For named subjects, always use their name.",
},
"detailed": {
"scene": 'Describe how the sequence begins, then the progression of events — all significant movements and actions in order, including the specifics that best identify the subjects: colors, clothing, carried items, positions, and paths of movement, plus environmental details like lighting changes when they stand out. Favor the most identifying details over exhaustive coverage, and keep every added detail observational rather than speculative. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name rather than a generic term. For unnamed objects, refer to them naturally with articles. Your description should align with and support the threat level you assign.',
"scene": 'Describe how the sequence begins, then the progression of events: all significant movements and actions in order, including the specifics that best identify the subjects: colors, clothing, carried items, positions, and paths of movement, plus environmental details like lighting changes when they stand out. Favor the most identifying details over exhaustive coverage, and keep every added detail observational rather than speculative. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name rather than a generic term. For unnamed objects, refer to them naturally with articles. Your description should align with and support the threat level you assign. Use no more than five sentences.',
"title": 'Write the title as a specific description of who did what and where, in under roughly twelve words, including the most distinguishing visible detail of the subject, such as clothing or vehicle color. Do not assign a role or purpose that is not visibly indicated by a uniform, a marked vehicle, or a "(delivery/service)" tag in Objects in Scene. Name the main thing done, not just movement through the scene, unless movement is all that happens. For named subjects, always use their name.',
"shortSummary": "Briefly summarize the primary activity across the observations, including the most identifying visible detail, such as vehicle color or clothing.",
"shortSummary": "Briefly summarize the primary activity across the observations in one sentence, including the most identifying visible detail, such as vehicle color or clothing. For named subjects, always use their name.",
},
}
@@ -61,9 +67,12 @@ def get_review_field_guidelines(response_style: str = "default") -> dict[str, st
# Explains the per-frame labels and tracker notes used by the annotated frame
# mode. Neither the notes nor this guidance say whether repeated detections are
# the same subject, since the tracking data cannot tell.
# the same subject, since the tracking data cannot tell. The last line keeps
# "no longer detected" from being read as the subject leaving the frame, which
# is only one of the reasons the tracker loses an object.
FRAME_ANNOTATION_GUIDANCE = """- Each image below is immediately preceded by a text label giving its frame number and how many seconds into the sequence it was captured. Use these labels to track the order of events and the time between them.
- Some images below are preceded by notes from the camera's object tracker recording what changed at that point: an object being first detected, starting to move, reversing direction, stopping, or no longer being detected. These notes come from tracking data rather than from the images, and they are reliable. Use them to establish how many distinct activities occur and in what order, and describe every one of them."""
- Some images below are preceded by notes from the camera's object tracker recording what changed at that point: an object being first detected, starting to move, reversing direction, stopping, or no longer being detected. These notes come from tracking data rather than from the images, and they are reliable. Use them to establish how many distinct activities occur and in what order, and describe every one of them.
- The notes say when something changed, not why. In particular, an object is no longer detected whenever the tracker loses sight of it, which happens when it leaves the frame and also when it goes out of view behind or inside something. Look at the frames before and after each note to determine what actually happened."""
def build_review_description_prompt(
+1 -6
View File
@@ -1470,9 +1470,6 @@ class RecordingExporter(threading.Thread):
try:
self._run_export(video_path, thumb_path)
except Exception:
self._discard_failed_export(video_path, thumb_path)
raise
finally:
# staged runs hold a full copy of the export, so they must not
# survive a failure the way the small chapter file could
@@ -1482,8 +1479,7 @@ class RecordingExporter(threading.Thread):
"""Drop the partial output and the row that promised it."""
Path(video_path).unlink(missing_ok=True)
Export.delete().where(Export.id == self.export_id).execute()
if thumb_path:
Path(thumb_path).unlink(missing_ok=True)
Path(thumb_path).unlink(missing_ok=True)
def _run_export(self, video_path: str, thumb_path: str) -> None:
try:
@@ -1502,7 +1498,6 @@ class RecordingExporter(threading.Thread):
else:
ffmpeg_cmd, playlist_lines = self.get_preview_export_command(video_path)
except DoesNotExist:
self._discard_failed_export(video_path, thumb_path)
return
# When neither custom ffmpeg arg is set the default path uses
+2 -8
View File
@@ -393,8 +393,8 @@ class TestHttpApp(BaseTestHttp):
def test_event_time_filtering(self):
morning_id = "123456.random"
evening_id = "654321.random"
morning = 1656568800 # 06/30/2022 6 am (GMT)
evening = 1656612000 # 06/30/2022 6 pm (GMT)
morning = 1656590400 # 06/30/2022 6 am (GMT)
evening = 1656633600 # 06/30/2022 6 pm (GMT)
with AuthTestClient(self.app) as client:
super().insert_mock_event(morning_id, morning)
@@ -417,12 +417,6 @@ class TestHttpApp(BaseTestHttp):
).json()
assert events
assert len(events) == 1
# an event in the first minute of the range is included
events = client.get(
"/events",
params={"time_range": "06:00,24:00"},
).json()
assert len(events) == 2
def test_set_delete_sub_label(self):
mock_event_updater = Mock(spec=EventMetadataPublisher)
-24
View File
@@ -427,30 +427,6 @@ class TestHttpExport(BaseTestHttp):
assert response.status_code == 400
assert Export.get(Export.id == "front_door_running").video_path == video
def test_delete_export_rejected_while_in_progress(self):
with tempfile.TemporaryDirectory() as tmpdir:
video = os.path.join(tmpdir, "front_door_abc123.mp4")
with open(video, "wb") as handle:
handle.write(b"video")
Export.create(
id="front_door_running",
camera="front_door",
name="front door export",
date=100,
video_path=video,
thumb_path=os.path.join(tmpdir, "t.webp"),
in_progress=True,
)
with AuthTestClient(self.app) as client:
response = client.post(
"/exports/delete", json={"ids": ["front_door_running"]}
)
assert response.status_code == 400
assert os.path.exists(video)
def test_rename_export_missing_file_leaves_the_row_alone(self):
with tempfile.TemporaryDirectory() as tmpdir:
video = os.path.join(tmpdir, "front_door_gone_abc123.mp4")
-4
View File
@@ -584,10 +584,6 @@ class TestHttpMedia(BaseTestHttp):
{"start_time": 1160, "end_time": 1300},
]
# a non-positive scale would never advance the gap scan
response = client.get("/recordings/unavailable", params={"scale": 0})
assert response.status_code == 422
def test_recordings_summary_day_attribution_by_start_time(self):
"""
A recording spanning midnight marks only its start day.
+13
View File
@@ -72,6 +72,19 @@ class TestReviewResponseStyle(unittest.TestCase):
f"unexpected override fields: {set(overrides)}",
)
def test_every_style_states_length_and_name_rules(self):
# The schema caps these fields and grammar-constrained providers cut
# text off at the cap, and models only reliably use names when each
# field asks for them.
for style in ["default", *REVIEW_RESPONSE_STYLES]:
fields = get_review_field_guidelines(style)
self.assertIn("no more than five sentences", fields["scene"])
self.assertIn("one", fields["shortSummary"])
self.assertIn("sentence", fields["shortSummary"])
for field_name in ("scene", "title", "shortSummary"):
self.assertIn("always use their name", fields[field_name])
def test_config_enum_matches_style_presets(self):
enum_styles = {e.value for e in ReviewResponseStyleEnum}
self.assertEqual(enum_styles, {"default", *REVIEW_RESPONSE_STYLES})
+55
View File
@@ -82,6 +82,10 @@ class TestEventNames(unittest.TestCase):
event = {"label": "waste_bin", "sub_label": "Compost"}
self.assertEqual(event_name(event), 'waste bin "Compost"')
def test_names_match_the_objects_in_scene_list(self):
event = {"label": "person", "sub_label": "known_subject"}
self.assertEqual(event_name(event), 'person "Known Subject"')
def test_repeated_objects_are_never_numbered(self):
# Whether these are the same subject is unknown, so the notes must not
# imply either answer.
@@ -297,6 +301,57 @@ class TestLateEvents(unittest.TestCase):
)
class TestEarlyEvents(unittest.TestCase):
def setUp(self):
# A car that pulled in and parked well before the first sampled frame,
# then leaves during the clip.
self.car = track(
"1789481990.000000-parked",
"car",
0.0,
straight_path((0.1, 0.9), (0.4, 0.6), 6, 1.0),
)
self.changes = [
{
"timestamp": 12.0,
"source_id": self.car["id"],
"class_type": "stationary",
},
{"timestamp": 70.0, "source_id": self.car["id"], "class_type": "active"},
]
def test_history_before_the_first_frame_is_summarized(self):
# Arrival notes on the first frame were read as the car pulling in
# during the clip.
timeline = build_timeline(
[self.car], 90.0, state_changes=self.changes, span_start=50.0
)
self.assertEqual(
timeline,
[
(50.0, "a car already in view when the sequence begins, not moving"),
(70.0, "a car starts moving again"),
],
)
def test_object_still_moving_at_the_first_frame_is_not_called_stationary(self):
timeline = build_timeline([self.car], 90.0, span_start=3.0)
self.assertEqual(
timeline[0], (3.0, "a car already in view when the sequence begins")
)
self.assertFalse(any("first detected" in p for _, p in timeline))
def test_objects_gone_before_the_first_frame_are_skipped(self):
gone = dict(self.car, end_time=20.0)
self.assertEqual(build_timeline([gone], 90.0, span_start=50.0), [])
def test_objects_detected_inside_the_clip_are_unchanged(self):
self.assertEqual(
build_timeline([self.car], 90.0, self.changes, span_start=0.0),
build_timeline([self.car], 90.0, self.changes),
)
class TestFrameBucketing(unittest.TestCase):
def test_moment_attaches_to_the_following_frame(self):
frame_times = [0.0, 10.0, 20.0, 30.0]
+38 -23
View File
@@ -42,6 +42,7 @@ from frigate.config.camera.genai import GenAIConfig, GenAIProviderEnum # noqa:
from frigate.genai.prompts import ( # noqa: E402
REVIEW_RESPONSE_STYLES,
build_review_description_response_format,
get_review_field_guidelines,
)
EXAMPLES_DIR = Path(__file__).resolve().parent / "genai-review-examples"
@@ -227,11 +228,14 @@ def list_examples() -> list[Path]:
)
def load_frames(example: Path) -> list[bytes]:
"""Load the example's frames as JPEG bytes in frame order.
def load_frames(example: Path) -> tuple[list[bytes], list[str] | None]:
"""Load the example's frames as JPEG bytes in frame order, with captions.
Frames are saved by Frigate as <index>.jpg or <index>.webp. Non-JPEG
images are re-encoded to JPEG to match what Frigate sends to providers.
In annotated frame mode Frigate also saves <index>.txt, the caption sent
directly before that frame; captions are returned only when every frame
has one, matching Frigate's fallback to plain frames.
"""
import cv2
@@ -242,34 +246,42 @@ def load_frames(example: Path) -> list[bytes]:
return (1 << 30, path.name)
frames: list[bytes] = []
captions: list[str] = []
files = sorted(
(f for f in example.iterdir() if f.suffix.lower() in IMAGE_EXTENSIONS),
key=frame_order,
)
for file in files:
if file.suffix.lower() in (".jpg", ".jpeg"):
frames.append(file.read_bytes())
continue
frame = file.read_bytes()
else:
image = cv2.imread(str(file))
if image is None:
print(f" Skipping unreadable image {file.name}")
continue
ok, jpg = cv2.imencode(".jpg", image, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
if not ok:
continue
frame = jpg.tobytes()
image = cv2.imread(str(file))
if image is None:
print(f" Skipping unreadable image {file.name}")
continue
ok, jpg = cv2.imencode(".jpg", image, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
if ok:
frames.append(jpg.tobytes())
return frames
frames.append(frame)
caption_file = file.with_suffix(".txt")
if caption_file.is_file():
captions.append(caption_file.read_text())
return frames, captions if captions and len(captions) == len(frames) else None
def apply_style(prompt: str, style: str) -> str:
"""Apply a style preset to a saved prompt.
"""Apply the current field guidance for a style preset to a saved prompt.
Presets replace the per-field response guidance lines, matching what
Frigate builds at runtime. Handles both the current guidance format
Every per-field response guidance line is replaced with what Frigate
builds at runtime for that style, so "default" restores the default
guidance on a prompt saved with another style, and older saves pick up
current guidance. Handles both the current guidance format
("- `scene`: ...") and the 0.17 format ("- `scene` (string): ...").
"""
overrides = REVIEW_RESPONSE_STYLES.get(style, {})
for field_name, guidance in overrides.items():
for field_name, guidance in get_review_field_guidelines(style).items():
pattern = rf"^- `{field_name}`(?: \(string\))?: .*$"
prompt, count = re.subn(
pattern,
@@ -292,7 +304,9 @@ def pretty_print_response(response: str) -> None:
print(json.dumps(parsed, indent=2, ensure_ascii=False))
def followup_loop(client: Any, base_prompt: str, frames: list[bytes]) -> None:
def followup_loop(
client: Any, base_prompt: str, frames: list[bytes], captions: list[str] | None
) -> None:
"""Let the user ask the model follow-up questions about the same frames."""
transcript = base_prompt
print("\nAsk follow-up questions about this sequence (blank line to finish).")
@@ -311,7 +325,7 @@ def followup_loop(client: Any, base_prompt: str, frames: list[bytes]) -> None:
f"Question: {question}"
)
start = time.monotonic()
answer = client._send(prompt, frames)
answer = client._send(prompt, frames, image_captions=captions)
elapsed = time.monotonic() - start
if answer is None:
print("No response from provider (see logs above).")
@@ -349,7 +363,7 @@ def run_example(settings: TesterSettings) -> None:
return
prompt = apply_style(prompt_file.read_text(), style)
frames = load_frames(example)
frames, captions = load_frames(example)
if not frames:
print(f"{example.name} contains no frame images, cannot run")
return
@@ -365,10 +379,11 @@ def run_example(settings: TesterSettings) -> None:
print(
f"\nSending {len(frames)} frames to {settings.provider} "
f"(model={settings.model or '(none)'}, style={style})..."
f"(model={settings.model or '(none)'}, style={style}, "
f"captions={'yes' if captions else 'no'})..."
)
start = time.monotonic()
response = client._send(prompt, frames, response_format)
response = client._send(prompt, frames, response_format, image_captions=captions)
elapsed = time.monotonic() - start
if response is None:
@@ -377,7 +392,7 @@ def run_example(settings: TesterSettings) -> None:
print(f"\nResponse ({elapsed:.1f}s):\n")
pretty_print_response(response)
followup_loop(client, f"{prompt}\n\nYour analysis:\n{response}", frames)
followup_loop(client, f"{prompt}\n\nYour analysis:\n{response}", frames, captions)
def main() -> None:
+7 -6
View File
@@ -97,12 +97,13 @@ test.describe("setup wizard account @high @mobile", () => {
.fill("a-long-enough-password");
await page.getByRole("button", { name: "Save" }).click();
const createCall = sent.find((call) => call.method === "POST");
expect(createCall?.body).toEqual({
username: "family",
password: "a-long-enough-password",
role: "viewer",
});
await expect
.poll(() => sent.find((call) => call.method === "POST")?.body)
.toEqual({
username: "family",
password: "a-long-enough-password",
role: "viewer",
});
});
test("shows anonymous copy when nobody is signed in", async ({
+23 -11
View File
@@ -101,10 +101,14 @@ test.describe("setup wizard hardware @high @mobile", () => {
await expect(page.getByText("Will use VAAPI (Intel/AMD)")).toBeVisible();
await page.getByRole("button", { name: "Next" }).click();
const hwaccelSave = saves.find((save) => save.config_data?.ffmpeg);
expect(hwaccelSave?.config_data?.ffmpeg).toEqual({
hwaccel_args: "preset-vaapi",
});
await expect
.poll(
() =>
saves.find((save) => save.config_data?.ffmpeg)?.config_data?.ffmpeg,
)
.toEqual({
hwaccel_args: "preset-vaapi",
});
// the saved steps only take effect after a restart
const restarts = await captureRestarts(page);
@@ -117,7 +121,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
await page.getByRole("button", { name: "Apply & Restart" }).click();
await expect(page.getByText("Starting Frigate...")).toBeVisible();
expect(restarts).toHaveLength(1);
await expect.poll(() => restarts).toHaveLength(1);
});
test("defers model setup for onnx hardware without Frigate+", async ({
@@ -228,10 +232,14 @@ test.describe("setup wizard hardware @high @mobile", () => {
await page.getByRole("button", { name: "Next" }).click();
// no camera was added, so there is no codec to match
const hwaccelSave = saves.find((save) => save.config_data?.ffmpeg);
expect(hwaccelSave?.config_data?.ffmpeg).toEqual({
hwaccel_args: "preset-jetson-h264",
});
await expect
.poll(
() =>
saves.find((save) => save.config_data?.ffmpeg)?.config_data?.ffmpeg,
)
.toEqual({
hwaccel_args: "preset-jetson-h264",
});
});
test("None writes an explicit empty hwaccel list", async ({
@@ -249,7 +257,11 @@ test.describe("setup wizard hardware @high @mobile", () => {
await page.getByRole("radio", { name: "None (software decoding)" }).click();
await page.getByRole("button", { name: "Next" }).click();
const hwaccelSave = saves.find((save) => save.config_data?.ffmpeg);
expect(hwaccelSave?.config_data?.ffmpeg).toEqual({ hwaccel_args: [] });
await expect
.poll(
() =>
saves.find((save) => save.config_data?.ffmpeg)?.config_data?.ffmpeg,
)
.toEqual({ hwaccel_args: [] });
});
});
+153 -157
View File
@@ -9,8 +9,8 @@
"version": "0.0.0",
"hasInstallScript": true,
"dependencies": {
"@cycjimmy/jsmpeg-player": "^6.1.2",
"@hookform/resolvers": "^6.0.0",
"@cycjimmy/jsmpeg-player": "^6.1.6",
"@hookform/resolvers": "^6.0.2",
"@radix-ui/react-alert-dialog": "^1.2.1",
"@radix-ui/react-aspect-ratio": "^1.2.0",
"@radix-ui/react-checkbox": "^1.4.1",
@@ -37,39 +37,39 @@
"@rjsf/shadcn": "^6.10.1",
"@rjsf/utils": "^6.10.1",
"@rjsf/validator-ajv8": "^6.10.1",
"apexcharts": "^8.4.1",
"axios": "^1.20.0",
"apexcharts": "^8.5.0",
"axios": "^1.21.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"copy-to-clipboard": "^4.0.2",
"date-fns": "^4.4.0",
"date-fns-tz": "^3.2.0",
"framer-motion": "^14.6.0",
"hls.js": "^1.7.3",
"i18next": "^26.5.5",
"i18next-http-backend": "^4.0.4",
"framer-motion": "^14.7.1",
"hls.js": "^1.7.4",
"i18next": "^26.5.11",
"i18next-http-backend": "^4.0.6",
"idb-keyval": "^6.3.0",
"js-yaml": "^5.5.0",
"konva": "^10.9.0",
"lodash": "^4.18.1",
"lucide-react": "^1.61.0",
"lucide-react": "^1.64.0",
"monaco-yaml": "^5.5.1",
"next-themes": "^0.4.6",
"nosleep.js": "^0.12.0",
"react": "^19.3.1",
"react-apexcharts": "^2.1.1",
"react-apexcharts": "^2.1.2",
"react-day-picker": "^10.0.2",
"react-device-detect": "^2.2.3",
"react-dom": "^19.3.1",
"react-dropzone": "^20.1.4",
"react-grid-layout": "^2.3.0",
"react-hook-form": "^7.90.0",
"react-i18next": "^17.1.1",
"react-hook-form": "^7.91.0",
"react-i18next": "^17.1.9",
"react-icons": "^5.7.0",
"react-konva": "^19.3.2",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.18.4",
"react-router-dom": "^7.18.5",
"react-swipeable": "^7.0.2",
"react-zoom-pan-pinch": "3.4.4",
"remark-gfm": "^4.0.0",
@@ -81,7 +81,7 @@
"tailwindcss-animate": "^1.0.7",
"use-long-press": "^3.3.0",
"vaul": "^1.1.2",
"virtua": "^0.53.6",
"virtua": "^0.54.0",
"vite-plugin-monaco-editor": "^1.1.0",
"zod": "^4.6.7"
},
@@ -90,11 +90,11 @@
"@playwright/test": "^1.64.1",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.27",
"@types/node": "^26.6.5",
"@types/node": "^26.6.6",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.2",
"autoprefixer": "^10.6.1",
"autoprefixer": "^10.6.2",
"esbuild": "^0.28.2",
"eslint": "^10.13.0",
"eslint-config-prettier": "^10.1.8",
@@ -105,12 +105,12 @@
"i18next-cli": "^1.5.11",
"monaco-editor": "^0.53.0",
"patch-package": "^8.0.1",
"postcss": "^8.5.31",
"postcss": "^8.5.32",
"prettier": "^3.9.10",
"prettier-plugin-tailwindcss": "^0.8.1",
"tailwindcss": "^3.4.19",
"typescript": "^6.0.3",
"typescript-eslint": "^8.72.0",
"typescript-eslint": "^8.72.1",
"vite": "^8.3.5"
}
},
@@ -428,9 +428,9 @@
}
},
"node_modules/@cycjimmy/jsmpeg-player": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@cycjimmy/jsmpeg-player/-/jsmpeg-player-6.1.2.tgz",
"integrity": "sha512-U9DBDe5fxHmbwQww9rFxMLNI2Wlg7DhPzI7AVFpq8GehiUP7+NwuMPXpP4zAd52sgkxtOqOeMjgE5g0ZLnQZ0w==",
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/@cycjimmy/jsmpeg-player/-/jsmpeg-player-6.1.6.tgz",
"integrity": "sha512-D3ZfarlwDAltkBzmYaAAvARqRvgN4DmM70fbYwRECh9gpT8IRzLSxsXcR0uoPj/W3VtFeQjwA1QBj+rDpXiUzA==",
"license": "MIT"
},
"node_modules/@date-fns/tz": {
@@ -1059,9 +1059,9 @@
"license": "MIT"
},
"node_modules/@hookform/resolvers": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-6.0.0.tgz",
"integrity": "sha512-6iBzQnB+YwAveVc8HUi6QYLITn5X+xNKRMFIcNXHZlvp2aTI7yWbj4n/JodXxirMCcBc4VCjb2aYAJh4x07mVw==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-6.0.2.tgz",
"integrity": "sha512-e2dqUOlaGuirKVOd/iZWk/Yg5zeaGw7t0hSc3LzxL3o7eZGwZkTyEbBL8H52m0wRKEsNLWoMGOiXGHbPHTXlEQ==",
"license": "MIT",
"dependencies": {
"@standard-schema/utils": "^0.3.0"
@@ -1069,7 +1069,6 @@
"peerDependencies": {
"@sinclair/typebox": ">=0.25.24",
"@standard-schema/spec": "^1.0.0",
"@typeschema/main": ">=0.13.7",
"@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0",
"ajv": "^8.12.0",
"ajv-errors": "^3.0.0",
@@ -1100,9 +1099,6 @@
"@standard-schema/spec": {
"optional": true
},
"@typeschema/main": {
"optional": true
},
"@vinejs/vine": {
"optional": true
},
@@ -3611,9 +3607,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "26.6.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
"integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
"version": "26.6.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz",
"integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3661,17 +3657,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.0.tgz",
"integrity": "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==",
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz",
"integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.72.0",
"@typescript-eslint/type-utils": "8.72.0",
"@typescript-eslint/utils": "8.72.0",
"@typescript-eslint/visitor-keys": "8.72.0",
"@typescript-eslint/scope-manager": "8.72.1",
"@typescript-eslint/type-utils": "8.72.1",
"@typescript-eslint/utils": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -3684,15 +3680,15 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.72.0",
"@typescript-eslint/parser": "^8.72.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.13",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz",
"integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==",
"version": "7.0.14",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
"integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3700,16 +3696,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.0.tgz",
"integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==",
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz",
"integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.72.0",
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/typescript-estree": "8.72.0",
"@typescript-eslint/visitor-keys": "8.72.0",
"@typescript-eslint/scope-manager": "8.72.1",
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1",
"debug": "^4.4.3"
},
"engines": {
@@ -3725,14 +3721,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz",
"integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==",
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz",
"integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.72.0",
"@typescript-eslint/types": "^8.72.0",
"@typescript-eslint/tsconfig-utils": "^8.72.1",
"@typescript-eslint/types": "^8.72.1",
"debug": "^4.4.3"
},
"engines": {
@@ -3747,14 +3743,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz",
"integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==",
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz",
"integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/visitor-keys": "8.72.0"
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3765,9 +3761,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz",
"integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==",
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz",
"integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3782,15 +3778,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz",
"integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==",
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz",
"integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/typescript-estree": "8.72.0",
"@typescript-eslint/utils": "8.72.0",
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1",
"@typescript-eslint/utils": "8.72.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -3807,9 +3803,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz",
"integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==",
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz",
"integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3821,16 +3817,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz",
"integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==",
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz",
"integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.72.0",
"@typescript-eslint/tsconfig-utils": "8.72.0",
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/visitor-keys": "8.72.0",
"@typescript-eslint/project-service": "8.72.1",
"@typescript-eslint/tsconfig-utils": "8.72.1",
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -3849,16 +3845,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz",
"integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==",
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz",
"integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.72.0",
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/typescript-estree": "8.72.0"
"@typescript-eslint/scope-manager": "8.72.1",
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3873,13 +3869,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz",
"integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==",
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz",
"integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/types": "8.72.1",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -4067,9 +4063,9 @@
"license": "SEE LICENSE IN LICENSE.md"
},
"node_modules/apexcharts": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-8.4.1.tgz",
"integrity": "sha512-29IbZ1rn3NRkDvete5EzZXy6yDSW7mjNXkytLpvaYHk0XbfsR+FvC7U78L3wMvJa699NBmix2z6r3Qm5YDvw0A==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-8.5.0.tgz",
"integrity": "sha512-mH9QiOwIeRGg635OzAc2mznuwrK9XHr9S1RcC26GNNOU6fUPe64cMc7hXGcM9bXz92nO4Ax7P6uxSYaTA7gBDw==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"apex-commons": "^0.8.3"
@@ -4126,9 +4122,9 @@
}
},
"node_modules/autoprefixer": {
"version": "10.6.1",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz",
"integrity": "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==",
"version": "10.6.2",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.2.tgz",
"integrity": "sha512-VOw9/yGTGwPPgzFcODtcv6zhEPIeIWkRSe10L2ri2f5vZ7ID7lCt6jZzRF5/ubgOZ+szYydmEF5AYsA5jxX1tA==",
"dev": true,
"funding": [
{
@@ -4163,9 +4159,9 @@
}
},
"node_modules/axios": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
"integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.21.0.tgz",
"integrity": "sha512-YCzXMB53Q9PZBsh7yN7wtapit4WepwcHS2fP8QhGsBxSz5YtgYobV7GGlWlAJi+RxfU77GQTQi2DSLUma42EYA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
@@ -5515,9 +5511,9 @@
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz",
"integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==",
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.2.tgz",
"integrity": "sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==",
"funding": [
{
"type": "individual",
@@ -5582,13 +5578,13 @@
}
},
"node_modules/framer-motion": {
"version": "14.6.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.6.0.tgz",
"integrity": "sha512-MzBgZxGgoHPfSLEXmo4o5XlZvb+nVRFdHtGjVhm6LMIEJKTt/8hNO023euL5mYyzc8llV/hQTfjnN68tNRHalw==",
"version": "14.7.1",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.7.1.tgz",
"integrity": "sha512-mFDYZxyfeZnpvL/zEQEqLZNYVA/ya47c9TBfZb/PEoNBBVSVvCLkMrCv9U38cuw72W161XENZwZB0+4nIBLEJg==",
"license": "MIT",
"dependencies": {
"motion-dom": "14.6.0",
"motion-utils": "14.6.0",
"motion-dom": "14.7.1",
"motion-utils": "14.7.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
@@ -5949,9 +5945,9 @@
}
},
"node_modules/hls.js": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.7.3.tgz",
"integrity": "sha512-MsPlx6yVW4Qv4C7mEVou4/gk/5cN2dVxOTnrNjPSmyH2f0Ln+/UkG10Ax1PE6OvrmTVPKgQYozTtBaPOuCzq7A==",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.7.4.tgz",
"integrity": "sha512-3GlxGdPvT2YHvQYOt+yOzT/DEior/qJVNIylHA77kgeoBTdQQqi70GwefNEUmHQeNGyjC8U62+bruqW/Tpm+tg==",
"license": "Apache-2.0"
},
"node_modules/hookified": {
@@ -5991,9 +5987,9 @@
}
},
"node_modules/i18next": {
"version": "26.5.5",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.5.5.tgz",
"integrity": "sha512-txdaNOlnZCdiTMSlEzl5hUJU8TC9R4J/RCR1G7XrDPbWT2t3mR142QPz84ZuZeXDt3yq+9zmsdPiakxYtmp3fw==",
"version": "26.5.11",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.5.11.tgz",
"integrity": "sha512-Wc0klBPKxqZ2tL2qdAnF8xvvnWgr1IMGSY2ikTiAvaFVbz7xiFARtkoU4RidBDw1SyziGROp0qxu6qnJ7l2Wug==",
"funding": [
{
"type": "individual",
@@ -6106,9 +6102,9 @@
}
},
"node_modules/i18next-http-backend": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.4.tgz",
"integrity": "sha512-nG9OQKvUsPiTVUBg1rCGmvmWhzxC9GOxVbGdTYyejHVMSUjutt1hiBNVSzhB/ZE7keL9Xkb880kUfhDovV5T5g==",
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.6.tgz",
"integrity": "sha512-k5OPDXAQH9CZ2Ju2DDw1ZC4WiYmjKUjOE4lcd51tNQyWjVDfpuwPcE9Y5VzDpz1tR1Z4wxkkj4H+ImxgwbfpeQ==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -6978,9 +6974,9 @@
}
},
"node_modules/lucide-react": {
"version": "1.61.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.61.0.tgz",
"integrity": "sha512-v2SQPPPNjnr+BlhL9Spuzmo5yR+hLkftfuE4E3jaxdNPgSH0hJKPcLJBP7yAta87jQNPsSERS59PJjCJ/CorBA==",
"version": "1.64.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.64.0.tgz",
"integrity": "sha512-v626F/bcLNvFom6vzztHILtG9tUgXXbgA5AGkd82YkV401+Ko/NHyrWafbPwGofa+6p/EgDJcvEkIwsMH9098w==",
"license": "ISC",
"peerDependencies": {
"@types/react": "*",
@@ -8055,18 +8051,18 @@
}
},
"node_modules/motion-dom": {
"version": "14.6.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.6.0.tgz",
"integrity": "sha512-lIojTl9aYSPIkeOOSzBliy1nNvpdO3JtVzW2WgTXQlOP4ZMuJhtWO3fP7OD4TVFwlRucdvxlDSMkL+fEQnQ52A==",
"version": "14.7.1",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.7.1.tgz",
"integrity": "sha512-p9q9F7+HoijbQjUG/cSGNmWS2t1OaAKxkd2RG5gQJRoDwoTap/Khb7K1P5rLW90crnSmx0rcRXzh3Jd5UNrpEw==",
"license": "MIT",
"dependencies": {
"motion-utils": "14.6.0"
"motion-utils": "14.7.0"
}
},
"node_modules/motion-utils": {
"version": "14.6.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-14.6.0.tgz",
"integrity": "sha512-bZJo2AmQ2aOFkaato6VXrTbkPG0XhMzivWUWSaI59pxyllj8QMQUFZ84MYRQnPkK+7dr4KldyJwF2+RM0sdq/w==",
"version": "14.7.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-14.7.0.tgz",
"integrity": "sha512-woO0/Nb7ys75f94fjvQi/6leiHGvZUYkeHUPEZ3vxsV+9OpEVhkxp01x8MlRwX0zt31EqgXsKDAoBoJLC148Tw==",
"license": "MIT"
},
"node_modules/ms": {
@@ -8548,9 +8544,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz",
"integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==",
"version": "8.5.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz",
"integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==",
"funding": [
{
"type": "opencollective",
@@ -8920,10 +8916,10 @@
}
},
"node_modules/react-apexcharts": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-2.1.1.tgz",
"integrity": "sha512-aV25n+1ZdAzRG0KhODVC48K8WbhMKBAr3yWSeaaBqM1ySn22kSSKblsNCUvlwegsT+cdMLuOrK9xGZb+IK+OkQ==",
"license": "SEE LICENSE IN LICENSE",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-2.1.2.tgz",
"integrity": "sha512-vIoltFDOxy/EIdyWam833mgEfZiOQ+R6oqCvRgbGqNNTNNITCKL91CNeXThi2L8+YyxbamxyWY/6aZV66mx1fA==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"prop-types": "^15.8.1"
},
@@ -9037,9 +9033,9 @@
}
},
"node_modules/react-hook-form": {
"version": "7.90.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.90.0.tgz",
"integrity": "sha512-+NdG0FLzCPhFOp2NNxaUYLF9fyZhNjx9FTBBSYWF71/l6BBlmIc/j6xlp3DbaHWLJbIG7Zd8oznA19xJClU97w==",
"version": "7.91.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.91.0.tgz",
"integrity": "sha512-5BEBUnCBwa28AFXYTYmOaB/+CSpJ/+CWVASHEvIGUzFdHyUiPHeRhG7r5hOZ8nZi0yP7pbHdFNXO/p31EKTJFQ==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
@@ -9059,9 +9055,9 @@
}
},
"node_modules/react-i18next": {
"version": "17.1.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.1.1.tgz",
"integrity": "sha512-Q76xpzxrZNcqhB7M31Ns7isulGOlmhURqRsJOGlYgJJCyoy1pMq6tRl8KQycGNNY1vPaxbIdrogrqY9nkb+SEA==",
"version": "17.1.9",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.1.9.tgz",
"integrity": "sha512-OgYqIH13XHPlWTaaI795Dz+k2AdKnXcOxNQVnf750T8Ij0TYWp1nYvXRpoJBulPqlyk2OAbn1PM4tFKiIj/byA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.7",
@@ -9235,9 +9231,9 @@
}
},
"node_modules/react-router": {
"version": "7.18.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz",
"integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==",
"version": "7.18.5",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.5.tgz",
"integrity": "sha512-dTgvcXDOrLPrlLHrcZAqWveiK1hMSqN+rGAJzegP8HtmNkfRxOoshGWBNX3ZJ0cZjOHkRdlUJtaIhZMLLyGyPw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -9257,12 +9253,12 @@
}
},
"node_modules/react-router-dom": {
"version": "7.18.4",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz",
"integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==",
"version": "7.18.5",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.5.tgz",
"integrity": "sha512-9HcfDMU6JJ/fp6gJZYGwNE6RknVGS4kBjH1HkNw959mCJi70TVwdipQWv2FEol9uGpi+SZTFtVpJPtEQfayeOw==",
"license": "MIT",
"dependencies": {
"react-router": "7.18.4"
"react-router": "7.18.5"
},
"engines": {
"node": ">=20.0.0"
@@ -9586,9 +9582,9 @@
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.3.tgz",
"integrity": "sha512-irZHtTqurLna57KQG/s+fzOwv/J6XSH4cs8M/vsrY3uCaQ3S6+QJguAH0WdN47SXlk82voMZ4naWMQf8fdEoIg==",
"license": "MIT"
},
"node_modules/set-function-length": {
@@ -10062,16 +10058,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.0.tgz",
"integrity": "sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==",
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz",
"integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.72.0",
"@typescript-eslint/parser": "8.72.0",
"@typescript-eslint/typescript-estree": "8.72.0",
"@typescript-eslint/utils": "8.72.0"
"@typescript-eslint/eslint-plugin": "8.72.1",
"@typescript-eslint/parser": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1",
"@typescript-eslint/utils": "8.72.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -10364,9 +10360,9 @@
}
},
"node_modules/virtua": {
"version": "0.53.6",
"resolved": "https://registry.npmjs.org/virtua/-/virtua-0.53.6.tgz",
"integrity": "sha512-/vQtSFNDFcp3Yz4klbpe8Z88eQeThSw49cFlgJQdsqh3SUnRlW4jLi6ZYt4YzdwerYGlP6KCgAOXNVO+PmUg7w==",
"version": "0.54.0",
"resolved": "https://registry.npmjs.org/virtua/-/virtua-0.54.0.tgz",
"integrity": "sha512-8nGGrDhX61MDVeqgyIaWrJqhRm9z2Nqa1WDqt0tk1m+ZWrvLesMP0iL0vL6xNsLCF2jE2ohwY+PqufdbVmRgEw==",
"license": "MIT",
"peerDependencies": {
"@angular/common": ">=20.0.0",
+18 -18
View File
@@ -21,8 +21,8 @@
"i18n:status": "i18next-cli status"
},
"dependencies": {
"@cycjimmy/jsmpeg-player": "^6.1.2",
"@hookform/resolvers": "^6.0.0",
"@cycjimmy/jsmpeg-player": "^6.1.6",
"@hookform/resolvers": "^6.0.2",
"@radix-ui/react-alert-dialog": "^1.2.1",
"@radix-ui/react-aspect-ratio": "^1.2.0",
"@radix-ui/react-checkbox": "^1.4.1",
@@ -49,39 +49,39 @@
"@rjsf/shadcn": "^6.10.1",
"@rjsf/utils": "^6.10.1",
"@rjsf/validator-ajv8": "^6.10.1",
"apexcharts": "^8.4.1",
"axios": "^1.20.0",
"apexcharts": "^8.5.0",
"axios": "^1.21.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"copy-to-clipboard": "^4.0.2",
"date-fns": "^4.4.0",
"date-fns-tz": "^3.2.0",
"framer-motion": "^14.6.0",
"hls.js": "^1.7.3",
"i18next": "^26.5.5",
"i18next-http-backend": "^4.0.4",
"framer-motion": "^14.7.1",
"hls.js": "^1.7.4",
"i18next": "^26.5.11",
"i18next-http-backend": "^4.0.6",
"idb-keyval": "^6.3.0",
"js-yaml": "^5.5.0",
"konva": "^10.9.0",
"lodash": "^4.18.1",
"lucide-react": "^1.61.0",
"lucide-react": "^1.64.0",
"monaco-yaml": "^5.5.1",
"next-themes": "^0.4.6",
"nosleep.js": "^0.12.0",
"react": "^19.3.1",
"react-apexcharts": "^2.1.1",
"react-apexcharts": "^2.1.2",
"react-day-picker": "^10.0.2",
"react-device-detect": "^2.2.3",
"react-dom": "^19.3.1",
"react-dropzone": "^20.1.4",
"react-grid-layout": "^2.3.0",
"react-hook-form": "^7.90.0",
"react-i18next": "^17.1.1",
"react-hook-form": "^7.91.0",
"react-i18next": "^17.1.9",
"react-icons": "^5.7.0",
"react-konva": "^19.3.2",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.18.4",
"react-router-dom": "^7.18.5",
"react-swipeable": "^7.0.2",
"react-zoom-pan-pinch": "3.4.4",
"remark-gfm": "^4.0.0",
@@ -93,7 +93,7 @@
"tailwindcss-animate": "^1.0.7",
"use-long-press": "^3.3.0",
"vaul": "^1.1.2",
"virtua": "^0.53.6",
"virtua": "^0.54.0",
"vite-plugin-monaco-editor": "^1.1.0",
"zod": "^4.6.7"
},
@@ -102,11 +102,11 @@
"@playwright/test": "^1.64.1",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.27",
"@types/node": "^26.6.5",
"@types/node": "^26.6.6",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.2",
"autoprefixer": "^10.6.1",
"autoprefixer": "^10.6.2",
"esbuild": "^0.28.2",
"eslint": "^10.13.0",
"eslint-config-prettier": "^10.1.8",
@@ -117,12 +117,12 @@
"i18next-cli": "^1.5.11",
"monaco-editor": "^0.53.0",
"patch-package": "^8.0.1",
"postcss": "^8.5.31",
"postcss": "^8.5.32",
"prettier": "^3.9.10",
"prettier-plugin-tailwindcss": "^0.8.1",
"tailwindcss": "^3.4.19",
"typescript": "^6.0.3",
"typescript-eslint": "^8.72.0",
"typescript-eslint": "^8.72.1",
"vite": "^8.3.5"
},
"overrides": {
@@ -415,19 +415,10 @@ export default function NotificationsSettingsExtras({
pushSubscription.unsubscribe();
workerRegistration.unregister();
setRegistration(null);
removeMessage("notification_settings", "registration");
});
})
.catch(() => {
toast.error(t("notification.toast.error.registerFailed"), {
position: "top-center",
});
workerRegistration.unregister();
setRegistration(null);
removeMessage("notification_settings", "registration");
});
},
[addMessage, removeMessage, publicKey, t],
[addMessage, publicKey, t],
);
useEffect(() => {
@@ -307,9 +307,6 @@ export function KnownPlatesField(props: FieldProps) {
);
const handleAddEntry = useCallback(() => {
if ("" in data) {
return;
}
const next = { ...data, "": [""] };
onChange(next, fieldPath);
}, [data, fieldPath, onChange]);
+30 -2
View File
@@ -690,8 +690,36 @@ export function ExportContent({
(option: ExportOption) => {
setSelectedOption(option);
const hours = Number(option);
const start = latestTime - (Number.isNaN(hours) ? 1 : hours) * 3600;
const now = new Date(latestTime * 1000);
let start: number;
switch (option) {
case "1":
now.setHours(now.getHours() - 1);
start = now.getTime() / 1000;
break;
case "4":
now.setHours(now.getHours() - 4);
start = now.getTime() / 1000;
break;
case "8":
now.setHours(now.getHours() - 8);
start = now.getTime() / 1000;
break;
case "12":
now.setHours(now.getHours() - 12);
start = now.getTime() / 1000;
break;
case "24":
now.setHours(now.getHours() - 24);
start = now.getTime() / 1000;
break;
case "custom":
start = latestTime - 3600;
break;
default:
start = latestTime - 3600;
}
setRange({
before: latestTime,
+1 -12
View File
@@ -147,18 +147,7 @@ export default function DetailStream({
// away instead of waiting for a future effectiveTime change.
useEffect(() => {
if (!reviewItems || reviewItems.length === 0) return;
// an id left over from another camera's items counts as unset
if (
activeReviewId &&
reviewItems.some(
(r) =>
`review-${r.id ?? r.start_time ?? Math.floor(r.start_time ?? 0)}` ===
activeReviewId,
)
) {
return;
}
if (activeReviewId) return;
let target: ReviewSegment | undefined;
let closest: { r: ReviewSegment; diff: number } | undefined;
+2 -1
View File
@@ -45,7 +45,8 @@ export function createRecordingReviewUrl(
pathname: string,
state: RecordingReviewLinkState,
): string {
const url = new URL(pathname.replace(/^\//, ""), baseUrl);
const url = new URL(baseUrl);
url.pathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
url.searchParams.set(
RECORDING_REVIEW_LINK_PARAM,
`${state.camera}_${Math.floor(state.timestamp)}`,
+15 -9
View File
@@ -575,19 +575,25 @@ export default function DraggableGridLayout({
};
useEffect(() => {
if (!allGroupsStreamingSettings) {
return;
}
const initialAudioStates: AudioState = {};
const initialVolumeStates: VolumeState = {};
Object.entries(currentGroupStreamingSettings ?? {}).forEach(
([camera, cameraSettings]) => {
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
initialVolumeStates[camera] = cameraSettings.volume ?? 1;
},
);
Object.entries(allGroupsStreamingSettings).forEach(([_, groupSettings]) => {
if (groupSettings) {
Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
initialVolumeStates[camera] = cameraSettings.volume ?? 1;
});
}
});
setAudioStates(initialAudioStates);
setVolumeStates(initialVolumeStates);
}, [currentGroupStreamingSettings, cameraGroup]);
}, [allGroupsStreamingSettings]);
const toggleAudio = (cameraName: string) => {
setAudioStates((prev) => ({
@@ -794,7 +800,7 @@ export default function DraggableGridLayout({
toggleAudio={() => toggleAudio(camera.name)}
statsState={statsStates[camera.name]}
toggleStats={() => toggleStats(camera.name)}
volumeState={volumeStates[camera.name] ?? 1}
volumeState={volumeStates[camera.name]}
setVolumeState={(value) =>
setVolumeStates((prev) => ({
...prev,
@@ -839,7 +845,7 @@ export default function DraggableGridLayout({
onError={(e) => handleError(camera.name, e)}
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
playAudio={audioStates[camera.name]}
volume={volumeStates[camera.name] ?? 1}
volume={volumeStates[camera.name]}
onLiveAspectChange={liveAspectHandlers[camera.name]}
/>
</GridLiveContextMenu>
+8 -10
View File
@@ -511,14 +511,7 @@ export default function LiveCameraView({
}
}, [webRTCVerdictPending]);
const toggleMic = () => {
setMic(!mic);
if (!mic && !audio) {
setAudio(true);
}
};
useKeyboardListener(["m", "t", "Escape"], (key, modifiers) => {
useKeyboardListener(["m", "Escape"], (key, modifiers) => {
if (!modifiers.down) {
return true;
}
@@ -532,7 +525,7 @@ export default function LiveCameraView({
break;
case "t":
if (supports2WayTalk && talkAvailable) {
toggleMic();
setMic(!mic);
return true;
}
break;
@@ -799,7 +792,12 @@ export default function LiveCameraView({
? t("twoWayTalk.disable", { ns: "views/live" })
: t("twoWayTalk.enable", { ns: "views/live" })
}
onClick={toggleMic}
onClick={() => {
setMic(!mic);
if (!mic && !audio) {
setAudio(true);
}
}}
disabled={!cameraEnabled || debug || !talkAvailable}
/>
)}
+14 -8
View File
@@ -326,19 +326,25 @@ export default function LiveDashboardView({
};
useEffect(() => {
if (!allGroupsStreamingSettings) {
return;
}
const initialAudioStates: AudioState = {};
const initialVolumeStates: VolumeState = {};
Object.entries(currentGroupStreamingSettings ?? {}).forEach(
([camera, cameraSettings]) => {
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
initialVolumeStates[camera] = cameraSettings.volume ?? 1;
},
);
Object.entries(allGroupsStreamingSettings).forEach(([_, groupSettings]) => {
if (groupSettings) {
Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
initialVolumeStates[camera] = cameraSettings.volume ?? 1;
});
}
});
setAudioStates(initialAudioStates);
setVolumeStates(initialVolumeStates);
}, [currentGroupStreamingSettings, cameraGroup]);
}, [allGroupsStreamingSettings]);
const toggleAudio = (cameraName: string): void => {
setAudioStates((prev) => ({
@@ -629,7 +635,7 @@ export default function LiveDashboardView({
resetPreferredLiveMode(camera.name)
}
playAudio={audioStates[camera.name] ?? false}
volume={volumeStates[camera.name] ?? 1}
volume={volumeStates[camera.name]}
/>
</LiveContextMenu>
);
@@ -86,21 +86,17 @@ export default function MediaSyncSettingsView() {
position: "top-center",
closeButton: true,
});
}
} catch (error) {
const alreadyRunning =
axios.isAxiosError(error) && error.response?.status === 409;
toast.error(
t(
alreadyRunning
? "maintenance.sync.alreadyRunning"
: "maintenance.sync.error",
),
{
} else if (response.status === 409) {
toast.error(t("maintenance.sync.alreadyRunning"), {
position: "top-center",
closeButton: true,
},
);
});
}
} catch {
toast.error(t("maintenance.sync.error"), {
position: "top-center",
closeButton: true,
});
} finally {
setIsSubmitting(false);
}
+5 -8
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import useSWR from "swr";
@@ -133,9 +133,6 @@ export default function TriggerView({
// watch websocket for updates
const { payload: triggers_status_ws } = useTriggers();
const highlightTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>(
{},
);
useEffect(() => {
if (!triggers_status_ws) return;
@@ -151,13 +148,13 @@ export default function TriggerView({
return current;
});
const name = triggers_status_ws.name;
clearTimeout(highlightTimers.current[name]);
highlightTimers.current[name] = setTimeout(() => {
const timeout = setTimeout(() => {
setTriggeredTrigger((prev) =>
(prev || []).filter((triggered) => triggered !== name),
(prev || []).filter((name) => name !== triggers_status_ws.name),
);
}, 3000);
return () => clearTimeout(timeout);
}, [triggers_status_ws, selectedCamera, mutate]);
useEffect(() => {