Compare commits

...

13 Commits

Author SHA1 Message Date
Josh Hawkins
67845d647f
Merge 441d57c76b into 097673b845 2025-11-14 16:11:44 +00:00
Josh Hawkins
441d57c76b ensure logging config is passed to camera capture and tracker processes 2025-11-14 10:11:39 -06:00
GuoQing Liu
097673b845
chore: i18n use cache key (#20885)
Some checks failed
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
* chore: i18n use cache key

* Fix indentation in Dockerfile for pip command

* Add build argument for GIT_COMMIT_HASH in CI workflow

* Add short-sha output to action.yml

* Update build args to use short SHA output

* build: use vite .env

* Remove unnecessary newline in Dockerfile

* Define proxy host variable in vite.config.ts

Add a new line to define the proxy host variable.
2025-11-14 09:36:46 -06:00
Nicolas Mowen
8e98dff671 Reduce review item buffering behavior 2025-11-12 17:10:50 -07:00
Nicolas Mowen
2d06055bbe Try and improve notification opening behavior 2025-11-12 16:29:12 -07:00
Nicolas Mowen
426699d3d0 Use timeline tab by default for notifications but add a query arg for customization 2025-11-12 16:22:55 -07:00
Josh Hawkins
73b9193a19 fix blue line height calc for in progress events 2025-11-12 11:42:50 -06:00
Josh Hawkins
daa9919966 don't show object track until video metadata is loaded 2025-11-12 11:21:40 -06:00
Nicolas Mowen
44077ebe43 Update reprocess message 2025-11-12 08:03:51 -07:00
Nicolas Mowen
341ffc194b Remove ability to right click on elements inside of face popup 2025-11-12 07:17:48 -07:00
Josh Hawkins
29a464f2b5 tracked object description box tweaks 2025-11-12 08:11:50 -06:00
Nicolas Mowen
16ac0b3f5e Properly sort keys for recording summary in StorageMetrics 2025-11-12 07:09:29 -07:00
Josh Hawkins
b105b3f2b4 don't flatten the search result cache when updating
this would cause an infinite swr fetch if something was mutated and then fetch was called again
2025-11-12 08:02:16 -06:00
16 changed files with 128 additions and 89 deletions

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ frigate/version.py
web/build web/build
web/node_modules web/node_modules
web/coverage web/coverage
web/.env
core core
!/web/**/*.ts !/web/**/*.ts
.idea/* .idea/*

View File

@ -14,6 +14,7 @@ push-boards: $(BOARDS:%=push-%)
version: version:
echo 'VERSION = "$(VERSION)-$(COMMIT_HASH)"' > frigate/version.py echo 'VERSION = "$(VERSION)-$(COMMIT_HASH)"' > frigate/version.py
echo 'VITE_GIT_COMMIT_HASH=$(COMMIT_HASH)' > web/.env
local: version local: version
docker buildx build --target=frigate --file docker/main/Dockerfile . \ docker buildx build --target=frigate --file docker/main/Dockerfile . \

View File

@ -136,6 +136,7 @@ class CameraMaintainer(threading.Thread):
self.ptz_metrics[name], self.ptz_metrics[name],
self.region_grids[name], self.region_grids[name],
self.stop_event, self.stop_event,
self.config.logger,
) )
self.camera_processes[config.name] = camera_process self.camera_processes[config.name] = camera_process
camera_process.start() camera_process.start()
@ -156,7 +157,11 @@ class CameraMaintainer(threading.Thread):
self.frame_manager.create(f"{config.name}_frame{i}", frame_size) self.frame_manager.create(f"{config.name}_frame{i}", frame_size)
capture_process = CameraCapture( capture_process = CameraCapture(
config, count, self.camera_metrics[name], self.stop_event config,
count,
self.camera_metrics[name],
self.stop_event,
self.config.logger,
) )
capture_process.daemon = True capture_process.daemon = True
self.capture_processes[name] = capture_process self.capture_processes[name] = capture_process

View File

@ -132,17 +132,15 @@ class ReviewDescriptionProcessor(PostProcessorApi):
if image_source == ImageSourceEnum.recordings: if image_source == ImageSourceEnum.recordings:
duration = final_data["end_time"] - final_data["start_time"] duration = final_data["end_time"] - final_data["start_time"]
buffer_extension = min( buffer_extension = min(5, duration * RECORDING_BUFFER_EXTENSION_PERCENT)
10, max(2, duration * RECORDING_BUFFER_EXTENSION_PERCENT)
)
# Ensure minimum total duration for short review items # Ensure minimum total duration for short review items
# This provides better context for brief events # This provides better context for brief events
total_duration = duration + (2 * buffer_extension) total_duration = duration + (2 * buffer_extension)
if total_duration < MIN_RECORDING_DURATION: if total_duration < MIN_RECORDING_DURATION:
# Expand buffer to reach minimum duration, still respecting max of 10s per side # Expand buffer to reach minimum duration, still respecting max of 5s per side
additional_buffer_per_side = (MIN_RECORDING_DURATION - duration) / 2 additional_buffer_per_side = (MIN_RECORDING_DURATION - duration) / 2
buffer_extension = min(10, additional_buffer_per_side) buffer_extension = min(5, additional_buffer_per_side)
thumbs = self.get_recording_frames( thumbs = self.get_recording_frames(
camera, camera,

View File

@ -424,7 +424,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
if not res: if not res:
return { return {
"message": "No face was recognized.", "message": "Model is still training, please try again in a few moments.",
"success": False, "success": False,
} }

View File

@ -16,7 +16,7 @@ from frigate.comms.recordings_updater import (
RecordingsDataSubscriber, RecordingsDataSubscriber,
RecordingsDataTypeEnum, RecordingsDataTypeEnum,
) )
from frigate.config import CameraConfig, DetectConfig, ModelConfig from frigate.config import CameraConfig, DetectConfig, LoggerConfig, ModelConfig
from frigate.config.camera.camera import CameraTypeEnum from frigate.config.camera.camera import CameraTypeEnum
from frigate.config.camera.updater import ( from frigate.config.camera.updater import (
CameraConfigUpdateEnum, CameraConfigUpdateEnum,
@ -539,6 +539,7 @@ class CameraCapture(FrigateProcess):
shm_frame_count: int, shm_frame_count: int,
camera_metrics: CameraMetrics, camera_metrics: CameraMetrics,
stop_event: MpEvent, stop_event: MpEvent,
log_config: LoggerConfig | None = None,
) -> None: ) -> None:
super().__init__( super().__init__(
stop_event, stop_event,
@ -549,9 +550,10 @@ class CameraCapture(FrigateProcess):
self.config = config self.config = config
self.shm_frame_count = shm_frame_count self.shm_frame_count = shm_frame_count
self.camera_metrics = camera_metrics self.camera_metrics = camera_metrics
self.log_config = log_config
def run(self) -> None: def run(self) -> None:
self.pre_run_setup() self.pre_run_setup(self.log_config)
camera_watchdog = CameraWatchdog( camera_watchdog = CameraWatchdog(
self.config, self.config,
self.shm_frame_count, self.shm_frame_count,
@ -577,6 +579,7 @@ class CameraTracker(FrigateProcess):
ptz_metrics: PTZMetrics, ptz_metrics: PTZMetrics,
region_grid: list[list[dict[str, Any]]], region_grid: list[list[dict[str, Any]]],
stop_event: MpEvent, stop_event: MpEvent,
log_config: LoggerConfig | None = None,
) -> None: ) -> None:
super().__init__( super().__init__(
stop_event, stop_event,
@ -592,9 +595,10 @@ class CameraTracker(FrigateProcess):
self.camera_metrics = camera_metrics self.camera_metrics = camera_metrics
self.ptz_metrics = ptz_metrics self.ptz_metrics = ptz_metrics
self.region_grid = region_grid self.region_grid = region_grid
self.log_config = log_config
def run(self) -> None: def run(self) -> None:
self.pre_run_setup() self.pre_run_setup(self.log_config)
frame_queue = self.camera_metrics.frame_queue frame_queue = self.camera_metrics.frame_queue
frame_shape = self.config.frame_shape frame_shape = self.config.frame_shape

1
web/.gitignore vendored
View File

@ -22,3 +22,4 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.env

View File

@ -44,11 +44,16 @@ self.addEventListener("notificationclick", (event) => {
switch (event.action ?? "default") { switch (event.action ?? "default") {
case "markReviewed": case "markReviewed":
if (event.notification.data) { if (event.notification.data) {
fetch("/api/reviews/viewed", { event.waitUntil(
method: "POST", fetch("/api/reviews/viewed", {
headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": 1 }, method: "POST",
body: JSON.stringify({ ids: [event.notification.data.id] }), headers: {
}); "Content-Type": "application/json",
"X-CSRF-TOKEN": 1,
},
body: JSON.stringify({ ids: [event.notification.data.id] }),
})
);
} }
break; break;
default: default:
@ -58,7 +63,7 @@ self.addEventListener("notificationclick", (event) => {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
if (clients.openWindow) { if (clients.openWindow) {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
return clients.openWindow(url); event.waitUntil(clients.openWindow(url));
} }
} }
} }

View File

@ -398,11 +398,7 @@ export function GroupedClassificationCard({
threshold={threshold} threshold={threshold}
selected={false} selected={false}
i18nLibrary={i18nLibrary} i18nLibrary={i18nLibrary}
onClick={(data, meta) => { onClick={() => {}}
if (meta || selectedItems.length > 0) {
onClick(data);
}
}}
> >
{children?.(data)} {children?.(data)}
</ClassificationCard> </ClassificationCard>

View File

@ -683,6 +683,22 @@ function ObjectDetailsTab({
const mutate = useGlobalMutation(); const mutate = useGlobalMutation();
// Helper to map over SWR cached search results while preserving
// either paginated format (SearchResult[][]) or flat format (SearchResult[])
const mapSearchResults = useCallback(
(
currentData: SearchResult[][] | SearchResult[] | undefined,
fn: (event: SearchResult) => SearchResult,
) => {
if (!currentData) return currentData;
if (Array.isArray(currentData[0])) {
return (currentData as SearchResult[][]).map((page) => page.map(fn));
}
return (currentData as SearchResult[]).map(fn);
},
[],
);
// users // users
const isAdmin = useIsAdmin(); const isAdmin = useIsAdmin();
@ -810,17 +826,12 @@ function ObjectDetailsTab({
(key.includes("events") || (key.includes("events") ||
key.includes("events/search") || key.includes("events/search") ||
key.includes("events/explore")), key.includes("events/explore")),
(currentData: SearchResult[][] | SearchResult[] | undefined) => { (currentData: SearchResult[][] | SearchResult[] | undefined) =>
if (!currentData) return currentData; mapSearchResults(currentData, (event) =>
// optimistic update event.id === search.id
return currentData ? { ...event, data: { ...event.data, description: desc } }
.flat() : event,
.map((event) => ),
event.id === search.id
? { ...event, data: { ...event.data, description: desc } }
: event,
);
},
{ {
optimisticData: true, optimisticData: true,
rollbackOnError: true, rollbackOnError: true,
@ -843,7 +854,7 @@ function ObjectDetailsTab({
); );
setDesc(search.data.description); setDesc(search.data.description);
}); });
}, [desc, search, mutate, t]); }, [desc, search, mutate, t, mapSearchResults]);
const regenerateDescription = useCallback( const regenerateDescription = useCallback(
(source: "snapshot" | "thumbnails") => { (source: "snapshot" | "thumbnails") => {
@ -915,9 +926,8 @@ function ObjectDetailsTab({
(key.includes("events") || (key.includes("events") ||
key.includes("events/search") || key.includes("events/search") ||
key.includes("events/explore")), key.includes("events/explore")),
(currentData: SearchResult[][] | SearchResult[] | undefined) => { (currentData: SearchResult[][] | SearchResult[] | undefined) =>
if (!currentData) return currentData; mapSearchResults(currentData, (event) =>
return currentData.flat().map((event) =>
event.id === search.id event.id === search.id
? { ? {
...event, ...event,
@ -928,8 +938,7 @@ function ObjectDetailsTab({
}, },
} }
: event, : event,
); ),
},
{ {
optimisticData: true, optimisticData: true,
rollbackOnError: true, rollbackOnError: true,
@ -963,7 +972,7 @@ function ObjectDetailsTab({
); );
}); });
}, },
[search, apiHost, mutate, setSearch, t], [search, apiHost, mutate, setSearch, t, mapSearchResults],
); );
// recognized plate // recognized plate
@ -992,9 +1001,8 @@ function ObjectDetailsTab({
(key.includes("events") || (key.includes("events") ||
key.includes("events/search") || key.includes("events/search") ||
key.includes("events/explore")), key.includes("events/explore")),
(currentData: SearchResult[][] | SearchResult[] | undefined) => { (currentData: SearchResult[][] | SearchResult[] | undefined) =>
if (!currentData) return currentData; mapSearchResults(currentData, (event) =>
return currentData.flat().map((event) =>
event.id === search.id event.id === search.id
? { ? {
...event, ...event,
@ -1005,8 +1013,7 @@ function ObjectDetailsTab({
}, },
} }
: event, : event,
); ),
},
{ {
optimisticData: true, optimisticData: true,
rollbackOnError: true, rollbackOnError: true,
@ -1040,7 +1047,7 @@ function ObjectDetailsTab({
); );
}); });
}, },
[search, apiHost, mutate, setSearch, t], [search, apiHost, mutate, setSearch, t, mapSearchResults],
); );
// speech transcription // speech transcription
@ -1102,17 +1109,12 @@ function ObjectDetailsTab({
(key.includes("events") || (key.includes("events") ||
key.includes("events/search") || key.includes("events/search") ||
key.includes("events/explore")), key.includes("events/explore")),
(currentData: SearchResult[][] | SearchResult[] | undefined) => { (currentData: SearchResult[][] | SearchResult[] | undefined) =>
if (!currentData) return currentData; mapSearchResults(currentData, (event) =>
// optimistic update event.id === search.id
return currentData ? { ...event, plus_id: "new_upload" }
.flat() : event,
.map((event) => ),
event.id === search.id
? { ...event, plus_id: "new_upload" }
: event,
);
},
{ {
optimisticData: true, optimisticData: true,
rollbackOnError: true, rollbackOnError: true,
@ -1120,7 +1122,7 @@ function ObjectDetailsTab({
}, },
); );
}, },
[search, mutate], [search, mutate, mapSearchResults],
); );
const popoverContainerRef = useRef<HTMLDivElement | null>(null); const popoverContainerRef = useRef<HTMLDivElement | null>(null);
@ -1503,7 +1505,7 @@ function ObjectDetailsTab({
) : ( ) : (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Textarea <Textarea
className="text-md h-32" className="text-md h-32 md:text-sm"
placeholder={t("details.description.placeholder")} placeholder={t("details.description.placeholder")}
value={desc} value={desc}
onChange={(e) => setDesc(e.target.value)} onChange={(e) => setDesc(e.target.value)}
@ -1511,25 +1513,7 @@ function ObjectDetailsTab({
onBlur={handleDescriptionBlur} onBlur={handleDescriptionBlur}
autoFocus autoFocus
/> />
<div className="flex flex-row justify-end gap-4"> <div className="mb-10 flex flex-row justify-end gap-5">
<Tooltip>
<TooltipTrigger asChild>
<button
aria-label={t("button.save", { ns: "common" })}
className="text-primary/40 hover:text-primary/80"
onClick={() => {
setIsEditingDesc(false);
updateDescription();
}}
>
<FaCheck className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent>
{t("button.save", { ns: "common" })}
</TooltipContent>
</Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
@ -1540,13 +1524,31 @@ function ObjectDetailsTab({
setDesc(originalDescRef.current ?? ""); setDesc(originalDescRef.current ?? "");
}} }}
> >
<FaTimes className="size-4" /> <FaTimes className="size-5" />
</button> </button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{t("button.cancel", { ns: "common" })} {t("button.cancel", { ns: "common" })}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
aria-label={t("button.save", { ns: "common" })}
className="text-primary/40 hover:text-primary/80"
onClick={() => {
setIsEditingDesc(false);
updateDescription();
}}
>
<FaCheck className="size-5" />
</button>
</TooltipTrigger>
<TooltipContent>
{t("button.save", { ns: "common" })}
</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
)} )}

View File

@ -221,12 +221,26 @@ export function TrackingDetails({
displaySource, displaySource,
]); ]);
const isWithinEventRange = const isWithinEventRange = useMemo(() => {
effectiveTime !== undefined && if (effectiveTime === undefined || event.start_time === undefined) {
event.start_time !== undefined && return false;
event.end_time !== undefined && }
effectiveTime >= event.start_time &&
effectiveTime <= event.end_time; // If an event has not ended yet, fall back to last timestamp in eventSequence
let eventEnd = event.end_time;
if (eventEnd == null && eventSequence && eventSequence.length > 0) {
const last = eventSequence[eventSequence.length - 1];
if (last && last.timestamp !== undefined) {
eventEnd = last.timestamp;
}
}
if (eventEnd == null) {
return false;
}
return effectiveTime >= event.start_time && effectiveTime <= eventEnd;
}, [effectiveTime, event.start_time, event.end_time, eventSequence]);
// Calculate how far down the blue line should extend based on effectiveTime // Calculate how far down the blue line should extend based on effectiveTime
const calculateLineHeight = useCallback(() => { const calculateLineHeight = useCallback(() => {

View File

@ -318,6 +318,7 @@ export default function HlsVideoPlayer({
{isDetailMode && {isDetailMode &&
camera && camera &&
currentTime && currentTime &&
loadedMetadata &&
videoDimensions.width > 0 && videoDimensions.width > 0 &&
videoDimensions.height > 0 && ( videoDimensions.height > 0 && (
<div className="absolute z-50 size-full"> <div className="absolute z-50 size-full">

View File

@ -15,6 +15,7 @@ import {
ReviewSummary, ReviewSummary,
SegmentedReviewData, SegmentedReviewData,
} from "@/types/review"; } from "@/types/review";
import { TimelineType } from "@/types/timeline";
import { import {
getBeginningOfDayTimestamp, getBeginningOfDayTimestamp,
getEndOfDayTimestamp, getEndOfDayTimestamp,
@ -49,6 +50,16 @@ export default function Events() {
false, false,
); );
const [notificationTab, setNotificationTab] =
useState<TimelineType>("timeline");
useSearchEffect("tab", (tab: string) => {
if (tab === "timeline" || tab === "events" || tab === "detail") {
setNotificationTab(tab as TimelineType);
}
return true;
});
useSearchEffect("id", (reviewId: string) => { useSearchEffect("id", (reviewId: string) => {
axios axios
.get(`review/${reviewId}`) .get(`review/${reviewId}`)
@ -66,7 +77,7 @@ export default function Events() {
camera: resp.data.camera, camera: resp.data.camera,
startTime, startTime,
severity: resp.data.severity, severity: resp.data.severity,
timelineType: "detail", timelineType: notificationTab,
}, },
true, true,
); );

View File

@ -1,4 +1,5 @@
import { ReviewSeverity } from "./review"; import { ReviewSeverity } from "./review";
import { TimelineType } from "./timeline";
export type Recording = { export type Recording = {
id: string; id: string;
@ -37,7 +38,7 @@ export type RecordingStartingPoint = {
camera: string; camera: string;
startTime: number; startTime: number;
severity: ReviewSeverity; severity: ReviewSeverity;
timelineType?: "timeline" | "events" | "detail"; timelineType?: TimelineType;
}; };
export type RecordingPlayerError = "stalled" | "startup"; export type RecordingPlayerError = "stalled" | "startup";

View File

@ -33,7 +33,7 @@ i18n
fallbackLng: "en", // use en if detected lng is not available fallbackLng: "en", // use en if detected lng is not available
backend: { backend: {
loadPath: "locales/{{lng}}/{{ns}}.json", loadPath: `locales/{{lng}}/{{ns}}.json?v=${import.meta.env.VITE_GIT_COMMIT_HASH || "unknown"}`,
}, },
ns: [ ns: [

View File

@ -72,8 +72,7 @@ export default function StorageMetrics({
const earliestDate = useMemo(() => { const earliestDate = useMemo(() => {
const keys = Object.keys(recordingsSummary || {}); const keys = Object.keys(recordingsSummary || {});
return keys.length return keys.length
? new TZDate(keys[keys.length - 1] + "T00:00:00", timezone).getTime() / ? new TZDate(keys[0] + "T00:00:00", timezone).getTime() / 1000
1000
: null; : null;
}, [recordingsSummary, timezone]); }, [recordingsSummary, timezone]);