Support creating a new face

This commit is contained in:
Nicolas Mowen 2025-03-28 11:14:54 -06:00
parent e9409e9070
commit 13d7eec965
2 changed files with 97 additions and 67 deletions

View File

@ -125,10 +125,13 @@ def train_face(request: Request, name: str, body: dict = None):
sanitized_name = sanitize_filename(name)
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
new_name = f"{sanitized_name}-{rand_id}.webp"
new_file = os.path.join(FACE_DIR, f"{sanitized_name}/{new_name}")
new_file_folder = os.path.join(FACE_DIR, f"{sanitized_name}")
if not os.path.exists(new_file_folder):
os.mkdir(new_file_folder)
if training_file_name:
shutil.move(training_file, new_file)
shutil.move(training_file, os.path.join(new_file_folder, new_name))
else:
try:
event: Event = Event.get(Event.id == event_id)
@ -155,7 +158,7 @@ def train_face(request: Request, name: str, body: dict = None):
x2 = x1 + int(face_box[2] * detect_config.width) - 4
y2 = y1 + int(face_box[3] * detect_config.height) - 4
face = snapshot[y1:y2, x1:x2]
cv2.imwrite(new_file, face)
cv2.imwrite(os.path.join(new_file_folder, new_name), face)
context: EmbeddingsContext = request.app.embeddings
context.clear_face_classifier()

View File

@ -3,6 +3,7 @@ import TimeAgo from "@/components/dynamic/TimeAgo";
import AddFaceIcon from "@/components/icons/AddFaceIcon";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import CreateFaceWizardDialog from "@/components/overlay/detail/FaceCreateWizardDialog";
import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog";
import UploadImageDialog from "@/components/overlay/dialog/UploadImageDialog";
import { Button } from "@/components/ui/button";
import {
@ -42,6 +43,7 @@ import { isDesktop, isMobile } from "react-device-detect";
import { useTranslation } from "react-i18next";
import {
LuImagePlus,
LuPlus,
LuRefreshCw,
LuScanFace,
LuSearch,
@ -605,6 +607,8 @@ function FaceAttempt({
// interaction
const [newFace, setNewFace] = useState(false);
const imgRef = useRef<HTMLImageElement | null>(null);
useContextMenu(imgRef, () => {
@ -663,76 +667,99 @@ function FaceAttempt({
}, [data, onRefresh, t]);
return (
<div
className={cn(
"relative flex cursor-pointer flex-col rounded-lg outline outline-[3px]",
selected
? "shadow-selected outline-selected"
: "outline-transparent duration-500",
)}
>
<div className="relative w-full overflow-hidden rounded-lg *:text-card-foreground">
<img
ref={imgRef}
className={cn("size-44", isMobile && "w-full")}
src={`${baseUrl}clips/faces/train/${data.filename}`}
onClick={(e) => onClick(data, e.metaKey || e.ctrlKey)}
<>
{newFace && (
<TextEntryDialog
open={true}
setOpen={setNewFace}
title="Create New Face"
onSave={(newName) => onTrainAttempt(newName)}
/>
<div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white">
<TimeAgo className="text-white" time={data.timestamp * 1000} dense />
)}
<div
className={cn(
"relative flex cursor-pointer flex-col rounded-lg outline outline-[3px]",
selected
? "shadow-selected outline-selected"
: "outline-transparent duration-500",
)}
>
<div className="relative w-full overflow-hidden rounded-lg *:text-card-foreground">
<img
ref={imgRef}
className={cn("size-44", isMobile && "w-full")}
src={`${baseUrl}clips/faces/train/${data.filename}`}
onClick={(e) => onClick(data, e.metaKey || e.ctrlKey)}
/>
<div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white">
<TimeAgo
className="text-white"
time={data.timestamp * 1000}
dense
/>
</div>
</div>
</div>
<div className="p-2">
<div className="flex w-full flex-row items-center justify-between gap-2">
<div className="flex flex-col items-start text-xs text-primary-variant">
<div className="capitalize">{data.name}</div>
<div
className={cn(
"",
scoreStatus == "match" && "text-success",
scoreStatus == "potential" && "text-orange-400",
scoreStatus == "unknown" && "text-danger",
)}
>
{Math.round(data.score * 100)}%
<div className="p-2">
<div className="flex w-full flex-row items-center justify-between gap-2">
<div className="flex flex-col items-start text-xs text-primary-variant">
<div className="capitalize">{data.name}</div>
<div
className={cn(
"",
scoreStatus == "match" && "text-success",
scoreStatus == "potential" && "text-orange-400",
scoreStatus == "unknown" && "text-danger",
)}
>
{Math.round(data.score * 100)}%
</div>
</div>
<div className="flex flex-row items-start justify-end gap-5 md:gap-4">
<Tooltip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<TooltipTrigger>
<AddFaceIcon className="size-5 cursor-pointer text-primary-variant hover:text-primary" />
</TooltipTrigger>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>{t("trainFaceAs")}</DropdownMenuLabel>
<DropdownMenuItem
className="flex cursor-pointer gap-2 capitalize"
onClick={() => setNewFace(true)}
>
<LuPlus />
Create New Face
</DropdownMenuItem>
{faceNames.map((faceName) => (
<DropdownMenuItem
key={faceName}
className="flex cursor-pointer gap-2 capitalize"
onClick={() => onTrainAttempt(faceName)}
>
<LuScanFace />
{faceName}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<TooltipContent>{t("trainFace")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<LuRefreshCw
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
onClick={() => onReprocess()}
/>
</TooltipTrigger>
<TooltipContent>{t("button.reprocessFace")}</TooltipContent>
</Tooltip>
</div>
</div>
<div className="flex flex-row items-start justify-end gap-5 md:gap-4">
<Tooltip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<TooltipTrigger>
<AddFaceIcon className="size-5 cursor-pointer text-primary-variant hover:text-primary" />
</TooltipTrigger>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>{t("trainFaceAs")}</DropdownMenuLabel>
{faceNames.map((faceName) => (
<DropdownMenuItem
key={faceName}
className="cursor-pointer capitalize"
onClick={() => onTrainAttempt(faceName)}
>
{faceName}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<TooltipContent>{t("trainFace")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<LuRefreshCw
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
onClick={() => onReprocess()}
/>
</TooltipTrigger>
<TooltipContent>{t("button.reprocessFace")}</TooltipContent>
</Tooltip>
</div>
</div>
</div>
</div>
</>
);
}