diff --git a/docker-compose.yml b/docker-compose.yml index a4d349194f..f368805939 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,7 +23,7 @@ services: # count: 1 # capabilities: [gpu] environment: - YOLO_MODELS: yolov7-320 + YOLO_MODELS: "" devices: - /dev/bus/usb:/dev/bus/usb # - /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware diff --git a/docker/tensorrt/Dockerfile.base b/docker/tensorrt/Dockerfile.base index 59ead46f51..8720b5c603 100644 --- a/docker/tensorrt/Dockerfile.base +++ b/docker/tensorrt/Dockerfile.base @@ -25,7 +25,7 @@ ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 COPY --from=trt-deps /usr/local/lib/libyolo_layer.so /usr/local/lib/libyolo_layer.so COPY --from=trt-deps /usr/local/src/tensorrt_demos /usr/local/src/tensorrt_demos COPY docker/tensorrt/detector/rootfs/ / -ENV YOLO_MODELS="yolov7-320" +ENV YOLO_MODELS="" HEALTHCHECK --start-period=600s --start-interval=5s --interval=15s --timeout=5s --retries=3 \ CMD curl --fail --silent --show-error http://127.0.0.1:5000/api/version || exit 1 diff --git a/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/run b/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/run index c39c7a0aa6..4d734e05a3 100755 --- a/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/run +++ b/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/run @@ -19,6 +19,11 @@ FIRST_MODEL=true MODEL_DOWNLOAD="" MODEL_CONVERT="" +if [ -z "$YOLO_MODELS"]; then + echo "tensorrt model preparation disabled" + exit 0 +fi + for model in ${YOLO_MODELS//,/ } do # Remove old link in case path/version changed diff --git a/docs/docs/configuration/genai.md b/docs/docs/configuration/genai.md index 2ee27f7242..2ec9a62763 100644 --- a/docs/docs/configuration/genai.md +++ b/docs/docs/configuration/genai.md @@ -3,9 +3,13 @@ id: genai title: Generative AI --- -Generative AI can be used to automatically generate descriptive text based on the thumbnails of your tracked objects. This helps with [Semantic Search](/configuration/semantic_search) in Frigate to provide more context about your tracked objects. +Generative AI can be used to automatically generate descriptive text based on the thumbnails of your tracked objects. This helps with [Semantic Search](/configuration/semantic_search) in Frigate to provide more context about your tracked objects. Descriptions are accessed via the _Explore_ view in the Frigate UI by clicking on a tracked object's thumbnail. -Semantic Search must be enabled to use Generative AI. Descriptions are accessed via the _Explore_ view in the Frigate UI by clicking on a tracked object's thumbnail. +:::info + +Semantic Search must be enabled to use Generative AI. + +::: ## Configuration diff --git a/docs/docs/configuration/object_detectors.md b/docs/docs/configuration/object_detectors.md index 7fefa74a40..24888ae42a 100644 --- a/docs/docs/configuration/object_detectors.md +++ b/docs/docs/configuration/object_detectors.md @@ -223,7 +223,7 @@ The model used for TensorRT must be preprocessed on the same hardware platform t The Frigate image will generate model files during startup if the specified model is not found. Processed models are stored in the `/config/model_cache` folder. Typically the `/config` path is mapped to a directory on the host already and the `model_cache` does not need to be mapped separately unless the user wants to store it in a different location on the host. -By default, the `yolov7-320` model will be generated, but this can be overridden by specifying the `YOLO_MODELS` environment variable in Docker. One or more models may be listed in a comma-separated format, and each one will be generated. To select no model generation, set the variable to an empty string, `YOLO_MODELS=""`. Models will only be generated if the corresponding `{model}.trt` file is not present in the `model_cache` folder, so you can force a model to be regenerated by deleting it from your Frigate data folder. +By default, no models will be generated, but this can be overridden by specifying the `YOLO_MODELS` environment variable in Docker. One or more models may be listed in a comma-separated format, and each one will be generated. Models will only be generated if the corresponding `{model}.trt` file is not present in the `model_cache` folder, so you can force a model to be regenerated by deleting it from your Frigate data folder. If you have a Jetson device with DLAs (Xavier or Orin), you can generate a model that will run on the DLA by appending `-dla` to your model name, e.g. specify `YOLO_MODELS=yolov7-320-dla`. The model will run on DLA0 (Frigate does not currently support DLA1). DLA-incompatible layers will fall back to running on the GPU. @@ -264,7 +264,7 @@ An example `docker-compose.yml` fragment that converts the `yolov4-608` and `yol ```yml frigate: environment: - - YOLO_MODELS=yolov4-608,yolov7x-640 + - YOLO_MODELS=yolov7-320,yolov7x-640 - USE_FP16=false ``` diff --git a/frigate/api/event.py b/frigate/api/event.py index e6cecc9c7b..ace3da8f84 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -1017,9 +1017,11 @@ def regenerate_description( status_code=404, ) + camera_config = request.app.frigate_config.cameras[event.camera] + if ( request.app.frigate_config.semantic_search.enabled - and request.app.frigate_config.genai.enabled + and camera_config.genai.enabled ): request.app.event_metadata_updater.publish((event.id, params.source)) diff --git a/frigate/events/cleanup.py b/frigate/events/cleanup.py index 7d3e7c4561..8ae38b5340 100644 --- a/frigate/events/cleanup.py +++ b/frigate/events/cleanup.py @@ -21,6 +21,9 @@ class EventCleanupType(str, Enum): snapshots = "snapshots" +CHUNK_SIZE = 50 + + class EventCleanup(threading.Thread): def __init__( self, config: FrigateConfig, stop_event: MpEvent, db: SqliteVecQueueDatabase @@ -107,6 +110,7 @@ def expire(self, media_type: EventCleanupType) -> list[str]: .namedtuples() .iterator() ) + logger.debug(f"{len(expired_events)} events can be expired") # delete the media from disk for expired in expired_events: media_name = f"{expired.camera}-{expired.id}" @@ -125,13 +129,34 @@ def expire(self, media_type: EventCleanupType) -> list[str]: logger.warning(f"Unable to delete event images: {e}") # update the clips attribute for the db entry - update_query = Event.update(update_params).where( + query = Event.select(Event.id).where( Event.camera.not_in(self.camera_keys), Event.start_time < expire_after, Event.label == event.label, Event.retain_indefinitely == False, ) - update_query.execute() + + events_to_update = [] + + for batch in query.iterator(): + events_to_update.extend([event.id for event in batch]) + if len(events_to_update) >= CHUNK_SIZE: + logger.debug( + f"Updating {update_params} for {len(events_to_update)} events" + ) + Event.update(update_params).where( + Event.id << events_to_update + ).execute() + events_to_update = [] + + # Update any remaining events + if events_to_update: + logger.debug( + f"Updating clips/snapshots attribute for {len(events_to_update)} events" + ) + Event.update(update_params).where( + Event.id << events_to_update + ).execute() events_to_update = [] @@ -196,7 +221,11 @@ def expire(self, media_type: EventCleanupType) -> list[str]: logger.warning(f"Unable to delete event images: {e}") # update the clips attribute for the db entry - Event.update(update_params).where(Event.id << events_to_update).execute() + for i in range(0, len(events_to_update), CHUNK_SIZE): + batch = events_to_update[i : i + CHUNK_SIZE] + logger.debug(f"Updating {update_params} for {len(batch)} events") + Event.update(update_params).where(Event.id << batch).execute() + return events_to_update def run(self) -> None: @@ -222,10 +251,11 @@ def run(self) -> None: .iterator() ) events_to_delete = [e.id for e in events] + logger.debug(f"Found {len(events_to_delete)} events that can be expired") if len(events_to_delete) > 0: - chunk_size = 50 - for i in range(0, len(events_to_delete), chunk_size): - chunk = events_to_delete[i : i + chunk_size] + for i in range(0, len(events_to_delete), CHUNK_SIZE): + chunk = events_to_delete[i : i + CHUNK_SIZE] + logger.debug(f"Deleting {len(chunk)} events from the database") Event.delete().where(Event.id << chunk).execute() if self.config.semantic_search.enabled: diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index e2d509383c..74fae9fea5 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -54,11 +54,10 @@ def _send(self, prompt: str, images: list[bytes]) -> Optional[str]: def get_genai_client(genai_config: GenAIConfig) -> Optional[GenAIClient]: """Get the GenAI client.""" - if genai_config.enabled: - load_providers() - provider = PROVIDERS.get(genai_config.provider) - if provider: - return provider(genai_config) + load_providers() + provider = PROVIDERS.get(genai_config.provider) + if provider: + return provider(genai_config) return None diff --git a/frigate/output/output.py b/frigate/output/output.py index 7d5b6d39a8..1859ebd69f 100644 --- a/frigate/output/output.py +++ b/frigate/output/output.py @@ -63,6 +63,7 @@ def receiveSignal(signalNumber, frame): birdseye: Optional[Birdseye] = None preview_recorders: dict[str, PreviewRecorder] = {} preview_write_times: dict[str, float] = {} + failed_frame_requests: dict[str, int] = {} move_preview_frames("cache") @@ -99,7 +100,16 @@ def receiveSignal(signalNumber, frame): if frame is None: logger.debug(f"Failed to get frame {frame_id} from SHM") + failed_frame_requests[camera] = failed_frame_requests.get(camera, 0) + 1 + + if failed_frame_requests[camera] > config.cameras[camera].detect.fps: + logger.warning( + f"Failed to retrieve many frames for {camera} from SHM, consider increasing SHM size if this continues." + ) + continue + else: + failed_frame_requests[camera] = 0 # send camera frame to ffmpeg process if websockets are connected if any( diff --git a/frigate/output/preview.py b/frigate/output/preview.py index a8915f688a..ae2ba4591c 100644 --- a/frigate/output/preview.py +++ b/frigate/output/preview.py @@ -78,7 +78,7 @@ def __init__( # write a PREVIEW at fps and 1 key frame per clip self.ffmpeg_cmd = parse_preset_hardware_acceleration_encode( config.ffmpeg.ffmpeg_path, - config.ffmpeg.hwaccel_args, + "default", input="-f concat -y -protocol_whitelist pipe,file -safe 0 -threads 1 -i /dev/stdin", output=f"-threads 1 -g {PREVIEW_KEYFRAME_INTERVAL} -bf 0 -b:v {PREVIEW_QUALITY_BIT_RATES[self.config.record.preview.quality]} {FPS_VFR_PARAM} -movflags +faststart -pix_fmt yuv420p {self.path}", type=EncodeTypeEnum.preview, @@ -154,6 +154,7 @@ def __init__(self, config: CameraConfig) -> None: self.start_time = 0 self.last_output_time = 0 self.output_frames = [] + if config.detect.width > config.detect.height: self.out_height = PREVIEW_HEIGHT self.out_width = ( @@ -274,7 +275,7 @@ def should_write_frame( return False - def write_frame_to_cache(self, frame_time: float, frame) -> None: + def write_frame_to_cache(self, frame_time: float, frame: np.ndarray) -> None: # resize yuv frame small_frame = np.zeros((self.out_height * 3 // 2, self.out_width), np.uint8) copy_yuv_to_position( @@ -303,7 +304,7 @@ def write_data( current_tracked_objects: list[dict[str, any]], motion_boxes: list[list[int]], frame_time: float, - frame, + frame: np.ndarray, ) -> bool: # check for updated record config _, updated_record_config = self.config_subscriber.check_for_update() @@ -332,6 +333,10 @@ def write_data( self.output_frames, self.requestor, ).start() + else: + logger.debug( + f"Not saving preview for {self.config.name} because there are no saved frames." + ) # reset frame cache self.segment_end = ( diff --git a/web/src/api/ws.tsx b/web/src/api/ws.tsx index b0c89d5ddf..c7bb74095b 100644 --- a/web/src/api/ws.tsx +++ b/web/src/api/ws.tsx @@ -69,7 +69,10 @@ function useValue(): useValueReturn { ...prevState, ...cameraStates, })); - setHasCameraState(true); + + if (Object.keys(cameraStates).length > 0) { + setHasCameraState(true); + } // we only want this to run initially when the config is loaded // eslint-disable-next-line react-hooks/exhaustive-deps }, [wsState]); @@ -93,6 +96,9 @@ function useValue(): useValueReturn { retain: false, }); }, + onClose: () => { + setHasCameraState(false); + }, shouldReconnect: () => true, retryOnError: true, }); diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx index 004fe5527c..d315965611 100644 --- a/web/src/components/filter/ReviewFilterGroup.tsx +++ b/web/src/components/filter/ReviewFilterGroup.tsx @@ -14,7 +14,7 @@ import MobileReviewSettingsDrawer, { } from "../overlay/MobileReviewSettingsDrawer"; import useOptimisticState from "@/hooks/use-optimistic-state"; import FilterSwitch from "./FilterSwitch"; -import { FilterList } from "@/types/filter"; +import { FilterList, GeneralFilter } from "@/types/filter"; import CalendarFilterButton from "./CalendarFilterButton"; import { CamerasFilterButton } from "./CamerasFilterButton"; import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog"; @@ -214,15 +214,9 @@ export default function ReviewFilterGroup({ showAll={filter?.showAll == true} allZones={filterValues.zones} selectedZones={filter?.zones} - setShowAll={(showAll) => { - onUpdateFilter({ ...filter, showAll }); + onUpdateFilter={(general) => { + onUpdateFilter({ ...filter, ...general }); }} - updateLabelFilter={(newLabels) => { - onUpdateFilter({ ...filter, labels: newLabels }); - }} - updateZoneFilter={(newZones) => - onUpdateFilter({ ...filter, zones: newZones }) - } /> )} {isMobile && mobileSettingsFeatures.length > 0 && ( @@ -300,37 +294,40 @@ type GeneralFilterButtonProps = { showAll: boolean; allZones: string[]; selectedZones?: string[]; - setShowAll: (showAll: boolean) => void; - updateLabelFilter: (labels: string[] | undefined) => void; - updateZoneFilter: (zones: string[] | undefined) => void; + filter?: GeneralFilter; + onUpdateFilter: (filter: GeneralFilter) => void; }; + function GeneralFilterButton({ allLabels, selectedLabels, + filter, currentSeverity, showAll, allZones, selectedZones, - setShowAll, - updateLabelFilter, - updateZoneFilter, + onUpdateFilter, }: GeneralFilterButtonProps) { const [open, setOpen] = useState(false); - const [currentLabels, setCurrentLabels] = useState( - selectedLabels, - ); - const [currentZones, setCurrentZones] = useState( - selectedZones, - ); + const [currentFilter, setCurrentFilter] = useState({ + labels: selectedLabels, + zones: selectedZones, + showAll: showAll, + ...filter, + }); - // ui + // Update local state when props change useEffect(() => { - setCurrentLabels(selectedLabels); - setCurrentZones(selectedZones); + setCurrentFilter({ + labels: selectedLabels, + zones: selectedZones, + showAll: showAll, + ...filter, + }); // only refresh when state changes // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedLabels, selectedZones]); + }, [selectedLabels, selectedZones, showAll, filter]); const trigger = ( - diff --git a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx index d58d485b93..3eeb639cd1 100644 --- a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx +++ b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx @@ -4,7 +4,7 @@ import { Button } from "../ui/button"; import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa"; import { TimeRange } from "@/types/timeline"; import { ExportContent, ExportPreviewDialog } from "./ExportDialog"; -import { ExportMode } from "@/types/filter"; +import { ExportMode, GeneralFilter } from "@/types/filter"; import ReviewActivityCalendar from "./ReviewActivityCalendar"; import { SelectSeparator } from "../ui/select"; import { ReviewFilter, ReviewSeverity, ReviewSummary } from "@/types/review"; @@ -114,12 +114,12 @@ export default function MobileReviewSettingsDrawer({ // filters - const [currentLabels, setCurrentLabels] = useState( - filter?.labels, - ); - const [currentZones, setCurrentZones] = useState( - filter?.zones, - ); + const [currentFilter, setCurrentFilter] = useState({ + labels: filter?.labels, + zones: filter?.zones, + showAll: filter?.showAll, + ...filter, + }); if (!isMobile) { return; @@ -260,23 +260,21 @@ export default function MobileReviewSettingsDrawer({ - onUpdateFilter({ ...filter, zones: newZones }) - } - setShowAll={(showAll) => { - onUpdateFilter({ ...filter, showAll }); + onUpdateFilter={setCurrentFilter} + onApply={() => { + if (currentFilter !== filter) { + onUpdateFilter(currentFilter); + } + }} + onReset={() => { + const resetFilter: GeneralFilter = {}; + setCurrentFilter(resetFilter); + onUpdateFilter(resetFilter); }} - setCurrentLabels={setCurrentLabels} - updateLabelFilter={(newLabels) => - onUpdateFilter({ ...filter, labels: newLabels }) - } onClose={() => setDrawerMode("select")} /> diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index f158df3291..f56074a52a 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -477,7 +477,7 @@ function ObjectDetailsTab({ onChange={(e) => setDesc(e.target.value)} />
- {config?.genai.enabled && ( + {config?.cameras[search.camera].genai.enabled && (