mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-28 19:06:52 +03:00
keep Frigate+ model references when saving the models section
`/api/config` served a Frigate+ model's path as the resolved `/config/model_cache/<id>` file, and since the models list is saved whole, editing any model in the settings UI wrote that cache path back to the config in place of `plus://<id>`. After a restart the model loaded as a custom model with the default labelmap. The config API now reports the `plus://<id>` reference the model was configured with, and the models section drops the fields the Frigate+ model info supplies (size, tensor, pixel format, dtype, and type) instead of pinning them in the config.
This commit is contained in:
@@ -393,6 +393,11 @@ def config(request: Request):
|
|||||||
model_dict["non_logo_attributes"] = model.non_logo_attributes
|
model_dict["non_logo_attributes"] = model.non_logo_attributes
|
||||||
model_dict["labelmap"] = model.merged_labelmap
|
model_dict["labelmap"] = model.merged_labelmap
|
||||||
|
|
||||||
|
# report the configured reference rather than the resolved cache path,
|
||||||
|
# so saving the config back doesn't lose the Frigate+ model
|
||||||
|
if model.plus_id:
|
||||||
|
model_dict["path"] = f"plus://{model.plus_id}"
|
||||||
|
|
||||||
if not config["plus"]["enabled"]:
|
if not config["plus"]["enabled"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class ModelConfig(BaseModel):
|
|||||||
_all_attributes: list[str] = PrivateAttr()
|
_all_attributes: list[str] = PrivateAttr()
|
||||||
_all_attribute_logos: list[str] = PrivateAttr()
|
_all_attribute_logos: list[str] = PrivateAttr()
|
||||||
_model_hash: str = PrivateAttr()
|
_model_hash: str = PrivateAttr()
|
||||||
|
_plus_id: str | None = PrivateAttr(default=None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def merged_labelmap(self) -> dict[int, str]:
|
def merged_labelmap(self) -> dict[int, str]:
|
||||||
@@ -148,6 +149,11 @@ class ModelConfig(BaseModel):
|
|||||||
def model_hash(self) -> str:
|
def model_hash(self) -> str:
|
||||||
return self._model_hash
|
return self._model_hash
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plus_id(self) -> str | None:
|
||||||
|
"""The Frigate+ model id, once a plus:// path has been resolved."""
|
||||||
|
return self._plus_id
|
||||||
|
|
||||||
def __init__(self, **config):
|
def __init__(self, **config):
|
||||||
super().__init__(**config)
|
super().__init__(**config)
|
||||||
|
|
||||||
@@ -178,6 +184,7 @@ class ModelConfig(BaseModel):
|
|||||||
os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
|
os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
|
||||||
|
|
||||||
model_id = self.path[7:]
|
model_id = self.path[7:]
|
||||||
|
self._plus_id = model_id
|
||||||
self.path = os.path.join(MODEL_CACHE_DIR, model_id)
|
self.path = os.path.join(MODEL_CACHE_DIR, model_id)
|
||||||
model_info_path = f"{self.path}.json"
|
model_info_path = f"{self.path}.json"
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import frigate.genai
|
import frigate.genai
|
||||||
from frigate.config import GenAIProviderEnum
|
from frigate.config import GenAIProviderEnum
|
||||||
from frigate.const import REDACTED_CREDENTIAL_SENTINEL
|
from frigate.const import MODEL_CACHE_DIR, REDACTED_CREDENTIAL_SENTINEL
|
||||||
from frigate.genai import GenAIClient
|
from frigate.genai import GenAIClient
|
||||||
from frigate.models import Event, Recordings, ReviewSegment
|
from frigate.models import Event, Recordings, ReviewSegment
|
||||||
from frigate.stats.emitter import StatsEmitter
|
from frigate.stats.emitter import StatsEmitter
|
||||||
@@ -90,6 +92,44 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
mqtt = response.json()["mqtt"]
|
mqtt = response.json()["mqtt"]
|
||||||
assert mqtt["password"] == REDACTED_CREDENTIAL_SENTINEL
|
assert mqtt["password"] == REDACTED_CREDENTIAL_SENTINEL
|
||||||
|
|
||||||
|
def test_config_response_keeps_plus_model_reference(self):
|
||||||
|
model_id = "test_plus_reference"
|
||||||
|
model_path = os.path.join(MODEL_CACHE_DIR, model_id)
|
||||||
|
os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
with open(model_path, "w") as f:
|
||||||
|
f.write("model")
|
||||||
|
|
||||||
|
with open(f"{model_path}.json", "w") as f:
|
||||||
|
json.dump(
|
||||||
|
{
|
||||||
|
"id": model_id,
|
||||||
|
"type": "ssd",
|
||||||
|
"supportedDetectors": ["cpu"],
|
||||||
|
"width": 320,
|
||||||
|
"height": 320,
|
||||||
|
"inputShape": "nhwc",
|
||||||
|
"pixelFormat": "rgb",
|
||||||
|
"labelMap": {"0": "person"},
|
||||||
|
},
|
||||||
|
f,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.addCleanup(os.remove, model_path)
|
||||||
|
self.addCleanup(os.remove, f"{model_path}.json")
|
||||||
|
self.minimal_config["models"] = [
|
||||||
|
{"path": f"plus://{model_id}", "devices": ["cpu"]}
|
||||||
|
]
|
||||||
|
app = super().create_app()
|
||||||
|
|
||||||
|
with AuthTestClient(app) as client:
|
||||||
|
response = client.get("/config")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["models"][0]["path"] == f"plus://{model_id}"
|
||||||
|
|
||||||
|
# detection still loads the resolved cache file
|
||||||
|
assert app.frigate_config.models[0].path == model_path
|
||||||
|
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
################################### POST /genai/probe Endpoint ##################################################
|
################################### POST /genai/probe Endpoint ##################################################
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { RJSFSchema } from "@rjsf/utils";
|
import { RJSFSchema } from "@rjsf/utils";
|
||||||
|
import { omit } from "lodash";
|
||||||
import { applySchemaDefaults } from "@/lib/config-schema";
|
import { applySchemaDefaults } from "@/lib/config-schema";
|
||||||
import { isJsonObject } from "@/lib/utils";
|
import { isJsonObject } from "@/lib/utils";
|
||||||
import { HiddenFieldContext, JsonObject, JsonValue } from "@/types/configForm";
|
import { HiddenFieldContext, JsonObject, JsonValue } from "@/types/configForm";
|
||||||
@@ -352,6 +353,17 @@ export function synthesizeMissingFilters(
|
|||||||
return { ...(data as JsonObject), filters: newFilters };
|
return { ...(data as JsonObject), filters: newFilters };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the backend fills these from the Frigate+ model info when it loads a
|
||||||
|
// plus:// model, so saving them would only pin values Frigate+ owns
|
||||||
|
const PLUS_SUPPLIED_MODEL_FIELDS = [
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"input_tensor",
|
||||||
|
"input_pixel_format",
|
||||||
|
"input_dtype",
|
||||||
|
"model_type",
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitize overrides payloads for section-specific quirks.
|
* Sanitize overrides payloads for section-specific quirks.
|
||||||
*/
|
*/
|
||||||
@@ -360,6 +372,21 @@ export function sanitizeOverridesForSection(
|
|||||||
level: string,
|
level: string,
|
||||||
overrides: unknown,
|
overrides: unknown,
|
||||||
): unknown {
|
): unknown {
|
||||||
|
// the models list is saved whole
|
||||||
|
if (sectionPath === "models" && Array.isArray(overrides)) {
|
||||||
|
return overrides.map((model) => {
|
||||||
|
if (
|
||||||
|
!isJsonObject(model) ||
|
||||||
|
typeof model.path !== "string" ||
|
||||||
|
!model.path.startsWith("plus://")
|
||||||
|
) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
return omit(model, PLUS_SUPPLIED_MODEL_FIELDS);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!overrides || !isJsonObject(overrides)) {
|
if (!overrides || !isJsonObject(overrides)) {
|
||||||
return overrides;
|
return overrides;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ function plusModelId(path: unknown): string | undefined {
|
|||||||
|
|
||||||
type ModelSourcePickerProps = {
|
type ModelSourcePickerProps = {
|
||||||
path: unknown;
|
path: unknown;
|
||||||
// Frigate+ metadata the backend attaches to a saved model, and the only
|
// Frigate+ metadata the backend attaches to a saved model
|
||||||
// reliable signal that one is active: it resolves `plus://<id>` to a local
|
|
||||||
// cache path before serving the config back
|
|
||||||
plus?: { id: string } | null;
|
plus?: { id: string } | null;
|
||||||
// the detector this model runs on, used to filter incompatible Plus models
|
// the detector this model runs on, used to filter incompatible Plus models
|
||||||
detector?: string;
|
detector?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user