Miscellaneous fixes (0.18 beta) (#23718)
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
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
CI / AMD64 Extra Build (push) Blocked by required conditions

* 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>
This commit is contained in:
Josh Hawkins
2026-07-14 18:49:03 -06:00
committed by GitHub
co-authored by Nicolas Mowen
parent 81b53b7835
commit a8eca68438
5 changed files with 177 additions and 49 deletions
+8 -8
View File
@@ -21,8 +21,6 @@ from frigate.config.camera.updater import (
CameraConfigUpdateTopic,
)
from frigate.const import (
CLIPS_DIR,
RECORD_DIR,
REPLAY_CAMERA_PREFIX,
REPLAY_DIR,
THUMB_DIR,
@@ -331,12 +329,14 @@ def cleanup_replay_cameras() -> None:
"""
stale_cameras: set[str] = set()
# Scan filesystem for leftover replay artifacts to derive camera names
for dir_path in [RECORD_DIR, CLIPS_DIR, THUMB_DIR]:
if os.path.isdir(dir_path):
for entry in os.listdir(dir_path):
if entry.startswith(REPLAY_CAMERA_PREFIX):
stale_cameras.add(entry)
# Derive stale camera names from THUMB_DIR (per-camera dirs) and
# REPLAY_DIR (the session's source clip); both listings are bounded by
# camera count. cleanup_camera_files below removes any remaining
# per-camera artifacts (snapshots, thumbnails, LPR images, etc.) by name.
if os.path.isdir(THUMB_DIR):
for entry in os.listdir(THUMB_DIR):
if entry.startswith(REPLAY_CAMERA_PREFIX):
stale_cameras.add(entry)
if os.path.isdir(REPLAY_DIR):
for entry in os.listdir(REPLAY_DIR):
+5 -1
View File
@@ -150,7 +150,11 @@ PRESETS_HW_ACCEL_SCALE["preset-rk-h265"] = PRESETS_HW_ACCEL_SCALE[FFMPEG_HWACCEL
PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = {
"preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m {2}",
"preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m {2}",
FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {2}",
# -vaapi_device is required in addition to -hwaccel_device: this is the only
# birdseye preset that uses hwupload, and ffmpeg 8 initializes filters before
# the decoder creates a device, so hwupload cannot see an -hwaccel_device one.
# See https://github.com/AlexxIT/go2rtc/issues/1984
FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -vaapi_device {3} -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {2}",
"preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {2}",
"preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v main -level:v 4.1 -async_depth:v 1 {2}",
FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner {1} -c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll {2}",
+46 -31
View File
@@ -76,29 +76,6 @@ def _parse_launch_arg(args: list[str], flag: str) -> str | None:
return args[idx + 1]
def _fetch_llama_props(base_url: str, model: str) -> dict[str, Any]:
"""Fetch /props from a llama.cpp server, with llama-swap fallback.
Raises the underlying RequestException if both endpoints fail; callers
decide how to surface the failure.
"""
try:
response = requests.get(
f"{base_url}/props",
params={"model": model},
timeout=10,
)
response.raise_for_status()
return cast(dict[str, Any], response.json())
except Exception:
response = requests.get(
f"{base_url}/upstream/{model}/props",
timeout=10,
)
response.raise_for_status()
return cast(dict[str, Any], response.json())
def _to_jpeg(img_bytes: bytes) -> bytes | None:
"""Convert image bytes to JPEG. llama.cpp/STB does not support WebP."""
try:
@@ -133,6 +110,43 @@ class LlamaCppClient(GenAIClient):
"""llama.cpp exposes an /embeddings endpoint for any loaded model."""
return True
def _auth_headers(self) -> dict | None:
"""Bearer auth header when an API key is configured, else None."""
if self.genai_config.api_key:
return {"Authorization": "Bearer " + self.genai_config.api_key}
return None
def _get(self, url: str, **kwargs: Any) -> requests.Response:
"""GET with the configured auth headers injected."""
return requests.get(url, headers=self._auth_headers(), **kwargs)
def _post(self, url: str, **kwargs: Any) -> requests.Response:
"""POST with the configured auth headers injected."""
return requests.post(url, headers=self._auth_headers(), **kwargs)
def _fetch_llama_props(self, base_url: str, model: str) -> dict[str, Any]:
"""Fetch /props from a llama.cpp server, with llama-swap fallback.
Raises the underlying RequestException if both endpoints fail; callers
decide how to surface the failure.
"""
try:
response = self._get(
f"{base_url}/props",
params={"model": model},
timeout=10,
)
response.raise_for_status()
return cast(dict[str, Any], response.json())
except Exception:
response = self._get(
f"{base_url}/upstream/{model}/props",
timeout=10,
)
response.raise_for_status()
return cast(dict[str, Any], response.json())
def _init_provider(self) -> str | None:
"""Initialize the client and query model metadata from the server."""
self.provider_options = {
@@ -216,7 +230,7 @@ class LlamaCppClient(GenAIClient):
model_entry: dict[str, Any] | None = None
try:
response = requests.get(f"{base_url}/v1/models", timeout=10)
response = self._get(f"{base_url}/v1/models", timeout=10)
response.raise_for_status()
models_data = response.json()
@@ -277,7 +291,7 @@ class LlamaCppClient(GenAIClient):
info["supports_tools"] = True
try:
props = _fetch_llama_props(base_url, configured_model)
props = self._fetch_llama_props(base_url, configured_model)
if info["context_size"] is None:
default_settings = props.get("default_generation_settings", {})
@@ -363,7 +377,7 @@ class LlamaCppClient(GenAIClient):
if self.supports_toggleable_thinking:
payload["chat_template_kwargs"] = {"enable_thinking": enable_thinking}
response = requests.post(
response = self._post(
f"{self.provider}/v1/chat/completions",
json=payload,
timeout=self.timeout,
@@ -413,7 +427,7 @@ class LlamaCppClient(GenAIClient):
if base_url is None:
return []
try:
response = requests.get(f"{base_url}/v1/models", timeout=10)
response = self._get(f"{base_url}/v1/models", timeout=10)
response.raise_for_status()
models = []
for m in response.json().get("data", []):
@@ -516,7 +530,7 @@ class LlamaCppClient(GenAIClient):
"messages": [{"role": "user", "content": content}],
"max_tokens": 1,
}
response = requests.post(
response = self._post(
f"{self.provider}/v1/chat/completions",
json=payload,
timeout=60,
@@ -626,7 +640,7 @@ class LlamaCppClient(GenAIClient):
if self.provider is None:
return False
try:
props = _fetch_llama_props(self.provider, self.genai_config.model)
props = self._fetch_llama_props(self.provider, self.genai_config.model)
except Exception as e:
logger.warning("Failed to refresh llama.cpp media marker: %s", e)
return False
@@ -687,7 +701,7 @@ class LlamaCppClient(GenAIClient):
return content
def post_embeddings() -> requests.Response:
return requests.post(
return self._post(
f"{self.provider}/embeddings",
json={"model": self.genai_config.model, "content": build_content()},
timeout=self.timeout,
@@ -791,7 +805,7 @@ class LlamaCppClient(GenAIClient):
stream=False,
enable_thinking=enable_thinking,
)
response = requests.post(
response = self._post(
f"{self.provider}/v1/chat/completions",
json=payload,
timeout=self.timeout,
@@ -872,6 +886,7 @@ class LlamaCppClient(GenAIClient):
"POST",
f"{self.provider}/v1/chat/completions",
json=payload,
headers=self._auth_headers(),
) as response:
response.raise_for_status()
async for line in response.aiter_lines():
+1 -1
View File
@@ -401,7 +401,7 @@ class _FakeAsyncClient:
async def __aexit__(self, *exc):
return False
def stream(self, method, url, json=None):
def stream(self, method, url, json=None, headers=None):
return _FakeStreamCtx(self._lines)