Repository: blakeblackshear/frigate
Pull request #18220: Fixes
Author: login: NickM-27
Created: 2025-05-14T14:19:35Z | Merged: 2025-05-14T22:44:07Z

## Proposed change
<!--
  Thank you!

  If you're introducing a new feature or significantly refactoring existing functionality,
  we encourage you to start a discussion first. This helps ensure your idea aligns with
  Frigate's development goals.

  Describe what this pull request does and how it will benefit users of Frigate.
  Please describe in detail any considerations, breaking changes, etc. that are
  made in this pull request.
-->

- Adjust HLS logic to trim beginning of first segment
- Use object details dialog instead of separate face dialog in FaceLibrary
- Autotracking RelativeZoom improvements
- Live UI enable / disable camera improvements
- many i18n fixes

## Type of change

- [ ] Dependency upgrade
- [x] Bugfix (non-breaking change which fixes an issue)
- [x] New feature
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code
- [x] Documentation Update

## Additional information

- This PR fixes or closes issue: fixes #
- This PR is related to issue:

## Checklist

<!--
  Put an `x` in the boxes that apply.
-->

- [x] The code change is tested and works locally.
- [x] Local tests pass. **Your PR cannot be merged unless tests pass**
- [x] There is no commented out code in this PR.
- [ ] UI changes including text have used i18n keys and have been added to the `en` locale.
- [x] The code has been formatted using Ruff (`ruff format frigate`)



Diff:
diff --git a/docs/docs/configuration/authentication.md b/docs/docs/configuration/authentication.md
index f83659c52c..84204cd46c 100644
--- a/docs/docs/configuration/authentication.md
+++ b/docs/docs/configuration/authentication.md
@@ -97,7 +97,7 @@ python3 -c 'import secrets; print(secrets.token_hex(64))'
 
 ### Header mapping
 
-If you have disabled Frigate's authentication and your proxy supports passing a header with authenticated usernames and/or roles, you can use the `header_map` config to specify the header name so it is passed to Frigate. For example, the following will map the `X-Forwarded-User` and `X-Forwarded-Role` values. Header names are not case sensitive.
+If you have disabled Frigate's authentication and your proxy supports passing a header with authenticated usernames and/or roles, you can use the `header_map` config to specify the header name so it is passed to Frigate. For example, the following will map the `X-Forwarded-User` and `X-Forwarded-Role` values. Header names are not case sensitive. Multiple values can be included in the role header, but they must be comma-separated.
 
 ```yaml
 proxy:
diff --git a/docs/docs/configuration/face_recognition.md b/docs/docs/configuration/face_recognition.md
index 7a4cde5589..d3170ec750 100644
--- a/docs/docs/configuration/face_recognition.md
+++ b/docs/docs/configuration/face_recognition.md
@@ -105,19 +105,25 @@ When choosing images to include in the face training set it is recommended to al
 
 :::
 
+### Understanding the Train Tab
+
+The Train tab in the face library displays recent face recognition attempts. Detected face images are grouped according to the person they were identified as potentially matching.
+
+Each face image is labeled with a name (or `Unknown`) along with the confidence score of the recognition attempt. While each image can be used to train the system for a specific person, not all images are suitable for training.
+
+Refer to the guidelines below for best practices on selecting images for training.
+
 ### Step 1 - Building a Strong Foundation
 
 When first enabling face recognition it is important to build a foundation of strong images. It is recommended to start by uploading 1-5 photos containing just this person's face. It is important that the person's face in the photo is front-facing and not turned, this will ensure a good starting point.
 
-Then it is recommended to use the `Face Library` tab in Frigate to select and train images for each person as they are detected. When building a strong foundation it is strongly recommended to only train on images that are front-facing. Ignore images from cameras that recognize faces from an angle.
-
-Aim to strike a balance between the quality of images while also having a range of conditions (day / night, different weather conditions, different times of day, etc.) in order to have diversity in the images used for each person and not have over-fitting.
+Then it is recommended to use the `Face Library` tab in Frigate to select and train images for each person as they are detected. When building a strong foundation it is strongly recommended to only train on images that are front-facing. Ignore images from cameras that recognize faces from an angle. Aim to strike a balance between the quality of images while also having a range of conditions (day / night, different weather conditions, different times of day, etc.) in order to have diversity in the images used for each person and not have over-fitting.
 
-Once a person starts to be consistently recognized correctly on images that are front-facing, it is time to move on to the next step.
+You do not want to train images that are 90%+ as these are already being confidently recognized. In this step the goal is to train on clear, lower scoring front-facing images until the majority of front-facing images for a given person are consistently recognized correctly. Then it is time to move on to step 2.
 
 ### Step 2 - Expanding The Dataset
 
-Once front-facing images are performing well, start choosing slightly off-angle images to include for training. It is important to still choose images where enough face detail is visible to recognize someone.
+Once front-facing images are performing well, start choosing slightly off-angle images to include for training. It is important to still choose images where enough face detail is visible to recognize someone, and you still only want to train on images that score lower.
 
 ## FAQ
 
diff --git a/docs/docs/configuration/zones.md b/docs/docs/configuration/zones.md
index a23a3a617f..717dd9df78 100644
--- a/docs/docs/configuration/zones.md
+++ b/docs/docs/configuration/zones.md
@@ -165,7 +165,7 @@ These speed values are output as a number in miles per hour (mph) or kilometers
 
 #### Best practices and caveats
 
-- Speed estimation works best with a straight road or path when your object travels in a straight line across that path. Avoid creating your zone near intersections or anywhere that objects would make a turn. If the bounding box changes shape (either because the object made a turn or became partially obscured, for example), speed estimation will not be accurate.
+- Speed estimation works best with a straight road or path when your object travels in a straight line across that path. Avoid creating your zone near intersections or anywhere that objects would make a turn. A large zone can be used, but it's not required and may even cause issues - if the object's bounding box changes shape (such as when it turns or becomes partially hidden), the speed estimate will be inaccurate. Generally it's best to make your zone large enough to capture a few frames, but small enough so that the bounding box doesn't change size as it travels through the zone.
 - Create a zone where the bottom center of your object's bounding box travels directly through it and does not become obscured at any time. See the photo example above.
 - Depending on the size and location of your zone, you may want to decrease the zone's `inertia` value from the default of 3.
 - The more accurate your real-world dimensions can be measured, the more accurate speed estimation will be. However, due to the way Frigate's tracking algorithm works, you may need to tweak the real-world distance values so that estimated speeds better match real-world speeds.
diff --git a/frigate/api/media.py b/frigate/api/media.py
index 9c56e363d2..717441f366 100644
--- a/frigate/api/media.py
+++ b/frigate/api/media.py
@@ -593,10 +593,12 @@ def run_download(ffmpeg_cmd: list[str], file_path: str):
         clip: Recordings
         for clip in recordings:
             file.write(f"file '{clip.path}'\n")
+
             # if this is the starting clip, add an inpoint
             if clip.start_time < start_ts:
                 file.write(f"inpoint {int(start_ts - clip.start_time)}\n")
-            # if this is the ending clip, add an outpoint
+
+            # if this is the ending clip and end trim is enabled, add an outpoint
             if clip.end_time > end_ts:
                 file.write(f"outpoint {int(end_ts - clip.start_time)}\n")
 
@@ -641,7 +643,12 @@ def run_download(ffmpeg_cmd: list[str], file_path: str):
 @router.get("/vod/{camera_name}/start/{start_ts}/end/{end_ts}")
 def vod_ts(camera_name: str, start_ts: float, end_ts: float):
     recordings = (
-        Recordings.select(Recordings.path, Recordings.duration, Recordings.end_time)
+        Recordings.select(
+            Recordings.path,
+            Recordings.duration,
+            Recordings.end_time,
+            Recordings.start_time,
+        )
         .where(
             Recordings.start_time.between(start_ts, end_ts)
             | Recordings.end_time.between(start_ts, end_ts)
@@ -661,14 +668,19 @@ def vod_ts(camera_name: str, start_ts: float, end_ts: float):
         clip = {"type": "source", "path": recording.path}
         duration = int(recording.duration * 1000)
 
-        # Determine if we need to end the last clip early
+        # adjust start offset if start_ts is after recording.start_time
+        if start_ts > recording.start_time:
+            inpoint = int((start_ts - recording.start_time) * 1000)
+            clip["clipFrom"] = inpoint
+            duration -= inpoint
+
+        # adjust end if recording.end_time is after end_ts
         if recording.end_time > end_ts:
             duration -= int((recording.end_time - end_ts) * 1000)
 
-            if duration == 0:
-                # this means the segment starts right at the end of the requested time range
-                # and it does not need to be included
-                continue
+        if duration <= 0:
+            # skip if the clip has no valid duration
+            continue
 
         if 0 < duration < max_duration_ms:
             clip["keyFrameDurations"] = [duration]
diff --git a/frigate/camera/state.py b/frigate/camera/state.py
index f7b60ed684..e5a9ada9d7 100644
--- a/frigate/camera/state.py
+++ b/frigate/camera/state.py
@@ -282,9 +282,13 @@ def update(
             }
             new_obj.thumbnail_data = thumbnail_data
             tracked_objects[id].thumbnail_data = thumbnail_data
-            self.best_objects[new_obj.obj_data["label"]] = new_obj
+            object_type = new_obj.obj_data["label"]
+            self.best_objects[object_type] = new_obj
 
             # call event handlers
+            for c in self.callbacks["snapshot"]:
+                c(self.name, self.best_objects[object_type], frame_name)
+
             for c in self.callbacks["start"]:
                 c(self.name, new_obj, frame_name)
 
diff --git a/frigate/config/classification.py b/frigate/config/classification.py
index 7f4f39bbdd..6733ade869 100644
--- a/frigate/config/classification.py
+++ b/frigate/config/classification.py
@@ -78,7 +78,7 @@ class FaceRecognitionConfig(FrigateBaseModel):
         le=1.0,
     )
     min_area: int = Field(
-        default=500, title="Min area of face box to consider running face recognition."
+        default=750, title="Min area of face box to consider running face recognition."
     )
     save_attempts: int = Field(
         default=100, ge=0, title="Number of face attempts to save in the train tab."
@@ -91,7 +91,7 @@ class FaceRecognitionConfig(FrigateBaseModel):
 class CameraFaceRecognitionConfig(FrigateBaseModel):
     enabled: bool = Field(default=False, title="Enable face recognition.")
     min_area: int = Field(
-        default=500, title="Min area of face box to consider running face recognition."
+        default=750, title="Min area of face box to consider running face recognition."
     )
 
     model_config = ConfigDict(extra="forbid", protected_namespaces=())
diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py
index 92bfe0ec05..f9fb70d4f1 100644
--- a/frigate/ptz/autotrack.py
+++ b/frigate/ptz/autotrack.py
@@ -1171,7 +1171,20 @@ def _autotrack_move_ptz(self, camera: str, obj: TrackedObject):
             zoom_predicted_movement_time = 0
 
             if np.any(average_velocity):
-                zoom_predicted_movement_time = abs(zoom) * self.zoom_time[camera]
+                # Calculate the intended change in zoom level
+                zoom_change = (1 - abs(zoom)) * (1 if zoom >= 0 else -1)
+
+                # Calculate new zoom level and clamp to [0, 1]
+                new_zoom = max(
+                    0, min(1, self.ptz_metrics[camera].zoom_level.value + zoom_change)
+                )
+
+                # Calculate the actual zoom distance
+                zoom_distance = abs(
+                    new_zoom - self.ptz_metrics[camera].zoom_level.value
+                )
+
+                zoom_predicted_movement_time = zoom_distance * self.zoom_time[camera]
 
                 zoom_predicted_box = (
                     predicted_box
@@ -1188,7 +1201,7 @@ def _autotrack_move_ptz(self, camera: str, obj: TrackedObject):
                 tilt = (0.5 - (centroid_y / camera_height)) * 2
 
             logger.debug(
-                f"{camera}: Zoom amount: {zoom}, zoom predicted time: {zoom_predicted_movement_time}, zoom predicted box: {tuple(zoom_predicted_box)}"
+                f"{camera}: Zoom amount: {zoom}, zoom distance: {zoom_distance}, zoom predicted time: {zoom_predicted_movement_time}, zoom predicted box: {tuple(zoom_predicted_box)}"
             )
 
         self._enqueue_move(camera, obj.obj_data["frame_time"], pan, tilt, zoom)
diff --git a/web/public/locales/en/components/filter.json b/web/public/locales/en/components/filter.json
index 7ec5c752e9..08a0ee2b2c 100644
--- a/web/public/locales/en/components/filter.json
+++ b/web/public/locales/en/components/filter.json
@@ -17,6 +17,7 @@
     }
   },
   "dates": {
+    "selectPreset": "Select a Preset…",
     "all": {
       "title": "All Dates",
       "short": "Dates"
diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json
index e8a5153ee1..13cc494ea6 100644
--- a/web/public/locales/en/views/explore.json
+++ b/web/public/locales/en/views/explore.json
@@ -43,6 +43,8 @@
     "adjustAnnotationSettings": "Adjust annotation settings",
     "scrollViewTips": "Scroll to view the significant moments of this object's lifecycle.",
     "autoTrackingTips": "Bounding box positions will be inaccurate for autotracking cameras.",
+    "count": "{{first}} of {{second}}",
+    "trackedPoint": "Tracked Point",
     "lifecycleItemDesc": {
       "visible": "{{label}} detected",
       "entered_zone": "{{label}} entered {{zones}}",
diff --git a/web/src/components/overlay/detail/ObjectLifecycle.tsx b/web/src/components/overlay/detail/ObjectLifecycle.tsx
index b7c43b2a33..3fc7028544 100644
--- a/web/src/components/overlay/detail/ObjectLifecycle.tsx
+++ b/web/src/components/overlay/detail/ObjectLifecycle.tsx
@@ -525,7 +525,10 @@ export default function ObjectLifecycle({
           {t("objectLifecycle.scrollViewTips")}
         </div>
         <div className="min-w-20 text-right text-sm text-muted-foreground">
-          {current + 1} of {eventSequence.length}
+          {t("objectLifecycle.count", {
+            first: current + 1,
+            second: eventSequence.length,
+          })}
         </div>
       </div>
       {config?.cameras[event.camera]?.onvif.autotracking.enabled_in_config && (
diff --git a/web/src/components/overlay/detail/ObjectPath.tsx b/web/src/components/overlay/detail/ObjectPath.tsx
index 06a47d5710..0101a71f14 100644
--- a/web/src/components/overlay/detail/ObjectPath.tsx
+++ b/web/src/components/overlay/detail/ObjectPath.tsx
@@ -7,6 +7,7 @@ import {
 } from "@/components/ui/tooltip";
 import { TooltipPortal } from "@radix-ui/react-tooltip";
 import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
+import { useTranslation } from "react-i18next";
 
 type ObjectPathProps = {
   positions?: Position[];
@@ -40,6 +41,7 @@ export function ObjectPath({
   onPointClick,
   visible = true,
 }: ObjectPathProps) {
+  const { t } = useTranslation(["views/explore"]);
   const getAbsolutePositions = useCallback(() => {
     if (!imgRef.current || !positions) return [];
     const imgRect = imgRef.current.getBoundingClientRect();
@@ -103,7 +105,7 @@ export function ObjectPath({
             <TooltipContent side="top" className="smart-capitalize">
               {pos.lifecycle_item
                 ? getLifecycleItemDescription(pos.lifecycle_item)
-                : "Tracked point"}
+                : t("objectLifecycle.trackedPoint")}
             </TooltipContent>
           </TooltipPortal>
         </Tooltip>
diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx
index 7a144b53ac..b6e1b0a14c 100644
--- a/web/src/components/overlay/detail/SearchDetailDialog.tsx
+++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx
@@ -864,16 +864,14 @@ function ObjectDetailsTab({
             className={cn("flex w-full flex-row gap-2", isMobile && "flex-col")}
           >
             {config?.semantic_search.enabled &&
+              setSimilarity != undefined &&
               search.data.type == "object" && (
                 <Button
                   className="w-full"
                   aria-label={t("itemMenu.findSimilar.aria")}
                   onClick={() => {
                     setSearch(undefined);
-
-                    if (setSimilarity) {
-                      setSimilarity();
-                    }
+                    setSimilarity();
                   }}
                 >
                   <div className="flex gap-1">
@@ -1101,7 +1099,7 @@ export function ObjectSnapshotTab({
                     <Tooltip>
                       <TooltipTrigger asChild>
                         <a
-                          href={`${baseUrl}api/events/${search?.id}/snapshot.jpg`}
+                          href={`${baseUrl}api/events/${search?.id}/snapshot.jpg?bbox=1`}
                           download={`${search?.camera}_${search?.label}.jpg`}
                         >
                           <Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500">
@@ -1270,7 +1268,7 @@ export function VideoTab({ search }: VideoTabProps) {
               <TooltipTrigger asChild>
                 <a
                   download
-                  href={`${baseUrl}api/${search.camera}/start/${search.start_time}/end/${endTime}/clip.mp4`}
+                  href={`${baseUrl}api/${search.camera}/start/${search.start_time}/end/${endTime}/clip.mp4?trim=end`}
                 >
                   <Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500">
                     <FaDownload className="size-4 text-white" />
diff --git a/web/src/components/overlay/dialog/PlatformAwareDialog.tsx b/web/src/components/overlay/dialog/PlatformAwareDialog.tsx
index 46966089b9..ad03ba7ebf 100644
--- a/web/src/components/overlay/dialog/PlatformAwareDialog.tsx
+++ b/web/src/components/overlay/dialog/PlatformAwareDialog.tsx
@@ -91,7 +91,7 @@ export function PlatformAwareSheet({
               className="mx-2"
               onClose={() => onOpenChange(false)}
             >
-              <MobilePageTitle>More Filters</MobilePageTitle>
+              <MobilePageTitle>{title}</MobilePageTitle>
             </MobilePageHeader>
             <div className={contentClassName}>{content}</div>
           </MobilePageContent>
diff --git a/web/src/components/overlay/dialog/SearchFilterDialog.tsx b/web/src/components/overlay/dialog/SearchFilterDialog.tsx
index bf0069911a..e3c54e0215 100644
--- a/web/src/components/overlay/dialog/SearchFilterDialog.tsx
+++ b/web/src/components/overlay/dialog/SearchFilterDialog.tsx
@@ -224,6 +224,7 @@ export default function SearchFilterDialog({
   return (
     <PlatformAwareSheet
       trigger={trigger}
+      title={t("more")}
       content={content}
       contentClassName={cn(
         "w-auto lg:min-w-[275px] scrollbar-container h-full overflow-auto px-4",
diff --git a/web/src/components/ui/calendar-range.tsx b/web/src/components/ui/calendar-range.tsx
index 96e2881fda..218cc79bbb 100644
--- a/web/src/components/ui/calendar-range.tsx
+++ b/web/src/components/ui/calendar-range.tsx
@@ -369,7 +369,11 @@ export function DateRangePicker({
                 }}
               >
                 <SelectTrigger className="mx-auto mb-2 w-[180px]">
-                  <SelectValue placeholder="Select..." />
+                  <SelectValue
+                    placeholder={t("dates.selectPreset", {
+                      ns: "components/filter",
+                    })}
+                  />
                 </SelectTrigger>
                 <SelectContent>
                   {PRESETS.map((preset) => (
diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx
index 921f8d5324..c31f59190f 100644
--- a/web/src/pages/FaceLibrary.tsx
+++ b/web/src/pages/FaceLibrary.tsx
@@ -31,11 +31,6 @@ import {
   DropdownMenuTrigger,
   DropdownMenuSeparator,
 } from "@/components/ui/dropdown-menu";
-import {
-  Popover,
-  PopoverContent,
-  PopoverTrigger,
-} from "@/components/ui/popover";
 import { Toaster } from "@/components/ui/sonner";
 import {
   Tooltip,
@@ -43,7 +38,6 @@ import {
   TooltipTrigger,
 } from "@/components/ui/tooltip";
 import useContextMenu from "@/hooks/use-contextmenu";
-import { useFormattedTimestamp } from "@/hooks/use-date-utils";
 import useKeyboardListener from "@/hooks/use-keyboard-listener";
 import useOptimisticState from "@/hooks/use-optimistic-state";
 import { cn } from "@/lib/utils";
@@ -58,7 +52,6 @@ import { Trans, useTranslation } from "react-i18next";
 import {
   LuFolderCheck,
   LuImagePlus,
-  LuInfo,
   LuPencil,
   LuRefreshCw,
   LuScanFace,
@@ -68,6 +61,10 @@ import {
 import { useNavigate } from "react-router-dom";
 import { toast } from "sonner";
 import useSWR from "swr";
+import SearchDetailDialog, {
+  SearchTab,
+} from "@/components/overlay/detail/SearchDetailDialog";
+import { SearchResult } from "@/types/search";
 
 export default function FaceLibrary() {
   const { t } = useTranslation(["views/faceLibrary"]);
@@ -663,18 +660,7 @@ function TrainingGrid({
   // selection
 
   const [selectedEvent, setSelectedEvent] = useState<Event>();
-
-  const formattedDate = useFormattedTimestamp(
-    selectedEvent?.start_time ?? 0,
-    config?.ui.time_format == "24hour"
-      ? t("time.formattedTimestampMonthDayYearHourMinute.24hour", {
-          ns: "common",
-        })
-      : t("time.formattedTimestampMonthDayYearHourMinute.12hour", {
-          ns: "common",
-        }),
-    config?.ui.timezone,
-  );
+  const [dialogTab, setDialogTab] = useState<SearchTab>("details");
 
   if (attemptImages.length == 0) {
     return (
@@ -687,66 +673,16 @@ function TrainingGrid({
 
   return (
     <>
-      <Dialog
-        open={selectedEvent != undefined}
-        onOpenChange={(open) => {
-          if (!open) {
-            setSelectedEvent(undefined);
-          }
-        }}
-      >
-        <DialogContent
-          className={cn(
-            "",
-            selectedEvent?.has_snapshot && isDesktop && "max-w-7xl",
-          )}
-        >
-          <DialogHeader>
-            <DialogTitle>{t("details.face")}</DialogTitle>
-            <DialogDescription>{t("details.faceDesc")}</DialogDescription>
-          </DialogHeader>
-          <div className="flex flex-col gap-1.5">
-            <div className="text-sm text-primary/40">{t("details.person")}</div>
-            <div className="text-sm smart-capitalize">
-              {selectedEvent?.sub_label ?? t("details.unknown")}
-            </div>
-          </div>
-          {selectedEvent?.data.sub_label_score && (
-            <div className="flex flex-col gap-1.5">
-              <div className="text-sm text-primary/40">
-                <div className="flex flex-row items-center gap-1">
-                  {t("details.subLabelScore")}
-                  <Popover>
-                    <PopoverTrigger asChild>
-                      <div className="cursor-pointer p-0">
-                        <LuInfo className="size-4" />
-                        <span className="sr-only">Info</span>
-                      </div>
-                    </PopoverTrigger>
-                    <PopoverContent className="w-80">
-                      {t("details.scoreInfo")}
-                    </PopoverContent>
-                  </Popover>
-                </div>
-              </div>
-              <div className="text-sm smart-capitalize">
-                {Math.round((selectedEvent?.data?.sub_label_score || 0) * 100)}%
-              </div>
-            </div>
-          )}
-          <div className="flex flex-col gap-1.5">
-            <div className="text-sm text-primary/40">
-              {t("details.timestamp")}
-            </div>
-            <div className="text-sm">{formattedDate}</div>
-          </div>
-          <img
-            className="mx-auto max-h-[60dvh] object-contain"
-            loading="lazy"
-            src={`${baseUrl}api/events/${selectedEvent?.id}/${selectedEvent?.has_snapshot ? "snapshot.jpg" : "thumbnail.jpg"}`}
-          />
-        </DialogContent>
-      </Dialog>
+      <SearchDetailDialog
+        search={
+          selectedEvent ? (selectedEvent as unknown as SearchResult) : undefined
+        }
+        page={dialogTab}
+        setSimilarity={undefined}
+        setSearchPage={setDialogTab}
+        setSearch={(search) => setSelectedEvent(search as unknown as Event)}
+        setInputFocused={() => {}}
+      />
 
       <div className="scrollbar-container flex flex-wrap gap-2 overflow-y-scroll p-1">
         {Object.entries(faceGroups).map(([key, group]) => {
@@ -853,11 +789,18 @@ function FaceAttemptGroup({
       }}
     >
       <div className="flex flex-row justify-between">
-        <div className="select-none smart-capitalize">
-          Person
-          {event?.sub_label
-            ? `: ${event.sub_label} (${Math.round((event.data.sub_label_score || 0) * 100)}%)`
-            : ": " + t("details.unknown")}
+        <div className="flex flex-col gap-1">
+          <div className="select-none smart-capitalize">
+            Person
+            {event?.sub_label
+              ? `: ${event.sub_label} (${Math.round((event.data.sub_label_score || 0) * 100)}%)`
+              : ": " + t("details.unknown")}
+          </div>
+          <TimeAgo
+            className="text-sm text-secondary-foreground"
+            time={group[0].timestamp * 1000}
+            dense
+          />
         </div>
         {event && (
           <Tooltip>
@@ -950,6 +893,14 @@ function FaceAttempt({
     onClick(data, true);
   });
 
+  const imageArea = useMemo(() => {
+    if (!imgRef.current) {
+      return undefined;
+    }
+
+    return imgRef.current.naturalWidth * imgRef.current.naturalHeight;
+  }, [imgRef]);
+
   // api calls
 
   const onTrainAttempt = useCallback(
@@ -1021,13 +972,11 @@ function FaceAttempt({
               onClick(data, e.metaKey || e.ctrlKey);
             }}
           />
-          <div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white">
-            <TimeAgo
-              className="text-white"
-              time={data.timestamp * 1000}
-              dense
-            />
-          </div>
+          {imageArea != undefined && (
+            <div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white">
+              {imageArea}px
+            </div>
+          )}
         </div>
         <div className="select-none p-2">
           <div className="flex w-full flex-row items-center justify-between gap-2">
diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx
index 3294888c0d..b57f0c8a7c 100644
--- a/web/src/views/live/LiveCameraView.tsx
+++ b/web/src/views/live/LiveCameraView.tsx
@@ -631,6 +631,7 @@ export default function LiveCameraView({
         <div className="flex flex-col items-center justify-center">
           <PtzControlPanel
             camera={camera.name}
+            enabled={cameraEnabled}
             clickOverlay={clickOverlay}
             setClickOverlay={setClickOverlay}
           />
@@ -689,15 +690,19 @@ function TooltipButton({
 
 function PtzControlPanel({
   camera,
+  enabled,
   clickOverlay,
   setClickOverlay,
 }: {
   camera: string;
+  enabled: boolean;
   clickOverlay: boolean;
   setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>;
 }) {
   const { t } = useTranslation(["views/live"]);
-  const { data: ptz } = useSWR<CameraPtzInfo>(`${camera}/ptz/info`);
+  const { data: ptz } = useSWR<CameraPtzInfo>(
+    enabled ? `${camera}/ptz/info` : null,
+  );
 
   const { send: sendPtz } = usePtzCommand(camera);
 


Reviews:
user: login: hawkeye217
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T17:18:09Z
user: login: NickM-27
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T17:23:21Z
user: login: hawkeye217
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T17:25:00Z
user: login: hawkeye217
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-14T22:33:29Z

Review comments:
diff_hunk: @@ -369,7 +369,9 @@ export function DateRangePicker({
                 }}
               >
                 <SelectTrigger className="mx-auto mb-2 w-[180px]">
-                  <SelectValue placeholder="Select..." />
+                  <SelectValue
+                    placeholder={`${t("export.select", { ns: "components/dialog" })}...`}
path: web/src/components/ui/calendar-range.tsx
user: login: hawkeye217
body: Probably want to include the ellipsis as part of the key here
created_at: 2025-05-14T17:18:03Z
author_association: COLLABORATOR
diff_hunk: @@ -369,7 +369,9 @@ export function DateRangePicker({
                 }}
               >
                 <SelectTrigger className="mx-auto mb-2 w-[180px]">
-                  <SelectValue placeholder="Select..." />
+                  <SelectValue
+                    placeholder={`${t("export.select", { ns: "components/dialog" })}...`}
path: web/src/components/ui/calendar-range.tsx
user: login: NickM-27
body: it doesn't fit in the other area that it is used, the export dialog
created_at: 2025-05-14T17:23:21Z
author_association: COLLABORATOR
diff_hunk: @@ -369,7 +369,9 @@ export function DateRangePicker({
                 }}
               >
                 <SelectTrigger className="mx-auto mb-2 w-[180px]">
-                  <SelectValue placeholder="Select..." />
+                  <SelectValue
+                    placeholder={`${t("export.select", { ns: "components/dialog" })}...`}
path: web/src/components/ui/calendar-range.tsx
user: login: hawkeye217
body: I'd just make a separate key then, since I'm not sure the ellipsis will translate correctly into other languages, especially RTL languages
created_at: 2025-05-14T17:24:59Z
author_association: COLLABORATOR

PR comments:
user: login: netlify[bot]
created_at: 2025-05-14T14:19:39Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *frigate-docs* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 10c79cce22cc6a0893de2aeef9c71ccca9a5a269 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/projects/frigate-docs/deploys/68251b1aee7f8b0008c8e21d |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-18220--frigate-docs.netlify.app](https://deploy-preview-18220--frigate-docs.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTE4MjIwLS1mcmlnYXRlLWRvY3MubmV0bGlmeS5hcHAifQ.j7S54_htpWqRCr-CwFv4lXnsd-2AnFMglLlxTN5z_ro)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [frigate-docs Preview](https://deploy-preview-18220--frigate-docs.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify project configuration](https://app.netlify.com/projects/frigate-docs/configuration/notifications#deploy-notifications)._
author_association: NONE
user: login: lawul
created_at: 2025-05-18T22:03:48Z
body: Starting with this PR (dev build d3d05fa) I noticed two things:

- When I submit a screenshot to frigate+ directly via camera history (not explore area), the screenshot shown in the frigate+ UI is a few seconds (<10) off.
- When I'm in camera history, switch to events-view and click an event, the recording position is not set to the start of the event but it's also a few seconds off into the ongoing event (I think the same amount like the frigate+ screenshots).

If necessary, I could also create a bug report in the discussions and provide more info about my setup, but since I can pin point it to this PR I thought you might know what change could have introduced this?
author_association: NONE
user: login: NickM-27
created_at: 2025-05-18T22:10:50Z
body: that is interesting as now that you mention it I did have some frigate+ submissions via the recording view that did not match up
author_association: COLLABORATOR
user: login: NickM-27
created_at: 2025-05-18T22:58:59Z
body: fixed in https://github.com/blakeblackshear/frigate/pull/18289
author_association: COLLABORATOR