cleanup's remove_empty_directories() can rmdir an empty clips/review, after which thumbnail writes silently fail. Ensure the directory exists before both cv2.imwrite calls and check their return value
* resolve saved credential sentinel to the stored api_key in the GenAI probe
* add profile faq
* center the multi-camera export time range on the current playback position
* add faq about preview restart cache
* clarify exports bulk download
* Catch faces that become empty after cropping
* don't drop batched camera add/remove config updates
TrackedObjectProcessor drained all pending camera config updates at once but handled them in a mutually exclusive if/elif on enabled/add/remove, so only one topic was processed per drain. When an add arrived in the same batch as an enabled update, the add was skipped and the new camera never got a camera state. Adding a camera reliably produced that batch: config_set now re-applies runtime overrides, which republishes an enabled update for every previously toggled camera immediately before the add, in the same request. The dashboard and camera capture still saw the camera (the maintainer does not subscribe to enabled, so it got a clean add-only batch), but object_processing did not, and disabling the camera then crashed with a KeyError on the unguarded camera_states lookup.
Handle add and remove independently instead of as exclusive branches so a batched add is no longer dropped, and guard the remove lookup so a missing state is skipped rather than raising. Drop the enabled branch entirely: it only ever set prev_enabled when it was None, but prev_enabled is seeded to a bool at camera state creation and is never None (mypy flags the body as unreachable), and the actual enable/disable transition is already driven by the disabled-state loop from config.enabled.
* Don't stay on motion search page when user cancels flow
* fix notification test button being blocked by websocket auth
* fix overflowing model names in settings genai widget
* add note about auth debugging
---------
Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
/api/config/set and camera deletion re-parse yaml into a fresh FrigateConfig and swap it in, then re-layered the persisted runtime toggle overrides so a camera the user turned off wouldn't come back on. That re-layer ran apply_runtime_state, which replays each override through the command handlers, so every save re-published a ZMQ config update, a retained MQTT state message, and a runtime-state disk write for every camera with a stored toggle. All of it was redundant: the worker processes were never swapped and still hold the live toggle values, so only the in-process config object the API and dispatcher read was out of date. The extra traffic churned the retained MQTT topics, amplified disk writes, and co-drained enabled updates with other topics on the config socket.
Add Dispatcher.reapply_runtime_state_to_config, which corrects only the swapped-in config object, mirroring the field mutations and gates of the _on_*_command handlers with no ZMQ, MQTT, or disk writes. swap_runtime_config now calls it instead of apply_runtime_state; apply_runtime_state is unchanged and still used at startup, where the workers genuinely must be told.
* preserve runtime camera toggles across config saves
Runtime toggles (camera on/off, detect, recordings, snapshots, audio) mutate the in-memory config and persist an override to .runtime_state.json. /api/config/set re-parses yaml into a fresh FrigateConfig and swaps it in, re-applying the yaml and profile layers but dropping the runtime layer, so a camera turned off from the dashboard came back on when an unrelated camera was saved. The workers were never notified, so it only appeared to come back: the UI streamed go2rtc while ffmpeg stayed stopped.
Extract the startup replay into Dispatcher.apply_runtime_state() and call it from config_set after the swap, re-layering the overrides and republishing them so workers and the UI reconverge.
Remove the broad clear_runtime_state() from ProfileManager.update_config, which is only ever reached from config_set: with a profile active, every save wiped every camera's overrides from disk. The broad wipe stays in activate_profile, where a real profile switch does invalidate the steady state. Saves still clear the keys they rewrote via clear_runtime_state_for_yaml_keys, so yaml wins where the two disagree.
* sync runtime config on camera delete and prune its overrides
Deleting a camera re-parsed yaml into a fresh FrigateConfig but only rebound app.frigate_config and genai_manager, never dispatcher.config (nor profile_manager, stats_emitter, or the runtime overrides). The API and the dispatcher then drifted onto different config objects until the next config save re-synced them, so the API reported surviving cameras with their yaml enabled state while the dispatcher still acted on their real runtime state.
Extract the config swap that config_set already does into a shared swap_runtime_config helper and call it from both sites, so every collaborator is rebound and the surviving cameras' runtime toggles are re-layered. Also drop the deleted camera's persisted overrides via a new clear_camera so a camera later added under the same name does not inherit them.
* Cleanup llama.cpp and use api key when configured
* don't report auto-populated object and audio filters as camera overrides
* derive stale replay cameras from bounded directory listings to avoid scanning all clips at startup
* fix tests
* add -vaapi_device to the birdseye vaapi encode preset so hwupload can initialize on ffmpeg 8
---------
Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
* resolve zone friendly names against the correct camera
* Improve handling of zone names in chat prompt
* show a numeric keyboard for numeric config form fields on mobile
* Specify english only for semantic search tool when model is JinaV1
* resolve export hwaccel args global value against the correct config path
---------
Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
The recognized_license_plate event filter passed attacker-controlled patterns to re.search on the single serialized SQLite queue thread, letting any authenticated user freeze the whole application with a catastrophic regex. This swaps stdlib re for the regex module with a per-evaluation timeout so a pathological pattern is aborted instead of stalling every database operation.
* Add logout endpoint to Nginx configuration to prevent logout from silently generating a new frigate_token cookie
* Change JWT cookie expiration to use max_age and have the appropriate expiration time based on JWT_SESSION_LENGTH
* ruff formatting
* Convert face crops to RGB before embedding
Face crops flow through the cv2 pipeline as BGR arrays, but
_process_image passes ndarrays to PIL without any channel conversion,
so the FaceNet and ArcFace embedders receive BGR input while both
models expect RGB. The error is symmetric between enrollment and
recognition so it partially cancels, but it still costs accuracy.
* Move BGR to RGB conversion into a shared helper
Deduplicate the channel swap from both _preprocess_inputs methods
into a BaseEmbedding._bgr_to_rgb static helper, as suggested in
review.
* add ability to edit enabled and save_attempts for classification models in the UI
* add state motion and interval configs to edit dialog
* fix preview playback rate for motion previews
* add docs note about environment vars and go2rtc
* update live view faq
* honor enabled flag for custom classification models
for both startup and dynamically, even though the UI doesn't currently have a way to toggle dynamically
* add test
* Pin ruff
* Add python upgrade fixes
This enables python upgrade checks in ruff to look for deprecated types and patterns. This namely fixes:
- usage of deprecated `Typing` which is now built in
- some specific exceptions which are caught and have new aliases
Some specific UP checks were also ignored as they are stylistic / unimportant and likely to cause bugs
* Remove async blocking calls
Use asyncio.to_thread on two remaining blocking calls to fix hanging event thread loop. Enable this specific rule to block it in the future.
* Use proper logging mechanism
* Correctly format logs
* Raise with context
When raising an exception include the from context to improve debugging
* Cleanup
* fix per-camera notification MQTT topics never being registered
register notifications/set and notifications/suspend callbacks for each camera, and gate the global notifications topics on per-camera config as well as global (matching WebPushClient creation in app.py). Unregistered topics were silently dropped by paho since only registered callbacks receive messages.
* add tests
Deleting the last entry of a mapping or sequence via config/set orphaned ruamel's comment tokens, which were then emitted above a flow-style {} / [] at column 0, which is unparseable yaml that failed validation and silently rolled the change back. The fix is to clear the emptied collection's stale comment metadata (and the parent's entry for it) so the dump stays valid. Non-empty collections are left untouched so sibling comments are preserved. This covers both emptied maps and emptied lists.
* fix stale active object indicators on the live dashboard
The camera_activity/<camera> snapshot cache is only written when a client sends onConnect, and object "end" events only update the local state of mounted useCameraActivity hooks, never the cache. As a result, a hook that seeded from a stale cache or missed an "end" event while disconnected showed objects that had already left, with no path to correct itself short of a full page reload.
This change will re-request the snapshot on hook mount (collapsed to one onConnect per task across camera cards), and always re-notify camera_activity topics so hooks reconcile against their own local state instead of relying on snapshot-vs-snapshot comparison, and clear the payload dedup cache on reconnect and resync so byte-identical snapshots still apply.
* docs tweaks
* fix mqtt log message
* use consistent values for lpr debug frame filenames
with millisecond resolution
* apply object events through a functional updater to prevent lost updates
The events effect derived a new objects list from the value captured at render time and wrote the whole list back. When events arrived close
together, a run derived from a stale list erased a concurrent run's removal; the resurrected object then had no remaining "end" event to clear it, and the add branch could mint a duplicate entry that no splice could ever remove, leaving the live dashboard showing active objects the backend had already cleared, until a page reload.
The fix is to apply each event inside setObjects so it operates on the true current list exactly once. Unchanged results return the same reference so React bails out of re-rendering, and the label rewrite is hoisted so added objects get the sub_label/verified label directly instead of relying on the effect re-running against its own state update.
* Handle back seeking going to previous clip
* scope /recordings/unavailable query to the caller's allowed cameras
* listen for config updates in activity manager
* don't set search after awaited request
Intentionally do NOT setSearch() to mark the open event submitted. This runs after the awaited request, by which point the user may have closed the dialog; re-setting the parent's selected event would resurrect it and the force-open effect would reopen it (see #23599). The local "submitted" state covers the open card, and mutate() updates the events cache so the grid and any future open reflect the result.
* fix ruff
#23201 removed pathlib import but for some reason it's just now causing ruff to fail
---------
Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
Resolve conflicts in the export pipeline where dev's job-queue refactor
met master's chapter-metadata and security work.
- Unify chapter support under ChaptersEnum (none / recording_segments /
review_items); the realtime stream-copy export selects the per-segment
or per-review-item builder by the camera's configured mode. Thread
chapters through ExportRecordingsBody -> _build_export_job -> ExportJob
-> RecordingExporter.
- Keep master's creation_time/comment export metadata and fix a
video_path duplication the textual merge introduced in the preview
command.
- Move the chapters request field to ExportRecordingsBody (the single
export endpoint) where it is actually honored.
Restore security fixes the automatic merge would have reverted:
- frigate/util/services.py: restore the #23493 rename to the public
is_go2rtc_arbitrary_exec_allowed so create_config.py's dynamic-source
exec guard imports and runs (the merge otherwise left a broken import).
- Preserve the export image-path ".." traversal check inside
_sanitize_existing_image, applied to single/custom/batch exports.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* offload preview ffmpeg encoding to a thread to avoid blocking the api event loop
* offload Frigate+ recording snapshot upload to a thread to avoid blocking the api event loop
* perf(track): avoid numpy reductions on tiny box lists in position smoothing
update_position runs per tracked object per frame. While a position has
fewer than 10 samples it calls np.percentile four times, and average_boxes
(per stationary object per frame) calls np.mean four times - all on lists of
at most 10 ints, where numpy's per-call dispatch/validation overhead
dominates the actual work.
Replace them with pure-Python equivalents:
- average_boxes: sum()/len() instead of np.mean (bit-identical output)
- interpolated_percentile(): linear-interpolated percentile matching
numpy.percentile (including its lerp branch at frac>=0.5) for the small
lists used here, in place of np.percentile
Measured in the release image (numpy 1.26.4) on a 10-element list:
np.percentile 18735 ns -> 191 ns/call (98x); np.mean-based average_boxes
7480 ns -> 591 ns (12.7x); ~74 us saved per object-frame in update_position.
A live py-spy --gil profile of a camera process_frames worker showed
np.percentile (update_position) and np.mean (average_boxes) among the top
Frigate-owned on-CPU frames.
Output is unchanged: added tests assert both helpers are bit-identical to
numpy over randomized small inputs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Drop interpolated_percentile, keep only average_boxes
Per review: reimplementing np.percentile hurts readability and risks
divergence from numpy (e.g. numpy 2.x). Revert update_position to
np.percentile and remove the helper; keep only the average_boxes change
(sum()/len() instead of np.mean), which stays bit-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two small per-object-per-frame improvements in the tracker hot path
(match_and_update), both bit-identical:
- get_stationary_threshold returned a freshly constructed StationaryThresholds
(a dataclass plus a list) on every call for any label not in the three
known lists - i.e. for common labels like person/dog. The default thresholds
are constant and never mutated, so return a shared module-level singleton,
as the other three cases already do.
- untracked_object_boxes membership used `box not in [list of boxes]` (O(n));
build a set of box tuples for O(1) membership. Boxes are hashable as tuples
and output is unchanged.
get_stationary_threshold appeared in a live py-spy --gil profile of a camera
process_frames worker. Adds tests for the threshold lookups.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
video/detect.py runs these for every frame:
- get_cluster_candidates: used_boxes was a list with `in` membership tests
inside the nested loop (O(n) per check). It is only ever membership-tested,
so switching it to a set (O(1)) leaves output unchanged.
- get_consolidated_object_detections: area(current_box) was recomputed on
every inner-loop iteration though it is loop-invariant; hoist it to one
call per outer detection.
Both are bit-identical (verified against the previous implementations over
randomized inputs). Measured in the release image, get_cluster_candidates on
a frame of 30 detection boxes: 59.2 us -> 42.1 us (1.4x); the gain scales
with the number of boxes per frame.
Adds a partition-invariant test (every box index lands in exactly one
cluster).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A record-enabled camera whose record stream produces no cache segments
never appears in grouped_recordings, so the per-camera prune in
RecordingMaintainer.move_files() never runs for it. Its
object_recordings_info and audio_recordings_info buffers then grow
without bound until the recording process is OOM-killed (discussion
#23451).
Run a prune every move_files() cycle for cameras absent from
grouped_recordings, dropping entries older than the longest a segment
could still wait in cache before being matched
(MAX_SEGMENTS_IN_CACHE * MAX_SEGMENT_DURATION * 2). Cameras present in
grouped_recordings are left untouched and keep their existing prune.
Add a regression test asserting that an absent camera's stale entries
are dropped (recent ones kept) while a present camera's entries are
left intact.
Co-authored-by: John Pescatore <johnpescatore@claude.internal.johnpescatore.com>
* perf(util): use monotonic clock and bounded deque in EventsPerSecond
EventsPerSecond is updated on every captured frame, every detection and
every processed frame across all cameras and detectors. The previous
implementation derived timestamps from datetime.now().timestamp() (wall
clock), so an NTP or manual clock adjustment could skew the rolling-window
expiry; it also stored timestamps in a list and expired them with
del self._timestamps[0] (O(n) per removal) plus a periodic slice-copy to
cap growth.
Switch to time.monotonic() for the interval math (correct by construction
and immune to wall-clock jumps) and a collections.deque(maxlen=...) so
expiry is O(1) (popleft) and retention is bounded automatically. This
mirrors the deque-based expiry already used in video/ffmpeg.py and
watchdog.py. Observable output is unchanged.
Adds frigate/test/test_builtin.py covering rate calculation, window
expiry and the memory bound.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: drop test_timestamps_are_memory_bounded
It only asserted that deque(maxlen=) caps length, which is stdlib behavior
rather than something this change needs to verify.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* display zone names consistently using friendly_name or raw id without transformation
* enforce camera-level access on go2rtc live stream websocket endpoints
* slightly darken bg-card
* change menu label
* move snapshot retain out of advanced fields
* add new ui options for collapsibles
* backend title and description
* remove unused snapshot retention field
* update reference config
* remove further references to snapshots retain.mode
* Implement tool call history keeping
* Refactor to match single message implementation
* Simplify data representation
* Cleanup chat page rendering
* Include system message to not break cache
* Formatting
* Update tests and update .gitignore
* update e2e mock data to remove deprecated fields
* remove scream audio label
scream was never mapped to anything in frigate's custom labelmap, yell is used everywhere
* document common audio labels
* deprecate ffmpeg 5
* language tweak
* add field message to recommend presets instead of manual hwaccel args
* add guidance to docs on choosing a detect fps
* serialize OpenVINO inference per process to prevent concurrent-inference segfault
* clean up
* add max scaling meta to login page
* add more detect section field messages
* fix icon layout in settings field messages
* tweak edit icon color
* republish MQTT switch states when a profile is activated or deactivated
* fix object mask default name when created from Explore tracking details
* tweak annotation offset max in UI
* optimize recordings/unavailable gap detection and drop empty motion activity buckets
* add tests