Miscellaneous Fixes (0.17 beta) (#21336)

* fix coral docs

* add note about sub label object classification with person

* Catch OSError for deleting classification image

* add docs for dummy camera debugging

* add to sidebar

* fix formatting

* fix

* avx instructions are required for classification

* break text on classification card to prevent button overflow

* Ensure there is no NameError when processing

* Don't use region for state classification models

* fix spelling

* Handle attribute based models

* Catch case of non-trained model that doesn't add infinite number of classification images

* Actually train object classification models automatically

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
Josh Hawkins
2025-12-17 17:52:27 -06:00
committed by GitHub
parent 13957fec00
commit ae009b9861
13 changed files with 235 additions and 76 deletions

View File

@@ -4,8 +4,8 @@ import { cn } from "@/lib/utils";
import {
ClassificationItemData,
ClassificationThreshold,
ClassifiedEvent,
} from "@/types/classification";
import { Event } from "@/types/event";
import { forwardRef, useMemo, useRef, useState } from "react";
import { isDesktop, isIOS, isMobile, isMobileOnly } from "react-device-detect";
import { useTranslation } from "react-i18next";
@@ -160,7 +160,7 @@ export const ClassificationCard = forwardRef<
data.score != undefined ? "text-xs" : "text-sm",
)}
>
<div className="smart-capitalize">
<div className="break-all smart-capitalize">
{data.name == "unknown"
? t("details.unknown")
: data.name == "none"
@@ -190,7 +190,7 @@ export const ClassificationCard = forwardRef<
type GroupedClassificationCardProps = {
group: ClassificationItemData[];
event?: Event;
classifiedEvent?: ClassifiedEvent;
threshold?: ClassificationThreshold;
selectedItems: string[];
i18nLibrary: string;
@@ -201,7 +201,7 @@ type GroupedClassificationCardProps = {
};
export function GroupedClassificationCard({
group,
event,
classifiedEvent,
threshold,
selectedItems,
i18nLibrary,
@@ -236,14 +236,15 @@ export function GroupedClassificationCard({
const bestTyped: ClassificationItemData = best;
return {
...bestTyped,
name: event
? event.sub_label && event.sub_label !== "none"
? event.sub_label
: t(noClassificationLabel)
: bestTyped.name,
score: event?.data?.sub_label_score,
name:
classifiedEvent?.label && classifiedEvent.label !== "none"
? classifiedEvent.label
: classifiedEvent
? t(noClassificationLabel)
: bestTyped.name,
score: classifiedEvent?.score,
};
}, [group, event, noClassificationLabel, t]);
}, [group, classifiedEvent, noClassificationLabel, t]);
const bestScoreStatus = useMemo(() => {
if (!bestItem?.score || !threshold) {
@@ -329,36 +330,38 @@ export function GroupedClassificationCard({
)}
>
<ContentTitle className="flex items-center gap-2 font-normal capitalize">
{event?.sub_label && event.sub_label !== "none"
? event.sub_label
{classifiedEvent?.label && classifiedEvent.label !== "none"
? classifiedEvent.label
: t(noClassificationLabel)}
{event?.sub_label && event.sub_label !== "none" && (
<div className="flex items-center gap-1">
<div
className={cn(
"",
bestScoreStatus == "match" && "text-success",
bestScoreStatus == "potential" && "text-orange-400",
bestScoreStatus == "unknown" && "text-danger",
)}
>{`${Math.round((event.data.sub_label_score || 0) * 100)}%`}</div>
<Popover>
<PopoverTrigger asChild>
<button
className="focus:outline-none"
aria-label={t("details.scoreInfo", {
ns: i18nLibrary,
})}
>
<LuInfo className="size-3" />
</button>
</PopoverTrigger>
<PopoverContent className="w-80 text-sm">
{t("details.scoreInfo", { ns: i18nLibrary })}
</PopoverContent>
</Popover>
</div>
)}
{classifiedEvent?.label &&
classifiedEvent.label !== "none" &&
classifiedEvent.score !== undefined && (
<div className="flex items-center gap-1">
<div
className={cn(
"",
bestScoreStatus == "match" && "text-success",
bestScoreStatus == "potential" && "text-orange-400",
bestScoreStatus == "unknown" && "text-danger",
)}
>{`${Math.round((classifiedEvent.score || 0) * 100)}%`}</div>
<Popover>
<PopoverTrigger asChild>
<button
className="focus:outline-none"
aria-label={t("details.scoreInfo", {
ns: i18nLibrary,
})}
>
<LuInfo className="size-3" />
</button>
</PopoverTrigger>
<PopoverContent className="w-80 text-sm">
{t("details.scoreInfo", { ns: i18nLibrary })}
</PopoverContent>
</Popover>
</div>
)}
</ContentTitle>
<ContentDescription className={cn("", isMobile && "px-2")}>
{time && (
@@ -372,14 +375,14 @@ export function GroupedClassificationCard({
</div>
{isDesktop && (
<div className="flex flex-row justify-between">
{event && (
{classifiedEvent && (
<Tooltip>
<TooltipTrigger asChild>
<div
className="cursor-pointer"
tabIndex={-1}
onClick={() => {
navigate(`/explore?event_id=${event.id}`);
navigate(`/explore?event_id=${classifiedEvent.id}`);
}}
>
<LuSearch className="size-4 text-secondary-foreground" />

View File

@@ -186,15 +186,17 @@ export default function Step3ChooseExamples({
await Promise.all(emptyFolderPromises);
// Step 3: Determine if we should train
// For state models, we need ALL states to have examples
// For object models, we need at least 2 classes with images
// For state models, we need ALL states to have examples (at least 2 states)
// For object models, we need at least 1 class with images (the rest go to "none")
const allStatesHaveExamplesForTraining =
step1Data.modelType !== "state" ||
step1Data.classes.every((className) =>
classesWithImages.has(className),
);
const shouldTrain =
allStatesHaveExamplesForTraining && classesWithImages.size >= 2;
step1Data.modelType === "object"
? classesWithImages.size >= 1
: allStatesHaveExamplesForTraining && classesWithImages.size >= 2;
// Step 4: Kick off training only if we have enough classes with images
if (shouldTrain) {