mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-29 23:29:01 +03:00
Miscellaneous fixes (0.18 beta) (#23763)
CI / AMD64 Build (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 / 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
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Semantic Search settings tests -- MEDIUM tier.
|
||||
*
|
||||
* Focuses on the model_size field, which is unused when a GenAI embeddings
|
||||
* provider is selected as the semantic search model. The resolved config always
|
||||
* reports model_size (it has a schema default of "small"), even when the YAML
|
||||
* file has no such key. Clearing model_size for a provider used to run
|
||||
* unconditionally, which falsely marked the section dirty on load and asked the
|
||||
* backend to delete a key that wasn't in the config file (KeyError: 'model_size').
|
||||
*/
|
||||
|
||||
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 PROVIDER = "llama_cpp";
|
||||
const SETTINGS_URL = "/settings?page=integrationSemanticSearch";
|
||||
const NOT_APPLICABLE = "Not applicable for GenAI providers";
|
||||
const UNSAVED = "You have unsaved changes";
|
||||
|
||||
type SemanticSearch = {
|
||||
enabled?: boolean;
|
||||
model?: string;
|
||||
model_size?: string;
|
||||
};
|
||||
|
||||
async function installRoutes(page: Page, semanticSearch: SemanticSearch) {
|
||||
const config = configFactory({
|
||||
genai: { [PROVIDER]: { provider: PROVIDER, roles: ["embeddings"] } },
|
||||
semantic_search: semanticSearch,
|
||||
});
|
||||
|
||||
let lastSavedConfig: unknown = null;
|
||||
|
||||
await page.route("**/api/config/schema.json", (route) =>
|
||||
route.fulfill({ json: CONFIG_SCHEMA }),
|
||||
);
|
||||
await page.route("**/api/config", (route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
return route.fulfill({ json: config });
|
||||
}
|
||||
return route.fulfill({ json: { success: true } });
|
||||
});
|
||||
await page.route("**/api/config/set", async (route) => {
|
||||
lastSavedConfig = route.request().postDataJSON();
|
||||
await route.fulfill({ json: { success: true, require_restart: false } });
|
||||
});
|
||||
await page.route("**/api/config/raw_paths", (route) =>
|
||||
route.fulfill({ json: { semantic_search: semanticSearch } }),
|
||||
);
|
||||
|
||||
return { capturedConfig: () => lastSavedConfig };
|
||||
}
|
||||
|
||||
test.describe("semantic search model_size @medium", () => {
|
||||
test("a provider with a defaulted model_size is not dirty on load", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
// model_size stays at its schema default ("small"), i.e. it is not present
|
||||
// in the YAML. This mirrors the reported bug: selecting a GenAI provider and
|
||||
// returning to the page.
|
||||
await installRoutes(frigateApp.page, {
|
||||
enabled: true,
|
||||
model: PROVIDER,
|
||||
});
|
||||
await frigateApp.goto(SETTINGS_URL);
|
||||
|
||||
// The provider path is active: model_size shows "Not applicable".
|
||||
await expect(frigateApp.page.getByText(NOT_APPLICABLE)).toBeVisible();
|
||||
|
||||
// Give any clearing effect time to fire, then confirm the section stayed
|
||||
// clean (no phantom unsaved-changes banner, Save disabled).
|
||||
await frigateApp.page.waitForTimeout(1000);
|
||||
await expect(frigateApp.page.getByText(UNSAVED)).toBeHidden();
|
||||
await expect(
|
||||
frigateApp.page.getByRole("button", { name: "Save", exact: true }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
test("switching from a configured non-default model_size clears it", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
// A genuinely configured non-default model_size ("large") can only come from
|
||||
// the YAML, so switching to a provider must still remove it.
|
||||
const capture = await installRoutes(frigateApp.page, {
|
||||
enabled: true,
|
||||
model: "jinav2",
|
||||
model_size: "large",
|
||||
});
|
||||
await frigateApp.goto(SETTINGS_URL);
|
||||
|
||||
// Starts clean on a Jina model.
|
||||
await expect(frigateApp.page.getByText(UNSAVED)).toBeHidden();
|
||||
|
||||
// Switch the model to the GenAI provider.
|
||||
await frigateApp.page
|
||||
.getByRole("combobox", { name: /Semantic search model/ })
|
||||
.click();
|
||||
await frigateApp.page.getByRole("option", { name: PROVIDER }).click();
|
||||
|
||||
// The change is now dirty and model_size is no longer applicable.
|
||||
await expect(frigateApp.page.getByText(NOT_APPLICABLE)).toBeVisible();
|
||||
await expect(frigateApp.page.getByText(UNSAVED)).toBeVisible();
|
||||
|
||||
await frigateApp.page
|
||||
.getByRole("button", { name: "Save", exact: true })
|
||||
.click();
|
||||
|
||||
// The saved payload removes model_size (empty string = "remove" key).
|
||||
await expect
|
||||
.poll(() => capture.capturedConfig(), { timeout: 5_000 })
|
||||
.toMatchObject({
|
||||
config_data: {
|
||||
semantic_search: { model: PROVIDER, model_size: "" },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1936,7 +1936,8 @@
|
||||
"inputDimensionsNotDetectResolution": "Model input width and height are the input dimensions of the object detection model, not your camera's detect resolution. They should match the dimensions of the model you're using — typically a square size like 320x320 or 640x640."
|
||||
},
|
||||
"ffmpeg": {
|
||||
"hwaccelManualNotRecommended": "Manual hardware acceleration arguments are not recommended. Unless a specific requirement exists, select the preset that matches your hardware."
|
||||
"hwaccelManualNotRecommended": "Manual hardware acceleration arguments are not recommended. Unless a specific requirement exists, select the preset that matches your hardware.",
|
||||
"inputsMissingGo2rtcStream": "An input below points at a go2rtc restream that no longer exists. Select an existing restream or enter the camera's URL manually, otherwise this camera will fail to connect."
|
||||
},
|
||||
"objects": {
|
||||
"genaiNoDescriptionsProvider": "You must configure a GenAI provider with the 'descriptions' role for descriptions to be generated."
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseRestreamStreamName } from "../theme/fields/streamSource";
|
||||
import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const arrayAsTextWidget = {
|
||||
@@ -42,6 +43,29 @@ const ffmpeg: SectionConfigOverrides = {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "inputs-missing-go2rtc-stream",
|
||||
field: "inputs",
|
||||
position: "before",
|
||||
messageKey: "configMessages.ffmpeg.inputsMissingGo2rtcStream",
|
||||
severity: "warning",
|
||||
docLink: "/configuration/restream",
|
||||
condition: (ctx) => {
|
||||
const streams = ctx.fullConfig?.go2rtc?.streams;
|
||||
const inputs = ctx.formData?.inputs;
|
||||
if (!Array.isArray(inputs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return inputs.some((input) => {
|
||||
const path = (input as { path?: unknown } | null)?.path;
|
||||
const streamName = parseRestreamStreamName(
|
||||
typeof path === "string" ? path : undefined,
|
||||
);
|
||||
return streamName !== undefined && !(streamName in (streams ?? {}));
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
fieldDocs: {
|
||||
hwaccel_args: "/configuration/ffmpeg_presets#hwaccel-presets",
|
||||
|
||||
@@ -170,6 +170,12 @@ export function CameraInputsField(props: FieldProps) {
|
||||
[go2rtcStreamNames],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSourceModeByIndex((previous) =>
|
||||
Object.keys(previous).length > 0 ? {} : previous,
|
||||
);
|
||||
}, [formContext?.cameraName]);
|
||||
|
||||
useEffect(() => {
|
||||
setOpenByIndex((previous) => {
|
||||
const next: Record<number, boolean> = {};
|
||||
@@ -222,18 +228,12 @@ export function CameraInputsField(props: FieldProps) {
|
||||
const handleSourceModeChange = useCallback(
|
||||
(index: number, nextMode: StreamSourceMode) => {
|
||||
const input = inputs[index];
|
||||
const currentPath =
|
||||
typeof input?.path === "string" ? input.path : undefined;
|
||||
|
||||
if (nextMode === "manual") {
|
||||
// Only revert the preset we set ourselves; never clobber custom args.
|
||||
if (input?.input_args === RESTREAM_PRESET) {
|
||||
handleFieldValuesChange(index, { input_args: undefined });
|
||||
}
|
||||
} else if (!parseRestreamStreamName(currentPath)) {
|
||||
// Entering restream with a non-restream path: clear it so the dropdown
|
||||
// shows its placeholder until a stream is chosen.
|
||||
handleFieldValuesChange(index, { path: undefined });
|
||||
// Only revert the preset we set ourselves; never clobber custom args.
|
||||
// The path is left alone until a stream is picked, so switching modes
|
||||
// never discards a typed URL or empties a required field.
|
||||
if (nextMode === "manual" && input?.input_args === RESTREAM_PRESET) {
|
||||
handleFieldValuesChange(index, { input_args: undefined });
|
||||
}
|
||||
|
||||
setSourceModeByIndex((previous) => ({ ...previous, [index]: nextMode }));
|
||||
|
||||
@@ -24,15 +24,19 @@ export function SemanticSearchModelSizeWidget(props: WidgetProps) {
|
||||
model !== "jinav1" &&
|
||||
model !== "jinav2";
|
||||
|
||||
// Clear model_size while on a provider (buildOverrides converts to ""
|
||||
// which the backend treats as "remove"). Restore the schema default
|
||||
// when returning to a Jina model so the field isn't left empty.
|
||||
// model_size is unused on a GenAI provider. Only clear it (which the backend
|
||||
// treats as "remove") for a non-default value, which can only come from the
|
||||
// config file. A defaulted value is indistinguishable from unset in the
|
||||
// resolved config, so clearing it would falsely dirty the field and delete a
|
||||
// YAML key that isn't there. Restore the default when returning to a Jina model.
|
||||
const { value, onChange, schema } = props;
|
||||
const schemaDefault = schema?.default as string | undefined;
|
||||
useEffect(() => {
|
||||
if (isProvider && value !== undefined) {
|
||||
onChange(undefined);
|
||||
} else if (!isProvider && value === undefined && schemaDefault) {
|
||||
if (isProvider) {
|
||||
if (value !== undefined && value !== schemaDefault) {
|
||||
onChange(undefined);
|
||||
}
|
||||
} else if (value === undefined && schemaDefault) {
|
||||
onChange(schemaDefault);
|
||||
}
|
||||
}, [isProvider, value, onChange, schemaDefault]);
|
||||
|
||||
Reference in New Issue
Block a user