feat: add explore model i18n keys

This commit is contained in:
ZhaiSoul 2025-03-11 19:53:44 +08:00
parent 118ef6b2fe
commit 84ba218b1b
11 changed files with 107 additions and 27 deletions

View File

@ -111,6 +111,7 @@
"restart": "Restart Frigate", "restart": "Restart Frigate",
"live": "Live", "live": "Live",
"live.allCameras": "All Cameras", "live.allCameras": "All Cameras",
"live.cameras": "Cameras",
"review": "Review", "review": "Review",
"explore": "Explore", "explore": "Explore",
"export": "Export", "export": "Export",

View File

@ -1,4 +1,32 @@
{ {
"exploreIsUnavailable": {
"title": "Explore is Unavailable",
"embeddingsReindexing": {
"context": "Explore can be used after tracked object embeddings have finished reindexing.",
"startingUp": "Starting up...",
"estimatedTime": "Estimated time remaining:",
"finishingShortly": "Finishing shortly",
"step": {
"thumbnailsEmbedded": "Thumbnails embedded: ",
"descriptionsEmbedded": "Descriptions embedded: ",
"trackedObjectsProcessed": "Tracked objects processed: "
}
},
"downloadingModels": {
"context": "Frigate is downloading the necessary embeddings models to support the Semantic Search feature. This may take several minutes depending on the speed of your network connection.",
"setup": {
"visionModel": "Vision model",
"visionModelFeatureExtractor": "Vision model feature extractor",
"textModel": "Text model",
"textTokenizer": "Text tokenizer"
},
"tips": {
"context": "You may want to reindex the embeddings of your tracked objects once the models are downloaded.",
"documentation": "Read the documentation"
},
"error": "An error has occurred. Check Frigate logs."
}
},
"trackedObjectDetails": "Tracked Object Details", "trackedObjectDetails": "Tracked Object Details",
"type": { "type": {
"details": "details", "details": "details",

View File

@ -69,6 +69,7 @@
"stats": { "stats": {
"ffmpegHighCpuUsage": "{{camera}} has high FFMPEG CPU usage ({{ffmpegAvg}}%)", "ffmpegHighCpuUsage": "{{camera}} has high FFMPEG CPU usage ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} has high detect CPU usage ({{detectAvg}}%)", "detectHighCpuUsage": "{{camera}} has high detect CPU usage ({{detectAvg}}%)",
"healthy": "System is healthy" "healthy": "System is healthy",
"reindexingEmbeddings": "Reindexing embeddings ({{processed}}% complete)"
} }
} }

View File

@ -110,6 +110,7 @@
"documentation": "文档", "documentation": "文档",
"live": "实时监控", "live": "实时监控",
"live.allCameras": "所有摄像头", "live.allCameras": "所有摄像头",
"live.cameras": "摄像头",
"review": "回放", "review": "回放",
"explore": "探测", "explore": "探测",
"export": "导出", "export": "导出",

View File

@ -1,4 +1,32 @@
{ {
"exploreIsUnavailable": {
"title": "探索功能不可用",
"embeddingsReindexing": {
"context": "跟踪对象嵌入重新索引完成后,可以使用探索功能。",
"startingUp": "启动中...",
"estimatedTime": "预计剩余时间:",
"finishingShortly": "即将完成",
"step": {
"thumbnailsEmbedded": "缩略图嵌入:",
"descriptionsEmbedded": "描述嵌入:",
"trackedObjectsProcessed": "跟踪对象已处理:"
}
},
"downloadingModels": {
"context": "Frigate正在下载支持语义搜索功能所需的嵌入模型。根据网络连接速度这可能需要几分钟。",
"setup": {
"visionModel": "视觉模型",
"visionModelFeatureExtractor": "视觉模型特征提取器",
"textModel": "文本模型",
"textTokenizer": "文本分词器"
},
"tips": {
"context": "模型下载完成后,您可能需要重新索引跟踪对象的嵌入。",
"documentation": "阅读文档(英文)"
},
"error": "发生错误。请检查Frigate日志。"
}
},
"trackedObjectDetails": "探测对象详情", "trackedObjectDetails": "探测对象详情",
"type": { "type": {
"details": "详情", "details": "详情",

View File

@ -69,6 +69,7 @@
"stats": { "stats": {
"ffmpegHighCpuUsage": "{{camera}} 的 FFMPEG CPU 使用率较高({{ffmpegAvg}}%", "ffmpegHighCpuUsage": "{{camera}} 的 FFMPEG CPU 使用率较高({{ffmpegAvg}}%",
"detectHighCpuUsage": "{{camera}} 的 探测 CPU 使用率较高({{detectAvg}}%", "detectHighCpuUsage": "{{camera}} 的 探测 CPU 使用率较高({{detectAvg}}%",
"healthy": "系统运行正常" "healthy": "系统运行正常",
"reindexingEmbeddings": "正在重新索引嵌入(已完成 {{processed}}%"
} }
} }

View File

@ -54,14 +54,19 @@ export default function Statusbar() {
clearMessages("embeddings-reindex"); clearMessages("embeddings-reindex");
addMessage( addMessage(
"embeddings-reindex", "embeddings-reindex",
`Reindexing embeddings (${Math.floor((reindexState.processed_objects / reindexState.total_objects) * 100)}% complete)`, t("stats.reindexingEmbeddings", {
processed: Math.floor(
(reindexState.processed_objects / reindexState.total_objects) *
100,
),
}),
); );
} }
if (reindexState.status === "completed") { if (reindexState.status === "completed") {
clearMessages("embeddings-reindex"); clearMessages("embeddings-reindex");
} }
} }
}, [reindexState, addMessage, clearMessages]); }, [reindexState, addMessage, clearMessages, t]);
return ( return (
<div className="absolute bottom-0 left-0 right-0 z-10 flex h-8 w-full items-center justify-between border-t border-secondary-highlight bg-background_alt px-4 dark:text-secondary-foreground"> <div className="absolute bottom-0 left-0 right-0 z-10 flex h-8 w-full items-center justify-between border-t border-secondary-highlight bg-background_alt px-4 dark:text-secondary-foreground">

View File

@ -38,7 +38,7 @@ export function CamerasFilterButton({
const buttonText = useMemo(() => { const buttonText = useMemo(() => {
if (isMobile) { if (isMobile) {
return "Cameras"; return t("menu.live.cameras", { ns: "common" });
} }
if (!selectedCameras || selectedCameras.length == 0) { if (!selectedCameras || selectedCameras.length == 0) {

View File

@ -109,7 +109,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
{t("menu.user.current", { {t("menu.user.current", {
user: profile?.username || t("menu.user.anonymous"), user: profile?.username || t("menu.user.anonymous"),
})}{" "} })}{" "}
{t("role." + profile?.role) && `(${t("role." + profile.role)})`} {t("role." + profile?.role) && `(${t("role." + profile?.role)})`}
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator className={isDesktop ? "mt-3" : "mt-1"} /> <DropdownMenuSeparator className={isDesktop ? "mt-3" : "mt-1"} />
{profile?.username && profile.username !== "anonymous" && ( {profile?.username && profile.username !== "anonymous" && (

View File

@ -170,7 +170,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
user: profile?.username || t("menu.user.anonymous"), user: profile?.username || t("menu.user.anonymous"),
})}{" "} })}{" "}
{t("role." + profile?.role) && {t("role." + profile?.role) &&
`(${t("role." + profile.role)})`} `(${t("role." + profile?.role)})`}
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator <DropdownMenuSeparator
className={isDesktop ? "mt-3" : "mt-1"} className={isDesktop ? "mt-3" : "mt-1"}

View File

@ -15,6 +15,7 @@ import { formatSecondsToDuration } from "@/utils/dateUtil";
import SearchView from "@/views/search/SearchView"; import SearchView from "@/views/search/SearchView";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { isMobileOnly } from "react-device-detect"; import { isMobileOnly } from "react-device-detect";
import { useTranslation } from "react-i18next";
import { LuCheck, LuExternalLink, LuX } from "react-icons/lu"; import { LuCheck, LuExternalLink, LuX } from "react-icons/lu";
import { TbExclamationCircle } from "react-icons/tb"; import { TbExclamationCircle } from "react-icons/tb";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@ -27,6 +28,8 @@ const API_LIMIT = 25;
export default function Explore() { export default function Explore() {
// search field handler // search field handler
const { t } = useTranslation(["views/explore"]);
const { data: config } = useSWR<FrigateConfig>("config", { const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false, revalidateOnFocus: false,
}); });
@ -353,13 +356,12 @@ export default function Explore() {
<div className="flex max-w-96 flex-col items-center justify-center space-y-3 rounded-lg bg-background/50 p-5"> <div className="flex max-w-96 flex-col items-center justify-center space-y-3 rounded-lg bg-background/50 p-5">
<div className="my-5 flex flex-col items-center gap-2 text-xl"> <div className="my-5 flex flex-col items-center gap-2 text-xl">
<TbExclamationCircle className="mb-3 size-10" /> <TbExclamationCircle className="mb-3 size-10" />
<div>Explore is Unavailable</div> <div>{t("exploreIsUnavailable.title")}</div>
</div> </div>
{embeddingsReindexing && allModelsLoaded && ( {embeddingsReindexing && allModelsLoaded && (
<> <>
<div className="text-center text-primary-variant"> <div className="text-center text-primary-variant">
Explore can be used after tracked object embeddings have {t("exploreIsUnavailable.embeddingsReindexing.context")}
finished reindexing.
</div> </div>
<div className="pt-5 text-center"> <div className="pt-5 text-center">
<AnimatedCircularProgressBar <AnimatedCircularProgressBar
@ -375,29 +377,35 @@ export default function Explore() {
<div className="mb-3 flex flex-col items-center justify-center gap-1"> <div className="mb-3 flex flex-col items-center justify-center gap-1">
<div className="text-primary-variant"> <div className="text-primary-variant">
{reindexState.time_remaining === -1 {reindexState.time_remaining === -1
? "Starting up..." ? t(
: "Estimated time remaining:"} "exploreIsUnavailable.embeddingsReindexing.startingUp",
)
: t(
"exploreIsUnavailable.embeddingsReindexing.estimatedTime",
)}
</div> </div>
{reindexState.time_remaining >= 0 && {reindexState.time_remaining >= 0 &&
(formatSecondsToDuration(reindexState.time_remaining) || (formatSecondsToDuration(reindexState.time_remaining) ||
"Finishing shortly")} t(
"exploreIsUnavailable.embeddingsReindexing.finishingShortly",
))}
</div> </div>
)} )}
<div className="flex flex-row items-center justify-center gap-3"> <div className="flex flex-row items-center justify-center gap-3">
<span className="text-primary-variant"> <span className="text-primary-variant">
Thumbnails embedded: t("exploreIsUnavailable.embeddingsReindexing.step.thumbnailsEmbedded")
</span> </span>
{reindexState.thumbnails} {reindexState.thumbnails}
</div> </div>
<div className="flex flex-row items-center justify-center gap-3"> <div className="flex flex-row items-center justify-center gap-3">
<span className="text-primary-variant"> <span className="text-primary-variant">
Descriptions embedded: t("exploreIsUnavailable.embeddingsReindexing.step.descriptionsEmbedded")
</span> </span>
{reindexState.descriptions} {reindexState.descriptions}
</div> </div>
<div className="flex flex-row items-center justify-center gap-3"> <div className="flex flex-row items-center justify-center gap-3">
<span className="text-primary-variant"> <span className="text-primary-variant">
Tracked objects processed: t("exploreIsUnavailable.embeddingsReindexing.step.trackedObjectsProcessed")
</span> </span>
{reindexState.processed_objects} /{" "} {reindexState.processed_objects} /{" "}
{reindexState.total_objects} {reindexState.total_objects}
@ -408,26 +416,32 @@ export default function Explore() {
{!allModelsLoaded && ( {!allModelsLoaded && (
<> <>
<div className="text-center text-primary-variant"> <div className="text-center text-primary-variant">
Frigate is downloading the necessary embeddings models to {t("exploreIsUnavailable.downloadingModels.context")}
support the Semantic Search feature. This may take several
minutes depending on the speed of your network connection.
</div> </div>
<div className="flex w-96 flex-col gap-2 py-5"> <div className="flex w-96 flex-col gap-2 py-5">
<div className="flex flex-row items-center justify-center gap-2"> <div className="flex flex-row items-center justify-center gap-2">
{renderModelStateIcon(visionModelState)} {renderModelStateIcon(visionModelState)}
Vision model {t(
"exploreIsUnavailable.downloadingModels.setup.visionModel",
)}
</div> </div>
<div className="flex flex-row items-center justify-center gap-2"> <div className="flex flex-row items-center justify-center gap-2">
{renderModelStateIcon(visionFeatureExtractorState)} {renderModelStateIcon(visionFeatureExtractorState)}
Vision model feature extractor {t(
"exploreIsUnavailable.downloadingModels.setup.visionModelFeatureExtractor",
)}
</div> </div>
<div className="flex flex-row items-center justify-center gap-2"> <div className="flex flex-row items-center justify-center gap-2">
{renderModelStateIcon(textModelState)} {renderModelStateIcon(textModelState)}
Text model {t(
"exploreIsUnavailable.downloadingModels.setup.textModel",
)}
</div> </div>
<div className="flex flex-row items-center justify-center gap-2"> <div className="flex flex-row items-center justify-center gap-2">
{renderModelStateIcon(textTokenizerState)} {renderModelStateIcon(textTokenizerState)}
Text tokenizer {t(
"exploreIsUnavailable.downloadingModels.setup.textTokenizer",
)}
</div> </div>
</div> </div>
{(textModelState === "error" || {(textModelState === "error" ||
@ -435,12 +449,11 @@ export default function Explore() {
visionModelState === "error" || visionModelState === "error" ||
visionFeatureExtractorState === "error") && ( visionFeatureExtractorState === "error") && (
<div className="my-3 max-w-96 text-center text-danger"> <div className="my-3 max-w-96 text-center text-danger">
An error has occurred. Check Frigate logs. {t("exploreIsUnavailable.downloadingModels.error")}
</div> </div>
)} )}
<div className="text-center text-primary-variant"> <div className="text-center text-primary-variant">
You may want to reindex the embeddings of your tracked objects {t("exploreIsUnavailable.downloadingModels.tips.context")}
once the models are downloaded.
</div> </div>
<div className="flex items-center text-primary-variant"> <div className="flex items-center text-primary-variant">
<Link <Link
@ -449,7 +462,9 @@ export default function Explore() {
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline" className="inline"
> >
Read the documentation{" "} {t(
"exploreIsUnavailable.downloadingModels.tips.documentation",
)}{" "}
<LuExternalLink className="ml-2 inline-flex size-3" /> <LuExternalLink className="ml-2 inline-flex size-3" />
</Link> </Link>
</div> </div>