mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-13 07:21:14 +03:00
add ability to edit enabled and save_attempts for classification models in the UI
This commit is contained in:
parent
5f6043aa92
commit
d14932c0af
@ -376,20 +376,22 @@ class EmbeddingMaintainer(threading.Thread):
|
|||||||
logger.info(f"Disabled classification processor for model: {model_name}")
|
logger.info(f"Disabled classification processor for model: {model_name}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if processor already exists
|
|
||||||
for processor in self.realtime_processors:
|
for processor in self.realtime_processors:
|
||||||
if isinstance(
|
if (
|
||||||
processor,
|
isinstance(
|
||||||
(
|
processor,
|
||||||
CustomStateClassificationProcessor,
|
(
|
||||||
CustomObjectClassificationProcessor,
|
CustomStateClassificationProcessor,
|
||||||
),
|
CustomObjectClassificationProcessor,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
and processor.model_config.name == model_name
|
||||||
):
|
):
|
||||||
if processor.model_config.name == model_name:
|
processor.model_config = model_config
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Classification processor for model {model_name} already exists, skipping"
|
f"Updated config for classification processor: {model_name}"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if model_config.state_config is not None:
|
if model_config.state_config is not None:
|
||||||
processor = CustomStateClassificationProcessor(
|
processor = CustomStateClassificationProcessor(
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"documentTitle": "Classification Models - Frigate",
|
"documentTitle": "Classification Models - Frigate",
|
||||||
|
"disabled": "Disabled",
|
||||||
"details": {
|
"details": {
|
||||||
"scoreInfo": "Score represents the average classification confidence across all detections of this object.",
|
"scoreInfo": "Score represents the average classification confidence across all detections of this object.",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
@ -64,7 +65,14 @@
|
|||||||
"title": "Edit Classification Model",
|
"title": "Edit Classification Model",
|
||||||
"descriptionState": "Edit the classes for this state classification model. Changes will require retraining the model.",
|
"descriptionState": "Edit the classes for this state classification model. Changes will require retraining the model.",
|
||||||
"descriptionObject": "Edit the object type and classification type for this object classification model.",
|
"descriptionObject": "Edit the object type and classification type for this object classification model.",
|
||||||
"stateClassesInfo": "Note: Changing state classes requires retraining the model with the updated classes."
|
"enabled": "Enabled",
|
||||||
|
"enabledDesc": "Run this model. When disabled, it stops running and no longer classifies.",
|
||||||
|
"saveAttempts": "Save Attempts",
|
||||||
|
"saveAttemptsDesc": "Number of classification attempt images to keep for the recent classifications UI.",
|
||||||
|
"stateClassesInfo": "Model updated. Retrain the model for the class changes to take effect.",
|
||||||
|
"errors": {
|
||||||
|
"saveAttemptsInvalid": "Save attempts must be a whole number of 0 or greater"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"deleteDatasetImages": {
|
"deleteDatasetImages": {
|
||||||
"title": "Delete Dataset Images",
|
"title": "Delete Dataset Images",
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@ -17,6 +18,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@ -50,14 +52,23 @@ type ClassificationModelEditDialogProps = {
|
|||||||
type ObjectClassificationType = "sub_label" | "attribute";
|
type ObjectClassificationType = "sub_label" | "attribute";
|
||||||
|
|
||||||
type ObjectFormData = {
|
type ObjectFormData = {
|
||||||
|
enabled: boolean;
|
||||||
|
saveAttempts: number;
|
||||||
objectLabel: string;
|
objectLabel: string;
|
||||||
objectType: ObjectClassificationType;
|
objectType: ObjectClassificationType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateFormData = {
|
type StateFormData = {
|
||||||
|
enabled: boolean;
|
||||||
|
saveAttempts: number;
|
||||||
classes: string[];
|
classes: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_SAVE_ATTEMPTS = {
|
||||||
|
object: 200,
|
||||||
|
state: 100,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export default function ClassificationModelEditDialog({
|
export default function ClassificationModelEditDialog({
|
||||||
open,
|
open,
|
||||||
model,
|
model,
|
||||||
@ -71,6 +82,10 @@ export default function ClassificationModelEditDialog({
|
|||||||
const isStateModel = model.state_config !== undefined;
|
const isStateModel = model.state_config !== undefined;
|
||||||
const isObjectModel = model.object_config !== undefined;
|
const isObjectModel = model.object_config !== undefined;
|
||||||
|
|
||||||
|
const defaultSaveAttempts = isObjectModel
|
||||||
|
? DEFAULT_SAVE_ATTEMPTS.object
|
||||||
|
: DEFAULT_SAVE_ATTEMPTS.state;
|
||||||
|
|
||||||
const objectLabels = useMemo(() => {
|
const objectLabels = useMemo(() => {
|
||||||
if (!config) return [];
|
if (!config) return [];
|
||||||
|
|
||||||
@ -93,8 +108,17 @@ export default function ClassificationModelEditDialog({
|
|||||||
|
|
||||||
// Define form schema based on model type
|
// Define form schema based on model type
|
||||||
const formSchema = useMemo(() => {
|
const formSchema = useMemo(() => {
|
||||||
|
const sharedFields = {
|
||||||
|
enabled: z.boolean(),
|
||||||
|
saveAttempts: z.coerce
|
||||||
|
.number({ message: t("edit.errors.saveAttemptsInvalid") })
|
||||||
|
.int(t("edit.errors.saveAttemptsInvalid"))
|
||||||
|
.min(0, t("edit.errors.saveAttemptsInvalid")),
|
||||||
|
};
|
||||||
|
|
||||||
if (isObjectModel) {
|
if (isObjectModel) {
|
||||||
return z.object({
|
return z.object({
|
||||||
|
...sharedFields,
|
||||||
objectLabel: z
|
objectLabel: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, t("wizard.step1.errors.objectLabelRequired")),
|
.min(1, t("wizard.step1.errors.objectLabelRequired")),
|
||||||
@ -103,6 +127,7 @@ export default function ClassificationModelEditDialog({
|
|||||||
} else {
|
} else {
|
||||||
// State model
|
// State model
|
||||||
return z.object({
|
return z.object({
|
||||||
|
...sharedFields,
|
||||||
classes: z
|
classes: z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
.min(1, t("wizard.step1.errors.classRequired"))
|
.min(1, t("wizard.step1.errors.classRequired"))
|
||||||
@ -129,12 +154,16 @@ export default function ClassificationModelEditDialog({
|
|||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: isObjectModel
|
defaultValues: isObjectModel
|
||||||
? ({
|
? ({
|
||||||
|
enabled: model.enabled,
|
||||||
|
saveAttempts: model.save_attempts ?? defaultSaveAttempts,
|
||||||
objectLabel: model.object_config?.objects?.[0] || "",
|
objectLabel: model.object_config?.objects?.[0] || "",
|
||||||
objectType:
|
objectType:
|
||||||
(model.object_config
|
(model.object_config
|
||||||
?.classification_type as ObjectClassificationType) || "sub_label",
|
?.classification_type as ObjectClassificationType) || "sub_label",
|
||||||
} as ObjectFormData)
|
} as ObjectFormData)
|
||||||
: ({
|
: ({
|
||||||
|
enabled: model.enabled,
|
||||||
|
saveAttempts: model.save_attempts ?? defaultSaveAttempts,
|
||||||
classes: [""], // Will be populated from dataset
|
classes: [""], // Will be populated from dataset
|
||||||
} as StateFormData),
|
} as StateFormData),
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
@ -151,6 +180,8 @@ export default function ClassificationModelEditDialog({
|
|||||||
if (open) {
|
if (open) {
|
||||||
if (isObjectModel) {
|
if (isObjectModel) {
|
||||||
form.reset({
|
form.reset({
|
||||||
|
enabled: model.enabled,
|
||||||
|
saveAttempts: model.save_attempts ?? defaultSaveAttempts,
|
||||||
objectLabel: model.object_config?.objects?.[0] || "",
|
objectLabel: model.object_config?.objects?.[0] || "",
|
||||||
objectType:
|
objectType:
|
||||||
(model.object_config
|
(model.object_config
|
||||||
@ -158,6 +189,8 @@ export default function ClassificationModelEditDialog({
|
|||||||
} as ObjectFormData);
|
} as ObjectFormData);
|
||||||
} else {
|
} else {
|
||||||
form.reset({
|
form.reset({
|
||||||
|
enabled: model.enabled,
|
||||||
|
saveAttempts: model.save_attempts ?? defaultSaveAttempts,
|
||||||
classes: [""],
|
classes: [""],
|
||||||
} as StateFormData);
|
} as StateFormData);
|
||||||
}
|
}
|
||||||
@ -166,7 +199,15 @@ export default function ClassificationModelEditDialog({
|
|||||||
mutateDataset();
|
mutateDataset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [open, isObjectModel, isStateModel, model, form, mutateDataset]);
|
}, [
|
||||||
|
open,
|
||||||
|
isObjectModel,
|
||||||
|
isStateModel,
|
||||||
|
model,
|
||||||
|
form,
|
||||||
|
mutateDataset,
|
||||||
|
defaultSaveAttempts,
|
||||||
|
]);
|
||||||
|
|
||||||
// Update form with classes from dataset when loaded
|
// Update form with classes from dataset when loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -243,9 +284,10 @@ export default function ClassificationModelEditDialog({
|
|||||||
classification: {
|
classification: {
|
||||||
custom: {
|
custom: {
|
||||||
[model.name]: {
|
[model.name]: {
|
||||||
enabled: model.enabled,
|
enabled: objectData.enabled,
|
||||||
name: model.name,
|
name: model.name,
|
||||||
threshold: model.threshold,
|
threshold: model.threshold,
|
||||||
|
save_attempts: objectData.saveAttempts,
|
||||||
object_config: {
|
object_config: {
|
||||||
objects: [objectData.objectLabel],
|
objects: [objectData.objectLabel],
|
||||||
classification_type: objectData.objectType,
|
classification_type: objectData.objectType,
|
||||||
@ -261,6 +303,22 @@ export default function ClassificationModelEditDialog({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const stateData = data as StateFormData;
|
const stateData = data as StateFormData;
|
||||||
|
|
||||||
|
await axios.put("/config/set", {
|
||||||
|
requires_restart: 0,
|
||||||
|
update_topic: `config/classification/custom/${model.name}`,
|
||||||
|
config_data: {
|
||||||
|
classification: {
|
||||||
|
custom: {
|
||||||
|
[model.name]: {
|
||||||
|
enabled: stateData.enabled,
|
||||||
|
save_attempts: stateData.saveAttempts,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const newClasses = stateData.classes.filter(
|
const newClasses = stateData.classes.filter(
|
||||||
(c) => c.trim().length > 0,
|
(c) => c.trim().length > 0,
|
||||||
);
|
);
|
||||||
@ -307,11 +365,11 @@ export default function ClassificationModelEditDialog({
|
|||||||
if (renamePromises.length > 0) {
|
if (renamePromises.length > 0) {
|
||||||
await Promise.all(renamePromises);
|
await Promise.all(renamePromises);
|
||||||
await mutate(`classification/${model.name}/dataset`);
|
await mutate(`classification/${model.name}/dataset`);
|
||||||
toast.success(t("toast.success.updatedModel"), {
|
toast.success(t("edit.stateClassesInfo"), {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.info(t("edit.stateClassesInfo"), {
|
toast.success(t("toast.success.updatedModel"), {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -359,6 +417,29 @@ export default function ClassificationModelEditDialog({
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enabled"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between gap-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel className="text-primary-variant">
|
||||||
|
{t("edit.enabled")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription className="text-xs">
|
||||||
|
{t("edit.enabledDesc")}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{isObjectModel && (
|
{isObjectModel && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
@ -520,6 +601,25 @@ export default function ClassificationModelEditDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="saveAttempts"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-primary-variant">
|
||||||
|
{t("edit.saveAttempts")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input className="h-8" inputMode="numeric" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-xs">
|
||||||
|
{t("edit.saveAttemptsDesc")}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
|
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import ClassificationModelWizardDialog from "@/components/classification/Classif
|
|||||||
import ClassificationModelEditDialog from "@/components/classification/ClassificationModelEditDialog";
|
import ClassificationModelEditDialog from "@/components/classification/ClassificationModelEditDialog";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import { ImageShadowOverlay } from "@/components/overlay/ImageShadowOverlay";
|
import { ImageShadowOverlay } from "@/components/overlay/ImageShadowOverlay";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button, buttonVariants } from "@/components/ui/button";
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
import useOptimisticState from "@/hooks/use-optimistic-state";
|
import useOptimisticState from "@/hooks/use-optimistic-state";
|
||||||
@ -330,7 +331,10 @@ function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) {
|
|||||||
{coverImage ? (
|
{coverImage ? (
|
||||||
<>
|
<>
|
||||||
<img
|
<img
|
||||||
className="size-full"
|
className={cn(
|
||||||
|
"size-full",
|
||||||
|
!config.enabled && "opacity-50 grayscale",
|
||||||
|
)}
|
||||||
src={`${baseUrl}clips/${config.name}/dataset/${coverImage.name}/${coverImage.img}`}
|
src={`${baseUrl}clips/${config.name}/dataset/${coverImage.name}/${coverImage.img}`}
|
||||||
/>
|
/>
|
||||||
<ImageShadowOverlay lowerClassName="h-[30%] z-0" />
|
<ImageShadowOverlay lowerClassName="h-[30%] z-0" />
|
||||||
@ -338,6 +342,14 @@ function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) {
|
|||||||
) : (
|
) : (
|
||||||
<Skeleton className="flex size-full items-center justify-center" />
|
<Skeleton className="flex size-full items-center justify-center" />
|
||||||
)}
|
)}
|
||||||
|
{!config.enabled && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="absolute right-2 top-2 z-40 text-primary-variant"
|
||||||
|
>
|
||||||
|
{t("disabled")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
<div className="absolute bottom-2 left-3 text-lg text-white smart-capitalize">
|
<div className="absolute bottom-2 left-3 text-lg text-white smart-capitalize">
|
||||||
{config.name}
|
{config.name}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user