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:
Nicolas Mowen 2026-05-10 11:09:28 -06:00 committed by GitHub
parent 0d4f1ec369
commit c4b74c9148
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 48 additions and 30 deletions

View File

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

View File

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

View File

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

View File

@ -380,7 +380,12 @@ class RecordingExporter(threading.Thread):
if label and label not in labels: if label and label not in labels:
labels.append(label) labels.append(label)
metadata = data.get("metadata") or {}
title = metadata.get("title")
if not title:
title = str(review.severity).capitalize() title = str(review.severity).capitalize()
if labels: if labels:
title = f"{title}: {', '.join(labels)}" title = f"{title}: {', '.join(labels)}"

View File

@ -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)",

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,6 +29,7 @@ export const supportedLanguageKeys = [
"tr", "tr",
"pl", "pl",
"hr", "hr",
"bs",
"sk", "sk",
"sl", "sl",
"lt", "lt",

View File

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