feat(asr-pipeline): merge PR #1 webauth/CSRF/locking hardening and web_password fallback
Build image / build (push) Canceled after 0s
Build ASR Pipeline / image (push) Canceled after 0s

This commit is contained in:
Codex
2026-11-02 06:37:33 -08:00
parent 9debd1c47d
commit 3c70861463
44 changed files with 4354 additions and 333 deletions
+300
View File
@@ -0,0 +1,300 @@
# 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` 已禁止写入人工确认队列。