Files
infra-docker-configs/servers/tools-pve/asr-pipeline/HANDOFF.md
T
Codex 3c70861463
Build image / build (push) Canceled after 0s
Build ASR Pipeline / image (push) Canceled after 0s
feat(asr-pipeline): merge PR #1 webauth/CSRF/locking hardening and web_password fallback
2026-11-02 06:37:33 -08:00

18 KiB
Raw Blame History

ASR 后处理管线 — 项目方案与交接状态

最后更新:2026-07-26(会话结束时移交)


0. 一句话现状

独立 Python 程序已上线并接管 systemd,召回问题已定位并大幅修复(1/13 → 12/13), 结构归拢已生效;但 Kai 指出的更根本的 6 项设计缺陷(S2 职责过载、背景重复、 数字处理、音通道过窄、缺关系图谱、纪要目标定位)只完成方案设计,未实现。 下一个接手的模型应从「第 4 节 待实施方案」开始。


1. 项目路径

用途 路径
生产程序(主) /home/claw/claude-workspace/OBSIDIAN/asr-pipeline/
入口 asr-pipeline/bin/asr-refine(自带 .venv,依赖仅 pypinyin / rapidfuzz / numpy)
代码包 asr-pipeline/asrpipe/*.py
词库(SQLite) asr-pipeline/state/lexicon.db
术语库 asr-pipeline/termbank/terms.json(27.7k) + allowlist.txt(9.5k)
缓存(可删) asr-pipeline/cache/
dry-run 产物 asr-pipeline/dry-run/<slug>/{cleaned.txt,draft.md,report.json}
评测实验室 /home/claw/claude-workspace/OBSIDIAN/asr-lab/(12 篇 fixture + 11 篇人工定稿 + REPORT.md)
fixture asr-lab/fixtures/<name>/{raw.txt,final.md,draft_baseline.md}
转写落地(触发点) ~/obsidian-vault/.asr-inbox/<slug>.txt(gitignored)
转写持久归档 OBSIDIAN/recordings/transcripts/<fid>-<datetime>.txt
私有粗稿 asr-pipeline/state/drafts/(不进 Vault/Git)
正式输出 Web 全部确认后直接写 ~/obsidian-vault/minutes/
术语表(只读) ~/obsidian-vault/minutes/_glossary.md
人物/公司卡(只读) ~/obsidian-vault/People/*.md、Companies/*.md
日志 OBSIDIAN/logs/asr-pipeline.log
凭证(600) OBSIDIAN/bin/.asr-refine.env(ANTHROPIC_AUTH_TOKEN= ZenMux key)
systemd 触发 ~/.config/systemd/user/obsidian-asr-refine.path → obsidian-asr-refine-native.service
旧链路(保留可回退) OBSIDIAN/bin/obsidian-asr-refine.sh + obsidian-asr-refine.service
skill 文档 ~/.claude/skills/memo-inbox/references/asr-refine-pipeline.md

常用命令

P=~/claude-workspace/OBSIDIAN/asr-pipeline/bin/asr-refine
$P doctor                 # 路径/凭证/词库/Hindsight/两个模型 全检
$P dry-run <file.txt>     # 全流程,不写 vault、不 commit
$P run-all                # 处理 .asr-inbox 中所有未处理转写
$P lexicon stats|sync|show|learn <别名> <规范名> --note "…"
$P build-termbank         # 重建术语库(联网)

回退到旧 claude -p 链路:

sed -i 's/^Unit=.*/Unit=obsidian-asr-refine.service/' ~/.config/systemd/user/obsidian-asr-refine.path
systemctl --user daemon-reload && systemctl --user restart obsidian-asr-refine.path

2. 当前架构(已实现)

①  音频 → recordings/inbox/           obsidian-recording-watch.service
②  meeting-watch.py → asr-transcribe.sh   MEATING_ASR_BACKEND=qwen3cam
      = :10099 Qwen3-ASR-1.7B(vLLM) + FSMN-VAD + ForcedAligner-0.6B + CAM++ + 声纹(11人)
      输出 【谭凯】…(声纹已命名,身份可信)/【speaker_N】…(未命名)
      ※ FunASR 不是独立后端,只是该服务内部 VAD/hotword 组件
③  桥接为 ~/obsidian-vault/.asr-inbox/<date>-<HHMMSS>录音.txt(原子 mv)
④  path unit → obsidian-asr-refine-native.service → asr-refine run-all
⑤  五阶段(见下)→ state/drafts/<slug>.md
⑥  git commit+push → 更新 _待确认.md → Telegram 通知(hermes-gateway :8644 签名 webhook)
⑦  Kai 在一次性 Web 页复核 → Sonnet 应用确认 → 直接发布正式 minutes/

五阶段

阶段 模块 内容 成本
S0 segment.py 退化噪声折叠 + 说话人感知分片(1800/200 overlap) 免费
S1c refine.keyword_recall LLM 抽专有名词 → Hindsight recall 1 次便宜调用
S1 flagger.py 候选生成:音近 + 别名 + 变体 + 关系支持 免费(4~13s)
S2 refine.clean_chunks 去口语化 + 候选裁决(并发 6) N 次便宜调用
S3 fidelity.py 确定性保真闸门 → 失败则严格重试 → 再失败保留原文 免费
S4 refine.write_draft 一次强模型调用,结构化 + 出处标注 1 次调用

模型(实测选型,勿随意改):

  • 清洗 google/gemini-3.5-flash-lite。对比 claude-haiku-4.5:闸门失败 19/87 vs 78/87, haiku 有 69 次是过度压缩(当摘要器,删掉 AUTOIMMUNE/ADCS/CD),且慢 2×、贵 2.9×。
  • 成稿 anthropic/claude-sonnet-5。
  • 端点 https://zenmux.ai/api/v1/chat/completions;并发 12 触发 429、6 稳定; temperature 被 sonnet-5/opus-5 拒(客户端已自愈并记忆);HTTP 402 是瞬时的(按 max_tokens 预留额度)。

3. 本轮已完成(可直接使用)

3.1 诊断结论(重要,别重复走弯路)

Kai 反馈"纠错太次",实测复现 2026-07-21-160435录音:

8 个规范实体全部 in_play=True,池子里也都有,13 个口语面形只有 1 个产出候选

不是召回/推断能力问题,是匹配器把答案扔了。 三条硬规则造成:

  1. if span == cand.form: return — 精确别名被当"已正确"丢弃。万亚坤 卡片写着 aliases:["老万"],迈威生物 写着 ["迈威"],全被吃掉。
  2. abs(len(span)-len(cand.form)) > 1 — 简称永远匹配不上全名(麦威↔迈威生物、洛奇↔洛启生物、保山↔宝山基金)。
  3. 只做等长整串 seq_score,无子串/称谓形态(光辉↔李光辉、张泰海↔张成海)。

另有第 4 条:in_play 里的名字若池子里没有 Cand(宝山基金无公司卡,仅靠纪要文件名进 in_play)则永不可匹配;且 in_play 被文件名/日期污染 25/70。

3.2 已实现的修复

lexicon.py(新) — SQLite 词库。设计铁律:Obsidian 只读,不回写 _glossary.md、不改卡片; 学到的别名/纠错单独沉淀(类比声纹库)。选 SQLite 而非 Hindsight 的理由:别名解析要确定性精确查表, Hindsight 是 LLM 抽取 + 语义召回(1.4s/次,概率性),两者互补 —— lexicon 定身份,Hindsight 供语境。

entity(canonical, kind, company, role, related, card)   251 条(95 有归属,70 有关系)
alias(alias, canonical, kind, origin, confirmed, hits, note)  210 条(82 卡片 + 128 学习/术语表)
correction(surface, canonical, n, confirmed, note)      158 条
meta(vault_sig, synced_at)
  • 从卡片 frontmatter 拉 aliases/company/role/related_people
  • 导入 _glossary.md 153 条,保留一对多语境映射(许博→许俊彦/许恒皓、徐总→徐劲前/邹洵)
  • 已录入 Kai 本轮 12 条确认(老尚→单永强、麦威→迈威生物、张泰海→张成海 等)
  • 坑已修:_fm 用 [ \t]* 而非 \s*(\s 吃换行,空 role: 会把下一行 status: active 当值)

flagger.py 重构 — 变体展开 + 多通道匹配:

  • variants():机构简称(org_head)、去姓称名、称谓(老X/小X/X总/X博/X哥/X姐/X老师/X董,博=博士 有 glossary 三例佐证)
  • 每条候选带 rule(推导方式)+ owner(任职)+ support(X(本次在谈) / X(近期相关))
  • RULE_MIN 分规则阈值;anchor 结构锁定:称谓字必须字面相同,只有名字音节可漂 (否则 0.82 阈值等于"一个音节对上就行",产生 36 条垃圾:想谈→谭凯、理解→李光辉、就动→刘冀)
  • one-off:等长、n-1 字相同、首字必须相同、且只对 canonical(不对别名 —— 否则 林老板 会让 我老板/跟老板/那老板 全指向邹洵,18 条垃圾)
  • consistency:文档内一致性传播,目标已确立后其他写法放宽到 0.80,但 <0.90 时首字必须相同
  • in_play 实体化(补 Cand)+ 去文件名/日期污染 + 去重(迈威 既是别名又被材料化 → 两个竞争候选)
  • 一对多别名全部加入(原来只取 targets[0],把 徐总→邹洵 丢了)
  • 平局时优先保留 alias 出处(万金→万津 两条都 1.0,丢了 alias 就降级成猜测)
  • settled 判定 + render_for_prompt A/B 分组:A=已确认唯一映射(必须替换),B=待判断
  • 性能:rapidfuzz 预筛一致性通道,最慢 24.5s → 12.9s

prompts.py:JUDGE_RULES 说明各 rule 含义与证据强度;DRAFT_PROMPT 加按议题归拢要求 (结论→支撑事实→待办,单节 ≤7 bullet,禁止平铺)+ 反过度回避条款(能确定就写全名,别写"身份不确定")。

refine.py:S1c 移到 S1 之前,keyword_recall 的实体名回灌 extra_in_play (原来只进背景文本,从不进候选空间 —— 无说话人标签的转写因此几乎没候选)。

cli.py:新增 lexicon sync|stats|learn|show;doctor 增加 lexicon 检查。

3.3 效果实测

指标 修复前 修复后
07-21 目标面形召回 1/13 12/13(仅 趟鬼 未召回,Kai 本人也未给答案)
flags 总数 / 密度 10 34(12 篇 fixture 合计 400,10.5/万字)
S1 耗时(最慢) 4.7s 12.9s
草稿结构 平铺 bullet 5 议题节,结论→事实→待办
保真 — num/latin/term = 1.00,gate_fail 0

老万 现在带交叉证据:万亚坤(人名·已确认别名,洛启生物(本次在谈)、实控人)。


4. 待实施方案(Kai 2026-07-26 给出的 8 点,核心工作在这里)

4.1 已量化的三条症状

Kai 判断 实测证据
去口语化 input 太复杂,flash-lite 兜不住 S2 单片提示词 4380 字,正文仅占 37%;候选块 1566 字(36%)比规则本身大 3 倍;指令:正文 = 1.7:1
召回事实重复 context 7 条长行中 3 对高重叠,一对 Jaccard=1.00;2081 字先砍到 678 字,砍之前没去重,预算全买重复
数字处理 07-21 篇中文数字串 15 个:两千/万十/十两/两两/一十亿;另一篇 91 个。不只中→阿拉伯,还有口吃混入数字和量级歧义("两千"=2000 还是 2千万,草稿自己在问)

4.2 【P1】S2 拆成两步(Kai 第 8 点)— 最高优先

现在 S2 一次干四件事:去口语 + 候选裁决 + 消化背景 + 满足闸门。这是一个已复现 bug 的根因: 严格重试提示词写「专业术语必须原样出现」,把已确认的实体替换又还原了。 证据:单片隔离测试 A 组替换成功(张泰海→张成海、老尚→单永强),但完整 run(retried=2) cleaned.txt 里 张泰海/老尚/老善/麦威/金山 全部未替换。

S2a 去口语化      仅 CLEAN_RULES + 正文(无候选、无背景)→ 保真闸门只管这一步
S2b 实体/数字归一  A 组由代码确定性替换;B 组一次专注 LLM 裁决(带关系事实,允许弃权)
  • 闸门只校验 S2a。S2b 有意改写 token,用闸门校它本身矛盾 → 改成白名单校验: S2b 输出与输入的差异必须能被映射表解释,否则拒绝(比闸门更严且不冲突)。
  • flash-lite 吐 token 快,两次调用不影响总时延(Kai 明确认可)。

4.3 【P1】A 组代码化确定性替换

settled 的映射不应依赖 LLM 服从指令。边界(必须实现,否则是净负):

  • 仅当目标实体本次 in_play 时才替换,否则降级 B 组 (金山→宝山基金 是特定语境确认,全局替换危险:金山也可能是地名/金山办公)
  • 排除 common_word(光辉/公司 这类)
  • 避免嵌套误替(白帆→白帆生物 不能产生 白帆生物生物,用负向 lookahead)

4.4 【P1】背景去重压缩(Kai 第 7 点)

  • 去重放在截断之前(Jaccard > 0.45 合并同义句)
  • 散文式记忆压成紧凑表,例: 刘冀|张科领弋 基金投资人|活跃|最近 2026-06-26|连接 基金/LP/白帆CDMO/桂林三金

4.5 【P2】关系图谱 + 紧密度衰减(Kai 第 3、4 点)

relation(a, b, kind, weight, first_seen, last_seen, n)
  kind: 任职 | 同事 | 项目参与 | 共同出现 | 出资

来源优先级:卡片 company/related_people → 纪要 people/companies 共现(带日期) → Hindsight 实体 → (可选)LLM 抽 typed 关系。建议先做确定性来源,评估够不够再上 LLM。

in_play 从「90 天硬截断 + 计数」改为指数衰减 w = Σ exp(-Δdays/λ),λ≈120 天, 直接进候选排序,替掉现在粗糙的 boost=0/1。

4.6 【P2】音通道放宽 + 字通道独立(Kai 第 1 点)

关键认知修正:张泰海→张成海 中 tai/cheng 音距很远,不是靠音捞回来的, 是靠"等长 + 同首字 + 只差一字"的字形锚定。所以要两条独立通道并行取较高者, 而不是把一切压进一个音近分数:

  • 音通道放宽:现有折叠 zh/z、ch/c、sh/s、n/l、f/h、ng、en/eng、in/ing。 需补:送气丢失(b/p、d/t、g/k)、尖团(j/z、q/c、x/s)、r/l、u/ü、ai/ei、ao/ou; 声调默认不计权(口音下不可靠)。注意 char→pinyin 回还本身会引入误差。
  • 字通道:首字锚定 + 尾字锚定 + n-1 字相同,任意长度(现在只做 ≥3 字且首字相同)。
  • 建议用加权评分替掉 6 个互相独立的 RULE_MIN 阈值(那套是打补丁堆出来的,规则一多互相漏), 结构性 gate(称谓字字面相同)保留。

4.7 【P2】数字归一(Kai 第 5 点)

  • 无歧义中文数字 → 阿拉伯(八千万→8000万、二零二一→2021)
  • 清理口吃残留(一一百八、两五千、万十、两两)
  • 量级歧义不猜:写成 两千[?万/千万] 并进「待你确认」
  • 闸门必须认识中文数字,否则转换会被判成数字丢失

4.8 【P1】纪要目标重新定位(Kai 第 6 点)

要改掉我此前的默认倾向。DRAFT_PROMPT 以梳理关键内容与线索为目标,明确写: 转写原文已保留在 transcript:,遗漏可事后按 Kai 指示补救;宁可写"此处录音模糊、金额量级不明", 不要为凑完整而编。闸门继续留在 S2a(防的是清洗阶段静默删信息,是另一回事), 但成稿阶段不再以逐字对齐为目标。

Kai 原话要点:「哪怕人类回听也有很大概率不能完全还原,所以撰写主要是总结梳理关键内容和线索, 不要追求逐字还原」。

4.9 【P0】必须承认的边界(不要再花时间攻)

  • 徐总:三个候选(徐劲前/邹洵/许俊彦)全与桂林三金相关,结构性歧义, 正确输出是 徐总(疑似 [[徐劲前]] 或 [[邹洵]]),不是硬选。
  • 趟鬼:零信息。
  • "张泰海、张博" 是同一人说两次还是两个人 —— 音频不可恢复。
  • Kai 明确说过:「有些模糊问题,你是永远无法解决的」。目标是 不含糊的直接落定 + 含糊的带证据出候选集,不是让管线替他决定。

5. 建议实施顺序

  1. 第一段(当天可回归):4.2 S2 拆分 + 4.3 A 组代码化 + 4.4 背景去重 + 4.8 成稿定位 —— 这四条直接解释了 Kai 看到的全部症状,改动最小、收益最确定。
  2. 第二段:4.7 数字归一(含闸门认中文数字)。
  3. 第三段:4.5 关系图谱 + 衰减权重(工程量最大)、4.6 音/字双通道重构。

回归验证要求(不可省)

  • asr-lab/fixtures 12 篇 + 11 篇人工定稿卡回归。
  • 过度纠错率必须保持 0(历史横评实测为 0),否则确定性替换是净负。
  • 保真三项(num/latin/term recall)在 S2a 上保持 1.00。
  • 07-21 录音的 13 个面形召回不低于 12/13。
  • 命令:dry-run 看 cleaned.txt 与 draft.md 的差异,判断问题在 S2 还是 S4。

6. 未提交 / 遗留

  • asr-pipeline/ 与 asr-lab/ 所在的 claude-workspace 不是 git 仓库,无版本控制,改动前建议手工备份。
  • state/lexicon.db 含 Kai 12 条人工确认,删库会丢(lexicon sync 只能重建卡片+术语表部分, origin='learned' 的丢失后无法自动恢复)。备份它。
  • dry-run/ 下有 3 份历史产物,可删。
  • skill 文档 asr-refine-pipeline.md 已更新到「独立程序 + qwen3cam + S0–S4」, 但尚未反映本轮 lexicon 与 S2 拆分方案,实施后需同步。
  • 已知未修 bug:见 4.2(严格重试还原已确认替换)。

2026-07-26 Codex 接手更新

当前实现已开始迁移到 v2 分阶段架构:纯去口语化与实体/数字规范化分离,确认映射由程序 确定性执行,Hindsight recall 在进入 prompt 前按实体压缩去重,并增加可复用的 SQLite 关系、互动、观察、模糊音和人工确认队列表。详细设计见 docs/architecture-v2.md。

四项核心能力已经接入生产判断链:历史纪要增量关系图、45 天半衰期的近期/频率权重、 由确认纠错学习的定向音节混淆,以及带类型/置信度/原值记录的确定性数字规范化。个人 模糊音只对当前关系语境中的实体生效,不影响全局术语库。

移动交付层已加入:每份生产草稿(不论实体候选是否触发)都会把 ## 待你确认 转成 逐项 Web 问题。顺序为通话背景确认(可填正确背景请求重跑草稿)、缺失声纹注册、内容 不确定项、草稿整体审核;通过既有 Hermes 签名 webhook 把一次性页面地址发到 Telegram。 页面可展开完整私有粗稿。全部回答后将审计记录留在 state,由独立 oneshot 调用 Sonnet, 通过正式 schema/Summary/来源校验后只把正式稿写到 minutes/ 并 push。确认的匿名 speaker 会立即写入 frontmatter 并调用 10099 声纹注册,失败则留给每日任务重试。 SQLite 以不同正式会议为单位统计实体;未建卡人物/公司达到 2 场时在 Web 提议建保守 存根,只有 Kai 同意才创建。页面完成后 10 秒退出,打开后空闲 45 分钟退出,最长运行 2 小时;服务不随开机常驻。dry-run 已禁止写入人工确认队列。