Compare commits

..
4 Commits
Author SHA1 Message Date
Kai Curry b6189a6a74 Simplify MQTT tracked_object_update docs with inline comments
Move scoring logic details to face recognition docs and keep
MQTT reference concise with inline field comments and links.
2026-02-09 16:27:37 -01:00
Kai Curry 565d7ba8b4 Clarify score is a running weighted average 2026-02-09 14:52:41 -01:00
Kai CurryandGitHub 535fe7aaef Fix score decimal in MQTT face recognition documentation
`0.0` in JSON is just `0`.
2026-02-09 14:48:34 -01:00
Kai Curry ecad939a62 Add detail to face recognition MQTT update docs
Clarify that the weighted average favors larger faces and
higher-confidence detections, that unknown attempts are excluded,
and document when name/score will be null/0.0.
2026-02-09 14:36:35 -01:00
21 changed files with 150 additions and 383 deletions
@@ -166,10 +166,6 @@ In this example:
- If no mapping matches, Frigate falls back to `default_role` if configured.
- If `role_map` is not defined, Frigate assumes the role header directly contains `admin`, `viewer`, or a custom role name.
**Note on matching semantics:**
- Admin precedence: if the `admin` mapping matches, Frigate resolves the session to `admin` to avoid accidental downgrade when a user belongs to multiple groups (for example both `admin` and `viewer` groups).
#### Port Considerations
**Authenticated Port (8971)**
+10 -6
View File
@@ -120,7 +120,7 @@ Message published for each changed tracked object. The first message is publishe
### `frigate/tracked_object_update`
Message published for updates to tracked object metadata, for example:
Message published for updates to tracked object metadata. All messages include an `id` field which is the tracked object's event ID, and can be used to look up the event via the API or match it to items in the UI.
#### Generative AI Description Update
@@ -134,12 +134,14 @@ Message published for updates to tracked object metadata, for example:
#### Face Recognition Update
Published after each recognition attempt, regardless of whether the score meets `recognition_threshold`. See the [Face Recognition](/configuration/face_recognition) documentation for details on how scoring works.
```json
{
"type": "face",
"id": "1607123955.475377-mxklsc",
"name": "John",
"score": 0.95,
"name": "John", // best matching person, or null if no match
"score": 0.95, // running weighted average across all recognition attempts
"camera": "front_door_cam",
"timestamp": 1607123958.748393
}
@@ -147,13 +149,15 @@ Message published for updates to tracked object metadata, for example:
#### License Plate Recognition Update
Published when a license plate is recognized on a car object. See the [License Plate Recognition](/configuration/lpr) documentation for details.
```json
{
"type": "lpr",
"id": "1607123955.475377-mxklsc",
"name": "John's Car",
"plate": "123ABC",
"score": 0.95,
"name": "John's Car", // known name for the plate, or null
"plate": "123ABC", // recognized plate string
"score": 0.95, // recognition confidence
"camera": "driveway_cam",
"timestamp": 1607123958.748393
}
+4 -11
View File
@@ -439,11 +439,10 @@ def resolve_role(
Determine the effective role for a request based on proxy headers and configuration.
Order of resolution:
1. If a role header is defined in proxy_config.header_map.role:
- If a role_map is configured, treat the header as group claims
(split by proxy_config.separator) and map to roles.
Admin matches short-circuit to admin.
- If no role_map is configured, treat the header as role names directly.
1. If a role header is defined in proxy_config.header_map.role:
- If a role_map is configured, treat the header as group claims
(split by proxy_config.separator) and map to roles.
- If no role_map is configured, treat the header as role names directly.
2. If no valid role is found, return proxy_config.default_role if it's valid in config_roles, else 'viewer'.
Args:
@@ -493,12 +492,6 @@ def resolve_role(
}
logger.debug("Matched roles from role_map: %s", matched_roles)
# If admin matches, prioritize it to avoid accidental downgrade when
# users belong to both admin and lower-privilege groups.
if "admin" in matched_roles and "admin" in config_roles:
logger.debug("Resolved role (with role_map) to 'admin'.")
return "admin"
if matched_roles:
resolved = next(
(r for r in config_roles if r in matched_roles), validated_default
+16 -26
View File
@@ -69,25 +69,6 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.events])
def _build_attribute_filter_clause(attributes: str):
filtered_attributes = [
attr.strip() for attr in attributes.split(",") if attr.strip()
]
attribute_clauses = []
for attr in filtered_attributes:
attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*')
escaped_attr = json.dumps(attr, ensure_ascii=True)[1:-1]
if escaped_attr != attr:
attribute_clauses.append(Event.data.cast("text") % f'*:"{escaped_attr}"*')
if not attribute_clauses:
return None
return reduce(operator.or_, attribute_clauses)
@router.get(
"/events",
response_model=list[EventResponse],
@@ -212,9 +193,14 @@ def events(
if attributes != "all":
# Custom classification results are stored as data[model_name] = result_value
attribute_clause = _build_attribute_filter_clause(attributes)
if attribute_clause is not None:
clauses.append(attribute_clause)
filtered_attributes = attributes.split(",")
attribute_clauses = []
for attr in filtered_attributes:
attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*')
attribute_clause = reduce(operator.or_, attribute_clauses)
clauses.append(attribute_clause)
if recognized_license_plate != "all":
filtered_recognized_license_plates = recognized_license_plate.split(",")
@@ -522,7 +508,7 @@ def events_search(
cameras = params.cameras
labels = params.labels
sub_labels = params.sub_labels
attributes = unquote(params.attributes)
attributes = params.attributes
zones = params.zones
after = params.after
before = params.before
@@ -621,9 +607,13 @@ def events_search(
if attributes != "all":
# Custom classification results are stored as data[model_name] = result_value
attribute_clause = _build_attribute_filter_clause(attributes)
if attribute_clause is not None:
event_filters.append(attribute_clause)
filtered_attributes = attributes.split(",")
attribute_clauses = []
for attr in filtered_attributes:
attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*')
event_filters.append(reduce(operator.or_, attribute_clauses))
if zones != "all":
zone_clauses = []
@@ -658,7 +658,6 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
def handle_request(self, topic, request_data):
if topic == EmbeddingsRequestEnum.reload_classification_model.value:
if request_data.get("model_name") == self.model_config.name:
self.__build_detector()
logger.info(
f"Successfully loaded updated model for {self.model_config.name}"
)
-13
View File
@@ -6,7 +6,6 @@ from typing import Dict
from frigate.comms.events_updater import EventEndPublisher, EventUpdateSubscriber
from frigate.config import FrigateConfig
from frigate.config.classification import ObjectClassificationType
from frigate.events.types import EventStateEnum, EventTypeEnum
from frigate.models import Event
from frigate.util.builtin import to_relative_box
@@ -248,18 +247,6 @@ class EventProcessor(threading.Thread):
"recognized_license_plate"
][1]
# only overwrite attribute-type custom model fields in the database if they're set
for name, model_config in self.config.classification.custom.items():
if (
model_config.object_config
and model_config.object_config.classification_type
== ObjectClassificationType.attribute
):
value = event_data.get(name)
if value is not None:
event[Event.data][name] = value[0]
event[Event.data][f"{name}_score"] = value[1]
(
Event.insert(event)
.on_conflict(
-51
View File
@@ -168,57 +168,6 @@ class TestHttpApp(BaseTestHttp):
assert events[0]["id"] == id
assert events[1]["id"] == id2
def test_get_event_list_match_multilingual_attribute(self):
event_id = "123456.zh"
attribute = "中文标签"
with AuthTestClient(self.app) as client:
super().insert_mock_event(event_id, data={"custom_attr": attribute})
events = client.get("/events", params={"attributes": attribute}).json()
assert len(events) == 1
assert events[0]["id"] == event_id
events = client.get(
"/events", params={"attributes": "%E4%B8%AD%E6%96%87%E6%A0%87%E7%AD%BE"}
).json()
assert len(events) == 1
assert events[0]["id"] == event_id
def test_events_search_match_multilingual_attribute(self):
event_id = "123456.zh.search"
attribute = "中文标签"
mock_embeddings = Mock()
mock_embeddings.search_thumbnail.return_value = [(event_id, 0.05)]
self.app.frigate_config.semantic_search.enabled = True
self.app.embeddings = mock_embeddings
with AuthTestClient(self.app) as client:
super().insert_mock_event(event_id, data={"custom_attr": attribute})
events = client.get(
"/events/search",
params={
"search_type": "similarity",
"event_id": event_id,
"attributes": attribute,
},
).json()
assert len(events) == 1
assert events[0]["id"] == event_id
events = client.get(
"/events/search",
params={
"search_type": "similarity",
"event_id": event_id,
"attributes": "%E4%B8%AD%E6%96%87%E6%A0%87%E7%AD%BE",
},
).json()
assert len(events) == 1
assert events[0]["id"] == event_id
def test_get_good_event(self):
id = "123456.random"
-15
View File
@@ -31,21 +31,6 @@ class TestProxyRoleResolution(unittest.TestCase):
role = resolve_role(headers, self.proxy_config, self.config_roles)
self.assertEqual(role, "admin")
def test_role_map_or_matching(self):
config = self.proxy_config
config.header_map.role_map = {
"admin": ["group_admin", "group_privileged"],
}
# OR semantics: a single matching group should map to the role
headers = {"x-remote-role": "group_admin"}
role = resolve_role(headers, config, self.config_roles)
self.assertEqual(role, "admin")
headers = {"x-remote-role": "group_admin|group_privileged"}
role = resolve_role(headers, config, self.config_roles)
self.assertEqual(role, "admin")
def test_direct_role_header_with_separator(self):
config = self.proxy_config
config.header_map.role_map = None # disable role_map
+1 -10
View File
@@ -33,7 +33,6 @@ from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
CameraConfigUpdateSubscriber,
)
from frigate.config.classification import ObjectClassificationType
from frigate.const import (
FAST_QUEUE_TIMEOUT,
UPDATE_CAMERA_ACTIVITY,
@@ -760,16 +759,8 @@ class TrackedObjectProcessor(threading.Thread):
self.update_mqtt_motion(camera, frame_time, motion_boxes)
attribute_model_names = [
name
for name, model_config in self.config.classification.custom.items()
if model_config.object_config
and model_config.object_config.classification_type
== ObjectClassificationType.attribute
]
tracked_objects = [
o.to_dict(attribute_model_names=attribute_model_names)
for o in camera_state.tracked_objects.values()
o.to_dict() for o in camera_state.tracked_objects.values()
]
# publish info on this frame
+1 -9
View File
@@ -376,10 +376,7 @@ class TrackedObject:
)
return (thumb_update, significant_change, path_update, autotracker_update)
def to_dict(
self,
attribute_model_names: list[str] | None = None,
) -> dict[str, Any]:
def to_dict(self) -> dict[str, Any]:
event = {
"id": self.obj_data["id"],
"camera": self.camera_config.name,
@@ -414,11 +411,6 @@ class TrackedObject:
"path_data": self.path_data.copy(),
"recognized_license_plate": self.obj_data.get("recognized_license_plate"),
}
if attribute_model_names is not None:
for name in attribute_model_names:
value = self.obj_data.get(name)
if value is not None:
event[name] = value
return event
+3 -21
View File
@@ -214,7 +214,6 @@ class CameraWatchdog(threading.Thread):
self.latest_valid_segment_time: float = 0
self.latest_invalid_segment_time: float = 0
self.latest_cache_segment_time: float = 0
self.record_enable_time: datetime | None = None
def _update_enabled_state(self) -> bool:
"""Fetch the latest config and update enabled state."""
@@ -262,9 +261,6 @@ class CameraWatchdog(threading.Thread):
def run(self) -> None:
if self._update_enabled_state():
self.start_all_ffmpeg()
# If recording is enabled at startup, set the grace period timer
if self.config.record.enabled:
self.record_enable_time = datetime.now().astimezone(timezone.utc)
time.sleep(self.sleeptime)
while not self.stop_event.wait(self.sleeptime):
@@ -274,15 +270,13 @@ class CameraWatchdog(threading.Thread):
self.logger.debug(f"Enabling camera {self.config.name}")
self.start_all_ffmpeg()
# reset all timestamps and record the enable time for grace period
# reset all timestamps
self.latest_valid_segment_time = 0
self.latest_invalid_segment_time = 0
self.latest_cache_segment_time = 0
self.record_enable_time = datetime.now().astimezone(timezone.utc)
else:
self.logger.debug(f"Disabling camera {self.config.name}")
self.stop_all_ffmpeg()
self.record_enable_time = None
# update camera status
self.requestor.send_data(
@@ -367,12 +361,6 @@ class CameraWatchdog(threading.Thread):
if self.config.record.enabled and "record" in p["roles"]:
now_utc = datetime.now().astimezone(timezone.utc)
# Check if we're within the grace period after enabling recording
# Grace period: 90 seconds allows time for ffmpeg to start and create first segment
in_grace_period = self.record_enable_time is not None and (
now_utc - self.record_enable_time
) < timedelta(seconds=90)
latest_cache_dt = (
datetime.fromtimestamp(
self.latest_cache_segment_time, tz=timezone.utc
@@ -398,16 +386,10 @@ class CameraWatchdog(threading.Thread):
)
# ensure segments are still being created and that they have valid video data
# Skip checks during grace period to allow segments to start being created
cache_stale = not in_grace_period and now_utc > (
latest_cache_dt + timedelta(seconds=120)
)
valid_stale = not in_grace_period and now_utc > (
latest_valid_dt + timedelta(seconds=120)
)
cache_stale = now_utc > (latest_cache_dt + timedelta(seconds=120))
valid_stale = now_utc > (latest_valid_dt + timedelta(seconds=120))
invalid_stale_condition = (
self.latest_invalid_segment_time > 0
and not in_grace_period
and now_utc > (latest_invalid_dt + timedelta(seconds=120))
and self.latest_valid_segment_time
<= self.latest_invalid_segment_time
@@ -1,7 +1,6 @@
{
"restart": {
"title": "Are you sure you want to restart Frigate?",
"description": "This will briefly stop Frigate while it restarts.",
"button": "Restart",
"restarting": {
"title": "Frigate is Restarting",
+13 -24
View File
@@ -19,8 +19,6 @@ import { Button } from "../ui/button";
import { FaCircleCheck } from "react-icons/fa6";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
import { getTranslatedLabel } from "@/utils/i18n";
import { formatList } from "@/utils/stringUtil";
type AnimatedEventCardProps = {
event: ReviewSegment;
@@ -52,35 +50,26 @@ export function AnimatedEventCard({
fetchPreviews: !currentHour,
});
const getEventType = useCallback(
(text: string) => {
if (event.data.sub_labels?.includes(text)) return "manual";
if (event.data.audio.includes(text)) return "audio";
return "object";
},
[event],
);
const tooltipText = useMemo(() => {
if (event?.data?.metadata?.title) {
return event.data.metadata.title;
}
return (
`${formatList(
[
...new Set([
...(event.data.objects || []),
...(event.data.sub_labels || []),
...(event.data.audio || []),
]),
]
.filter((item) => item !== undefined && !item.includes("-verified"))
.map((text) => getTranslatedLabel(text, getEventType(text)))
.sort(),
)} ` + t("detected")
`${[
...new Set([
...(event.data.objects || []),
...(event.data.sub_labels || []),
...(event.data.audio || []),
]),
]
.filter((item) => item !== undefined && !item.includes("-verified"))
.map((text) => text.charAt(0).toUpperCase() + text.substring(1))
.sort()
.join(", ")
.replaceAll("-verified", "")} ` + t("detected")
);
}, [event, getEventType, t]);
}, [event, t]);
// visibility
+15 -22
View File
@@ -33,14 +33,13 @@ import axios from "axios";
import { toast } from "sonner";
import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { Button, buttonVariants } from "../ui/button";
import { Trans, useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
import { LuCircle } from "react-icons/lu";
import { MdAutoAwesome } from "react-icons/md";
import { GenAISummaryDialog } from "../overlay/chip/GenAISummaryChip";
import { getTranslatedLabel } from "@/utils/i18n";
import { formatList } from "@/utils/stringUtil";
type ReviewCardProps = {
event: ReviewSegment;
@@ -124,12 +123,6 @@ export default function ReviewCard({
}
}, [bypassDialogRef, onDelete]);
const getEventType = (text: string) => {
if (event.data.sub_labels?.includes(text)) return "manual";
if (event.data.audio.includes(text)) return "audio";
return "object";
};
const content = (
<div
className="relative flex w-full cursor-pointer flex-col gap-1.5"
@@ -204,20 +197,20 @@ export default function ReviewCard({
</div>
</TooltipTrigger>
<TooltipContent className="smart-capitalize">
{formatList(
[
...new Set([
...(event.data.objects || []),
...(event.data.sub_labels || []),
...(event.data.audio || []),
]),
]
.filter(
(item) => item !== undefined && !item.includes("-verified"),
)
.map((text) => getTranslatedLabel(text, getEventType(text)))
.sort(),
)}
{[
...new Set([
...(event.data.objects || []),
...(event.data.sub_labels || []),
...(event.data.audio || []),
]),
]
.filter(
(item) => item !== undefined && !item.includes("-verified"),
)
.map((text) => capitalizeFirstLetter(text))
.sort()
.join(", ")
.replaceAll("-verified", "")}
</TooltipContent>
</Tooltip>
<TimeAgo
+1 -49
View File
@@ -42,20 +42,12 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { isDesktop, isMobile } from "react-device-detect";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerTitle,
DrawerTrigger,
} from "../ui/drawer";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogPortal,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { TooltipPortal } from "@radix-ui/react-tooltip";
@@ -202,16 +194,6 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
: "max-h-[75dvh] overflow-hidden p-2"
}
>
{!isDesktop && (
<>
<DrawerTitle className="sr-only">
{t("menu.settings")}
</DrawerTitle>
<DrawerDescription className="sr-only">
{t("menu.settings")}
</DrawerDescription>
</>
)}
<div className="scrollbar-container w-full flex-col overflow-y-auto overflow-x-hidden">
{isMobile && (
<div className="mb-2">
@@ -373,16 +355,6 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
: "scrollbar-container max-h-[75dvh] w-[92%] overflow-y-scroll rounded-lg md:rounded-2xl"
}
>
{!isDesktop && (
<>
<DialogTitle className="sr-only">
{t("menu.languages")}
</DialogTitle>
<DialogDescription className="sr-only">
{t("menu.languages")}
</DialogDescription>
</>
)}
<span tabIndex={0} className="sr-only" />
{languages.map(({ code, label }) => (
<MenuItem
@@ -423,16 +395,6 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
isDesktop ? "" : "w-[92%] rounded-lg md:rounded-2xl"
}
>
{!isDesktop && (
<>
<DialogTitle className="sr-only">
{t("menu.darkMode.label")}
</DialogTitle>
<DialogDescription className="sr-only">
{t("menu.darkMode.label")}
</DialogDescription>
</>
)}
<span tabIndex={0} className="sr-only" />
<MenuItem
className={
@@ -510,16 +472,6 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
isDesktop ? "" : "w-[92%] rounded-lg md:rounded-2xl"
}
>
{!isDesktop && (
<>
<DialogTitle className="sr-only">
{t("menu.theme.label")}
</DialogTitle>
<DialogDescription className="sr-only">
{t("menu.theme.label")}
</DialogDescription>
</>
)}
<span tabIndex={0} className="sr-only" />
{colorSchemes.map((scheme) => (
<MenuItem
@@ -4,7 +4,6 @@ import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
@@ -38,12 +37,6 @@ export default function RestartDialog({
const [restartingSheetOpen, setRestartingSheetOpen] = useState(false);
const [countdown, setCountdown] = useState(60);
const clearBodyPointerEvents = () => {
if (typeof document !== "undefined") {
document.body.style.pointerEvents = "";
}
};
useEffect(() => {
setRestartDialogOpen(isOpen);
}, [isOpen]);
@@ -81,25 +74,14 @@ export default function RestartDialog({
<>
<AlertDialog
open={restartDialogOpen}
onOpenChange={(open) => {
if (!open) {
setRestartDialogOpen(false);
onClose();
clearBodyPointerEvents();
}
onOpenChange={() => {
setRestartDialogOpen(false);
onClose();
}}
>
<AlertDialogContent
onCloseAutoFocus={(event) => {
event.preventDefault();
clearBodyPointerEvents();
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("restart.title")}</AlertDialogTitle>
<AlertDialogDescription className="sr-only">
{t("restart.description")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
+14 -15
View File
@@ -371,23 +371,22 @@ export default function LivePlayer({
</TooltipTrigger>
</div>
<TooltipPortal>
<TooltipContent>
<TooltipContent className="smart-capitalize">
{formatList(
[
...new Set(
(objects || [])
.map(({ label, sub_label }) => {
const isManual = label.endsWith("verified");
const text = isManual ? sub_label : label;
const type = isManual ? "manual" : "object";
return getTranslatedLabel(text, type);
})
.filter(
(translated) =>
translated && !translated.includes("-verified"),
),
),
].sort(),
...new Set([
...(objects || []).map(({ label, sub_label }) =>
label.endsWith("verified")
? sub_label
: label.replaceAll("_", " "),
),
]),
]
.filter((label) => label?.includes("-verified") == false)
.map((label) =>
getTranslatedLabel(label.replace("-verified", "")),
)
.sort(),
)}
</TooltipContent>
</TooltipPortal>
@@ -28,7 +28,6 @@ import { useTranslation } from "react-i18next";
import { FaExclamationTriangle } from "react-icons/fa";
import { MdOutlinePersonSearch } from "react-icons/md";
import { getTranslatedLabel } from "@/utils/i18n";
import { formatList } from "@/utils/stringUtil";
type PreviewPlayerProps = {
review: ReviewSegment;
@@ -183,8 +182,9 @@ export default function PreviewThumbnailPlayer({
);
const getEventType = (text: string) => {
if (review.data.sub_labels?.includes(text)) return "manual";
if (review.data.objects.includes(text)) return "object";
if (review.data.audio.includes(text)) return "audio";
if (review.data.sub_labels?.includes(text)) return "manual";
return "object";
};
@@ -268,16 +268,13 @@ export default function PreviewThumbnailPlayer({
className={`flex items-start justify-between space-x-1 ${playingBack ? "hidden" : ""} bg-gradient-to-br ${review.has_been_reviewed ? "bg-green-600 from-green-600 to-green-700" : "bg-gray-500 from-gray-400 to-gray-500"} z-0`}
onClick={() => onClick(review, false, true)}
>
{review.data.objects
.sort()
.map((object, idx) =>
getIconForLabel(
object,
"object",
"size-3 text-white",
`${object}-${idx}`,
),
)}
{review.data.objects.sort().map((object) => {
return getIconForLabel(
object,
"object",
"size-3 text-white",
);
})}
{review.data.audio.map((audio) => {
return getIconForLabel(
audio,
@@ -291,26 +288,23 @@ export default function PreviewThumbnailPlayer({
</div>
</TooltipTrigger>
</div>
<TooltipContent>
<TooltipContent className="smart-capitalize">
{review.data.metadata
? review.data.metadata.title
: formatList(
[
...new Set([
...(review.data.objects || []),
...(review.data.sub_labels || []),
...(review.data.audio || []),
]),
]
.filter(
(item) =>
item !== undefined && !item.includes("-verified"),
)
.map((text) =>
getTranslatedLabel(text, getEventType(text)),
)
.sort(),
)}
: [
...new Set([
...(review.data.objects || []),
...(review.data.sub_labels || []),
...(review.data.audio || []),
]),
]
.filter(
(item) =>
item !== undefined && !item.includes("-verified"),
)
.map((text) => getTranslatedLabel(text, getEventType(text)))
.sort()
.join(", ")}
</TooltipContent>
</Tooltip>
{!!(
+1
View File
@@ -26,5 +26,6 @@ export const supportedLanguageKeys = [
"lt",
"uk",
"cs",
"sk",
"hu",
];
+40 -45
View File
@@ -62,86 +62,83 @@ export function getIconForLabel(
label: string,
type: EventType = "object",
className?: string,
key?: string,
) {
const iconKey = key || label;
if (label.endsWith("-verified")) {
return getVerifiedIcon(label, className, type, iconKey);
return getVerifiedIcon(label, className, type);
} else if (label.endsWith("-plate")) {
return getRecognizedPlateIcon(label, className, type, iconKey);
return getRecognizedPlateIcon(label, className, type);
}
switch (label) {
// objects
case "bear":
return <GiPolarBear key={iconKey} className={className} />;
return <GiPolarBear key={label} className={className} />;
case "bicycle":
return <FaBicycle key={iconKey} className={className} />;
return <FaBicycle key={label} className={className} />;
case "bird":
return <PiBirdFill key={iconKey} className={className} />;
return <PiBirdFill key={label} className={className} />;
case "boat":
return <GiSailboat key={iconKey} className={className} />;
return <GiSailboat key={label} className={className} />;
case "bus":
case "school_bus":
return <FaBus key={iconKey} className={className} />;
return <FaBus key={label} className={className} />;
case "car":
case "vehicle":
return <FaCarSide key={iconKey} className={className} />;
return <FaCarSide key={label} className={className} />;
case "cat":
return <FaCat key={iconKey} className={className} />;
return <FaCat key={label} className={className} />;
case "deer":
return <GiDeer key={iconKey} className={className} />;
return <GiDeer key={label} className={className} />;
case "animal":
case "bark":
case "dog":
return <FaDog key={iconKey} className={className} />;
return <FaDog key={label} className={className} />;
case "fox":
return <GiFox key={iconKey} className={className} />;
return <GiFox key={label} className={className} />;
case "goat":
return <GiGoat key={iconKey} className={className} />;
return <GiGoat key={label} className={className} />;
case "horse":
return <FaHorse key={iconKey} className={className} />;
return <FaHorse key={label} className={className} />;
case "kangaroo":
return <GiKangaroo key={iconKey} className={className} />;
return <GiKangaroo key={label} className={className} />;
case "license_plate":
return <LuScanBarcode key={iconKey} className={className} />;
return <LuScanBarcode key={label} className={className} />;
case "motorcycle":
return <FaMotorcycle key={iconKey} className={className} />;
return <FaMotorcycle key={label} className={className} />;
case "mouse":
return <FaMouse key={iconKey} className={className} />;
return <FaMouse key={label} className={className} />;
case "package":
return <LuBox key={iconKey} className={className} />;
return <LuBox key={label} className={className} />;
case "person":
return <BsPersonWalking key={iconKey} className={className} />;
return <BsPersonWalking key={label} className={className} />;
case "rabbit":
return <GiRabbit key={iconKey} className={className} />;
return <GiRabbit key={label} className={className} />;
case "raccoon":
return <GiRaccoonHead key={iconKey} className={className} />;
return <GiRaccoonHead key={label} className={className} />;
case "robot_lawnmower":
return <FaHockeyPuck key={iconKey} className={className} />;
return <FaHockeyPuck key={label} className={className} />;
case "sports_ball":
return <FaFootballBall key={iconKey} className={className} />;
return <FaFootballBall key={label} className={className} />;
case "skunk":
return <GiSquirrel key={iconKey} className={className} />;
return <GiSquirrel key={label} className={className} />;
case "squirrel":
return <LuIcons.LuSquirrel key={iconKey} className={className} />;
return <LuIcons.LuSquirrel key={label} className={className} />;
case "umbrella":
return <FaUmbrella key={iconKey} className={className} />;
return <FaUmbrella key={label} className={className} />;
case "waste_bin":
return <FaRegTrashAlt key={iconKey} className={className} />;
return <FaRegTrashAlt key={label} className={className} />;
// audio
case "crying":
case "laughter":
case "scream":
case "speech":
case "yell":
return <MdRecordVoiceOver key={iconKey} className={className} />;
return <MdRecordVoiceOver key={label} className={className} />;
case "fire_alarm":
return <FaFire key={iconKey} className={className} />;
return <FaFire key={label} className={className} />;
// sub labels
case "amazon":
return <FaAmazon key={iconKey} className={className} />;
return <FaAmazon key={label} className={className} />;
case "an_post":
case "canada_post":
case "dpd":
@@ -151,20 +148,20 @@ export function getIconForLabel(
case "postnord":
case "purolator":
case "royal_mail":
return <GiPostStamp key={iconKey} className={className} />;
return <GiPostStamp key={label} className={className} />;
case "dhl":
return <FaDhl key={iconKey} className={className} />;
return <FaDhl key={label} className={className} />;
case "fedex":
return <FaFedex key={iconKey} className={className} />;
return <FaFedex key={label} className={className} />;
case "ups":
return <FaUps key={iconKey} className={className} />;
return <FaUps key={label} className={className} />;
case "usps":
return <FaUsps key={iconKey} className={className} />;
return <FaUsps key={label} className={className} />;
default:
if (type === "audio") {
return <GiSoundWaves key={iconKey} className={className} />;
return <GiSoundWaves key={label} className={className} />;
}
return <LuLassoSelect key={iconKey} className={className} />;
return <LuLassoSelect key={label} className={className} />;
}
}
@@ -172,12 +169,11 @@ function getVerifiedIcon(
label: string,
className?: string,
type: EventType = "object",
key?: string,
) {
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
return (
<div key={key} className="relative flex items-center">
<div key={label} className="relative flex items-center">
{getIconForLabel(simpleLabel, type, className)}
<FaCheckCircle className="absolute -bottom-0.5 -right-0.5 size-2" />
</div>
@@ -188,12 +184,11 @@ function getRecognizedPlateIcon(
label: string,
className?: string,
type: EventType = "object",
key?: string,
) {
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
return (
<div key={key} className="relative inline-flex items-center">
<div key={label} className="relative inline-flex items-center">
{getIconForLabel(simpleLabel, type, className)}
<LuScanBarcode className="absolute -bottom-0.5 -right-0.5 size-2" />
</div>
+3 -8
View File
@@ -33,12 +33,7 @@ import {
useRef,
useState,
} from "react";
import {
isDesktop,
isMobile,
isMobileOnly,
isTablet,
} from "react-device-detect";
import { isDesktop, isMobile } from "react-device-detect";
import { IoMdArrowRoundBack } from "react-icons/io";
import { useNavigate } from "react-router-dom";
import { Toaster } from "@/components/ui/sonner";
@@ -743,7 +738,7 @@ export function RecordingView({
aspectRatio: getCameraAspect(mainCamera),
}}
>
{(isDesktop || isTablet) && (
{isDesktop && (
<GenAISummaryDialog
review={activeReviewItem}
onOpen={onAnalysisOpen}
@@ -1006,7 +1001,7 @@ function Timeline({
),
)}
>
{isMobileOnly && timelineType == "timeline" && (
{isMobile && timelineType == "timeline" && (
<GenAISummaryDialog review={activeReviewItem} onOpen={onAnalysisOpen}>
<GenAISummaryChip review={activeReviewItem} />
</GenAISummaryDialog>