mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-06 19:07:20 +03:00
Miscellaneous fixes (#23279)
* use monotonic clock for detector inference duration to prevent negative values from wall clock steps * add ability to set camera's webui_url from camera management pane * Gemini send thought signature * Update docs * copy face and lpr configs from source camera to replay camera * add guard * improve dummy camera docs * remove version number * fix stale field message after reverting a conditional form field Routes field-level conditional messages through a dedicated React Context instead of merging them into uiSchema. RJSF's Form keeps state.uiSchema sticky across renders during processPendingChange (formData is updated, uiSchema is not), so a previously injected ui:messages array stays attached to a field even after the triggering condition flips back to false. Context propagation re-runs FieldTemplate directly on every provider value change, sidestepping that staleness. * add semantic search field message to note that model_size is irrelevant when embeddings provider is selected --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
co-authored by
Nicolas Mowen
parent
a4a592b4e6
commit
0bdf5002a0
@@ -238,6 +238,10 @@ class DebugReplayManager:
|
||||
zone_dump.setdefault("coordinates", zone_config.coordinates)
|
||||
zones_dict[zone_name] = zone_dump
|
||||
|
||||
# Extract LPR and face recognition configs
|
||||
lpr_dict = source_config.lpr.model_dump()
|
||||
face_recognition_dict = source_config.face_recognition.model_dump()
|
||||
|
||||
# Extract motion config (exclude runtime fields)
|
||||
motion_dict = {}
|
||||
if source_config.motion is not None:
|
||||
@@ -287,8 +291,8 @@ class DebugReplayManager:
|
||||
},
|
||||
"birdseye": {"enabled": False},
|
||||
"audio": {"enabled": False},
|
||||
"lpr": {"enabled": False},
|
||||
"face_recognition": {"enabled": False},
|
||||
"lpr": lpr_dict,
|
||||
"face_recognition": face_recognition_dict,
|
||||
}
|
||||
|
||||
def _cleanup_db(self, camera_name: str) -> None:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Gemini Provider for Frigate AI."""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, AsyncGenerator, Optional
|
||||
@@ -14,6 +16,27 @@ from frigate.genai import GenAIClient, register_genai_provider
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _decode_thought_signature(value: Any) -> Optional[bytes]:
|
||||
"""Decode a base64-encoded thought_signature carried across conversation turns."""
|
||||
if not value:
|
||||
return None
|
||||
if isinstance(value, bytes):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return base64.b64decode(value)
|
||||
except (binascii.Error, ValueError):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _encode_thought_signature(signature: Optional[bytes]) -> Optional[str]:
|
||||
"""Encode bytes thought_signature as base64 so it survives JSON-friendly transport."""
|
||||
if not signature:
|
||||
return None
|
||||
return base64.b64encode(signature).decode("ascii")
|
||||
|
||||
|
||||
def _stats_from_gemini_usage(usage: Any) -> Optional[dict[str, Any]]:
|
||||
"""Build a stats dict from a Gemini usage_metadata object."""
|
||||
prompt_tokens = getattr(usage, "prompt_token_count", None)
|
||||
@@ -169,11 +192,17 @@ class GeminiClient(GenAIClient):
|
||||
if not isinstance(tc_args, dict):
|
||||
tc_args = {}
|
||||
if tc_name:
|
||||
parts.append(
|
||||
types.Part.from_function_call(
|
||||
name=tc_name, args=tc_args
|
||||
)
|
||||
fc_part = types.Part.from_function_call(
|
||||
name=tc_name, args=tc_args
|
||||
)
|
||||
# Thinking-capable Gemini models require the original
|
||||
# thought_signature to be echoed back on functionCall
|
||||
# parts after a tool response, or the next request
|
||||
# fails with INVALID_ARGUMENT.
|
||||
sig = _decode_thought_signature(tc.get("thought_signature"))
|
||||
if sig:
|
||||
fc_part.thought_signature = sig
|
||||
parts.append(fc_part)
|
||||
if not parts:
|
||||
parts.append(types.Part.from_text(text=" "))
|
||||
gemini_messages.append(types.Content(role="model", parts=parts))
|
||||
@@ -310,6 +339,9 @@ class GeminiClient(GenAIClient):
|
||||
"id": part.function_call.name or "",
|
||||
"name": part.function_call.name or "",
|
||||
"arguments": arguments,
|
||||
"thought_signature": _encode_thought_signature(
|
||||
getattr(part, "thought_signature", None)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -418,11 +450,17 @@ class GeminiClient(GenAIClient):
|
||||
if not isinstance(tc_args, dict):
|
||||
tc_args = {}
|
||||
if tc_name:
|
||||
parts.append(
|
||||
types.Part.from_function_call(
|
||||
name=tc_name, args=tc_args
|
||||
)
|
||||
fc_part = types.Part.from_function_call(
|
||||
name=tc_name, args=tc_args
|
||||
)
|
||||
# Thinking-capable Gemini models require the original
|
||||
# thought_signature to be echoed back on functionCall
|
||||
# parts after a tool response, or the next request
|
||||
# fails with INVALID_ARGUMENT.
|
||||
sig = _decode_thought_signature(tc.get("thought_signature"))
|
||||
if sig:
|
||||
fc_part.thought_signature = sig
|
||||
parts.append(fc_part)
|
||||
if not parts:
|
||||
parts.append(types.Part.from_text(text=" "))
|
||||
gemini_messages.append(types.Content(role="model", parts=parts))
|
||||
@@ -588,6 +626,7 @@ class GeminiClient(GenAIClient):
|
||||
"id": tool_call_id,
|
||||
"name": tool_call_name,
|
||||
"arguments": "",
|
||||
"thought_signature": None,
|
||||
}
|
||||
|
||||
# Accumulate arguments
|
||||
@@ -598,6 +637,13 @@ class GeminiClient(GenAIClient):
|
||||
else str(arguments)
|
||||
)
|
||||
|
||||
# Capture latest thought_signature for this call
|
||||
chunk_sig = getattr(part, "thought_signature", None)
|
||||
if chunk_sig:
|
||||
tool_calls_by_index[found_index][
|
||||
"thought_signature"
|
||||
] = chunk_sig
|
||||
|
||||
# Build final message
|
||||
full_content = "".join(content_parts).strip() or None
|
||||
full_reasoning = "".join(reasoning_parts).strip() or None
|
||||
@@ -618,6 +664,9 @@ class GeminiClient(GenAIClient):
|
||||
"id": tc["id"],
|
||||
"name": tc["name"],
|
||||
"arguments": parsed_args,
|
||||
"thought_signature": _encode_thought_signature(
|
||||
tc.get("thought_signature")
|
||||
),
|
||||
}
|
||||
)
|
||||
finish_reason = "tool_calls"
|
||||
|
||||
@@ -69,6 +69,14 @@ def build_assistant_message_for_conversation(
|
||||
"name": tc["name"],
|
||||
"arguments": json.dumps(tc.get("arguments") or {}),
|
||||
},
|
||||
# Gemini-only: opaque signature that must be echoed back on
|
||||
# the same functionCall part in the next turn. Other providers
|
||||
# do not set or read this.
|
||||
**(
|
||||
{"thought_signature": tc["thought_signature"]}
|
||||
if tc.get("thought_signature")
|
||||
else {}
|
||||
),
|
||||
}
|
||||
for tc in tool_calls_raw
|
||||
]
|
||||
|
||||
@@ -167,8 +167,9 @@ class DetectorRunner(FrigateProcess):
|
||||
|
||||
# detect and send the output
|
||||
self.start_time.value = datetime.datetime.now().timestamp()
|
||||
mono_start = time.monotonic()
|
||||
detections = object_detector.detect_raw(input_frame)
|
||||
duration = datetime.datetime.now().timestamp() - self.start_time.value
|
||||
duration = time.monotonic() - mono_start
|
||||
frame_manager.close(connection_id)
|
||||
|
||||
if connection_id not in self.outputs:
|
||||
|
||||
@@ -1331,6 +1331,8 @@ class PtzAutoTracker:
|
||||
return self.tracked_object[camera]["region"]
|
||||
|
||||
def autotrack_object(self, camera: str, obj: TrackedObject):
|
||||
if camera not in self.config.cameras:
|
||||
return
|
||||
camera_config = self.config.cameras[camera]
|
||||
|
||||
if camera_config.onvif.autotracking.enabled:
|
||||
|
||||
Reference in New Issue
Block a user