diff --git a/frigate/data_processing/post/types.py b/frigate/data_processing/post/types.py index 0344c7616..3698e99a8 100644 --- a/frigate/data_processing/post/types.py +++ b/frigate/data_processing/post/types.py @@ -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, diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index 3bc98100c..ce0034670 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -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 diff --git a/frigate/genai/llama_cpp.py b/frigate/genai/llama_cpp.py index 11060e537..de45deadb 100644 --- a/frigate/genai/llama_cpp.py +++ b/frigate/genai/llama_cpp.py @@ -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 diff --git a/frigate/record/export.py b/frigate/record/export.py index ef2fdc810..9f571a5a5 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -380,9 +380,14 @@ class RecordingExporter(threading.Thread): if label and label not in labels: labels.append(label) - title = str(review.severity).capitalize() - if labels: - title = f"{title}: {', '.join(labels)}" + metadata = data.get("metadata") or {} + title = metadata.get("title") + + if not title: + title = str(review.severity).capitalize() + + if labels: + title = f"{title}: {', '.join(labels)}" chapter_blocks.append( "[CHAPTER]\n" diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json index 8b86b69d4..a05126c68 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -207,6 +207,7 @@ "th": "ไทย (Thai)", "ca": "Català (Catalan)", "hr": "Hrvatski (Croatian)", + "bs": "Bosanski (Bosnian)", "sr": "Српски (Serbian)", "sl": "Slovenščina (Slovenian)", "lt": "Lietuvių (Lithuanian)", diff --git a/web/src/components/card/ReviewCard.tsx b/web/src/components/card/ReviewCard.tsx index a57c1e9ed..33d651ed8 100644 --- a/web/src/components/card/ReviewCard.tsx +++ b/web/src/components/card/ReviewCard.tsx @@ -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) { diff --git a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx index f1d8ae14a..78f08098c 100644 --- a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx @@ -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; diff --git a/web/src/components/overlay/MultiExportDialog.tsx b/web/src/components/overlay/MultiExportDialog.tsx index c914f3edd..f37b99b84 100644 --- a/web/src/components/overlay/MultiExportDialog.tsx +++ b/web/src/components/overlay/MultiExportDialog.tsx @@ -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, })); diff --git a/web/src/components/player/VideoControls.tsx b/web/src/components/player/VideoControls.tsx index 5a45e83ac..ab4340e0e 100644 --- a/web/src/components/player/VideoControls.tsx +++ b/web/src/components/player/VideoControls.tsx @@ -198,7 +198,7 @@ export default function VideoControls({ return (