refactor(park-desktop): admin 后台新 UI + vision_yolo 手势接入,清理设计文档与安装包,补充工作区规范

This commit is contained in:
Pine
2026-08-24 11:27:55 +08:00
parent 9f2b90aeca
commit 4e73a6bba8
19 changed files with 700 additions and 1239 deletions
+4
View File
@@ -39,6 +39,10 @@ class Settings:
DATA_FILE = Path(_env("DPM_DATA_FILE", str(BASE_DIR / "data.json")))
DIST_DIR = Path(_env("DPM_DIST_DIR", str(BASE_DIR.parent / "dist"))) # 前端构建产物(可选托管)
# ---- 视频帧保存(摄像头识别帧,仅检测到人脸/手势时落盘,便于排查) ----
SAVE_VISION_FRAMES = _env("DPM_SAVE_VISION_FRAMES", "1") == "1"
VISION_FRAME_SAVE_DIR = Path(_env("DPM_VISION_SAVE_DIR", str(MEDIA_DIR / "video")))
# ---- MQTT(优先使用 .env 中的局域网 Broker 配置) ----
MQTT_ENABLED = _env("DPM_MQTT_ENABLED", "1") == "1"
MQTT_HOST = _env("DPM_MQTT_HOST", _env("MQTT_BROKER_HOST", "192.168.1.9"))
+5 -15
View File
@@ -14,7 +14,7 @@ from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
@@ -171,17 +171,7 @@ async def update_check(body: UpdateCheckBody):
# ---------- 前端静态托管 + SPA 回退(浏览器直接访问 :8000 即可) ----------
@app.get("/{path:path}", include_in_schema=False)
async def spa_fallback(path: str):
if not DIST.exists():
return JSONResponse({"detail": "前端未构建(dist 目录不存在)"}, status_code=404)
dist = DIST.resolve()
if not path:
target = dist / "index.html"
else:
target = (dist / path).resolve()
if target.is_file() and target.is_relative_to(dist):
return FileResponse(target)
index = dist / "index.html"
if index.exists():
return FileResponse(index)
return JSONResponse({"detail": "Not Found"}, status_code=404)
async def spa_fallback(request: Request):
"""默认导向管理后台:屏蔽通过后端地址直接访问大屏前端(含首页 / 及 /twin /ai /screen 等)。
/api、/admin、/static、/file 等已在更早路由/挂载处理,不会走到这里。"""
return RedirectResponse(url="/admin", status_code=307)
+39
View File
@@ -19,8 +19,12 @@ from pathlib import Path
from PIL import Image
from .config import settings
log = logging.getLogger("dpm.vision")
_frame_seq = 0
MODEL_DIR = Path(__file__).resolve().parent.parent / "models"
FACE_MODEL_PATH = MODEL_DIR / "yolov8n-face.pt"
POSE_MODEL_PATH = MODEL_DIR / "yolov8n-pose.pt"
@@ -188,6 +192,40 @@ def predict_jpeg(jpeg_bytes: bytes):
}
def _should_save(data: dict) -> bool:
"""仅当画面检测到人脸/手势时才值得落盘保存。"""
return (
int(data.get("faces") or 0) > 0
or bool(data.get("raised"))
or bool(data.get("fists"))
or bool(data.get("both_up"))
or bool(data.get("pointing"))
or bool(data.get("hands_close"))
)
def maybe_save_frame(jpeg: bytes, data: dict):
"""仅将命中(有人/有动作)的 JPEG 帧落盘到 MEDIA_DIR/video/<小时目录>/,便于排查识别链路。
目录按小时分割(YYYYMMDD_HH),不自动清理。
可通过 DPM_SAVE_VISION_FRAMES=0 关闭,或 DPM_VISION_SAVE_DIR 改目录。"""
global _frame_seq
if not settings.SAVE_VISION_FRAMES:
return
if not _should_save(data):
return
_frame_seq += 1
ts = time.strftime("%Y%m%d_%H%M%S")
hour = time.strftime("%Y%m%d_%H")
try:
save_dir = settings.VISION_FRAME_SAVE_DIR / hour
save_dir.mkdir(parents=True, exist_ok=True)
path = save_dir / f"{ts}_{_frame_seq:05d}.jpg"
with open(path, "wb") as f:
f.write(jpeg)
except Exception as e: # noqa: BLE001
log.warning("vision: 保存帧失败 %s", e)
def predict_base64(b64: str):
"""入口:base64 JPEG → 检测结果 dict(含 ok 标记,失败时带 error)"""
try:
@@ -198,6 +236,7 @@ def predict_base64(b64: str):
try:
data = predict_jpeg(jpeg)
data["ok"] = True
maybe_save_frame(jpeg, data)
return data
except Exception as e: # noqa: BLE001
log.error("vision: 推理失败 %s", e, exc_info=True)