Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Review summary #10196

Merged
merged 5 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions frigate/api/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@
)
from frigate.models import Event, Timeline
from frigate.object_processing import TrackedObject
from frigate.util.builtin import (
get_tz_modifiers,
)
from frigate.util.builtin import get_tz_modifiers

logger = logging.getLogger(__name__)

Expand Down
103 changes: 102 additions & 1 deletion frigate/api/review.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
make_response,
request,
)
from peewee import DoesNotExist, operator
from peewee import Case, DoesNotExist, fn, operator

from frigate.models import ReviewSegment
from frigate.util.builtin import get_tz_modifiers

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -70,6 +71,106 @@ def review():
return jsonify([r for r in review])


@ReviewBp.route("/review/summary")
def review_summary():
tz_name = request.args.get("timezone", default="utc", type=str)
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name)
month_ago = (datetime.now() - timedelta(days=30)).timestamp()

groups = (
ReviewSegment.select(
fn.strftime(
"%Y-%m-%d",
fn.datetime(
ReviewSegment.start_time,
"unixepoch",
hour_modifier,
minute_modifier,
),
).alias("day"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == "alert"),
ReviewSegment.has_been_reviewed,
)
],
0,
)
).alias("reviewed_alert"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == "detection"),
ReviewSegment.has_been_reviewed,
)
],
0,
)
).alias("reviewed_detection"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == "significant_motion"),
ReviewSegment.has_been_reviewed,
)
],
0,
)
).alias("reviewed_motion"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == "alert"),
1,
)
],
0,
)
).alias("total_alert"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == "detection"),
1,
)
],
0,
)
).alias("total_detection"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == "significant_motion"),
1,
)
],
0,
)
).alias("total_motion"),
)
.where(ReviewSegment.start_time > month_ago)
.group_by(
(ReviewSegment.start_time + seconds_offset).cast("int") / (3600 * 24),
)
.order_by(ReviewSegment.start_time.desc())
)

return jsonify([e for e in groups.dicts().iterator()])


@ReviewBp.route("/review/<id>/viewed", methods=("POST",))
def set_reviewed(id):
try:
Expand Down
8 changes: 4 additions & 4 deletions frigate/record/maintainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@

class SegmentInfo:
def __init__(
self, motion_box_count: int, active_object_count: int, average_dBFS: int
self, motion_area: int, active_object_count: int, average_dBFS: int
) -> None:
self.motion_box_count = motion_box_count
self.motion_area = motion_area
self.active_object_count = active_object_count
self.average_dBFS = average_dBFS

def should_discard_segment(self, retain_mode: RetainModeEnum) -> bool:
return (
retain_mode == RetainModeEnum.motion
and self.motion_box_count == 0
and self.motion_area == 0
and self.average_dBFS == 0
) or (
retain_mode == RetainModeEnum.active_objects
Expand Down Expand Up @@ -412,7 +412,7 @@ async def move_segment(
Recordings.start_time: start_time.timestamp(),
Recordings.end_time: end_time.timestamp(),
Recordings.duration: duration,
Recordings.motion: segment_info.motion_box_count,
Recordings.motion: segment_info.motion_area,
# TODO: update this to store list of active objects at some point
Recordings.objects: segment_info.active_object_count,
Recordings.dBFS: segment_info.average_dBFS,
Expand Down
16 changes: 4 additions & 12 deletions frigate/test/test_record_retention.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,20 @@

class TestRecordRetention(unittest.TestCase):
def test_motion_should_keep_motion_not_object(self):
segment_info = SegmentInfo(
motion_box_count=1, active_object_count=0, average_dBFS=0
)
segment_info = SegmentInfo(motion_area=1, active_object_count=0, average_dBFS=0)
assert not segment_info.should_discard_segment(RetainModeEnum.motion)
assert segment_info.should_discard_segment(RetainModeEnum.active_objects)

def test_object_should_keep_object_not_motion(self):
segment_info = SegmentInfo(
motion_box_count=0, active_object_count=1, average_dBFS=0
)
segment_info = SegmentInfo(motion_area=0, active_object_count=1, average_dBFS=0)
assert segment_info.should_discard_segment(RetainModeEnum.motion)
assert not segment_info.should_discard_segment(RetainModeEnum.active_objects)

def test_all_should_keep_all(self):
segment_info = SegmentInfo(
motion_box_count=0, active_object_count=0, average_dBFS=0
)
segment_info = SegmentInfo(motion_area=0, active_object_count=0, average_dBFS=0)
assert not segment_info.should_discard_segment(RetainModeEnum.all)

def test_should_keep_audio_in_motion_mode(self):
segment_info = SegmentInfo(
motion_box_count=0, active_object_count=0, average_dBFS=1
)
segment_info = SegmentInfo(motion_area=0, active_object_count=0, average_dBFS=1)
assert not segment_info.should_discard_segment(RetainModeEnum.motion)
assert segment_info.should_discard_segment(RetainModeEnum.active_objects)
13 changes: 13 additions & 0 deletions web/src/hooks/use-date-utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FrigateConfig } from "@/types/frigateConfig";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { useMemo } from "react";

Expand All @@ -10,3 +11,15 @@ export function useFormattedTimestamp(timestamp: number, format: string) {

return formattedTimestamp;
}

export function useTimezone(config: FrigateConfig | undefined) {
return useMemo(() => {
if (!config) {
return undefined;
}

return (
config.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone
);
}, [config]);
}
19 changes: 19 additions & 0 deletions web/src/pages/Events.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import ActivityIndicator from "@/components/indicators/activity-indicator";
import useApiFilter from "@/hooks/use-api-filter";
import { useTimezone } from "@/hooks/use-date-utils";
import useOverlayState from "@/hooks/use-overlay-state";
import { FrigateConfig } from "@/types/frigateConfig";
import { Preview } from "@/types/preview";
import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review";
import DesktopRecordingView from "@/views/events/DesktopRecordingView";
Expand All @@ -12,6 +15,9 @@ import useSWRInfinite from "swr/infinite";
const API_LIMIT = 100;

export default function Events() {
const { data: config } = useSWR<FrigateConfig>("config");
const timezone = useTimezone(config);

// recordings viewer

const [severity, setSeverity] = useState<ReviewSeverity>("alert");
Expand Down Expand Up @@ -100,6 +106,14 @@ export default function Events() {

const reloadData = useCallback(() => setBeforeTs(Date.now() / 1000), []);

// review summary

const { data: reviewSummary } = useSWR([
"review/summary",
{ timezone: timezone },
{ revalidateOnFocus: false },
]);

// preview videos

const previewTimes = useMemo(() => {
Expand Down Expand Up @@ -200,6 +214,10 @@ export default function Events() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedReviewId, reviewPages]);

if (!timezone) {
return <ActivityIndicator />;
}

if (selectedData) {
return (
<DesktopRecordingView
Expand All @@ -212,6 +230,7 @@ export default function Events() {
return (
<EventView
reviewPages={reviewPages}
reviewSummary={reviewSummary}
relevantPreviews={allPreviews}
timeRange={selectedTimeRange}
reachedEnd={isDone}
Expand Down
10 changes: 10 additions & 0 deletions web/src/types/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,13 @@ export type ReviewFilter = {
after?: number;
showReviewed?: 0 | 1;
};

export type ReviewSummary = {
day: string;
reviewed_alert: number;
reviewed_detection: number;
reviewed_motion: number;
total_alert: number;
total_detection: number;
total_motion: number;
};
48 changes: 44 additions & 4 deletions web/src/views/events/EventView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { useEventUtils } from "@/hooks/use-event-utils";
import { useScrollLockout } from "@/hooks/use-mouse-listener";
import { FrigateConfig } from "@/types/frigateConfig";
import { Preview } from "@/types/preview";
import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review";
import {
ReviewFilter,
ReviewSegment,
ReviewSeverity,
ReviewSummary,
} from "@/types/review";
import axios from "axios";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { isDesktop, isMobile } from "react-device-detect";
Expand All @@ -20,6 +25,7 @@ import useSWR from "swr";

type EventViewProps = {
reviewPages?: ReviewSegment[][];
reviewSummary?: ReviewSummary[];
relevantPreviews?: Preview[];
timeRange: { before: number; after: number };
reachedEnd: boolean;
Expand All @@ -35,6 +41,7 @@ type EventViewProps = {
};
export default function EventView({
reviewPages,
reviewSummary,
relevantPreviews,
timeRange,
reachedEnd,
Expand All @@ -52,6 +59,35 @@ export default function EventView({
const contentRef = useRef<HTMLDivElement | null>(null);
const segmentDuration = 60;

// review counts

const reviewCounts = useMemo(() => {
if (!reviewSummary) {
return { alert: 0, detection: 0, significant_motion: 0 };
}

let summary;
if (filter?.before == undefined) {
summary = reviewSummary[0];
} else {
summary = reviewSummary[0];
}

if (filter?.showReviewed == 1) {
return {
alert: summary.total_alert,
detection: summary.total_detection,
significant_motion: summary.total_motion,
};
} else {
return {
alert: summary.total_alert - summary.reviewed_alert,
detection: summary.total_detection - summary.reviewed_detection,
significant_motion: summary.total_motion - summary.reviewed_motion,
};
}
}, [filter, reviewSummary]);

// review paging

const reviewItems = useMemo(() => {
Expand Down Expand Up @@ -264,15 +300,17 @@ export default function EventView({
aria-label="Select alerts"
>
<MdCircle className="size-2 md:mr-[10px] text-severity_alert" />
<div className="hidden md:block">Alerts</div>
<div className="hidden md:block">Alerts ∙ {reviewCounts.alert}</div>
</ToggleGroupItem>
<ToggleGroupItem
className={`${severity == "detection" ? "" : "text-gray-500"}`}
value="detection"
aria-label="Select detections"
>
<MdCircle className="size-2 md:mr-[10px] text-severity_detection" />
<div className="hidden md:block">Detections</div>
<div className="hidden md:block">
Detections ∙ {reviewCounts.detection}
</div>
</ToggleGroupItem>
<ToggleGroupItem
className={`px-3 py-4 rounded-2xl ${
Expand All @@ -282,7 +320,9 @@ export default function EventView({
aria-label="Select motion"
>
<MdCircle className="size-2 md:mr-[10px] text-severity_motion" />
<div className="hidden md:block">Motion</div>
<div className="hidden md:block">
Motion ∙ {reviewCounts.significant_motion}
</div>
</ToggleGroupItem>
</ToggleGroup>

Expand Down
Loading