mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-01 19:17:41 +03:00
Miscellaneous fixes (#23032)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
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
Some checks are pending
CI / AMD64 Build (push) Waiting to run
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
* ensure embeddings process restarts after maintainer thread crash * add docs link to media sync settings * fix color Co-authored-by: Copilot <copilot@github.com> * match link color with other sections * ensure recording staleness threshold scales with segment_time * docs tweak * Fix llama.cpp media marker * Fix gemini tools call --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
parent
a182385618
commit
95b5b89ed9
@ -171,7 +171,7 @@ When choosing images to include in the face training set it is recommended to al
|
||||
- If it is difficult to make out details in a persons face it will not be helpful in training.
|
||||
- Avoid images with extreme under/over-exposure.
|
||||
- Avoid blurry / pixelated images.
|
||||
- Avoid training on infrared (gray-scale). The models are trained on color images and will be able to extract features from gray-scale images.
|
||||
- Avoid training on infrared (gray-scale). The models are trained on color images and will not be able to extract features from gray-scale images.
|
||||
- Using images of people wearing hats / sunglasses may confuse the model.
|
||||
- Do not upload too many similar images at the same time, it is recommended to train no more than 4-6 similar images for each person to avoid over-fitting.
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from json.decoder import JSONDecodeError
|
||||
from multiprocessing.synchronize import Event as MpEvent
|
||||
@ -52,6 +53,14 @@ class EmbeddingProcess(FrigateProcess):
|
||||
self.stop_event,
|
||||
)
|
||||
maintainer.start()
|
||||
maintainer.join()
|
||||
|
||||
# If the maintainer thread exited but no shutdown was requested, it
|
||||
# crashed. Surface as a non-zero exit so the watchdog restarts us
|
||||
# instead of treating the silent thread death as a clean shutdown.
|
||||
if not self.stop_event.is_set():
|
||||
logger.error("Embeddings maintainer thread exited unexpectedly")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class EmbeddingsContext:
|
||||
|
||||
@ -143,15 +143,17 @@ class GeminiClient(GenAIClient):
|
||||
)
|
||||
elif role == "tool":
|
||||
# Handle tool response
|
||||
function_response = {
|
||||
"name": msg.get("name", ""),
|
||||
"response": content,
|
||||
}
|
||||
response_payload = (
|
||||
content if isinstance(content, dict) else {"result": content}
|
||||
)
|
||||
gemini_messages.append(
|
||||
types.Content(
|
||||
role="function",
|
||||
parts=[
|
||||
types.Part.from_function_response(function_response) # type: ignore[misc,call-arg,arg-type]
|
||||
types.Part.from_function_response(
|
||||
name=msg.get("name", ""),
|
||||
response=response_payload,
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
@ -350,15 +352,17 @@ class GeminiClient(GenAIClient):
|
||||
)
|
||||
elif role == "tool":
|
||||
# Handle tool response
|
||||
function_response = {
|
||||
"name": msg.get("name", ""),
|
||||
"response": content,
|
||||
}
|
||||
response_payload = (
|
||||
content if isinstance(content, dict) else {"result": content}
|
||||
)
|
||||
gemini_messages.append(
|
||||
types.Content(
|
||||
role="function",
|
||||
parts=[
|
||||
types.Part.from_function_response(function_response) # type: ignore[misc,call-arg,arg-type]
|
||||
types.Part.from_function_response(
|
||||
name=msg.get("name", ""),
|
||||
response=response_payload,
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
@ -44,6 +44,7 @@ class LlamaCppClient(GenAIClient):
|
||||
_supports_tools: bool
|
||||
_image_token_cache: dict[tuple[int, int], int]
|
||||
_text_baseline_tokens: int | None
|
||||
_media_marker: str
|
||||
|
||||
def _init_provider(self) -> str | None:
|
||||
"""Initialize the client and query model metadata from the server."""
|
||||
@ -56,6 +57,7 @@ class LlamaCppClient(GenAIClient):
|
||||
self._supports_tools = False
|
||||
self._image_token_cache = {}
|
||||
self._text_baseline_tokens = None
|
||||
self._media_marker = "<__media__>"
|
||||
|
||||
base_url = (
|
||||
self.genai_config.base_url.rstrip("/")
|
||||
@ -141,6 +143,13 @@ class LlamaCppClient(GenAIClient):
|
||||
chat_caps = props.get("chat_template_caps", {})
|
||||
self._supports_tools = chat_caps.get("supports_tools", False)
|
||||
|
||||
# Media marker for multimodal embeddings; the server randomizes this
|
||||
# per startup unless LLAMA_MEDIA_MARKER is set, so we must read it
|
||||
# from /props rather than hardcoding "<__media__>".
|
||||
media_marker = props.get("media_marker")
|
||||
if isinstance(media_marker, str) and media_marker:
|
||||
self._media_marker = media_marker
|
||||
|
||||
logger.info(
|
||||
"llama.cpp model '%s' initialized — context: %s, vision: %s, audio: %s, tools: %s",
|
||||
configured_model,
|
||||
@ -465,10 +474,11 @@ class LlamaCppClient(GenAIClient):
|
||||
jpeg_bytes = _to_jpeg(img)
|
||||
to_encode = jpeg_bytes if jpeg_bytes is not None else img
|
||||
encoded = base64.b64encode(to_encode).decode("utf-8")
|
||||
# prompt_string must contain <__media__> placeholder for image tokenization
|
||||
# prompt_string must contain the server's media marker placeholder.
|
||||
# The marker is randomized per server startup (read from /props).
|
||||
content.append(
|
||||
{
|
||||
"prompt_string": "<__media__>\n",
|
||||
"prompt_string": f"{self._media_marker}\n",
|
||||
"multimodal_data": [encoded], # type: ignore[dict-item]
|
||||
}
|
||||
)
|
||||
|
||||
@ -24,7 +24,7 @@ from frigate.config.camera.updater import (
|
||||
)
|
||||
from frigate.const import PROCESS_PRIORITY_HIGH
|
||||
from frigate.log import LogPipe
|
||||
from frigate.util.builtin import EventsPerSecond
|
||||
from frigate.util.builtin import EventsPerSecond, get_ffmpeg_arg_list
|
||||
from frigate.util.ffmpeg import start_or_restart_ffmpeg, stop_ffmpeg
|
||||
from frigate.util.image import (
|
||||
FrameManager,
|
||||
@ -34,6 +34,23 @@ from frigate.util.process import FrigateProcess
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# all built-in record presets use this segment_time
|
||||
DEFAULT_RECORD_SEGMENT_TIME = 10
|
||||
|
||||
|
||||
def _get_record_segment_time(config: CameraConfig) -> int:
|
||||
"""Extract -segment_time from the camera's record output args."""
|
||||
record_args = get_ffmpeg_arg_list(config.ffmpeg.output_args.record)
|
||||
|
||||
if record_args and record_args[0].startswith("preset"):
|
||||
return DEFAULT_RECORD_SEGMENT_TIME
|
||||
|
||||
try:
|
||||
idx = record_args.index("-segment_time")
|
||||
return int(record_args[idx + 1])
|
||||
except (ValueError, IndexError):
|
||||
return DEFAULT_RECORD_SEGMENT_TIME
|
||||
|
||||
|
||||
def capture_frames(
|
||||
ffmpeg_process: sp.Popen[Any],
|
||||
@ -164,6 +181,12 @@ class CameraWatchdog(threading.Thread):
|
||||
self.latest_cache_segment_time: float = 0
|
||||
self.record_enable_time: datetime | None = None
|
||||
|
||||
# `valid` segments are published with the segment's start time, so the
|
||||
# gap between consecutive publishes can reach 2 * segment_time. Pad the
|
||||
# staleness threshold so it's never tighter than that worst case.
|
||||
segment_time = _get_record_segment_time(self.config)
|
||||
self.record_stale_threshold = max(120, 2 * segment_time + 30)
|
||||
|
||||
# Stall tracking (based on last processed frame)
|
||||
self._stall_timestamps: deque[float] = deque()
|
||||
self._stall_active: bool = False
|
||||
@ -413,16 +436,17 @@ class CameraWatchdog(threading.Thread):
|
||||
|
||||
# ensure segments are still being created and that they have valid video data
|
||||
# Skip checks during grace period to allow segments to start being created
|
||||
stale_window = timedelta(seconds=self.record_stale_threshold)
|
||||
cache_stale = not in_grace_period and now_utc > (
|
||||
latest_cache_dt + timedelta(seconds=120)
|
||||
latest_cache_dt + stale_window
|
||||
)
|
||||
valid_stale = not in_grace_period and now_utc > (
|
||||
latest_valid_dt + timedelta(seconds=120)
|
||||
latest_valid_dt + stale_window
|
||||
)
|
||||
invalid_stale_condition = (
|
||||
self.latest_invalid_segment_time > 0
|
||||
and not in_grace_period
|
||||
and now_utc > (latest_invalid_dt + timedelta(seconds=120))
|
||||
and now_utc > (latest_invalid_dt + stale_window)
|
||||
and self.latest_valid_segment_time
|
||||
<= self.latest_invalid_segment_time
|
||||
)
|
||||
@ -439,7 +463,7 @@ class CameraWatchdog(threading.Thread):
|
||||
)
|
||||
|
||||
self.logger.error(
|
||||
f"{reason} for {self.config.name} in the last 120s. Restarting the ffmpeg record process..."
|
||||
f"{reason} for {self.config.name} in the last {self.record_stale_threshold}s. Restarting the ffmpeg record process..."
|
||||
)
|
||||
p["process"] = start_or_restart_ffmpeg(
|
||||
p["cmd"],
|
||||
|
||||
@ -28,6 +28,7 @@ class MonitoredProcess:
|
||||
restart_timestamps: deque[float] = field(
|
||||
default_factory=lambda: deque(maxlen=MAX_RESTARTS)
|
||||
)
|
||||
clean_exit_logged: bool = False
|
||||
|
||||
def is_restarting_too_fast(self, now: float) -> bool:
|
||||
while (
|
||||
@ -72,7 +73,9 @@ class FrigateWatchdog(threading.Thread):
|
||||
|
||||
exitcode = entry.process.exitcode
|
||||
if exitcode == 0:
|
||||
logger.info("Process %s exited cleanly, not restarting", entry.name)
|
||||
if not entry.clean_exit_logged:
|
||||
logger.info("Process %s exited cleanly, not restarting", entry.name)
|
||||
entry.clean_exit_logged = True
|
||||
return
|
||||
|
||||
logger.warning(
|
||||
|
||||
@ -10,13 +10,16 @@ import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { useJobStatus } from "@/api/ws";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { LuCheck, LuX } from "react-icons/lu";
|
||||
import { LuCheck, LuExternalLink, LuX } from "react-icons/lu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import { MediaSyncResults, MediaSyncStats } from "@/types/ws";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function MediaSyncSettingsView() {
|
||||
const { t } = useTranslation("views/settings");
|
||||
const { getLocaleDocUrl } = useDocDomain();
|
||||
const [selectedMediaTypes, setSelectedMediaTypes] = useState<string[]>([
|
||||
"all",
|
||||
]);
|
||||
@ -109,13 +112,25 @@ export default function MediaSyncSettingsView() {
|
||||
<Heading as="h4" className="mb-2 hidden md:block">
|
||||
{t("maintenance.sync.title")}
|
||||
</Heading>
|
||||
|
||||
<div className="max-w-6xl">
|
||||
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-muted-foreground">
|
||||
<p>{t("maintenance.sync.desc")}</p>
|
||||
|
||||
<div className="flex items-center text-primary-variant">
|
||||
<Link
|
||||
to={getLocaleDocUrl(
|
||||
"configuration/record#syncing-media-files-with-disk",
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
{t("readTheDocumentation", { ns: "common" })}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Media Types Selection */}
|
||||
<div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user