Update to support correct data format

This commit is contained in:
Nicolas Mowen 2025-11-10 09:23:35 -07:00
parent dc19dd5495
commit e5a2e9637b
4 changed files with 62 additions and 32 deletions

View File

@ -28,6 +28,7 @@ import {
CustomClassificationModelConfig, CustomClassificationModelConfig,
FrigateConfig, FrigateConfig,
} from "@/types/frigateConfig"; } from "@/types/frigateConfig";
import { ClassificationDatasetResponse } from "@/types/classification";
import { getTranslatedLabel } from "@/utils/i18n"; import { getTranslatedLabel } from "@/utils/i18n";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios"; import axios from "axios";
@ -140,16 +141,19 @@ export default function ClassificationModelEditDialog({
}); });
// Fetch dataset to get current classes for state models // Fetch dataset to get current classes for state models
const { data: dataset } = useSWR<{ const { data: dataset } = useSWR<ClassificationDatasetResponse>(
[id: string]: string[]; isStateModel ? `classification/${model.name}/dataset` : null,
}>(isStateModel ? `classification/${model.name}/dataset` : null, { {
revalidateOnFocus: false, revalidateOnFocus: false,
}); },
);
// Update form with classes from dataset when loaded // Update form with classes from dataset when loaded
useEffect(() => { useEffect(() => {
if (isStateModel && dataset) { if (isStateModel && dataset?.categories) {
const classes = Object.keys(dataset).filter((key) => key !== "none"); const classes = Object.keys(dataset.categories).filter(
(key) => key !== "none",
);
if (classes.length > 0) { if (classes.length > 0) {
(form as ReturnType<typeof useForm<StateFormData>>).setValue( (form as ReturnType<typeof useForm<StateFormData>>).setValue(
"classes", "classes",

View File

@ -20,3 +20,17 @@ export type ClassificationThreshold = {
recognition: number; recognition: number;
unknown: number; unknown: number;
}; };
export type ClassificationDatasetResponse = {
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;
dataset_changed: boolean;
} | null;
};

View File

@ -11,6 +11,7 @@ import {
CustomClassificationModelConfig, CustomClassificationModelConfig,
FrigateConfig, FrigateConfig,
} from "@/types/frigateConfig"; } from "@/types/frigateConfig";
import { ClassificationDatasetResponse } from "@/types/classification";
import { useCallback, useEffect, useMemo, 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";
@ -209,9 +210,10 @@ type ModelCardProps = {
function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) { function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) {
const { t } = useTranslation(["views/classificationModel"]); const { t } = useTranslation(["views/classificationModel"]);
const { data: dataset } = useSWR<{ const { data: dataset } = useSWR<ClassificationDatasetResponse>(
[id: string]: string[]; `classification/${config.name}/dataset`,
}>(`classification/${config.name}/dataset`, { revalidateOnFocus: false }); { revalidateOnFocus: false },
);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false);
@ -260,20 +262,25 @@ function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) {
}, []); }, []);
const coverImage = useMemo(() => { const coverImage = useMemo(() => {
if (!dataset) { if (!dataset || !dataset.categories) {
return undefined; return undefined;
} }
const keys = Object.keys(dataset).filter((key) => key != "none"); const keys = Object.keys(dataset.categories).filter((key) => key != "none");
const selectedKey = keys[0]; 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 undefined;
} }
return { return {
name: selectedKey, name: selectedKey,
img: dataset[selectedKey][0], img: images[0],
}; };
}, [dataset]); }, [dataset]);
@ -317,11 +324,19 @@ function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) {
)} )}
onClick={onClick} onClick={onClick}
> >
<img {coverImage ? (
className="size-full" <>
src={`${baseUrl}clips/${config.name}/dataset/${coverImage?.name}/${coverImage?.img}`} <img
/> className="size-full"
<ImageShadowOverlay lowerClassName="h-[30%] z-0" /> 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"> <div className="absolute bottom-2 left-3 text-lg text-white smart-capitalize">
{config.name} {config.name}
</div> </div>

View File

@ -59,7 +59,11 @@ import { useNavigate } from "react-router-dom";
import { IoMdArrowRoundBack } from "react-icons/io"; import { IoMdArrowRoundBack } from "react-icons/io";
import TrainFilterDialog from "@/components/overlay/dialog/TrainFilterDialog"; import TrainFilterDialog from "@/components/overlay/dialog/TrainFilterDialog";
import useApiFilter from "@/hooks/use-api-filter"; import useApiFilter from "@/hooks/use-api-filter";
import { ClassificationItemData, TrainFilter } from "@/types/classification"; import {
ClassificationDatasetResponse,
ClassificationItemData,
TrainFilter,
} from "@/types/classification";
import { import {
ClassificationCard, ClassificationCard,
GroupedClassificationCard, GroupedClassificationCard,
@ -118,17 +122,10 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
const { data: trainImages, mutate: refreshTrain } = useSWR<string[]>( const { data: trainImages, mutate: refreshTrain } = useSWR<string[]>(
`classification/${model.name}/train`, `classification/${model.name}/train`,
); );
const { data: datasetResponse, mutate: refreshDataset } = useSWR<{ const { data: datasetResponse, mutate: refreshDataset } =
categories: { [id: string]: string[] }; useSWR<ClassificationDatasetResponse>(
training_metadata: { `classification/${model.name}/dataset`,
has_trained: boolean; );
last_training_date: string | null;
last_training_image_count: number;
current_image_count: number;
new_images_count: number;
dataset_changed: boolean;
} | null;
}>(`classification/${model.name}/dataset`);
const dataset = datasetResponse?.categories || {}; const dataset = datasetResponse?.categories || {};
const trainingMetadata = datasetResponse?.training_metadata; const trainingMetadata = datasetResponse?.training_metadata;