mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-20 12:06:43 +03:00
Compare commits
No commits in common. "c84bfd3ace1026d06c5c438b903ace1adb395347" and "0314bdf84cee1abb06d4d5c1cf83f5922c6c599c" have entirely different histories.
c84bfd3ace
...
0314bdf84c
@ -595,13 +595,9 @@ def get_classification_dataset(name: str):
|
||||
"last_training_image_count": 0,
|
||||
"current_image_count": current_image_count,
|
||||
"new_images_count": current_image_count,
|
||||
"dataset_changed": current_image_count > 0,
|
||||
}
|
||||
else:
|
||||
last_training_count = metadata.get("last_training_image_count", 0)
|
||||
# Dataset has changed if count is different (either added or deleted images)
|
||||
dataset_changed = current_image_count != last_training_count
|
||||
# Only show positive count for new images (ignore deletions in the count display)
|
||||
new_images_count = max(0, current_image_count - last_training_count)
|
||||
training_metadata = {
|
||||
"has_trained": True,
|
||||
@ -609,7 +605,6 @@ def get_classification_dataset(name: str):
|
||||
"last_training_image_count": last_training_count,
|
||||
"current_image_count": current_image_count,
|
||||
"new_images_count": new_images_count,
|
||||
"dataset_changed": dataset_changed,
|
||||
}
|
||||
|
||||
return JSONResponse(
|
||||
@ -953,29 +948,31 @@ async def generate_object_examples(request: Request, body: GenerateObjectExample
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Delete a classification model",
|
||||
description="""Deletes a specific classification model and all its associated data.
|
||||
Works even if the model is not in the config (e.g., partially created during wizard).
|
||||
Returns a success message.""",
|
||||
The name must exist in the classification models. Returns a success message or an error if the name is invalid.""",
|
||||
)
|
||||
def delete_classification_model(request: Request, name: str):
|
||||
sanitized_name = sanitize_filename(name)
|
||||
config: FrigateConfig = request.app.frigate_config
|
||||
|
||||
if name not in config.classification.custom:
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": f"{name} is not a known classification model.",
|
||||
}
|
||||
),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
# Delete the classification model's data directory in clips
|
||||
data_dir = os.path.join(CLIPS_DIR, sanitized_name)
|
||||
data_dir = os.path.join(CLIPS_DIR, sanitize_filename(name))
|
||||
if os.path.exists(data_dir):
|
||||
try:
|
||||
shutil.rmtree(data_dir)
|
||||
logger.info(f"Deleted classification data directory for {name}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to delete data directory for {name}: {e}")
|
||||
|
||||
# Delete the classification model's files in model_cache
|
||||
model_dir = os.path.join(MODEL_CACHE_DIR, sanitized_name)
|
||||
model_dir = os.path.join(MODEL_CACHE_DIR, sanitize_filename(name))
|
||||
if os.path.exists(model_dir):
|
||||
try:
|
||||
shutil.rmtree(model_dir)
|
||||
logger.info(f"Deleted classification model directory for {name}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to delete model directory for {name}: {e}")
|
||||
|
||||
return JSONResponse(
|
||||
content=(
|
||||
|
||||
@ -4,6 +4,7 @@ import logging
|
||||
import os
|
||||
|
||||
import sherpa_onnx
|
||||
from faster_whisper.utils import download_model
|
||||
|
||||
from frigate.comms.inter_process import InterProcessRequestor
|
||||
from frigate.const import MODEL_CACHE_DIR
|
||||
@ -24,9 +25,6 @@ class AudioTranscriptionModelRunner:
|
||||
|
||||
if model_size == "large":
|
||||
# use the Whisper download function instead of our own
|
||||
# Import dynamically to avoid crashes on systems without AVX support
|
||||
from faster_whisper.utils import download_model
|
||||
|
||||
logger.debug("Downloading Whisper audio transcription model")
|
||||
download_model(
|
||||
size_or_id="small" if device == "cuda" else "tiny",
|
||||
|
||||
@ -6,6 +6,7 @@ import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from faster_whisper import WhisperModel
|
||||
from peewee import DoesNotExist
|
||||
|
||||
from frigate.comms.inter_process import InterProcessRequestor
|
||||
@ -50,9 +51,6 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
|
||||
|
||||
def __build_recognizer(self) -> None:
|
||||
try:
|
||||
# Import dynamically to avoid crashes on systems without AVX support
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
self.recognizer = WhisperModel(
|
||||
model_size_or_path="small",
|
||||
device="cuda"
|
||||
|
||||
@ -394,11 +394,7 @@ class OpenVINOModelRunner(BaseModelRunner):
|
||||
self.infer_request.set_input_tensor(input_index, input_tensor)
|
||||
|
||||
# Run inference
|
||||
try:
|
||||
self.infer_request.infer()
|
||||
except Exception as e:
|
||||
logger.error(f"Error during OpenVINO inference: {e}")
|
||||
return []
|
||||
|
||||
# Get all output tensors
|
||||
outputs = []
|
||||
|
||||
@ -16,7 +16,6 @@
|
||||
"tooltip": {
|
||||
"trainingInProgress": "Model is currently training",
|
||||
"noNewImages": "No new images to train. Classify more images in the dataset first.",
|
||||
"noChanges": "No changes to the dataset since last training.",
|
||||
"modelNotReady": "Model is not ready for training"
|
||||
},
|
||||
"toast": {
|
||||
@ -44,9 +43,7 @@
|
||||
},
|
||||
"deleteCategory": {
|
||||
"title": "Delete Class",
|
||||
"desc": "Are you sure you want to delete the class {{name}}? This will permanently delete all associated images and require re-training the model.",
|
||||
"minClassesTitle": "Cannot Delete Class",
|
||||
"minClassesDesc": "A classification model must have at least 2 classes. Add another class before deleting this one."
|
||||
"desc": "Are you sure you want to delete the class {{name}}? This will permanently delete all associated images and require re-training the model."
|
||||
},
|
||||
"deleteModel": {
|
||||
"title": "Delete Classification Model",
|
||||
|
||||
@ -15,7 +15,6 @@ import Step3ChooseExamples, {
|
||||
} from "./wizard/Step3ChooseExamples";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import axios from "axios";
|
||||
|
||||
const OBJECT_STEPS = [
|
||||
"wizard.steps.nameAndDefine",
|
||||
@ -121,18 +120,7 @@ export default function ClassificationModelWizardDialog({
|
||||
dispatch({ type: "PREVIOUS_STEP" });
|
||||
};
|
||||
|
||||
const handleCancel = async () => {
|
||||
// Clean up any generated training images if we're cancelling from Step 3
|
||||
if (wizardState.step1Data && wizardState.step3Data?.examplesGenerated) {
|
||||
try {
|
||||
await axios.delete(
|
||||
`/classification/${wizardState.step1Data.modelName}`,
|
||||
);
|
||||
} catch (error) {
|
||||
// Silently fail - user is already cancelling
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
dispatch({ type: "RESET" });
|
||||
onClose();
|
||||
};
|
||||
|
||||
@ -165,15 +165,18 @@ export default function Step3ChooseExamples({
|
||||
const isLastClass = currentClassIndex === allClasses.length - 1;
|
||||
|
||||
if (isLastClass) {
|
||||
// For object models, assign remaining unclassified images to "none"
|
||||
// For state models, this should never happen since we require all images to be classified
|
||||
if (step1Data.modelType !== "state") {
|
||||
// Assign remaining unclassified images
|
||||
unknownImages.slice(0, 24).forEach((imageName) => {
|
||||
if (!newClassifications[imageName]) {
|
||||
// For state models with 2 classes, assign to the last class
|
||||
// For object models, assign to "none"
|
||||
if (step1Data.modelType === "state" && allClasses.length === 2) {
|
||||
newClassifications[imageName] = allClasses[allClasses.length - 1];
|
||||
} else {
|
||||
newClassifications[imageName] = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// All done, trigger training immediately
|
||||
setImageClassifications(newClassifications);
|
||||
@ -313,15 +316,8 @@ export default function Step3ChooseExamples({
|
||||
return images;
|
||||
}
|
||||
|
||||
// If we're viewing a previous class (going back), show images for that class
|
||||
// Otherwise show only unclassified images
|
||||
const currentClassInView = allClasses[currentClassIndex];
|
||||
return images.filter((img) => {
|
||||
const imgClass = imageClassifications[img];
|
||||
// Show if: unclassified OR classified with current class we're viewing
|
||||
return !imgClass || imgClass === currentClassInView;
|
||||
});
|
||||
}, [unknownImages, imageClassifications, allClasses, currentClassIndex]);
|
||||
return images.filter((img) => !imageClassifications[img]);
|
||||
}, [unknownImages, imageClassifications]);
|
||||
|
||||
const allImagesClassified = useMemo(() => {
|
||||
return unclassifiedImages.length === 0;
|
||||
@ -330,26 +326,15 @@ export default function Step3ChooseExamples({
|
||||
// For state models on the last class, require all images to be classified
|
||||
const isLastClass = currentClassIndex === allClasses.length - 1;
|
||||
const canProceed = useMemo(() => {
|
||||
if (step1Data.modelType === "state" && isLastClass) {
|
||||
// Check if all 24 images will be classified after current selections are applied
|
||||
const totalImages = unknownImages.slice(0, 24).length;
|
||||
|
||||
// Count images that will be classified (either already classified or currently selected)
|
||||
const allImages = unknownImages.slice(0, 24);
|
||||
const willBeClassified = allImages.filter((img) => {
|
||||
return imageClassifications[img] || selectedImages.has(img);
|
||||
}).length;
|
||||
|
||||
return willBeClassified >= totalImages;
|
||||
if (
|
||||
step1Data.modelType === "state" &&
|
||||
isLastClass &&
|
||||
!allImagesClassified
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [
|
||||
step1Data.modelType,
|
||||
isLastClass,
|
||||
unknownImages,
|
||||
imageClassifications,
|
||||
selectedImages,
|
||||
]);
|
||||
}, [step1Data.modelType, isLastClass, allImagesClassified]);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
if (currentClassIndex > 0) {
|
||||
|
||||
@ -12,13 +12,13 @@ export function ImageShadowOverlay({
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-x-0 top-0 z-10 h-[30%] w-full rounded-lg bg-gradient-to-b from-black/20 to-transparent",
|
||||
"pointer-events-none absolute inset-x-0 top-0 z-10 h-[30%] w-full rounded-lg bg-gradient-to-b from-black/20 to-transparent md:rounded-2xl",
|
||||
upperClassName,
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-x-0 bottom-0 z-10 h-[10%] w-full rounded-lg bg-gradient-to-t from-black/20 to-transparent",
|
||||
"pointer-events-none absolute inset-x-0 bottom-0 z-10 h-[10%] w-full rounded-lg bg-gradient-to-t from-black/20 to-transparent md:rounded-2xl",
|
||||
lowerClassName,
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -77,10 +77,7 @@ export default function BirdseyeLivePlayer({
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<ImageShadowOverlay
|
||||
upperClassName="md:rounded-2xl"
|
||||
lowerClassName="md:rounded-2xl"
|
||||
/>
|
||||
<ImageShadowOverlay />
|
||||
<div className="size-full" ref={playerRef}>
|
||||
{player}
|
||||
</div>
|
||||
|
||||
@ -331,10 +331,7 @@ export default function LivePlayer({
|
||||
>
|
||||
{cameraEnabled &&
|
||||
((showStillWithoutActivity && !liveReady) || liveReady) && (
|
||||
<ImageShadowOverlay
|
||||
upperClassName="md:rounded-2xl"
|
||||
lowerClassName="md:rounded-2xl"
|
||||
/>
|
||||
<ImageShadowOverlay />
|
||||
)}
|
||||
{player}
|
||||
{cameraEnabled &&
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import useSWR from "swr";
|
||||
|
||||
@ -42,23 +36,6 @@ export function DetailStreamProvider({
|
||||
() => initialSelectedObjectIds ?? [],
|
||||
);
|
||||
|
||||
// When the parent provides a new initialSelectedObjectIds (for example
|
||||
// when navigating between search results) update the selection so children
|
||||
// like `ObjectTrackOverlay` receive the new ids immediately. We only
|
||||
// perform this update when the incoming value actually changes.
|
||||
useEffect(() => {
|
||||
if (
|
||||
initialSelectedObjectIds &&
|
||||
(initialSelectedObjectIds.length !== selectedObjectIds.length ||
|
||||
initialSelectedObjectIds.some((v, i) => selectedObjectIds[i] !== v))
|
||||
) {
|
||||
setSelectedObjectIds(initialSelectedObjectIds);
|
||||
}
|
||||
// Intentionally include selectedObjectIds to compare previous value and
|
||||
// avoid overwriting user interactions unless the incoming prop changed.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialSelectedObjectIds]);
|
||||
|
||||
const toggleObjectSelection = (id: string | undefined) => {
|
||||
if (id === undefined) {
|
||||
setSelectedObjectIds([]);
|
||||
@ -86,33 +63,10 @@ export function DetailStreamProvider({
|
||||
setAnnotationOffset(cfgOffset);
|
||||
}, [config, camera]);
|
||||
|
||||
// Clear selected objects when exiting detail mode or when the camera
|
||||
// changes for providers that are not initialized with an explicit
|
||||
// `initialSelectedObjectIds` (e.g., the RecordingView). For providers
|
||||
// that receive `initialSelectedObjectIds` (like SearchDetailDialog) we
|
||||
// avoid clearing on camera change to prevent a race with children that
|
||||
// immediately set selection when mounting.
|
||||
const prevCameraRef = useRef<string | undefined>(undefined);
|
||||
// Clear selected objects when exiting detail mode or changing cameras
|
||||
useEffect(() => {
|
||||
// Always clear when leaving detail mode
|
||||
if (!isDetailMode) {
|
||||
setSelectedObjectIds([]);
|
||||
prevCameraRef.current = camera;
|
||||
return;
|
||||
}
|
||||
|
||||
// If camera changed and the parent did not provide initialSelectedObjectIds,
|
||||
// clear selection to preserve previous behavior.
|
||||
if (
|
||||
prevCameraRef.current !== undefined &&
|
||||
prevCameraRef.current !== camera &&
|
||||
initialSelectedObjectIds === undefined
|
||||
) {
|
||||
setSelectedObjectIds([]);
|
||||
}
|
||||
|
||||
prevCameraRef.current = camera;
|
||||
}, [isDetailMode, camera, initialSelectedObjectIds]);
|
||||
}, [isDetailMode, camera]);
|
||||
|
||||
const value: DetailStreamContextType = {
|
||||
selectedObjectIds,
|
||||
|
||||
@ -126,7 +126,6 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
||||
last_training_image_count: number;
|
||||
current_image_count: number;
|
||||
new_images_count: number;
|
||||
dataset_changed: boolean;
|
||||
} | null;
|
||||
}>(`classification/${model.name}/dataset`);
|
||||
|
||||
@ -265,11 +264,10 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// Always refresh dataset to update the categories list
|
||||
refreshDataset();
|
||||
|
||||
if (pageToggle == "train") {
|
||||
refreshTrain();
|
||||
} else {
|
||||
refreshDataset();
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -447,7 +445,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
||||
variant={modelState == "failed" ? "destructive" : "select"}
|
||||
disabled={
|
||||
(modelState != "complete" && modelState != "failed") ||
|
||||
!trainingMetadata?.dataset_changed
|
||||
(trainingMetadata?.new_images_count ?? 0) === 0
|
||||
}
|
||||
>
|
||||
{modelState == "training" ? (
|
||||
@ -468,14 +466,14 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{(!trainingMetadata?.dataset_changed ||
|
||||
{((trainingMetadata?.new_images_count ?? 0) === 0 ||
|
||||
(modelState != "complete" && modelState != "failed")) && (
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
{modelState == "training"
|
||||
? t("tooltip.trainingInProgress")
|
||||
: !trainingMetadata?.dataset_changed
|
||||
? t("tooltip.noChanges")
|
||||
: trainingMetadata?.new_images_count === 0
|
||||
? t("tooltip.noNewImages")
|
||||
: t("tooltip.modelNotReady")}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
@ -573,28 +571,13 @@ function LibrarySelector({
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{Object.keys(dataset).length <= 2
|
||||
? t("deleteCategory.minClassesTitle")
|
||||
: t("deleteCategory.title")}
|
||||
</DialogTitle>
|
||||
<DialogTitle>{t("deleteCategory.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{Object.keys(dataset).length <= 2
|
||||
? t("deleteCategory.minClassesDesc")
|
||||
: t("deleteCategory.desc", { name: confirmDelete })}
|
||||
{t("deleteCategory.desc", { name: confirmDelete })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end gap-2">
|
||||
{Object.keys(dataset).length <= 2 ? (
|
||||
<Button variant="outline" onClick={() => setConfirmDelete(null)}>
|
||||
{t("button.ok", { ns: "common" })}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setConfirmDelete(null)}
|
||||
>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
@ -609,8 +592,6 @@ function LibrarySelector({
|
||||
>
|
||||
{t("button.delete", { ns: "common" })}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user