mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-11 16:05:26 +03:00
Miscellaneous fixes (#23155)
* Change order * Improve title * add loading spinner to exports * Simplify JSON since not all providers see or use this the same * Add fields to primary prompt * Adjust centering for no overrides * Use GenAI title for exports when available * detect form-root objects by field path instead of schema identity * add bosnian * Strip v1 if included in url * prevent fast clicks in video controls from selecting text * Use title for metadata chapters --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
parent
0d4f1ec369
commit
c4b74c9148
@ -2,7 +2,7 @@ from typing import Annotated
|
|||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, StringConstraints
|
from pydantic import BaseModel, ConfigDict, Field, StringConstraints
|
||||||
|
|
||||||
ObservationItem = Annotated[str, StringConstraints(min_length=20, max_length=160)]
|
ObservationItem = Annotated[str, StringConstraints(min_length=20, max_length=200)]
|
||||||
|
|
||||||
|
|
||||||
class ReviewMetadata(BaseModel):
|
class ReviewMetadata(BaseModel):
|
||||||
@ -11,33 +11,22 @@ class ReviewMetadata(BaseModel):
|
|||||||
observations: list[ObservationItem] = Field(
|
observations: list[ObservationItem] = Field(
|
||||||
...,
|
...,
|
||||||
min_length=3,
|
min_length=3,
|
||||||
max_length=15,
|
max_length=8,
|
||||||
description=(
|
description="Enumerate the significant observations across all frames, in chronological order.",
|
||||||
"Enumerate the significant observations across all frames, in "
|
|
||||||
"chronological order, BEFORE composing the scene narrative. "
|
|
||||||
"Include the very start of the activity — for example, a vehicle "
|
|
||||||
"entering the frame or pulling into the driveway — even if it "
|
|
||||||
"lasts only a few frames and the rest of the clip is dominated "
|
|
||||||
"by a longer activity. Include each arrival, departure, motion "
|
|
||||||
"event, object handled, and notable change in position or state. "
|
|
||||||
"Each item is a single concrete fact written as a complete "
|
|
||||||
"sentence. Do not summarize, interpret, or assign meaning here — "
|
|
||||||
"that belongs in the scene field."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
title: str = Field(
|
|
||||||
max_length=80,
|
|
||||||
description="Under 10 words. Name the apparent purpose or outcome of the activity together with the location involved. Do not narrate or list the sequence of actions step by step.",
|
|
||||||
)
|
)
|
||||||
scene: str = Field(
|
scene: str = Field(
|
||||||
min_length=150,
|
min_length=150,
|
||||||
max_length=600,
|
max_length=600,
|
||||||
description="A chronological narrative of what happens from start to finish, drawing directly from the items in observations.",
|
description="A chronological narrative of what happens from start to finish, drawing directly from the items in observations.",
|
||||||
)
|
)
|
||||||
|
title: str = Field(
|
||||||
|
max_length=80,
|
||||||
|
description="Title for the activity.",
|
||||||
|
)
|
||||||
shortSummary: str = Field(
|
shortSummary: str = Field(
|
||||||
min_length=70,
|
min_length=70,
|
||||||
max_length=120,
|
max_length=140,
|
||||||
description="A brief 2-sentence summary of the scene, suitable for notifications.",
|
description="A brief summary for the activity.",
|
||||||
)
|
)
|
||||||
confidence: float = Field(
|
confidence: float = Field(
|
||||||
ge=0.0,
|
ge=0.0,
|
||||||
|
|||||||
@ -108,10 +108,11 @@ When forming your description:
|
|||||||
## Response Field Guidelines
|
## Response Field Guidelines
|
||||||
|
|
||||||
Respond with a JSON object matching the provided schema. Field-specific guidance:
|
Respond with a JSON object matching the provided schema. Field-specific guidance:
|
||||||
|
- `observations`: Include the very start of the activity — for example, a vehicle entering the frame or pulling into the driveway — even if it lasts only a few frames and the rest of the clip is dominated by a longer activity. Include each arrival, departure, object handled, and notable change in position or state. Each item is a single concrete fact written as a complete sentence.
|
||||||
- `scene`: Describe how the sequence begins, then the progression of events — all significant movements and actions in order. For example, if a vehicle arrives and then a person exits, describe both sequentially. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name — do not replace them with generic terms. For unnamed objects (e.g., "person", "car"), refer to them naturally with articles (e.g., "a person", "the car"). Your description should align with and support the threat level you assign.
|
- `scene`: Describe how the sequence begins, then the progression of events — all significant movements and actions in order. For example, if a vehicle arrives and then a person exits, describe both sequentially. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name — do not replace them with generic terms. For unnamed objects (e.g., "person", "car"), refer to them naturally with articles (e.g., "a person", "the car"). Your description should align with and support the threat level you assign.
|
||||||
- `title`: Characterize **what took place and where** — interpret the overall purpose or outcome, do not simply compress the scene description into fewer words. Include the relevant location (zone, area, or entry point). For named subjects, always use their name. For unnamed objects, refer to them naturally with articles. No editorial qualifiers like "routine" or "suspicious."
|
- `title`: Name the primary activity across the observations, together with the location. An activity is what is being done with objects, tools, or surfaces; locomotion through the scene qualifies as the activity only when no other interaction is observed. For named subjects, always use their name. For unnamed objects, refer to them naturally with articles.
|
||||||
|
- `shortSummary`: Briefly summarize the primary activity across the observations.
|
||||||
- `potential_threat_level`: Must be consistent with your scene description and the activity patterns above.
|
- `potential_threat_level`: Must be consistent with your scene description and the activity patterns above.
|
||||||
{get_concern_prompt()}
|
|
||||||
|
|
||||||
## Sequence Details
|
## Sequence Details
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,8 @@ class LlamaCppClient(GenAIClient):
|
|||||||
|
|
||||||
if base_url is None:
|
if base_url is None:
|
||||||
return None
|
return None
|
||||||
|
else:
|
||||||
|
base_url = base_url.replace("/v1", "") # Strip /v1 if included in base_url
|
||||||
|
|
||||||
configured_model = self.genai_config.model
|
configured_model = self.genai_config.model
|
||||||
|
|
||||||
|
|||||||
@ -380,9 +380,14 @@ class RecordingExporter(threading.Thread):
|
|||||||
if label and label not in labels:
|
if label and label not in labels:
|
||||||
labels.append(label)
|
labels.append(label)
|
||||||
|
|
||||||
title = str(review.severity).capitalize()
|
metadata = data.get("metadata") or {}
|
||||||
if labels:
|
title = metadata.get("title")
|
||||||
title = f"{title}: {', '.join(labels)}"
|
|
||||||
|
if not title:
|
||||||
|
title = str(review.severity).capitalize()
|
||||||
|
|
||||||
|
if labels:
|
||||||
|
title = f"{title}: {', '.join(labels)}"
|
||||||
|
|
||||||
chapter_blocks.append(
|
chapter_blocks.append(
|
||||||
"[CHAPTER]\n"
|
"[CHAPTER]\n"
|
||||||
|
|||||||
@ -207,6 +207,7 @@
|
|||||||
"th": "ไทย (Thai)",
|
"th": "ไทย (Thai)",
|
||||||
"ca": "Català (Catalan)",
|
"ca": "Català (Catalan)",
|
||||||
"hr": "Hrvatski (Croatian)",
|
"hr": "Hrvatski (Croatian)",
|
||||||
|
"bs": "Bosanski (Bosnian)",
|
||||||
"sr": "Српски (Serbian)",
|
"sr": "Српски (Serbian)",
|
||||||
"sl": "Slovenščina (Slovenian)",
|
"sl": "Slovenščina (Slovenian)",
|
||||||
"lt": "Lietuvių (Lithuanian)",
|
"lt": "Lietuvių (Lithuanian)",
|
||||||
|
|||||||
@ -79,10 +79,15 @@ export default function ReviewCard({
|
|||||||
? event.end_time + REVIEW_PADDING
|
? event.end_time + REVIEW_PADDING
|
||||||
: Date.now() / 1000;
|
: Date.now() / 1000;
|
||||||
|
|
||||||
|
const genAiTitle = event.data.metadata?.title?.trim();
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post(
|
.post(
|
||||||
`export/${event.camera}/start/${event.start_time - REVIEW_PADDING}/end/${endTime}`,
|
`export/${event.camera}/start/${event.start_time - REVIEW_PADDING}/end/${endTime}`,
|
||||||
{ playback: "realtime" },
|
{
|
||||||
|
playback: "realtime",
|
||||||
|
...(genAiTitle ? { name: genAiTitle } : {}),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status < 300) {
|
if (response.status < 300) {
|
||||||
|
|||||||
@ -52,12 +52,11 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
} = props;
|
} = props;
|
||||||
const formContext = registry?.formContext as ConfigFormContext | undefined;
|
const formContext = registry?.formContext as ConfigFormContext | undefined;
|
||||||
|
|
||||||
// Check if this is a root-level object
|
|
||||||
const isRoot = registry?.rootSchema === schema;
|
|
||||||
const overrides = formContext?.overrides;
|
const overrides = formContext?.overrides;
|
||||||
const baselineFormData = formContext?.baselineFormData;
|
const baselineFormData = formContext?.baselineFormData;
|
||||||
const hiddenFields = formContext?.hiddenFields;
|
const hiddenFields = formContext?.hiddenFields;
|
||||||
const fieldPath = props.fieldPathId.path;
|
const fieldPath = props.fieldPathId.path;
|
||||||
|
const isRoot = fieldPath.length === 0;
|
||||||
const restartRequired = formContext?.restartRequired;
|
const restartRequired = formContext?.restartRequired;
|
||||||
const defaultRequiresRestart = formContext?.requiresRestart ?? true;
|
const defaultRequiresRestart = formContext?.requiresRestart ?? true;
|
||||||
|
|
||||||
|
|||||||
@ -178,6 +178,7 @@ export default function MultiExportDialog({
|
|||||||
start_time: review.start_time - REVIEW_PADDING,
|
start_time: review.start_time - REVIEW_PADDING,
|
||||||
end_time: (review.end_time ?? Date.now() / 1000) + REVIEW_PADDING,
|
end_time: (review.end_time ?? Date.now() / 1000) + REVIEW_PADDING,
|
||||||
image_path: review.thumb_path || undefined,
|
image_path: review.thumb_path || undefined,
|
||||||
|
friendly_name: review.data.metadata?.title?.trim() || undefined,
|
||||||
client_item_id: review.id,
|
client_item_id: review.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -198,7 +198,7 @@ export default function VideoControls({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 flex w-auto items-center justify-between gap-4 rounded-lg bg-background/60 px-4 py-2 text-primary sm:flex-nowrap sm:gap-8",
|
"z-50 flex w-auto select-none items-center justify-between gap-4 rounded-lg bg-background/60 px-4 py-2 text-primary sm:flex-nowrap sm:gap-8",
|
||||||
className,
|
className,
|
||||||
isMobileOnly &&
|
isMobileOnly &&
|
||||||
Object.values(features).filter((feat) => feat).length >
|
Object.values(features).filter((feat) => feat).length >
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export function ProfileSectionDropdown({
|
|||||||
className="group flex items-start justify-between gap-2"
|
className="group flex items-start justify-between gap-2"
|
||||||
onClick={() => onSelectProfile(profile)}
|
onClick={() => onSelectProfile(profile)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-start gap-2">
|
||||||
<div className="flex w-full flex-row items-center justify-start gap-2">
|
<div className="flex w-full flex-row items-center justify-start gap-2">
|
||||||
{isActive && <Check className="h-3.5 w-3.5 shrink-0" />}
|
{isActive && <Check className="h-3.5 w-3.5 shrink-0" />}
|
||||||
<span
|
<span
|
||||||
|
|||||||
@ -38,6 +38,7 @@ const localeMap: Record<string, () => Promise<Locale>> = {
|
|||||||
th: () => import("date-fns/locale/th").then((module) => module.th),
|
th: () => import("date-fns/locale/th").then((module) => module.th),
|
||||||
ca: () => import("date-fns/locale/ca").then((module) => module.ca),
|
ca: () => import("date-fns/locale/ca").then((module) => module.ca),
|
||||||
hr: () => import("date-fns/locale/hr").then((module) => module.hr),
|
hr: () => import("date-fns/locale/hr").then((module) => module.hr),
|
||||||
|
bs: () => import("date-fns/locale/bs").then((module) => module.bs),
|
||||||
sl: () => import("date-fns/locale/sl").then((module) => module.sl),
|
sl: () => import("date-fns/locale/sl").then((module) => module.sl),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export const supportedLanguageKeys = [
|
|||||||
"tr",
|
"tr",
|
||||||
"pl",
|
"pl",
|
||||||
"hr",
|
"hr",
|
||||||
|
"bs",
|
||||||
"sk",
|
"sk",
|
||||||
"sl",
|
"sl",
|
||||||
"lt",
|
"lt",
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
CaseCard,
|
CaseCard,
|
||||||
ExportCard,
|
ExportCard,
|
||||||
} from "@/components/card/ExportCard";
|
} from "@/components/card/ExportCard";
|
||||||
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
@ -864,6 +865,7 @@ function Exports() {
|
|||||||
search={search}
|
search={search}
|
||||||
selectedExports={selectedExports}
|
selectedExports={selectedExports}
|
||||||
selectionMode={selectionMode}
|
selectionMode={selectionMode}
|
||||||
|
isLoading={cases === undefined || rawExports === undefined}
|
||||||
onSelectExport={onSelectExport}
|
onSelectExport={onSelectExport}
|
||||||
setSelected={setSelected}
|
setSelected={setSelected}
|
||||||
renameClip={onHandleRename}
|
renameClip={onHandleRename}
|
||||||
@ -882,6 +884,7 @@ function Exports() {
|
|||||||
activeJobs={activeJobsByCase["none"] || []}
|
activeJobs={activeJobsByCase["none"] || []}
|
||||||
selectedExports={selectedExports}
|
selectedExports={selectedExports}
|
||||||
selectionMode={selectionMode}
|
selectionMode={selectionMode}
|
||||||
|
isLoading={cases === undefined || rawExports === undefined}
|
||||||
onSelectExport={onSelectExport}
|
onSelectExport={onSelectExport}
|
||||||
setSelectedCaseId={setSelectedCaseId}
|
setSelectedCaseId={setSelectedCaseId}
|
||||||
setSelected={setSelected}
|
setSelected={setSelected}
|
||||||
@ -903,6 +906,7 @@ type AllExportsViewProps = {
|
|||||||
activeJobs: ExportJob[];
|
activeJobs: ExportJob[];
|
||||||
selectedExports: Export[];
|
selectedExports: Export[];
|
||||||
selectionMode: boolean;
|
selectionMode: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
onSelectExport: (e: Export) => void;
|
onSelectExport: (e: Export) => void;
|
||||||
setSelectedCaseId: (id: string) => void;
|
setSelectedCaseId: (id: string) => void;
|
||||||
setSelected: (e: Export) => void;
|
setSelected: (e: Export) => void;
|
||||||
@ -919,6 +923,7 @@ function AllExportsView({
|
|||||||
activeJobs,
|
activeJobs,
|
||||||
selectedExports,
|
selectedExports,
|
||||||
selectionMode,
|
selectionMode,
|
||||||
|
isLoading,
|
||||||
onSelectExport,
|
onSelectExport,
|
||||||
setSelectedCaseId,
|
setSelectedCaseId,
|
||||||
setSelected,
|
setSelected,
|
||||||
@ -1027,6 +1032,8 @@ function AllExportsView({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
) : isLoading ? (
|
||||||
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
||||||
<LuFolderX className="size-16" />
|
<LuFolderX className="size-16" />
|
||||||
@ -1046,6 +1053,7 @@ type CaseViewProps = {
|
|||||||
search: string;
|
search: string;
|
||||||
selectedExports: Export[];
|
selectedExports: Export[];
|
||||||
selectionMode: boolean;
|
selectionMode: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
onSelectExport: (e: Export) => void;
|
onSelectExport: (e: Export) => void;
|
||||||
setSelected: (e: Export) => void;
|
setSelected: (e: Export) => void;
|
||||||
renameClip: (id: string, update: string) => void;
|
renameClip: (id: string, update: string) => void;
|
||||||
@ -1063,6 +1071,7 @@ function CaseView({
|
|||||||
search,
|
search,
|
||||||
selectedExports,
|
selectedExports,
|
||||||
selectionMode,
|
selectionMode,
|
||||||
|
isLoading,
|
||||||
onSelectExport,
|
onSelectExport,
|
||||||
setSelected,
|
setSelected,
|
||||||
renameClip,
|
renameClip,
|
||||||
@ -1201,6 +1210,10 @@ function CaseView({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : isLoading ? (
|
||||||
|
<div className="flex min-h-[16rem] flex-1 items-center justify-center">
|
||||||
|
<ActivityIndicator />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex min-h-[16rem] flex-col items-center justify-center p-6 text-center">
|
<div className="flex min-h-[16rem] flex-col items-center justify-center p-6 text-center">
|
||||||
<LuFolderX className="size-12" />
|
<LuFolderX className="size-12" />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user