mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-20 20:16:42 +03:00
Compare commits
11 Commits
67845d647f
...
40cc33fa37
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40cc33fa37 | ||
|
|
8e98dff671 | ||
|
|
2d06055bbe | ||
|
|
426699d3d0 | ||
|
|
73b9193a19 | ||
|
|
daa9919966 | ||
|
|
44077ebe43 | ||
|
|
341ffc194b | ||
|
|
29a464f2b5 | ||
|
|
16ac0b3f5e | ||
|
|
b105b3f2b4 |
@ -132,17 +132,15 @@ class ReviewDescriptionProcessor(PostProcessorApi):
|
||||
|
||||
if image_source == ImageSourceEnum.recordings:
|
||||
duration = final_data["end_time"] - final_data["start_time"]
|
||||
buffer_extension = min(
|
||||
10, max(2, duration * RECORDING_BUFFER_EXTENSION_PERCENT)
|
||||
)
|
||||
buffer_extension = min(5, duration * RECORDING_BUFFER_EXTENSION_PERCENT)
|
||||
|
||||
# Ensure minimum total duration for short review items
|
||||
# This provides better context for brief events
|
||||
total_duration = duration + (2 * buffer_extension)
|
||||
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
|
||||
buffer_extension = min(10, additional_buffer_per_side)
|
||||
buffer_extension = min(5, additional_buffer_per_side)
|
||||
|
||||
thumbs = self.get_recording_frames(
|
||||
camera,
|
||||
|
||||
@ -424,7 +424,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
|
||||
|
||||
if not res:
|
||||
return {
|
||||
"message": "No face was recognized.",
|
||||
"message": "Model is still training, please try again in a few moments.",
|
||||
"success": False,
|
||||
}
|
||||
|
||||
|
||||
@ -44,11 +44,16 @@ self.addEventListener("notificationclick", (event) => {
|
||||
switch (event.action ?? "default") {
|
||||
case "markReviewed":
|
||||
if (event.notification.data) {
|
||||
fetch("/api/reviews/viewed", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": 1 },
|
||||
body: JSON.stringify({ ids: [event.notification.data.id] }),
|
||||
});
|
||||
event.waitUntil(
|
||||
fetch("/api/reviews/viewed", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-TOKEN": 1,
|
||||
},
|
||||
body: JSON.stringify({ ids: [event.notification.data.id] }),
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@ -58,7 +63,7 @@ self.addEventListener("notificationclick", (event) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
if (clients.openWindow) {
|
||||
// eslint-disable-next-line no-undef
|
||||
return clients.openWindow(url);
|
||||
event.waitUntil(clients.openWindow(url));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -398,11 +398,7 @@ export function GroupedClassificationCard({
|
||||
threshold={threshold}
|
||||
selected={false}
|
||||
i18nLibrary={i18nLibrary}
|
||||
onClick={(data, meta) => {
|
||||
if (meta || selectedItems.length > 0) {
|
||||
onClick(data);
|
||||
}
|
||||
}}
|
||||
onClick={() => {}}
|
||||
>
|
||||
{children?.(data)}
|
||||
</ClassificationCard>
|
||||
|
||||
@ -683,6 +683,22 @@ function ObjectDetailsTab({
|
||||
|
||||
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
|
||||
|
||||
const isAdmin = useIsAdmin();
|
||||
@ -810,17 +826,12 @@ function ObjectDetailsTab({
|
||||
(key.includes("events") ||
|
||||
key.includes("events/search") ||
|
||||
key.includes("events/explore")),
|
||||
(currentData: SearchResult[][] | SearchResult[] | undefined) => {
|
||||
if (!currentData) return currentData;
|
||||
// optimistic update
|
||||
return currentData
|
||||
.flat()
|
||||
.map((event) =>
|
||||
event.id === search.id
|
||||
? { ...event, data: { ...event.data, description: desc } }
|
||||
: event,
|
||||
);
|
||||
},
|
||||
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
|
||||
mapSearchResults(currentData, (event) =>
|
||||
event.id === search.id
|
||||
? { ...event, data: { ...event.data, description: desc } }
|
||||
: event,
|
||||
),
|
||||
{
|
||||
optimisticData: true,
|
||||
rollbackOnError: true,
|
||||
@ -843,7 +854,7 @@ function ObjectDetailsTab({
|
||||
);
|
||||
setDesc(search.data.description);
|
||||
});
|
||||
}, [desc, search, mutate, t]);
|
||||
}, [desc, search, mutate, t, mapSearchResults]);
|
||||
|
||||
const regenerateDescription = useCallback(
|
||||
(source: "snapshot" | "thumbnails") => {
|
||||
@ -915,9 +926,8 @@ function ObjectDetailsTab({
|
||||
(key.includes("events") ||
|
||||
key.includes("events/search") ||
|
||||
key.includes("events/explore")),
|
||||
(currentData: SearchResult[][] | SearchResult[] | undefined) => {
|
||||
if (!currentData) return currentData;
|
||||
return currentData.flat().map((event) =>
|
||||
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
|
||||
mapSearchResults(currentData, (event) =>
|
||||
event.id === search.id
|
||||
? {
|
||||
...event,
|
||||
@ -928,8 +938,7 @@ function ObjectDetailsTab({
|
||||
},
|
||||
}
|
||||
: event,
|
||||
);
|
||||
},
|
||||
),
|
||||
{
|
||||
optimisticData: true,
|
||||
rollbackOnError: true,
|
||||
@ -963,7 +972,7 @@ function ObjectDetailsTab({
|
||||
);
|
||||
});
|
||||
},
|
||||
[search, apiHost, mutate, setSearch, t],
|
||||
[search, apiHost, mutate, setSearch, t, mapSearchResults],
|
||||
);
|
||||
|
||||
// recognized plate
|
||||
@ -992,9 +1001,8 @@ function ObjectDetailsTab({
|
||||
(key.includes("events") ||
|
||||
key.includes("events/search") ||
|
||||
key.includes("events/explore")),
|
||||
(currentData: SearchResult[][] | SearchResult[] | undefined) => {
|
||||
if (!currentData) return currentData;
|
||||
return currentData.flat().map((event) =>
|
||||
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
|
||||
mapSearchResults(currentData, (event) =>
|
||||
event.id === search.id
|
||||
? {
|
||||
...event,
|
||||
@ -1005,8 +1013,7 @@ function ObjectDetailsTab({
|
||||
},
|
||||
}
|
||||
: event,
|
||||
);
|
||||
},
|
||||
),
|
||||
{
|
||||
optimisticData: true,
|
||||
rollbackOnError: true,
|
||||
@ -1040,7 +1047,7 @@ function ObjectDetailsTab({
|
||||
);
|
||||
});
|
||||
},
|
||||
[search, apiHost, mutate, setSearch, t],
|
||||
[search, apiHost, mutate, setSearch, t, mapSearchResults],
|
||||
);
|
||||
|
||||
// speech transcription
|
||||
@ -1102,17 +1109,12 @@ function ObjectDetailsTab({
|
||||
(key.includes("events") ||
|
||||
key.includes("events/search") ||
|
||||
key.includes("events/explore")),
|
||||
(currentData: SearchResult[][] | SearchResult[] | undefined) => {
|
||||
if (!currentData) return currentData;
|
||||
// optimistic update
|
||||
return currentData
|
||||
.flat()
|
||||
.map((event) =>
|
||||
event.id === search.id
|
||||
? { ...event, plus_id: "new_upload" }
|
||||
: event,
|
||||
);
|
||||
},
|
||||
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
|
||||
mapSearchResults(currentData, (event) =>
|
||||
event.id === search.id
|
||||
? { ...event, plus_id: "new_upload" }
|
||||
: event,
|
||||
),
|
||||
{
|
||||
optimisticData: true,
|
||||
rollbackOnError: true,
|
||||
@ -1120,7 +1122,7 @@ function ObjectDetailsTab({
|
||||
},
|
||||
);
|
||||
},
|
||||
[search, mutate],
|
||||
[search, mutate, mapSearchResults],
|
||||
);
|
||||
|
||||
const popoverContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -1503,7 +1505,7 @@ function ObjectDetailsTab({
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Textarea
|
||||
className="text-md h-32"
|
||||
className="text-md h-32 md:text-sm"
|
||||
placeholder={t("details.description.placeholder")}
|
||||
value={desc}
|
||||
onChange={(e) => setDesc(e.target.value)}
|
||||
@ -1511,25 +1513,7 @@ function ObjectDetailsTab({
|
||||
onBlur={handleDescriptionBlur}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex flex-row justify-end gap-4">
|
||||
<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>
|
||||
|
||||
<div className="mb-10 flex flex-row justify-end gap-5">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
@ -1540,13 +1524,31 @@ function ObjectDetailsTab({
|
||||
setDesc(originalDescRef.current ?? "");
|
||||
}}
|
||||
>
|
||||
<FaTimes className="size-4" />
|
||||
<FaTimes className="size-5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
</TooltipContent>
|
||||
</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>
|
||||
)}
|
||||
|
||||
@ -221,12 +221,26 @@ export function TrackingDetails({
|
||||
displaySource,
|
||||
]);
|
||||
|
||||
const isWithinEventRange =
|
||||
effectiveTime !== undefined &&
|
||||
event.start_time !== undefined &&
|
||||
event.end_time !== undefined &&
|
||||
effectiveTime >= event.start_time &&
|
||||
effectiveTime <= event.end_time;
|
||||
const isWithinEventRange = useMemo(() => {
|
||||
if (effectiveTime === undefined || event.start_time === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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
|
||||
const calculateLineHeight = useCallback(() => {
|
||||
|
||||
@ -318,6 +318,7 @@ export default function HlsVideoPlayer({
|
||||
{isDetailMode &&
|
||||
camera &&
|
||||
currentTime &&
|
||||
loadedMetadata &&
|
||||
videoDimensions.width > 0 &&
|
||||
videoDimensions.height > 0 && (
|
||||
<div className="absolute z-50 size-full">
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
ReviewSummary,
|
||||
SegmentedReviewData,
|
||||
} from "@/types/review";
|
||||
import { TimelineType } from "@/types/timeline";
|
||||
import {
|
||||
getBeginningOfDayTimestamp,
|
||||
getEndOfDayTimestamp,
|
||||
@ -49,6 +50,16 @@ export default function Events() {
|
||||
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) => {
|
||||
axios
|
||||
.get(`review/${reviewId}`)
|
||||
@ -66,7 +77,7 @@ export default function Events() {
|
||||
camera: resp.data.camera,
|
||||
startTime,
|
||||
severity: resp.data.severity,
|
||||
timelineType: "detail",
|
||||
timelineType: notificationTab,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ReviewSeverity } from "./review";
|
||||
import { TimelineType } from "./timeline";
|
||||
|
||||
export type Recording = {
|
||||
id: string;
|
||||
@ -37,7 +38,7 @@ export type RecordingStartingPoint = {
|
||||
camera: string;
|
||||
startTime: number;
|
||||
severity: ReviewSeverity;
|
||||
timelineType?: "timeline" | "events" | "detail";
|
||||
timelineType?: TimelineType;
|
||||
};
|
||||
|
||||
export type RecordingPlayerError = "stalled" | "startup";
|
||||
|
||||
@ -72,8 +72,7 @@ export default function StorageMetrics({
|
||||
const earliestDate = useMemo(() => {
|
||||
const keys = Object.keys(recordingsSummary || {});
|
||||
return keys.length
|
||||
? new TZDate(keys[keys.length - 1] + "T00:00:00", timezone).getTime() /
|
||||
1000
|
||||
? new TZDate(keys[0] + "T00:00:00", timezone).getTime() / 1000
|
||||
: null;
|
||||
}, [recordingsSummary, timezone]);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user