mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-22 08:08:22 +03:00
Compare commits
16 Commits
cf9a4a9407
...
ab43a33d19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab43a33d19 | ||
|
|
f3c0c8b61a | ||
|
|
19e36fc56b | ||
|
|
6258b90e91 | ||
|
|
1d94b24cfe | ||
|
|
294e04adb1 | ||
|
|
d4af7f0966 | ||
|
|
8ddf3ac72e | ||
|
|
5b7fe10caf | ||
|
|
d8e32b8724 | ||
|
|
ca91d3d82b | ||
|
|
88b6bd7535 | ||
|
|
9f73145d63 | ||
|
|
f2e3579030 | ||
|
|
796a12bf10 | ||
|
|
aa6cd1ec05 |
@ -662,8 +662,11 @@ def delete_classification_dataset_images(
|
||||
if os.path.isfile(file_path):
|
||||
os.unlink(file_path)
|
||||
|
||||
if os.path.exists(folder) and not os.listdir(folder):
|
||||
os.rmdir(folder)
|
||||
|
||||
return JSONResponse(
|
||||
content=({"success": True, "message": "Successfully deleted faces."}),
|
||||
content=({"success": True, "message": "Successfully deleted images."}),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
@ -723,7 +726,7 @@ def categorize_classification_image(request: Request, name: str, body: dict = No
|
||||
os.unlink(training_file)
|
||||
|
||||
return JSONResponse(
|
||||
content=({"success": True, "message": "Successfully deleted faces."}),
|
||||
content=({"success": True, "message": "Successfully categorized image."}),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
@ -761,7 +764,7 @@ def delete_classification_train_images(request: Request, name: str, body: dict =
|
||||
os.unlink(file_path)
|
||||
|
||||
return JSONResponse(
|
||||
content=({"success": True, "message": "Successfully deleted faces."}),
|
||||
content=({"success": True, "message": "Successfully deleted images."}),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@ from typing import Optional
|
||||
from faster_whisper import WhisperModel
|
||||
from peewee import DoesNotExist
|
||||
|
||||
from frigate.comms.embeddings_updater import EmbeddingsRequestEnum
|
||||
from frigate.comms.inter_process import InterProcessRequestor
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.const import (
|
||||
@ -32,11 +31,13 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
|
||||
self,
|
||||
config: FrigateConfig,
|
||||
requestor: InterProcessRequestor,
|
||||
embeddings,
|
||||
metrics: DataProcessorMetrics,
|
||||
):
|
||||
super().__init__(config, metrics, None)
|
||||
self.config = config
|
||||
self.requestor = requestor
|
||||
self.embeddings = embeddings
|
||||
self.recognizer = None
|
||||
self.transcription_lock = threading.Lock()
|
||||
self.transcription_thread = None
|
||||
@ -128,10 +129,7 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
|
||||
)
|
||||
|
||||
# Embed the description
|
||||
self.requestor.send_data(
|
||||
EmbeddingsRequestEnum.embed_description.value,
|
||||
{"id": event_id, "description": transcription},
|
||||
)
|
||||
self.embeddings.embed_description(event_id, transcription)
|
||||
|
||||
except DoesNotExist:
|
||||
logger.debug("No recording found for audio transcription post-processing")
|
||||
|
||||
@ -226,7 +226,9 @@ class EmbeddingMaintainer(threading.Thread):
|
||||
for c in self.config.cameras.values()
|
||||
):
|
||||
self.post_processors.append(
|
||||
AudioTranscriptionPostProcessor(self.config, self.requestor, metrics)
|
||||
AudioTranscriptionPostProcessor(
|
||||
self.config, self.requestor, self.embeddings, metrics
|
||||
)
|
||||
)
|
||||
|
||||
semantic_trigger_processor: SemanticTriggerProcessor | None = None
|
||||
|
||||
@ -14,7 +14,6 @@ type SearchThumbnailProps = {
|
||||
findSimilar: () => void;
|
||||
refreshResults: () => void;
|
||||
showTrackingDetails: () => void;
|
||||
showSnapshot: () => void;
|
||||
addTrigger: () => void;
|
||||
};
|
||||
|
||||
@ -24,7 +23,6 @@ export default function SearchThumbnailFooter({
|
||||
findSimilar,
|
||||
refreshResults,
|
||||
showTrackingDetails,
|
||||
showSnapshot,
|
||||
addTrigger,
|
||||
}: SearchThumbnailProps) {
|
||||
const { t } = useTranslation(["views/search"]);
|
||||
@ -62,7 +60,6 @@ export default function SearchThumbnailFooter({
|
||||
findSimilar={findSimilar}
|
||||
refreshResults={refreshResults}
|
||||
showTrackingDetails={showTrackingDetails}
|
||||
showSnapshot={showSnapshot}
|
||||
addTrigger={addTrigger}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -6,10 +6,7 @@ import { toast } from "sonner";
|
||||
import axios from "axios";
|
||||
import { LuCamera, LuDownload, LuTrash2 } from "react-icons/lu";
|
||||
import { FiMoreVertical } from "react-icons/fi";
|
||||
import { FaArrowsRotate } from "react-icons/fa6";
|
||||
import { MdImageSearch } from "react-icons/md";
|
||||
import FrigatePlusIcon from "@/components/icons/FrigatePlusIcon";
|
||||
import { isMobileOnly } from "react-device-detect";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
ContextMenu,
|
||||
@ -33,23 +30,18 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { BsFillLightningFill } from "react-icons/bs";
|
||||
import BlurredIconButton from "../button/BlurredIconButton";
|
||||
import { PiPath } from "react-icons/pi";
|
||||
|
||||
type SearchResultActionsProps = {
|
||||
searchResult: SearchResult;
|
||||
findSimilar: () => void;
|
||||
refreshResults: () => void;
|
||||
showTrackingDetails: () => void;
|
||||
showSnapshot: () => void;
|
||||
addTrigger: () => void;
|
||||
isContextMenu?: boolean;
|
||||
children?: ReactNode;
|
||||
@ -60,7 +52,6 @@ export default function SearchResultActions({
|
||||
findSimilar,
|
||||
refreshResults,
|
||||
showTrackingDetails,
|
||||
showSnapshot,
|
||||
addTrigger,
|
||||
isContextMenu = false,
|
||||
children,
|
||||
@ -129,7 +120,7 @@ export default function SearchResultActions({
|
||||
aria-label={t("itemMenu.viewTrackingDetails.aria")}
|
||||
onClick={showTrackingDetails}
|
||||
>
|
||||
<FaArrowsRotate className="mr-2 size-4" />
|
||||
<PiPath className="mr-2 size-4" />
|
||||
<span>{t("itemMenu.viewTrackingDetails.label")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
@ -152,18 +143,14 @@ export default function SearchResultActions({
|
||||
<span>{t("itemMenu.addTrigger.label")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{isMobileOnly &&
|
||||
config?.plus?.enabled &&
|
||||
searchResult.has_snapshot &&
|
||||
searchResult.end_time &&
|
||||
searchResult.data.type == "object" &&
|
||||
!searchResult.plus_id && (
|
||||
{config?.semantic_search?.enabled &&
|
||||
searchResult.data.type == "object" && (
|
||||
<MenuItem
|
||||
aria-label={t("itemMenu.submitToPlus.aria")}
|
||||
onClick={showSnapshot}
|
||||
aria-label={t("itemMenu.findSimilar.aria")}
|
||||
onClick={findSimilar}
|
||||
>
|
||||
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
|
||||
<span>{t("itemMenu.submitToPlus.label")}</span>
|
||||
<MdImageSearch className="mr-2 size-4" />
|
||||
<span>{t("itemMenu.findSimilar.label")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
@ -211,44 +198,6 @@ export default function SearchResultActions({
|
||||
</ContextMenu>
|
||||
) : (
|
||||
<>
|
||||
{config?.semantic_search?.enabled &&
|
||||
searchResult.data.type == "object" && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<BlurredIconButton
|
||||
onClick={findSimilar}
|
||||
aria-label={t("itemMenu.findSimilar.aria")}
|
||||
>
|
||||
<MdImageSearch className="size-5" />
|
||||
</BlurredIconButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("itemMenu.findSimilar.label")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!isMobileOnly &&
|
||||
config?.plus?.enabled &&
|
||||
searchResult.has_snapshot &&
|
||||
searchResult.end_time &&
|
||||
searchResult.data.type == "object" &&
|
||||
!searchResult.plus_id && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<BlurredIconButton
|
||||
onClick={showSnapshot}
|
||||
aria-label={t("itemMenu.submitToPlus.aria")}
|
||||
>
|
||||
<FrigatePlusIcon className="size-5" />
|
||||
</BlurredIconButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("itemMenu.submitToPlus.label")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<BlurredIconButton aria-label={t("itemMenu.more.aria")}>
|
||||
|
||||
@ -97,14 +97,12 @@ export default function ClassificationSelectionDialog({
|
||||
|
||||
return (
|
||||
<div className={className ?? "flex"}>
|
||||
{newClass && (
|
||||
<TextEntryDialog
|
||||
open={true}
|
||||
setOpen={setNewClass}
|
||||
title={t("createCategory.new")}
|
||||
onSave={(newCat) => onCategorizeImage(newCat)}
|
||||
/>
|
||||
)}
|
||||
<TextEntryDialog
|
||||
open={newClass}
|
||||
setOpen={setNewClass}
|
||||
title={t("createCategory.new")}
|
||||
onSave={(newCat) => onCategorizeImage(newCat)}
|
||||
/>
|
||||
|
||||
<Tooltip>
|
||||
<Selector>
|
||||
|
||||
@ -141,50 +141,52 @@ export function AnnotationSettingsPane({
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-1 flex-col space-y-6"
|
||||
className="flex flex-1 flex-col space-y-3"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="annotationOffset"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start justify-between space-x-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<FormLabel>
|
||||
{t("trackingDetails.annotationSettings.offset.label")}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
<Trans ns="views/explore">
|
||||
trackingDetails.annotationSettings.offset.millisecondsToOffset
|
||||
</Trans>
|
||||
<FormMessage />
|
||||
<div className="mt-2">
|
||||
{t("trackingDetails.annotationSettings.offset.tips")}
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to={getLocaleDocUrl("configuration/reference")}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
{t("readTheDocumentation", { ns: "common" })}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
<>
|
||||
<FormItem className="flex flex-row items-start justify-between space-x-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<FormLabel>
|
||||
{t("trackingDetails.annotationSettings.offset.label")}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
<Trans ns="views/explore">
|
||||
trackingDetails.annotationSettings.offset.millisecondsToOffset
|
||||
</Trans>
|
||||
<FormMessage />
|
||||
</FormDescription>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="min-w-24">
|
||||
<FormControl>
|
||||
<Input
|
||||
className="text-md w-full border border-input bg-background p-2 text-center hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
placeholder="0"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</FormDescription>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="min-w-24">
|
||||
<FormControl>
|
||||
<Input
|
||||
className="text-md w-full border border-input bg-background p-2 text-center hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
placeholder="0"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</FormItem>
|
||||
<div className="mt-1 text-sm text-secondary-foreground">
|
||||
{t("trackingDetails.annotationSettings.offset.tips")}
|
||||
<div className="mt-2 flex items-center text-primary-variant">
|
||||
<Link
|
||||
to={getLocaleDocUrl("configuration/reference")}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
{t("readTheDocumentation", { ns: "common" })}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
@ -111,6 +111,23 @@ export default function DetailActionsMenu({
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{config?.semantic_search.enabled && search.data.type == "object" && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
setTimeout(() => {
|
||||
navigate(
|
||||
`/settings?page=triggers&camera=${search.camera}&event_id=${search.id}`,
|
||||
);
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
<div className="flex cursor-pointer items-center gap-2">
|
||||
<span>{t("itemMenu.addTrigger.label")}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -1242,106 +1242,110 @@ function ObjectDetailsTab({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"my-2 flex w-full flex-col justify-between gap-1.5",
|
||||
state == "submitted" && "flex-row",
|
||||
)}
|
||||
>
|
||||
<div className="text-sm text-primary/40">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{t("explore.plus.submitToPlus.label", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer p-0">
|
||||
<LuInfo className="size-4" />
|
||||
<span className="sr-only">Info</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
container={popoverContainerRef.current}
|
||||
className="w-80 text-xs"
|
||||
>
|
||||
{t("explore.plus.submitToPlus.desc", {
|
||||
{search.data.type === "object" &&
|
||||
!search.plus_id &&
|
||||
config?.plus?.enabled && (
|
||||
<div
|
||||
className={cn(
|
||||
"my-2 flex w-full flex-col justify-between gap-1.5",
|
||||
state == "submitted" && "flex-row",
|
||||
)}
|
||||
>
|
||||
<div className="text-sm text-primary/40">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{t("explore.plus.submitToPlus.label", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer p-0">
|
||||
<LuInfo className="size-4" />
|
||||
<span className="sr-only">Info</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
container={popoverContainerRef.current}
|
||||
className="w-80 text-xs"
|
||||
>
|
||||
{t("explore.plus.submitToPlus.desc", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between gap-2 text-sm">
|
||||
{state == "reviewing" && (
|
||||
<>
|
||||
<div>
|
||||
{i18n.language === "en" ? (
|
||||
// English with a/an logic plus label
|
||||
<>
|
||||
{/^[aeiou]/i.test(search?.label || "") ? (
|
||||
<Trans
|
||||
ns="components/dialog"
|
||||
values={{ label: search?.label }}
|
||||
>
|
||||
explore.plus.review.question.ask_an
|
||||
</Trans>
|
||||
<div className="flex flex-row items-center justify-between gap-2 text-sm">
|
||||
{state == "reviewing" && (
|
||||
<>
|
||||
<div>
|
||||
{i18n.language === "en" ? (
|
||||
// English with a/an logic plus label
|
||||
<>
|
||||
{/^[aeiou]/i.test(search?.label || "") ? (
|
||||
<Trans
|
||||
ns="components/dialog"
|
||||
values={{ label: search?.label }}
|
||||
>
|
||||
explore.plus.review.question.ask_an
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans
|
||||
ns="components/dialog"
|
||||
values={{ label: search?.label }}
|
||||
>
|
||||
explore.plus.review.question.ask_a
|
||||
</Trans>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// For other languages
|
||||
<Trans
|
||||
ns="components/dialog"
|
||||
values={{ label: search?.label }}
|
||||
values={{
|
||||
untranslatedLabel: search?.label,
|
||||
translatedLabel: getTranslatedLabel(search?.label),
|
||||
}}
|
||||
>
|
||||
explore.plus.review.question.ask_a
|
||||
explore.plus.review.question.ask_full
|
||||
</Trans>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// For other languages
|
||||
<Trans
|
||||
ns="components/dialog"
|
||||
values={{
|
||||
untranslatedLabel: search?.label,
|
||||
translatedLabel: getTranslatedLabel(search?.label),
|
||||
}}
|
||||
>
|
||||
explore.plus.review.question.ask_full
|
||||
</Trans>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex max-w-xl flex-row gap-2">
|
||||
<Button
|
||||
className="flex-1 bg-success"
|
||||
aria-label={t("button.yes", { ns: "common" })}
|
||||
onClick={() => {
|
||||
setState("uploading");
|
||||
onSubmitToPlus(false);
|
||||
}}
|
||||
>
|
||||
{t("button.yes", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1 text-white"
|
||||
aria-label={t("button.no", { ns: "common" })}
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setState("uploading");
|
||||
onSubmitToPlus(true);
|
||||
}}
|
||||
>
|
||||
{t("button.no", { ns: "common" })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{state == "uploading" && <ActivityIndicator />}
|
||||
{state == "submitted" && (
|
||||
<div className="flex flex-row items-center justify-center gap-2">
|
||||
<FaCheckCircle className="size-4 text-success" />
|
||||
{t("explore.plus.review.state.submitted")}
|
||||
</div>
|
||||
<div className="flex max-w-xl flex-row gap-2">
|
||||
<Button
|
||||
className="flex-1 bg-success"
|
||||
aria-label={t("button.yes", { ns: "common" })}
|
||||
onClick={() => {
|
||||
setState("uploading");
|
||||
onSubmitToPlus(false);
|
||||
}}
|
||||
>
|
||||
{t("button.yes", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1 text-white"
|
||||
aria-label={t("button.no", { ns: "common" })}
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setState("uploading");
|
||||
onSubmitToPlus(true);
|
||||
}}
|
||||
>
|
||||
{t("button.no", { ns: "common" })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{state == "uploading" && <ActivityIndicator />}
|
||||
{state == "submitted" && (
|
||||
<div className="flex flex-row items-center justify-center gap-2">
|
||||
<FaCheckCircle className="size-4 text-success" />
|
||||
{t("explore.plus.review.state.submitted")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{config?.cameras[search.camera].objects.genai.enabled &&
|
||||
!search.end_time &&
|
||||
|
||||
@ -457,7 +457,7 @@ export function TrackingDetails({
|
||||
>
|
||||
{config?.cameras[event.camera]?.onvif.autotracking
|
||||
.enabled_in_config && (
|
||||
<div className="mb-2 text-sm text-danger">
|
||||
<div className="mb-2 ml-3 text-sm text-danger">
|
||||
{t("trackingDetails.autoTrackingTips")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -98,7 +98,11 @@ export default function RestartDialog({
|
||||
open={restartingSheetOpen}
|
||||
onOpenChange={() => setRestartingSheetOpen(false)}
|
||||
>
|
||||
<SheetContent side="top" onInteractOutside={(e) => e.preventDefault()}>
|
||||
<SheetContent
|
||||
side="top"
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
className="[&>button:first-of-type]:hidden"
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<ActivityIndicator />
|
||||
<SheetHeader className="mt-5 text-center">
|
||||
|
||||
@ -192,7 +192,7 @@ export default function DetailStream({
|
||||
<div className="relative flex h-full flex-col">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="scrollbar-container flex-1 overflow-y-auto pb-14"
|
||||
className="scrollbar-container flex-1 overflow-y-auto overflow-x-hidden pb-14"
|
||||
>
|
||||
<div className="space-y-4 py-2">
|
||||
{reviewItems?.length === 0 ? (
|
||||
@ -811,7 +811,7 @@ function ObjectTimeline({
|
||||
|
||||
if (!timeline || timeline.length === 0) {
|
||||
return (
|
||||
<div className="py-2 text-sm text-muted-foreground">
|
||||
<div className="ml-8 text-sm text-muted-foreground">
|
||||
{t("detail.noObjectDetailData")}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -55,20 +55,24 @@ export default function EventMenu({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onSelect={handleObjectSelect}>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={handleObjectSelect}
|
||||
>
|
||||
{isSelected
|
||||
? t("itemMenu.hideObjectDetails.label")
|
||||
: t("itemMenu.showObjectDetails.label")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="my-0.5" />
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={() => {
|
||||
navigate(`/explore?event_id=${event.id}`);
|
||||
}}
|
||||
>
|
||||
{t("details.item.button.viewInExplore")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<DropdownMenuItem className="cursor-pointer" asChild>
|
||||
<a
|
||||
download
|
||||
href={
|
||||
@ -86,6 +90,7 @@ export default function EventMenu({
|
||||
event.data.type == "object" &&
|
||||
config?.plus?.enabled && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={() => {
|
||||
setIsOpen(false);
|
||||
onOpenUpload?.(event);
|
||||
@ -97,6 +102,7 @@ export default function EventMenu({
|
||||
|
||||
{event.has_snapshot && config?.semantic_search?.enabled && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={() => {
|
||||
if (onOpenSimilarity) onOpenSimilarity(event);
|
||||
else
|
||||
|
||||
@ -118,6 +118,11 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
||||
|
||||
const [trainFilter, setTrainFilter] = useApiFilter<TrainFilter>();
|
||||
|
||||
const refreshAll = useCallback(() => {
|
||||
refreshTrain();
|
||||
refreshDataset();
|
||||
}, [refreshTrain, refreshDataset]);
|
||||
|
||||
// image multiselect
|
||||
|
||||
const [selectedImages, setSelectedImages] = useState<string[]>([]);
|
||||
@ -183,11 +188,12 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
||||
);
|
||||
|
||||
const onDelete = useCallback(
|
||||
(ids: string[], isName: boolean = false) => {
|
||||
(ids: string[], isName: boolean = false, category?: string) => {
|
||||
const targetCategory = category || pageToggle;
|
||||
const api =
|
||||
pageToggle == "train"
|
||||
targetCategory == "train"
|
||||
? `/classification/${model.name}/train/delete`
|
||||
: `/classification/${model.name}/dataset/${pageToggle}/delete`;
|
||||
: `/classification/${model.name}/dataset/${targetCategory}/delete`;
|
||||
|
||||
axios
|
||||
.post(api, { ids })
|
||||
@ -408,7 +414,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
||||
trainImages={trainImages || []}
|
||||
trainFilter={trainFilter}
|
||||
selectedImages={selectedImages}
|
||||
onRefresh={refreshTrain}
|
||||
onRefresh={refreshAll}
|
||||
onClickImages={onClickImages}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
@ -432,7 +438,7 @@ type LibrarySelectorProps = {
|
||||
dataset: { [id: string]: string[] };
|
||||
trainImages: string[];
|
||||
setPageToggle: (toggle: string) => void;
|
||||
onDelete: (ids: string[], isName: boolean) => void;
|
||||
onDelete: (ids: string[], isName: boolean, category?: string) => void;
|
||||
onRename: (old_name: string, new_name: string) => void;
|
||||
};
|
||||
function LibrarySelector({
|
||||
@ -448,7 +454,7 @@ function LibrarySelector({
|
||||
// data
|
||||
|
||||
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
||||
const [renameClass, setRenameFace] = useState<string | null>(null);
|
||||
const [renameClass, setRenameClass] = useState<string | null>(null);
|
||||
const pageTitle = useMemo(() => {
|
||||
if (pageToggle != "train") {
|
||||
return pageToggle;
|
||||
@ -463,12 +469,12 @@ function LibrarySelector({
|
||||
|
||||
// interaction
|
||||
|
||||
const handleDeleteFace = useCallback(
|
||||
const handleDeleteCategory = useCallback(
|
||||
(name: string) => {
|
||||
// Get all image IDs for this face
|
||||
// Get all image IDs for this category
|
||||
const imageIds = dataset?.[name] || [];
|
||||
|
||||
onDelete(imageIds, true);
|
||||
onDelete(imageIds, true, name);
|
||||
setPageToggle("train");
|
||||
},
|
||||
[dataset, onDelete, setPageToggle],
|
||||
@ -476,7 +482,7 @@ function LibrarySelector({
|
||||
|
||||
const handleSetOpen = useCallback(
|
||||
(open: boolean) => {
|
||||
setRenameFace(open ? renameClass : null);
|
||||
setRenameClass(open ? renameClass : null);
|
||||
},
|
||||
[renameClass],
|
||||
);
|
||||
@ -503,7 +509,7 @@ function LibrarySelector({
|
||||
className="text-white"
|
||||
onClick={() => {
|
||||
if (confirmDelete) {
|
||||
handleDeleteFace(confirmDelete);
|
||||
handleDeleteCategory(confirmDelete);
|
||||
setConfirmDelete(null);
|
||||
}
|
||||
}}
|
||||
@ -521,7 +527,7 @@ function LibrarySelector({
|
||||
description={t("renameCategory.desc", { name: renameClass })}
|
||||
onSave={(newName) => {
|
||||
onRename(renameClass!, newName);
|
||||
setRenameFace(null);
|
||||
setRenameClass(null);
|
||||
}}
|
||||
defaultValue={renameClass || ""}
|
||||
regexPattern={/^[\p{L}\p{N}\s'_-]{1,50}$/u}
|
||||
@ -588,7 +594,7 @@ function LibrarySelector({
|
||||
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setRenameFace(id);
|
||||
setRenameClass(id);
|
||||
}}
|
||||
>
|
||||
<LuPencil className="size-4 text-primary" />
|
||||
|
||||
@ -236,10 +236,6 @@ function ExploreThumbnailImage({
|
||||
onSelectSearch(event, false, "tracking_details");
|
||||
};
|
||||
|
||||
const handleShowSnapshot = () => {
|
||||
onSelectSearch(event, false, "snapshot");
|
||||
};
|
||||
|
||||
const handleAddTrigger = () => {
|
||||
navigate(
|
||||
`/settings?page=triggers&camera=${event.camera}&event_id=${event.id}`,
|
||||
@ -252,7 +248,6 @@ function ExploreThumbnailImage({
|
||||
findSimilar={handleFindSimilar}
|
||||
refreshResults={mutate}
|
||||
showTrackingDetails={handleShowTrackingDetails}
|
||||
showSnapshot={handleShowSnapshot}
|
||||
addTrigger={handleAddTrigger}
|
||||
isContextMenu={true}
|
||||
>
|
||||
|
||||
@ -985,7 +985,7 @@ function Timeline({
|
||||
),
|
||||
)}
|
||||
>
|
||||
{isMobile && (
|
||||
{isMobile && timelineType == "timeline" && (
|
||||
<GenAISummaryDialog review={activeReviewItem} onOpen={onAnalysisOpen} />
|
||||
)}
|
||||
|
||||
|
||||
@ -688,9 +688,6 @@ export default function SearchView({
|
||||
showTrackingDetails={() =>
|
||||
onSelectSearch(value, false, "tracking_details")
|
||||
}
|
||||
showSnapshot={() =>
|
||||
onSelectSearch(value, false, "snapshot")
|
||||
}
|
||||
addTrigger={() => {
|
||||
if (
|
||||
config?.semantic_search.enabled &&
|
||||
|
||||
@ -403,7 +403,8 @@ export default function TriggerView({
|
||||
setShowCreate(true);
|
||||
setSelectedTrigger({
|
||||
enabled: true,
|
||||
name: "",
|
||||
name: eventId,
|
||||
friendly_name: "",
|
||||
type: "thumbnail",
|
||||
data: eventId,
|
||||
threshold: 0.5,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user