# 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//{cleaned.txt,draft.md,report.json}` | | **评测实验室** | `/home/claw/claude-workspace/OBSIDIAN/asr-lab/`(12 篇 fixture + 11 篇人工定稿 + `REPORT.md`) | | fixture | `asr-lab/fixtures//{raw.txt,final.md,draft_baseline.md}` | | 转写落地(触发点) | `~/obsidian-vault/.asr-inbox/.txt`(gitignored) | | 转写持久归档 | `OBSIDIAN/recordings/transcripts/-.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` | ### 常用命令 ```bash P=~/claude-workspace/OBSIDIAN/asr-pipeline/bin/asr-refine $P doctor # 路径/凭证/词库/Hindsight/两个模型 全检 $P dry-run # 全流程,不写 vault、不 commit $P run-all # 处理 .asr-inbox 中所有未处理转写 $P lexicon stats|sync|show|learn <别名> <规范名> --note "…" $P build-termbank # 重建术语库(联网) ``` 回退到旧 `claude -p` 链路: ```bash 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/-录音.txt(原子 mv) ④ path unit → obsidian-asr-refine-native.service → asr-refine run-all ⑤ 五阶段(见下)→ state/drafts/.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 点) ```sql 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` 已禁止写入人工确认队列。