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:
Josh Hawkins
2024-10-10 14:28:43 -05:00
committed by GitHub
parent 8ade85edec
commit f67ec241d4
8 changed files with 397 additions and 99 deletions

View File

@@ -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 },

View 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>
);
}

View File

@@ -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>
) : (

View File

@@ -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";