mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-06 14:52:47 +03:00
Refactor model scene definitions (#24508)
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
* Refactor model scene definitions * Cleanup * Validate model paths in the UI * Handle form validation
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Camera detect scene tests -- MEDIUM tier.
|
||||
*
|
||||
* Scenes are free-form names declared by the configured models, so a camera
|
||||
* only needs to pick one when there is a choice to make. The choices are the
|
||||
* scenes of the configured models, plus a saved scene that no model uses.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test, expect } from "../../fixtures/frigate-test";
|
||||
import type { Page } from "@playwright/test";
|
||||
import { configFactory } from "../../fixtures/mock-data/config";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const CONFIG_SCHEMA = JSON.parse(
|
||||
readFileSync(
|
||||
resolve(__dirname, "../../fixtures/mock-data/config-schema.json"),
|
||||
"utf-8",
|
||||
),
|
||||
);
|
||||
|
||||
const SETTINGS_URL = "/settings?page=cameraDetect&camera=front_door";
|
||||
|
||||
async function installRoutes(
|
||||
page: Page,
|
||||
models: { scene: string; devices: string[] }[],
|
||||
cameraScene?: string,
|
||||
) {
|
||||
const config = configFactory({
|
||||
models,
|
||||
...(cameraScene
|
||||
? { cameras: { front_door: { detect: { scene: cameraScene } } } }
|
||||
: {}),
|
||||
} as never);
|
||||
|
||||
await page.route("**/api/config/schema.json", (route) =>
|
||||
route.fulfill({ json: CONFIG_SCHEMA }),
|
||||
);
|
||||
await page.route("**/api/config", (route) =>
|
||||
route.request().method() === "GET"
|
||||
? route.fulfill({ json: config })
|
||||
: route.fulfill({ json: { success: true } }),
|
||||
);
|
||||
await page.route("**/api/config/raw_paths", (route) =>
|
||||
route.fulfill({ json: {} }),
|
||||
);
|
||||
}
|
||||
|
||||
test.describe("camera detect scene @medium", () => {
|
||||
test("is hidden when there is only one model", async ({ frigateApp }) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "default", devices: ["cpu"] },
|
||||
]);
|
||||
await frigateApp.goto(SETTINGS_URL);
|
||||
|
||||
const root = frigateApp.page.locator("#pageRoot");
|
||||
await expect(root).toContainText("Detect FPS");
|
||||
await expect(root).not.toContainText("Detect scene");
|
||||
});
|
||||
|
||||
test("offers the default and configured model scenes", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "default", devices: ["cpu"] },
|
||||
{ scene: "thermal", devices: ["openvino:GPU.0"] },
|
||||
]);
|
||||
await frigateApp.goto(SETTINGS_URL);
|
||||
|
||||
const root = frigateApp.page.locator("#pageRoot");
|
||||
await expect(root).toContainText("Detect scene");
|
||||
|
||||
await frigateApp.page.locator("#root_scene").click();
|
||||
const options = frigateApp.page.getByRole("option");
|
||||
await expect(options).toHaveText(["Default", "thermal"]);
|
||||
});
|
||||
|
||||
test("a saved scene no model uses stays editable", async ({ frigateApp }) => {
|
||||
// the camera falls back to the default model, but its saved scene would
|
||||
// silently take effect if a model for it were added later
|
||||
await installRoutes(
|
||||
frigateApp.page,
|
||||
[{ scene: "default", devices: ["cpu"] }],
|
||||
"garage",
|
||||
);
|
||||
await frigateApp.goto(SETTINGS_URL);
|
||||
|
||||
await expect(frigateApp.page.locator("#pageRoot")).toContainText(
|
||||
"Detect scene",
|
||||
);
|
||||
|
||||
await frigateApp.page.locator("#root_scene").click();
|
||||
await expect(frigateApp.page.getByRole("option")).toHaveText([
|
||||
"Default",
|
||||
"garage",
|
||||
]);
|
||||
});
|
||||
|
||||
test("default is only offered when a default model exists", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await installRoutes(
|
||||
frigateApp.page,
|
||||
[
|
||||
{ scene: "thermal", devices: ["cpu"] },
|
||||
{ scene: "visible", devices: ["openvino:GPU.0"] },
|
||||
],
|
||||
"thermal",
|
||||
);
|
||||
await frigateApp.goto(SETTINGS_URL);
|
||||
|
||||
await frigateApp.page.locator("#root_scene").click();
|
||||
await expect(frigateApp.page.getByRole("option")).toHaveText([
|
||||
"thermal",
|
||||
"visible",
|
||||
]);
|
||||
});
|
||||
|
||||
test("one model every camera selects needs no choice", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await installRoutes(
|
||||
frigateApp.page,
|
||||
[{ scene: "thermal", devices: ["cpu"] }],
|
||||
"thermal",
|
||||
);
|
||||
await frigateApp.goto(SETTINGS_URL);
|
||||
|
||||
const root = frigateApp.page.locator("#pageRoot");
|
||||
await expect(root).toContainText("Detect FPS");
|
||||
await expect(root).not.toContainText("Detect scene");
|
||||
});
|
||||
});
|
||||
@@ -129,19 +129,19 @@ const openPage = async (frigateApp: {
|
||||
test.describe("Detection models settings @high", () => {
|
||||
test("renders a card per configured model", async ({ frigateApp }) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "all", devices: ["cpu"] },
|
||||
{ scene: "default", devices: ["cpu"] },
|
||||
{ scene: "outdoor", devices: ["edgetpu:pci:0"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
const root = frigateApp.page.locator("#pageRoot");
|
||||
await expect(root).toContainText("All cameras");
|
||||
await expect(root).toContainText("Outdoor");
|
||||
await expect(root).toContainText("Default");
|
||||
await expect(root).toContainText("outdoor");
|
||||
});
|
||||
|
||||
test("unlimited hardware offers a detector count", async ({ frigateApp }) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "all", devices: ["openvino:GPU.0"] },
|
||||
{ scene: "default", devices: ["openvino:GPU.0"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
@@ -169,7 +169,7 @@ test.describe("Detection models settings @high", () => {
|
||||
frigateApp,
|
||||
}) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "all", devices: ["openvino:GPU.0", "openvino:GPU.0"] },
|
||||
{ scene: "default", devices: ["openvino:GPU.0", "openvino:GPU.0"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
@@ -190,7 +190,7 @@ test.describe("Detection models settings @high", () => {
|
||||
frigateApp,
|
||||
}) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "all", devices: ["edgetpu:pci:0"] },
|
||||
{ scene: "default", devices: ["edgetpu:pci:0"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
@@ -206,7 +206,7 @@ test.describe("Detection models settings @high", () => {
|
||||
frigateApp,
|
||||
}) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "all", devices: ["edgetpu:pci:0"] },
|
||||
{ scene: "default", devices: ["edgetpu:pci:0"] },
|
||||
{ scene: "outdoor", devices: ["edgetpu:pci:1"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
@@ -221,7 +221,7 @@ test.describe("Detection models settings @high", () => {
|
||||
frigateApp,
|
||||
}) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "all", devices: ["openvino:GPU.0"] },
|
||||
{ scene: "default", devices: ["openvino:GPU.0"] },
|
||||
{ scene: "outdoor", devices: ["openvino:GPU.1"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
@@ -234,23 +234,141 @@ test.describe("Detection models settings @high", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("adding a model appends a card with an unused scene", async ({
|
||||
test("adding a model appends a card with a scene to name", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await installRoutes(frigateApp.page, [{ scene: "all", devices: ["cpu"] }]);
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "default", devices: ["cpu"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
await frigateApp.page.getByRole("button", { name: "Add model" }).click();
|
||||
|
||||
// "all" is taken, so the new card takes the next available scene
|
||||
await expect(frigateApp.page.locator("#pageRoot")).toContainText("Indoor");
|
||||
// the new card's scene starts empty, and a scene is required to save
|
||||
const scene = frigateApp.page.locator("#models-1-scene");
|
||||
await expect(scene).toHaveValue("");
|
||||
await expect(
|
||||
frigateApp.page.getByRole("button", { name: /^Save$/ }),
|
||||
).toBeDisabled();
|
||||
|
||||
// the card keeps focus while its scene is typed
|
||||
await scene.fill("thermal");
|
||||
await expect(scene).toHaveValue("thermal");
|
||||
await expect(scene).toBeFocused();
|
||||
});
|
||||
|
||||
test("one model file cannot be split across scenes", 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/other.onnx",
|
||||
},
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
await frigateApp.page
|
||||
.locator("#root_1_path")
|
||||
.fill("/config/model_cache/yolo.onnx");
|
||||
|
||||
await expect(frigateApp.page.locator("#pageRoot")).toContainText(
|
||||
"The Default and driveway models use the same model file",
|
||||
);
|
||||
await expect(
|
||||
frigateApp.page.getByRole("button", { name: /^Save$/ }),
|
||||
).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 ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{
|
||||
scene: "default",
|
||||
devices: ["openvino:GPU.0"],
|
||||
path: "/config/model_cache/yolo.onnx",
|
||||
},
|
||||
{
|
||||
scene: "driveway",
|
||||
devices: ["onnx"],
|
||||
path: "/config/model_cache/yolo.onnx",
|
||||
},
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
await expect(frigateApp.page.locator("#pageRoot")).toContainText(
|
||||
"driveway",
|
||||
);
|
||||
await expect(frigateApp.page.locator("#pageRoot")).not.toContainText(
|
||||
"models use the same model file",
|
||||
);
|
||||
});
|
||||
|
||||
test("two models cannot share a scene", async ({ frigateApp }) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "default", devices: ["cpu"] },
|
||||
{ scene: "thermal", devices: ["edgetpu:pci:0"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
await frigateApp.page.locator("#models-1-scene").fill("default");
|
||||
|
||||
await expect(frigateApp.page.locator("#pageRoot")).toContainText(
|
||||
"Each model must use a different scene",
|
||||
);
|
||||
await expect(
|
||||
frigateApp.page.getByRole("button", { name: /^Save$/ }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
test("hardware is summarized rather than listed device by device", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "all", devices: ["openvino:GPU.0", "openvino:GPU.0"] },
|
||||
{ scene: "default", devices: ["openvino:GPU.0", "openvino:GPU.0"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
@@ -269,7 +387,7 @@ test.describe("Detection models settings @high", () => {
|
||||
frigateApp.page,
|
||||
[
|
||||
{
|
||||
scene: "all",
|
||||
scene: "default",
|
||||
devices: ["openvino:GPU.0"],
|
||||
path: "plus://abc123",
|
||||
plus: PLUS_MODEL,
|
||||
@@ -294,7 +412,7 @@ test.describe("Detection models settings @high", () => {
|
||||
frigateApp.page,
|
||||
[
|
||||
{
|
||||
scene: "all",
|
||||
scene: "default",
|
||||
devices: ["openvino:GPU.0"],
|
||||
path: "/config/custom.onnx",
|
||||
},
|
||||
@@ -325,7 +443,7 @@ test.describe("Detection models settings @high", () => {
|
||||
frigateApp.page,
|
||||
[
|
||||
{
|
||||
scene: "all",
|
||||
scene: "default",
|
||||
devices: ["openvino:GPU.0"],
|
||||
path: "plus://abc123",
|
||||
plus: PLUS_MODEL,
|
||||
@@ -359,7 +477,7 @@ test.describe("Detection models settings @high", () => {
|
||||
}) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{
|
||||
scene: "all",
|
||||
scene: "default",
|
||||
devices: ["openvino:GPU.0"],
|
||||
path: "plus://abc123",
|
||||
width: 320,
|
||||
@@ -383,7 +501,13 @@ test.describe("Detection models settings @high", () => {
|
||||
// says nothing; which device it was built for is what the user picks on
|
||||
await installRoutes(
|
||||
frigateApp.page,
|
||||
[{ scene: "all", devices: ["hailo:PCIe"], path: "/config/custom.hef" }],
|
||||
[
|
||||
{
|
||||
scene: "default",
|
||||
devices: ["hailo:PCIe"],
|
||||
path: "/config/custom.hef",
|
||||
},
|
||||
],
|
||||
true,
|
||||
HAILO_PLUS_MODELS,
|
||||
true,
|
||||
@@ -413,7 +537,7 @@ test.describe("Detection models settings @high", () => {
|
||||
// that must not read as an edit.
|
||||
await installRoutes(frigateApp.page, [
|
||||
{
|
||||
scene: "all",
|
||||
scene: "default",
|
||||
devices: ["openvino:GPU.0", "openvino:GPU.0"],
|
||||
path: "/config/model_cache/abc123",
|
||||
width: 320,
|
||||
@@ -438,12 +562,14 @@ test.describe("Detection models settings @high", () => {
|
||||
frigateApp,
|
||||
}) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "all", devices: ["openvino:GPU.0"] },
|
||||
{ scene: "default", devices: ["openvino:GPU.0"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
const root = frigateApp.page.locator("#pageRoot");
|
||||
await expect(root).toContainText("The environment this model is for");
|
||||
await expect(root).toContainText(
|
||||
"A name for the cameras this model is for",
|
||||
);
|
||||
await expect(root).toContainText(
|
||||
"The hardware this model runs its detection on",
|
||||
);
|
||||
@@ -454,7 +580,7 @@ test.describe("Detection models settings @high", () => {
|
||||
frigateApp,
|
||||
}) => {
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "all", devices: ["edgetpu:pci:0"] },
|
||||
{ scene: "default", devices: ["edgetpu:pci:0"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
@@ -465,10 +591,10 @@ test.describe("Detection models settings @high", () => {
|
||||
});
|
||||
|
||||
test("removing the default model blocks saving", async ({ frigateApp }) => {
|
||||
// a camera that names no scene runs the "all" model, so deleting it would
|
||||
// a camera that names no scene runs the default model, so deleting it would
|
||||
// leave those cameras with nothing to fall back to
|
||||
await installRoutes(frigateApp.page, [
|
||||
{ scene: "all", devices: ["cpu"] },
|
||||
{ scene: "default", devices: ["cpu"] },
|
||||
{ scene: "outdoor", devices: ["edgetpu:pci:0"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
@@ -479,7 +605,7 @@ test.describe("Detection models settings @high", () => {
|
||||
.click();
|
||||
|
||||
await expect(frigateApp.page.locator("#pageRoot")).toContainText(
|
||||
"One model must use a scene of 'All cameras'",
|
||||
"One model must use the default scene",
|
||||
);
|
||||
await expect(
|
||||
frigateApp.page.getByRole("button", { name: /^Save$/ }),
|
||||
@@ -490,7 +616,7 @@ test.describe("Detection models settings @high", () => {
|
||||
// shareable hardware can report several addressable units; every one of
|
||||
// them must be reachable, not just the first
|
||||
const saves = await installRoutes(frigateApp.page, [
|
||||
{ scene: "all", devices: ["openvino:GPU.0"] },
|
||||
{ scene: "default", devices: ["openvino:GPU.0"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
@@ -508,7 +634,7 @@ test.describe("Detection models settings @high", () => {
|
||||
frigateApp,
|
||||
}) => {
|
||||
const saves = await installRoutes(frigateApp.page, [
|
||||
{ scene: "all", devices: ["openvino:GPU.0", "openvino:GPU.1"] },
|
||||
{ scene: "default", devices: ["openvino:GPU.0", "openvino:GPU.1"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
@@ -531,7 +657,7 @@ test.describe("Detection models settings @high", () => {
|
||||
frigateApp,
|
||||
}) => {
|
||||
const saves = await installRoutes(frigateApp.page, [
|
||||
{ scene: "all", devices: ["edgetpu:pci:0"] },
|
||||
{ scene: "default", devices: ["edgetpu:pci:0"] },
|
||||
]);
|
||||
await openPage(frigateApp);
|
||||
|
||||
|
||||
@@ -483,7 +483,7 @@ test.describe("System — Health hardware pane @medium", () => {
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: { models: [{ scene: "all", devices: ["openvino:GPU"] }] },
|
||||
config: { models: [{ scene: "default", devices: ["openvino:GPU"] }] },
|
||||
stats: {
|
||||
...QUIET_STATS,
|
||||
detectors: { "openvino:GPU": { inference_speed: 12.3 } },
|
||||
@@ -500,7 +500,7 @@ test.describe("System — Health hardware pane @medium", () => {
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: { models: [{ scene: "all", devices: ["hailo"] }] },
|
||||
config: { models: [{ scene: "default", devices: ["hailo"] }] },
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
@@ -516,7 +516,7 @@ test.describe("System — Health hardware pane @medium", () => {
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: { models: [{ scene: "all", devices: ["openvino:AUTO"] }] },
|
||||
config: { models: [{ scene: "default", devices: ["openvino:AUTO"] }] },
|
||||
stats: {
|
||||
...QUIET_STATS,
|
||||
detectors: { "openvino:AUTO": { inference_speed: 12 } },
|
||||
@@ -536,7 +536,7 @@ test.describe("System — Health hardware pane @medium", () => {
|
||||
}) => {
|
||||
// the default image runs onnx on the CPU and the probe reports nothing
|
||||
await frigateApp.installDefaults({
|
||||
config: { models: [{ scene: "all", devices: ["onnx"] }] },
|
||||
config: { models: [{ scene: "default", devices: ["onnx"] }] },
|
||||
stats: { ...QUIET_STATS, detectors: { onnx: { inference_speed: 40 } } },
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
@@ -549,7 +549,7 @@ test.describe("System — Health hardware pane @medium", () => {
|
||||
|
||||
test("detection row warns on slow inference", async ({ frigateApp }) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: { models: [{ scene: "all", devices: ["openvino:GPU"] }] },
|
||||
config: { models: [{ scene: "default", devices: ["openvino:GPU"] }] },
|
||||
stats: {
|
||||
...QUIET_STATS,
|
||||
detectors: { "openvino:GPU": { inference_speed: 60 } },
|
||||
|
||||
Reference in New Issue
Block a user