## 乱码修复 Gemini 流式接口 resp.iter_lines(decode_unicode=True) 没有显式设置 resp.encoding,requests 自己猜编码猜错了(Gemini 的 SSE 响应 Content-Type 不带 charset 参数),导致中文变成典型的 UTF-8 被当 Latin-1 解码的乱码。 fam-core 转发这一跳的 requests.iter_lines 调用有同样的坑,一并修了。 两处响应的 Content-Type 都补上 charset=utf-8,减少下游再犯同样错误的机会。 顺手用 latin-1/utf-8 往返把 NAS chat_history 里已经存了乱码的 3 条历史记录 恢复成正常文字。 ## 问答模型链重新设计(跟视频分析完全独立) 用户要求问答不再用 flash/omni,优先用 NVIDIA 免费文字模型里上下文最大的几个, 不行再退 Gemini 非 flash 文字模型,最后本地 Ollama 兜底。 - base_adapter 新增 usage 字段(纯标记,不影响行为) - qa.py 的 QAOrchestrator 只挑 role='text' 的适配器参与问答(按 config 出现 顺序决定降级顺序),role='vision' 的视频分析适配器(gemini-flash-latest / nvidia omni)不再参与问答,两条链路彻底解耦 - 新增问答专用 NVIDIA 文字模型链(同一适配器内部 model_chain 降级): nemotron-3-ultra-550b-a55b(1M上下文/561B) -> nemotron-3-super-120b-a12b (1M上下文/124B) -> openai/gpt-oss-120b(131K上下文/117B)。三个都用真实 API 调用验证过当前账号可访问;同时验证过 llama-3.1-70b-instruct 和 nemotron-super-49b-v1.5 都收到"08/25/2026 起弃用"通知,排除; nemotron-ultra-253b/mistral-large-2/nemotron-4-340b/kimi-k2.6 在目录里 能看到但实测调用返回 404"账号无权限",排除 - 新增问答专用 Gemini 非 flash 文字模型:gemini-pro-latest -> gemini-2.5-pro (1M 上下文,复用视频分析同一批多 key 轮换) - /api/edge/chat/ask(/stream) 默认 max_tokens 从 1024 提到 3072:新链路 优先用的都是"推理"模型,回答前会先吐一段思考过程,1024 经常在思考阶段 就被截断,用户永远看不到真正答案 新增 test_qa.py::test_init_only_keeps_text_role_adapters_in_config_order 验证角色过滤+顺序正确。 本地检查过 Ollama 服务本身:systemd enabled+running(8h 正常运行, qwen2.5:7b 已加载),并非没启动——用户观察到的现象另有原因。
244 lines
13 KiB
YAML
244 lines
13 KiB
YAML
# FAM-Edge 配置文件 (Oracle 端) - 新架构 v2
|
||
#
|
||
# 新架构(2026-08-21 重构):
|
||
# 1. 不再切片/抽帧:整视频直传云端 VLM(Gemini 用 Files API,NVIDIA 用整视频 video_url)
|
||
# 2. 视频来源:rclone 从 Google 硬盘实时同步到本地 local_dir,监听目录处理新视频
|
||
# 3. Oracle 自建 SQLite 库存储所有视频摘要/事件/人物,并对外提供同步接口供 NAS 拉取
|
||
# 4. 独立 person_service 汇总全量人物 -> LLM 合并为规范人物表 -> 回灌视频提示
|
||
# 5. NAS 仅作管理后台,每 30 分钟从甲骨文拉增量镜像到本地 MariaDB
|
||
|
||
# Oracle 端 HTTP 服务
|
||
server:
|
||
host: "0.0.0.0"
|
||
port: 5000
|
||
|
||
# Google 硬盘同步(rclone 负责同步落地,本段仅描述监听行为)
|
||
gdrive_sync:
|
||
enabled: true
|
||
local_dir: "/opt/fam-edge/gdrive_videos" # rclone 同步落地目录(video_processing 监听此目录)
|
||
watch_interval_sec: 30 # 监听新视频的轮询间隔
|
||
camera_name: "客厅" # 摄像头名称(注入视频提示)
|
||
# 文件名解析开始时间:监控文件名含时间戳时使用(如 2026-08-21_081500.mp4)
|
||
parse_start_from_filename: true
|
||
|
||
# Oracle 本地库(视频摘要/事件/人物)
|
||
oracle_db:
|
||
path: "/opt/fam-edge/data/oracle.db"
|
||
|
||
# NAS 拉取同步接口鉴权 token(与 NAS oracle_sync.token 一致,走 .env,不明文入库)
|
||
sync_api:
|
||
token: "${ORACLE_SYNC_TOKEN}"
|
||
|
||
# 人物识别服务(2026-08-22 停用):已被闭集识别 person_identifier 取代
|
||
# (汤圆/媳妇走性别年龄规则免费识别,爷爷/爸爸走视觉大模型比对参考图,见
|
||
# person_identifier 配置块)。原 LLM 合并逻辑不可靠(用户原话"现在的识别全是
|
||
# 错的"),继续跑只会用不可靠的猜测持续覆盖 people 表的 canonical_name,跟新
|
||
# 系统的结果打架——停用,不删除代码(关键冲突检测逻辑 _features_conflict 仍
|
||
# 保留供参考/未来复用)。
|
||
person_service:
|
||
enabled: false
|
||
schedule_interval_sec: 1800
|
||
model: "gemini"
|
||
|
||
# 视频处理
|
||
video_processing:
|
||
max_concurrent: 1 # 消费者线程数(串行处理,避免云端并发超额)
|
||
timeout: 900 # 兜底单视频分析超时
|
||
timeout_multiplier: 2 # 模型消费超时倍数:在 models[i].timeout 原值上 ×2(大视频上传+分析耗时)
|
||
max_retries: 10 # 单视频失败最大重试次数(配额/过载等瞬时故障给足重试机会)
|
||
retry_interval_sec: 3600 # 失败重试最小间隔:距上次失败 ≥1h 才重新入队,等配额恢复
|
||
file_validate: true # 登记入队前用 ffprobe 校验文件可解码;失败标记 invalid 不入队
|
||
stable_window_sec: 60 # 文件 mtime 稳定窗口:写入中(rclone 同步未完成)的文件跳过本轮
|
||
# 降级顺序:先 gemini 整视频,失败再 nvidia 整视频;两者都失败 -> 标记 failed
|
||
vision_order: ["gemini", "nvidia"]
|
||
|
||
# 运动事件(2026-08-22 运动事件驱动架构):
|
||
# - NAS 端 fam-core MotionNotifier 轮询 SS 事件后,POST 推送到甲骨文
|
||
# /api/ss/motion,落库 ss_motion_events(start_time/duration 为 Unix epoch);
|
||
# 并定期用空 events 调接口当心跳,证明推送链路存活。
|
||
# - video_processor 不再整段分析:整段素材按 ss_motion_events 中【已结束】的
|
||
# 运动事件分割成运动片段(motion_segment 配置),只分析运动片段。
|
||
dsm_motion_prefilter:
|
||
max_heartbeat_age_sec: 900 # 心跳超过 15 分钟没更新 -> 判定推送链路可能已死
|
||
|
||
# 运动片段分割(运动事件驱动架构,2026-08-22 新增):
|
||
# 整段素材视频(rclone 同步落地)按 ss_motion_events 分割成运动片段再分析。
|
||
# 只分割已结束事件(start_time+duration 落在当前时刻附近),进行中的事件
|
||
# 等结束后的下一轮再分割;素材保持 pending 直到窗口内事件全部结束。
|
||
motion_segment:
|
||
clips_dir: "/opt/fam-edge/motion_clips" # 分割产物目录
|
||
keep_audio: true # 保留音频(pcm_alaw -> aac 64k 转码);false 则 -an 去音频
|
||
min_duration_sec: 1 # 短于该时长的事件不分割
|
||
unfinished_grace_sec: 10 # start_time+duration 距当前 ≤ 该秒视为"已结束"容差
|
||
|
||
# 闭集人物识别(2026-08-22 新增,家里固定 4 人:爷爷/爸爸/媳妇/汤圆):
|
||
# 原来靠大模型自己编的"人物A/B/C"临时 uid + 文字特征描述跨视频合并,验证下来
|
||
# 不可靠(用户原话"现在的识别全是错的")。人脸向量方案也验证过,家庭监控这种
|
||
# 大广角/远距离画面下同人内部相似度经常比不同人还低,此路不通。
|
||
# 现在改成:汤圆(幼儿/儿童特征)/媳妇(唯一成年女性) 直接用 Gemini 已产出的
|
||
# 性别/年龄字段判断,免费且验证下来接近 100% 准;爷爷/爸爸(两个成年男性,纯
|
||
# 外观规则/人脸向量都区分不开) 改用视觉大模型"看参考图比对"——NVIDIA 实测
|
||
# 6/6 全对且配额与主分析链路完全独立,设为优先,Gemini flash-lite(7/8) 兜底。
|
||
# 参考图目录结构:{ref_dir}/爷爷/*.jpg、{ref_dir}/爸爸/*.jpg(已用确认过身份的
|
||
# 历史截图种好,见 PROGRESS.md 记录)。
|
||
person_identifier:
|
||
enabled: true
|
||
ref_dir: "/opt/fam-edge/data/person_refs"
|
||
max_ref_per_person: 6
|
||
# 连续两次分类调用之间的最小间隔(不分 provider 统一限速):正常处理新片段时
|
||
# 调用本来就稀疏,这个主要是给历史数据批量回填用的,避免短时间内密集调用打爆配额
|
||
min_call_interval_sec: 2
|
||
nvidia:
|
||
api_key: "${NVIDIA_API_KEY}"
|
||
model_name: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"
|
||
fallback_models: [] # 目前只验证过这一个能用的 NVIDIA 视觉模型,先留好扩展位
|
||
timeout: 60
|
||
max_retries: 3 # 每个模型对瞬时故障(429/5xx/超时)最多重试几次(含首次)
|
||
retry_backoff_sec: 3 # 重试前基础等待秒数,指数退避(3s -> 6s -> 12s)
|
||
gemini:
|
||
api_key: "${GEMINI_API_KEY}"
|
||
extra_api_keys: ["${GEMINI_API_KEY_2}", "${GEMINI_API_KEY_3}", "${GEMINI_API_KEY_4}"]
|
||
model_name: "gemini-flash-lite-latest"
|
||
timeout: 60
|
||
max_retries: 2
|
||
retry_backoff_sec: 3
|
||
|
||
# 智能问答降级链(与视频分析独立):Gemini -> NVIDIA -> 本地 Ollama
|
||
models:
|
||
- provider: "gemini"
|
||
role: "vision"
|
||
enabled: true
|
||
model_name: "gemini-flash-latest" # 主模型(每日免费配额 20 请求,按模型独立)
|
||
fallback_models:
|
||
- "gemini-flash-lite-latest"
|
||
api_key: "${GEMINI_API_KEY}"
|
||
# 多 Key 轮换(各自独立 Google Cloud 项目,配额互不影响):主 key 配额用尽时
|
||
# 依次尝试这些 key,每个 key 都会重新走一遍模型 fallback 链。key 本身放 .env,
|
||
# 这里只放环境变量名,不直接写密钥。
|
||
extra_api_keys:
|
||
- "${GEMINI_API_KEY_2}"
|
||
- "${GEMINI_API_KEY_3}"
|
||
- "${GEMINI_API_KEY_4}"
|
||
# 每个 key 对应的 Google Cloud 项目名(顺序对应上面 api_key + extra_api_keys),
|
||
# 用于模型统计页展示是哪个项目在跑,不填则退回 "key1/key2/..."
|
||
key_labels:
|
||
- "智能摄像头-1"
|
||
- "智能摄像头-2"
|
||
- "智能摄像头-3"
|
||
- "智能摄像头-4"
|
||
timeout: 600
|
||
# 问答专用超时(跟上面视频分析的 timeout 分开):用户在等交互式回答,一个
|
||
# key/模型卡住不该等 10 分钟,超时要短,快速降级到下一个 key/模型/provider
|
||
chat_timeout: 20
|
||
# 模型级独立超时(最终值,不参与编排层 ×2 放大)
|
||
# gemini-flash-lite 实测 ~22-34s,按用户要求放宽至 8 分钟(480s),避免大视频/排队时过早切断
|
||
model_timeouts:
|
||
"gemini-flash-lite-latest": 480
|
||
circuit_breaker:
|
||
enabled: true
|
||
threshold: 5
|
||
cooldown: 300
|
||
|
||
- provider: "nvidia"
|
||
role: "vision"
|
||
enabled: true
|
||
# 模型可用性实测记录(2026-08-21,用真实短视频逐个探测 chat.completions 接口):
|
||
# omni(本行 model_name):video_url 只认 base64 data URI,NVCF asset_id 引用
|
||
# 方式对它直接 500("Only base64 data URLs are supported for now")——本适配器
|
||
# 已改为 base64 内嵌整段视频,见 max_base64_mb。确认可用(真实返回结构化 JSON)。
|
||
# nemotron-nano-12b-v2-vl:走 asset_id 引用需要 NVCF-ASSET-DIR/
|
||
# NVCF-FUNCTION-ASSET-IDS 请求头,这两个头的值由 NVCF 服务端按内部路径生成,
|
||
# 客户端传什么都 400 "Invalid NVCF-ASSET-DIR",标准 OpenAI 兼容调用打不通,已移除。
|
||
# meta/llama-3.2-11b-vision-instruct:明确不支持视频输入
|
||
# ("At most 0 video(s) may be provided"),只能单图,已移除。
|
||
# base64 方案受请求体大小限制(实测约 25MB 上限),真实监控视频压缩后通常在
|
||
# 20MB 上下,超过 max_base64_mb 直接跳过(不做注定失败的慢速编码),不是本地
|
||
# 故意限制过窄——这是当前唯一能打通的 NVIDIA 视频理解路径。
|
||
model_name: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"
|
||
fallback_models: []
|
||
base_url: "https://integrate.api.nvidia.com/v1"
|
||
api_key: "${NVIDIA_API_KEY}"
|
||
timeout: 600
|
||
chat_timeout: 20 # 问答专用超时,跟视频分析的 timeout 分开
|
||
max_base64_mb: 20 # 超过此大小直接跳过 NVIDIA,不做注定失败的编码+上传
|
||
switch_interval_sec: 5 # 模型切换间隔:一个失败后等待再试下一个(未来加模型时用)
|
||
model_timeouts: # 模型级独立超时(最终值,不参与 ×2)
|
||
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": 300
|
||
circuit_breaker:
|
||
enabled: true
|
||
threshold: 5
|
||
cooldown: 300
|
||
|
||
# 问答专用 NVIDIA 文字模型链(2026-08-23 新增,跟上面视频分析用的 omni 模型
|
||
# 完全独立):用户要求问答不用 flash/omni,优先找 NVIDIA 免费文字模型里上下文
|
||
# 最大的几个。实测(2026-08-23)在当前账号可用、非 deprecated 的候选里:
|
||
# nemotron-3-ultra-550b-a55b: 1M 上下文,561B,最强,free endpoint 已验证可调
|
||
# nemotron-3-super-120b-a12b: 1M 上下文,124B,同为 Nemotron-3 系列备份
|
||
# openai/gpt-oss-120b: 131K 上下文,117B,不同厂商备份(Nemotron 系列整体
|
||
# 出问题时的多样性兜底)
|
||
# 淘汰原因记录:meta/llama-3.1-70b-instruct、nvidia/llama-3.3-nemotron-
|
||
# super-49b-v1.5 均已收到 "will be deprecated on 08/25/2026" 通知,不用;
|
||
# nvidia/llama-3.1-nemotron-ultra-253b-v1、mistralai/mistral-large-2-instruct、
|
||
# nvidia/nemotron-4-340b-instruct、moonshotai/kimi-k2.6 在 /v1/models 目录
|
||
# 里能看到,但实测调用 chat.completions 返回 404 "Not found for account"
|
||
# (免费层没有这些模型的调用权限,文档列出不代表能调)。
|
||
- provider: "nvidia"
|
||
role: "text"
|
||
usage: "qa_primary"
|
||
enabled: true
|
||
model_name: "nvidia/nemotron-3-ultra-550b-a55b"
|
||
fallback_models:
|
||
- "nvidia/nemotron-3-super-120b-a12b"
|
||
- "openai/gpt-oss-120b"
|
||
base_url: "https://integrate.api.nvidia.com/v1"
|
||
api_key: "${NVIDIA_API_KEY}"
|
||
timeout: 600
|
||
# 这几个都是"推理"模型,回答前会先输出一段思考过程再给最终答案,比普通模型
|
||
# 更费 token/更慢,问答超时给宽松一点(不是简单文字模型那种 20s 就该出结果)
|
||
chat_timeout: 45
|
||
switch_interval_sec: 3
|
||
circuit_breaker:
|
||
enabled: true
|
||
threshold: 5
|
||
cooldown: 300
|
||
|
||
# 问答专用 Gemini 非 flash 文字模型(2026-08-23 新增):用户明确要求问答不用
|
||
# flash,这里走 gemini-pro-latest(1M 上下文,跟 gemini-flash-latest 同样的
|
||
# "-latest" 别名习惯,自动跟最新 pro 版本),失败再退 gemini-2.5-pro。
|
||
# key 复用视频分析同一批(各自独立 Google Cloud 项目,配额互不影响)。
|
||
- provider: "gemini"
|
||
role: "text"
|
||
usage: "qa_primary"
|
||
enabled: true
|
||
model_name: "gemini-pro-latest"
|
||
fallback_models:
|
||
- "gemini-2.5-pro"
|
||
api_key: "${GEMINI_API_KEY}"
|
||
extra_api_keys:
|
||
- "${GEMINI_API_KEY_2}"
|
||
- "${GEMINI_API_KEY_3}"
|
||
- "${GEMINI_API_KEY_4}"
|
||
key_labels:
|
||
- "智能摄像头-1"
|
||
- "智能摄像头-2"
|
||
- "智能摄像头-3"
|
||
- "智能摄像头-4"
|
||
timeout: 60
|
||
chat_timeout: 30
|
||
circuit_breaker:
|
||
enabled: true
|
||
threshold: 5
|
||
cooldown: 300
|
||
|
||
# 本地模型:纯文本 qwen2.5:7b,仅参与智能问答兜底
|
||
- provider: "ollama"
|
||
role: "text"
|
||
usage: "qa_fallback"
|
||
enabled: true
|
||
model_name: "qwen2.5:7b"
|
||
base_url: "http://localhost:11434"
|
||
timeout: 120
|
||
num_predict: 512
|
||
circuit_breaker:
|
||
enabled: false
|