fix(ui): sync_events 查询补 JOIN sync_videos 取 camera_name(修 1054 报错);feat(缩略图): Oracle 处理成功后抽首帧存 thumbs/{id}.jpg + /api/oracle/video/{id}/thumb 带 token 接口;fam-ui 视频会话显示缩略图(oracle_url+token);start_ui.sh 注入 .env
This commit is contained in:
@@ -12,7 +12,7 @@ API-Gateway - Flask 蓝图(新架构 v2)
|
||||
"""
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask import Blueprint, request, jsonify, send_file
|
||||
|
||||
from ..logger import setup_logger
|
||||
from .. import state
|
||||
@@ -116,6 +116,20 @@ def chat_ask():
|
||||
return jsonify({"answer": answer, "provider": provider}), 200
|
||||
|
||||
|
||||
@api_bp.route('/api/oracle/video/<int:video_id>/thumb', methods=['GET'])
|
||||
def video_thumb(video_id):
|
||||
"""返回视频首帧缩略图(token 校验,不公网裸奔)。
|
||||
|
||||
缩略图由 video_processor 处理成功后生成于 /opt/fam-edge/thumbs/{video_id}.jpg。
|
||||
"""
|
||||
if not _check_token():
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
path = f"/opt/fam-edge/thumbs/{video_id}.jpg"
|
||||
if not os.path.isfile(path):
|
||||
return jsonify({"error": "thumb_not_found"}), 404
|
||||
return send_file(path, mimetype='image/jpeg')
|
||||
|
||||
|
||||
@api_bp.route('/health', methods=['GET'])
|
||||
def health():
|
||||
"""健康检查"""
|
||||
|
||||
@@ -140,6 +140,10 @@ class VideoProcessor:
|
||||
})
|
||||
|
||||
self.db.mark_video_processed(video_id, summary, norm_events, people, provider)
|
||||
# 抽首帧生成缩略图(供前端展示;失败不影响主流程)
|
||||
row = self.db.get_video_by_id(video_id)
|
||||
if row and row['local_path']:
|
||||
self._generate_thumb(video_id, row['local_path'])
|
||||
|
||||
# 更新 people 表(标签级,待 person_service 合并)
|
||||
for p in people:
|
||||
@@ -147,3 +151,31 @@ class VideoProcessor:
|
||||
self.db.upsert_person(p, source='llm')
|
||||
logger.info(f"[video_id={video_id}] 已落库: summary={len(summary)}字, "
|
||||
f"events={len(norm_events)}, people={people}")
|
||||
|
||||
def _generate_thumb(self, video_id: int, video_path: str) -> bool:
|
||||
"""抽视频首帧生成 JPEG 缩略图(/opt/fam-edge/thumbs/{video_id}.jpg)"""
|
||||
try:
|
||||
import cv2
|
||||
db_path = self.config.get('oracle_db', {}).get(
|
||||
'path', '/opt/fam-edge/data/oracle.db')
|
||||
thumbs_dir = os.path.abspath(os.path.join(
|
||||
os.path.dirname(db_path), '..', 'thumbs'))
|
||||
os.makedirs(thumbs_dir, exist_ok=True)
|
||||
out = os.path.join(thumbs_dir, f"{video_id}.jpg")
|
||||
cap = cv2.VideoCapture(video_path)
|
||||
try:
|
||||
ok, frame = cap.read()
|
||||
finally:
|
||||
cap.release()
|
||||
if not ok or frame is None:
|
||||
logger.warning(f"抽帧失败 video_id={video_id}: 无法读取首帧")
|
||||
return False
|
||||
h, w = frame.shape[:2]
|
||||
if w > 640:
|
||||
frame = cv2.resize(frame, (640, int(h * 640 / w)))
|
||||
cv2.imwrite(out, frame, [cv2.IMWRITE_JPEG_QUALITY, 65])
|
||||
logger.info(f"缩略图已生成: {out}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"抽帧异常 video_id={video_id}: {e}")
|
||||
return False
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
# FAM-UI 配置文件 (NAS 端) - 新架构 v2(2026-08-21)
|
||||
# NAS 仅作管理后台,前端读本地 MariaDB 同步镜像,不再读取关键帧图片。
|
||||
# NAS 仅作管理后台,前端读本地 MariaDB 同步镜像;视频缩略图经 Oracle 带 token 接口获取。
|
||||
|
||||
core_url: "http://127.0.0.1:8000"
|
||||
|
||||
# 甲骨文 FAM-Edge 地址(视频缩略图接口,与同步同 token)
|
||||
oracle_url: "http://129.146.203.203:5000"
|
||||
oracle_token: "${ORACLE_SYNC_TOKEN}"
|
||||
|
||||
database:
|
||||
host: "127.0.0.1"
|
||||
port: 3306
|
||||
|
||||
@@ -25,6 +25,8 @@ from config_loader import load_config
|
||||
|
||||
_cfg = load_config()
|
||||
_core_url = _cfg.get('core_url', 'http://127.0.0.1:8000')
|
||||
_oracle_url = (_cfg.get('oracle_url') or '').rstrip('/')
|
||||
_oracle_token = _cfg.get('oracle_token') or ''
|
||||
_db_cfg = _cfg.get('database', {})
|
||||
|
||||
esc = _html.escape
|
||||
@@ -518,8 +520,18 @@ if page == "🕒 事件时间轴":
|
||||
f'<span class="bdg bdg-model">{esc(p)}</span>' for p in str(provider).split(','))
|
||||
|
||||
summary = vid.get('summary_json') or '暂无全局摘要'
|
||||
# 视频缩略图(Oracle 带 token 接口,无图时优雅降级)
|
||||
thumb_html = ''
|
||||
if _oracle_url:
|
||||
thumb_html = (
|
||||
f'<img src="{_oracle_url}/api/oracle/video/{vid["id"]}/thumb'
|
||||
f'?token={_oracle_token}" '
|
||||
f'style="width:100%;max-height:240px;object-fit:cover;'
|
||||
f'border-radius:10px;margin-bottom:10px;'
|
||||
f'onerror="this.style.display=\'none\'"/>')
|
||||
st.markdown(
|
||||
f'<div class="ev-head">'
|
||||
f'{thumb_html}'
|
||||
f'<div class="ev-title">'
|
||||
f'<span>{esc(vid.get("camera_name") or "未知摄像头")}</span>'
|
||||
f'<span class="bdg bdg-cam">会话 #{vid["id"]}</span>'
|
||||
@@ -534,8 +546,11 @@ if page == "🕒 事件时间轴":
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""SELECT ts, description, person_list_json, is_attention_event, camera_name
|
||||
FROM sync_events WHERE video_id=%s ORDER BY ts ASC""",
|
||||
"""SELECT e.ts, e.description, e.person_list_json,
|
||||
e.is_attention_event, v.camera_name
|
||||
FROM sync_events e
|
||||
JOIN sync_videos v ON e.video_id = v.id
|
||||
WHERE e.video_id=%s ORDER BY e.ts ASC""",
|
||||
(selected_id,))
|
||||
events = cursor.fetchall()
|
||||
finally:
|
||||
|
||||
@@ -14,6 +14,11 @@ fi
|
||||
|
||||
cd "$APP_DIR"
|
||||
|
||||
# 注入项目级 .env(ORACLE_SYNC_TOKEN 等),供 config.yaml ${VAR} 解析
|
||||
if [ -f "$(dirname "$APP_DIR")/.env" ]; then
|
||||
source "$(dirname "$APP_DIR")/.env"
|
||||
fi
|
||||
|
||||
# 检查虚拟环境
|
||||
if [ -d "venv" ]; then
|
||||
source venv/bin/activate
|
||||
|
||||
Reference in New Issue
Block a user