mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-30 20:06:51 +03:00
Handle form validation
This commit is contained in:
@@ -286,6 +286,42 @@ test.describe("Detection models settings @high", () => {
|
|||||||
).toBeDisabled();
|
).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("two models without a path share the detector's default model", async ({
|
||||||
|
frigateApp,
|
||||||
|
}) => {
|
||||||
|
await installRoutes(frigateApp.page, [
|
||||||
|
{ scene: "default", devices: ["cpu"] },
|
||||||
|
{ scene: "driveway", devices: ["cpu"] },
|
||||||
|
]);
|
||||||
|
await openPage(frigateApp);
|
||||||
|
|
||||||
|
await expect(frigateApp.page.locator("#pageRoot")).toContainText(
|
||||||
|
"The Default and driveway models use the same model file",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("another spelling of the same path is the same model", async ({
|
||||||
|
frigateApp,
|
||||||
|
}) => {
|
||||||
|
await installRoutes(frigateApp.page, [
|
||||||
|
{
|
||||||
|
scene: "default",
|
||||||
|
devices: ["openvino:GPU.0"],
|
||||||
|
path: "/config/model_cache/yolo.onnx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scene: "driveway",
|
||||||
|
devices: ["openvino:GPU.1"],
|
||||||
|
path: "/config//model_cache/./tmp/../yolo.onnx",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await openPage(frigateApp);
|
||||||
|
|
||||||
|
await expect(frigateApp.page.locator("#pageRoot")).toContainText(
|
||||||
|
"The Default and driveway models use the same model file",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("one model file may run on different detectors", async ({
|
test("one model file may run on different detectors", async ({
|
||||||
frigateApp,
|
frigateApp,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -3,6 +3,51 @@ import type { TFunction } from "i18next";
|
|||||||
import { isJsonObject } from "@/lib/utils";
|
import { isJsonObject } from "@/lib/utils";
|
||||||
import { DEFAULT_SCENE, getSceneLabel } from "@/utils/modelUtil";
|
import { DEFAULT_SCENE, getSceneLabel } from "@/utils/modelUtil";
|
||||||
|
|
||||||
|
/** The model the backend loads for a detector when no path is configured. */
|
||||||
|
function defaultModelPath(detector: string): string | undefined {
|
||||||
|
if (detector === "cpu" || detector.endsWith("_tfl")) {
|
||||||
|
return "/cpu_model.tflite";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detector === "edgetpu") {
|
||||||
|
return "/edgetpu_model.tflite";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detector === "openvino") {
|
||||||
|
return "/openvino-model/ssdlite_mobilenet_v2.xml";
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapse `//`, `.`, and `..` so spellings of one file path compare equal,
|
||||||
|
* as the backend's realpath does. Symlinks and copies can only be seen by the
|
||||||
|
* backend, which still combines them at startup.
|
||||||
|
*/
|
||||||
|
function normalizeModelPath(path: string): string {
|
||||||
|
// plus://<id> and other URLs are compared as written
|
||||||
|
if (path.includes("://")) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments: string[] = [];
|
||||||
|
|
||||||
|
for (const segment of path.split("/")) {
|
||||||
|
if (segment === "" || segment === ".") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment === ".." && segments.length && segments.at(-1) !== "..") {
|
||||||
|
segments.pop();
|
||||||
|
} else if (segment !== ".." || !path.startsWith("/")) {
|
||||||
|
segments.push(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${path.startsWith("/") ? "/" : ""}${segments.join("/")}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A camera that names no scene runs the model whose scene is `default`.
|
* A camera that names no scene runs the model whose scene is `default`.
|
||||||
* Without one the backend rejects the config outright once a second model
|
* Without one the backend rejects the config outright once a second model
|
||||||
@@ -38,14 +83,23 @@ export function validateModelScenes(
|
|||||||
const seen = new Map<string, string>();
|
const seen = new Map<string, string>();
|
||||||
|
|
||||||
formData.forEach((model, index) => {
|
formData.forEach((model, index) => {
|
||||||
if (!isJsonObject(model) || typeof model.path !== "string" || !model.path) {
|
if (!isJsonObject(model)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const devices = Array.isArray(model.devices) ? model.devices : [];
|
const devices = Array.isArray(model.devices) ? model.devices : [];
|
||||||
const detector =
|
const detector =
|
||||||
typeof devices[0] === "string" ? devices[0].split(":")[0] : "";
|
typeof devices[0] === "string" ? devices[0].split(":")[0] : "";
|
||||||
const key = `${detector}|${model.path}`;
|
const path =
|
||||||
|
typeof model.path === "string" && model.path
|
||||||
|
? normalizeModelPath(model.path)
|
||||||
|
: defaultModelPath(detector);
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${detector}|${path}`;
|
||||||
const other = seen.get(key);
|
const other = seen.get(key);
|
||||||
|
|
||||||
if (other === undefined) {
|
if (other === undefined) {
|
||||||
|
|||||||
Reference in New Issue
Block a user