Miscellaneous Fixes (#20866)

* Don't warn when event ids have expired for trigger sync

* Import faster_whisper conditinally to avoid illegal instruction

* Catch OpenVINO runtime error

* fix race condition in detail stream context

navigating between tracked objects in Explore would sometimes prevent the object track from appearing

* Handle case where classification images are deleted

* Adjust default rounded corners on larger screens

* Improve flow handling for classification state

* Remove images when wizard is cancelled

* Improve deletion handling for classes

* Set constraints on review buffers

* Update to support correct data format

* Set minimum duration for recording based review items

* Use friendly name in review genai prompt

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
Josh Hawkins
2025-11-10 10:03:56 -07:00
committed by GitHub
co-authored by Nicolas Mowen
parent 99a363c047
commit c371fc0c87
20 changed files with 287 additions and 113 deletions
@@ -11,6 +11,7 @@ import {
CustomClassificationModelConfig,
FrigateConfig,
} from "@/types/frigateConfig";
import { ClassificationDatasetResponse } from "@/types/classification";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { FaFolderPlus } from "react-icons/fa";
@@ -209,9 +210,10 @@ type ModelCardProps = {
function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) {
const { t } = useTranslation(["views/classificationModel"]);
const { data: dataset } = useSWR<{
[id: string]: string[];
}>(`classification/${config.name}/dataset`, { revalidateOnFocus: false });
const { data: dataset } = useSWR<ClassificationDatasetResponse>(
`classification/${config.name}/dataset`,
{ revalidateOnFocus: false },
);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
@@ -260,20 +262,25 @@ function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) {
}, []);
const coverImage = useMemo(() => {
if (!dataset) {
if (!dataset || !dataset.categories) {
return undefined;
}
const keys = Object.keys(dataset).filter((key) => key != "none");
const selectedKey = keys[0];
const keys = Object.keys(dataset.categories).filter((key) => key != "none");
if (keys.length === 0) {
return undefined;
}
if (!dataset[selectedKey]) {
const selectedKey = keys[0];
const images = dataset.categories[selectedKey];
if (!images || images.length === 0) {
return undefined;
}
return {
name: selectedKey,
img: dataset[selectedKey][0],
img: images[0],
};
}, [dataset]);
@@ -317,11 +324,19 @@ function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) {
)}
onClick={onClick}
>
<img
className="size-full"
src={`${baseUrl}clips/${config.name}/dataset/${coverImage?.name}/${coverImage?.img}`}
/>
<ImageShadowOverlay lowerClassName="h-[30%] z-0" />
{coverImage ? (
<>
<img
className="size-full"
src={`${baseUrl}clips/${config.name}/dataset/${coverImage.name}/${coverImage.img}`}
/>
<ImageShadowOverlay lowerClassName="h-[30%] z-0" />
</>
) : (
<div className="flex size-full items-center justify-center bg-background_alt">
<MdModelTraining className="size-16 text-muted-foreground" />
</div>
)}
<div className="absolute bottom-2 left-3 text-lg text-white smart-capitalize">
{config.name}
</div>
@@ -59,7 +59,11 @@ import { useNavigate } from "react-router-dom";
import { IoMdArrowRoundBack } from "react-icons/io";
import TrainFilterDialog from "@/components/overlay/dialog/TrainFilterDialog";
import useApiFilter from "@/hooks/use-api-filter";
import { ClassificationItemData, TrainFilter } from "@/types/classification";
import {
ClassificationDatasetResponse,
ClassificationItemData,
TrainFilter,
} from "@/types/classification";
import {
ClassificationCard,
GroupedClassificationCard,
@@ -118,16 +122,10 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
const { data: trainImages, mutate: refreshTrain } = useSWR<string[]>(
`classification/${model.name}/train`,
);
const { data: datasetResponse, mutate: refreshDataset } = useSWR<{
categories: { [id: string]: string[] };
training_metadata: {
has_trained: boolean;
last_training_date: string | null;
last_training_image_count: number;
current_image_count: number;
new_images_count: number;
} | null;
}>(`classification/${model.name}/dataset`);
const { data: datasetResponse, mutate: refreshDataset } =
useSWR<ClassificationDatasetResponse>(
`classification/${model.name}/dataset`,
);
const dataset = datasetResponse?.categories || {};
const trainingMetadata = datasetResponse?.training_metadata;
@@ -264,10 +262,11 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
);
}
// Always refresh dataset to update the categories list
refreshDataset();
if (pageToggle == "train") {
refreshTrain();
} else {
refreshDataset();
}
}
})
@@ -445,7 +444,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
variant={modelState == "failed" ? "destructive" : "select"}
disabled={
(modelState != "complete" && modelState != "failed") ||
(trainingMetadata?.new_images_count ?? 0) === 0
!trainingMetadata?.dataset_changed
}
>
{modelState == "training" ? (
@@ -466,14 +465,14 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
)}
</Button>
</TooltipTrigger>
{((trainingMetadata?.new_images_count ?? 0) === 0 ||
{(!trainingMetadata?.dataset_changed ||
(modelState != "complete" && modelState != "failed")) && (
<TooltipPortal>
<TooltipContent>
{modelState == "training"
? t("tooltip.trainingInProgress")
: trainingMetadata?.new_images_count === 0
? t("tooltip.noNewImages")
: !trainingMetadata?.dataset_changed
? t("tooltip.noChanges")
: t("tooltip.modelNotReady")}
</TooltipContent>
</TooltipPortal>
@@ -571,27 +570,44 @@ function LibrarySelector({
>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("deleteCategory.title")}</DialogTitle>
<DialogTitle>
{Object.keys(dataset).length <= 2
? t("deleteCategory.minClassesTitle")
: t("deleteCategory.title")}
</DialogTitle>
<DialogDescription>
{t("deleteCategory.desc", { name: confirmDelete })}
{Object.keys(dataset).length <= 2
? t("deleteCategory.minClassesDesc")
: t("deleteCategory.desc", { name: confirmDelete })}
</DialogDescription>
</DialogHeader>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setConfirmDelete(null)}>
{t("button.cancel", { ns: "common" })}
</Button>
<Button
variant="destructive"
className="text-white"
onClick={() => {
if (confirmDelete) {
handleDeleteCategory(confirmDelete);
setConfirmDelete(null);
}
}}
>
{t("button.delete", { ns: "common" })}
</Button>
{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
variant="destructive"
className="text-white"
onClick={() => {
if (confirmDelete) {
handleDeleteCategory(confirmDelete);
setConfirmDelete(null);
}
}}
>
{t("button.delete", { ns: "common" })}
</Button>
</>
)}
</div>
</DialogContent>
</Dialog>