Add support for uploading images

This commit is contained in:
Nicolas Mowen 2024-11-26 08:40:32 -07:00
parent 41cb52c4cb
commit 3684c441a5
3 changed files with 51 additions and 33 deletions

View File

@ -2,10 +2,10 @@
import logging import logging
import os import os
from pathvalidate import sanitize_filename
from fastapi import APIRouter, Request, UploadFile from fastapi import APIRouter, Request, UploadFile
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pathvalidate import sanitize_filename
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
from frigate.const import FACE_DIR from frigate.const import FACE_DIR
@ -30,15 +30,6 @@ def get_faces():
@router.post("/faces/{name}") @router.post("/faces/{name}")
async def register_face(request: Request, name: str, file: UploadFile): async def register_face(request: Request, name: str, file: UploadFile):
# if not file.content_type.startswith("image"):
# return JSONResponse(
# status_code=400,
# content={
# "success": False,
# "message": "Only an image can be used to register a face.",
# },
# )
context: EmbeddingsContext = request.app.embeddings context: EmbeddingsContext = request.app.embeddings
context.register_face(name, await file.read()) context.register_face(name, await file.read())
return JSONResponse( return JSONResponse(

View File

@ -10,6 +10,7 @@ import {
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useCallback } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@ -18,23 +19,36 @@ type UploadImageDialogProps = {
title: string; title: string;
description?: string; description?: string;
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
onSave: (file: File) => void;
}; };
export default function UploadImageDialog({ export default function UploadImageDialog({
open, open,
title, title,
description, description,
setOpen, setOpen,
onSave,
}: UploadImageDialogProps) { }: UploadImageDialogProps) {
const formSchema = z.object({ const formSchema = z.object({
image: z file: z.instanceof(FileList, { message: "Please select an image file." }),
.instanceof(File, { message: "Please select an image file." })
.refine((file) => file.size < 1000000, "Image size is too large."),
}); });
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
mode: "onChange",
}); });
const fileRef = form.register("file");
// upload handler
const onSubmit = useCallback(
(data: z.infer<typeof formSchema>) => {
if (!data["file"]) {
return;
}
onSave(data["file"]["0"]);
},
[onSave],
);
return ( return (
<Dialog open={open} defaultOpen={false} onOpenChange={setOpen}> <Dialog open={open} defaultOpen={false} onOpenChange={setOpen}>
@ -44,31 +58,30 @@ export default function UploadImageDialog({
{description && <DialogDescription>{description}</DialogDescription>} {description && <DialogDescription>{description}</DialogDescription>}
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form> <form onSubmit={form.handleSubmit(onSubmit)}>
<FormField <FormField
control={form.control} control={form.control}
name="image" name="file"
render={({ field }) => ( render={() => (
<FormItem> <FormItem>
<FormControl> <FormControl>
{ <Input
// @ts-expect-error ignore className="aspect-video h-40 w-full"
<Input type="file"
className="aspect-video h-40 w-full" {...fileRef}
type="file" />
{...field}
/>
}
</FormControl> </FormControl>
</FormItem> </FormItem>
)} )}
/> />
<DialogFooter className="pt-4">
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="select" type="submit">
Save
</Button>
</DialogFooter>
</form> </form>
</Form> </Form>
<DialogFooter>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="select">Save</Button>
</DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -18,10 +18,6 @@ export default function FaceLibrary() {
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
const tabsRef = useRef<HTMLDivElement | null>(null); const tabsRef = useRef<HTMLDivElement | null>(null);
// upload
const [upload, setUpload] = useState(false);
// face data // face data
const { data: faceData } = useSWR("faces"); const { data: faceData } = useSWR("faces");
@ -43,6 +39,23 @@ export default function FaceLibrary() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [faces]); }, [faces]);
// upload
const [upload, setUpload] = useState(false);
const onUploadImage = useCallback(
(file: File) => {
const formData = new FormData();
formData.append("file", file);
axios.post(`faces/${pageToggle}`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
},
[pageToggle],
);
return ( return (
<div className="flex size-full flex-col p-2"> <div className="flex size-full flex-col p-2">
<Toaster /> <Toaster />
@ -52,6 +65,7 @@ export default function FaceLibrary() {
title="Upload Face Image" title="Upload Face Image"
description={`Upload an image to scan for faces and include for ${pageToggle}`} description={`Upload an image to scan for faces and include for ${pageToggle}`}
setOpen={setUpload} setOpen={setUpload}
onSave={onUploadImage}
/> />
<div className="relative flex h-11 w-full items-center justify-between"> <div className="relative flex h-11 w-full items-center justify-between">