mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-27 06:39:00 +03:00
Refactor detector and model management (#23995)
* Refactor detector and model management * Fix model resolution field
This commit is contained in:
committed by
Josh Hawkins
parent
7b42d94bfe
commit
5c9c02002f
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -102,11 +102,12 @@ def generate_config():
|
||||
snapshot = config.model_dump()
|
||||
|
||||
# Runtime-computed fields not in the Pydantic dump
|
||||
all_attrs = set()
|
||||
for attrs in snapshot.get("model", {}).get("attributes_map", {}).values():
|
||||
all_attrs.update(attrs)
|
||||
snapshot["model"]["all_attributes"] = sorted(all_attrs)
|
||||
snapshot["model"]["colormap"] = {}
|
||||
for model in snapshot.get("models", []):
|
||||
all_attrs = set()
|
||||
for attrs in model.get("attributes_map", {}).values():
|
||||
all_attrs.update(attrs)
|
||||
model["all_attributes"] = sorted(all_attrs)
|
||||
model["colormap"] = {}
|
||||
|
||||
return snapshot
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
|
||||
import { test, expect } from "../../fixtures/frigate-test";
|
||||
|
||||
test.describe("Detectors and model Settings @high", () => {
|
||||
// The settings page still reads the removed `detectors` and `model` config
|
||||
// keys, so it cannot render against a `models` config. Re-enable these once
|
||||
// the page is rebuilt around the models list.
|
||||
test.describe.skip("Detectors and model Settings @high", () => {
|
||||
test("page renders with detector and model cards", async ({ frigateApp }) => {
|
||||
await frigateApp.goto("/settings?page=systemDetectorsAndModel");
|
||||
await frigateApp.page.waitForTimeout(2000);
|
||||
|
||||
@@ -98,6 +98,10 @@
|
||||
"label": "Detect width",
|
||||
"description": "Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution."
|
||||
},
|
||||
"scene": {
|
||||
"label": "Detect scene",
|
||||
"description": "The environment this camera looks at, used to pick which of the configured models runs on it. Defaults to the model with a scene of 'all'."
|
||||
},
|
||||
"fps": {
|
||||
"label": "Detect FPS",
|
||||
"description": "Desired frames per second to run detection on; lower values reduce CPU usage (recommended value is 5, only set higher - at most 10 - if tracking extremely fast moving objects)."
|
||||
|
||||
@@ -275,172 +275,17 @@
|
||||
"description": "Unit system for display (metric or imperial) used in the UI and MQTT."
|
||||
}
|
||||
},
|
||||
"detectors": {
|
||||
"label": "Detector hardware",
|
||||
"description": "Configuration for object detectors (CPU, GPU, ONNX backends) and any detector-specific model settings.",
|
||||
"type": {
|
||||
"label": "Type"
|
||||
"models": {
|
||||
"label": "Detection models",
|
||||
"description": "Object detection models and the hardware each one runs on. Cameras pick a model by matching their detect.scene against a model's scene.",
|
||||
"scene": {
|
||||
"label": "Model scene",
|
||||
"description": "The camera environment this model is used for. Cameras select a model by setting detect.scene to a matching value, and 'all' is used by any camera that does not set one."
|
||||
},
|
||||
"model": {
|
||||
"label": "Detector specific model configuration",
|
||||
"description": "Detector-specific model configuration options (path, input size, etc.).",
|
||||
"path": {
|
||||
"label": "Custom object detector model path",
|
||||
"description": "Path to a custom detection model file (or plus://<model_id> for Frigate+ models)."
|
||||
},
|
||||
"labelmap_path": {
|
||||
"label": "Label map for custom object detector",
|
||||
"description": "Path to a labelmap file that maps numeric classes to string labels for the detector."
|
||||
},
|
||||
"width": {
|
||||
"label": "Object detection model input width",
|
||||
"description": "Width of the model input tensor in pixels."
|
||||
},
|
||||
"height": {
|
||||
"label": "Object detection model input height",
|
||||
"description": "Height of the model input tensor in pixels."
|
||||
},
|
||||
"labelmap": {
|
||||
"label": "Labelmap customization",
|
||||
"description": "Overrides or remapping entries to merge into the standard labelmap."
|
||||
},
|
||||
"attributes_map": {
|
||||
"label": "Map of object labels to their attribute labels",
|
||||
"description": "Mapping from object labels to attribute labels used to attach metadata (for example 'car' -> ['license_plate'])."
|
||||
},
|
||||
"input_tensor": {
|
||||
"label": "Model Input Tensor Shape",
|
||||
"description": "Tensor format expected by the model: 'nhwc' or 'nchw'."
|
||||
},
|
||||
"input_pixel_format": {
|
||||
"label": "Model Input Pixel Color Format",
|
||||
"description": "Pixel colorspace expected by the model: 'rgb', 'bgr', or 'yuv'."
|
||||
},
|
||||
"input_dtype": {
|
||||
"label": "Model Input D Type",
|
||||
"description": "Data type of the model input tensor (for example 'float32')."
|
||||
},
|
||||
"model_type": {
|
||||
"label": "Object Detection Model Type",
|
||||
"description": "Detector model architecture type (ssd, yolox, yolonas, yolo-generic, rfdetr, dfine) used by some detectors for optimization."
|
||||
}
|
||||
"devices": {
|
||||
"label": "Detection hardware",
|
||||
"description": "Hardware this model runs on, as '<detector>' or '<detector>:<device>' (for example 'edgetpu:pci:0' or 'openvino:GPU'). Listing the same device more than once runs additional inference processes on it."
|
||||
},
|
||||
"model_path": {
|
||||
"label": "Detector specific model path",
|
||||
"description": "File path to the detector model binary if required by the chosen detector."
|
||||
},
|
||||
"axengine": {
|
||||
"label": "AXEngine NPU",
|
||||
"description": "AXERA AX650N/AX8850N NPU detector running compiled .axmodel files via the AXEngine runtime."
|
||||
},
|
||||
"cpu": {
|
||||
"label": "CPU",
|
||||
"description": "CPU TFLite detector that runs TensorFlow Lite models on the host CPU without hardware acceleration. Not recommended.",
|
||||
"num_threads": {
|
||||
"label": "Number of detection threads",
|
||||
"description": "The number of threads used for CPU-based inference."
|
||||
}
|
||||
},
|
||||
"deepstack": {
|
||||
"label": "DeepStack",
|
||||
"description": "DeepStack/CodeProject.AI detector that sends images to a remote DeepStack HTTP API for inference. Not recommended.",
|
||||
"api_url": {
|
||||
"label": "DeepStack API URL",
|
||||
"description": "The URL of the DeepStack API."
|
||||
},
|
||||
"api_timeout": {
|
||||
"label": "DeepStack API timeout (in seconds)",
|
||||
"description": "Maximum time allowed for a DeepStack API request."
|
||||
},
|
||||
"api_key": {
|
||||
"label": "DeepStack API key (if required)",
|
||||
"description": "Optional API key for authenticated DeepStack services."
|
||||
}
|
||||
},
|
||||
"edgetpu": {
|
||||
"label": "EdgeTPU",
|
||||
"description": "EdgeTPU detector that runs TensorFlow Lite models compiled for Coral EdgeTPU using the EdgeTPU delegate.",
|
||||
"device": {
|
||||
"label": "Device Type",
|
||||
"description": "The device to use for EdgeTPU inference (e.g. 'usb', 'pci')."
|
||||
}
|
||||
},
|
||||
"hailo8l": {
|
||||
"label": "Hailo-8/Hailo-8L",
|
||||
"description": "Hailo-8/Hailo-8L detector using HEF models and the HailoRT SDK for inference on Hailo hardware.",
|
||||
"device": {
|
||||
"label": "Device Type",
|
||||
"description": "The device to use for Hailo inference (e.g. 'PCIe', 'M.2')."
|
||||
}
|
||||
},
|
||||
"memryx": {
|
||||
"label": "MemryX",
|
||||
"description": "MemryX MX3 detector that runs compiled DFP models on MemryX accelerators.",
|
||||
"device": {
|
||||
"label": "Device Path",
|
||||
"description": "The device to use for MemryX inference (e.g. 'PCIe')."
|
||||
}
|
||||
},
|
||||
"onnx": {
|
||||
"label": "ONNX",
|
||||
"description": "ONNX detector for running ONNX models; will use available acceleration backends (CUDA/ROCm/OpenVINO) when available.",
|
||||
"device": {
|
||||
"label": "Device Type",
|
||||
"description": "The device to use for ONNX inference (e.g. 'AUTO', 'CPU', 'GPU')."
|
||||
}
|
||||
},
|
||||
"openvino": {
|
||||
"label": "OpenVINO",
|
||||
"description": "OpenVINO detector for AMD and Intel CPUs, Intel GPUs and Intel VPU hardware.",
|
||||
"device": {
|
||||
"label": "Device Type",
|
||||
"description": "The device to use for OpenVINO inference (e.g. 'CPU', 'GPU', 'NPU')."
|
||||
}
|
||||
},
|
||||
"rknn": {
|
||||
"label": "RKNN",
|
||||
"description": "RKNN detector for Rockchip NPUs; runs compiled RKNN models on Rockchip hardware.",
|
||||
"num_cores": {
|
||||
"label": "Number of NPU cores to use.",
|
||||
"description": "The number of NPU cores to use (0 for auto)."
|
||||
}
|
||||
},
|
||||
"synaptics": {
|
||||
"label": "Synaptics",
|
||||
"description": "Synaptics NPU detector for models in .synap format using the Synap SDK on Synaptics hardware."
|
||||
},
|
||||
"teflon_tfl": {
|
||||
"label": "Teflon",
|
||||
"description": "Teflon delegate detector for TFLite using Mesa Teflon delegate library to accelerate inference on supported GPUs."
|
||||
},
|
||||
"tensorrt": {
|
||||
"label": "TensorRT",
|
||||
"description": "TensorRT detector for Nvidia Jetson devices using serialized TensorRT engines for accelerated inference.",
|
||||
"device": {
|
||||
"label": "GPU Device Index",
|
||||
"description": "The GPU device index to use."
|
||||
}
|
||||
},
|
||||
"zmq": {
|
||||
"label": "ZMQ IPC",
|
||||
"description": "ZMQ IPC detector that offloads inference to an external process via a ZeroMQ IPC endpoint.",
|
||||
"endpoint": {
|
||||
"label": "ZMQ IPC endpoint",
|
||||
"description": "The ZMQ endpoint to connect to."
|
||||
},
|
||||
"request_timeout_ms": {
|
||||
"label": "ZMQ request timeout in milliseconds",
|
||||
"description": "Timeout for ZMQ requests in milliseconds."
|
||||
},
|
||||
"linger_ms": {
|
||||
"label": "ZMQ socket linger in milliseconds",
|
||||
"description": "Socket linger period in milliseconds."
|
||||
}
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"label": "Detection model",
|
||||
"description": "Settings to configure a custom object detection model and its input shape.",
|
||||
"path": {
|
||||
"label": "Custom object detector model path",
|
||||
"description": "Path to a custom detection model file (or plus://<model_id> for Frigate+ models)."
|
||||
@@ -621,6 +466,10 @@
|
||||
"label": "Detect width",
|
||||
"description": "Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution."
|
||||
},
|
||||
"scene": {
|
||||
"label": "Detect scene",
|
||||
"description": "The environment this camera looks at, used to pick which of the configured models runs on it. Defaults to the model with a scene of 'all'."
|
||||
},
|
||||
"fps": {
|
||||
"label": "Detect FPS",
|
||||
"description": "Desired frames per second to run detection on; lower values reduce CPU usage (recommended value is 5, only set higher - at most 10 - if tracking extremely fast moving objects)."
|
||||
|
||||
@@ -13,6 +13,7 @@ import { cn } from "@/lib/utils";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import useContextMenu from "@/hooks/use-contextmenu";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { isAttributeOfLabel } from "@/utils/modelUtil";
|
||||
|
||||
type SearchThumbnailProps = {
|
||||
searchResult: SearchResult;
|
||||
@@ -58,9 +59,7 @@ export default function SearchThumbnail({
|
||||
}
|
||||
|
||||
if (
|
||||
config.model.attributes_map[searchResult.label]?.includes(
|
||||
searchResult.sub_label,
|
||||
)
|
||||
isAttributeOfLabel(config, searchResult.label, searchResult.sub_label)
|
||||
) {
|
||||
return searchResult.sub_label;
|
||||
}
|
||||
@@ -82,9 +81,7 @@ export default function SearchThumbnail({
|
||||
}
|
||||
|
||||
if (
|
||||
config.model.attributes_map[searchResult.label]?.includes(
|
||||
searchResult.sub_label,
|
||||
)
|
||||
isAttributeOfLabel(config, searchResult.label, searchResult.sub_label)
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from "@/types/frigateConfig";
|
||||
import { ClassificationDatasetResponse } from "@/types/classification";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { isAttributeLabel } from "@/utils/modelUtil";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import axios from "axios";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
@@ -99,7 +100,7 @@ export default function ClassificationModelEditDialog({
|
||||
}
|
||||
|
||||
cameraConfig.objects.track.forEach((label) => {
|
||||
if (!config.model.all_attributes.includes(label)) {
|
||||
if (!isAttributeLabel(config, label)) {
|
||||
labels.add(label);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import { isAttributeLabel } from "@/utils/modelUtil";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -72,7 +73,7 @@ export default function Step1NameAndDefine({
|
||||
}
|
||||
|
||||
cameraConfig.objects.track.forEach((label) => {
|
||||
if (!config.model.all_attributes.includes(label)) {
|
||||
if (!isAttributeLabel(config, label)) {
|
||||
labels.add(label);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,23 +19,16 @@ function collectLabelmapLabels(labelmap: unknown, labels: Set<string>) {
|
||||
});
|
||||
}
|
||||
|
||||
// Read labelmap labels from the global model and detector models.
|
||||
// Read labelmap labels from every configured detection model.
|
||||
function getLabelmapLabels(context: FormContext): string[] {
|
||||
const labels = new Set<string>();
|
||||
const fullConfig = context.fullConfig as FrigateConfig | undefined;
|
||||
|
||||
if (fullConfig?.model) {
|
||||
collectLabelmapLabels(fullConfig.model.labelmap, labels);
|
||||
}
|
||||
|
||||
if (fullConfig?.detectors) {
|
||||
// detectors is a map of detector configs; each may include a model labelmap.
|
||||
Object.values(fullConfig.detectors).forEach((detector) => {
|
||||
if (detector?.model?.labelmap) {
|
||||
collectLabelmapLabels(detector.model.labelmap, labels);
|
||||
}
|
||||
});
|
||||
}
|
||||
fullConfig?.models?.forEach((model) => {
|
||||
if (model?.labelmap) {
|
||||
collectLabelmapLabels(model.labelmap, labels);
|
||||
}
|
||||
});
|
||||
|
||||
return [...labels];
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { CalendarRangeFilterButton } from "./CalendarFilterButton";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { isAttributeLabel } from "@/utils/modelUtil";
|
||||
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
||||
|
||||
type SearchFilterGroupProps = {
|
||||
@@ -73,7 +74,7 @@ export default function SearchFilterGroup({
|
||||
}
|
||||
|
||||
cameraConfig.objects.track.forEach((label) => {
|
||||
if (!config.model.all_attributes.includes(label)) {
|
||||
if (!isAttributeLabel(config, label)) {
|
||||
labels.add(label);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Event } from "@/types/event";
|
||||
import { resolveZoneName } from "@/hooks/use-zone-friendly-name";
|
||||
import { getPrimaryModel } from "@/utils/modelUtil";
|
||||
|
||||
// Use a small tolerance (10ms) for browsers with seek precision by-design issues
|
||||
const TOLERANCE = 0.01;
|
||||
@@ -178,7 +179,7 @@ export default function ObjectTrackOverlay({
|
||||
|
||||
const getObjectColor = useCallback(
|
||||
(label: string, objectId: string) => {
|
||||
const objectColor = config?.model?.colormap[label];
|
||||
const objectColor = getPrimaryModel(config)?.colormap?.[label];
|
||||
if (objectColor) {
|
||||
const reversed = [...objectColor].reverse();
|
||||
return `rgb(${reversed.join(",")})`;
|
||||
|
||||
@@ -49,6 +49,7 @@ import Logo from "@/components/Logo";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import DebugDrawingLayer from "@/components/overlay/DebugDrawingLayer";
|
||||
import { getPrimaryModel } from "@/utils/modelUtil";
|
||||
import { IoMdArrowRoundBack } from "react-icons/io";
|
||||
|
||||
type DebugReplayStatus = {
|
||||
@@ -642,7 +643,7 @@ function ObjectList({ cameraConfig, objects, config }: ObjectListProps) {
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
return config.model?.colormap;
|
||||
return getPrimaryModel(config)?.colormap;
|
||||
}, [config]);
|
||||
|
||||
const getColorForObjectName = useCallback(
|
||||
|
||||
@@ -115,6 +115,7 @@ import SaveAllPreviewPopover, {
|
||||
type SaveAllPreviewItem,
|
||||
} from "@/components/overlay/detail/SaveAllPreviewPopover";
|
||||
import { useRestart } from "@/api/ws";
|
||||
import { getPrimaryModel } from "@/utils/modelUtil";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -949,14 +950,16 @@ export default function Settings() {
|
||||
const pendingKeySet = Object.keys(
|
||||
sanitizedDetectors as JsonObject,
|
||||
).sort();
|
||||
const savedKeySet = Object.keys(config.detectors ?? {}).sort();
|
||||
const savedKeySet = [
|
||||
...(getPrimaryModel(config)?.devices ?? []),
|
||||
].sort();
|
||||
detectorKeysChanged =
|
||||
JSON.stringify(pendingKeySet) !== JSON.stringify(savedKeySet);
|
||||
}
|
||||
let modelTabChanged = false;
|
||||
if (sanitizedModel && typeof sanitizedModel === "object") {
|
||||
const newPath = (sanitizedModel as { path?: string }).path;
|
||||
const oldPath = config.model?.path;
|
||||
const oldPath = getPrimaryModel(config)?.path;
|
||||
const newIsPlus =
|
||||
typeof newPath === "string" && newPath.startsWith("plus://");
|
||||
const oldIsPlus =
|
||||
|
||||
@@ -66,6 +66,7 @@ export interface CameraConfig {
|
||||
height: number;
|
||||
max_disappeared: number;
|
||||
min_initialized: number;
|
||||
scene: string | null;
|
||||
stationary: {
|
||||
interval: number;
|
||||
max_frames: {
|
||||
@@ -405,6 +406,32 @@ export type GenAIAgentConfig = {
|
||||
runtime_options?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type DetectionModelConfig = {
|
||||
scene: string;
|
||||
devices: string[];
|
||||
height: number;
|
||||
input_pixel_format: string;
|
||||
input_tensor: string;
|
||||
labelmap: Record<string, unknown>;
|
||||
labelmap_path: string | null;
|
||||
model_type: string;
|
||||
path: string | null;
|
||||
width: number;
|
||||
colormap: { [key: string]: [number, number, number] };
|
||||
attributes_map: { [key: string]: string[] };
|
||||
all_attributes: string[];
|
||||
plus?: {
|
||||
name: string;
|
||||
id: string;
|
||||
trainDate: string;
|
||||
baseModel: string;
|
||||
isBaseModel: boolean;
|
||||
supportedDetectors: string[];
|
||||
width: number;
|
||||
height: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export interface FrigateConfig {
|
||||
version: string;
|
||||
safe_mode: boolean;
|
||||
@@ -468,23 +495,6 @@ export interface FrigateConfig {
|
||||
width: number | null;
|
||||
};
|
||||
|
||||
detectors: {
|
||||
coral: {
|
||||
device: string;
|
||||
model: {
|
||||
height: number;
|
||||
input_pixel_format: string;
|
||||
input_tensor: string;
|
||||
labelmap: Record<string, string>;
|
||||
labelmap_path: string | null;
|
||||
model_type: string;
|
||||
path: string;
|
||||
width: number;
|
||||
};
|
||||
type: string;
|
||||
};
|
||||
};
|
||||
|
||||
environment_vars: Record<string, unknown>;
|
||||
|
||||
face_recognition: FaceRecognitionConfig;
|
||||
@@ -524,29 +534,7 @@ export interface FrigateConfig {
|
||||
logs: Record<string, string>;
|
||||
};
|
||||
|
||||
model: {
|
||||
height: number;
|
||||
input_pixel_format: string;
|
||||
input_tensor: string;
|
||||
labelmap: Record<string, unknown>;
|
||||
labelmap_path: string | null;
|
||||
model_type: string;
|
||||
path: string | null;
|
||||
width: number;
|
||||
colormap: { [key: string]: [number, number, number] };
|
||||
attributes_map: { [key: string]: string[] };
|
||||
all_attributes: string[];
|
||||
plus?: {
|
||||
name: string;
|
||||
id: string;
|
||||
trainDate: string;
|
||||
baseModel: string;
|
||||
isBaseModel: boolean;
|
||||
supportedDetectors: string[];
|
||||
width: number;
|
||||
height: number;
|
||||
} | null;
|
||||
};
|
||||
models: DetectionModelConfig[];
|
||||
|
||||
motion: Record<string, unknown> | null;
|
||||
|
||||
|
||||
@@ -493,6 +493,7 @@ export interface SectionSavePayload {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { resolveAndCleanSchema } from "@/lib/config-schema";
|
||||
import { getAllAttributes } from "@/utils/modelUtil";
|
||||
|
||||
type SchemaWithDefinitions = RJSFSchema & {
|
||||
$defs?: Record<string, RJSFSchema>;
|
||||
@@ -796,7 +797,7 @@ export function getEffectiveAttributeLabels(
|
||||
fullCameraConfig: CameraConfig | undefined,
|
||||
level: "global" | "camera" | "replay" | undefined,
|
||||
): string[] {
|
||||
const all = fullConfig?.model?.all_attributes ?? [];
|
||||
const all = getAllAttributes(fullConfig);
|
||||
if (level !== "global" && fullCameraConfig?.type === "lpr") {
|
||||
return all.filter((attr) => attr !== "license_plate");
|
||||
}
|
||||
|
||||
@@ -56,8 +56,10 @@ export function getAttributeLabels(config?: FrigateConfig) {
|
||||
|
||||
const labels = new Set();
|
||||
|
||||
Object.values(config.model.attributes_map).forEach((values) =>
|
||||
values.forEach((label) => labels.add(label)),
|
||||
config.models?.forEach((model) =>
|
||||
Object.values(model.attributes_map ?? {}).forEach((values) =>
|
||||
values.forEach((label) => labels.add(label)),
|
||||
),
|
||||
);
|
||||
return [...labels];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { DetectionModelConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||
|
||||
/**
|
||||
* The model a camera runs on, matched by the camera's detect scene.
|
||||
*
|
||||
* Falls back to the model for every scene, then to the only configured model,
|
||||
* which is what the backend does when a camera does not name a scene.
|
||||
*/
|
||||
export function getModelForCamera(
|
||||
config?: FrigateConfig,
|
||||
camera?: string,
|
||||
): DetectionModelConfig | undefined {
|
||||
const models = config?.models;
|
||||
|
||||
if (!models?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const scene = camera ? config?.cameras?.[camera]?.detect?.scene : undefined;
|
||||
|
||||
if (scene) {
|
||||
const match = models.find((model) => model.scene == scene);
|
||||
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return models.find((model) => model.scene == "all") ?? models[0];
|
||||
}
|
||||
|
||||
/** The model used when the question is not about a specific camera. */
|
||||
export function getPrimaryModel(
|
||||
config?: FrigateConfig,
|
||||
): DetectionModelConfig | undefined {
|
||||
return getModelForCamera(config);
|
||||
}
|
||||
|
||||
/** Every object attribute across all configured models. */
|
||||
export function getAllAttributes(config?: FrigateConfig): string[] {
|
||||
const attributes = new Set<string>();
|
||||
|
||||
config?.models?.forEach((model) =>
|
||||
model.all_attributes?.forEach((attribute) => attributes.add(attribute)),
|
||||
);
|
||||
|
||||
return [...attributes];
|
||||
}
|
||||
|
||||
/** Whether a label is an attribute of any configured model. */
|
||||
export function isAttributeLabel(
|
||||
config: FrigateConfig | undefined,
|
||||
label: string,
|
||||
): boolean {
|
||||
return !!config?.models?.some((model) =>
|
||||
model.all_attributes?.includes(label),
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether an attribute belongs to a parent label in any configured model. */
|
||||
export function isAttributeOfLabel(
|
||||
config: FrigateConfig | undefined,
|
||||
label: string,
|
||||
attribute: string,
|
||||
): boolean {
|
||||
return !!config?.models?.some((model) =>
|
||||
model.attributes_map?.[label]?.includes(attribute),
|
||||
);
|
||||
}
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
import { ConfigSectionTemplate } from "@/components/config-form/sections";
|
||||
import { ConfigMessageBanner } from "@/components/config-form/ConfigMessageBanner";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { getPrimaryModel } from "@/utils/modelUtil";
|
||||
import {
|
||||
buildHiddenFieldContext,
|
||||
getSectionConfig,
|
||||
@@ -115,8 +116,9 @@ const STATUS_BAR_KEY = "detectors_and_model";
|
||||
const EMPTY_PENDING: Record<string, ConfigSectionData> = {};
|
||||
|
||||
const deriveInitialState = (config: FrigateConfig): PageState => {
|
||||
const plusModelId = config.model?.plus?.id;
|
||||
const modelPath = config.model?.path;
|
||||
const primaryModel = getPrimaryModel(config);
|
||||
const plusModelId = primaryModel?.plus?.id;
|
||||
const modelPath = primaryModel?.path;
|
||||
const plusEnabled = Boolean(config.plus?.enabled);
|
||||
|
||||
// The reliable signal that a Plus model is currently active is the
|
||||
@@ -136,10 +138,12 @@ const deriveInitialState = (config: FrigateConfig): PageState => {
|
||||
modelTab = "custom";
|
||||
}
|
||||
|
||||
const { plus: _plus, ...modelWithoutPlus } = (config.model ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const {
|
||||
plus: _plus,
|
||||
scene: _scene,
|
||||
devices: _devices,
|
||||
...modelWithoutPlus
|
||||
} = (primaryModel ?? {}) as Record<string, unknown>;
|
||||
// If a Plus model is active, the resolved `model.path` is auto-derived from
|
||||
// `plus.id` — drop it so the Custom tab starts clean and doesn't silently
|
||||
// re-save the same Plus model when the user thinks they switched modes.
|
||||
@@ -148,7 +152,7 @@ const deriveInitialState = (config: FrigateConfig): PageState => {
|
||||
}
|
||||
|
||||
return {
|
||||
detectors: (config.detectors ?? {}) as ConfigSectionData,
|
||||
detectors: { devices: primaryModel?.devices ?? [] } as ConfigSectionData,
|
||||
modelTab,
|
||||
plusModelId: plusModelId ?? undefined,
|
||||
customModel: modelWithoutPlus as ConfigSectionData,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { isReplayCamera } from "@/utils/cameraUtil";
|
||||
import type { SettingsPageProps } from "@/views/settings/SingleSectionPage";
|
||||
import { getPrimaryModel } from "@/utils/modelUtil";
|
||||
|
||||
export default function FrigatePlusSettingsView(_props: SettingsPageProps) {
|
||||
const { t } = useTranslation("views/settings");
|
||||
@@ -51,7 +52,7 @@ export default function FrigatePlusSettingsView(_props: SettingsPageProps) {
|
||||
description={
|
||||
<>
|
||||
<p>{t("frigatePlus.apiKey.desc")}</p>
|
||||
{!config?.model.plus && (
|
||||
{!getPrimaryModel(config)?.plus && (
|
||||
<div className="mt-2 flex items-center text-primary-variant">
|
||||
<Link
|
||||
to="https://frigate.video/plus"
|
||||
@@ -85,7 +86,7 @@ export default function FrigatePlusSettingsView(_props: SettingsPageProps) {
|
||||
|
||||
{config?.plus?.enabled && (
|
||||
<FrigatePlusCurrentModelSummary
|
||||
plusModel={config.model.plus}
|
||||
plusModel={getPrimaryModel(config)?.plus}
|
||||
action={
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -34,6 +34,7 @@ import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
|
||||
import { AudioLevelGraph } from "@/components/audio/AudioLevelGraph";
|
||||
import { useWs } from "@/api/ws";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getPrimaryModel } from "@/utils/modelUtil";
|
||||
|
||||
type ObjectSettingsViewProps = {
|
||||
selectedCamera?: string;
|
||||
@@ -172,11 +173,10 @@ export default function ObjectSettingsView({
|
||||
<div className="mb-5 space-y-3 text-sm text-muted-foreground">
|
||||
<p>
|
||||
{t("debug.detectorDesc", {
|
||||
detectors: config
|
||||
? Object.keys(config?.detectors)
|
||||
.map((detector) => capitalizeFirstLetter(detector))
|
||||
.join(",")
|
||||
: "",
|
||||
detectors: (config?.models ?? [])
|
||||
.flatMap((model) => model.devices ?? [])
|
||||
.map((device) => capitalizeFirstLetter(device))
|
||||
.join(","),
|
||||
})}
|
||||
</p>
|
||||
<p>{t("debug.desc")}</p>
|
||||
@@ -380,7 +380,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
return config.model?.colormap;
|
||||
return getPrimaryModel(config)?.colormap;
|
||||
}, [config]);
|
||||
|
||||
const getColorForObjectName = useCallback(
|
||||
|
||||
@@ -3,11 +3,11 @@ import {
|
||||
SettingsGroupCard,
|
||||
SplitCardRow,
|
||||
} from "@/components/card/SettingsGroupCard";
|
||||
import type { FrigateConfig } from "@/types/frigateConfig";
|
||||
import type { DetectionModelConfig } from "@/types/frigateConfig";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type FrigatePlusCurrentModelSummaryProps = {
|
||||
plusModel: FrigateConfig["model"]["plus"];
|
||||
plusModel: DetectionModelConfig["plus"];
|
||||
action?: ReactNode;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user