feat: 事件时间轴缩略帧 + 人物管理头像 + 人物合并硬规则校验
## 新架构:Oracle 集中计算 + NAS 代理展示 ### Oracle 端 (fam-edge) - 新增 frame_service: ffmpeg 视频抽帧 + VLM 人物定位裁剪头像(磁盘缓存) - 新增 /api/oracle/frame: 按 video_id+ts 抽帧返回 jpeg(带 token) - 新增 /api/oracle/avatar: 按 label 生成人物头像(VLM 定位人物 + 兜底整帧居中) - 新增 person_identifier: 人物身份识别模块 - Gemini 适配器支持 flash/flash-lite 双模型切换,429 自动降级 - frame_service VLM 全模型 429 时进入 10 分钟熔断,避免每次请求白打配额 - 兜底头像不落缓存,配额恢复后自动重试 VLM 精确定位 ### 人物合并硬规则校验(框架级修复) - person_service: LLM 合并结果落库前加硬冲突检测 - 性别冲突 → 绝不合并 - 年龄档跨未成年/成年 → 绝不合并(防止把爷爷/宝宝并进同一人) - oracle_db: upsert_person 入口剥离括号后缀(人物A(别名:人物B) → 人物A),消灭垃圾人物行 - 修复 set_canonical 丢弃 source 参数的 bug(旧代码硬编码 'manual' 导致错误合并被永久固化) - get_events_for_label: 只提取该身份组的特征文本,头像定位更精准 ### NAS 端 (fam-core) - 新增 img_proxy: /api/proxy/frame 和 /api/proxy/avatar 代理 Oracle 图片 - app.py 注册 img_bp 蓝图 - oracle_sync / db_layer / member_manager 同步人物表 ### UI 端 (fam-ui) - 事件时间轴: 每条事件卡片加时间点缩略帧 - 人物管理: 每人卡片加头像(150x150 圆角) - parse_persons: 剥离括号备注,与 Oracle 归一化一致 - 新增 EventItem 组件、Timeline 页改造 - Chat / ServiceStatus 页相应调整 ### 数据库 - scripts/ddl.sql: 同步表结构更新 - Oracle people 表: features_json / display_uid / source 字段完善
This commit is contained in:
@@ -33,3 +33,34 @@ class QAOrchestrator:
|
||||
return answer, adapter.provider_name
|
||||
logger.info(f"QA {adapter.provider_name} 无返回,降级下一模型")
|
||||
return None, None
|
||||
|
||||
def run_qa_stream(self, prompt: str, max_tokens: int = 1024):
|
||||
"""流式版:依次尝试各适配器的 chat_stream(),yield 结构化事件字典。
|
||||
|
||||
事件类型:
|
||||
{"type":"provider_trying","provider":p} 开始尝试这个 provider
|
||||
{"type":"chunk","provider":p,"text":t} 这个 provider 吐出的文本增量
|
||||
{"type":"provider_failed","provider":p} 这个 provider 一个字都没吐出就失败,换下一个
|
||||
{"type":"done","provider":p} 成功结束(这个 provider 至少吐出过一块)
|
||||
{"type":"all_failed"} 所有 provider 都失败
|
||||
|
||||
跟 run_qa 一样"仅在还没吐出任何文本时才允许换下一个 provider"——一旦
|
||||
开始给用户看字了,中途失败就结束这次生成,不再悄悄换源接着写。
|
||||
"""
|
||||
for adapter in self.adapters:
|
||||
yield {"type": "provider_trying", "provider": adapter.provider_name}
|
||||
got_any = False
|
||||
try:
|
||||
for chunk in adapter.chat_stream(prompt, max_tokens=max_tokens):
|
||||
if chunk:
|
||||
got_any = True
|
||||
yield {"type": "chunk", "provider": adapter.provider_name, "text": chunk}
|
||||
except Exception as e:
|
||||
logger.warning(f"QA {adapter.provider_name} 流式异常: {e}")
|
||||
if got_any:
|
||||
logger.info(f"QA 流式命中 provider={adapter.provider_name}")
|
||||
yield {"type": "done", "provider": adapter.provider_name}
|
||||
return
|
||||
logger.info(f"QA {adapter.provider_name} 流式无返回,降级下一模型")
|
||||
yield {"type": "provider_failed", "provider": adapter.provider_name}
|
||||
yield {"type": "all_failed"}
|
||||
|
||||
Reference in New Issue
Block a user