mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-29 15:19:00 +03:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93016c662f | ||
|
|
c08fa15724 | ||
|
|
f3543cfee2 |
@@ -39,14 +39,14 @@ jobs:
|
||||
STABLE_TAG=${BASE}:stable
|
||||
PULL_TAG=${BASE}:${BUILD_TAG}
|
||||
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG} docker://${VERSION_TAG}
|
||||
for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm; do
|
||||
for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm synaptics; do
|
||||
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG}-${variant} docker://${VERSION_TAG}-${variant}
|
||||
done
|
||||
|
||||
# stable tag
|
||||
if [[ "${BUILD_TYPE}" == "stable" ]]; then
|
||||
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG} docker://${STABLE_TAG}
|
||||
for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm; do
|
||||
for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm synaptics; do
|
||||
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG}-${variant} docker://${STABLE_TAG}-${variant}
|
||||
done
|
||||
fi
|
||||
|
||||
@@ -112,6 +112,17 @@ review:
|
||||
- animals in the garden
|
||||
```
|
||||
|
||||
### Preferred Language
|
||||
|
||||
By default, review summaries are generated in English. You can configure Frigate to generate summaries in your preferred language by setting the `preferred_language` option:
|
||||
|
||||
```yaml
|
||||
review:
|
||||
genai:
|
||||
enabled: true
|
||||
preferred_language: Spanish
|
||||
```
|
||||
|
||||
## Review Reports
|
||||
|
||||
Along with individual review item summaries, Generative AI provides the ability to request a report of a given time period. For example, you can get a daily report while on a vacation of any suspicious activity or other concerns that may require review.
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
"show": "Show {{item}}",
|
||||
"ID": "ID",
|
||||
"none": "None",
|
||||
"all": "All"
|
||||
"all": "All",
|
||||
"other": "Other"
|
||||
},
|
||||
"list": {
|
||||
"two": "{{0}} and {{1}}",
|
||||
|
||||
@@ -86,7 +86,14 @@
|
||||
"otherProcesses": {
|
||||
"title": "Other Processes",
|
||||
"processCpuUsage": "Process CPU Usage",
|
||||
"processMemoryUsage": "Process Memory Usage"
|
||||
"processMemoryUsage": "Process Memory Usage",
|
||||
"series": {
|
||||
"go2rtc": "go2rtc",
|
||||
"recording": "recording",
|
||||
"review_segment": "review segment",
|
||||
"embeddings": "embeddings",
|
||||
"audio_detector": "audio detector"
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { AuthProvider } from "@/context/auth-context";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "./types/frigateConfig";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { isRedirectingToLogin } from "@/api/auth-redirect";
|
||||
|
||||
const Live = lazy(() => import("@/pages/Live"));
|
||||
const Events = lazy(() => import("@/pages/Events"));
|
||||
@@ -58,6 +59,16 @@ function DefaultAppView() {
|
||||
? Object.keys(config.auth.roles)
|
||||
: undefined;
|
||||
|
||||
// Show loading indicator during redirect to prevent React from attempting to render
|
||||
// lazy components, which would cause error #426 (suspension during synchronous navigation)
|
||||
if (isRedirectingToLogin()) {
|
||||
return (
|
||||
<div className="size-full overflow-hidden">
|
||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="size-full overflow-hidden">
|
||||
{isDesktop && <Sidebar />}
|
||||
|
||||
@@ -28,6 +28,14 @@ export default function ProtectedRoute({
|
||||
}
|
||||
}, [auth.isLoading, auth.isAuthenticated, auth.user]);
|
||||
|
||||
// Show loading indicator during redirect to prevent React from attempting to render
|
||||
// lazy components, which would cause error #426 (suspension during synchronous navigation)
|
||||
if (isRedirectingToLogin()) {
|
||||
return (
|
||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||
);
|
||||
}
|
||||
|
||||
if (auth.isLoading) {
|
||||
return (
|
||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||
|
||||
@@ -166,7 +166,7 @@ export const ClassificationCard = forwardRef<
|
||||
<div className="break-all smart-capitalize">
|
||||
{data.name == "unknown"
|
||||
? t("details.unknown")
|
||||
: data.name == "none"
|
||||
: data.name.toLowerCase() == "none"
|
||||
? t("details.none")
|
||||
: data.name}
|
||||
</div>
|
||||
|
||||
@@ -181,7 +181,7 @@ export default function ReviewCard({
|
||||
key={`${object}-${idx}`}
|
||||
className="rounded-full bg-muted-foreground p-1"
|
||||
>
|
||||
{getIconForLabel(object, "size-3 text-white")}
|
||||
{getIconForLabel(object, "object", "size-3 text-white")}
|
||||
</div>
|
||||
))}
|
||||
{event.data.audio.map((audio, idx) => (
|
||||
@@ -189,7 +189,7 @@ export default function ReviewCard({
|
||||
key={`${audio}-${idx}`}
|
||||
className="rounded-full bg-muted-foreground p-1"
|
||||
>
|
||||
{getIconForLabel(audio, "size-3 text-white")}
|
||||
{getIconForLabel(audio, "audio", "size-3 text-white")}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -133,7 +133,11 @@ export default function SearchThumbnail({
|
||||
className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize`}
|
||||
onClick={() => onClick(searchResult, false, true)}
|
||||
>
|
||||
{getIconForLabel(objectLabel, "size-3 text-white")}
|
||||
{getIconForLabel(
|
||||
objectLabel,
|
||||
searchResult.data.type,
|
||||
"size-3 text-white",
|
||||
)}
|
||||
{Math.floor(
|
||||
(searchResult.data.score ??
|
||||
searchResult.data.top_score ??
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import useSWR from "swr";
|
||||
import { formatSecondsToDuration } from "@/utils/dateUtil";
|
||||
import { useDateLocale } from "@/hooks/use-date-locale";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -48,12 +49,13 @@ export default function SetPasswordDialog({
|
||||
const { t } = useTranslation(["views/settings", "common"]);
|
||||
const { getLocaleDocUrl } = useDocDomain();
|
||||
const isAdmin = useIsAdmin();
|
||||
const dateLocale = useDateLocale();
|
||||
|
||||
const { data: config } = useSWR("config");
|
||||
const refreshSeconds: number | undefined =
|
||||
config?.auth?.refresh_time ?? undefined;
|
||||
const refreshTimeLabel = refreshSeconds
|
||||
? formatSecondsToDuration(refreshSeconds)
|
||||
? formatSecondsToDuration(refreshSeconds, dateLocale)
|
||||
: t("time.30minutes", { ns: "common" });
|
||||
|
||||
// visibility toggles for password fields
|
||||
|
||||
@@ -1296,7 +1296,11 @@ function ObjectDetailsTab({
|
||||
{t("details.label")}
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2 text-sm smart-capitalize">
|
||||
{getIconForLabel(search.label, "size-4 text-primary")}
|
||||
{getIconForLabel(
|
||||
search.label,
|
||||
search.data.type,
|
||||
"size-4 text-primary",
|
||||
)}
|
||||
{getTranslatedLabel(search.label, search.data.type)}
|
||||
{search.sub_label && ` (${search.sub_label})`}
|
||||
{isAdmin && search.end_time && (
|
||||
|
||||
@@ -266,7 +266,7 @@ export function TrackingDetails({
|
||||
|
||||
const label = event.sub_label
|
||||
? event.sub_label
|
||||
: getTranslatedLabel(event.label);
|
||||
: getTranslatedLabel(event.label, event.data.type);
|
||||
|
||||
const getZoneColor = useCallback(
|
||||
(zoneName: string) => {
|
||||
@@ -665,6 +665,7 @@ export function TrackingDetails({
|
||||
>
|
||||
{getIconForLabel(
|
||||
event.sub_label ? event.label + "-verified" : event.label,
|
||||
event.data.type,
|
||||
"size-4 text-white",
|
||||
)}
|
||||
</div>
|
||||
@@ -998,7 +999,7 @@ function LifecycleIconRow({
|
||||
<div className="ml-3 flex-shrink-0 px-1 text-right text-xs text-primary-variant">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<div className="whitespace-nowrap">{formattedEventTimestamp}</div>
|
||||
{((isAdmin && config?.plus?.enabled) || item.data.box) && (
|
||||
{isAdmin && config?.plus?.enabled && item.data.box && (
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger>
|
||||
<div className="rounded p-1 pr-2" role="button">
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
} from "@/types/live";
|
||||
import { getIconForLabel } from "@/utils/iconUtil";
|
||||
import Chip from "../indicators/Chip";
|
||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TbExclamationCircle } from "react-icons/tb";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
@@ -26,6 +25,8 @@ import { LuVideoOff } from "react-icons/lu";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
|
||||
import { ImageShadowOverlay } from "../overlay/ImageShadowOverlay";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { formatList } from "@/utils/stringUtil";
|
||||
|
||||
type LivePlayerProps = {
|
||||
cameraRef?: (ref: HTMLDivElement | null) => void;
|
||||
@@ -358,7 +359,11 @@ export default function LivePlayer({
|
||||
]),
|
||||
]
|
||||
.map((label) => {
|
||||
return getIconForLabel(label, "size-3 text-white");
|
||||
return getIconForLabel(
|
||||
label,
|
||||
"object",
|
||||
"size-3 text-white",
|
||||
);
|
||||
})
|
||||
.sort()}
|
||||
</Chip>
|
||||
@@ -367,20 +372,22 @@ export default function LivePlayer({
|
||||
</div>
|
||||
<TooltipPortal>
|
||||
<TooltipContent className="smart-capitalize">
|
||||
{[
|
||||
...new Set([
|
||||
...(objects || []).map(({ label, sub_label }) =>
|
||||
label.endsWith("verified")
|
||||
? sub_label
|
||||
: label.replaceAll("_", " "),
|
||||
),
|
||||
]),
|
||||
]
|
||||
.filter((label) => label?.includes("-verified") == false)
|
||||
.map((label) => capitalizeFirstLetter(label))
|
||||
.sort()
|
||||
.join(", ")
|
||||
.replaceAll("-verified", "")}
|
||||
{formatList(
|
||||
[
|
||||
...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>
|
||||
</Tooltip>
|
||||
|
||||
@@ -262,10 +262,18 @@ export default function PreviewThumbnailPlayer({
|
||||
onClick={() => onClick(review, false, true)}
|
||||
>
|
||||
{review.data.objects.sort().map((object) => {
|
||||
return getIconForLabel(object, "size-3 text-white");
|
||||
return getIconForLabel(
|
||||
object,
|
||||
"object",
|
||||
"size-3 text-white",
|
||||
);
|
||||
})}
|
||||
{review.data.audio.map((audio) => {
|
||||
return getIconForLabel(audio, "size-3 text-white");
|
||||
return getIconForLabel(
|
||||
audio,
|
||||
"audio",
|
||||
"size-3 text-white",
|
||||
);
|
||||
})}
|
||||
</Chip>
|
||||
</>
|
||||
|
||||
@@ -417,7 +417,9 @@ export default function Step1NameCamera({
|
||||
<SelectContent>
|
||||
{CAMERA_BRANDS.map((brand) => (
|
||||
<SelectItem key={brand.value} value={brand.value}>
|
||||
{brand.label}
|
||||
{brand.label.toLowerCase() === "other"
|
||||
? t("label.other", { ns: "common" })
|
||||
: brand.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import useSWR from "swr";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { Event } from "@/types/event";
|
||||
import { EventType } from "@/types/search";
|
||||
import { getIconForLabel } from "@/utils/iconUtil";
|
||||
import { REVIEW_PADDING, ReviewSegment } from "@/types/review";
|
||||
import { LuChevronDown, LuCircle, LuChevronRight } from "react-icons/lu";
|
||||
@@ -346,22 +347,29 @@ function ReviewGroup({
|
||||
: null,
|
||||
);
|
||||
|
||||
const rawIconLabels: string[] = [
|
||||
const rawIconLabels: Array<{ label: string; type: EventType }> = [
|
||||
...(fetchedEvents
|
||||
? fetchedEvents.map((e) =>
|
||||
e.sub_label ? e.label + "-verified" : e.label,
|
||||
)
|
||||
: (review.data?.objects ?? [])),
|
||||
...(review.data?.audio ?? []),
|
||||
? fetchedEvents.map((e) => ({
|
||||
label: e.sub_label ? e.label + "-verified" : e.label,
|
||||
type: e.data.type,
|
||||
}))
|
||||
: (review.data?.objects ?? []).map((obj) => ({
|
||||
label: obj,
|
||||
type: "object" as EventType,
|
||||
}))),
|
||||
...(review.data?.audio ?? []).map((audio) => ({
|
||||
label: audio,
|
||||
type: "audio" as EventType,
|
||||
})),
|
||||
];
|
||||
|
||||
// limit to 5 icons
|
||||
const seen = new Set<string>();
|
||||
const iconLabels: string[] = [];
|
||||
for (const lbl of rawIconLabels) {
|
||||
if (!seen.has(lbl)) {
|
||||
seen.add(lbl);
|
||||
iconLabels.push(lbl);
|
||||
const iconLabels: Array<{ label: string; type: EventType }> = [];
|
||||
for (const item of rawIconLabels) {
|
||||
if (!seen.has(item.label)) {
|
||||
seen.add(item.label);
|
||||
iconLabels.push(item);
|
||||
if (iconLabels.length >= 5) break;
|
||||
}
|
||||
}
|
||||
@@ -418,12 +426,12 @@ function ReviewGroup({
|
||||
<div className="flex flex-row gap-3">
|
||||
<div className="text-sm font-medium">{displayTime}</div>
|
||||
<div className="relative flex items-center gap-2 text-white">
|
||||
{iconLabels.slice(0, 5).map((lbl, idx) => (
|
||||
{iconLabels.slice(0, 5).map(({ label: lbl, type }, idx) => (
|
||||
<div
|
||||
key={`${lbl}-${idx}`}
|
||||
className="rounded-full bg-muted-foreground p-1"
|
||||
>
|
||||
{getIconForLabel(lbl, "size-3 text-white")}
|
||||
{getIconForLabel(lbl, type, "size-3 text-white")}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -516,7 +524,11 @@ function ReviewGroup({
|
||||
>
|
||||
<div className="ml-1.5 flex items-center gap-2 text-sm font-medium">
|
||||
<div className="rounded-full bg-muted-foreground p-1">
|
||||
{getIconForLabel(audioLabel, "size-3 text-white")}
|
||||
{getIconForLabel(
|
||||
audioLabel,
|
||||
"audio",
|
||||
"size-3 text-white",
|
||||
)}
|
||||
</div>
|
||||
<span>{getTranslatedLabel(audioLabel, "audio")}</span>
|
||||
</div>
|
||||
@@ -618,6 +630,7 @@ function EventList({
|
||||
>
|
||||
{getIconForLabel(
|
||||
event.sub_label ? event.label + "-verified" : event.label,
|
||||
event.data.type,
|
||||
"size-3 text-white",
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fromUnixTime, intervalToDuration, formatDuration } from "date-fns";
|
||||
import { Locale } from "date-fns/locale";
|
||||
import { enUS, Locale } from "date-fns/locale";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
import i18n from "@/utils/i18n";
|
||||
export const longToDate = (long: number): Date => new Date(long * 1000);
|
||||
@@ -293,9 +293,13 @@ export const getDurationFromTimestamps = (
|
||||
/**
|
||||
*
|
||||
* @param seconds - number of seconds to convert into hours, minutes and seconds
|
||||
* @param locale - the date-fns locale to use for formatting
|
||||
* @returns string - formatted duration in hours, minutes and seconds
|
||||
*/
|
||||
export const formatSecondsToDuration = (seconds: number): string => {
|
||||
export const formatSecondsToDuration = (
|
||||
seconds: number,
|
||||
locale?: Locale,
|
||||
): string => {
|
||||
if (isNaN(seconds) || seconds < 0) {
|
||||
return "Invalid duration";
|
||||
}
|
||||
@@ -304,6 +308,7 @@ export const formatSecondsToDuration = (seconds: number): string => {
|
||||
return formatDuration(duration, {
|
||||
format: ["hours", "minutes", "seconds"],
|
||||
delimiter: ", ",
|
||||
locale: locale ?? enUS,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IconName } from "@/components/icons/IconPicker";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { EventType } from "@/types/search";
|
||||
import { BsPersonWalking } from "react-icons/bs";
|
||||
import {
|
||||
FaAmazon,
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
GiRabbit,
|
||||
GiRaccoonHead,
|
||||
GiSailboat,
|
||||
GiSoundWaves,
|
||||
GiSquirrel,
|
||||
} from "react-icons/gi";
|
||||
import { LuBox, LuLassoSelect, LuScanBarcode } from "react-icons/lu";
|
||||
@@ -56,11 +58,15 @@ export function isValidIconName(value: string): value is IconName {
|
||||
return Object.keys(LuIcons).includes(value as IconName);
|
||||
}
|
||||
|
||||
export function getIconForLabel(label: string, className?: string) {
|
||||
export function getIconForLabel(
|
||||
label: string,
|
||||
type: EventType = "object",
|
||||
className?: string,
|
||||
) {
|
||||
if (label.endsWith("-verified")) {
|
||||
return getVerifiedIcon(label, className);
|
||||
return getVerifiedIcon(label, className, type);
|
||||
} else if (label.endsWith("-plate")) {
|
||||
return getRecognizedPlateIcon(label, className);
|
||||
return getRecognizedPlateIcon(label, className, type);
|
||||
}
|
||||
|
||||
switch (label) {
|
||||
@@ -152,27 +158,38 @@ export function getIconForLabel(label: string, className?: string) {
|
||||
case "usps":
|
||||
return <FaUsps key={label} className={className} />;
|
||||
default:
|
||||
if (type === "audio") {
|
||||
return <GiSoundWaves key={label} className={className} />;
|
||||
}
|
||||
return <LuLassoSelect key={label} className={className} />;
|
||||
}
|
||||
}
|
||||
|
||||
function getVerifiedIcon(label: string, className?: string) {
|
||||
function getVerifiedIcon(
|
||||
label: string,
|
||||
className?: string,
|
||||
type: EventType = "object",
|
||||
) {
|
||||
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
||||
|
||||
return (
|
||||
<div key={label} className="flex items-center">
|
||||
{getIconForLabel(simpleLabel, className)}
|
||||
{getIconForLabel(simpleLabel, type, className)}
|
||||
<FaCheckCircle className="absolute size-2 translate-x-[80%] translate-y-3/4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getRecognizedPlateIcon(label: string, className?: string) {
|
||||
function getRecognizedPlateIcon(
|
||||
label: string,
|
||||
className?: string,
|
||||
type: EventType = "object",
|
||||
) {
|
||||
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
||||
|
||||
return (
|
||||
<div key={label} className="flex items-center">
|
||||
{getIconForLabel(simpleLabel, className)}
|
||||
{getIconForLabel(simpleLabel, type, className)}
|
||||
<LuScanBarcode className="absolute size-2.5 translate-x-[50%] translate-y-3/4" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,10 @@ export function getLifecycleItemDescription(
|
||||
|
||||
const label = lifecycleItem.data.sub_label
|
||||
? capitalizeFirstLetter(rawLabel)
|
||||
: getTranslatedLabel(rawLabel);
|
||||
: getTranslatedLabel(
|
||||
rawLabel,
|
||||
lifecycleItem.class_type === "heard" ? "audio" : "object",
|
||||
);
|
||||
|
||||
switch (lifecycleItem.class_type) {
|
||||
case "visible":
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
||||
// title
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${model.name} - ${t("documentTitle")}`;
|
||||
document.title = `${model.name.toUpperCase()} - ${t("documentTitle")}`;
|
||||
}, [model.name, t]);
|
||||
|
||||
// model state
|
||||
|
||||
@@ -406,7 +406,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
|
||||
: getColorForObjectName(obj.label),
|
||||
}}
|
||||
>
|
||||
{getIconForLabel(obj.label, "size-5 text-white")}
|
||||
{getIconForLabel(obj.label, "object", "size-5 text-white")}
|
||||
</div>
|
||||
<div className="ml-3 text-lg">
|
||||
{getTranslatedLabel(obj.label)}
|
||||
@@ -494,7 +494,7 @@ function AudioList({ cameraConfig, audioDetections }: AudioListProps) {
|
||||
<div className="flex flex-row items-center gap-3 pb-1">
|
||||
<div className="flex flex-1 flex-row items-center justify-start p-3 pl-1">
|
||||
<div className="rounded-lg bg-selected p-2">
|
||||
{getIconForLabel(key, "size-5 text-white")}
|
||||
{getIconForLabel(key, "audio", "size-5 text-white")}
|
||||
</div>
|
||||
<div className="ml-3 text-lg">{getTranslatedLabel(key)}</div>
|
||||
</div>
|
||||
|
||||
@@ -855,7 +855,7 @@ export default function GeneralMetrics({
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
graphId={`${series.name}-cpu`}
|
||||
name={series.name.replaceAll("_", " ")}
|
||||
name={t(`general.otherProcesses.series.${series.name}`)}
|
||||
unit="%"
|
||||
threshold={DetectorCpuThreshold}
|
||||
updateTimes={updateTimes}
|
||||
|
||||
Reference in New Issue
Block a user