Compare commits

...

11 Commits

Author SHA1 Message Date
Josh Hawkins
da1671b6eb prevent fast clicks in video controls from selecting text 2026-05-09 20:29:52 -05:00
Nicolas Mowen
ba5e4119cb Strip v1 if included in url 2026-05-09 14:10:08 -06:00
Josh Hawkins
44f4c6b9e3 add bosnian 2026-05-09 08:56:30 -05:00
Josh Hawkins
7bcb5a5319 detect form-root objects by field path instead of schema identity 2026-05-09 08:51:36 -05:00
Nicolas Mowen
bf1e2daa76 Use GenAI title for exports when available 2026-05-08 14:00:31 -06:00
Nicolas Mowen
2a4d3a5eb1 Adjust centering for no overrides 2026-05-08 12:30:41 -06:00
Nicolas Mowen
a1a9fd82a7 Add fields to primary prompt 2026-05-08 08:15:49 -06:00
Nicolas Mowen
733c50c6e8 Simplify JSON since not all providers see or use this the same 2026-05-08 08:15:49 -06:00
Josh Hawkins
7f3118f1ef add loading spinner to exports 2026-05-08 08:13:45 -05:00
Nicolas Mowen
95e8603055 Improve title 2026-05-07 21:05:34 -06:00
Nicolas Mowen
02ef484972 Change order 2026-05-07 21:02:53 -06:00
12 changed files with 40 additions and 27 deletions

View File

@ -2,7 +2,7 @@ from typing import Annotated
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):
@ -11,33 +11,22 @@ class ReviewMetadata(BaseModel):
observations: list[ObservationItem] = Field(
...,
min_length=3,
max_length=15,
description=(
"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.",
max_length=8,
description="Enumerate the significant observations across all frames, in chronological order.",
)
scene: str = Field(
min_length=150,
max_length=600,
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(
min_length=70,
max_length=120,
description="A brief 2-sentence summary of the scene, suitable for notifications.",
max_length=140,
description="A brief summary for the activity.",
)
confidence: float = Field(
ge=0.0,

View File

@ -108,10 +108,11 @@ When forming your description:
## Response Field Guidelines
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.
- `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.
{get_concern_prompt()}
## Sequence Details

View File

@ -67,6 +67,8 @@ class LlamaCppClient(GenAIClient):
if base_url is None:
return None
else:
base_url = base_url.replace("/v1", "") # Strip /v1 if included in base_url
configured_model = self.genai_config.model

View File

@ -207,6 +207,7 @@
"th": "ไทย (Thai)",
"ca": "Català (Catalan)",
"hr": "Hrvatski (Croatian)",
"bs": "Bosanski (Bosnian)",
"sr": "Српски (Serbian)",
"sl": "Slovenščina (Slovenian)",
"lt": "Lietuvių (Lithuanian)",

View File

@ -79,10 +79,15 @@ export default function ReviewCard({
? event.end_time + REVIEW_PADDING
: Date.now() / 1000;
const genAiTitle = event.data.metadata?.title?.trim();
axios
.post(
`export/${event.camera}/start/${event.start_time - REVIEW_PADDING}/end/${endTime}`,
{ playback: "realtime" },
{
playback: "realtime",
...(genAiTitle ? { name: genAiTitle } : {}),
},
)
.then((response) => {
if (response.status < 300) {

View File

@ -52,12 +52,11 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
} = props;
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 baselineFormData = formContext?.baselineFormData;
const hiddenFields = formContext?.hiddenFields;
const fieldPath = props.fieldPathId.path;
const isRoot = fieldPath.length === 0;
const restartRequired = formContext?.restartRequired;
const defaultRequiresRestart = formContext?.requiresRestart ?? true;

View File

@ -178,6 +178,7 @@ export default function MultiExportDialog({
start_time: review.start_time - REVIEW_PADDING,
end_time: (review.end_time ?? Date.now() / 1000) + REVIEW_PADDING,
image_path: review.thumb_path || undefined,
friendly_name: review.data.metadata?.title?.trim() || undefined,
client_item_id: review.id,
}));

View File

@ -198,7 +198,7 @@ export default function VideoControls({
return (
<div
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,
isMobileOnly &&
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"
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">
{isActive && <Check className="h-3.5 w-3.5 shrink-0" />}
<span

View File

@ -38,6 +38,7 @@ const localeMap: Record<string, () => Promise<Locale>> = {
th: () => import("date-fns/locale/th").then((module) => module.th),
ca: () => import("date-fns/locale/ca").then((module) => module.ca),
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),
};

View File

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

View File

@ -5,6 +5,7 @@ import {
CaseCard,
ExportCard,
} from "@/components/card/ExportCard";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import {
AlertDialog,
AlertDialogCancel,
@ -864,6 +865,7 @@ function Exports() {
search={search}
selectedExports={selectedExports}
selectionMode={selectionMode}
isLoading={cases === undefined || rawExports === undefined}
onSelectExport={onSelectExport}
setSelected={setSelected}
renameClip={onHandleRename}
@ -882,6 +884,7 @@ function Exports() {
activeJobs={activeJobsByCase["none"] || []}
selectedExports={selectedExports}
selectionMode={selectionMode}
isLoading={cases === undefined || rawExports === undefined}
onSelectExport={onSelectExport}
setSelectedCaseId={setSelectedCaseId}
setSelected={setSelected}
@ -903,6 +906,7 @@ type AllExportsViewProps = {
activeJobs: ExportJob[];
selectedExports: Export[];
selectionMode: boolean;
isLoading: boolean;
onSelectExport: (e: Export) => void;
setSelectedCaseId: (id: string) => void;
setSelected: (e: Export) => void;
@ -919,6 +923,7 @@ function AllExportsView({
activeJobs,
selectedExports,
selectionMode,
isLoading,
onSelectExport,
setSelectedCaseId,
setSelected,
@ -1027,6 +1032,8 @@ function AllExportsView({
</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">
<LuFolderX className="size-16" />
@ -1046,6 +1053,7 @@ type CaseViewProps = {
search: string;
selectedExports: Export[];
selectionMode: boolean;
isLoading: boolean;
onSelectExport: (e: Export) => void;
setSelected: (e: Export) => void;
renameClip: (id: string, update: string) => void;
@ -1063,6 +1071,7 @@ function CaseView({
search,
selectedExports,
selectionMode,
isLoading,
onSelectExport,
setSelected,
renameClip,
@ -1201,6 +1210,10 @@ function CaseView({
/>
))}
</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">
<LuFolderX className="size-12" />