feat: UI 时间轴重构 + 关键帧图片全链路持久化
界面重做(深色监控面板主题):
- 事件时间轴页: 左侧事件列表卡片 + 右侧关键帧时间轴
(时间点 + 视频帧 + 人物/动作/衣着摘要 + 关注标记)
- 全局 CSS: 深色主题、卡片化按钮、统计卡、对话气泡、成员卡片
- 侧边栏任务队列状态徽章; 历史帧无图时优雅降级占位
关键帧持久化链路:
- Edge orchestrator: process_push_task 成功后把 keyframes
base64 注入 frame_details[i].frame_image (位置对齐视觉输入帧)
- NAS event_receiver: 落库前 base64 解码写盘到
fam-ui/static/frames/event_{id}/frame_{idx}.jpg
- payload 增量 ~300KB/事件 (6帧 jpeg q80), 队列/拉取均无压力
配置: fam-core/fam-ui 新增 storage.frame_image_dir
This commit is contained in:
@@ -47,3 +47,7 @@ chat_handler:
|
||||
# 智能问答统一走 FAM-Edge 编排端点(Gemini → NVIDIA → 本地 Ollama 兜底)
|
||||
qa_url: "http://129.146.203.203:5000/api/edge/chat/ask"
|
||||
timeout: 120
|
||||
|
||||
storage:
|
||||
# 关键帧落盘目录(event_receiver 写入,fam-ui 读取展示时间轴)
|
||||
frame_image_dir: "/volume1/web/sentinel-home-ai/fam-ui/static/frames"
|
||||
|
||||
@@ -36,3 +36,7 @@ chat_handler:
|
||||
ollama_url: "http://100.x.x.20:11434/api/generate"
|
||||
model_name: "llava-phi3"
|
||||
timeout: 120
|
||||
|
||||
storage:
|
||||
# 关键帧落盘目录(event_receiver 写入,fam-ui 读取展示时间轴)
|
||||
frame_image_dir: "/volume1/web/sentinel-home-ai/fam-ui/static/frames"
|
||||
|
||||
@@ -3,21 +3,31 @@ Event-Receiver - Flask 蓝图,接收 Edge 回调,写库
|
||||
|
||||
处理逻辑:
|
||||
1. 成功回调: 插入 monitor_events 1 条 + 遍历 frame_details 逐条插入 event_details
|
||||
2. 对未命名的 abstract_label 自动 upsert 到 family_members
|
||||
3. 更新 process_tasks 状态为 SUCCESS
|
||||
4. 失败回调: 更新任务状态为 FAILED,记录 failure_stage
|
||||
2. frame_details 携带的关键帧 base64 落盘到 fam-ui 静态目录(供时间轴展示)
|
||||
3. 对未命名的 abstract_label 自动 upsert 到 family_members
|
||||
4. 更新 process_tasks 状态为 SUCCESS
|
||||
5. 失败回调: 更新任务状态为 FAILED,记录 failure_stage
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import base64
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
from ..logger import setup_logger
|
||||
from ..config_loader import load_config
|
||||
from .. import db_layer
|
||||
|
||||
logger = setup_logger('fam-core.event_receiver')
|
||||
|
||||
event_bp = Blueprint('event_receiver', __name__)
|
||||
|
||||
# 关键帧落盘目录(fam-ui 读取展示;fam-core 与 fam-ui 同机部署)
|
||||
_cfg = load_config()
|
||||
FRAME_IMAGE_DIR = _cfg.get('storage', {}).get(
|
||||
'frame_image_dir',
|
||||
'/volume1/web/sentinel-home-ai/fam-ui/static/frames')
|
||||
|
||||
# 匹配 "人物A" / "人物B" 等 abstract_label
|
||||
_ABSTRACT_LABEL_PATTERN = re.compile(r'^人物[A-Z]$')
|
||||
|
||||
@@ -27,6 +37,28 @@ def _is_abstract_label(person: str) -> bool:
|
||||
return bool(_ABSTRACT_LABEL_PATTERN.match(person))
|
||||
|
||||
|
||||
def _save_frame_images(event_id: int, frame_details: list) -> int:
|
||||
"""把 frame_details 中的 base64 关键帧落盘,返回成功张数"""
|
||||
saved = 0
|
||||
for frame in frame_details:
|
||||
img_b64 = frame.pop('frame_image', None)
|
||||
if not img_b64:
|
||||
continue
|
||||
idx = frame.get('frame_index', 0)
|
||||
try:
|
||||
out_dir = os.path.join(FRAME_IMAGE_DIR, f'event_{event_id}')
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
out_path = os.path.join(out_dir, f'frame_{idx}.jpg')
|
||||
with open(out_path, 'wb') as f:
|
||||
f.write(base64.b64decode(img_b64))
|
||||
saved += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"[event_id={event_id}] 关键帧落盘失败 frame_{idx}: {e}")
|
||||
if saved:
|
||||
logger.info(f"[event_id={event_id}] 关键帧落盘 {saved} 张 -> {FRAME_IMAGE_DIR}")
|
||||
return saved
|
||||
|
||||
|
||||
def _upsert_abstract_members(frame_details: list):
|
||||
"""对未命名的 abstract_label 自动 upsert 到 family_members"""
|
||||
seen = {}
|
||||
@@ -63,8 +95,14 @@ def apply_success_event(task_id, data: dict) -> int:
|
||||
compute_provider=data.get('compute_provider', [])
|
||||
)
|
||||
|
||||
# 2. 遍历 frame_details 逐条插入
|
||||
# 2. 关键帧图片落盘(先落盘再入库:pop 掉 base64 后 insert,避免大字段进 DB)
|
||||
frame_details = data.get('frame_details', [])
|
||||
try:
|
||||
_save_frame_images(event_id, frame_details)
|
||||
except Exception as e:
|
||||
logger.warning(f"[event_id={event_id}] 关键帧落盘异常(不影响入库): {e}")
|
||||
|
||||
# 3. 遍历 frame_details 逐条插入
|
||||
for frame in frame_details:
|
||||
db_layer.insert_event_detail(
|
||||
event_id=event_id,
|
||||
|
||||
Reference in New Issue
Block a user