Compare commits

...

5 Commits

Author SHA1 Message Date
GuoQing Liu
4a4d82c8ab
Merge 33048ebc01 into de2144f158 2025-11-26 22:22:32 +08:00
Nicolas Mowen
de2144f158
Miscellaneous Fixes (#21050)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* Don't add to history when opening search dialog

* Update caniuse

* Revamp the history handling for dialog components

* clarify audio transcription docs

* Use titlecase helper

* Allow running object clasasification on stationary objects

* small spacing tweaks for tablets

* require admin role to delete users

* explicitly prevent deletion of admin user

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-11-26 07:23:51 -06:00
ZhaiSoul
33048ebc01 docs: add license translation 2025-11-25 15:48:52 +00:00
ZhaiSoul
665c5c9ea6 style: Improve the styling of the Chinese document jump tips bar in dark mode 2025-11-25 15:11:26 +00:00
ZhaiSoul
5febd5e178 docs: update chinese readme 2025-11-25 15:08:39 +00:00
15 changed files with 197 additions and 148 deletions

View File

@ -1,28 +1,31 @@
<p align="center">
<img align="center" alt="logo" src="docs/static/img/frigate.png">
<img align="center" alt="logo" src="docs/static/img/branding/frigate.png">
</p>
# Frigate - 一个具有实时目标检测的本地NVR
# Frigate NVR™ - 一个具有实时目标检测的本地 NVR
[English](https://github.com/blakeblackshear/frigate) | \[简体中文\]
[English](https://github.com/blakeblackshear/frigate) | \[简体中文\]
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
<a href="https://hosted.weblate.org/engage/frigate-nvr/-/zh_Hans/">
<img src="https://hosted.weblate.org/widget/frigate-nvr/-/zh_Hans/svg-badge.svg" alt="翻译状态" />
</a>
一个完整的本地网络视频录像机NVR专为[Home Assistant](https://www.home-assistant.io)设计具备AI物体检测功能。使用OpenCV和TensorFlow在本地为IP摄像头执行实时物体检测。
一个完整的本地网络视频录像机NVR专为[Home Assistant](https://www.home-assistant.io)设计,具备 AI 目标/物体检测功能。使用 OpenCV TensorFlow 在本地为 IP 摄像头执行实时物体检测。
强烈推荐使用GPU或者AI加速器例如[Google Coral加速器](https://coral.ai/products/) 或者 [Hailo](https://hailo.ai/)。它们的性能甚至超过目前的顶级CPU并且可以以极低的耗电实现更优的性能。
- 通过[自定义组件](https://github.com/blakeblackshear/frigate-hass-integration)与Home Assistant紧密集成
- 设计上通过仅在必要时和必要地点寻找物体,最大限度地减少资源使用并最大化性能
强烈推荐使用 GPU 或者 AI 加速器(例如[Google Coral 加速器](https://coral.ai/products/) 或者 [Hailo](https://hailo.ai/)等)。它们的运行效率远远高于现在的顶级 CPU并且功耗也极低。
- 通过[自定义组件](https://github.com/blakeblackshear/frigate-hass-integration)与 Home Assistant 紧密集成
- 设计上通过仅在必要时和必要地点寻找目标,最大限度地减少资源使用并最大化性能
- 大量利用多进程处理,强调实时性而非处理每一帧
- 使用非常低开销的运动检测来确定运行物体检测的位置
- 使用TensorFlow进行物体检测运行在单独的进程中以达到最大FPS
- 通过MQTT进行通信便于集成到其他系统中
- 使用非常低开销的画面变动检测(也叫运动检测)来确定运行目标检测的位置
- 使用 TensorFlow 进行目标检测,并运行在单独的进程中以达到最大 FPS
- 通过 MQTT 进行通信,便于集成到其他系统中
- 根据检测到的物体设置保留时间进行视频录制
- 24/7全天候录制
- 通过RTSP重新流传输以减少摄像头的连接数
- 支持WebRTC和MSE实现低延迟的实时观看
- 24/7 全天候录制
- 通过 RTSP 重新流传输以减少摄像头的连接数
- 支持 WebRTC MSE实现低延迟的实时观看
## 社区中文翻译文档
@ -32,39 +35,55 @@
如果您想通过捐赠支持开发,请使用 [Github Sponsors](https://github.com/sponsors/blakeblackshear)。
## 协议
本项目采用 **MIT 许可证**授权。
**代码部分**:本代码库中的源代码、配置文件和文档均遵循 [MIT 许可证](LICENSE)。您可以自由使用、修改和分发这些代码,但必须保留原始版权声明。
**商标部分**“Frigate”名称、“Frigate NVR”品牌以及 Frigate 的 Logo 为 **Frigate LLC 的商标****不在** MIT 许可证覆盖范围内。
有关品牌资产的规范使用详情,请参阅我们的[《商标政策》](TRADEMARK.md)。
## 截图
### 实时监控面板
<div>
<img width="800" alt="实时监控面板" src="https://github.com/blakeblackshear/frigate/assets/569905/5e713cb9-9db5-41dc-947a-6937c3bc376e">
</div>
### 简单的核查工作流程
<div>
<img width="800" alt="简单的审查工作流程" src="https://github.com/blakeblackshear/frigate/assets/569905/6fed96e8-3b18-40e5-9ddc-31e6f3c9f2ff">
</div>
### 多摄像头可按时间轴查看
<div>
<img width="800" alt="多摄像头可按时间轴查看" src="https://github.com/blakeblackshear/frigate/assets/569905/d6788a15-0eeb-4427-a8d4-80b93cae3d74">
</div>
### 内置遮罩和区域编辑器
<div>
<img width="800" alt="内置遮罩和区域编辑器" src="https://github.com/blakeblackshear/frigate/assets/569905/d7885fc3-bfe6-452f-b7d0-d957cb3e31f5">
</div>
## 翻译
我们使用 [Weblate](https://hosted.weblate.org/projects/frigate-nvr/) 平台提供翻译支持,欢迎参与进来一起完善。
## 非官方中文讨论社区
欢迎加入中文讨论QQ群[1043861059](https://qm.qq.com/q/7vQKsTmSz)
欢迎加入中文讨论 QQ 群:[1043861059](https://qm.qq.com/q/7vQKsTmSz)
Bilibilihttps://space.bilibili.com/3546894915602564
## 中文社区赞助商
[![EdgeOne](https://edgeone.ai/media/34fe3a45-492d-4ea4-ae5d-ea1087ca7b4b.png)](https://edgeone.ai/zh?from=github)
本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助
---
**Copyright © 2025 Frigate LLC.**

View File

@ -75,7 +75,13 @@ audio:
### Audio Transcription
Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAIs open-source Whisper models via `faster-whisper`. To enable transcription, enable it in your config. Note that audio detection must also be enabled as described above in order to use audio transcription features.
Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAIs open-source Whisper models via `faster-whisper`. The goal of this feature is to support Semantic Search for `speech` audio events. Frigate is not intended to act as a continuous, fully-automatic speech transcription service — automatically transcribing all speech (or queuing many audio events for transcription) requires substantial CPU (or GPU) resources and is impractical on most systems. For this reason, transcriptions for events are initiated manually from the UI or the API rather than being run continuously in the background.
Transcription accuracy also depends heavily on the quality of your camera's microphone and recording conditions. Many cameras use inexpensive microphones, and distance to the speaker, low audio bitrate, or background noise can significantly reduce transcription quality. If you need higher accuracy, more robust long-running queues, or large-scale automatic transcription, consider using the HTTP API in combination with an automation platform and a cloud transcription service.
#### Configuration
To enable transcription, enable it in your config. Note that audio detection must also be enabled as described above in order to use audio transcription features.
```yaml
audio_transcription:

View File

@ -1,13 +1,18 @@
.alert {
padding: 12px;
background: #fff8e6;
border-bottom: 1px solid #ffd166;
text-align: center;
font-size: 15px;
}
.alert a {
color: #1890ff;
font-weight: 500;
margin-left: 6px;
}
padding: 12px;
background: #fff8e6;
border-bottom: 1px solid #ffd166;
text-align: center;
font-size: 15px;
}
[data-theme="dark"] .alert {
background: #3b2f0b;
border-bottom: 1px solid #665c22;
}
.alert a {
color: #1890ff;
font-weight: 500;
margin-left: 6px;
}

View File

@ -578,8 +578,14 @@ def create_user(
return JSONResponse(content={"username": body.username})
@router.delete("/users/{username}")
def delete_user(username: str):
@router.delete("/users/{username}", dependencies=[Depends(require_role(["admin"]))])
def delete_user(request: Request, username: str):
# Prevent deletion of the built-in admin user
if username == "admin":
return JSONResponse(
content={"message": "Cannot delete admin user"}, status_code=403
)
User.delete_by_id(username)
return JSONResponse(content={"success": True})

View File

@ -12,6 +12,7 @@ from typing import Any
import cv2
from peewee import DoesNotExist
from titlecase import titlecase
from frigate.comms.embeddings_updater import EmbeddingsRequestEnum
from frigate.comms.inter_process import InterProcessRequestor
@ -455,14 +456,14 @@ def run_analysis(
for i, verified_label in enumerate(final_data["data"]["verified_objects"]):
object_type = verified_label.replace("-verified", "").replace("_", " ")
name = sub_labels_list[i].replace("_", " ").title()
name = titlecase(sub_labels_list[i].replace("_", " "))
unified_objects.append(f"{name} ({object_type})")
for label in objects_list:
if "-verified" in label:
continue
elif label in labelmap_objects:
object_type = label.replace("_", " ").title()
object_type = titlecase(label.replace("_", " "))
if label in attribute_labels:
unified_objects.append(f"{object_type} (delivery/service)")

View File

@ -405,9 +405,6 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
if obj_data.get("end_time") is not None:
return
if obj_data.get("stationary"):
return
object_id = obj_data["id"]
if (

6
web/package-lock.json generated
View File

@ -4702,9 +4702,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001651",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"version": "1.0.30001757",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
"dev": true,
"funding": [
{

View File

@ -13,7 +13,7 @@ import { cn } from "@/lib/utils";
import { isPWA } from "@/utils/isPWA";
import { Button } from "@/components/ui/button";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { useHistoryBack } from "@/hooks/use-history-back";
const MobilePageContext = createContext<{
open: boolean;
@ -24,15 +24,16 @@ type MobilePageProps = {
children: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
enableHistoryBack?: boolean;
};
export function MobilePage({
children,
open: controlledOpen,
onOpenChange,
enableHistoryBack = true,
}: MobilePageProps) {
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
const location = useLocation();
const open = controlledOpen ?? uncontrolledOpen;
const setOpen = useCallback(
@ -46,33 +47,12 @@ export function MobilePage({
[onOpenChange, setUncontrolledOpen],
);
useEffect(() => {
let isActive = true;
if (open && isActive) {
window.history.pushState({ isMobilePage: true }, "", location.pathname);
}
const handlePopState = (event: PopStateEvent) => {
if (open && isActive) {
event.preventDefault();
setOpen(false);
// Delay replaceState to ensure state updates are processed
setTimeout(() => {
if (isActive) {
window.history.replaceState(null, "", location.pathname);
}
}, 0);
}
};
window.addEventListener("popstate", handlePopState);
return () => {
isActive = false;
window.removeEventListener("popstate", handlePopState);
};
}, [open, setOpen, location.pathname]);
// Handle browser back button to close mobile page
useHistoryBack({
enabled: enableHistoryBack,
open,
onClose: () => setOpen(false),
});
return (
<MobilePageContext.Provider value={{ open, onOpenChange: setOpen }}>

View File

@ -113,7 +113,12 @@ export function PlatformAwareSheet({
}
return (
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
<Sheet
open={open}
onOpenChange={onOpenChange}
modal={false}
enableHistoryBack
>
<SheetTrigger asChild className={triggerClassName}>
{trigger}
</SheetTrigger>

View File

@ -2,6 +2,7 @@ import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
import { useHistoryBack } from "@/hooks/use-history-back";
// Enhanced Dialog with History Support
interface HistoryDialogProps extends DialogPrimitive.DialogProps {
@ -15,51 +16,28 @@ const Dialog = ({
...props
}: HistoryDialogProps) => {
const [internalOpen, setInternalOpen] = React.useState(open || false);
const historyStateRef = React.useRef<null | {
listener: (e: PopStateEvent) => void;
}>(null);
// Sync internal state with controlled open prop
React.useEffect(() => {
if (open !== undefined) {
setInternalOpen(open);
}
}, [open]);
React.useEffect(() => {
if (enableHistoryBack) {
if (internalOpen) {
window.history.pushState({ dialogOpen: true }, "");
const handleOpenChange = React.useCallback(
(newOpen: boolean) => {
setInternalOpen(newOpen);
onOpenChange?.(newOpen);
},
[onOpenChange],
);
const listener = () => {
setInternalOpen(false);
if (onOpenChange) onOpenChange(false);
};
historyStateRef.current = { listener };
window.addEventListener("popstate", listener);
return () => {
if (internalOpen) {
window.removeEventListener("popstate", listener);
historyStateRef.current = null;
}
};
} else if (historyStateRef.current) {
window.removeEventListener(
"popstate",
historyStateRef.current.listener,
);
historyStateRef.current = null;
}
}
}, [enableHistoryBack, internalOpen, onOpenChange]);
const handleOpenChange = (open: boolean) => {
setInternalOpen(open);
if (onOpenChange) {
onOpenChange(open);
}
};
// Handle browser back button to close dialog
useHistoryBack({
enabled: enableHistoryBack,
open: internalOpen,
onClose: () => handleOpenChange(false),
});
return (
<DialogPrimitive.Root

View File

@ -4,6 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
import { useHistoryBack } from "@/hooks/use-history-back";
// Enhanced Sheet with History Support
interface HistorySheetProps extends SheetPrimitive.DialogProps {
@ -17,51 +18,28 @@ const Sheet = ({
...props
}: HistorySheetProps) => {
const [internalOpen, setInternalOpen] = React.useState(open || false);
const historyStateRef = React.useRef<null | {
listener: (e: PopStateEvent) => void;
}>(null);
// Sync internal state with controlled open prop
React.useEffect(() => {
if (open !== undefined) {
setInternalOpen(open);
}
}, [open]);
React.useEffect(() => {
if (enableHistoryBack) {
if (internalOpen) {
window.history.pushState({ sheetOpen: true }, "");
const handleOpenChange = React.useCallback(
(newOpen: boolean) => {
setInternalOpen(newOpen);
onOpenChange?.(newOpen);
},
[onOpenChange],
);
const listener = () => {
setInternalOpen(false);
if (onOpenChange) onOpenChange(false);
};
historyStateRef.current = { listener };
window.addEventListener("popstate", listener);
return () => {
if (internalOpen) {
window.removeEventListener("popstate", listener);
historyStateRef.current = null;
}
};
} else if (historyStateRef.current) {
window.removeEventListener(
"popstate",
historyStateRef.current.listener,
);
historyStateRef.current = null;
}
}
}, [enableHistoryBack, internalOpen, onOpenChange]);
const handleOpenChange = (open: boolean) => {
setInternalOpen(open);
if (onOpenChange) {
onOpenChange(open);
}
};
// Handle browser back button to close sheet
useHistoryBack({
enabled: enableHistoryBack,
open: internalOpen,
onClose: () => handleOpenChange(false),
});
return (
<SheetPrimitive.Root

View File

@ -0,0 +1,57 @@
import * as React from "react";
interface UseHistoryBackOptions {
enabled: boolean;
open: boolean;
onClose: () => void;
}
/**
* Hook that manages browser history for overlay components (dialogs, sheets, etc.)
* When enabled, pressing the browser back button will close the overlay instead of navigating away.
*/
export function useHistoryBack({
enabled,
open,
onClose,
}: UseHistoryBackOptions): void {
const historyPushedRef = React.useRef(false);
const closedByBackRef = React.useRef(false);
// Keep onClose in a ref to avoid effect re-runs that cause multiple history pushes
const onCloseRef = React.useRef(onClose);
React.useLayoutEffect(() => {
onCloseRef.current = onClose;
});
React.useEffect(() => {
if (!enabled) return;
if (open) {
// Only push history state if we haven't already (prevents duplicates in strict mode)
if (!historyPushedRef.current) {
window.history.pushState({ overlayOpen: true }, "");
historyPushedRef.current = true;
}
const handlePopState = () => {
closedByBackRef.current = true;
historyPushedRef.current = false;
onCloseRef.current();
};
window.addEventListener("popstate", handlePopState);
return () => {
window.removeEventListener("popstate", handlePopState);
};
} else {
// Overlay is closing - clean up history if we pushed and it wasn't via back button
if (historyPushedRef.current && !closedByBackRef.current) {
window.history.back();
}
historyPushedRef.current = false;
closedByBackRef.current = false;
}
}, [enabled, open]);
}

View File

@ -35,6 +35,7 @@ import { useTranslation } from "react-i18next";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { getTranslatedLabel } from "@/utils/i18n";
import { cn } from "@/lib/utils";
type MasksAndZoneViewProps = {
selectedCamera: string;
@ -697,7 +698,10 @@ export default function MasksAndZonesView({
</div>
<div
ref={containerRef}
className="flex max-h-[50%] md:mr-3 md:h-dvh md:max-h-full md:w-7/12 md:grow"
className={cn(
"flex max-h-[50%] md:h-dvh md:max-h-full md:w-7/12 md:grow",
isDesktop && "md:mr-3",
)}
>
<div className="mx-auto flex size-full flex-row justify-center">
{cameraConfig &&

View File

@ -23,6 +23,8 @@ import { LuExternalLink } from "react-icons/lu";
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
import { Trans, useTranslation } from "react-i18next";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { cn } from "@/lib/utils";
import { isDesktop } from "react-device-detect";
type MotionTunerViewProps = {
selectedCamera: string;
@ -325,7 +327,12 @@ export default function MotionTunerView({
</div>
{cameraConfig ? (
<div className="flex max-h-[70%] md:mr-3 md:h-dvh md:max-h-full md:w-7/12 md:grow">
<div
className={cn(
"flex max-h-[70%] md:h-dvh md:max-h-full md:w-7/12 md:grow",
isDesktop && "md:mr-3",
)}
>
<div className="size-full min-h-10">
<AutoUpdatingCameraImage
camera={cameraConfig.name}

View File

@ -43,6 +43,7 @@ import { useTriggers } from "@/api/ws";
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
import { CiCircleAlert } from "react-icons/ci";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { isDesktop } from "react-device-detect";
type ConfigSetBody = {
requires_restart: number;
@ -440,7 +441,12 @@ export default function TriggerView({
return (
<div className="flex size-full flex-col md:flex-row">
<Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none md:mr-3 md:mt-0">
<div
className={cn(
"scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2",
isDesktop && "order-none mr-3 mt-0",
)}
>
{!isSemanticSearchEnabled ? (
<div className="mb-5 flex flex-row items-center justify-between gap-2">
<div className="flex flex-col items-start">
@ -651,7 +657,7 @@ export default function TriggerView({
</div>
{/* Desktop Table View */}
<div className="scrollbar-container hidden flex-1 overflow-hidden rounded-lg border border-border bg-background_alt md:mr-3 md:block">
<div className="scrollbar-container hidden flex-1 overflow-hidden rounded-lg border border-border bg-background_alt md:block">
<div className="h-full overflow-auto">
<Table>
<TableHeader className="sticky top-0 bg-muted/50">