从钉钉录音到可回听的结构化逐字稿:我的两个 Command 本地工作流
文章目录
从钉钉录音到可回听的结构化逐字稿:我的两个 Command 本地工作流
我经常用钉钉录音记录会议、培训和访谈。录音越来越多以后,我遇到了两个非常具体的问题:一是每次都要进入应用手动下载;二是即使把音频下载到了电脑里,我也很少愿意重新听完一两个小时的录音。
后来我把整个过程改造成了一条本地工作流:
QwenNote/钉钉录音 → 一键同步到本地 → 无损整理结构化逐字稿 → 浏览器中按时间戳回听
日常使用时,我只需要双击两个文件:
双击同步QwenNote.command:把新录音、摘要和原始逐字稿同步到本地。双击打开逐字稿.command:打开本地音频逐字稿页面,点击任意时间戳即可回听。
两个 Command 中间还有一个关键环节:使用 lecture-notes-lossless 技能,把原始转写稿整理成忠于原文的结构化逐字稿。它不是摘要器,而是一套可以确定性验收的“轻清理+标题结构”流程。

隐私说明: 本文配图使用脱敏模拟界面。录音标题、正文、日期、时长、段落数和文件大小均为虚构;功能、操作流程和标准文件结构依据真实系统。
本文既是这套工作流的搭建说明,也是给我自己的维护笔记。文中的令牌、个人录音内容和内部地址都不会公开。
一、最后得到什么
每条录音对应一个一级子文件夹,标准结构如下:
records/
├── 双击同步QwenNote.command
├── 双击打开逐字稿.command
├── .qwennote-sync/
│ └── sync_qwennote.py
├── .transcript-app/
│ ├── server.py
│ ├── pyproject.toml
│ └── web/
│ ├── index.html
│ ├── app.js
│ └── style.css
└── 2026-09-24 某次培训/
├── 录音.mp3
├── 摘要.md
├── 逐字稿.md
└── 逐字稿_结构化.md
其中四个业务文件的分工是:
| 文件 | 用途 |
|---|---|
| 录音.mp3 | 原始录音,用于回听和核对 |
| 摘要.md | QwenNote 生成的摘要、元信息和待办 |
| 逐字稿.md | 带时间戳和发言人的原始转写稿 |
| 逐字稿_结构化.md | 轻清理、带章节结构、可供回听页面读取的版本 |
有些录音文件夹中还保留了 逐字稿_结构化_回放.html。它可以作为独立页面存档,但当前更推荐使用一套共享的 .transcript-app:程序会自动扫描所有合格的录音文件夹,无需给每条录音重复生成一套页面。
为避免公开内部录音信息,本文统一使用一条虚构录音展示界面:标题为“个人知识管理入门”,时长为 42 分 18 秒,页面载入 86 个时间戳段落,示例资料库包含 12 条录音。图中的正文、日期和文件大小也全部是演示数据,仅用于说明页面如何工作;实际系统会读取本地真实文件。
二、整体架构
这条流程可以拆成三个彼此独立的阶段:
阶段 A:同步
QwenNote MCP
├── 录音列表
├── AI 摘要与待办
├── 分页逐字稿
└── 音频下载地址
↓
本地标准三件套:摘要.md、逐字稿.md、录音.mp3
阶段 B:无损整理
逐字稿.md
→ prepare_transcript.py
→ outline.json(只规划标题)
→ build_notes.py
→ verify_notes.py
→ 逐字稿_结构化.md
阶段 C:联动回听
录音.mp3 + 逐字稿_结构化.md
→ 本地 Flask 服务
→ 浏览器中的目录、播放器、时间戳和可编辑正文
把三段拆开很重要。同步程序不负责“发挥”或改写正文;整理技能不负责下载;回听页面也不负责生成内容。每一段只处理一个明确问题,出错时比较容易定位。
三、准备环境
这套方案当前针对 macOS,使用 Python 3.11 以上版本,并用 uv 管理 Python 环境。
1. 安装 uv
如果电脑还没有安装 uv,按照 uv 官方方式安装。安装后确认:
uv --version
本文涉及的 Python 项目都遵循同一套约定:
uv sync
uv run script.py
不要把系统 Python 和项目依赖混在一起。
2. 准备资料库目录
例如:
mkdir -p "$HOME/Documents/records"
为了后续双击使用,建议把启动文件和两个隐藏程序目录都放在 records 根目录下。
如果是从零建立这两个 Python 项目,用 uv init 创建,而不是手工拼虚拟环境:
cd "$HOME/Documents/records"
uv init --bare .qwennote-sync
uv init --bare .transcript-app
cd .transcript-app
uv add flask
同步程序当前只使用 Python 标准库,不需要额外依赖;回听程序使用 Flask。现有项目拿到 pyproject.toml 和 uv.lock 后,只需执行 uv sync。
3. 准备 QwenNote 授权
同步脚本通过 QwenNote MCP 读取当前账号的录音数据。需要先完成 QwenNote 的正常授权,并在资料库根目录准备:
.qwennote_creds.json
文件包含以下字段:
{
"client_id": "<your-client-id>",
"access_token": "<your-access-token>",
"refresh_token": "<your-refresh-token>",
"expires_in": 0,
"token_obtained_at": 0
}
这里展示的只能是结构,绝对不要把自己的真实令牌复制到博客、截图或代码仓库。建议至少执行:
chmod 600 .qwennote_creds.json
并在 .gitignore 中加入:
.qwennote_creds.json
.sync_state.json
.sync.log
.sync_qwennote.lock/
.transcript-app/runtime/
.transcript-app/backups/
QwenNote 的授权方式或 MCP 接口可能更新。如果你要复刻这套方案,凭据应通过自己账号的正式授权流程获得,不能复制别人的令牌。
四、Command 1:一键同步 QwenNote 录音
1. 同步脚本做了什么
.qwennote-sync/sync_qwennote.py 的主流程是:
- 读取本地凭据;
- 初始化 QwenNote MCP;
- 访问令牌过期时,使用刷新令牌更新;
- 分页读取全部录音列表;
- 使用 UUID 判断哪些录音已经处理;
- 对新录音分别获取摘要、待办、分页逐字稿和音频地址;
- 按“日期+标题”创建文件夹;
- 统一保存为
摘要.md、逐字稿.md、录音.mp3; - 把处理状态写入
.sync_state.json。
当前使用的核心 MCP 工具包括:
list_my_minutes
get_minutes_ai_summary
list_minutes_todos
get_minutes_transcription
get_minutes_media_url
逐字稿按页拉取,每一段被保存为统一格式:
[00:06] 发言人 3: 行,那我们开始……
如果摘要中包含远程图片,同步脚本还会尝试把图片下载到 records/images/,再把 Markdown 中的链接改成本地相对路径。
2. 为什么要保存同步状态
.sync_state.json 保存 QwenNote 录音 UUID、标题和本地文件夹之间的对应关系。它解决了三个问题:
- 再次运行时不会重复下载已经处理的录音;
- 云端标题发生变化时,可以同步调整本地文件夹名;
- 某个标准文件缺失时,可以单独补齐,而不是重新下载全部内容。
同步程序不会把 逐字稿_结构化.md 当成云端原稿覆盖。结构化文件是后续整理产生的独立资产。
3. 双击启动文件
双击同步QwenNote.command 主要负责把脚本包装成普通用户可以双击的入口:
- 自动定位资料库目录;
- 自动查找
uv; - 用目录锁防止两个同步任务同时运行;
- 把终端输出追加到
.sync.log; - 成功后发送 macOS 通知;
- 失败时提示查看日志。
核心执行命令只有一行:
uv run .qwennote-sync/sync_qwennote.py
首次配置后,要保证启动文件可执行:
chmod +x "双击同步QwenNote.command"
之后日常使用只需双击。看到“本次新增 0 条”并不代表失败,它通常表示云端没有新录音。
五、用 lecture-notes-lossless 生成无损结构化逐字稿
这一阶段不是摘要,也不是让大模型重新写一遍。
我使用的 lecture-notes-lossless 技能把原始转写稿视为不可自由改写的数据源:正文只能由脚本逐段生成,模型只负责标题规划、带证据的疑难纠错,以及有来源标记的 PPT 独有补充。
1. 基本原则
- 不概括、不缩写、不重排正文;
- 保留原始顺序、第一人称、例子、互动和判断链;
- 只删除白名单填充词、精确口吃、重复标点、时间戳和技术元数据;
- ASR 纠错必须记录原词、新词和证据;
- 标题最多四层,任一父标题最多七个直接子标题;
- 构建完成后必须用脚本核对每一段正文。
这也是为什么我把它称为“无损结构化”,而不是“AI 总结”。
2. 准备技能环境
不同电脑上的技能安装目录可能不同,先把路径替换成自己的实际位置:
SKILL_DIR="/path/to/lecture-notes-lossless/1.0.2"
cd "$SKILL_DIR"
uv sync
3. 解析原始逐字稿
假设要处理的录音目录是:
RECORD_DIR="/absolute/path/to/records/2026-09-24 某次培训"
WORK_DIR="$RECORD_DIR/.lossless-work"
执行:
cd "$SKILL_DIR"
uv run scripts/prepare_transcript.py \
"$RECORD_DIR/逐字稿.md" \
--work-dir "$WORK_DIR"
脚本会生成:
.lossless-work/
├── segments.jsonl
├── canonical_body.md
├── operations.jsonl
└── manifest.json
其中 segments.jsonl 是逐段数据源,operations.jsonl 是每一次删除或替换的账本,manifest.json 记录段落数、首尾时间戳、字符数和正文哈希。
4. 有证据地纠正 ASR
如果发现明确的专有名词错误,不要直接修改中间文件,而是创建 corrections.json:
[
{
"segment_id": "S0018",
"old": "错误识别词",
"new": "正确术语",
"category": "asr",
"reason": "纠正专有名词",
"evidence": "PPT 第 12 页"
}
]
然后重新运行准备脚本:
uv run scripts/prepare_transcript.py \
"$RECORD_DIR/逐字稿.md" \
--work-dir "$WORK_DIR" \
--corrections "$WORK_DIR/corrections.json"
没有证据的疑难词宁可保留,也不要凭常识猜测。
5. 只规划标题
模型在这个阶段只生成 outline.json,不能重写正文:
{
"title": "某次培训",
"headings": [
{"before": "S0001", "level": 2, "title": "一、课程背景与目标"},
{"before": "S0012", "level": 3, "title": "1.1 现场提出的第一个问题"},
{"before": "S0040", "level": 2, "title": "二、具体案例与操作"}
]
}
标题只能插在段落边界,不能改变任何段落的顺序。第一条标题必须放在 S0001 前,标题层级只能使用 H2、H3 和 H4。
6. 构建并保留时间戳
为了让回听页面能够定位音频,我在构建时启用 --timestamps:
uv run scripts/build_notes.py \
--segments "$WORK_DIR/segments.jsonl" \
--outline "$WORK_DIR/outline.json" \
--output "$RECORD_DIR/逐字稿_结构化.md" \
--report "$WORK_DIR/build_report.json" \
--timestamps
结构化正文格式如下:
# 某次培训
## 一、课程背景与目标
### 1.1 现场提出的第一个问题
[00:06] 发言人 3:
行,那我们开始……
7. 强制验收
uv run scripts/verify_notes.py \
--segments "$WORK_DIR/segments.jsonl" \
--notes "$RECORD_DIR/逐字稿_结构化.md" \
--manifest "$WORK_DIR/manifest.json" \
--report "$WORK_DIR/verify_report.json" \
--timestamps
只有下面的条件全部成立,才算完成:
body_exact_match = True
原文段落覆盖 = N/N
unknown_body_additions = 0
unlogged_body_deletions = 0
标题最大深度 ≤ 4
每个父标题的直接子标题数 ≤ 7
如果失败,不要直接手工修改最终文件来绕过校验。应回到 corrections.json 或 outline.json 修正,再重新构建和验收。
六、Command 2:打开本地音频逐字稿页面
1. 页面如何发现录音
.transcript-app/server.py 会扫描 records 下的一级子文件夹。只有同时包含下面两个文件的文件夹才会出现在页面中:
录音.mp3
逐字稿_结构化.md
因此新增录音后无需修改网页代码,也不需要复制一份新的播放器程序。重新打开页面,它就会自动出现在录音下拉列表中。
2. 本地应用结构
这个小工具是一个非常轻量的 uv 项目:
[project]
name = "transcript-studio"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"flask>=3.1.3",
]
后端负责:
- 扫描录音目录;
- 解析 Markdown 标题、发言人和时间戳;
- 支持 MP3 的浏览器分段读取;
- 保存逐字稿修改;
- 修改前自动备份;
- 用 SHA-256 版本号检测外部修改冲突。
前端负责:
- 显示 H2/H3 目录;
- 搜索并切换录音;
- 点击段落或时间戳跳转音频;
- 播放时高亮当前段落并滚动跟随;
- 直接编辑段落;
- 停止输入约 1.2 秒后自动保存。
3. 为什么使用本地服务,而不是直接双击静态 HTML
浏览器直接打开 file:// 页面时,对本地音频读取、跨文件访问和安全保存会有较多限制。用本地服务可以统一处理这些问题,同时仍然保持数据只在自己的电脑中流动。
服务只监听:
127.0.0.1
它不会绑定局域网地址,也不会把录音上传到服务器。启动时随机选择一个可用端口,浏览器关闭且约 10 分钟没有活动后,服务会自动退出。
4. 双击启动文件做了什么
双击打开逐字稿.command 会:
- 检查
.transcript-app是否存在; - 如果虚拟环境不存在,自动执行
uv sync; - 检查已有服务是否属于当前资料库;
- 已有服务可用时直接复用;
- 否则选择随机端口并在后台启动 Flask;
- 健康检查通过后自动打开浏览器;
- 关闭这次启动产生的 Terminal 标签页。
首次使用同样要设置执行权限:
chmod +x "双击打开逐字稿.command"
七、日常使用流程
搭建完成以后,我平时只做下面几步:
第一步:同步
在 Finder 中双击:
双击同步QwenNote.command
等待看到“同步完成”。新录音会以“日期+标题”自动建立文件夹。
第二步:无损整理
让 Codex 对新文件夹中的 逐字稿.md 使用 lecture-notes-lossless,要求:
使用 lecture-notes-lossless 整理这份逐字稿,保留时间戳,
生成逐字稿_结构化.md,并运行强制无损验收。
整理结束后,应查看验收报告,而不是只看文章是否“像一篇好看的笔记”。
第三步:回听和校对
双击:
双击打开逐字稿.command
在页面中选择录音,点击目录或时间戳开始回听。发现 ASR 错误时可以直接修改,页面会在保存前创建旧版本备份。
八、常见问题
1. 双击 .command 没有反应
先检查执行权限:
chmod +x "双击同步QwenNote.command"
chmod +x "双击打开逐字稿.command"
2. 同步失败
查看:
.sync.log
常见原因包括授权失效、网络异常、接口更新和凭据文件缺失。脚本能够自动刷新过期的 access token,但 refresh token 本身失效时,需要重新完成账号授权。
3. 显示“本次新增 0 条”
这通常是正常结果,表示云端列表读取成功,但没有尚未处理的新录音。
4. 页面中找不到某条录音
检查对应一级文件夹中是否同时存在:
录音.mp3
逐字稿_结构化.md
文件名必须完全一致。
5. 回听页面启动失败
查看:
.transcript-app/runtime/server.log
如果虚拟环境损坏,可进入 .transcript-app 后重新同步:
uv sync
6. 页面提示磁盘内容已经变化
这是版本冲突保护生效了。说明浏览器打开页面后,另一个程序又修改了同一份 Markdown。先载入磁盘上的最新版本,再继续编辑,不要强行覆盖。
7. 如何恢复误改内容
每次从页面保存前,旧版本都会按录音文件夹分类保存在:
.transcript-app/backups/
九、隐私与安全边界
录音资料往往包含会议、个人谈话、工作安排甚至敏感信息,所以这套方案把本地优先作为硬约束:
- 回听服务只监听
127.0.0.1; - 不把 MP3 和逐字稿上传到额外的第三方服务;
- 令牌文件不进入 Git;
- 对外截图优先选择公开培训,不展示内部谈话标题;
- 发布博客前再次检查图片、日志和代码块中是否出现令牌;
- 如果需要远程访问,不要直接把 Flask 开发服务暴露到公网。
还要注意:QwenNote 同步本身需要访问 QwenNote 的云端接口,这是录音从云端进入本地所必需的网络行为;“本地优先”主要指同步完成后的整理、播放和编辑环节。
十、给 Codex 的复刻清单
如果要让 Codex 在另一台 Mac 上重建这套工作流,可以直接把下面这段作为任务要求:
请在 records 目录中搭建一套本地音频逐字稿资料库:
1. 使用 uv 管理所有 Python 环境。
2. 创建 .qwennote-sync/sync_qwennote.py,通过我已授权的 QwenNote MCP:
- 分页获取全部录音;
- 下载摘要、待办、分页逐字稿和 MP3;
- 按“日期+标题”建立一级文件夹;
- 标准化为 摘要.md、逐字稿.md、录音.mp3;
- 使用状态文件防止重复下载;
- 不覆盖 逐字稿_结构化.md;
- 不在代码、日志或回复中暴露令牌。
3. 创建可双击的 双击同步QwenNote.command:
- 自动定位目录和 uv;
- 防止并发同步;
- 写入 .sync.log;
- 成功后显示通知。
4. 使用 lecture-notes-lossless 把逐字稿.md 构建成带时间戳的
逐字稿_结构化.md,并通过 verify_notes.py 的强制验收。
5. 创建 .transcript-app uv 项目和 双击打开逐字稿.command:
- 只监听 127.0.0.1;
- 自动扫描同时包含 录音.mp3 与 逐字稿_结构化.md 的一级子目录;
- 支持录音搜索与切换;
- 点击时间戳跳转;
- 播放时高亮并跟随;
- 文本编辑后自动保存;
- 保存前备份;
- 使用内容哈希防止外部修改被静默覆盖;
- 10 分钟无活动后自动退出。
6. 完成后实际双击两个 Command,并用一条非敏感录音验证完整流程。
十一、这套工作流真正解决了什么
我以前把“已经录音”误认为“已经保存了信息”,把“已经下载”误认为“以后会重新利用”。实际上,只要回听的启动成本仍然很高,录音最终还是会成为数字仓库里的沉没资料。
这套工作流没有试图用一段短摘要替代完整录音,而是同时保留了三种不同精度的入口:
- 想快速了解时看
摘要.md; - 想系统阅读时看
逐字稿_结构化.md; - 想核对语气和上下文时,点击时间戳回到原始音频。
对我来说,真正有价值的不是“AI 帮我总结了一次”,而是录音终于从一个很难重新打开的文件,变成了可搜索、可阅读、可定位、可校对、可持续积累的个人资料库。