18 KiB
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 个产出候选
不是召回/推断能力问题,是匹配器把答案扔了。 三条硬规则造成:
if span == cand.form: return— 精确别名被当"已正确"丢弃。万亚坤卡片写着aliases:["老万"],迈威生物写着["迈威"],全被吃掉。abs(len(span)-len(cand.form)) > 1— 简称永远匹配不上全名(麦威↔迈威生物、洛奇↔洛启生物、保山↔宝山基金)。- 只做等长整串
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.md153 条,保留一对多语境映射(许博→许俊彦/许恒皓、徐总→徐劲前/邹洵) - 已录入 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_promptA/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. 建议实施顺序
- 第一段(当天可回归):4.2 S2 拆分 + 4.3 A 组代码化 + 4.4 背景去重 + 4.8 成稿定位 —— 这四条直接解释了 Kai 看到的全部症状,改动最小、收益最确定。
- 第二段:4.7 数字归一(含闸门认中文数字)。
- 第三段:4.5 关系图谱 + 衰减权重(工程量最大)、4.6 音/字双通道重构。
回归验证要求(不可省)
asr-lab/fixtures12 篇 + 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 已禁止写入人工确认队列。