Files
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

301 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` |
### 常用命令
```bash
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` 链路:
```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/<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 点)
```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` 已禁止写入人工确认队列。