forked from Github/frigate
Add embeddings reindex progress to the UI (#14268)
* refactor dispatcher * add reindex to dictionary * add circular progress bar component * Add progress to UI when embeddings are reindexing * readd comments to dispatcher for clarity * Only report progress every 10 events so we don't spam the logs and websocket * clean up
This commit is contained in:
@@ -2,6 +2,7 @@ import { baseUrl } from "./baseUrl";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import useWebSocket, { ReadyState } from "react-use-websocket";
|
||||
import {
|
||||
EmbeddingsReindexProgressType,
|
||||
FrigateCameraState,
|
||||
FrigateEvent,
|
||||
FrigateReview,
|
||||
@@ -302,6 +303,42 @@ export function useModelState(
|
||||
return { payload: data ? data[model] : undefined };
|
||||
}
|
||||
|
||||
export function useEmbeddingsReindexProgress(
|
||||
revalidateOnFocus: boolean = true,
|
||||
): {
|
||||
payload: EmbeddingsReindexProgressType;
|
||||
} {
|
||||
const {
|
||||
value: { payload },
|
||||
send: sendCommand,
|
||||
} = useWs("embeddings_reindex_progress", "embeddingsReindexProgress");
|
||||
|
||||
const data = useDeepMemo(JSON.parse(payload as string));
|
||||
|
||||
useEffect(() => {
|
||||
let listener = undefined;
|
||||
if (revalidateOnFocus) {
|
||||
sendCommand("embeddingsReindexProgress");
|
||||
listener = () => {
|
||||
if (document.visibilityState == "visible") {
|
||||
sendCommand("embeddingsReindexProgress");
|
||||
}
|
||||
};
|
||||
addEventListener("visibilitychange", listener);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (listener) {
|
||||
removeEventListener("visibilitychange", listener);
|
||||
}
|
||||
};
|
||||
// we know that these deps are correct
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [revalidateOnFocus]);
|
||||
|
||||
return { payload: data };
|
||||
}
|
||||
|
||||
export function useMotionActivity(camera: string): { payload: string } {
|
||||
const {
|
||||
value: { payload },
|
||||
|
||||
108
web/src/components/ui/circular-progress-bar.tsx
Normal file
108
web/src/components/ui/circular-progress-bar.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
max: number;
|
||||
value: number;
|
||||
min: number;
|
||||
gaugePrimaryColor: string;
|
||||
gaugeSecondaryColor: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function AnimatedCircularProgressBar({
|
||||
max = 100,
|
||||
min = 0,
|
||||
value = 0,
|
||||
gaugePrimaryColor,
|
||||
gaugeSecondaryColor,
|
||||
className,
|
||||
}: Props) {
|
||||
const circumference = 2 * Math.PI * 45;
|
||||
const percentPx = circumference / 100;
|
||||
const currentPercent = Math.floor(((value - min) / (max - min)) * 100);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("relative size-40 text-2xl font-semibold", className)}
|
||||
style={
|
||||
{
|
||||
"--circle-size": "100px",
|
||||
"--circumference": circumference,
|
||||
"--percent-to-px": `${percentPx}px`,
|
||||
"--gap-percent": "5",
|
||||
"--offset-factor": "0",
|
||||
"--transition-length": "1s",
|
||||
"--transition-step": "200ms",
|
||||
"--delay": "0s",
|
||||
"--percent-to-deg": "3.6deg",
|
||||
transform: "translateZ(0)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
className="size-full"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 100 100"
|
||||
>
|
||||
{currentPercent <= 90 && currentPercent >= 0 && (
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="45"
|
||||
strokeWidth="10"
|
||||
strokeDashoffset="0"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="opacity-100"
|
||||
style={
|
||||
{
|
||||
stroke: gaugeSecondaryColor,
|
||||
"--stroke-percent": 90 - currentPercent,
|
||||
"--offset-factor-secondary": "calc(1 - var(--offset-factor))",
|
||||
strokeDasharray:
|
||||
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
||||
transform:
|
||||
"rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
|
||||
transition: "all var(--transition-length) ease var(--delay)",
|
||||
transformOrigin:
|
||||
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="45"
|
||||
strokeWidth="10"
|
||||
strokeDashoffset="0"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="opacity-100"
|
||||
style={
|
||||
{
|
||||
stroke: gaugePrimaryColor,
|
||||
"--stroke-percent": currentPercent,
|
||||
strokeDasharray:
|
||||
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
||||
transition:
|
||||
"var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
|
||||
transitionProperty: "stroke-dasharray,transform",
|
||||
transform:
|
||||
"rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))",
|
||||
transformOrigin:
|
||||
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
data-current-value={currentPercent}
|
||||
className="duration-[var(--transition-length)] delay-[var(--delay)] absolute inset-0 m-auto size-fit ease-linear animate-in fade-in"
|
||||
>
|
||||
{currentPercent}%
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useEventUpdate, useModelState } from "@/api/ws";
|
||||
import {
|
||||
useEmbeddingsReindexProgress,
|
||||
useEventUpdate,
|
||||
useModelState,
|
||||
} from "@/api/ws";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import AnimatedCircularProgressBar from "@/components/ui/circular-progress-bar";
|
||||
import { useApiFilterArgs } from "@/hooks/use-api-filter";
|
||||
import { useTimezone } from "@/hooks/use-date-utils";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
@@ -182,6 +187,18 @@ export default function Explore() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [eventUpdate]);
|
||||
|
||||
// embeddings reindex progress
|
||||
|
||||
const { payload: reindexProgress } = useEmbeddingsReindexProgress();
|
||||
|
||||
const embeddingsReindexing = useMemo(
|
||||
() =>
|
||||
reindexProgress
|
||||
? reindexProgress.total_objects - reindexProgress.processed_objects > 0
|
||||
: undefined,
|
||||
[reindexProgress],
|
||||
);
|
||||
|
||||
// model states
|
||||
|
||||
const { payload: textModelState } = useModelState(
|
||||
@@ -238,59 +255,101 @@ export default function Explore() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{config?.semantic_search.enabled && !allModelsLoaded ? (
|
||||
{config?.semantic_search.enabled &&
|
||||
(!allModelsLoaded || embeddingsReindexing) ? (
|
||||
<div className="absolute inset-0 left-1/2 top-1/2 flex h-96 w-96 -translate-x-1/2 -translate-y-1/2">
|
||||
<div className="flex flex-col items-center justify-center space-y-3 rounded-lg bg-background/50 p-5">
|
||||
<div className="flex max-w-96 flex-col items-center justify-center space-y-3 rounded-lg bg-background/50 p-5">
|
||||
<div className="my-5 flex flex-col items-center gap-2 text-xl">
|
||||
<TbExclamationCircle className="mb-3 size-10" />
|
||||
<div>Search Unavailable</div>
|
||||
</div>
|
||||
<div className="max-w-96 text-center">
|
||||
Frigate is downloading the necessary embeddings models to support
|
||||
semantic searching. This may take several minutes depending on the
|
||||
speed of your network connection.
|
||||
</div>
|
||||
<div className="flex w-96 flex-col gap-2 py-5">
|
||||
<div className="flex flex-row items-center justify-center gap-2">
|
||||
{renderModelStateIcon(visionModelState)}
|
||||
Vision model
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center gap-2">
|
||||
{renderModelStateIcon(visionFeatureExtractorState)}
|
||||
Vision model feature extractor
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center gap-2">
|
||||
{renderModelStateIcon(textModelState)}
|
||||
Text model
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center gap-2">
|
||||
{renderModelStateIcon(textTokenizerState)}
|
||||
Text tokenizer
|
||||
</div>
|
||||
</div>
|
||||
{(textModelState === "error" ||
|
||||
textTokenizerState === "error" ||
|
||||
visionModelState === "error" ||
|
||||
visionFeatureExtractorState === "error") && (
|
||||
<div className="my-3 max-w-96 text-center text-danger">
|
||||
An error has occurred. Check Frigate logs.
|
||||
</div>
|
||||
{embeddingsReindexing && (
|
||||
<>
|
||||
<div className="text-center text-primary-variant">
|
||||
Search can be used after tracked object embeddings have
|
||||
finished reindexing.
|
||||
</div>
|
||||
<div className="pt-5 text-center">
|
||||
<AnimatedCircularProgressBar
|
||||
min={0}
|
||||
max={reindexProgress.total_objects}
|
||||
value={reindexProgress.processed_objects}
|
||||
gaugePrimaryColor="hsl(var(--selected))"
|
||||
gaugeSecondaryColor="hsl(var(--secondary))"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-96 flex-col gap-2 py-5">
|
||||
<div className="flex flex-row items-center justify-center gap-3">
|
||||
<span className="text-primary-variant">
|
||||
Thumbnails embedded:
|
||||
</span>
|
||||
{reindexProgress.thumbnails}
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center gap-3">
|
||||
<span className="text-primary-variant">
|
||||
Descriptions embedded:
|
||||
</span>
|
||||
{reindexProgress.descriptions}
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center gap-3">
|
||||
<span className="text-primary-variant">
|
||||
Tracked objects processed:
|
||||
</span>
|
||||
{reindexProgress.processed_objects}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!allModelsLoaded && (
|
||||
<>
|
||||
<div className="text-center text-primary-variant">
|
||||
Frigate is downloading the necessary embeddings models to
|
||||
support semantic searching. This may take several minutes
|
||||
depending on the speed of your network connection.
|
||||
</div>
|
||||
<div className="flex w-96 flex-col gap-2 py-5">
|
||||
<div className="flex flex-row items-center justify-center gap-2">
|
||||
{renderModelStateIcon(visionModelState)}
|
||||
Vision model
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center gap-2">
|
||||
{renderModelStateIcon(visionFeatureExtractorState)}
|
||||
Vision model feature extractor
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center gap-2">
|
||||
{renderModelStateIcon(textModelState)}
|
||||
Text model
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center gap-2">
|
||||
{renderModelStateIcon(textTokenizerState)}
|
||||
Text tokenizer
|
||||
</div>
|
||||
</div>
|
||||
{(textModelState === "error" ||
|
||||
textTokenizerState === "error" ||
|
||||
visionModelState === "error" ||
|
||||
visionFeatureExtractorState === "error") && (
|
||||
<div className="my-3 max-w-96 text-center text-danger">
|
||||
An error has occurred. Check Frigate logs.
|
||||
</div>
|
||||
)}
|
||||
<div className="text-center text-primary-variant">
|
||||
You may want to reindex the embeddings of your tracked objects
|
||||
once the models are downloaded.
|
||||
</div>
|
||||
<div className="flex items-center text-primary-variant">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/semantic_search"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Read the documentation{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="max-w-96 text-center">
|
||||
You may want to reindex the embeddings of your tracked objects
|
||||
once the models are downloaded.
|
||||
</div>
|
||||
<div className="flex max-w-96 items-center text-primary-variant">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/semantic_search"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Read the documentation{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -62,4 +62,11 @@ export type ModelState =
|
||||
| "downloaded"
|
||||
| "error";
|
||||
|
||||
export type EmbeddingsReindexProgressType = {
|
||||
thumbnails: number;
|
||||
descriptions: number;
|
||||
processed_objects: number;
|
||||
total_objects: number;
|
||||
};
|
||||
|
||||
export type ToggleableSetting = "ON" | "OFF";
|
||||
|
||||
Reference in New Issue
Block a user