refactor(fam-edge): 重构第一阶段 - 人物图片零额外调用 + 运行时稳定性 + 工程质量
人物图片功能重做: bbox 随核心视频分析那一次 Gemini 调用一并产出(prompts.py 加
person_appearances.bbox 字段, [ymin,xmin,ymax,xmax] 0-1000 归一化), frame_service
直接用存好的 bbox 裁剪头像/事件缩略图, 删除原来"展示时额外调用 Gemini 定位人物"的
整套逻辑(locate_person_bbox/VLM 校验/熔断), 从架构上消除与核心视频分析共抢配额的
问题; 用真实数据验证裁剪结果正确框住人物本体。
NVIDIA 模型修复: 实测原配置的 3 个模型均不可用(asset_id 引用 500/400, 不支持视频),
改用 nemotron-3-nano-omni 的 base64 内嵌视频方式(唯一实测打通), 加 max_base64_mb
防止对大文件做注定失败的编码。
Gemini 多 Key 轮换: 支持 extra_api_keys 配置多个独立项目的 key, 配额用尽时依次
换 key 重试(每换 key 需重新上传, Files API 按项目隔离)。
稳定性加固: CircuitBreaker HALF_OPEN 清空旧失败计数(修复探测一失败就重新 OPEN 的
bug); chat() 统一接入熔断器(原来只有视频分析路径检查); NVIDIA 适配器改用共享
json_parser(原来自己重复实现且不做 schema 校验); Gemini Files API 上传超时也尝试
清理远程孤儿文件; video_processor/video_queue 里直接操作 OracleDB._conn 的裸 SQL
改走新增的 set_event_start_time/mark_video_invalid/reset_video_to_pending 方法;
/health 加入队列线程存活状态; 密钥改用 ${ENV_VAR} 引用(.env 已支持自动加载),
不再明文写入 config.yaml。
工程质量: 新增 fam-edge/tests(32 个单元测试, 覆盖熔断器状态机/JSON 解析容错/
时间戳解析/bbox 坐标换算/多 key 解析), 新增 scripts/smoke_test.py(发版前接口
稳定性检查); 清理死代码(OllamaAdapter.analyze_frames、get_sync_delta 死分支、
未使用的 vision_timeout/max_concurrent_tasks 配置项); 修正 get_events_for_label
排序(改最近优先 + 过滤畸形历史时间戳)。
已部署 Oracle 并跑通 smoke test 全部 6 项检查。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,19 +5,18 @@ API-Gateway - Flask 蓝图(新架构 v3)
|
||||
GET /api/oracle/sync NAS 每 30 分钟拉取增量(since + token 校验)
|
||||
POST /api/oracle/people/correct NAS 推送手动命名校正(label -> canonical_name)
|
||||
POST /api/edge/chat/ask 智能问答编排(Gemini -> NVIDIA -> Ollama)
|
||||
GET /api/oracle/activity 实时服务状态 + 最近活动流
|
||||
GET /health 健康检查
|
||||
GET /api/oracle/activity 实时服务状态 + 最近活动流
|
||||
GET /api/oracle/frame 事件时刻缩略帧(ffmpeg 抽帧 + 磁盘缓存)
|
||||
GET /api/oracle/avatar 人物头像(按视频分析产出的 bbox 裁剪)
|
||||
GET /health 健康检查(DB + 队列线程存活)
|
||||
|
||||
已移除(v3 去除帧图/avatar 依赖,改用大模型特征值):
|
||||
/api/oracle/video/<id>/thumb, /api/oracle/event/<id>/thumb,
|
||||
/api/oracle/person/avatar —— 不再生成 jpg,UI 读 sync_people.features_json
|
||||
|
||||
已移除(旧推送/分块/队列模式): /video/push, /enqueue, /chunk, /assemble,
|
||||
/results, /queue/stats, /mark_frames
|
||||
图片能力(v4): 计算全部在 Oracle 本机 ffmpeg 抽帧/裁剪;人物 bbox 随视频分析那
|
||||
一次 Gemini 调用一并产出(见 ai_orchestrator/prompts.py),frame_service 不再
|
||||
额外调用任何模型。NAS 经 core 代理读取,不在 NAS 做图像计算。
|
||||
"""
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask import Blueprint, request, jsonify, Response
|
||||
|
||||
from ..logger import setup_logger
|
||||
from .. import state
|
||||
@@ -165,13 +164,64 @@ def activity():
|
||||
}), 200
|
||||
|
||||
|
||||
@api_bp.route('/api/oracle/frame', methods=['GET'])
|
||||
def oracle_frame():
|
||||
"""事件时刻缩略帧:video_id + 绝对时间戳 -> jpeg(token 校验,磁盘缓存)"""
|
||||
if not _check_token():
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
video_id = request.args.get('video_id', type=int)
|
||||
ts = request.args.get('ts', '')
|
||||
w = request.args.get('w', default=400, type=int)
|
||||
if not video_id or not ts:
|
||||
return jsonify({"error": "缺少 video_id / ts"}), 400
|
||||
from ..frame_service import extract_frame
|
||||
data = extract_frame(state.get_db(), video_id, ts, width=w)
|
||||
if data is None:
|
||||
return jsonify({"error": "抽帧失败"}), 404
|
||||
return Response(data, mimetype='image/jpeg')
|
||||
|
||||
|
||||
@api_bp.route('/api/oracle/avatar', methods=['GET'])
|
||||
def oracle_avatar():
|
||||
"""人物头像:label/canonical -> jpeg(VLM 定位人物裁剪,磁盘缓存)"""
|
||||
if not _check_token():
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
label = request.args.get('label', '')
|
||||
w = request.args.get('w', default=160, type=int)
|
||||
if not label:
|
||||
return jsonify({"error": "缺少 label"}), 400
|
||||
from ..frame_service import build_avatar
|
||||
data = build_avatar(state.get_db(), label, width=w)
|
||||
if data is None:
|
||||
return jsonify({"error": "头像生成失败"}), 404
|
||||
return Response(data, mimetype='image/jpeg')
|
||||
|
||||
|
||||
@api_bp.route('/health', methods=['GET'])
|
||||
def health():
|
||||
"""健康检查"""
|
||||
"""健康检查:DB 连通性 + producer/consumer 线程存活状态。
|
||||
|
||||
之前只查一次 DB,队列线程全死了也会报 ok;现在把 VideoQueue.is_alive() 也
|
||||
带上,让 /health 真正能反映处理管线是否还在跑。
|
||||
"""
|
||||
try:
|
||||
db = state.get_db()
|
||||
vids = db._conn.execute(
|
||||
"SELECT COUNT(*) c FROM videos WHERE status='done'").fetchone()['c']
|
||||
return jsonify({"status": "ok", "processed_videos": vids}), 200
|
||||
except Exception as e:
|
||||
return jsonify({"status": "error", "error": str(e)}), 500
|
||||
|
||||
queue = state.get_queue()
|
||||
queue_alive = None
|
||||
if queue is not None:
|
||||
try:
|
||||
queue_alive = queue.is_alive()
|
||||
except Exception as e:
|
||||
logger.warning(f"queue.is_alive 异常: {e}")
|
||||
|
||||
status = "ok" if (queue is None or queue_alive) else "degraded"
|
||||
return jsonify({
|
||||
"status": status,
|
||||
"processed_videos": vids,
|
||||
"queue_alive": queue_alive,
|
||||
}), 200 if status == "ok" else 503
|
||||
|
||||
Reference in New Issue
Block a user