mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Improve review book keeping (#10735)
* Improve review book keeping * Cleanup * Cleanup for new labels * Final cleanup * Fix sub label checking
This commit is contained in:
		
							parent
							
								
									89f843cf95
								
							
						
					
					
						commit
						4d522be7fb
					
				@ -17,7 +17,7 @@ from frigate.comms.config_updater import ConfigSubscriber
 | 
				
			|||||||
from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum
 | 
					from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum
 | 
				
			||||||
from frigate.comms.inter_process import InterProcessRequestor
 | 
					from frigate.comms.inter_process import InterProcessRequestor
 | 
				
			||||||
from frigate.config import CameraConfig, FrigateConfig
 | 
					from frigate.config import CameraConfig, FrigateConfig
 | 
				
			||||||
from frigate.const import CLIPS_DIR, UPSERT_REVIEW_SEGMENT
 | 
					from frigate.const import ALL_ATTRIBUTE_LABELS, CLIPS_DIR, UPSERT_REVIEW_SEGMENT
 | 
				
			||||||
from frigate.models import ReviewSegment
 | 
					from frigate.models import ReviewSegment
 | 
				
			||||||
from frigate.object_processing import TrackedObject
 | 
					from frigate.object_processing import TrackedObject
 | 
				
			||||||
from frigate.util.image import SharedMemoryFrameManager, calculate_16_9_crop
 | 
					from frigate.util.image import SharedMemoryFrameManager, calculate_16_9_crop
 | 
				
			||||||
@ -45,9 +45,7 @@ class PendingReviewSegment:
 | 
				
			|||||||
        camera: str,
 | 
					        camera: str,
 | 
				
			||||||
        frame_time: float,
 | 
					        frame_time: float,
 | 
				
			||||||
        severity: SeverityEnum,
 | 
					        severity: SeverityEnum,
 | 
				
			||||||
        detections: set[str] = set(),
 | 
					        detections: dict[str, str],
 | 
				
			||||||
        objects: set[str] = set(),
 | 
					 | 
				
			||||||
        sub_labels: set[str] = set(),
 | 
					 | 
				
			||||||
        zones: set[str] = set(),
 | 
					        zones: set[str] = set(),
 | 
				
			||||||
        audio: set[str] = set(),
 | 
					        audio: set[str] = set(),
 | 
				
			||||||
        motion: list[int] = [],
 | 
					        motion: list[int] = [],
 | 
				
			||||||
@ -58,8 +56,6 @@ class PendingReviewSegment:
 | 
				
			|||||||
        self.start_time = frame_time
 | 
					        self.start_time = frame_time
 | 
				
			||||||
        self.severity = severity
 | 
					        self.severity = severity
 | 
				
			||||||
        self.detections = detections
 | 
					        self.detections = detections
 | 
				
			||||||
        self.objects = objects
 | 
					 | 
				
			||||||
        self.sub_labels = sub_labels
 | 
					 | 
				
			||||||
        self.zones = zones
 | 
					        self.zones = zones
 | 
				
			||||||
        self.audio = audio
 | 
					        self.audio = audio
 | 
				
			||||||
        self.sig_motion_areas = motion
 | 
					        self.sig_motion_areas = motion
 | 
				
			||||||
@ -114,9 +110,8 @@ class PendingReviewSegment:
 | 
				
			|||||||
            ReviewSegment.severity: self.severity.value,
 | 
					            ReviewSegment.severity: self.severity.value,
 | 
				
			||||||
            ReviewSegment.thumb_path: path,
 | 
					            ReviewSegment.thumb_path: path,
 | 
				
			||||||
            ReviewSegment.data: {
 | 
					            ReviewSegment.data: {
 | 
				
			||||||
                "detections": list(self.detections),
 | 
					                "detections": list(set(self.detections.keys())),
 | 
				
			||||||
                "objects": list(self.objects),
 | 
					                "objects": list(set(self.detections.values())),
 | 
				
			||||||
                "sub_labels": list(self.sub_labels),
 | 
					 | 
				
			||||||
                "zones": list(self.zones),
 | 
					                "zones": list(self.zones),
 | 
				
			||||||
                "audio": list(self.audio),
 | 
					                "audio": list(self.audio),
 | 
				
			||||||
                "significant_motion_areas": self.sig_motion_areas,
 | 
					                "significant_motion_areas": self.sig_motion_areas,
 | 
				
			||||||
@ -180,11 +175,12 @@ class ReviewSegmentMaintainer(threading.Thread):
 | 
				
			|||||||
                self.frame_manager.close(frame_id)
 | 
					                self.frame_manager.close(frame_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for object in active_objects:
 | 
					            for object in active_objects:
 | 
				
			||||||
                segment.detections.add(object["id"])
 | 
					                if not object["sub_label"]:
 | 
				
			||||||
                segment.objects.add(object["label"])
 | 
					                    segment.detections[object["id"]] = object["label"]
 | 
				
			||||||
 | 
					                elif object["sub_label"][0] in ALL_ATTRIBUTE_LABELS:
 | 
				
			||||||
                if object["sub_label"]:
 | 
					                    segment.detections[object["id"]] = object["sub_label"][0]
 | 
				
			||||||
                    segment.sub_labels.add(object["sub_label"][0])
 | 
					                else:
 | 
				
			||||||
 | 
					                    segment.detections[object["id"]] = f'{object["label"]}-verified'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # if object is alert label and has qualified for recording
 | 
					                # if object is alert label and has qualified for recording
 | 
				
			||||||
                # mark this review as alert
 | 
					                # mark this review as alert
 | 
				
			||||||
@ -224,9 +220,7 @@ class ReviewSegmentMaintainer(threading.Thread):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if len(active_objects) > 0:
 | 
					        if len(active_objects) > 0:
 | 
				
			||||||
            has_sig_object = False
 | 
					            has_sig_object = False
 | 
				
			||||||
            detections: set = set()
 | 
					            detections: dict[str, str] = {}
 | 
				
			||||||
            objects: set = set()
 | 
					 | 
				
			||||||
            sub_labels: set = set()
 | 
					 | 
				
			||||||
            zones: set = set()
 | 
					            zones: set = set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for object in active_objects:
 | 
					            for object in active_objects:
 | 
				
			||||||
@ -237,11 +231,12 @@ class ReviewSegmentMaintainer(threading.Thread):
 | 
				
			|||||||
                ):
 | 
					                ):
 | 
				
			||||||
                    has_sig_object = True
 | 
					                    has_sig_object = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                detections.add(object["id"])
 | 
					                if not object["sub_label"]:
 | 
				
			||||||
                objects.add(object["label"])
 | 
					                    detections[object["id"]] = object["label"]
 | 
				
			||||||
 | 
					                elif object["sub_label"][0] in ALL_ATTRIBUTE_LABELS:
 | 
				
			||||||
                if object["sub_label"]:
 | 
					                    detections[object["id"]] = object["sub_label"][0]
 | 
				
			||||||
                    sub_labels.add(object["sub_label"][0])
 | 
					                else:
 | 
				
			||||||
 | 
					                    detections[object["id"]] = f'{object["label"]}-verified'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                zones.update(object["current_zones"])
 | 
					                zones.update(object["current_zones"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -250,8 +245,6 @@ class ReviewSegmentMaintainer(threading.Thread):
 | 
				
			|||||||
                frame_time,
 | 
					                frame_time,
 | 
				
			||||||
                SeverityEnum.alert if has_sig_object else SeverityEnum.detection,
 | 
					                SeverityEnum.alert if has_sig_object else SeverityEnum.detection,
 | 
				
			||||||
                detections,
 | 
					                detections,
 | 
				
			||||||
                objects=objects,
 | 
					 | 
				
			||||||
                sub_labels=sub_labels,
 | 
					 | 
				
			||||||
                audio=set(),
 | 
					                audio=set(),
 | 
				
			||||||
                zones=zones,
 | 
					                zones=zones,
 | 
				
			||||||
                motion=[],
 | 
					                motion=[],
 | 
				
			||||||
@ -268,9 +261,8 @@ class ReviewSegmentMaintainer(threading.Thread):
 | 
				
			|||||||
                camera,
 | 
					                camera,
 | 
				
			||||||
                frame_time,
 | 
					                frame_time,
 | 
				
			||||||
                SeverityEnum.signification_motion,
 | 
					                SeverityEnum.signification_motion,
 | 
				
			||||||
                detections=set(),
 | 
					                detections={},
 | 
				
			||||||
                objects=set(),
 | 
					                audio=set(),
 | 
				
			||||||
                sub_labels=set(),
 | 
					 | 
				
			||||||
                motion=motion,
 | 
					                motion=motion,
 | 
				
			||||||
                zones=set(),
 | 
					                zones=set(),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
@ -340,9 +332,7 @@ class ReviewSegmentMaintainer(threading.Thread):
 | 
				
			|||||||
                        camera,
 | 
					                        camera,
 | 
				
			||||||
                        frame_time,
 | 
					                        frame_time,
 | 
				
			||||||
                        SeverityEnum.detection,
 | 
					                        SeverityEnum.detection,
 | 
				
			||||||
                        set(),
 | 
					                        {},
 | 
				
			||||||
                        set(),
 | 
					 | 
				
			||||||
                        set(),
 | 
					 | 
				
			||||||
                        set(),
 | 
					                        set(),
 | 
				
			||||||
                        set(audio_detections),
 | 
					                        set(audio_detections),
 | 
				
			||||||
                        [],
 | 
					                        [],
 | 
				
			||||||
 | 
				
			|||||||
@ -83,7 +83,7 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </TooltipTrigger>
 | 
					      </TooltipTrigger>
 | 
				
			||||||
      <TooltipContent>
 | 
					      <TooltipContent>
 | 
				
			||||||
        {`${[...event.data.objects, ...event.data.audio, ...(event.data.sub_labels || [])].join(", ")} detected`}
 | 
					        {`${[...event.data.objects, ...event.data.audio].join(", ").replaceAll("-verified", "")} detected`}
 | 
				
			||||||
      </TooltipContent>
 | 
					      </TooltipContent>
 | 
				
			||||||
    </Tooltip>
 | 
					    </Tooltip>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import { baseUrl } from "@/api/baseUrl";
 | 
				
			|||||||
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
 | 
					import { useFormattedTimestamp } from "@/hooks/use-date-utils";
 | 
				
			||||||
import { FrigateConfig } from "@/types/frigateConfig";
 | 
					import { FrigateConfig } from "@/types/frigateConfig";
 | 
				
			||||||
import { ReviewSegment } from "@/types/review";
 | 
					import { ReviewSegment } from "@/types/review";
 | 
				
			||||||
import { getIconForLabel, getIconForSubLabel } from "@/utils/iconUtil";
 | 
					import { getIconForLabel } from "@/utils/iconUtil";
 | 
				
			||||||
import { isSafari } from "react-device-detect";
 | 
					import { isSafari } from "react-device-detect";
 | 
				
			||||||
import useSWR from "swr";
 | 
					import useSWR from "swr";
 | 
				
			||||||
import TimeAgo from "../dynamic/TimeAgo";
 | 
					import TimeAgo from "../dynamic/TimeAgo";
 | 
				
			||||||
@ -57,9 +57,6 @@ export default function ReviewCard({
 | 
				
			|||||||
          {event.data.audio.map((audio) => {
 | 
					          {event.data.audio.map((audio) => {
 | 
				
			||||||
            return getIconForLabel(audio, "size-3 text-white");
 | 
					            return getIconForLabel(audio, "size-3 text-white");
 | 
				
			||||||
          })}
 | 
					          })}
 | 
				
			||||||
          {event.data.sub_labels?.map((sub) => {
 | 
					 | 
				
			||||||
            return getIconForSubLabel(sub, "size-3 text-white");
 | 
					 | 
				
			||||||
          })}
 | 
					 | 
				
			||||||
          <div className="font-extra-light text-xs">{formattedDate}</div>
 | 
					          <div className="font-extra-light text-xs">{formattedDate}</div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <TimeAgo
 | 
					        <TimeAgo
 | 
				
			||||||
 | 
				
			|||||||
@ -30,7 +30,6 @@ import MobileReviewSettingsDrawer, {
 | 
				
			|||||||
  DrawerFeatures,
 | 
					  DrawerFeatures,
 | 
				
			||||||
} from "../overlay/MobileReviewSettingsDrawer";
 | 
					} from "../overlay/MobileReviewSettingsDrawer";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
 | 
					 | 
				
			||||||
const REVIEW_FILTERS = [
 | 
					const REVIEW_FILTERS = [
 | 
				
			||||||
  "cameras",
 | 
					  "cameras",
 | 
				
			||||||
  "reviewed",
 | 
					  "reviewed",
 | 
				
			||||||
@ -77,9 +76,7 @@ export default function ReviewFilterGroup({
 | 
				
			|||||||
    cameras.forEach((camera) => {
 | 
					    cameras.forEach((camera) => {
 | 
				
			||||||
      const cameraConfig = config.cameras[camera];
 | 
					      const cameraConfig = config.cameras[camera];
 | 
				
			||||||
      cameraConfig.objects.track.forEach((label) => {
 | 
					      cameraConfig.objects.track.forEach((label) => {
 | 
				
			||||||
        if (!ATTRIBUTES.includes(label)) {
 | 
					        labels.add(label);
 | 
				
			||||||
          labels.add(label);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (cameraConfig.audio.enabled_in_config) {
 | 
					      if (cameraConfig.audio.enabled_in_config) {
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,7 @@ import { useApiHost } from "@/api";
 | 
				
			|||||||
import { isCurrentHour } from "@/utils/dateUtil";
 | 
					import { isCurrentHour } from "@/utils/dateUtil";
 | 
				
			||||||
import { ReviewSegment } from "@/types/review";
 | 
					import { ReviewSegment } from "@/types/review";
 | 
				
			||||||
import { Slider } from "../ui/slider-no-thumb";
 | 
					import { Slider } from "../ui/slider-no-thumb";
 | 
				
			||||||
import { getIconForLabel, getIconForSubLabel } from "@/utils/iconUtil";
 | 
					import { getIconForLabel } from "@/utils/iconUtil";
 | 
				
			||||||
import TimeAgo from "../dynamic/TimeAgo";
 | 
					import TimeAgo from "../dynamic/TimeAgo";
 | 
				
			||||||
import useSWR from "swr";
 | 
					import useSWR from "swr";
 | 
				
			||||||
import { FrigateConfig } from "@/types/frigateConfig";
 | 
					import { FrigateConfig } from "@/types/frigateConfig";
 | 
				
			||||||
@ -227,9 +227,6 @@ export default function PreviewThumbnailPlayer({
 | 
				
			|||||||
                        {review.data.audio.map((audio) => {
 | 
					                        {review.data.audio.map((audio) => {
 | 
				
			||||||
                          return getIconForLabel(audio, "size-3 text-white");
 | 
					                          return getIconForLabel(audio, "size-3 text-white");
 | 
				
			||||||
                        })}
 | 
					                        })}
 | 
				
			||||||
                        {review.data.sub_labels?.map((sub) => {
 | 
					 | 
				
			||||||
                          return getIconForSubLabel(sub, "size-3 text-white");
 | 
					 | 
				
			||||||
                        })}
 | 
					 | 
				
			||||||
                      </Chip>
 | 
					                      </Chip>
 | 
				
			||||||
                    </>
 | 
					                    </>
 | 
				
			||||||
                  )}
 | 
					                  )}
 | 
				
			||||||
@ -237,13 +234,10 @@ export default function PreviewThumbnailPlayer({
 | 
				
			|||||||
              </TooltipTrigger>
 | 
					              </TooltipTrigger>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <TooltipContent className="capitalize">
 | 
					            <TooltipContent className="capitalize">
 | 
				
			||||||
              {[
 | 
					              {[...(review.data.objects || []), ...(review.data.audio || [])]
 | 
				
			||||||
                ...(review.data.objects || []),
 | 
					 | 
				
			||||||
                ...(review.data.audio || []),
 | 
					 | 
				
			||||||
                ...(review.data.sub_labels || []),
 | 
					 | 
				
			||||||
              ]
 | 
					 | 
				
			||||||
                .filter((item) => item !== undefined)
 | 
					                .filter((item) => item !== undefined)
 | 
				
			||||||
                .join(", ")}
 | 
					                .join(", ")
 | 
				
			||||||
 | 
					                .replaceAll("-verified", "")}
 | 
				
			||||||
            </TooltipContent>
 | 
					            </TooltipContent>
 | 
				
			||||||
          </Tooltip>
 | 
					          </Tooltip>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,6 @@ export type ReviewData = {
 | 
				
			|||||||
  audio: string[];
 | 
					  audio: string[];
 | 
				
			||||||
  detections: string[];
 | 
					  detections: string[];
 | 
				
			||||||
  objects: string[];
 | 
					  objects: string[];
 | 
				
			||||||
  sub_labels?: string[];
 | 
					 | 
				
			||||||
  significant_motion_areas: number[];
 | 
					  significant_motion_areas: number[];
 | 
				
			||||||
  zones: string[];
 | 
					  zones: string[];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -3,6 +3,7 @@ import {
 | 
				
			|||||||
  FaAmazon,
 | 
					  FaAmazon,
 | 
				
			||||||
  FaCarSide,
 | 
					  FaCarSide,
 | 
				
			||||||
  FaCat,
 | 
					  FaCat,
 | 
				
			||||||
 | 
					  FaCheckCircle,
 | 
				
			||||||
  FaCircle,
 | 
					  FaCircle,
 | 
				
			||||||
  FaDog,
 | 
					  FaDog,
 | 
				
			||||||
  FaFedex,
 | 
					  FaFedex,
 | 
				
			||||||
@ -34,6 +35,10 @@ export function getIconForGroup(icon: string, className: string = "size-4") {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getIconForLabel(label: string, className?: string) {
 | 
					export function getIconForLabel(label: string, className?: string) {
 | 
				
			||||||
 | 
					  if (label.endsWith("-verified")) {
 | 
				
			||||||
 | 
					    return getVerifiedIcon(label, className);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  switch (label) {
 | 
					  switch (label) {
 | 
				
			||||||
    case "car":
 | 
					    case "car":
 | 
				
			||||||
      return <FaCarSide key={label} className={className} />;
 | 
					      return <FaCarSide key={label} className={className} />;
 | 
				
			||||||
@ -48,24 +53,32 @@ export function getIconForLabel(label: string, className?: string) {
 | 
				
			|||||||
      return <LuBox key={label} className={className} />;
 | 
					      return <LuBox key={label} className={className} />;
 | 
				
			||||||
    case "person":
 | 
					    case "person":
 | 
				
			||||||
      return <BsPersonWalking key={label} className={className} />;
 | 
					      return <BsPersonWalking key={label} className={className} />;
 | 
				
			||||||
 | 
					    // audio
 | 
				
			||||||
    case "crying":
 | 
					    case "crying":
 | 
				
			||||||
    case "laughter":
 | 
					    case "laughter":
 | 
				
			||||||
    case "scream":
 | 
					    case "scream":
 | 
				
			||||||
    case "speech":
 | 
					    case "speech":
 | 
				
			||||||
    case "yell":
 | 
					    case "yell":
 | 
				
			||||||
      return <MdRecordVoiceOver key={label} className={className} />;
 | 
					      return <MdRecordVoiceOver key={label} className={className} />;
 | 
				
			||||||
    default:
 | 
					    // sub labels
 | 
				
			||||||
      return <LuLassoSelect key={label} className={className} />;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function getIconForSubLabel(label: string, className?: string) {
 | 
					 | 
				
			||||||
  switch (label) {
 | 
					 | 
				
			||||||
    case "amazon":
 | 
					    case "amazon":
 | 
				
			||||||
      return <FaAmazon key={label} className={className} />;
 | 
					      return <FaAmazon key={label} className={className} />;
 | 
				
			||||||
    case "fedex":
 | 
					    case "fedex":
 | 
				
			||||||
      return <FaFedex key={label} className={className} />;
 | 
					      return <FaFedex key={label} className={className} />;
 | 
				
			||||||
    case "ups":
 | 
					    case "ups":
 | 
				
			||||||
      return <FaUps key={label} className={className} />;
 | 
					      return <FaUps key={label} className={className} />;
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return <LuLassoSelect key={label} className={className} />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getVerifiedIcon(label: string, className?: string) {
 | 
				
			||||||
 | 
					  const simpleLabel = label.substring(0, label.lastIndexOf("-"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="flex items-center">
 | 
				
			||||||
 | 
					      {getIconForLabel(simpleLabel, className)}
 | 
				
			||||||
 | 
					      <FaCheckCircle className="absolute size-2 translate-x-[80%] translate-y-3/4" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user