mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-06-21 03:41:55 +03:00
Tweaks (#23292)
Some checks failed
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
Some checks failed
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
* add review padding to explore debug replay api calls * add semantic search model size widget disables model_size select with n/a text when an embeddings genai provider is selected * regenerate zone contours and per-zone filter masks on detect resolution change * treat null as a clear sentinel in buildOverrides so nullable field edits don't snap back * extract replay config sheet to new component * add validation and messages for detect settings
This commit is contained in:
parent
3a09d01bbe
commit
d556ff8df2
@ -770,6 +770,13 @@ def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONRespo
|
|||||||
),
|
),
|
||||||
cam_cfg.objects,
|
cam_cfg.objects,
|
||||||
)
|
)
|
||||||
|
if cam_cfg.zones:
|
||||||
|
request.app.config_publisher.publish_update(
|
||||||
|
CameraConfigUpdateTopic(
|
||||||
|
CameraConfigUpdateEnum.zones, camera
|
||||||
|
),
|
||||||
|
cam_cfg.zones,
|
||||||
|
)
|
||||||
request.app.config_publisher.publish_update(
|
request.app.config_publisher.publish_update(
|
||||||
CameraConfigUpdateTopic(
|
CameraConfigUpdateTopic(
|
||||||
CameraConfigUpdateEnum.refresh, camera
|
CameraConfigUpdateEnum.refresh, camera
|
||||||
|
|||||||
@ -816,6 +816,17 @@ def apply_section_update(camera_config, section: str, update: dict) -> Optional[
|
|||||||
**filt.model_dump(exclude_unset=True, exclude={"mask", "raw_mask"}),
|
**filt.model_dump(exclude_unset=True, exclude={"mask", "raw_mask"}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Regenerate zone contours and per-zone filter masks at the new
|
||||||
|
# frame_shape so zone outlines and membership stay relative
|
||||||
|
for zone in camera_config.zones.values():
|
||||||
|
if zone.filters:
|
||||||
|
for zone_obj_name, zone_filter in zone.filters.items():
|
||||||
|
zone.filters[zone_obj_name] = RuntimeFilterConfig(
|
||||||
|
frame_shape=new_frame_shape,
|
||||||
|
**zone_filter.model_dump(exclude_unset=True),
|
||||||
|
)
|
||||||
|
zone.generate_contour(new_frame_shape)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
merged = deep_merge(current.model_dump(), update, override=True)
|
merged = deep_merge(current.model_dump(), update, override=True)
|
||||||
setattr(camera_config, section, current.__class__.model_validate(merged))
|
setattr(camera_config, section, current.__class__.model_validate(merged))
|
||||||
|
|||||||
@ -28,5 +28,8 @@
|
|||||||
"detectRequired": "At least one input stream must be assigned the 'detect' role.",
|
"detectRequired": "At least one input stream must be assigned the 'detect' role.",
|
||||||
"hwaccelDetectOnly": "Only the input stream with the detect role can define hardware acceleration arguments."
|
"hwaccelDetectOnly": "Only the input stream with the detect role can define hardware acceleration arguments."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"detect": {
|
||||||
|
"dimensionMustBeEven": "Must be an even number."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1543,6 +1543,9 @@
|
|||||||
"builtIn": "Built-in Models",
|
"builtIn": "Built-in Models",
|
||||||
"genaiProviders": "GenAI Providers"
|
"genaiProviders": "GenAI Providers"
|
||||||
},
|
},
|
||||||
|
"semanticSearchModelSize": {
|
||||||
|
"notApplicable": "Not applicable for GenAI providers"
|
||||||
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Review Settings"
|
"title": "Review Settings"
|
||||||
},
|
},
|
||||||
@ -1791,7 +1794,9 @@
|
|||||||
},
|
},
|
||||||
"detect": {
|
"detect": {
|
||||||
"fpsGreaterThanFive": "Setting the detect FPS higher than 5 is not recommended. Higher values may cause performance issues and will not provide any benefit.",
|
"fpsGreaterThanFive": "Setting the detect FPS higher than 5 is not recommended. Higher values may cause performance issues and will not provide any benefit.",
|
||||||
"disabled": "Object detection is disabled. Snapshots, review items, and enrichments such as face recognition, license plate recognition, and Generative AI will not function."
|
"disabled": "Object detection is disabled. Snapshots, review items, and enrichments such as face recognition, license plate recognition, and Generative AI will not function.",
|
||||||
|
"resolutionShouldBeMultipleOfFour": "For best results, detect width and height should be multiples of 4. Other even values may produce visual artifacts or slight distortion in the detect stream.",
|
||||||
|
"aspectRatioMismatch": "The width and height you've entered don't match the aspect ratio of your current detect resolution. This may produce a stretched or distorted image."
|
||||||
},
|
},
|
||||||
"objects": {
|
"objects": {
|
||||||
"genaiNoDescriptionsProvider": "You must configure a GenAI provider with the 'descriptions' role for descriptions to be generated."
|
"genaiNoDescriptionsProvider": "You must configure a GenAI provider with the 'descriptions' role for descriptions to be generated."
|
||||||
@ -1820,8 +1825,7 @@
|
|||||||
"mixedTypesSuggestion": "All detectors must use the same type. Remove existing detectors or select {{type}}."
|
"mixedTypesSuggestion": "All detectors must use the same type. Remove existing detectors or select {{type}}."
|
||||||
},
|
},
|
||||||
"semanticSearch": {
|
"semanticSearch": {
|
||||||
"jinav2SmallModelSize": "The 'small' size with the Jina V2 model has high RAM and inference cost. The 'large' model with a discrete GPU is recommended.",
|
"jinav2SmallModelSize": "The 'small' size with the Jina V2 model has high RAM and inference cost. The 'large' model with a discrete GPU is recommended."
|
||||||
"modelSizeIgnoredForProvider": "Model size only applies to the built-in Jina models. This value will be ignored when using a GenAI embedding provider."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
web/src/components/config-form/LiveFormDataContext.ts
Normal file
13
web/src/components/config-form/LiveFormDataContext.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
import type { ConfigSectionData } from "@/types/configForm";
|
||||||
|
|
||||||
|
// Mirrors the current section's in-flight form data so widgets can react
|
||||||
|
// to changes that RJSF wouldn't otherwise re-render them for. RJSF's
|
||||||
|
// Form memoizes SchemaField via deep equality and, in some transitions
|
||||||
|
// (notably reverting a field to its saved value), can skip re-rendering
|
||||||
|
// a widget even though the form data it depends on changed. useContext
|
||||||
|
// re-runs consumers directly on every provider value update, sidestepping
|
||||||
|
// that.
|
||||||
|
export const LiveFormDataContext = createContext<ConfigSectionData | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
@ -11,6 +11,50 @@ const detect: SectionConfigOverrides = {
|
|||||||
condition: (ctx) =>
|
condition: (ctx) =>
|
||||||
ctx.level === "camera" && ctx.formData?.enabled === false,
|
ctx.level === "camera" && ctx.formData?.enabled === false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "detect-resolution-not-multiple-of-four",
|
||||||
|
messageKey: "configMessages.detect.resolutionShouldBeMultipleOfFour",
|
||||||
|
severity: "warning",
|
||||||
|
condition: (ctx) => {
|
||||||
|
const width = ctx.formData?.width as number | null | undefined;
|
||||||
|
const height = ctx.formData?.height as number | null | undefined;
|
||||||
|
const isEvenButNotFour = (v: unknown) =>
|
||||||
|
typeof v === "number" && v % 2 === 0 && v % 4 !== 0;
|
||||||
|
return isEvenButNotFour(width) || isEvenButNotFour(height);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "detect-aspect-ratio-mismatch",
|
||||||
|
messageKey: "configMessages.detect.aspectRatioMismatch",
|
||||||
|
severity: "warning",
|
||||||
|
condition: (ctx) => {
|
||||||
|
const newWidth = ctx.formData?.width as number | null | undefined;
|
||||||
|
const newHeight = ctx.formData?.height as number | null | undefined;
|
||||||
|
if (typeof newWidth !== "number" || typeof newHeight !== "number") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const saved =
|
||||||
|
ctx.level === "camera"
|
||||||
|
? ctx.fullCameraConfig?.detect
|
||||||
|
: ctx.fullConfig?.detect;
|
||||||
|
const savedWidth = saved?.width;
|
||||||
|
const savedHeight = saved?.height;
|
||||||
|
if (
|
||||||
|
typeof savedWidth !== "number" ||
|
||||||
|
typeof savedHeight !== "number" ||
|
||||||
|
savedWidth <= 0 ||
|
||||||
|
savedHeight <= 0
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (newWidth === savedWidth && newHeight === savedHeight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const newRatio = newWidth / newHeight;
|
||||||
|
const savedRatio = savedWidth / savedHeight;
|
||||||
|
return Math.abs(newRatio - savedRatio) > 0.01;
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
fieldMessages: [
|
fieldMessages: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -29,28 +29,13 @@ const semanticSearch: SectionConfigOverrides = {
|
|||||||
ctx.formData?.model === "jinav2" &&
|
ctx.formData?.model === "jinav2" &&
|
||||||
ctx.formData?.model_size === "small",
|
ctx.formData?.model_size === "small",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "model-size-ignored-for-provider",
|
|
||||||
field: "model_size",
|
|
||||||
messageKey: "configMessages.semanticSearch.modelSizeIgnoredForProvider",
|
|
||||||
severity: "info",
|
|
||||||
position: "after",
|
|
||||||
condition: (ctx) => {
|
|
||||||
const model = ctx.formData?.model;
|
|
||||||
return (
|
|
||||||
typeof model === "string" &&
|
|
||||||
model !== "" &&
|
|
||||||
model !== "jinav1" &&
|
|
||||||
model !== "jinav2"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
model: {
|
model: {
|
||||||
"ui:widget": "semanticSearchModel",
|
"ui:widget": "semanticSearchModel",
|
||||||
},
|
},
|
||||||
model_size: {
|
model_size: {
|
||||||
|
"ui:widget": "semanticSearchModelSize",
|
||||||
"ui:options": { size: "xs", enumI18nPrefix: "modelSize" },
|
"ui:options": { size: "xs", enumI18nPrefix: "modelSize" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
36
web/src/components/config-form/section-validations/detect.ts
Normal file
36
web/src/components/config-form/section-validations/detect.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import type { FormValidation } from "@rjsf/utils";
|
||||||
|
import type { TFunction } from "i18next";
|
||||||
|
import { isJsonObject } from "@/lib/utils";
|
||||||
|
import type { JsonObject } from "@/types/configForm";
|
||||||
|
|
||||||
|
export function validateDetectDimensions(
|
||||||
|
formData: unknown,
|
||||||
|
errors: FormValidation,
|
||||||
|
t: TFunction,
|
||||||
|
): FormValidation {
|
||||||
|
if (!isJsonObject(formData as JsonObject)) {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = formData as JsonObject;
|
||||||
|
const width = data.width;
|
||||||
|
const height = data.height;
|
||||||
|
|
||||||
|
const widthErrors = errors.width as
|
||||||
|
| { addError?: (message: string) => void }
|
||||||
|
| undefined;
|
||||||
|
const heightErrors = errors.height as
|
||||||
|
| { addError?: (message: string) => void }
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const message = t("detect.dimensionMustBeEven", { ns: "config/validation" });
|
||||||
|
|
||||||
|
if (typeof width === "number" && width % 2 !== 0) {
|
||||||
|
widthErrors?.addError?.(message);
|
||||||
|
}
|
||||||
|
if (typeof height === "number" && height % 2 !== 0) {
|
||||||
|
heightErrors?.addError?.(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import type { FormValidation } from "@rjsf/utils";
|
import type { FormValidation } from "@rjsf/utils";
|
||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
|
import { validateDetectDimensions } from "./detect";
|
||||||
import { validateFfmpegInputRoles } from "./ffmpeg";
|
import { validateFfmpegInputRoles } from "./ffmpeg";
|
||||||
import { validateProxyRoleHeader } from "./proxy";
|
import { validateProxyRoleHeader } from "./proxy";
|
||||||
|
|
||||||
@ -19,6 +20,10 @@ export function getSectionValidation({
|
|||||||
level,
|
level,
|
||||||
t,
|
t,
|
||||||
}: SectionValidationOptions): SectionValidation | undefined {
|
}: SectionValidationOptions): SectionValidation | undefined {
|
||||||
|
if (sectionPath === "detect") {
|
||||||
|
return (formData, errors) => validateDetectDimensions(formData, errors, t);
|
||||||
|
}
|
||||||
|
|
||||||
if (sectionPath === "ffmpeg" && level === "camera") {
|
if (sectionPath === "ffmpeg" && level === "camera") {
|
||||||
return (formData, errors) => validateFfmpegInputRoles(formData, errors, t);
|
return (formData, errors) => validateFfmpegInputRoles(formData, errors, t);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,6 +87,7 @@ import type {
|
|||||||
import { useConfigMessages } from "@/hooks/use-config-messages";
|
import { useConfigMessages } from "@/hooks/use-config-messages";
|
||||||
import { ConfigMessageBanner } from "../ConfigMessageBanner";
|
import { ConfigMessageBanner } from "../ConfigMessageBanner";
|
||||||
import { FieldMessagesContext } from "../FieldMessagesContext";
|
import { FieldMessagesContext } from "../FieldMessagesContext";
|
||||||
|
import { LiveFormDataContext } from "../LiveFormDataContext";
|
||||||
|
|
||||||
export interface SectionConfig {
|
export interface SectionConfig {
|
||||||
/** Field ordering within the section */
|
/** Field ordering within the section */
|
||||||
@ -998,59 +999,63 @@ export function ConfigSection({
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<ConfigMessageBanner messages={activeMessages} />
|
<ConfigMessageBanner messages={activeMessages} />
|
||||||
<FieldMessagesContext.Provider value={activeFieldMessages}>
|
<FieldMessagesContext.Provider value={activeFieldMessages}>
|
||||||
<ConfigForm
|
<LiveFormDataContext.Provider
|
||||||
key={formKey}
|
value={(currentFormData as ConfigSectionData | null) ?? null}
|
||||||
schema={modifiedSchema}
|
>
|
||||||
formData={currentFormData}
|
<ConfigForm
|
||||||
onChange={handleChange}
|
key={formKey}
|
||||||
onValidationChange={setHasValidationErrors}
|
schema={modifiedSchema}
|
||||||
fieldOrder={sectionConfig.fieldOrder}
|
formData={currentFormData}
|
||||||
fieldGroups={sectionConfig.fieldGroups}
|
onChange={handleChange}
|
||||||
hiddenFields={effectiveHiddenFields}
|
onValidationChange={setHasValidationErrors}
|
||||||
advancedFields={sectionConfig.advancedFields}
|
fieldOrder={sectionConfig.fieldOrder}
|
||||||
liveValidate={sectionConfig.liveValidate}
|
fieldGroups={sectionConfig.fieldGroups}
|
||||||
uiSchema={sectionConfig.uiSchema}
|
hiddenFields={effectiveHiddenFields}
|
||||||
disabled={disabled || isSaving}
|
advancedFields={sectionConfig.advancedFields}
|
||||||
readonly={readonly}
|
liveValidate={sectionConfig.liveValidate}
|
||||||
showSubmit={false}
|
uiSchema={sectionConfig.uiSchema}
|
||||||
i18nNamespace={configNamespace}
|
disabled={disabled || isSaving}
|
||||||
customValidate={customValidate}
|
readonly={readonly}
|
||||||
formContext={{
|
showSubmit={false}
|
||||||
level: effectiveLevel,
|
i18nNamespace={configNamespace}
|
||||||
cameraName,
|
customValidate={customValidate}
|
||||||
globalValue,
|
formContext={{
|
||||||
cameraValue,
|
level: effectiveLevel,
|
||||||
hasChanges,
|
cameraName,
|
||||||
extraHasChanges,
|
globalValue,
|
||||||
setExtraHasChanges,
|
cameraValue,
|
||||||
overrides: uiOverrides as JsonValue | undefined,
|
hasChanges,
|
||||||
formData: currentFormData as ConfigSectionData,
|
extraHasChanges,
|
||||||
baselineFormData: effectiveBaselineFormData as ConfigSectionData,
|
setExtraHasChanges,
|
||||||
pendingDataBySection,
|
overrides: uiOverrides as JsonValue | undefined,
|
||||||
onPendingDataChange,
|
formData: currentFormData as ConfigSectionData,
|
||||||
onFormDataChange: (data: ConfigSectionData) => handleChange(data),
|
baselineFormData: effectiveBaselineFormData as ConfigSectionData,
|
||||||
// For widgets that need access to full camera config (e.g., zone names)
|
pendingDataBySection,
|
||||||
fullCameraConfig:
|
onPendingDataChange,
|
||||||
effectiveLevel === "camera" && cameraName
|
onFormDataChange: (data: ConfigSectionData) => handleChange(data),
|
||||||
? config?.cameras?.[cameraName]
|
// For widgets that need access to full camera config (e.g., zone names)
|
||||||
: undefined,
|
fullCameraConfig:
|
||||||
fullConfig: config,
|
effectiveLevel === "camera" && cameraName
|
||||||
// When rendering camera-level sections, provide the section path so
|
? config?.cameras?.[cameraName]
|
||||||
// field templates can look up keys under the `config/cameras` namespace
|
: undefined,
|
||||||
// When using a consolidated global namespace, keys are nested
|
fullConfig: config,
|
||||||
// under the section name (e.g., `audio.label`) so provide the
|
// When rendering camera-level sections, provide the section path so
|
||||||
// section prefix to templates so they can attempt `${section}.${field}` lookups.
|
// field templates can look up keys under the `config/cameras` namespace
|
||||||
sectionI18nPrefix: sectionPath,
|
// When using a consolidated global namespace, keys are nested
|
||||||
t,
|
// under the section name (e.g., `audio.label`) so provide the
|
||||||
renderers: wrappedRenderers,
|
// section prefix to templates so they can attempt `${section}.${field}` lookups.
|
||||||
sectionDocs: sectionConfig.sectionDocs,
|
sectionI18nPrefix: sectionPath,
|
||||||
fieldDocs: sectionConfig.fieldDocs,
|
t,
|
||||||
hiddenFields: effectiveHiddenFields,
|
renderers: wrappedRenderers,
|
||||||
restartRequired: sectionConfig.restartRequired,
|
sectionDocs: sectionConfig.sectionDocs,
|
||||||
requiresRestart,
|
fieldDocs: sectionConfig.fieldDocs,
|
||||||
isProfile: !!profileName,
|
hiddenFields: effectiveHiddenFields,
|
||||||
}}
|
restartRequired: sectionConfig.restartRequired,
|
||||||
/>
|
requiresRestart,
|
||||||
|
isProfile: !!profileName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</LiveFormDataContext.Provider>
|
||||||
</FieldMessagesContext.Provider>
|
</FieldMessagesContext.Provider>
|
||||||
|
|
||||||
{!embedded && (
|
{!embedded && (
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import { TimezoneSelectWidget } from "./widgets/TimezoneSelectWidget";
|
|||||||
import { CameraPathWidget } from "./widgets/CameraPathWidget";
|
import { CameraPathWidget } from "./widgets/CameraPathWidget";
|
||||||
import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget";
|
import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget";
|
||||||
import { SemanticSearchModelWidget } from "./widgets/SemanticSearchModelWidget";
|
import { SemanticSearchModelWidget } from "./widgets/SemanticSearchModelWidget";
|
||||||
|
import { SemanticSearchModelSizeWidget } from "./widgets/SemanticSearchModelSizeWidget";
|
||||||
import { OnvifProfileWidget } from "./widgets/OnvifProfileWidget";
|
import { OnvifProfileWidget } from "./widgets/OnvifProfileWidget";
|
||||||
|
|
||||||
import { FieldTemplate } from "./templates/FieldTemplate";
|
import { FieldTemplate } from "./templates/FieldTemplate";
|
||||||
@ -86,6 +87,7 @@ export const frigateTheme: FrigateTheme = {
|
|||||||
timezoneSelect: TimezoneSelectWidget,
|
timezoneSelect: TimezoneSelectWidget,
|
||||||
optionalField: OptionalFieldWidget,
|
optionalField: OptionalFieldWidget,
|
||||||
semanticSearchModel: SemanticSearchModelWidget,
|
semanticSearchModel: SemanticSearchModelWidget,
|
||||||
|
semanticSearchModelSize: SemanticSearchModelSizeWidget,
|
||||||
onvifProfile: OnvifProfileWidget,
|
onvifProfile: OnvifProfileWidget,
|
||||||
},
|
},
|
||||||
templates: {
|
templates: {
|
||||||
|
|||||||
@ -0,0 +1,57 @@
|
|||||||
|
// Disables model_size and shows "N/A" when a GenAI provider is selected.
|
||||||
|
// Reads model via LiveFormDataContext so it re-runs even when RJSF's
|
||||||
|
// SchemaField memoization would skip this widget.
|
||||||
|
import type { WidgetProps } from "@rjsf/utils";
|
||||||
|
import { useContext, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { LiveFormDataContext } from "../../LiveFormDataContext";
|
||||||
|
import { getSizedFieldClassName } from "../utils";
|
||||||
|
import { SelectWidget } from "./SelectWidget";
|
||||||
|
|
||||||
|
export function SemanticSearchModelSizeWidget(props: WidgetProps) {
|
||||||
|
const { t } = useTranslation(["views/settings"]);
|
||||||
|
const liveFormData = useContext(LiveFormDataContext);
|
||||||
|
const model = liveFormData?.model;
|
||||||
|
const isProvider =
|
||||||
|
typeof model === "string" &&
|
||||||
|
model !== "" &&
|
||||||
|
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.
|
||||||
|
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) {
|
||||||
|
onChange(schemaDefault);
|
||||||
|
}
|
||||||
|
}, [isProvider, value, onChange, schemaDefault]);
|
||||||
|
|
||||||
|
if (isProvider) {
|
||||||
|
const fieldClassName = getSizedFieldClassName(props.options ?? {}, "sm");
|
||||||
|
return (
|
||||||
|
<Select value="" disabled>
|
||||||
|
<SelectTrigger className={fieldClassName}>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t("configForm.semanticSearchModelSize.notApplicable", {
|
||||||
|
defaultValue: "Not applicable for GenAI providers",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent />
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SelectWidget {...props} />;
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { useState, ReactNode, useCallback } from "react";
|
import { useState, ReactNode, useCallback } from "react";
|
||||||
import { SearchResult } from "@/types/search";
|
import { SearchResult } from "@/types/search";
|
||||||
|
import { REVIEW_PADDING } from "@/types/review";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -94,8 +95,8 @@ export default function SearchResultActions({
|
|||||||
axios
|
axios
|
||||||
.post("debug_replay/start", {
|
.post("debug_replay/start", {
|
||||||
camera: event.camera,
|
camera: event.camera,
|
||||||
start_time: event.start_time,
|
start_time: (event.start_time ?? 0) - REVIEW_PADDING,
|
||||||
end_time: event.end_time,
|
end_time: (event.end_time ?? Date.now() / 1000) + REVIEW_PADDING,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 202 || response.status === 200) {
|
if (response.status === 202 || response.status === 200) {
|
||||||
|
|||||||
120
web/src/components/overlay/DebugReplayConfigSheet.tsx
Normal file
120
web/src/components/overlay/DebugReplayConfigSheet.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { LuSettings } from "react-icons/lu";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
|
import { ConfigSectionTemplate } from "@/components/config-form/sections/ConfigSectionTemplate";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { PlatformAwareSheet } from "@/components/overlay/dialog/PlatformAwareDialog";
|
||||||
|
import { useConfigSchema } from "@/hooks/use-config-schema";
|
||||||
|
import type { FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
|
||||||
|
type DebugReplayConfigSheetProps = {
|
||||||
|
replayCamera: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DebugReplayConfigSheet({
|
||||||
|
replayCamera,
|
||||||
|
}: DebugReplayConfigSheetProps) {
|
||||||
|
const { t } = useTranslation(["views/replay"]);
|
||||||
|
const configSchema = useConfigSchema();
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
});
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlatformAwareSheet
|
||||||
|
trigger={
|
||||||
|
<Button variant="outline" size="sm" className="flex items-center gap-2">
|
||||||
|
<LuSettings className="size-4" />
|
||||||
|
<span className="hidden md:inline">{t("page.configuration")}</span>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
title={t("page.configuration")}
|
||||||
|
titleClassName="text-lg font-semibold"
|
||||||
|
contentClassName="scrollbar-container flex flex-col gap-0 overflow-y-auto px-6 pb-6 sm:max-w-xl md:max-w-2xl xl:max-w-3xl"
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<p className="mb-5 text-sm text-muted-foreground">
|
||||||
|
{t("page.configurationDesc")}
|
||||||
|
</p>
|
||||||
|
{configSchema == null ? (
|
||||||
|
<div className="flex h-40 items-center justify-center">
|
||||||
|
<ActivityIndicator />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<ConfigSectionTemplate
|
||||||
|
sectionKey="detect"
|
||||||
|
level="replay"
|
||||||
|
cameraName={replayCamera}
|
||||||
|
skipSave
|
||||||
|
noStickyButtons
|
||||||
|
requiresRestart={false}
|
||||||
|
collapsible
|
||||||
|
defaultCollapsed={false}
|
||||||
|
showTitle
|
||||||
|
showOverrideIndicator={false}
|
||||||
|
/>
|
||||||
|
<ConfigSectionTemplate
|
||||||
|
sectionKey="motion"
|
||||||
|
level="replay"
|
||||||
|
cameraName={replayCamera}
|
||||||
|
skipSave
|
||||||
|
noStickyButtons
|
||||||
|
requiresRestart={false}
|
||||||
|
collapsible
|
||||||
|
defaultCollapsed={false}
|
||||||
|
showTitle
|
||||||
|
showOverrideIndicator={false}
|
||||||
|
/>
|
||||||
|
<ConfigSectionTemplate
|
||||||
|
sectionKey="objects"
|
||||||
|
level="replay"
|
||||||
|
cameraName={replayCamera}
|
||||||
|
skipSave
|
||||||
|
noStickyButtons
|
||||||
|
requiresRestart={false}
|
||||||
|
collapsible
|
||||||
|
defaultCollapsed={false}
|
||||||
|
showTitle
|
||||||
|
showOverrideIndicator={false}
|
||||||
|
/>
|
||||||
|
{config?.face_recognition?.enabled && (
|
||||||
|
<ConfigSectionTemplate
|
||||||
|
sectionKey="face_recognition"
|
||||||
|
level="replay"
|
||||||
|
cameraName={replayCamera}
|
||||||
|
skipSave
|
||||||
|
noStickyButtons
|
||||||
|
requiresRestart={false}
|
||||||
|
collapsible
|
||||||
|
defaultCollapsed={false}
|
||||||
|
showTitle
|
||||||
|
showOverrideIndicator={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{config?.lpr?.enabled && (
|
||||||
|
<ConfigSectionTemplate
|
||||||
|
sectionKey="lpr"
|
||||||
|
level="replay"
|
||||||
|
cameraName={replayCamera}
|
||||||
|
skipSave
|
||||||
|
noStickyButtons
|
||||||
|
requiresRestart={false}
|
||||||
|
collapsible
|
||||||
|
defaultCollapsed={false}
|
||||||
|
showTitle
|
||||||
|
showOverrideIndicator={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -63,8 +63,8 @@ export default function DetailActionsMenu({
|
|||||||
axios
|
axios
|
||||||
.post("debug_replay/start", {
|
.post("debug_replay/start", {
|
||||||
camera: search.camera,
|
camera: search.camera,
|
||||||
start_time: search.start_time,
|
start_time: (search.start_time ?? 0) - REVIEW_PADDING,
|
||||||
end_time: search.end_time,
|
end_time: (search.end_time ?? Date.now() / 1000) + REVIEW_PADDING,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 202 || response.status === 200) {
|
if (response.status === 202 || response.status === 200) {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { baseUrl } from "@/api/baseUrl";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Event } from "@/types/event";
|
import { Event } from "@/types/event";
|
||||||
|
import { REVIEW_PADDING } from "@/types/review";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||||
@ -58,8 +59,8 @@ export default function EventMenu({
|
|||||||
axios
|
axios
|
||||||
.post("debug_replay/start", {
|
.post("debug_replay/start", {
|
||||||
camera: event.camera,
|
camera: event.camera,
|
||||||
start_time: event.start_time,
|
start_time: (event.start_time ?? 0) - REVIEW_PADDING,
|
||||||
end_time: event.end_time,
|
end_time: (event.end_time ?? Date.now() / 1000) + REVIEW_PADDING,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 202 || response.status === 200) {
|
if (response.status === 202 || response.status === 200) {
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { PlatformAwareSheet } from "@/components/overlay/dialog/PlatformAwareDialog";
|
import { DebugReplayConfigSheet } from "@/components/overlay/DebugReplayConfigSheet";
|
||||||
import { useCameraActivity } from "@/hooks/use-camera-activity";
|
import { useCameraActivity } from "@/hooks/use-camera-activity";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Heading from "@/components/ui/heading";
|
import Heading from "@/components/ui/heading";
|
||||||
@ -40,16 +40,14 @@ import { Progress } from "@/components/ui/progress";
|
|||||||
import { ObjectType } from "@/types/ws";
|
import { ObjectType } from "@/types/ws";
|
||||||
import { useJobStatus } from "@/api/ws";
|
import { useJobStatus } from "@/api/ws";
|
||||||
import WsMessageFeed from "@/components/ws/WsMessageFeed";
|
import WsMessageFeed from "@/components/ws/WsMessageFeed";
|
||||||
import { ConfigSectionTemplate } from "@/components/config-form/sections/ConfigSectionTemplate";
|
|
||||||
|
|
||||||
import { LuExternalLink, LuInfo, LuSettings } from "react-icons/lu";
|
import { LuExternalLink, LuInfo } from "react-icons/lu";
|
||||||
import { LuSquare } from "react-icons/lu";
|
import { LuSquare } from "react-icons/lu";
|
||||||
import { MdReplay } from "react-icons/md";
|
import { MdReplay } from "react-icons/md";
|
||||||
import { isDesktop, isMobile } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
import Logo from "@/components/Logo";
|
import Logo from "@/components/Logo";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||||
import { useConfigSchema } from "@/hooks/use-config-schema";
|
|
||||||
import DebugDrawingLayer from "@/components/overlay/DebugDrawingLayer";
|
import DebugDrawingLayer from "@/components/overlay/DebugDrawingLayer";
|
||||||
import { IoMdArrowRoundBack } from "react-icons/io";
|
import { IoMdArrowRoundBack } from "react-icons/io";
|
||||||
|
|
||||||
@ -125,7 +123,6 @@ export default function Replay() {
|
|||||||
});
|
});
|
||||||
const { payload: replayJob } =
|
const { payload: replayJob } =
|
||||||
useJobStatus<DebugReplayJobResults>("debug_replay");
|
useJobStatus<DebugReplayJobResults>("debug_replay");
|
||||||
const configSchema = useConfigSchema();
|
|
||||||
const [isInitializing, setIsInitializing] = useState(true);
|
const [isInitializing, setIsInitializing] = useState(true);
|
||||||
|
|
||||||
// Refresh status immediately on mount to avoid showing "no session" briefly
|
// Refresh status immediately on mount to avoid showing "no session" briefly
|
||||||
@ -139,7 +136,6 @@ export default function Replay() {
|
|||||||
|
|
||||||
const [options, setOptions] = useState<DebugOptions>(DEFAULT_OPTIONS);
|
const [options, setOptions] = useState<DebugOptions>(DEFAULT_OPTIONS);
|
||||||
const [isStopping, setIsStopping] = useState(false);
|
const [isStopping, setIsStopping] = useState(false);
|
||||||
const [configDialogOpen, setConfigDialogOpen] = useState(false);
|
|
||||||
|
|
||||||
const searchParams = useMemo(() => {
|
const searchParams = useMemo(() => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@ -327,103 +323,8 @@ export default function Replay() {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<PlatformAwareSheet
|
<DebugReplayConfigSheet
|
||||||
trigger={
|
replayCamera={status.replay_camera ?? undefined}
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<LuSettings className="size-4" />
|
|
||||||
<span className="hidden md:inline">
|
|
||||||
{t("page.configuration")}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
title={t("page.configuration")}
|
|
||||||
titleClassName="text-lg font-semibold"
|
|
||||||
contentClassName="scrollbar-container flex flex-col gap-0 overflow-y-auto px-6 pb-6 sm:max-w-xl md:max-w-2xl xl:max-w-3xl"
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
<p className="mb-5 text-sm text-muted-foreground">
|
|
||||||
{t("page.configurationDesc")}
|
|
||||||
</p>
|
|
||||||
{configSchema == null ? (
|
|
||||||
<div className="flex h-40 items-center justify-center">
|
|
||||||
<ActivityIndicator />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<ConfigSectionTemplate
|
|
||||||
sectionKey="detect"
|
|
||||||
level="replay"
|
|
||||||
cameraName={status.replay_camera ?? undefined}
|
|
||||||
skipSave
|
|
||||||
noStickyButtons
|
|
||||||
requiresRestart={false}
|
|
||||||
collapsible
|
|
||||||
defaultCollapsed={false}
|
|
||||||
showTitle
|
|
||||||
showOverrideIndicator={false}
|
|
||||||
/>
|
|
||||||
<ConfigSectionTemplate
|
|
||||||
sectionKey="motion"
|
|
||||||
level="replay"
|
|
||||||
cameraName={status.replay_camera ?? undefined}
|
|
||||||
skipSave
|
|
||||||
noStickyButtons
|
|
||||||
requiresRestart={false}
|
|
||||||
collapsible
|
|
||||||
defaultCollapsed={false}
|
|
||||||
showTitle
|
|
||||||
showOverrideIndicator={false}
|
|
||||||
/>
|
|
||||||
<ConfigSectionTemplate
|
|
||||||
sectionKey="objects"
|
|
||||||
level="replay"
|
|
||||||
cameraName={status.replay_camera ?? undefined}
|
|
||||||
skipSave
|
|
||||||
noStickyButtons
|
|
||||||
requiresRestart={false}
|
|
||||||
collapsible
|
|
||||||
defaultCollapsed={false}
|
|
||||||
showTitle
|
|
||||||
showOverrideIndicator={false}
|
|
||||||
/>
|
|
||||||
{config?.face_recognition?.enabled && (
|
|
||||||
<ConfigSectionTemplate
|
|
||||||
sectionKey="face_recognition"
|
|
||||||
level="replay"
|
|
||||||
cameraName={status.replay_camera ?? undefined}
|
|
||||||
skipSave
|
|
||||||
noStickyButtons
|
|
||||||
requiresRestart={false}
|
|
||||||
collapsible
|
|
||||||
defaultCollapsed={false}
|
|
||||||
showTitle
|
|
||||||
showOverrideIndicator={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{config?.lpr?.enabled && (
|
|
||||||
<ConfigSectionTemplate
|
|
||||||
sectionKey="lpr"
|
|
||||||
level="replay"
|
|
||||||
cameraName={status.replay_camera ?? undefined}
|
|
||||||
skipSave
|
|
||||||
noStickyButtons
|
|
||||||
requiresRestart={false}
|
|
||||||
collapsible
|
|
||||||
defaultCollapsed={false}
|
|
||||||
showTitle
|
|
||||||
showOverrideIndicator={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
open={configDialogOpen}
|
|
||||||
onOpenChange={setConfigDialogOpen}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
|
|||||||
@ -229,7 +229,12 @@ export function buildOverrides(
|
|||||||
|
|
||||||
const result: JsonObject = {};
|
const result: JsonObject = {};
|
||||||
for (const [key, value] of Object.entries(currentObj)) {
|
for (const [key, value] of Object.entries(currentObj)) {
|
||||||
if (value === undefined && baseObj && baseObj[key] !== undefined) {
|
if (
|
||||||
|
(value === undefined || value === null) &&
|
||||||
|
baseObj &&
|
||||||
|
baseObj[key] !== undefined &&
|
||||||
|
baseObj[key] !== null
|
||||||
|
) {
|
||||||
result[key] = "";
|
result[key] = "";
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user