mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-26 19:58:58 +03:00
Add ability to manually run Review Descriptions from the UI / API (#24222)
* Add ability to manually run Review Descriptions from the UI / API * Fix not handling None type for call
This commit is contained in:
@@ -21,7 +21,9 @@ import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import { useReviewDescriptions } from "@/hooks/use-review-descriptions";
|
||||
import MultiExportDialog from "../overlay/MultiExportDialog";
|
||||
import { MdAutoAwesome } from "react-icons/md";
|
||||
|
||||
type ReviewActionGroupProps = {
|
||||
selectedReviews: ReviewSegment[];
|
||||
@@ -45,6 +47,13 @@ export default function ReviewActionGroup({
|
||||
(review) => review.has_been_reviewed,
|
||||
);
|
||||
|
||||
const { canGenerateDescription, generateDescription } =
|
||||
useReviewDescriptions();
|
||||
|
||||
// only a single item can be sent through the descriptions process at a time
|
||||
const showGenerateDescription =
|
||||
selectedReviews.length == 1 && canGenerateDescription(selectedReviews[0]);
|
||||
|
||||
const onToggleReviewed = useCallback(async () => {
|
||||
const ids = selectedReviews.map((review) => review.id);
|
||||
await axios.post(`reviews/viewed`, {
|
||||
@@ -166,6 +175,24 @@ export default function ReviewActionGroup({
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{showGenerateDescription && (
|
||||
<Button
|
||||
className="flex items-center gap-2 p-2"
|
||||
aria-label={t("recording.button.generateDescription")}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
generateDescription(selectedReviews[0]);
|
||||
onClearSelected();
|
||||
}}
|
||||
>
|
||||
<MdAutoAwesome className="text-secondary-foreground" />
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
{t("recording.button.generateDescription")}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{selectedReviews.length >= 2 &&
|
||||
selectedReviews.length <= MAX_BATCH_EXPORT_ITEMS && (
|
||||
<MultiExportDialog
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import axios from "axios";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
import { useIsAdmin } from "./use-is-admin";
|
||||
import { GenAIRolesResponse } from "@/types/chat";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { ReviewSegment } from "@/types/review";
|
||||
|
||||
// recordings are the only source used for on demand runs, and their frames
|
||||
// cost far more tokens than preview frames do
|
||||
export const MIN_REVIEW_DESCRIPTION_CONTEXT = 32000;
|
||||
|
||||
/**
|
||||
* Gating and dispatch for re-running a review item through GenAI descriptions.
|
||||
*/
|
||||
export function useReviewDescriptions() {
|
||||
const { t } = useTranslation(["components/dialog"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
const { data: genaiRoles } = useSWR<GenAIRolesResponse>(
|
||||
isAdmin ? "genai/roles" : null,
|
||||
{ revalidateOnFocus: false },
|
||||
);
|
||||
|
||||
const hasSufficientContext = useMemo(
|
||||
() =>
|
||||
(genaiRoles?.descriptions?.context_size ?? 0) >=
|
||||
MIN_REVIEW_DESCRIPTION_CONTEXT,
|
||||
[genaiRoles],
|
||||
);
|
||||
|
||||
const canGenerateDescription = useCallback(
|
||||
(review: ReviewSegment) =>
|
||||
isAdmin &&
|
||||
hasSufficientContext &&
|
||||
!!review.end_time &&
|
||||
!!config?.cameras[review.camera]?.review?.genai?.enabled,
|
||||
[config, hasSufficientContext, isAdmin],
|
||||
);
|
||||
|
||||
const generateDescription = useCallback(
|
||||
(review: ReviewSegment) => {
|
||||
axios
|
||||
.put(`review/${review.id}/regenerate_description`)
|
||||
.then(() => {
|
||||
toast.success(t("recording.genaiDescription.toast.success"), {
|
||||
position: "top-center",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage =
|
||||
error.response?.data?.message || error.message || "Unknown error";
|
||||
toast.error(
|
||||
t("recording.genaiDescription.toast.error", {
|
||||
error: errorMessage,
|
||||
}),
|
||||
{ position: "top-center" },
|
||||
);
|
||||
});
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
return { canGenerateDescription, generateDescription };
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GenAIRole } from "@/types/frigateConfig";
|
||||
export type ToolCallFunction = {
|
||||
name: string;
|
||||
arguments: string;
|
||||
@@ -57,3 +58,11 @@ export type GenAIProviderInfo = {
|
||||
};
|
||||
|
||||
export type GenAIModelsResponse = Record<string, GenAIProviderInfo>;
|
||||
|
||||
export type GenAIRoleInfo = {
|
||||
name: string;
|
||||
model: string;
|
||||
context_size: number;
|
||||
};
|
||||
|
||||
export type GenAIRolesResponse = Partial<Record<GenAIRole, GenAIRoleInfo>>;
|
||||
|
||||
Reference in New Issue
Block a user