diff --git a/docker/main/requirements-wheels.txt b/docker/main/requirements-wheels.txt index 814d6ef8d..41bbcba09 100644 --- a/docker/main/requirements-wheels.txt +++ b/docker/main/requirements-wheels.txt @@ -32,7 +32,7 @@ unidecode == 1.3.* # OpenVino (ONNX installed in wheels-post) openvino == 2024.3.* # Embeddings -chromadb == 0.5.0 +chromadb == 0.5.7 onnx_clip == 4.0.* # Generative AI google-generativeai == 0.6.* diff --git a/frigate/api/defs/events_query_parameters.py b/frigate/api/defs/events_query_parameters.py index 884cbe9f6..02bbc31ea 100644 --- a/frigate/api/defs/events_query_parameters.py +++ b/frigate/api/defs/events_query_parameters.py @@ -43,6 +43,7 @@ class EventsSearchQueryParams(BaseModel): zones: Optional[str] = "all" after: Optional[float] = None before: Optional[float] = None + time_range: Optional[str] = DEFAULT_TIME_RANGE timezone: Optional[str] = "utc" diff --git a/frigate/api/event.py b/frigate/api/event.py index 556d19909..4e45b10de 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -357,6 +357,7 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) zones = params.zones after = params.after before = params.before + time_range = params.time_range # for similarity search event_id = params.event_id @@ -403,36 +404,85 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) if include_thumbnails: selected_columns.append(Event.thumbnail) - # Build the where clause for the embeddings query - embeddings_filters = [] + # Build the initial SQLite query filters + event_filters = [] if cameras != "all": camera_list = cameras.split(",") - embeddings_filters.append({"camera": {"$in": camera_list}}) + event_filters.append((Event.camera << camera_list)) if labels != "all": label_list = labels.split(",") - embeddings_filters.append({"label": {"$in": label_list}}) + event_filters.append((Event.label << label_list)) if zones != "all": + # use matching so events with multiple zones + # still match on a search where any zone matches + zone_clauses = [] filtered_zones = zones.split(",") - zone_filters = [{f"zones_{zone}": {"$eq": True}} for zone in filtered_zones] - if len(zone_filters) > 1: - embeddings_filters.append({"$or": zone_filters}) - else: - embeddings_filters.append(zone_filters[0]) + + if "None" in filtered_zones: + filtered_zones.remove("None") + zone_clauses.append((Event.zones.length() == 0)) + + for zone in filtered_zones: + zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*')) + + zone_clause = reduce(operator.or_, zone_clauses) + event_filters.append((zone_clause)) if after: - embeddings_filters.append({"start_time": {"$gt": after}}) + event_filters.append((Event.start_time > after)) if before: - embeddings_filters.append({"start_time": {"$lt": before}}) + event_filters.append((Event.start_time < before)) - where = None - if len(embeddings_filters) > 1: - where = {"$and": embeddings_filters} - elif len(embeddings_filters) == 1: - where = embeddings_filters[0] + if time_range != DEFAULT_TIME_RANGE: + # get timezone arg to ensure browser times are used + tz_name = params.timezone + hour_modifier, minute_modifier, _ = get_tz_modifiers(tz_name) + + times = time_range.split(",") + time_after = times[0] + time_before = times[1] + + start_hour_fun = fn.strftime( + "%H:%M", + fn.datetime(Event.start_time, "unixepoch", hour_modifier, minute_modifier), + ) + + # cases where user wants events overnight, ex: from 20:00 to 06:00 + # should use or operator + if time_after > time_before: + event_filters.append( + ( + reduce( + operator.or_, + [(start_hour_fun > time_after), (start_hour_fun < time_before)], + ) + ) + ) + # all other cases should be and operator + else: + event_filters.append((start_hour_fun > time_after)) + event_filters.append((start_hour_fun < time_before)) + + if event_filters: + filtered_event_ids = ( + Event.select(Event.id) + .where(reduce(operator.and_, event_filters)) + .tuples() + .iterator() + ) + event_ids = [event_id[0] for event_id in filtered_event_ids] + + if not event_ids: + return JSONResponse(content=[]) # No events to search on + else: + event_ids = [] + + # Build the Chroma where clause based on the event IDs + where = {"id": {"$in": event_ids}} if event_ids else {} thumb_ids = {} desc_ids = {} diff --git a/frigate/embeddings/embeddings.py b/frigate/embeddings/embeddings.py index ba5363cab..540764c1b 100644 --- a/frigate/embeddings/embeddings.py +++ b/frigate/embeddings/embeddings.py @@ -43,7 +43,7 @@ def get_metadata(event: Event) -> dict: { k: v for k, v in event_dict.items() - if k not in ["id", "thumbnail"] + if k not in ["thumbnail"] and v is not None and isinstance(v, (str, int, float, bool)) } diff --git a/web/src/components/card/SearchThumbnail.tsx b/web/src/components/card/SearchThumbnail.tsx index 385d5ebd4..c595cc85f 100644 --- a/web/src/components/card/SearchThumbnail.tsx +++ b/web/src/components/card/SearchThumbnail.tsx @@ -15,6 +15,7 @@ import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { SearchResult } from "@/types/search"; import useContextMenu from "@/hooks/use-contextmenu"; import { cn } from "@/lib/utils"; +import { TooltipPortal } from "@radix-ui/react-tooltip"; type SearchThumbnailProps = { searchResult: SearchResult; @@ -95,16 +96,18 @@ export default function SearchThumbnail({ - - {[...new Set([searchResult.label])] - .filter( - (item) => item !== undefined && !item.includes("-verified"), - ) - .map((text) => capitalizeFirstLetter(text)) - .sort() - .join(", ") - .replaceAll("-verified", "")} - + + + {[...new Set([searchResult.label])] + .filter( + (item) => item !== undefined && !item.includes("-verified"), + ) + .map((text) => capitalizeFirstLetter(text)) + .sort() + .join(", ") + .replaceAll("-verified", "")} + +
diff --git a/web/src/components/filter/CamerasFilterButton.tsx b/web/src/components/filter/CamerasFilterButton.tsx index 12c5431bf..ef71a96be 100644 --- a/web/src/components/filter/CamerasFilterButton.tsx +++ b/web/src/components/filter/CamerasFilterButton.tsx @@ -112,7 +112,10 @@ export function CamerasFilterButton({
setCurrentCameras([...conf.cameras])} + onClick={() => { + setAllCamerasSelected(false); + setCurrentCameras([...conf.cameras]); + }} > {name}
diff --git a/web/src/components/navigation/Bottombar.tsx b/web/src/components/navigation/Bottombar.tsx index d82f69c60..c30f347e6 100644 --- a/web/src/components/navigation/Bottombar.tsx +++ b/web/src/components/navigation/Bottombar.tsx @@ -27,7 +27,7 @@ function Bottombar() { isPWA && isIOS ? "portrait:items-start portrait:pt-1 landscape:items-center" : "items-center", - isMobile && !isPWA && "h-12 landscape:md:h-16", + isMobile && !isPWA && "h-12 md:h-16", )} > {navItems.map((item) => ( diff --git a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx index 57430ec7a..c9879b8cb 100644 --- a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx +++ b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx @@ -201,21 +201,24 @@ export default function MobileReviewSettingsDrawer({ Calendar - { - onUpdateFilter({ - ...filter, - after: day == undefined ? undefined : day.getTime() / 1000, - before: day == undefined ? undefined : getEndOfDayTimestamp(day), - }); - }} - /> +
+ { + onUpdateFilter({ + ...filter, + after: day == undefined ? undefined : day.getTime() / 1000, + before: + day == undefined ? undefined : getEndOfDayTimestamp(day), + }); + }} + /> +
)}