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

* Refactor model scene definitions

* Cleanup

* Validate model paths in the UI

* Handle form validation
This commit is contained in:
Nicolas Mowen
2026-09-30 06:02:14 -06:00
committed by GitHub
parent 5e87d101da
commit 1bb61eb808
30 changed files with 862 additions and 227 deletions
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");
});
});
+154 -28
View File
@@ -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);
+5 -5
View File
@@ -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 } },