mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-08-08 13:51:01 +02:00
Dynamic embeddings reindexing (#17418)
* reindex with api endpoint and zmq * threading * frontend * require admin role
This commit is contained in:
parent
67dd50a7f7
commit
23c3323871
@ -298,3 +298,49 @@ def reprocess_license_plate(request: Request, event_id: str):
|
||||
content=response,
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/reindex", dependencies=[Depends(require_role(["admin"]))])
|
||||
def reindex_embeddings(request: Request):
|
||||
if not request.app.frigate_config.semantic_search.enabled:
|
||||
message = (
|
||||
"Cannot reindex tracked object embeddings, Semantic Search is not enabled."
|
||||
)
|
||||
logger.error(message)
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": message,
|
||||
}
|
||||
),
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
context: EmbeddingsContext = request.app.embeddings
|
||||
response = context.reindex_embeddings()
|
||||
|
||||
if response == "started":
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"message": "Embeddings reindexing has started.",
|
||||
},
|
||||
status_code=202, # 202 Accepted
|
||||
)
|
||||
elif response == "in_progress":
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": False,
|
||||
"message": "Embeddings reindexing is already in progress.",
|
||||
},
|
||||
status_code=409, # 409 Conflict
|
||||
)
|
||||
else:
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": False,
|
||||
"message": "Failed to start reindexing.",
|
||||
},
|
||||
status_code=500,
|
||||
)
|
||||
|
@ -17,6 +17,7 @@ class EmbeddingsRequestEnum(Enum):
|
||||
register_face = "register_face"
|
||||
reprocess_face = "reprocess_face"
|
||||
reprocess_plate = "reprocess_plate"
|
||||
reindex = "reindex"
|
||||
|
||||
|
||||
class EmbeddingsResponder:
|
||||
|
@ -250,3 +250,6 @@ class EmbeddingsContext:
|
||||
return self.requestor.send_data(
|
||||
EmbeddingsRequestEnum.reprocess_plate.value, {"event": event}
|
||||
)
|
||||
|
||||
def reindex_embeddings(self) -> dict[str, any]:
|
||||
return self.requestor.send_data(EmbeddingsRequestEnum.reindex.value, {})
|
||||
|
@ -3,6 +3,7 @@
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
from numpy import ndarray
|
||||
@ -74,6 +75,10 @@ class Embeddings:
|
||||
self.metrics = metrics
|
||||
self.requestor = InterProcessRequestor()
|
||||
|
||||
self.reindex_lock = threading.Lock()
|
||||
self.reindex_thread = None
|
||||
self.reindex_running = False
|
||||
|
||||
# Create tables if they don't exist
|
||||
self.db.create_embeddings_tables()
|
||||
|
||||
@ -368,3 +373,27 @@ class Embeddings:
|
||||
totals["status"] = "completed"
|
||||
|
||||
self.requestor.send_data(UPDATE_EMBEDDINGS_REINDEX_PROGRESS, totals)
|
||||
|
||||
def start_reindex(self) -> bool:
|
||||
"""Start reindexing in a separate thread if not already running."""
|
||||
with self.reindex_lock:
|
||||
if self.reindex_running:
|
||||
logger.warning("Reindex embeddings is already running.")
|
||||
return False
|
||||
|
||||
# Mark as running and start the thread
|
||||
self.reindex_running = True
|
||||
self.reindex_thread = threading.Thread(
|
||||
target=self._reindex_wrapper, daemon=True
|
||||
)
|
||||
self.reindex_thread.start()
|
||||
return True
|
||||
|
||||
def _reindex_wrapper(self) -> None:
|
||||
"""Wrapper to run reindex and reset running flag when done."""
|
||||
try:
|
||||
self.reindex()
|
||||
finally:
|
||||
with self.reindex_lock:
|
||||
self.reindex_running = False
|
||||
self.reindex_thread = None
|
||||
|
@ -206,6 +206,9 @@ class EmbeddingMaintainer(threading.Thread):
|
||||
self.embeddings.embed_description("", data, upsert=False),
|
||||
pack=False,
|
||||
)
|
||||
elif topic == EmbeddingsRequestEnum.reindex.value:
|
||||
response = self.embeddings.start_reindex()
|
||||
return "started" if response else "in_progress"
|
||||
|
||||
processors = [self.realtime_processors, self.post_processors]
|
||||
for processor_list in processors:
|
||||
|
@ -87,9 +87,15 @@
|
||||
"title": "Semantic Search",
|
||||
"desc": "Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one.",
|
||||
"readTheDocumentation": "Read the Documentation",
|
||||
"reindexOnStartup": {
|
||||
"label": "Re-Index On Startup",
|
||||
"desc": "Re-indexing will reprocess all thumbnails and descriptions (if enabled) and apply the embeddings on each startup. <em>Don't forget to disable the option after restarting!</em>"
|
||||
"reindexNow": {
|
||||
"label": "Reindex Now",
|
||||
"desc": "Reindexing will regenerate embeddings for all tracked object. This process runs in the background and may max out your CPU and take a fair amount of time depending on the number of tracked objects you have.",
|
||||
"confirmTitle": "Confirm Reindexing",
|
||||
"confirmDesc": "Are you sure you want to reindex all tracked object embeddings? This process will run in the background but it may max out your CPU and take a fair amount of time. You can watch the progress on the Explore page.",
|
||||
"confirmButton": "Reindex",
|
||||
"success": "Reindexing started successfully.",
|
||||
"alreadyInProgress": "Reindexing is already in progress.",
|
||||
"error": "Failed to start reindexing: {{errorMessage}}"
|
||||
},
|
||||
"modelSize": {
|
||||
"label": "Model Size",
|
||||
|
@ -21,11 +21,21 @@ import {
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
type ClassificationSettings = {
|
||||
search: {
|
||||
enabled?: boolean;
|
||||
reindex?: boolean;
|
||||
model_size?: SearchModelSize;
|
||||
};
|
||||
face: {
|
||||
@ -48,39 +58,22 @@ export default function ClassificationSettingsView({
|
||||
useSWR<FrigateConfig>("config");
|
||||
const [changedValue, setChangedValue] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isReindexDialogOpen, setIsReindexDialogOpen] = useState(false);
|
||||
|
||||
const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!;
|
||||
|
||||
const [classificationSettings, setClassificationSettings] =
|
||||
useState<ClassificationSettings>({
|
||||
search: {
|
||||
enabled: undefined,
|
||||
reindex: undefined,
|
||||
model_size: undefined,
|
||||
},
|
||||
face: {
|
||||
enabled: undefined,
|
||||
model_size: undefined,
|
||||
},
|
||||
lpr: {
|
||||
enabled: undefined,
|
||||
},
|
||||
search: { enabled: undefined, model_size: undefined },
|
||||
face: { enabled: undefined, model_size: undefined },
|
||||
lpr: { enabled: undefined },
|
||||
});
|
||||
|
||||
const [origSearchSettings, setOrigSearchSettings] =
|
||||
useState<ClassificationSettings>({
|
||||
search: {
|
||||
enabled: undefined,
|
||||
reindex: undefined,
|
||||
model_size: undefined,
|
||||
},
|
||||
face: {
|
||||
enabled: undefined,
|
||||
model_size: undefined,
|
||||
},
|
||||
lpr: {
|
||||
enabled: undefined,
|
||||
},
|
||||
search: { enabled: undefined, model_size: undefined },
|
||||
face: { enabled: undefined, model_size: undefined },
|
||||
lpr: { enabled: undefined },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@ -89,32 +82,26 @@ export default function ClassificationSettingsView({
|
||||
setClassificationSettings({
|
||||
search: {
|
||||
enabled: config.semantic_search.enabled,
|
||||
reindex: config.semantic_search.reindex,
|
||||
model_size: config.semantic_search.model_size,
|
||||
},
|
||||
face: {
|
||||
enabled: config.face_recognition.enabled,
|
||||
model_size: config.face_recognition.model_size,
|
||||
},
|
||||
lpr: {
|
||||
enabled: config.lpr.enabled,
|
||||
},
|
||||
lpr: { enabled: config.lpr.enabled },
|
||||
});
|
||||
}
|
||||
|
||||
setOrigSearchSettings({
|
||||
search: {
|
||||
enabled: config.semantic_search.enabled,
|
||||
reindex: config.semantic_search.reindex,
|
||||
model_size: config.semantic_search.model_size,
|
||||
},
|
||||
face: {
|
||||
enabled: config.face_recognition.enabled,
|
||||
model_size: config.face_recognition.model_size,
|
||||
},
|
||||
lpr: {
|
||||
enabled: config.lpr.enabled,
|
||||
},
|
||||
lpr: { enabled: config.lpr.enabled },
|
||||
});
|
||||
}
|
||||
// we know that these deps are correct
|
||||
@ -125,10 +112,7 @@ export default function ClassificationSettingsView({
|
||||
newConfig: Partial<ClassificationSettings>,
|
||||
) => {
|
||||
setClassificationSettings((prevConfig) => ({
|
||||
search: {
|
||||
...prevConfig.search,
|
||||
...newConfig.search,
|
||||
},
|
||||
search: { ...prevConfig.search, ...newConfig.search },
|
||||
face: { ...prevConfig.face, ...newConfig.face },
|
||||
lpr: { ...prevConfig.lpr, ...newConfig.lpr },
|
||||
}));
|
||||
@ -141,10 +125,8 @@ export default function ClassificationSettingsView({
|
||||
|
||||
axios
|
||||
.put(
|
||||
`config/set?semantic_search.enabled=${classificationSettings.search.enabled ? "True" : "False"}&semantic_search.reindex=${classificationSettings.search.reindex ? "True" : "False"}&semantic_search.model_size=${classificationSettings.search.model_size}&face_recognition.enabled=${classificationSettings.face.enabled ? "True" : "False"}&face_recognition.model_size=${classificationSettings.face.model_size}&lpr.enabled=${classificationSettings.lpr.enabled ? "True" : "False"}`,
|
||||
{
|
||||
requires_restart: 0,
|
||||
},
|
||||
`config/set?semantic_search.enabled=${classificationSettings.search.enabled ? "True" : "False"}&semantic_search.model_size=${classificationSettings.search.model_size}&face_recognition.enabled=${classificationSettings.face.enabled ? "True" : "False"}&face_recognition.model_size=${classificationSettings.face.model_size}&lpr.enabled=${classificationSettings.lpr.enabled ? "True" : "False"}`,
|
||||
{ requires_restart: 0 },
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
@ -156,9 +138,7 @@ export default function ClassificationSettingsView({
|
||||
} else {
|
||||
toast.error(
|
||||
t("classification.toast.error", { errorMessage: res.statusText }),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
{ position: "top-center" },
|
||||
);
|
||||
}
|
||||
})
|
||||
@ -169,9 +149,7 @@ export default function ClassificationSettingsView({
|
||||
"Unknown error";
|
||||
toast.error(
|
||||
t("toast.save.error.title", { errorMessage, ns: "common" }),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
{ position: "top-center" },
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
@ -191,6 +169,43 @@ export default function ClassificationSettingsView({
|
||||
removeMessage("search_settings", "search_settings");
|
||||
}, [origSearchSettings, removeMessage]);
|
||||
|
||||
const onReindex = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
axios
|
||||
.put("/reindex")
|
||||
.then((res) => {
|
||||
if (res.status === 202) {
|
||||
toast.success(t("classification.semanticSearch.reindexNow.success"), {
|
||||
position: "top-center",
|
||||
});
|
||||
} else {
|
||||
toast.error(
|
||||
t("classification.semanticSearch.reindexNow.error", {
|
||||
errorMessage: res.statusText,
|
||||
}),
|
||||
{ position: "top-center" },
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage =
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(
|
||||
t("classification.semanticSearch.reindexNow.error", {
|
||||
errorMessage,
|
||||
}),
|
||||
{ position: "top-center" },
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
setIsReindexDialogOpen(false);
|
||||
});
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (changedValue) {
|
||||
addMessage(
|
||||
@ -262,28 +277,18 @@ export default function ClassificationSettingsView({
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<Switch
|
||||
id="reindex"
|
||||
className="mr-3"
|
||||
disabled={classificationSettings.search.reindex === undefined}
|
||||
checked={classificationSettings.search.reindex === true}
|
||||
onCheckedChange={(isChecked) => {
|
||||
handleClassificationConfigChange({
|
||||
search: { reindex: isChecked },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="reindex">
|
||||
{t("classification.semanticSearch.reindexOnStartup.label")}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={isLoading || !classificationSettings.search.enabled}
|
||||
onClick={() => setIsReindexDialogOpen(true)}
|
||||
aria-label={t("classification.semanticSearch.reindexNow.label")}
|
||||
>
|
||||
{t("classification.semanticSearch.reindexNow.label")}
|
||||
</Button>
|
||||
<div className="mt-3 text-sm text-muted-foreground">
|
||||
<Trans ns="views/settings">
|
||||
classification.semanticSearch.reindexOnStartup.desc
|
||||
classification.semanticSearch.reindexNow.desc
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
@ -316,9 +321,7 @@ export default function ClassificationSettingsView({
|
||||
value={classificationSettings.search.model_size}
|
||||
onValueChange={(value) =>
|
||||
handleClassificationConfigChange({
|
||||
search: {
|
||||
model_size: value as SearchModelSize,
|
||||
},
|
||||
search: { model_size: value as SearchModelSize },
|
||||
})
|
||||
}
|
||||
>
|
||||
@ -346,6 +349,35 @@ export default function ClassificationSettingsView({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertDialog
|
||||
open={isReindexDialogOpen}
|
||||
onOpenChange={setIsReindexDialogOpen}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("classification.semanticSearch.reindexNow.confirmTitle")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Trans ns="views/settings">
|
||||
classification.semanticSearch.reindexNow.confirmDesc
|
||||
</Trans>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => setIsReindexDialogOpen(false)}>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onReindex}
|
||||
className={buttonVariants({ variant: "select" })}
|
||||
>
|
||||
{t("classification.semanticSearch.reindexNow.confirmButton")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<div className="my-2 space-y-6">
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user