助手本体:会话引擎、LLM 路由、记忆、身份、语音适配
从 Obsidian 笔记库剥离为独立仓库。规划文档与 P0 报告留在笔记库, 代码(助手本体、TTS 服务、P0 基准脚本、ESP32 固件)在此。 架构:服务器是助手,本地是客户端 - 助手本体跑在 cadadmin-server,N5095 只做 USB 外设桥与网络桥接 - 起因:实测 N5095 无 AVX2,跑不动可用的语音模型 当前技术栈 - LLM:DeepSeek deepseek-flash(云端,必须关思考) - TTS:Qwen3-TTS 1.7B(服务器 3090,独立 HTTP 服务) - STT:Zipformer 中英双语 int8(CPU,中文识别率未测) 实测:端到端 16/16 通过,首音延迟 1545ms (对比:本地 LLM + vits-zh-ll 是 179ms,但音色不满意——音质与延迟 在此硬件上此消彼长) 设计要点 - MemoryStore 接口按"可能远程"设计(全 async、可超时、可失败) - user_id / session_id / track_id 三者分开,人脸只作候选不自动切身份 - 记忆查询强制带 owner_user_id,无按 id 单独取的口子 - 自动抽取的记忆是"候选",默认不参与检索 - 异步写记忆绑定创建时的身份,不读"当前用户" - 引擎与传输解耦,只用文件音频即可端到端测试 模型与音频产物不进版本管理(1.1GB),由 download_models.sh 恢复
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""身份层测试。
|
||||
|
||||
最关键的一条:**人脸认出来不等于切身份**。
|
||||
方案 §6 写得很直白:"看到熟人不等于确认他正在说话"。
|
||||
做错的后果是——路人从摄像头前走过,助手就把你的私人记忆念了出来。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
|
||||
|
||||
from app.identity import ( # noqa: E402
|
||||
GUEST_USER_ID, IdentityClaim, ManualIdentityProvider,
|
||||
)
|
||||
|
||||
OK, FAIL = [], []
|
||||
|
||||
|
||||
def check(name: str, cond: bool, detail: str = "") -> None:
|
||||
(OK if cond else FAIL).append(name)
|
||||
print(f" [{'PASS' if cond else 'FAIL'}] {name}" + (f" — {detail}" if detail else ""))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=== 1. 初始状态 ===")
|
||||
p = ManualIdentityProvider()
|
||||
d = p.current()
|
||||
check("默认是访客", d.user_id == GUEST_USER_ID and d.is_guest, d.user_id)
|
||||
check("访客信任等级独立", d.trust_level == "guest")
|
||||
|
||||
print("\n=== 2. 显式切换(首版唯一可靠入口)===")
|
||||
d = p.switch("u_ylhan", reason="对话确认")
|
||||
check("切换生效", d.user_id == "u_ylhan" and d.confirmed)
|
||||
check("信任等级为 normal", d.trust_level == "normal")
|
||||
check("版本号 +1", p.version == 1, str(p.version))
|
||||
v = p.version
|
||||
p.switch("u_ylhan") # 切到同一个人
|
||||
check("切到同一人不增版本号", p.version == v, f"{v} → {p.version}")
|
||||
|
||||
print("\n=== 3. 人脸候选不得自动切换(关键)===")
|
||||
p2 = ManualIdentityProvider("u_ylhan")
|
||||
before = p2.current().user_id
|
||||
d = p2.resolve([IdentityClaim(source="face", candidate_user_id="u_other",
|
||||
confidence=0.95, track_id="track_7")])
|
||||
check("身份未被自动切换", d.user_id == before, f"{before} → {d.user_id}")
|
||||
check("但仍保留建议", any(s.candidate_user_id == "u_other" for s in d.suggestions),
|
||||
d.reason)
|
||||
check("track_id 未污染 user_id", d.user_id != "track_7")
|
||||
|
||||
# 访客状态下也不应变
|
||||
p3 = ManualIdentityProvider()
|
||||
d3 = p3.resolve([IdentityClaim(source="face", candidate_user_id="u_ylhan",
|
||||
confidence=0.99)])
|
||||
check("访客状态也不自动切换", d3.user_id == GUEST_USER_ID and d3.is_guest, d3.reason)
|
||||
|
||||
print("\n=== 4. 低置信度连建议都不给 ===")
|
||||
p4 = ManualIdentityProvider()
|
||||
d4 = p4.resolve([IdentityClaim(source="face", candidate_user_id="u_x", confidence=0.3)])
|
||||
check("低置信度无建议", len(d4.suggestions) == 0, f"{len(d4.suggestions)} 条")
|
||||
|
||||
print("\n=== 5. 权威来源可以切换 ===")
|
||||
p5 = ManualIdentityProvider()
|
||||
d5 = p5.resolve([IdentityClaim(source="client_login", candidate_user_id="u_phone",
|
||||
confidence=1.0)])
|
||||
check("客户端登录态可切身份", d5.user_id == "u_phone" and d5.confirmed, d5.reason)
|
||||
|
||||
d5b = p5.resolve([IdentityClaim(source="manual", candidate_user_id="u_ylhan")])
|
||||
check("显式来源可切身份", d5b.user_id == "u_ylhan")
|
||||
|
||||
print("\n=== 6. 权威线索互相矛盾时不切 ===")
|
||||
p6 = ManualIdentityProvider("u_ylhan")
|
||||
d6 = p6.resolve([
|
||||
IdentityClaim(source="manual", candidate_user_id="u_a", confidence=1.0),
|
||||
IdentityClaim(source="client_login", candidate_user_id="u_b", confidence=1.0),
|
||||
])
|
||||
check("矛盾时保持原身份", d6.user_id == "u_ylhan", d6.reason)
|
||||
|
||||
print("\n=== 7. reset 回访客 ===")
|
||||
p7 = ManualIdentityProvider("u_ylhan")
|
||||
v7 = p7.version
|
||||
d7 = p7.reset()
|
||||
check("回到访客", d7.user_id == GUEST_USER_ID and d7.is_guest)
|
||||
check("版本号 +1(上层据此丢弃旧上下文)", p7.version == v7 + 1,
|
||||
f"{v7} → {p7.version}")
|
||||
|
||||
print("\n=== 8. 空 user_id 应报错而非静默接受 ===")
|
||||
try:
|
||||
p7.switch("")
|
||||
check("拒绝空 user_id", False, "没报错")
|
||||
except ValueError:
|
||||
check("拒绝空 user_id", True)
|
||||
|
||||
print("\n" + "=" * 62)
|
||||
print(f"通过 {len(OK)} / {len(OK) + len(FAIL)}")
|
||||
for n in FAIL:
|
||||
print(f" 未通过: {n}")
|
||||
return 0 if not FAIL else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user