mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 21:44:13 +03:00
Classification fixes (#20771)
* Fully delete a model * Fix deletion dialog * Fix classification back step * Adjust selection gradient * Fix * Fix
This commit is contained in:
parent
d44340eca6
commit
4f76b34f44
@ -31,13 +31,15 @@ from frigate.api.defs.response.generic_response import GenericResponse
|
|||||||
from frigate.api.defs.tags import Tags
|
from frigate.api.defs.tags import Tags
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.config.camera import DetectConfig
|
from frigate.config.camera import DetectConfig
|
||||||
from frigate.const import CLIPS_DIR, FACE_DIR
|
from frigate.const import CLIPS_DIR, FACE_DIR, MODEL_CACHE_DIR
|
||||||
from frigate.embeddings import EmbeddingsContext
|
from frigate.embeddings import EmbeddingsContext
|
||||||
from frigate.models import Event
|
from frigate.models import Event
|
||||||
|
from frigate.util.builtin import update_yaml_file_bulk
|
||||||
from frigate.util.classification import (
|
from frigate.util.classification import (
|
||||||
collect_object_classification_examples,
|
collect_object_classification_examples,
|
||||||
collect_state_classification_examples,
|
collect_state_classification_examples,
|
||||||
)
|
)
|
||||||
|
from frigate.util.config import find_config_file
|
||||||
from frigate.util.path import get_event_snapshot
|
from frigate.util.path import get_event_snapshot
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -828,12 +830,34 @@ def delete_classification_model(request: Request, name: str):
|
|||||||
status_code=404,
|
status_code=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete the classification model's data directory
|
# Delete the classification model's data directory in clips
|
||||||
model_dir = os.path.join(CLIPS_DIR, sanitize_filename(name))
|
data_dir = os.path.join(CLIPS_DIR, sanitize_filename(name))
|
||||||
|
if os.path.exists(data_dir):
|
||||||
|
shutil.rmtree(data_dir)
|
||||||
|
|
||||||
|
# Delete the classification model's files in model_cache
|
||||||
|
model_dir = os.path.join(MODEL_CACHE_DIR, sanitize_filename(name))
|
||||||
if os.path.exists(model_dir):
|
if os.path.exists(model_dir):
|
||||||
shutil.rmtree(model_dir)
|
shutil.rmtree(model_dir)
|
||||||
|
|
||||||
|
# Remove the model from the config file
|
||||||
|
config_file = find_config_file()
|
||||||
|
try:
|
||||||
|
# Setting value to empty string deletes the key
|
||||||
|
updates = {f"classification.custom.{name}": None}
|
||||||
|
update_yaml_file_bulk(config_file, updates)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating config file: {e}")
|
||||||
|
return JSONResponse(
|
||||||
|
content=(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": "Failed to update config file.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
status_code=500,
|
||||||
|
)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content=(
|
content=(
|
||||||
{
|
{
|
||||||
|
|||||||
@ -317,6 +317,21 @@ export default function Step3ChooseExamples({
|
|||||||
return unclassifiedImages.length === 0;
|
return unclassifiedImages.length === 0;
|
||||||
}, [unclassifiedImages]);
|
}, [unclassifiedImages]);
|
||||||
|
|
||||||
|
const handleBack = useCallback(() => {
|
||||||
|
if (currentClassIndex > 0) {
|
||||||
|
const previousClass = allClasses[currentClassIndex - 1];
|
||||||
|
setCurrentClassIndex((prev) => prev - 1);
|
||||||
|
|
||||||
|
// Restore selections for the previous class
|
||||||
|
const previousSelections = Object.entries(imageClassifications)
|
||||||
|
.filter(([_, className]) => className === previousClass)
|
||||||
|
.map(([imageName, _]) => imageName);
|
||||||
|
setSelectedImages(new Set(previousSelections));
|
||||||
|
} else {
|
||||||
|
onBack();
|
||||||
|
}
|
||||||
|
}, [currentClassIndex, allClasses, imageClassifications, onBack]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{isTraining ? (
|
{isTraining ? (
|
||||||
@ -420,7 +435,7 @@ export default function Step3ChooseExamples({
|
|||||||
|
|
||||||
{!isTraining && (
|
{!isTraining && (
|
||||||
<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 type="button" onClick={onBack} className="sm:flex-1">
|
<Button type="button" onClick={handleBack} className="sm:flex-1">
|
||||||
{t("button.back", { ns: "common" })}
|
{t("button.back", { ns: "common" })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
CustomClassificationModelConfig,
|
CustomClassificationModelConfig,
|
||||||
FrigateConfig,
|
FrigateConfig,
|
||||||
} from "@/types/frigateConfig";
|
} from "@/types/frigateConfig";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FaFolderPlus } from "react-icons/fa";
|
import { FaFolderPlus } from "react-icons/fa";
|
||||||
import { MdModelTraining } from "react-icons/md";
|
import { MdModelTraining } from "react-icons/md";
|
||||||
@ -21,7 +21,6 @@ import Heading from "@/components/ui/heading";
|
|||||||
import { useOverlayState } from "@/hooks/use-overlay-state";
|
import { useOverlayState } from "@/hooks/use-overlay-state";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -212,12 +211,6 @@ function ModelCard({ config, onClick, onDelete }: ModelCardProps) {
|
|||||||
}>(`classification/${config.name}/dataset`, { revalidateOnFocus: false });
|
}>(`classification/${config.name}/dataset`, { revalidateOnFocus: false });
|
||||||
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const bypassDialogRef = useRef(false);
|
|
||||||
|
|
||||||
useKeyboardListener(["Shift"], (_, modifiers) => {
|
|
||||||
bypassDialogRef.current = modifiers.shift;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleDelete = useCallback(async () => {
|
const handleDelete = useCallback(async () => {
|
||||||
await axios
|
await axios
|
||||||
@ -241,13 +234,10 @@ function ModelCard({ config, onClick, onDelete }: ModelCardProps) {
|
|||||||
});
|
});
|
||||||
}, [config, onDelete, t]);
|
}, [config, onDelete, t]);
|
||||||
|
|
||||||
const handleDeleteClick = useCallback(() => {
|
const handleDeleteClick = useCallback((e: React.MouseEvent) => {
|
||||||
if (bypassDialogRef.current) {
|
e.stopPropagation();
|
||||||
handleDelete();
|
|
||||||
} else {
|
|
||||||
setDeleteDialogOpen(true);
|
setDeleteDialogOpen(true);
|
||||||
}
|
}, []);
|
||||||
}, [handleDelete]);
|
|
||||||
|
|
||||||
const coverImage = useMemo(() => {
|
const coverImage = useMemo(() => {
|
||||||
if (!dataset) {
|
if (!dataset) {
|
||||||
@ -304,7 +294,7 @@ function ModelCard({ config, onClick, onDelete }: ModelCardProps) {
|
|||||||
className="size-full"
|
className="size-full"
|
||||||
src={`${baseUrl}clips/${config.name}/dataset/${coverImage?.name}/${coverImage?.img}`}
|
src={`${baseUrl}clips/${config.name}/dataset/${coverImage?.name}/${coverImage?.img}`}
|
||||||
/>
|
/>
|
||||||
<ImageShadowOverlay />
|
<ImageShadowOverlay lowerClassName="h-[30%] z-0" />
|
||||||
<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>
|
||||||
@ -315,14 +305,13 @@ function ModelCard({ config, onClick, onDelete }: ModelCardProps) {
|
|||||||
<FiMoreVertical className="size-5 text-white" />
|
<FiMoreVertical className="size-5 text-white" />
|
||||||
</BlurredIconButton>
|
</BlurredIconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<DropdownMenuItem onClick={handleDeleteClick}>
|
<DropdownMenuItem onClick={handleDeleteClick}>
|
||||||
<LuTrash2 className="mr-2 size-4" />
|
<LuTrash2 className="mr-2 size-4" />
|
||||||
<span>
|
<span>{t("button.delete", { ns: "common" })}</span>
|
||||||
{bypassDialogRef.current
|
|
||||||
? t("button.deleteNow", { ns: "common" })
|
|
||||||
: t("button.delete", { ns: "common" })}
|
|
||||||
</span>
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user