## 新架构: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 字段完善
82 lines
3.1 KiB
Python
82 lines
3.1 KiB
Python
from fam_edge.qa import QAOrchestrator
|
||
|
||
|
||
class _FakeAdapter:
|
||
def __init__(self, provider_name, chunks=None, raises=False):
|
||
self.provider_name = provider_name
|
||
self._chunks = chunks or []
|
||
self._raises = raises
|
||
|
||
def chat_stream(self, prompt, max_tokens=512):
|
||
if self._raises:
|
||
raise RuntimeError("boom")
|
||
for c in self._chunks:
|
||
yield c
|
||
|
||
def chat(self, prompt, max_tokens=512):
|
||
return ''.join(self._chunks) or None
|
||
|
||
|
||
def _orchestrator(adapters):
|
||
qa = QAOrchestrator.__new__(QAOrchestrator) # 跳过 __init__(不需要真实 config/adapters)
|
||
qa.adapters = adapters
|
||
return qa
|
||
|
||
|
||
def test_run_qa_stream_first_provider_success():
|
||
qa = _orchestrator([_FakeAdapter("gemini", chunks=["你", "好"])])
|
||
events = list(qa.run_qa_stream("hi"))
|
||
types = [e["type"] for e in events]
|
||
assert types == ["provider_trying", "chunk", "chunk", "done"]
|
||
assert events[1]["text"] == "你"
|
||
assert events[2]["text"] == "好"
|
||
assert events[-1]["provider"] == "gemini"
|
||
|
||
|
||
def test_run_qa_stream_falls_back_when_first_yields_nothing():
|
||
"""核心诉求: 第一个 provider 一个字都没吐出来才允许换下一个——不是失败就切,
|
||
是"完全没有产出"才切。"""
|
||
qa = _orchestrator([
|
||
_FakeAdapter("gemini", chunks=[]),
|
||
_FakeAdapter("nvidia", chunks=["答案"]),
|
||
])
|
||
events = list(qa.run_qa_stream("hi"))
|
||
types = [e["type"] for e in events]
|
||
assert types == ["provider_trying", "provider_failed", "provider_trying", "chunk", "done"]
|
||
assert events[-1]["provider"] == "nvidia"
|
||
|
||
|
||
def test_run_qa_stream_does_not_switch_after_partial_output():
|
||
"""核心诉求: 已经开始吐字之后中途失败,不能悄悄换下一个 provider 接着写
|
||
(会出现两段风格/内容不连贯的回答拼在一起)——直接结束这次生成。"""
|
||
class _PartialThenRaise:
|
||
provider_name = "gemini"
|
||
def chat_stream(self, prompt, max_tokens=512):
|
||
yield "先吐"
|
||
raise RuntimeError("connection reset")
|
||
|
||
qa = _orchestrator([_PartialThenRaise(), _FakeAdapter("nvidia", chunks=["不该被用到"])])
|
||
events = list(qa.run_qa_stream("hi"))
|
||
types = [e["type"] for e in events]
|
||
assert types == ["provider_trying", "chunk", "done"]
|
||
assert events[1]["text"] == "先吐"
|
||
assert events[-1]["provider"] == "gemini"
|
||
|
||
|
||
def test_run_qa_stream_all_providers_fail():
|
||
qa = _orchestrator([
|
||
_FakeAdapter("gemini", chunks=[]),
|
||
_FakeAdapter("nvidia", chunks=[], raises=True),
|
||
])
|
||
events = list(qa.run_qa_stream("hi"))
|
||
assert events[-1]["type"] == "all_failed"
|
||
assert "provider_failed" in [e["type"] for e in events]
|
||
|
||
|
||
def test_run_qa_stream_exception_treated_as_no_output():
|
||
qa = _orchestrator([_FakeAdapter("gemini", raises=True), _FakeAdapter("nvidia", chunks=["ok"])])
|
||
events = list(qa.run_qa_stream("hi"))
|
||
assert events[0] == {"type": "provider_trying", "provider": "gemini"}
|
||
assert events[1] == {"type": "provider_failed", "provider": "gemini"}
|
||
assert events[-1]["provider"] == "nvidia"
|