mcpField
W
whosaid

whosaid

本地服务
github.com/sblattj媒体与设计7今天更新

关于 whosaid

whosaid 是面向 Apple Silicon Mac 的离线转录与会议搜索命令行工具。它结合语音识别、说话人标注和本地声纹识别,并可通过 MCP 向客户端提供转录与搜索功能。

主要功能

  1. 01离线转录与说话人分离
  2. 02声纹登记与持久说话人命名
  3. 03本地转录索引与搜索
  4. 04说话人卡片与会议摘要
  5. 05监视文件夹并自动转录

适用场景

  1. 01为会议转录并标注说话人
  2. 02搜索录音会议中的决策内容
  3. 03自动处理 Voice Memos 录音

MCP 相关

`whosaid mcp` 向 MCP 客户端提供转录、重新标注、诊断和只读工作区搜索工具。除非启用可选的 Claude 摘要功能,处理过程均在本地进行。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"sblattj-whosaid": {
"command": "uvx",
"args": [
"whosaid"
],
"env": {
"WHOSAID_WORKSPACE": "<WHOSAID_WORKSPACE>"
}
}
}
}
启动命令uvx whosaid
需要配置的环境变量
WHOSAID_WORKSPACEAbsolute path to the meetings workspace the search tools read

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypiwhosaid1.15.2

工具

17

收录的工具列表 · 更新于 3 天前

参数类型必填说明
langstring否
audiostring是
formatenum否txt | srt | vtt | tsv | json | all
outdirany否
diarizeboolean否
accuracyenum否fast | accurate
speakersany否
max_speakersany否
min_speakersany否
match_thresholdany否
anchor_thresholdany否
expected_speakersany否

详细介绍

whosaid

面向 Apple Silicon 的本地说话人归属转录——在 Mac 上识别谁说了什么,一切都留在本机。

许可证:MIT 平台:macOS Apple Silicon

whosaid 演示 — 终端中的说话人归属转录

whosaid 将 Whisper 语音转文字与说话人分离结合起来,把会议、访谈、通话或播客录音转成一份标明谁在何时说话的转录稿——完全在 Apple Silicon Mac 上离线运行并在设备上处理,无需云服务、API 密钥或 Hugging Face 令牌。可以把它看作 whisper + 说话人标签 + 基于声音的说话人识别,只需一条命令。

输出示例(meeting.speakers.txt):

[00:00:04] Alice: Thanks for jumping on, I know it's late for you.
[00:00:11] SPEAKER_01: No problem at all, happy to make it work.
[00:00:19] Alice: Let's start with the roadmap for next quarter.
[00:00:27] SPEAKER_01: Sounds good, I've got a few updates on that.

为什么选择 whosaid

  • 会议、访谈、通话、播客——获取一份为每轮发言标注说话人的转录稿,而不是只有一大段文字。
  • 从设计上保护隐私——音频、文本和声音嵌入绝不会离开你的 Mac。唯一的云端步骤是你主动选择启用的:[summarizer] engine = "claude" 会将每份转录稿的文本发送给 Anthropic,以起草行动项。不设置此项,就不会发送任何内容。
  • 在自己的发言行中显示你的名字——只需一次约 45 秒的声音注册,让 whosaid 学会识别你的声音,这样你的发言就会显示你的名字,而不是 SPEAKER_00。
  • 告知发言人数——自动检测不同说话人的数量,并在开头报告,同时统计每位说话人的发言轮数和发言时长。
  • 说话人卡片,帮你辨认每个人——whosaid 会为每位说话人生成一张卡片,收录最具代表性的语音片段,让你读几行就知道是谁在说话。
  • 记住你命名的人——使用 whosaid relabel 识别一次说话人后,其声纹就会保存到私有本地注册表中,以后每份转录稿都会自动为其命名。
  • 长录音也能快速处理——超过约 15 分钟的录音会按并行窗口进行说话人分离,再通过声纹拼接为一致的说话人;因此,处理一场 80 分钟的会议只需几分钟,而非十几分钟,并能找回与单次处理相同的说话人。
  • 无需账户、API 密钥或 Hugging Face 令牌——所有模型都来自开放、无需授权的来源。
  • 搜索所有录音。 whosaid index 会为会议工作区中的所有转录稿建立本地全文索引(并可在使用 Ollama 时建立语义索引);whosaid search 查找对应发言,whosaid context 显示前后一分钟的内容,whosaid graph 则回答谁在何时承诺了什么。请参阅搜索会议。
  • 无需动手即可处理 Voice Memos 录音。 whosaid watch install 会在 Voice Memos 文件夹(或任何文件夹)上安装 launchd Agent;停止录音后,几分钟内会议就会完成转录、摘要生成并可供搜索。请参阅免手动导入。
  • 也可供 AI Agent 使用。 whosaid mcp 会将转录、重新标记、诊断和只读工作区搜索工具作为 MCP 工具提供给 Claude Code、Claude Desktop、Kiro 及其他 MCP 客户端,并提供与命令行相同的本地处理保障。

对比

正在寻找 MacWhisper、whisperX 或 aTrain 的替代方案? 如果你想要完全在 Apple Silicon 上离线运行、能为说话人标注身份,并能给声音标上真实 姓名(而不只是 SPEAKER_00 标签)的转录工具,那么 whosaid 正好填补了这一空缺。下面是它的表现:

whosaidwhisperXplain mlx-whisper云端转录 API
说话人标签支持支持不支持因提供商而异
根据声音识别说话人姓名支持(注册)不支持不支持不支持
跨会议记住说话人支持 — 持久化的本地声纹注册表不支持不支持不支持
完全离线运行支持部分支持 — 需要下载受限模型支持不支持
对长音频进行并行说话人分离支持不支持—(不支持说话人分离)因提供商而异
需要账户/令牌不需要需要 — 用于受限 pyannote 模型的 Hugging Face 令牌不需要需要 — API 密钥
安装开销ffmpeg + uv,临时环境torch + pyannote + 完整的 HF 技术栈仅 mlx-whisper无(仅网络客户端)

快速入门

git clone https://github.com/sblattj/whosaid && cd whosaid   # get the code
./bootstrap.sh                                                # check deps, download models, install ~/.local/bin/whosaid
whosaid enroll                                                # ~45s reading a printed passage — teaches whosaid your voice
whosaid path/to/meeting.m4a                                   # transcribe + diarize + label -> meeting.speakers.txt (and friends)

如果你的 shell 的 PATH 中没有 ~/.local/bin,请将其添加进去,或使用安装命令的绝对路径调用它。设置 WHOSAID_INSTALL_DIR=/another/bin ./whosaid install 可选择其他安装目录。安装的命令是指向检出目录的符号链接,因此更新检出目录就会更新该命令,无需复制或重复实现。

whosaid 的辅助脚本需要 Python 3.11 或更高版本,因为它使用 tomllib 读取 whosaid.toml。启动器会选择一个解释器:如果设置了 $WHOSAID_PYTHON,就使用它;否则,依次从 PATH 中查找 python3、python3.13、python3.12 和 python3.11,并使用首个版本不低于 3.11 的解释器;若仍未找到,则使用 uv python find '>=3.11'。它会自动跳过 macOS 自带的 python3(3.9)。如果没有符合条件的解释器,运行 Python 的命令会报错并停止;运行 uv python install 3.13 即可解决。whosaid doctor 会显示选中的解释器。

如果已安装 uv、ffmpeg 和 ffprobe,则无需 Homebrew。启动器和监视程序除了会搜索常见的系统前缀,还会搜索 ~/.local/bin、~/bin 和 ~/homebrew/bin。设置过程可以将固定版本且经过校验和验证的 Apple Silicon uv 归档文件安装到 ~/.local/bin。通过 Homebrew 安装 FFmpeg 需要有 bottle;对于非标准前缀,不会悄悄改为从源码构建。

对于没有安装 Homebrew 的 Mac,或无法获取 bottle 的 Mac,请使用:

./bootstrap.sh --build-ffmpeg

这会将固定版本的 FFmpeg 源码构建为用户本地的 ffmpeg 和 ffprobe 可执行文件。此过程需要 Apple 的 Command Line Tools(如果尚未安装,请运行 xcode-select --install)、编译时间和可用磁盘空间,但不需要由管理员管理的安装前缀,也不需要安装 GPG。源代码归档文件会通过 macOS 的 shasum 与经 FFmpeg 签名发布版本验证的校验和进行比对。现有可用工具会予以保留。随后,设置过程会按常规预热 Python 环境并下载模型。

命令

命令功能说明
./bootstrap.sh [--yes] [--build-ffmpeg](也可使用 whosaid setup)检查功能支持、安装依赖、预下载模型并安装命令。缺少 FFmpeg 工具时,--build-ffmpeg 会选择经过验证的用户本地源码构建方式。可幂等执行,安全地重复运行。
whosaid install [--force]在 ~/.local/bin(或 WHOSAID_INSTALL_DIR)中安装/更新命令符号链接。拒绝替换无关命令;除非使用 --force,否则拒绝从位于 /tmp、/private/tmp、/var/tmp 或 $TMPDIR 下的检出目录安装——操作系统清理临时目录后,该符号链接将失效。
whosaid enroll [Name]录制约 45 秒的麦克风音频,内容为朗读一段打印好的文字,并保存为 voices/<Name>.wav。请参阅说话人身份:登记片段与注册表。
whosaid enroll <Name> --from FILE [--ss T] [--t D|--to T] [--force]从现有录音中提取片段,而非使用麦克风(提取 → 验证 → 保存,无需交互)。--ss/--t/--to 接受秒数或 M:SS/H:MM:SS;与麦克风登记采用相同的 ≥15 秒且非静音标准。
whosaid record [--label L]在前台录制麦克风音频到 recordings/<timestamp>[-label].m4a,然后自动转写。
whosaid record --app A [--no-mic] [--label L]通过 ScreenCaptureKit 录制一个应用(Zoom、浏览器等)的音频,并连同麦克风录音保存为立体声 recordings/<timestamp>[-label].m4a(左声道 = 麦克风,右声道 = 应用),然后转写。A 可以是 bundle ID、应用名称,或其中任一项的唯一部分;运行 whosaid record --list-apps 可查看列表。需要 macOS 15+、Xcode Command Line Tools(首次使用时会将小型 Swift 辅助程序 lib/record_app.swift 构建到 ~/.cache/whosaid)以及终端的“屏幕与系统音频录制”权限。无需虚拟音频设备。--no-mic 仅录制应用音频。使用 --me NAME(或 $WHOSAID_OWNER)时,仅麦克风中听到的声音会命名为 NAME,应用一侧的声音不会命名。如果 macOS 终止录制,会在原处重新启动。
whosaid record --browser [--cdp-port P] [--target T] [--leave-selector CSS] [--max-seconds N] [--leave] [--no-mic] [--into DIR] [--label L]通过 Chrome DevTools 端口录制一个浏览器会议标签页(Teams 网页版、Meet、Zoom 网页版),保存为立体声 .m4a(左声道 = 麦克风,右声道 = 标签页)。无需屏幕录制权限;通话结束后会自动停止。不指定 --into 时,会保存到 recordings/ 并转写;指定 --into DIR 时,只保存并打印路径。请参阅录制浏览器会议标签页。
whosaid <audio>… [flags]默认命令:转写一个或多个音频文件、区分说话人并添加标签。如果 <out>/<base>.json 已存在,--reuse-asr 会跳过 Whisper,仅重新进行说话人区分(如果 json 文件缺失则报错;不能与 --no-diarize 一起使用)。whosaid transcribe <audio>… 是同一命令的显式写法(与 whosaid_transcribe MCP 工具名称一致)。
whosaid relabel <base> SPEAKER_02=Jane …阅读说话人卡片后,为各个聚类添加真实姓名。重写转写文本和卡片,并将每个已命名的声纹保存到本地注册表,供后续转写使用;如果新聚类与现有注册表条目不匹配(相似度低于匹配阈值,默认值为 0.50),则拒绝覆盖该条目,除非使用 --force。不会重新转写。
whosaid relabel <base> SPEAKER_04=Alice --blend将该聚类与 Alice 已保存的声纹取平均,而非替换声纹,以利用新录音增强声纹。与普通 relabel 使用相同的相似度保护机制。请参阅说话人身份:登记片段与注册表。
whosaid relabel <base> SPEAKER_04=Alice --no-save [--note TEXT]仅为转写文本添加标签:重命名转写文本和卡片中的聚类,但不会写入注册表。适用于对话中能明显判断说话人身份,但其聚类声纹质量较差的情况——如果将一个较长的混合聚类保存为该说话人的声纹,会降低其登记声纹的质量。标签会保留在 sidecar(local_labels)中,经 relabel --auto 和 whosaid samples 后仍然有效;每张卡片会标记为 Alice_Example (transcript-only label; registry untouched)。请参阅说话人身份:登记片段与注册表。
whosaid relabel <base> --auto [--forget NAME ...] [--fold-unknown]对现有转写文本重新应用命名,不需提供映射:基于缓存的 sidecar 重新运行注册表匹配和吸收流程(与 transcribe 一样,也会使用 voices/ 中的登记片段),并重写转写文本和卡片。会识别转写完成后登记的声音,并移除注册表条目和片段均已不存在的自动分配名称。--forget NAME(可重复使用)会先从此次会议中清除 NAME;如果 NAME 仍已登记且匹配成功,该名称可能会重新出现。添加 --fold-unknown 可修复缓存的自动说话人区分结果中的匿名伪聚类;此选项必须与 --auto 一起使用,且不会分配身份。不会重新转写或重新区分说话人。在会议工作区中,base 为 transcript。请参阅说话人身份:登记片段与注册表。
whosaid backfill <ws> [--dry-run]将当前注册表中的名称和角色应用到历史会议,重新生成待办事项,并刷新汇总、工作清单、搜索、关系图和 wiki。更改会议文件前会报告冲突。
whosaid speakers export --out FILE将私人说话人注册表导出到新的备份文件,包括声纹、角色和元数据。
whosaid speakers import FILE [--merge|--overwrite]恢复注册表备份。目标位置已存在时,必须明确指定冲突处理策略;--overwrite 会替换整个注册表。
whosaid samples <base> [-o DIR] [--audio FILE] [--per-speaker N] [--seconds S] [--json]为每个说话人聚类导出一个简短的代表性 WAV 文件——取时长最长的已区分说话人片段,并截断至 --seconds(默认 8)——以便在信任自动标签或登记前试听并确认身份。音频取自 sidecar 自带的 source.path;如果 sidecar 写入时尚无此元数据,则可显式指定 --audio FILE。
whosaid ingest <audio>… --into DIR [--action-items] [--engine E] [--index]将一批录音转写到按日期排列的会议文件夹中(按内容哈希实现幂等)。--engine 用于选择待办事项摘要器;--index 会在之后运行汇总和索引。请参阅会议工作区。
whosaid roll-up <ws> [--action-items] [--refold MEETING[=OLD_MD]]... [--index] [--owner NAME|me] [--all-owners]重建工作区索引(_INDEX.md)、审计文件、去重后的待办事项与开发承诺语料,以及负责人排序后的 _WORKLIST-<Owner>.md;--index 还会重建搜索索引。--refold MEETING[=OLD_MD](可重复使用;隐含启用 --action-items,不能与 --rebuild 一起使用)会重新读取一个已生成汇总的会议中重新生成的 action-items.md:保留 ID、手动设置的状态和标题;负责人根据新的说话人标签更新;OLD_MD 是该会议之前的文件,因此手动更改过标题的待办事项也能重新匹配。
whosaid reprocess <ws> [MEETING ...] [--dry-run] [--all] [--no-items] [--speakers N] [--min-speakers N] [--max-speakers N] [--expected-speakers A,B] [--match-threshold X] [--engine E] [--hook CMD]重新区分旧版说话人区分器处理错误的会议,复用现有 ASR(transcribe --reuse-asr),保留 --no-save 标签和手动编辑,并在 <ws>/_reprocess-backups/ 中创建备份。--dry-run 只报告结果。请参阅升级到 1.11。
whosaid commitments <ws> [--owner NAME|me] [--all-owners] [--json] [-o FILE]根据语料按需打印个人待办清单(P1/P2/P3),无需执行汇总。请参阅开发承诺。
whosaid index <ws> [--no-embed] [--rebuild]构建 <ws>/_search.db(全文搜索、可选嵌入、实体关系图)和 <ws>/_WIKI.md。请参阅搜索会议内容。
whosaid search <ws> "<query>" [--mode exact|meaning|hybrid] [--speaker S] [--meeting M] [-k N] [--json]搜索所有已标注说话人的转写文本;每条命中结果都包含会议、时间戳、说话人和片段。
whosaid context <ws> <meeting> <HH:MM:SS> [--before S] [--after S] [--json]显示某一时刻前后的原话,方便在上下文中阅读命中结果。
whosaid graph <ws> items|item AI-NNN|person [Name]|prs|meetings|speakers|commitments [--json]实体视图:带筛选条件的待办事项、单个待办事项的历史、某人的承诺与请求、PR 提及、会议覆盖情况和发言占比。commitments 会列出统一的 CM 表(可使用 --owner、--source transcript|action-items、--status 筛选)——自 #23 起,工作清单、whosaid commitments 和 wiki 共用同一个底层模型:汇总过程会将待办事项中的自有任务条目折叠进与口头表述共用的 CM-NNN 语料(同一事项、两种来源的记录,并包含条目的时间戳);graph build 会加载该语料,而不是重新解析这些条目。
whosaid wiki <ws> [--stdout] [-o FILE]根据关系图重新生成 <ws>/_WIKI.md。
whosaid watch run|install|uninstall|status --into <ws>通过 launchd Agent 无需手动操作地导入新录音。请参阅免手动导入。
whosaid memos list|pull|delete "<title>"|shortcut-recipeVoice Memos 辅助工具,不会触碰应用文件或数据库。请参阅Voice Memos 辅助工具。
whosaid doctor只读环境报告,包括 Ollama 的可访问性以及 $WHOSAID_WORKSPACE 的索引状态。
whosaid version | whosaid --version | whosaid -V显示已安装版本(从 git 检出目录运行时,还会附带 git describe 后缀)。
录制浏览器会议标签页(无需权限)

whosaid record --app 需要为你的终端授予屏幕与系统音频录制权限。浏览器会议可以绕过这一步。启动一个带调试端口的 Chromium 系浏览器(Chrome、Vivaldi、Edge),在其中加入会议,然后录制该标签页:

# once, when you launch the browser
open -a "Google Chrome" --args --remote-debugging-port=9222

whosaid record --browser                      # port 9222, or $WHOSAID_CDP_PORT
whosaid record --browser --cdp-port 9333 --target teams.microsoft.com
  • 只录制该标签页。左声道是你的麦克风,右声道是会议音频。不会录制系统声音或通知。
  • --target 可以是 targetId,也可以是标签页 URL 中的一段内容。如果可能匹配多个标签页,就需要指定它。错误信息会列出候选项。
  • 通话结束时会自动停止。检测到挂断按钮后开始监控,连续 2 次轮询未检测到按钮后停止。如果始终未检测到按钮,请使用 Ctrl-C 或 --max-seconds(默认值为 10800)。--leave-selector CSS 会覆盖内置的 Teams、Meet 和 Zoom 网页选择器。--leave 会在达到 --max-seconds 时点击挂断按钮。
  • 如果在通话中关闭标签页,已采集的内容会保留。
  • --into DIR 会将文件写入 DIR/<timestamp>[-label].m4a,并跳过转录。文件只有在录制完成后才会出现,因此可以将其保存到 whosaid watch 源文件夹,由监视器接收。
  • 如果使用 Agent,请调用 whosaid_record_browser MCP 工具,并将 action 设置为 start、status 或 stop。它会在后台运行,无需终端。
从现有录音中登记

已经有一段此人发言的录音?跳过麦克风,直接从中截取参考片段:

whosaid enroll Alice --from meeting.m4a --ss 3:20 --t 20   # or --to 3:40

会议工作区

一次性转录的结果是文件;定期举行的会议系列则是语料库。会议工作区层为该语料库提供了存放位置:每段录音都会转录到带日期的文件夹中,每场会议都可以附带生成的行动项和开发承诺,而一次汇总即可生成索引、审计记录、持续更新的行动项列表,以及涵盖所有会议的开发承诺语料库——整个过程仍完全离线。

whosaid ingest — 批量导入至按日期命名的文件夹
whosaid ingest weekly/*.m4a --into ./meetings --folder-by created \
  --tz America/Los_Angeles --action-items

每个文件都会在传入完整的 whosaid <audio>… 标志集后进行转录,并保存到 meetings/YYYY-MM-DD-HHMM/。时间戳取自录音容器自身的 creation_time,并按 --tz(默认 UTC)呈现;如果没有,则使用文件的修改时间(mtime)。因此,文件夹顺序反映的是会议实际发生的时间,而不是你后来复制文件的时间。 导入操作以源文件的 sha256 为依据保持幂等:重新运行批次不会重新转录或重复录音。

启用 --action-items 后,每场会议还会生成一个 action-items.md。生成过程可通过插件定制:钩子命令会从 stdin 接收带说话者标签的转录文本,环境中还会提供 WHOSAID_SPEAKERS 和 WHOSAID_TRANSCRIPT_PATH;命令写入 stdout 的内容将成为 Markdown。可通过 --hook CMD 为本次运行指定钩子,也可设置一次 WHOSAID_ACTION_ITEMS_HOOK。若未指定钩子,则会写入一个骨架文件,整个过程保持离线。钩子示例——仅供说明;任何能将 stdin 转换为 Markdown 的命令都可以:

#!/bin/sh
# Drafts action items with a local Ollama model.
ollama run llama3.2 "List this meeting's action items as markdown bullets (Owner: task):"

现在,无需钩子也能生成实际草稿:--engine ollama(或默认的 --engine auto,若 Ollama 正在运行则会使用它)会启用由本地模型起草的行动项中所述的内置摘要器;--engine claude 则是可选的云端方案。--engine 会自动启用 --action-items。添加 --index 后,所有文件导入完成时,导入流程会自动为你运行 whosaid roll-up <ws> --action-items 和 whosaid index <ws>,这样新会议便可通过同一条命令进行搜索:

whosaid ingest weekly/*.m4a --into ./meetings --engine ollama --index

启用 --commitments 后,每场会议还会生成一个 commitments.md,其中记录你在会议中以第一人称作出的承诺(参见 Dev-commitments)。通过 whosaid relabel --role 设置的角色会决定哪些提示语会被纳入:将你自己的声音标记为 self(并将经理标记为 boss),提取器就会跟踪你承诺的事项,并将经理要求的事项排在更高优先级。 --min-words N 可为本次运行调整片段过滤条件(例如 "I'll do that" 这样的片段会被丢弃;默认值为 [commitments] min_words,若未设置则为 2;设为 0 可禁用)。

whosaid roll-up — 索引、审计和行动项语料库
whosaid roll-up ./meetings --action-items
  • 覆盖索引 — _INDEX.md 每场会议占一行(创建日期、时长,以及是否已转录、已区分说话人和包含行动项),另有一项完整性审计,用于标记孤立目录和过期的清单条目;还有一个周期性主题部分,用于显示在多场会议中出现的主题。一旦有开发承诺,该文件还会追加一个单行的未完成/总数统计部分。两个输出路径都可通过 -o 和 --action-items-out 覆盖。
  • 行动项语料库 — 使用 --action-items 时,_ACTION-ITEMS.md 会跨会议去重条目(按文本相似度;阈值为 --similarity-threshold,范围 0.5–1.0,默认值 0.82),并先按负责人、再按状态分组。ID 保持稳定(AI-001…,且永不重新编号),每个条目都带有 first_seen/last_seen 日期及其出现列表;未完成/进行中/已解决状态会在重新运行时保留,因此该语料库展现的是贯穿整个系列的动态历史,而不是单场会议快照。条目可以带有自由格式的类型,该类型会显示在状态后的括号中;相似度略低于阈值的配对会显示在末尾的 可能的重复项(供审核) 部分。
  • 开发承诺语料库 — 只要存在任何会议的 commitments.json(通过 ingest --commitments 生成),就会自动汇总到 _COMMITMENTS.md:ID 稳定为 CM-NNN 且永不重新编号,使用相同的 0.82 文本相似度去重和 0.10 的近似匹配审核区间,并按状态、再按发言者分组;以 **[boss]** 标记老板要求的事项。对 _COMMITMENTS.md 的手动编辑(将某项标记为 done``、重命名,或通过 (merged CM-NNN)合并)会在下次汇总时折叠回语料库,与行动项语料库的处理完全相同。折叠时会应用与提取相同的[commitments] min_words片段规则,因此,在应用筛选前写入的(或由钩子写入的)单场会议文件不会把 “I'll do that” 放进语料库:跳过的条目会列在 _已丢弃片段(供审核)_ 下,并在_commitments.json中保留为dropped;语料库中已有的 CM 条目不会被改动。 汇总时只会重新折叠承诺输入有变化(commitments.json、action-items.md、*.speakers.txt、角色),或新增、移除的会议,因此 Teams 的小幅变更只需几秒;日志会显示 refolding N changed commitment folder(s): ...,折叠时间较长时则每处理 25 个目录显示一次 folded N/M commitment folder(s)。ID 和手动编辑都会保留;已移除行贡献的字段(提示、老板优先级、请求者)会根据剩余内容重新推导。whosaid.toml中的[commitments]` 发生变化时,仍会重新折叠所有会议。
  • 个人工作清单 _WORKLIST-<Owner>.md:只要存在承诺语料库或已归属负责人的行动项,汇总时也会按 P1/P2/P3 优先级写入该负责人的未完成事项(参见开发承诺)。负责人取自 --owner NAME,否则为具有 self 角色的发言者,再否则为 [workspace] owner;--all-owners 会为每位参与者写入一个文件。这是重新生成的视图,不会与现有内容合并:请编辑语料库,而非工作清单。

去重依据文本相似度(difflib,即上述 --similarity-threshold);如果本地 Ollama 在 127.0.0.1 上响应,且 [search] embed = true,还会使用嵌入余弦相似度:只要 difflib 比率或 [search] embed_model 余弦相似度达到相应阈值,两个文本就会匹配([commitments] embed_threshold,默认值 0.90),因此 "I'll write the rollout runbook for the platform team" 和 "for the platform team write the rollout runbook" 会合并为一个条目。没有 Ollama、embed = false,或 URL 非回环地址时,则只使用 difflib,并记录一行日志说明此情况。

默认情况下,汇总采用增量式且只追加内容:没有新内容时,重新运行不会写入任何内容。--rebuild 可用于彻底重建 — 它会重置清单和语料库,并根据磁盘上的目录重新生成两者。 状态以纯 JSON 格式存放在工作区目录中:_workspace.json(清单;通过 commitments_corpus 指向 dev-commitments 语料库)、_action-items.json(语料库;同时记录当前生效的 similarity_threshold),以及与之并行的 _commitments.json。 这些文件都可以安全读取和手动编辑——手动将某个条目标记为 resolved,正是关闭提取器措辞有误的条目的预期方式。直接在 _ACTION-ITEMS.md(以及遵循同一约定的 _COMMITMENTS.md)中进行的手动编辑,会在下次汇总时合并回去,并在重新运行后保留: 状态、类型、标题修改,以及 (merged AI-NNN) 合并注释(被合并的条目仍保留原 ID,并以 [merged → AI-NNN] 的形式折叠显示)。只有 --rebuild 会丢弃这些修改。--index 会在汇总后立即运行 whosaid index <ws>。

升级到 1.11:重新处理较早的会议

v1.10 修复了两个说话人分离器故障:短片段会虚增自动检测的说话人数;时长不足 15 分钟的录音会忽略 --speakers N,导致人数统计偏低。此前处理的会议仍保留旧的说话人划分,其行动项和承诺也会沿用旧划分(幽灵“说话人”会拥有行动项,或者两个人会被合并为一个人)。whosaid reprocess 会重新进行说话人分离,而不会重新运行 Whisper。

whosaid reprocess ./meetings --dry-run     # 1. what would change; touches nothing
whosaid reprocess ./meetings               # 2. re-diarize the flagged meetings (or --all)
$EDITOR ./meetings/_ACTION-ITEMS.md        # 3. review the refolded corpus
  • 检测器(--dry-run)。 它会读取每个会议的 <base>.diarization.json,并逐个会议打印检查结果。likely 包括:brief-fragments(自动人数统计包含一个仅由时长不足 2 s 的轮次组成的簇)和 short-hinted-undercount(音频时长不足 900 s,指定了 --speakers N,但检测到的人数少于 N),以及 bound-pinned(--max-speakers 上限或 whosaid.toml 中的 max_speakers 将已饱和的自动人数统计设为上限,而不是将其限制在上限以内;此问题已在 v1.11.2 中修复)。possible 包括:short-whole-file(音频时长不足 900 s,自动人数统计,没有人数估算;v1.10 的 --no-chunk 运行结果看起来也一样)和 ref-order(匹配到两个或更多 --ref 语音,这些语音在 v1.10 之前按参数顺序分配)。v1.11.0 或 v1.11.1 写入的 Sidecar 文件仅检查 bound-pinned;v1.11.2 或更高版本的文件不会被标记。若没有 MEETING 且没有 --all,reprocess 只会处理存在检查结果的会议;指定 MEETING 名称(文件夹名称)或 --all 则会覆盖此行为。
  • 不运行 ASR。 每个会议都会以 whosaid transcribe --reuse-asr 重新运行,复用现有的 <base>.json。每个会议的说话人分离需要几秒到一分钟。重新生成行动项时,每个会议都会运行一次摘要器,而 claude 引擎会对每个会议进行一次云端调用(参见claude 引擎);--engine E 和 --hook CMD 与 ingest 中一样,用于选择摘要器。--no-items 会跳过行动项和承诺的重新生成。
  • 说话人数。 除非传入 --speakers、--min-speakers、--max-speakers 或 --expected-speakers 来替换原设置,否则会沿用会议首次进行说话人分离时使用的标志(--speakers N 或 --min/--max-speakers 范围,取自 Sidecar 文件的 detect_mode)。Sidecar 文件中不会记录预期说话人姓名,因此不会恢复这些姓名。--match-threshold X 用于设置注册表匹配阈值。
  • 保留的内容。 relabel --no-save 设置的标签会根据片段重叠关系沿用到新簇。对于注册表已知的姓名,不会重新应用角色。随后,reprocess 会为每个重新生成的会议运行 whosaid roll-up <ws> --action-items --refold MEETING=<backup>/action-items.md,因此语料库 ID(AI-NNN/CM-NNN)、手动设置的状态和手动编辑的标题都会保留,而所有者则会根据新的说话人标签更新。
  • 备份与恢复。 修改会议之前,reprocess 会将该会议的 <base>.speakers.txt、<base>.speaker-cards.txt、<base>.rttm、<base>.diarization.json、action-items.{md,json} 和 commitments.{md,json} 复制到 <ws>/_reprocess-backups/<meeting>/<UTC timestamp>/,并在某个步骤失败时自行恢复。如要撤销不想要的运行结果,请将这些文件复制回会议文件夹,然后运行 whosaid roll-up <ws> --refold MEETING(普通的 roll-up 会跳过已折叠的会议)。以 _ 开头的文件夹会被 roll-up 和工作区扫描跳过;确认无误后可将其删除。

在 8 个首次由 v1.8.2 处理的合成会议中(已知实际说话人数),说话人数误差绝对值总和从 11 降至 6;其中两个使用 --speakers N 运行的会议,检测到的说话人数从 2 人(实际分别为 5 人和 3 人)变为准确人数。8 个会议中,Whisper 均未运行;--no-save 标签、手动设置的 done 状态以及手动修改的标题在重新折叠后都得到保留,ID 也未改变。

MCP 工具 whosaid_reprocess 封装了相同的命令;其 dry_run 默认值为 true。

whosaid teams ingest — Microsoft Teams 聊天,不含音频

许多决策和承诺是在 Teams 聊天中达成的,而非录制的通话中。whosaid teams ingest 会将聊天导出内容添加到同一工作区中。无需说话人分离,也无需 Whisper,因为每条消息都已注明作者。

whosaid teams ingest teams-export.json --into ~/meetings --tz America/Los_Angeles --self Alice_Example
whosaid roll-up ~/meetings --index

输入是浏览器抓取器的输出,见 contrib/teams-scraper/。可以是其 raw_by_chat 映射、由 {chat, author, ts, epoch, text} 记录组成的平面列表或 NDJSON,也可以是包含相同记录、但使用 timestamp_iso/epoch_ms 的数据。抓取器的 README 还介绍了运行方法,以及它所依赖的 Teams 网页 DOM 契约。导入会按每个聊天的每个本地日期创建一个带日期的文件夹(YYYY-MM-DD-HHMM,时间为当天第一条消息的时间)。每个文件夹包含:

  • teams.speakers.txt:[HH:MM:SS] Speaker: text 格式的对话轮次,时间戳为本地时间。
  • teams.diarization.json:来源信息侧车文件。source.kind 为 "teams-chat",且每个片段都会保留消息的精确 epoch_ms。

搜索、关系图、承诺和工作清单读取 teams.speakers.txt 的方式与读取录音转录文本相同。搜索结果中的 file 为 teams.speakers.txt;whosaid graph <ws> meetings --kind teams-chat(或 whosaid_meetings MCP 工具的 kind 参数)可用于区分聊天日期和录音。没有音频的文件夹,其 created 时间取自侧车文件中的 source.creation_time。汇总审计会将其视为纯文本,而不会将其标记为缺少转录文本。

在 whosaid.toml 中,将 Teams 显示名称映射到你在其他地方使用的名称,并为人员指定角色:

[teams.names]
"Doe, Jane (Vendor, consultant)" = "Jane_Example"

[teams.roles]
Jane_Example = "boss"

未映射的作者会保留其显示名称。如果未在 [teams.roles] 中设置角色,则会使用语音注册表中同名人员的角色。--self NAME 会覆盖这两项设置。重新导入有重叠内容的导出是安全的:消息会按 epoch_ms 去重并合并到现有文件夹中;--dry-run 会显示将要写入的内容。

whosaid teams ingest-transcript:Microsoft Teams 会议转录文本,不含音频

当 Teams 已经转录了会议时,回顾页面的“转录文本”面板会标明每位说话人。whosaid teams ingest-transcript 会将该转录文本添加到工作区,无需音频。

whosaid teams ingest-transcript transcript.json --into ~/meetings --tz America/Los_Angeles --self Alice_Example

输入为 contrib/teams-scraper/teams-transcript-scraper.js 中的 window.__tt.result。该面板是虚拟化列表,因此抓取器会从 React 中读取列表的完整项目数组,而不是仅读取屏幕上显示的几行;如果失败,则会采用滚动抓取方式,并恢复滚动位置。一场会议对应一个以会议开始时间命名的文件夹(YYYY-MM-DD-HHMM)。teams.speakers.txt 会将每个对话轮次标记为相对于开始时间的偏移量,与录音的转录文本类似;teams.diarization.json 包含 source.kind "teams-transcript"、transcript_id 以及 Start/Stop 事件。开始时间取自导出内容的 meeting.start_iso,或使用 --start ISO 指定。重新导入时,会重写已包含相同 transcript_id 的文件夹。[teams.names]、[teams.roles]、--self 和 --dry-run 的用法与聊天导入相同。

搜索会议

包含十几场会议的工作区,是一个你无法从头重读的语料库。whosaid index 会将其转换为 可供查询的内容:覆盖每个发言轮次的精确全文索引、通过本地嵌入模型进行的可选语义搜索、 实体图谱(人员、会议、行动项、带时间戳的承诺、PR 提及),以及生成的维基。所有内容都存放 在工作区中,都可以重新构建,而且不会离开本机。

whosaid index ./meetings                     # build _search.db + _WIKI.md (embeds if Ollama is up)
whosaid index ./meetings --no-embed          # exact search only, no Ollama needed
whosaid search ./meetings "budget"           # every turn that says budget
whosaid search ./meetings "who is blocked" --mode meaning
whosaid search ./meetings '"token exchange"' --speaker Bob_Example --meeting 2026-09-16 -k 5
whosaid context ./meetings 2026-09-16-0900 00:00:06   # the verbatim minute around a hit
whosaid graph ./meetings items --owner Alice_Example --status open
whosaid graph ./meetings item AI-003         # one item: occurrences + timestamped commitments
whosaid graph ./meetings person Bob_Example  # talk share, commitments made, asks received
whosaid graph ./meetings prs | whosaid graph ./meetings meetings | whosaid graph ./meetings speakers
whosaid wiki ./meetings --stdout             # the generated wiki, regenerated from the graph

所有命令中的 <ws> 都可以省略:命令会依次回退到 $WHOSAID_WORKSPACE,然后在当前目录 包含 _workspace.json 或 whosaid.toml 时使用当前目录。任何包含 *.speakers.txt 的 子文件夹都会被视为一场会议,无论文件夹名称是否带日期,因此手动制作的转录稿也会被索引。

三种搜索模式。 --mode exact(默认)使用 SQLite FTS5:支持单词、"引号短语"、 AND/OR/NOT 和 prefix*,且只需 Python。--mode meaning 通过 Ollama 在 127.0.0.1:11434 上使用 nomic-embed-text 对查询进行嵌入,并返回最接近的发言轮次, 因此,搜索「谁被卡住了」可以找到「我卡住了,得等审核完成」,即使两者没有共同的词。 --mode hybrid 会同时运行两种模式并融合排序结果;索引有了嵌入后,通常就该使用这种模式。 每条结果的输出格式相同,--json 则会为每条结果提供一个包含相同字段的对象:

$ whosaid search ./meetings "budget"
whosaid: engine: exact
[2026-09-16-0900 @ 00:00:06] Bob_Example: I will review the »budget« spreadsheet before Thursday.
1 hit(s).

$ whosaid context ./meetings 2026-09-16-0900 00:00:06 --before 10 --after 10
whosaid: 2026-09-16-0900: 3 turn(s) between 0s and 16s
[00:00:01] Alice_Example: Let's start with the roadmap for next quarter.
[00:00:06] Bob_Example: I will review the budget spreadsheet before Thursday.
[00:00:14] Alice_Example: Great, and I will send the draft out to the team today.

若要使用语义搜索,只需设置一次 Ollama:brew install ollama && brew services start ollama && ollama pull nomic-embed-text。如果没有安装,index 会附带说明并跳过嵌入步骤, search 则只运行精确搜索;不会因此失败。

图谱视图。 实体表仅根据可靠信号构建:清单、去重后的行动项语料库、每场会议的 action-items.md,以及转录稿本身。items 可按 --owner、--requester、--status 和 --type 筛选;item AI-NNN 会显示某个行动项的所有出现记录,以及其背后的每条带时间戳 承诺;person [Name] 是按负责人查看的视图(发言占比、承诺事项、向他人提出的请求);prs 会列出发言或笔记中提及的拉取请求编号;meetings 显示每个文件夹的覆盖情况;speakers 显示整个工作区的发言占比。每个视图都支持 --json。

维基。 _WIKI.md 是生成的汇总文档,包含人员、会议、未完成事项和 PR 提及;每个行动项 都会注明其承诺时间,格式为 [meeting @ time]。它会在运行 index 和 whosaid wiki 时重新 生成,因此始终与数据保持一致。请编辑语料库,而不是维基。

每个工作区的设置:whosaid.toml。 这是可选文件,放在转录稿旁边。每个键都有默认值; WHOSAID_OWNER、WHOSAID_OLLAMA 和 WHOSAID_SUMMARIZER_MODEL 可在每次运行时覆盖对应的键。

[workspace]
owner = "Alice_Example"          # whose action items this workspace tracks
aliases = ["Ali", "Alicia"]      # how the transcript may misspell the owner (default: first name)

[groups]                         # optional, ordered; drives the action-item sections
leadership = ["Bob_Example"]
team = ["Carol_Example", "Dan_Example"]

[summarizer]
engine = "auto"                  # auto | ollama | claude | hook | none (claude: opt-in, cloud)
model = "qwen2.5:14b"
timeout = 900                    # seconds per model call
num_predict = 2048               # max tokens per model reply; caps a runaway generation
think = false                    # thinking models (qwen3, qwen3.x) skip reasoning; much faster
                                 # per model: think = { "qwen3:14b" = true, default = false }
claude_model = "opus"            # engine = "claude": any `claude --model` value
claude_timeout = 900             # seconds for the one claude call
claude_bin = ""                  # path to `claude` (default: WHOSAID_CLAUDE_BIN, PATH, ~/.local/bin)
fallback = "ollama"              # engine = "claude" failed: "ollama" (if it is up) or "none" (skeleton)

[search]
ollama = "http://127.0.0.1:11434"
embed_model = "nomic-embed-text"
embed = true                     # false: exact search only, never contact Ollama

[commitments]                    # optional; ranks _WORKLIST-<Owner>.md. A list you set replaces
boss = []                        # the default list, so omit a key to keep the built-in cues.
deadline_cues = ["today", "tonight", "tomorrow", "eod", "end of day", "this week", "next week"]
blocking_cues = ["blocking", "blocked", "urgent", "asap", "critical", "hotfix", "outage", "prod issue", "release blocker", "customer escalation"]
negators = ["not", "no", "non", "never", "isn't", "won't", "don't", "without", "hardly"]
embed_threshold = 0.90           # cosine at or above this is a duplicate (with [search] embed)
min_words = 2                    # content words a commitment clause needs; 0 keeps every fragment
[commitments.weights]            # score = sum of the signals that fired
boss = 5
blocking = 4
deadline = 4
overdue = 1                      # replaces deadline once a relative deadline has passed
repeat = 2                       # per extra meeting
recent = 1
strong = 1
requested = 1
negative = -3

[watch]
source = ""                      # folder to watch (default: the macOS Voice Memos store)
# speakers = 2                   # exact diarization speaker count (a 1:1-call workspace)
# min_speakers = 2               # lower bound on auto-detected speaker count
# max_speakers = 4               # upper bound (a ceiling for a mixed-size meeting workspace)
# expected_speakers = ["Alice_Example", "Bob_Example"]   # roster, or "Alice_Example,Bob_Example"

[diarize]                        # same four keys, for EVERY ingest into this workspace
# max_speakers = 8               # e.g. a ceiling near the team's real headcount

_search.db 是什么。 工作区中的一个 SQLite 文件:包含一张记录每个发言轮次的 FTS5 表(会议、时间戳、发言者、文本)、一张以发言轮次为键存储嵌入的表,以及图谱表。 它是一个派生文件。你可以随时将其删除,然后重新运行 whosaid index;--rebuild 会执行 相同操作,并重新嵌入每个发言轮次。

由本地模型起草的行动事项

whosaid ingest --action-items 可以使用本地模型为每场会议起草 action-items.md,而无需使用钩子。--engine(或 whosaid.toml 中的 [summarizer] engine)用于选择处理方式:

引擎处理方式
auto(默认)如果配置了钩子,则使用钩子;否则,如果 Ollama 在 localhost 上有响应,则使用 Ollama;再否则生成骨架文件。
ollama使用下方内置的摘要器。如果 Ollama 未运行或缺少模型,会发出警告并写入骨架文件(退出码为 0)。
claude需显式启用,使用云端服务。 转录文本会发送给 Anthropic。请参阅Claude 引擎。auto 永远不会选择它。
hook使用 --hook / WHOSAID_ACTION_ITEMS_HOOK 命令,与之前完全相同。
none仅生成骨架文件(列出发言者,不包含事项)。

模型不会编造时间戳或证据。脚本会以确定性方式选出候选发言轮次(工作区所有者的每个实质性发言轮次、每个提及所有者的发言轮次,再对其余内容进行一次模型处理,以查找未指明对象的请求),然后模型逐条阅读发言轮次(较长的发言轮次按句子分组),并以 SKIP 或项目符号作答,每项都附带一段简短的原文引述。每段引述都会与其所属发言轮次核对;不匹配时会标记出来,而非悄然丢弃,并且证据发言轮次会逐字附加到文件末尾。

章节来自你的配置:每个 [groups] 条目对应一个章节(领导层提出的要求、团队提出的要求等),还包括领导层对团队的指示、所有者自己的承诺,以及用于其余内容的推断章节。团队指示是领导层为整个团队设定的优先事项、截止日期、流程或期望,例如“从现在起,每个 PR 都必须关联一个工单”。即使从未点名所有者、所有者也从未发言,这仍算作所有者的事项。领导层指 [groups] 中名为 leadership 的条目,以及转录中被标注为 boss 的任何人;如果两者都不存在,则省略该章节。文件开头会显示 DRAFT 横幅,提醒读者内容尚未审核;末尾则会附上每条项目符号对应的准确原文证据。将任何内容合并到 _ACTION-ITEMS.md 之前,请先阅读证据。

模型选择:默认模型为 qwen2.5:14b(ollama pull qwen2.5:14b;在 M 系列 Mac 上处理一小时的会议,大约需要一分钟)。WHOSAID_SUMMARIZER_MODEL=qwen2.5:7b 的速度大约快一倍,但结果也大约嘈杂一倍。Ollama 只会通过 127.0.0.1 联系。

Claude 引擎(可选启用,云端)

engine = "claude"(或 --engine claude)会通过你已登录的 Claude Code CLI 使用 Claude 起草同一份文件:每场会议调用一次 claude -p,读取完整转录文本并以 JSON 格式返回事项。这会将转录文本发送给 Anthropic。 音频和声纹仍保留在本地。只有录音内容可以发送给云端模型时才启用此选项;auto 永远不会选择它。

其余部分遵循本地引擎的约定:使用相同的章节(由计划决定,而非模型)、相同的防护规则(来自领导层以外人员的团队指示,或以「Sam,……」开头的句子,都会被丢弃),以及相同的验证机制。每项内容的发言者和时间均取自包含其引文的转录轮次;若没有任何轮次包含该引文,则会标记为 ⚠;证据块会逐字列出已验证的轮次。横幅下方的注释会注明模型,并说明转录文本已发送给 Anthropic。

此调用在隔离环境中运行:没有工具、MCP 服务、设置、钩子、技能或斜杠命令;不保存会话;以空临时目录作为当前工作目录;并清除 ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN,使其使用你的 Claude 登录,而非 API 密钥。同时也会清除模型别名覆盖项(ANTHROPIC_MODEL、ANTHROPIC_DEFAULT_OPUS_MODEL 等)以及父级 Claude Code 会话的 CLAUDECODE / CLAUDE_CODE_* 变量,因此只有 claude_model 会决定所用模型,且在 Claude Code 内运行时的行为与监视器一致。二进制文件依次从 [summarizer] claude_bin、WHOSAID_CLAUDE_BIN、PATH 中的 claude,以及 ~/.local/bin/claude 查找,因此 whosaid watch 的 LaunchAgent 也能找到它。如果调用失败,引擎会发出警告;当 fallback = "ollama" 且 Ollama 正在运行时,会回退到本地 Ollama 引擎,否则写入骨架内容(退出码为 0)。在 6 个评估样例上,Claude 引擎调用 6 次,F1 得分为 0.960;同一 Opus 模型通过逐轮处理流程运行时调用 84 次,得分为 0.899;qwen3:14b 得分为 0.895(docs/eval.md)。

免手动操作的导入:whosaid watch

在语音备忘录中录制会议,然后离开;按下停止按钮后几分钟内,录音就会完成转录、摘要整理并可供搜索。whosaid watch 是一个 launchd LaunchAgent,会监视某个文件夹(默认是 macOS 语音备忘录存储目录),并将每段新录音依次交给 whosaid ingest --action-items --commitments 处理,然后运行 roll-up(也会刷新工作清单),最后运行 index。

whosaid watch install --into ~/meetings --seed      # install; mark the memos already there as done
whosaid watch install --into ~/meetings --dry-run   # print the plist and every step; change nothing
whosaid watch status  --into ~/meetings             # installed? loaded? last run? (--json too)
whosaid watch run     --into ~/meetings --dry-run   # what WOULD be ingested right now
whosaid watch uninstall --into ~/meetings --purge   # stop the agent, remove state + staging + interpreter

运行过程如下:源文件夹中尚未记录在 <ws>/.watch_state.json 且 mtime 已稳定 [watch] stable_seconds 秒(默认 120)的每个音频文件,都会被复制到 <ws>/.watch_staging/ 并从该目录导入;mtime 仍在变化的文件会被视为仍在录制或同步中,流程会等待(等待时间不超过 max_wait_seconds)并重新扫描。.watch.lock 可避免两个流程同时运行。该 Agent 会在文件夹发生变化时触发(WatchPaths),并每隔 --interval 秒触发一次(默认 900),作为安全保障。--seed 会将已存在的录音标记为已处理,避免重新处理已有音频库。--source DIR 可监视任意文件夹,而不仅限于语音备忘录;--offline 会将 Hugging Face 离线变量写入 Agent,使无法联网的机器不再尝试下载;--env K=V 可添加 Agent 应携带的其他环境变量;运行 run 时,--engine E 可选择摘要生成器。录音源目录与会议工作区(转录文本、语料库、_search.db)必须是不同文件夹——不能是同一目录,也不能互相嵌套,否则 install / run 会拒绝启动。 说话人分离提示。 监视器无法询问房间里实际有多少人,而盲目自动检测说话人可能会把一次较长的通话过度拆分成一堆虚构说话人,明明应该识别出两位。对于只录制一对一通话的工作区,设置 [watch] speakers = 2;对于会议人数不一的工作区,可将 max_speakers 设为上限;min_speakers 和 expected_speakers(参见下方的 --expected-speakers 标志说明)也从同一配置块读取。这些设置适用于整个工作区,而非单条录音——每条导入的录音都会使用相同提示——并且会在下一次运行时立即生效,无需重新安装,因为 run 每次运行都会重新读取 whosaid.toml;install 的 --dry-run 摘要会显示将使用的提示。无效值(非整数、min_speakers 大于 max_speakers、名称为空)会导致 run 和 install 拒绝执行,并指出错误的键。

[diarize] 接受相同的四个键,并应用于对该工作区执行的每次 whosaid ingest --into(手动执行或由监视器执行),以及输出文件夹位于工作区内时的 MCP whosaid_transcribe。这些是默认值,而不是合并值:命令上的任何说话人标志(包括监视器自己的 [watch] 提示)都会替换本次运行的整组 [diarize] 设置。无效值会导致 ingest、MCP transcribe、run 和 install 拒绝执行,并指出错误的键。

重新安装会保留已保存的环境设置,包括自定义工具路径。当前导出的证书设置和显式指定的 --env 值可以覆盖已保存的值;--env 的优先级最高。在受管网络中,监视器可以使用组织安装的 macOS 信任根或 CA 证书包,而无需关闭证书验证:

whosaid watch install --into ~/meetings --no-fda --env UV_SYSTEM_CERTS=1
# If your organization supplies a PEM bundle instead:
whosaid watch install --into ~/meetings --no-fda \
  --env SSL_CERT_FILE=/path/to/organization-ca.pem --env UV_SYSTEM_CERTS=1

安装程序还会将导出的 SSL_CERT_DIR、REQUESTS_CA_BUNDLE、CURL_CA_BUNDLE 和旧版 UV_NATIVE_TLS 设置传递给 launchd。证书错误会保留原始错误,并提供以下设置指南。若要彻底丢弃已保存的环境设置,请先卸载监视器,再使用所需设置进行安装。卸载不会删除共享的用户本地工具和 CA 文件。

将完全磁盘访问权限限定到单个二进制文件。 语音备忘录存储受 TCC 保护:只有获授完全磁盘访问权限的可执行文件才能读取它,而且 macOS 按可执行文件路径授予权限。将权限授予终端或日常使用的 Python,会让它们运行的所有程序都获得特权。因此,install 会在 $HOME/.local/opt/whosaid-watch/bin/whosaid-watch 配置一个专用的临时签名解释器(命名方式可确保它在 FDA 列表中显示为该名称),让 Agent 使用它,并打开相应的系统设置面板。该解释器只运行监视器;它会从存储中复制每条录音,再将副本交给 whosaid,因此 whosaid、uv、ffmpeg 和模型读取的都是普通文件,无需获得权限。添加这一个路径是唯一需要手动执行的步骤。如果某个二进制文件已经获授权限,--interpreter PATH 会重复使用它,而不是配置新的解释器。多个工作区可以共存:标签默认为 com.whosaid.watch.<8 hex of the workspace path>,也可以传入 --label。

日志保存在 <ws>/.watch.log 中(launchd 会将监视器的标准输出和标准错误写入其中)。uninstall 会卸载并移除 Agent;--purge 还会移除专用解释器以及工作区的 .watch_state.json、.watch.lock 和 .watch_staging/,但绝不会删除日志。

没有管理员权限?

“完全磁盘访问权限”开关会要求输入管理员密码——即使 install 只为一个专用二进制文件授予权限——而在公司管理的 Mac 上,“隐私与安全性”面板可能会被完全隐藏,因此标准用户通常根本无法授予该权限。这里有一个无需管理员介入的替代方案:watch 无需完全磁盘访问权限即可读取 ~/Library 以外的任何文件夹,而 --no-fda 会将其设为默认模式(如果未设置 --source 且未设置 [watch] source,它会监视 ~/Recordings,并在需要时创建该文件夹):

whosaid watch install --into ~/meetings --no-fda   # watches ~/Recordings; no FDA, no admin

此模式会复用启动器的 Python,不会创建专门复制的解释器。安装 Voice Memos 的完全磁盘访问权限时,安装程序会选择支持复制虚拟环境的 Python。如果只有 Apple 的 Command Line Tools Python 可用,请使用 uv python install 3.12 安装兼容的本地用户解释器,然后重新运行;--interpreter PATH 仍可显式选择现有解释器。

将录音放入该普通文件夹有三种方式:

  1. iPhone 快捷指令“录制音频 -> 保存文件” -> Dropbox(唯一完全自动化的选项):在“快捷指令”应用中,将“录制音频”和“保存文件”操作串联起来,并将 Dropbox 文件夹(例如 whosaid)设为目标位置,然后将快捷指令绑定到操作按钮(设置 > 操作按钮 > 快捷指令)或轻点背面(设置 > 辅助功能 > 触控 > 轻点背面)。录音会同步到 Mac 上的 ~/Dropbox/whosaid。
  2. QuickTime Player(内置,无需安装):文件 > 新建音频录制,录音后停止,并保存到 ~/Recordings。
  3. 拖放:将备忘录从 Voice Memos 应用拖到 ~/Recordings,或者在 iOS 上使用“共享” > “存储到‘文件’” > Dropbox 文件夹。监视器会在下一轮扫描时像处理其他文件一样导入这些录音。

这里 Dropbox 比 iCloud Drive 更合适:~/Dropbox 是 ~/Library 以外的普通文件夹,而 iCloud Drive 在磁盘上的根目录(~/Library/Mobile Documents)与 TCC 权限机制相关,没有完全磁盘访问权限就无法确认是否可读。通过 iCloud Drive 同步的录音应用(例如 Just Press Record)也有同样的问题:它们的文件会存放在 ~/Library 下,因此无法供无完全磁盘访问权限的监视器使用——只有当 App Store 中的录音应用能将文件写入你选择的 ~/Library 以外文件夹时,才可作为替代方案。

无管理员权限的配置也能使用菜单栏图标,但有一个预期中的限制。SwiftBar 插件(whosaid watch menubar)显示的所有图标(🔴 REC 除外)都来自 watch status --json 和工作区的 .watch.log,无需授予权限——因此,空闲、等待、导入中、完成、警告、未加载等图标,下拉菜单中的链接和操作,以及状态转换通知,都能在 --no-fda 监视器上照常运行。🔴 REC 是个例外,原因有两个:

  • REC 按设计就是 Voice Memos 存储库状态的信号。“正在录音”表示存在一条没有同步文件且时长为 0 秒的存储记录;普通文件夹没有录音进行中的信号,因此即使已授予所有权限,--no-fda 监视器也永远不会显示 REC——没有红点是预期行为,并非故障。
  • SwiftBar 本身也需要单独授予“完全磁盘访问权限”,且同样必须由管理员授权。没有该权限时,插件的存储库探测会静默失败——REC 红点不会出现,下拉菜单会显示红色提示“SwiftBar cannot read the Voice Memos store”,并提供前往权限面板的链接和重新启动操作。菜单中的其他功能仍可正常使用。

完整说明(包括为什么新授予的权限需要重新启动),请参阅菜单栏图标。

菜单栏图标:whosaid watch menubar(SwiftBar)

whosaid watch menubar install [--workspace <ws>] [--interval 10s] 会将一个仅依赖标准库的 SwiftBar 插件(contrib/swiftbar/whosaid.10s.py)创建符号链接到 SwiftBar 的插件目录,让你一眼就能在菜单栏中看到监视器的状态;menubar uninstall 会移除插件,menubar status 会报告 SwiftBar 是否已安装并运行,以及配置的录音源是否可读取。whosaid doctor 会打印相同的报告。使用普通文件夹(如 ~/Recordings)的监视器无需单独授予“语音备忘录”访问权限。如果 SwiftBar 尚未设置插件目录首选项,安装程序会配置 ~/.config/swiftbar,并在链接插件前验证该首选项。现有的目录首选项会保留。安装后请刷新或重新启动 SwiftBar,以加载插件;首选项写入和符号链接创建成功,本身并不能证明应用已加载插件。

图标含义
🎙空闲,正在监视存储
🔴 REC正在录制备忘录
⏳ 90s新备忘录,正在等待文件稳定
⚙️ 3m正在转录 / 说话人分离 / 提取待办事项(已用时)
✅已摄取(保留 30 分钟,然后恢复空闲)
⚠️需要查看
🎙✗启动代理未加载

下拉菜单包含阶段日志尾部、代理状态行、工作区链接、最新的日期会议文件夹、每个 _WORKLIST-<Owner>.md 和 _WIKI.md,以及跟踪日志、立即触发监视器(launchctl kickstart -k)和刷新或重新启动 SwiftBar 的操作。

有两项该插件无法替你解决的平台问题:当配置的来源是“语音备忘录”时,读取其存储需要向SwiftBar 本身授予“完全磁盘访问权限”(监视器获得的授权是独立的——如果探测失败,插件会显示设置面板链接和重新启动操作,因为新授予的权限只有在重新启动后才会生效);在带刘海的显示器上,新的状态项可能会显示在刘海后面——运行 defaults write com.ameba.SwiftBar "NSStatusItem Preferred Position <plugin path>" -float <n> 并重新启动即可修复。

“语音备忘录”辅助工具:whosaid memos

whosaid memos list                          # titles and sync state (reads a COPY of the database)
whosaid memos pull --latest -o ./inbox      # copy the newest recording out of the store
whosaid memos pull --title "Standup" -o .   # or one by exact title
whosaid memos delete "Standup" --yes        # delete through the app's own action
whosaid memos shortcut-recipe               # build (or print how to build) the Shortcut delete uses

delete 不会接触 CloudRecordings.db 或 .m4a 文件。它会通过一个名为“Delete Voice Memo”的单操作快捷指令,运行“语音备忘录”的“删除录音”App Intent;这与在应用中点击“删除”完全相同:iCloud 会在你的设备间保持同步,备忘录会移至“最近删除”(可在 30 天内恢复),应用数据库也会保持一致。在应用不知情的情况下编辑存储会导致 iCloud 同步失常,并可能损坏资料库,因此没有会执行此操作的 --force 选项。如果可以签名,shortcut-recipe 会为你构建并签署该快捷指令;在没有 iCloud 账户的 Mac 上,它会打印单操作配方,供你在“快捷指令”应用中手动构建(--no-sign 只会打印配方)。

在自己的录音上对比设置:whosaid ab

whosaid ab standup.m4a --a "--speakers 2" --b "" --same-asr   # one transcription, two diarizations
whosaid ab --compare run-old/ run-new/                        # compare two existing output dirs

它会在说话人注册表的副本上运行两次 whosaid(不会学习或摄取任何内容),并报告说话人数量、发言占比、词语一致度、最大的分歧以及名称一致度,无需真实标注。详情请见:docs/ab-compare.md。

开发承诺

开发承诺是由自己角色的发言者向他人作出的一项第一人称承诺 (“我会在周五前发送迁移计划”)。每场会议的转录文本都会生成 commitments.md + commitments.json;汇总会将它们合并成一个持续更新的语料库, _COMMITMENTS.md / _commitments.json——这样,“我在整个系列中承诺了什么?” 就有了一个统一的答案。

提取采用标准库启发式规则,不使用大模型,也不联网:以第一人称提示语(i'll、i will、i plan to、let me、i owe 等)开头的从句会被记录;否定表达(i won't、i can't)会保留,但会标记为 negative;疑问从句则会跳过。 角色设置会影响提取:设置角色后,只有 self 发言者的提示语会被计入;如果承诺前一轮是另一位发言者在请求或指示(“can you…”、“please…”),该条目会记录 requested_by——当该发言者的角色为 boss 时,优先级为 high。可插拔钩子(在 commitments 子命令中使用 --hook CMD,或设置 WHOSAID_COMMITMENTS_HOOK)可替代启发式规则;它会通过 stdin 接收转录文本,以及 WHOSAID_SPEAKERS 和 WHOSAID_ROLES。

会被丢弃的内容。 仅有提示语并不构成承诺:“I'll do that”“I'll check”“I'll bring that up”都没有可执行内容(承诺的事项在前一轮中),因此只会让工作清单充斥没有对象、无法与真实条目去重的行。因此,提取器要求每个从句至少包含 [commitments] min_words 个实词(默认为 2)。实词是指移除代词(包括提示语本身)、限定词、助词、介词、助动词和填充词后的词:“I'll bump the version”有两个(bump、version);“I'll see what I can do”有一个;“I'll do that”则没有。如果条目带有 requested_by,或提及截止时间或紧急程度(today、tomorrow、this week、by Friday、urgent、blocking),一个实词就足够,因为请求已经提供了对象:在“Can you own the rollout?”之后,“I'll own it”会被保留。先进行从句清理:重复结巴的提示语会合并(“I'll I'll start investigating that while I”变为“I'll start investigating that”);从句会在从属连词处结束(while、because、if、when、unless、until、which 等:“I can create an Epic if needed”变为“I can create an Epic”);悬空的句尾代词或介词会被删去。设置 min_words = 0 可保留所有从句;也可以在 whosaid ingest 中传入 --min-words N,仅对单次运行生效。

查看接近门槛的内容。 被丢弃的从句会像低相似度合并项一样报告:每场会议的 commitments.md 末尾都会有一个 Dropped fragments (review) 小节(- (Alice_Example) I'll check @ 00:00:22 (fragment: 1 content word, min 2)),并在 commitments.json 中以 dropped 字段记录;提取器还会记录日志 dropped N fragment(s) (min_words=2)。汇总合并时也会应用同一规则,因此较旧的 commitments.json 文件不会将片段导入语料库;跳过的内容会列在 _COMMITMENTS.md 的 Dropped fragments (review) 小节中,并保存在 _commitments.json 中。如果其中出现了真实承诺,请在 whosaid.toml 中降低 min_words,然后重新运行(roll-up --rebuild 会重新合并每场会议)。

该语料库的工作方式与行动项语料库完全相同:使用稳定且不会重新编号的 CM-NNN ID,采用相同的 0.82 文本相似度去重规则和 0.10 的近似匹配复核区间,并在下一次汇总时合并对 _COMMITMENTS.md 的手动编辑。条目的渲染格式如下:

- **CM-002** [open] (Stephen) 2026-09-14-1802 → 2026-09-21-1802 (2×): **[boss]** update the exec deck
排序后的工作清单:_WORKLIST-<Owner>.md 和 whosaid commitments

语料库回答“我承诺了什么”;工作清单回答“我应该先做什么”。每次汇总(以及按需执行的 whosaid commitments <ws>)都会合并所有者的承诺(CM-NNN,发言者 = 所有者)和分配给他们的行动项(AI-NNN,所有者匹配时忽略大小写,且 _ 与空格可互换,并包括 [workspace] aliases),如果来源指针或显式 ID 引用表明两者等同,就将这两个来源合并(该项只出现一次,并标记为 (also AI-012)),然后按优先级排列未完成项。完全确定性,不使用 LLM:

层级规则
P1上级提出要求(priority high、请求者角色为 boss、[commitments] boss,或未记录角色时 [groups] leadership 中的姓名)、存在阻塞/紧急提示(blocking、urgent、asap、hotfix、outage、prod issue、release blocker、customer escalation,……)、存在尚未过期的截止日期提示(tomorrow、eod、by Friday、next week、ISO 日期,……),或在 3 次及以上会议中出现
P2在 2 次会议中出现、由任何人提出,或最新会议中出现强提示(i'll own、i will、i promise、i owe,……)
P3其余情况(i can、let me、i plan to 等弱提示不计分)

带有否定的承诺(i won't)绝不会是 P1,并会受到扣分。同一层级内,按分数(信号权重总和)、最近时间、ID 排序;每行都会说明原因:

- **CM-002** [open] 2026-09-14-1802 → 2026-09-21-1802 (2×) P1 · boss · due=tomorrow · 2 meetings: update the exec deck (also AI-012)

三条规则可确保提示判断准确(GitHub issue #21):

  • 否定。 如果提示词前 3 个词内、且位于同一分句中出现否定词,则该提示不生效:“send non-urgent questions to the channel”、“not blocking anyone”、“no rush”、“isn't really critical”、“won't block us”均不计入;而“not done yet, this is urgent”仍然计入(逗号会开启新分句)。否定词列表为 [commitments] negators(默认值为 not、no、non、never、isn't、aren't、wasn't、won't、don't、doesn't、didn't、without、nothing、hardly,以及转录文本可能生成的不带撇号拼写形式)。
  • 单独出现的名词不代表紧急情况。 默认情况下,prod、production、release、ship、customer(s) 不属于阻塞提示:“post it in the release chat”和“shipping version 2.5”都只是普通工作。默认值使用的是短语(prod issue、production is down、release blocker、blocking the release、before we ship、customer escalation、customer is waiting,……)。如果工作区中这些单独的名词确实代表紧迫事项,可通过 [commitments] blocking_cues 将其加回来(你设置的列表会取代默认列表,因此请保留你仍需要的条目)。
  • 相对截止日期会过期。 today、tonight、eod、tomorrow、this week、eow、next week、by Friday、before the 14th、on sept 3、by 9/20 和 ISO 日期,都会根据该事项最近一次出现的会议日期进行解析(this week 指该周的星期五,若当天是周末则指下一个星期五;next week 指再下一周的星期五;the 14th 若已过期则顺延至下个月)。在解析出的日期仍未到来时,该行会显示 due=<cue>,截止日期也会计入 P1;日期一旦早于今天,该行就会显示 overdue=YYYY-MM-DD,较低的 overdue 权重(默认值为 1)会取代 deadline,仅凭截止日期不再属于 P1,因此,两周前提到的“today”会排在今天早上的实际工作之后。没有明确日历日期含义的提示(this sprint、before the demo、before the release)永不过期。设置 WHOSAID_TODAY=YYYY-MM-DD 可为测试和回放固定“今天”的日期。 已解决并合并的事项列在 ## Done / history 下。该文件是重新生成的视图(手动编辑会在下次运行时被覆盖;ID 永不重新编号;JSON 语料库仍是唯一事实来源),因此请在 _COMMITMENTS.md / _ACTION-ITEMS.md 中关闭或重命名事项。负责人由 --owner NAME 指定,也可使用 --owner me(默认值):即最新会议的 commitments.json 或 # Role: 标头中角色为 self 的发言者;否则使用 [workspace] owner / WHOSAID_OWNER。 --all-owners 会为每位参与者写入一个文件。whosaid commitments <ws> --json 会为脚本和 MCP 工具输出 {owner, generated_from, items: [{id, source, text, status, tier, score, why, first_seen, last_seen, occurrences, requested_by, negative, …}]}。提示词列表、负责人姓名、权重和嵌入阈值均可在 whosaid.toml [commitments] 中覆盖(参见搜索会议下的示例)。
证据、里程碑和领导层优先级

可选的工作区 _tracking.json 台账会将已审查的证据与生成的语料库分开记录。重新索引时会保留该台账。事项、人员和工作清单视图使用相同的状态历史、里程碑状态和已接受的所有权。合并的更改可以完成编码里程碑,但部署仍可能尚未完成。缺少证据并不表示状态为 not_started。

whosaid tracking import ./meetings ./reviewed-events.json
whosaid commitments ./meetings --status delivered --json
whosaid commitments ./meetings --accepted-only --as-of 2020-01-31 --json
whosaid tracking priorities ./meetings --group leadership --as-of 2020-01-31

义务类型用于区分明确承诺、请求、长期指令、建议和推断的后续步骤。接受义务需要有明确归属的证据;仅分配任务并不足够。长期指令和推断任务会单独显示,且不会累积逾期分数。精确的源指针可以将摘要事项与提取出的承诺对应起来,而不凭空生成第二项承诺。指针含糊时,相关事项会分开保留。

优先级断言会保留发言者、范围、证据和日期。明确排名会与推断排名区分开来;不同领导者之间相互冲突的断言会继续显示。请求的交接会一直处于待处理状态,直到被接受。历史视图依据事件发生时间和当前配置的群组成员身份,而非还原历史组织架构。

导入的消息会保留所有来源引用;已确认的重叠副本在搜索结果和重复频率评分中只计一次。含糊的对话匹配需要明确的本地别名。请参阅消息身份和跟踪事件格式及示例。

MCP 客户端可以调用 whosaid_priorities 和 whosaid_tracking_suggestions。 建议会在截止时间内扫描本地索引,且必须经过审核才能导入;它们既不会验证外部工件,也不会自动更改状态。现有事项/工作清单工具接受 kind、accepted_only 和 as_of 参数。省略分页上限时,响应上限均为 50,包括仅指定偏移量的调用。精简行仍会保留状态、类型、接受情况和证据可用性。快照令牌和缓存新鲜度会涵盖语料库、台账、配置、转录文本及说话人分离侧车文件的内容变化,包括来源的新增和移除。没有来源指纹的旧缓存仍可读取,但会标记为过时。历史查询不能用当前缓存答案替代。实时工作清单使用来源身份,不会探测或调用嵌入模型。

在 AI Agent 中使用(MCP)

whosaid 的本地私有 GPU 转录和说话人分离功能也以 MCP 工具的形式提供,因此任何 MCP 客户端——Claude Code、Claude Desktop 等——都可以直接调用这些功能,无需通过命令行调用。通过 whosaid mcp 启动的 stdio 服务只需要 uv,而 whosaid 本身已经依赖 uv;与命令行工具一样,音频绝不会离开本机。

将其添加到 MCP 客户端配置中:

{
  "mcpServers": {
    "whosaid": { "command": "/ABSOLUTE/PATH/TO/whosaid", "args": ["mcp"] }
  }
}

command 是 whosaid 脚本本身的路径——代码检出目录中的 ./whosaid,或已安装的 ~/.local/bin/whosaid 符号链接。

工具功能
whosaid_transcribe转录音频文件并进行说话人分离,写入带标签的转录文本、说话人卡片和用于重新标注的辅助文件。
whosaid_relabel为 SPEAKER_NN 集群命名并记住名称——保存到本地注册表,并自动应用于所有后续转录。可选的 roles({Name: role})用于为说话人添加角色标签(自己/老板/同级/下属/外部人员)。
whosaid_list_speakers只读:列出已登记的声音和已知的注册表名称(含角色标签)。
whosaid_doctor只读就绪检查——检查模型是否已缓存、依赖项是否齐全,以及麦克风/音频设备是否可用;转录失败时请先运行此工具。
whosaid_enroll_from_file从现有音频片段登记指定说话人的声音,无需麦克风。
whosaid_samples为每个说话人集群导出一个具有代表性的短 WAV 文件(选取最长片段,并将时长限制在 seconds 范围内),以便你在信任标签前试听并确认身份。
whosaid_reprocess在工作区中重新处理较早的会议并进行说话人分离(封装 whosaid reprocess,复用现有 ASR)。此操作会修改数据,但 dry_run 默认为 true:只报告计划,不会进行任何更改,除非你传入 dry_run=false。
whosaid_record_browser通过 DevTools 端口录制浏览器会议标签页。action 可以是 start、stop 或 status。不是只读操作。需要使用 --remote-debugging-port 启动浏览器;无需屏幕录制权限。通话结束时会自动停止。

enroll 和麦克风/应用录音 record 仍只能通过命令行使用。它们需要交互式终端和麦克风权限。首次调用 whosaid_transcribe 时会下载约 1.5 GB 的模型;请先调用 whosaid_doctor 检查是否就绪。

工作区搜索工具(只读)

工作区搜索层也已开放,因此 Agent 可以直接根据转录回答“我们对预算做了什么决定”,而不必通读整份转录。每个工具都接受可选的 workspace 参数;否则使用服务环境中的 WHOSAID_WORKSPACE。不会回退到当前目录。它们都不会重建索引:写入由 whosaid index(或监视器)负责,工具读取索引、语料库以及可选的跟踪台账。

工具功能
whosaid_search按查询返回轮次级命中结果(会议文件夹、时间戳、说话人、片段),支持 exact、meaning 或 hybrid 模式,并可按说话人/会议筛选。
whosaid_context返回某个命中项周围的原文轮次,让 Agent 阅读一分钟内容,而不是整份转录。
whosaid_items按负责人、请求者、状态、类型、种类、已接受的责任归属或历史日期筛选。按数字 ID 顺序排列,默认每页 50 条,并提供计数、续传偏移量和快照令牌。精简行保留状态和证据可用性。
whosaid_item返回一条项目及其出现记录和带时间戳的承诺。
whosaid_person返回某人的发言占比、所做承诺以及收到的请求。
whosaid_meetings返回所有会议文件夹及其覆盖情况。
whosaid_prs提取在发言或笔记中提及的拉取请求编号。
whosaid_speakers返回整个工作区中每位说话人的发言占比。
whosaid_workspace_status显示索引是否存在、包含多少轮次,以及 Ollama 是否可访问。
whosaid_worklist对承诺进行排序和协调,可按层级、种类、状态、接受情况和历史日期筛选。默认每页 50 条。超时信息包含已用时间和阶段;当前缓存会报告生成时间和新鲜度。use_cache=true 直接读取缓存。历史查询需要实时协调。
whosaid_priorities返回当前或历史优先级声明,可按已配置的领导层组筛选,并保留证据和冲突。
whosaid_tracking_suggestions在有限范围内本地搜索后续完成证据。返回需要审核的候选项和限制;绝不会更改状态。

资源均读取 WHOSAID_WORKSPACE:whosaid://workspace/wiki(_WIKI.md)、 whosaid://workspace/action-items(_ACTION-ITEMS.md)、whosaid://workspace/index (_INDEX.md),以及每场会议的 whosaid://workspace/meeting/{folder}/transcript 和 whosaid://workspace/meeting/{folder}/action-items。

使用设定好的工作区注册它。Claude Code:

claude mcp add --scope user whosaid -e WHOSAID_WORKSPACE=$HOME/meetings -- whosaid mcp

Kiro(~/.kiro/settings/mcp.json)以及任何接受相同 JSON 格式的客户端:

{
  "mcpServers": {
    "whosaid": {
      "command": "whosaid",
      "args": ["mcp"],
      "env": { "WHOSAID_WORKSPACE": "/Users/<you>/meetings" }
    }
  }
}

将工作区写为绝对路径(大多数客户端不会在 env 中展开 $HOME);如果 whosaid 脚本不在客户端的 PATH 中,则将 command 设置为该脚本的绝对路径。

主要标志(用于 whosaid <audio>…)
标志含义
-o, --outdir DIR输出目录(默认:与输入文件位于同一目录)。
-m, --model NAME要使用的 Whisper 模型。
--accurate使用完整的 large-v3 模型(默认;保留此选项以供脚本使用)。
--fast本次运行使用 large-v3-turbo(速度更快,但处理困难音频时稳定性较低)。
-l, --lang LANG指定转录语言。
-f, --format FMT输出格式:txt、srt、vtt、tsv、json 或 all。
-n, --name NAME覆盖输出的基本名称(仅限单个输入;默认:根据输入文件名生成)。
--speakers N指定确切的说话人数。覆盖自动检测结果(以及任何 --min/--max-speakers 设置)。
--min-speakers N自动检测说话人数的下限。
--max-speakers N自动检测说话人数的上限;同时降低 20 人的硬性上限。如果你大致知道在场人数,但不确定确切人数,请使用一个范围——若估算结果落在范围边界上,会被报告为不可靠,而不会作为结果输出。
--expected-speakers A,B,C列出你预期会在这段录音中出现的人(以逗号分隔,可重复使用)。每个名字都必须是已知声音——已登记的片段或注册表条目。聚类会锚定到这些人的声纹:与其中一人的相似度达到 --anchor-threshold 的话轮会被分配给此人,其余话轮会聚类为新的说话人,而列表中从未发言的人会被移除。对于出席人数不固定的固定团队,应使用此标志;使用 --speakers N 可能会将不同的人合并。
--anchor-threshold F--expected-speakers 将话轮分配给已知声音所需的余弦相似度。默认值为 0.70;低于此值的话轮会转由普通聚类处理,而不会被赋予置信度较低的名字。
-j, --jobs N长音频的并行说话人分离工作线程数(默认:自动,约为核心数减 2,最多 8 个)。
--chunk-seconds S并行说话人分离的窗口长度(默认:自动——约为 --jobs 个窗口,最短 300s)。
--no-chunk使用 sherpa 自带的聚类处理整个文件(不并行分块,也不进行人数估算)。
--match-threshold F已知声音认领某个聚类所需达到的余弦相似度(别名为 --ref-threshold)。默认值为 0.50;若某个聚类与最佳候选的相似度低于 F,则保留匿名标签 SPEAKER_NN,而不会赋予置信度较低的名字。如果出现错误姓名,请调高此值(例如 0.6);如果想匹配更多声音,则调低此值。
--absorb-threshold F尚未命名的聚类并入已知声音所需的余弦相似度,用于合并同一人的虚假拆分。默认值为 0.85。适用于未分配到本次会议的声音(参见 --placed-absorb-threshold)。
--placed-absorb-threshold F对已分配到本次会议的声音(即已拥有一个聚类)使用较低的合并阈值:当某个尚未命名的聚类的大部分发言时间最接近该声音(以秒数加权的 talk_share 门限,#77),且该聚类与该声音自身聚类的相似度比与其声纹的相似度高 0.035(sibling_sim)时,会以余弦相似度 F 将其并入该声音。此功能需要逐话轮声纹,因此 relabel --auto 和 --no-chunk 会回退到 --absorb-threshold。默认值为 0.70。
--no-diarize跳过说话人分离;仅写入纯转录文本。
环境变量
变量用途
WHOSAID_PYTHON用于辅助脚本的 Python 3.11+ 解释器(默认:自动检测,参见快速入门)。
WHOSAID_MODEL默认 Whisper 模型,可由 -m 覆盖。
WHOSAID_LANG默认转录语言,可由 -l 覆盖。
WHOSAID_VOICE_REFS覆盖注册语音片段目录(默认:voices/)。
WHOSAID_SPEAKER_DB命名声纹的本地说话人注册表(默认:~/.config/whosaid/speakers.json)。私有,绝不推送。
WHOSAID_MATCH_THRESHOLD默认注册表/参考匹配阈值,可由 --match-threshold 覆盖(默认:0.50)。
WHOSAID_ABSORB_THRESHOLD默认吸收阶段阈值,可由 --absorb-threshold 覆盖(默认:0.85)。
WHOSAID_PLACED_ABSORB_THRESHOLD对已在会议中定位的语音使用的默认吸收阈值,可由 --placed-absorb-threshold 覆盖(默认:0.70;受发言占比门控)。
WHOSAID_ANCHOR_THRESHOLD--expected-speakers 的默认逐轮锚定阈值,可由 --anchor-threshold 覆盖(默认:0.70)。
DIARIZE_EMB_NAME说话人嵌入模型。默认是 NeMo nemo_en_titanet_small.onnx(以英语为母语,速度约为 sherpa 基准测试中 ERes2Net 的 2.5 倍)。同一版本的替代模型:3dspeaker_speech_eres2net_sv_en_voxceleb_16k.onnx(英语 ERes2Net)或用于普通话的 …_zh-cn_…。注册表声纹按模型关联,因此切换模型后需重新注册说话人。
WHOSAID_REC_DEVICErecord 和 enroll 使用的 avfoundation 输入设备。
WHOSAID_INSTALL_DIRwhosaid install 使用的命令安装目录(默认:~/.local/bin)。
WHOSAID_ACTION_ITEMS_HOOKingest --action-items 的默认行动项钩子(通过标准输入接收转录文本,通过标准输出输出 Markdown),可由 --hook 覆盖。
WHOSAID_WORKSPACEindex、search、context、graph、wiki、watch 的默认会议工作区;也用于 whosaid doctor 的索引状态行,以及 whosaid mcp 的只读工作区工具和资源。
WHOSAID_OWNER工作区跟踪谁的行动项;可覆盖 whosaid.toml 中的 [workspace] owner。
WHOSAID_TODAY工作清单在判断相对截止日期(today、tomorrow、by Friday,等等)是否逾期时使用的当天日期,格式为 YYYY-MM-DD(默认:系统时钟日期)。
WHOSAID_OLLAMA用于嵌入和内置摘要器的 Ollama 基础 URL(默认:http://127.0.0.1:11434)。仅支持 localhost 作为目标地址。
WHOSAID_SUMMARIZER_MODEL--engine ollama 使用的 Ollama 模型(默认:qwen2.5:14b);可覆盖 [summarizer] model。
WHOSAID_CLAUDE_BIN当 [summarizer] claude_bin 未设置时,--engine claude 使用的 claude 二进制文件(默认:PATH 中的 claude,否则为 ~/.local/bin/claude)。
WHOSAID_BIN监视器运行的 whosaid 命令(默认:启动 whosaid watch 的脚本)。
WHOSAID_COMMITMENTS_HOOK开发承诺提取的默认钩子(ingest --commitments),可在每次运行时由 --hook 覆盖。接收标准输入中的转录文本,以及 WHOSAID_SPEAKERS/WHOSAID_ROLES。
HF_HOMEHugging Face 缓存位置(Whisper 模型存放位置)。
SHERPA_DIARIZE_CACHE说话人分离模型缓存位置(默认:~/.cache/sherpa-diarization)。

说话人身份:注册语音片段与注册表

whosaid 有两种相互独立的方式,可为 SPEAKER_NN 簇指定姓名。了解某个标签由哪种方式决定很重要——这是 GitHub issue #1 中提到的另一个疑惑。 1. 注册音频片段(voices/、WHOSAID_VOICE_REFS)。 目录中的每个 *.wav/*.m4a 文件都会在每次转录时重新生成嵌入,并以 --ref Name=path 的形式传给说话人分离器(在每次调用 diarize 前运行 whosaid 脚本中的 --ref "$refname=$ref" 循环)。使用 whosaid enroll [Name] 录制(通过麦克风,或使用 --from FILE 从现有录音中截取片段)。由于每次运行都会重新生成片段嵌入,即使更改 DIARIZE_EMB_NAME,也无需额外操作:嵌入始终与当前启用的模型保持一致。

2. 注册表(~/.config/whosaid/speakers.json、WHOSAID_SPEAKER_DB)。 JSON 文件,包含 {name, model, embedding, added} 记录。使用 whosaid relabel SPEAKER_XX=Name(或转录时使用 --save-speaker)写入一次,此后可一直复用,无需重新生成嵌入。每条记录都按保存时使用的嵌入 model 进行索引,因此更改 DIARIZE_EMB_NAME 会让旧模型对应的所有注册表记录悄然失效——不会报错,只是不再匹配——直到你使用新模型重新标注。

角色标签。 注册表记录可以包含可选的 "role" 字符串;常用标签包括 self、boss、peer、report 和 external,也允许使用自定义的小写标签。可通过 whosaid relabel <base> --role Karen=boss(可重复使用)或 MCP whosaid_relabel 工具中的 roles 映射来设置。角色会存储在注册表记录中,在重新保存声纹时予以保留,并以 <base>.speakers.txt 中的 # Role: Karen = boss 标题行、说话人卡片上的 Karen [boss] 标签,以及 sidecar 中的顶层 "roles" 键呈现。角色会影响下游语义:self 表示你自己的声音,而角色为 boss 的说话人的请求在行动项和 dev-commitments 语料库中的优先级更高。

将注册表更改应用到历史会议。 重新标注会议只会更新该会议;要让现有工作区与当前注册表保持一致,请使用回填:

whosaid backfill ~/meetings --dry-run   # inspect per-meeting changes and conflicts
whosaid backfill ~/meetings             # apply, then refresh all derived views

回填会使用缓存的说话人分离 sidecar,不会再次转录音频。它会保留转录正文和仅转录文件中的标签,更新说话人角色标题和卡片,并重新生成启发式承诺。写入会议文件前会报告承诺中的冲突编辑;请先协调这些编辑,再重试。语料库整理内容(包括经过编辑的承诺文本和状态)会在源数据刷新后保留。汇总会为每场会议的承诺输入和角色生成指纹,仅重新汇总指纹发生变化的会议(或新增、删除的会议),同时保留 ID 和整理内容;更改 whosaid.toml 中的 [commitments] 配置会重新汇总所有会议。更改注册表角色后,也请运行回填,以更新每场会议的输入。

备份或迁移注册表。 两个命令都会遵循 WHOSAID_SPEAKER_DB,且无需加载模型:

whosaid speakers export --out ./speakers-backup.json
whosaid speakers import ./speakers-backup.json          # destination must not exist
whosaid speakers import ./speakers-backup.json --merge  # add non-conflicting entries

导出会以带版本的格式保留名称、模型标识符、嵌入、角色、备注及其他注册表元数据。导入会在写入前验证整个文件,并拒绝合并冲突。如需有意替换目标端的所有记录,请使用 --overwrite;此操作也会移除备份中不存在的声音。若输出路径已存在,导出会拒绝执行。注册表写入采用原子操作,备份和导入文件仅所有者可访问。这些文件包含私有声纹:请勿将其提交到源代码管理中,在设备间迁移时请使用私密的加密传输方式。voices/ 中的注册音频与注册表备份相互独立。 究竟是哪种方式给聚类命名。 name_clusters()(lib/diarize_sherpa.py)按顺序运行三个阶段,后续阶段只会处理前面阶段未命名的聚类:(1)注册表最佳匹配,(2)--ref 注册音频片段,(3)吸收(将仍未命名的聚类并入一个已知声音——来自注册表或音频片段——且相似度高于 --absorb-threshold)。阶段 1 和 2 都会检查 --match-threshold(默认值为 0.50,环境变量为 WHOSAID_MATCH_THRESHOLD):低于该阈值时,聚类会保留匿名标签,而不是采用置信度较低的名称。两道关卡(#77)可避免缺席的声音被匹配:注册表或参考音频中的声音不能认领与某个已出现在会议中的声音得分相同或更高的聚类(报告中会记录 matched: false 和 blocked_by);吸收阶段只有对已出现的声音,才可使用较低的 --placed-absorb-threshold(0.70),且只有当该聚类至少 60% 的发言时长(按秒加权)都来自最接近该声音的轮次(决策字段为 talk_share;仍会记录较早的逐轮 purity 以供比较),并且该聚类与此录音中该声音自身聚类的距离至少比与其声纹的距离近 0.035(sibling_sim)时才可使用。这样一来,仅仅听起来像已注册声音的未注册说话者仍会保持匿名;其他所有声音仍需满足 --absorb-threshold(0.85)。

逐项检查。 ls voices/ 会列出注册音频片段;whosaid doctor 会打印注册表中的声纹数量和路径;MCP whosaid_list_speakers 工具会同时列出两者。<base>.diarization.json 中的 registry_matches 会记录每个聚类是由哪个阶段命名(或接近命名),因此你可以准确判断触发了哪种机制;而 whosaid samples <base> 可让你试听每个聚类的音频片段,在信任标签之前进行确认。

仅用于转录文本的标签(relabel --no-save)。 为聚类命名不一定要改写注册表。whosaid relabel <base> SPEAKER_04=Alice_Example --no-save [--note TEXT] 只会重命名转录文本和卡片中的聚类,绝不会写入注册表;当对话内容能明显判断说话者身份,但该聚类并不代表一个清晰一致的声音时,这正是你需要的做法。这就是 GitHub issue #19 所述的情况:在一场有 7 人参加的会议中,根据发言可以认出 SPEAKER_04 是 Alice,但她的聚类包含 176 个轮次,且混有多个声音;若保存该聚类,就会用质量较差的声纹替换她已注册的声纹。标签会存储在旁文件的 local_labels 中(可选的 --note 也会一并保存),因此在运行 relabel --auto 和 whosaid samples 后仍会保留;每张说话者卡片都会显示 Alice_Example (transcript-only label; registry untouched),因此标签的来源始终一目了然。同一决策另一侧的相关保护机制:现在,写入注册表的路径(带有指定项的 relabel、transcribe --save-speaker)会拒绝替换与新聚类相似度低于匹配阈值(默认值为 0.50)的现有声纹——传入 --force 可有意覆盖,或传入 --no-save 仅为当前转录文本添加标签。

增强声纹(relabel --blend)。 普通的 relabel 会用这一个聚类替换已保存的声纹,因此,根据一对一通话建立的声纹,在不同录制条件下的群组会议中,可能无法很好地匹配到同一个人。whosaid relabel <base> SPEAKER_00=Alice_Example --blend 会将该聚类平均融合进现有声纹(取两个单位向量的均值,再重新归一化),并在条目的 added 字段中将来源记录为 <previous>+blend:<base>。上述相似度保护机制仍然适用。若没有现有声纹,则会原样保存该聚类。

工作原理

 audio file
     |
     v
 MLX Whisper (Metal GPU)  ------------->  <base>.txt / .srt / .vtt / .tsv / .json
     |
     v
 sherpa-onnx diarization (CPU)  -------->  <base>.rttm
     |
     v
 cosine-match vs voices/*.wav  --------->  <base>.speakers.txt

转录和说话人分离作为两个独立的本地阶段运行,并在最后合并。 转录默认使用 MLX Whisper(完整的 large-v3、mlx-community/whisper-large-v3-mlx,或使用 --fast 时的 large-v3-turbo),通过 Metal 利用 Mac 的 GPU,并采用经过幻觉加固的解码路径:温度回退阶梯保持启用, condition_on_previous_text 设为关闭状态,幻觉静音阈值则可避免将静音误转为重复词填充内容。说话人分离 通过 sherpa-onnx 离线分离在 CPU 上运行:pyannote 的 segmentation-3.0 ONNX 模型检测说话人及其发言时间, 说话人嵌入模型(默认使用 NeMo TitaNet-small,可通过 DIARIZE_EMB_NAME 配置)为每段发言生成嵌入, 然后聚类(可通过 --speakers N 提供提示)将发言归组为说话人。两个说话人分离模型都很小(总计约 30 MB), 来自无需访问控制的 GitHub 发布版本——不需要 Hugging Face 令牌——并缓存在本地的 ~/.cache/sherpa-diarization/ 中。

回退到 turbo。 默认模型已从 large-v3-turbo 改为 large-v3(词错误率更稳定,长会议的转录时间约为原来的 1.85 倍)。 如需恢复 turbo,可在单次运行时传入 --fast,或设置 WHOSAID_MODEL=mlx-community/whisper-large-v3-turbo 使其成为持久设置(WHOSAID_MODEL 一直用于覆盖默认模型;显式指定的 -m、--fast 或 --accurate 优先级更高)。

最后,voices/ 中的每段音频,以及本地注册表中的每个声纹,都会以相同方式生成嵌入,并通过余弦相似度与某个聚类匹配(匹配分数达到或超过 --match-threshold 时,默认值为 0.50,该聚类便会获得名称); 未匹配的聚类则保留 SPEAKER_00 这类标签。除首次下载模型外,所有操作都在临时的 uv 环境中运行,因此不会在你的机器上留下持久安装的 Python。

告知它预期会有哪些人。 如果你已经知道与会者名单,--expected-speakers Alice,Bob,Carol 会将说话人分离从猜测变成查找。名单中的每个人都必须是已知语音(voices/ 中已注册的音频片段,或注册表条目),之后聚类便会以这些人员为锚点:将每段发言的声纹与每位预期人员的声纹进行比较,余弦相似度 ≥ --anchor-threshold(默认 0.70)的发言会直接归到对应人员名下;只有剩余发言——来宾、陌生人,以及偶尔评分较低的发言——才会进入常规人数估算和聚类。名单上从未发言的人会被静默忽略,因此每周参会人员不同的固定会议也可以使用同一份名单。这与 --speakers N 的区别在于:指定精确人数时,只要实际出席人数少于 N,就会把两位不同的人合并;自动检测则会过度分段,之后还可能无法匹配某位主要说话人的平均聚类。阈值特意设得较高——TitaNet-small 对同一说话人不同发言的评分约为 0.90(第 10 百分位约为 0.72),而对陌生人的评分约为 0.25(第 90 百分位约为 0.45);错误锚定会把真实姓名标到错误的话语上,而漏掉锚定只会让该发言进入聚类,吸收阶段仍有机会找回它。每个锚点的结果(turns、mean_cosine)会写入 sidecar 的 anchors 下,每个锚定聚类则会写入 registry_matches,并带有 pass: "anchor"。由于锚定需要逐段发言的声纹,此标志会在任何录音时长下强制启用分块说话人分离路径。 长录音并行处理。 对于超过约 15 分钟的音频,分说会将其拆分为不重叠的时间窗口,并在多个 CPU 工作线程上并发分段和嵌入;随后对所有片段的声纹执行一次全局聚类,以找回跨窗口边界保持一致的说话人。由于聚类会一次性查看所有片段,因此能找回与单次处理相同的说话人,同时速度快数倍。较短的录音会在一个窗口内使用相同的逐片段聚类方式,因此说话人数估算器以及 --min-speakers/--max-speakers 可用于任意时长的录音。传入 --no-chunk 可改用 sherpa 的整文件聚类,或传入 --jobs/--chunk-seconds 进行调整。

输出文件

文件内容
<base>.txt纯文本转录稿。
<base>.srtSubRip 字幕。
<base>.vttWebVTT 字幕。
<base>.tsv带时间戳的制表符分隔片段。
<base>.json完整的 Whisper 片段输出。
<base>.rttm原始分说片段,采用标准 RTTM 格式。
<base>.speakers.txt带说话人标签的转录稿:将 Whisper 文本与分说片段及登记姓名合并;当说话人带有角色标签时,包含 # Role: NAME = ROLE 标头行。
<base>.speaker-cards.txt每位说话人对应一张卡片,包含片段数、发言时长和代表性片段——阅读此文件以识别每个 SPEAKER_NN,然后使用 whosaid relabel 为其命名。
<base>.diarization.json缓存的片段和各聚类的声纹,因此 whosaid relabel 无需重新分说即可重命名并保存说话人。还包含 registry_matches、source(见下文),以及从 v1.11.0 开始包含 whosaid_version;whosaid reprocess 会利用它跳过无需重新分说的会议。
<base>.samples/按需由 whosaid samples <base> 创建:每个说话人聚类对应一个简短的代表性 WAV 文件(SPEAKER_NN[-Name].wav),方便在信任自动标签前快速试听。

会议工作区(whosaid ingest --into <ws>)会在工作区根目录添加以下文件。带下划线的文件由程序生成,可以安全删除;点文件是监视器的工作状态。

文件内容
<ws>/YYYY-MM-DD-HHMM/一场会议:源音频、上述 transcript.* 输出,以及按需生成的 action-items.md / action-items.json。
_workspace.json清单:每场已导入会议对应一条记录,包含其源文件 sha256(用于实现导入幂等性)。
_INDEX.md覆盖情况索引和完整性审计,由 whosaid roll-up 生成。
_ACTION-ITEMS.md、_action-items.json去重后的行动项集合及其状态(稳定的 AI-NNN ID、状态、出现次数)。可手动编辑。
_search.dbSQLite:包含所有片段的 FTS5 索引、存储的嵌入向量和实体关系图表。由 whosaid index 重建;可随意删除。
_WIKI.md生成的 Wiki,由 whosaid index 和 whosaid wiki 重新生成。
whosaid.toml可选的工作区设置(所有者、群组、摘要器、搜索、监视)。这是此处唯一需要手动编写的文件。
.watch_state.json、.watch.lock、.watch_staging/、.watch.logwhosaid watch 状态:已处理的录音、重叠保护、从 Voice Memos 存储暂存出来的副本,以及 Agent 日志。

sidecar 中可供机器读取的附加数据,因此使用方无需抓取 stderr 或调用 ffprobe:

Sidecar 键内容
registry_matches每项命名判定对应一条记录,包括相似但未匹配的情况:{"cluster": "SPEAKER_03", "name": "Alice", "similarity": 0.919, "threshold": 0.5, "matched": true, "pass": "registry"}。pass 的值为 registry、ref、absorb 或 anchor;matched: false 表示该判定未能识别出该声音;被边界差值门槛拒绝的 registry/ref 记录还会带有 blocked_by(已分配的名称);合并到已分配声音的 absorb 记录还会带有 talk_share(判定字段:在 best 为候选声音且 score >= 0.40 的轮次中,该集群的轮次发言时间占比,按秒数加权;达到 >= 0.60 时视为匹配;无法获取片段时,每轮权重为 1)、sibling_sim(该集群与候选声音在本次会议中已拥有的集群之间的最高余弦相似度;还必须满足 >= similarity + 0.035;没有可用的所有者质心时为 null,此时拒绝匹配)、runner_up / runner_up_share(在该集群的发言时间中获胜最多的其他已知声音,按 talk_share 的方式加权;如果没有则为 null / 0),以及 second_placed / second_placed_sim(按质心余弦相似度在本次会议中排在下一位的声音;如果没有则为 null);runner_up_any / runner_up_any_share / runner_up_any_sim(相同的 runner-up,但本次会议中其他匿名集群也参与竞争,另包含获胜集群与候选声音之间的质心余弦相似度;已知声音获胜时为 null);runner_up_placed / runner_up_placed_name / runner_up_placed_share / runner_up_placed_sim(相同的 runner-up,但已命名给其他声音的集群也按其在本次会议中的质心参与竞争;_name 是所有者声音);runner_up_placed_alone / _name / _share / _sim(相同的计算方式,但 ONLY 让这些已分配集群的质心参与竞争,不使用声纹,因此未在本次会议中出现的已登记声音无法胜出);以上内容均会记录下来用于调整混合判定,不作为门槛(#77);以及 purity(记录下来用于比较:该集群中判定一致的轮次占比),还有 turns,即按时间顺序排列的逐轮输入(start、end、与候选声音的 score、最佳已知声音 best 及其 best_score,分数保留 4 位小数;无法获取时 start / end 为 null),以便离线调整门槛:purity 是满足 score >= threshold 且 best 与记录中的 name 相同的轮次占比。匿名集群合并后,cluster 指向保留的集群;可选的 original_cluster 则记录提供相似度证据的原始集群。由 whosaid relabel --auto 更新,也会打印在转录 JSON 行中。
source录音来源信息:{"path": "/abs/path.m4a", "duration_seconds": 1834.2, "creation_time": "2026-09-14T18:02:11.000000Z"}。creation_time 是容器标签;如果文件中没有该信息,则为 null。
roles可选 — 仅当至少有一位发言者带有登记角色时才会出现:{"Karen": "boss"}。下游使用方(例如承诺事项提取器)会读取此字段。
uncounted["SPEAKER_NN", ...](没有时为空):总发言时间不足 1 秒的匿名集群(无名称)。这些集群不会计入 num_speakers,也不会出现在发言者卡片或转录中的 # Speakers (N) 标题内(会另用 # Uncounted 行列出);它们也不是工作区参会者;其发言仍会以 SPEAKER_NN 标签保留在转录中,且 relabel SPEAKER_NN=Name 仍然有效(已命名的集群始终计数)。relabel --auto 会对现有会议重新应用此规则。
count_before_fold、count_after_fold、fold_note匿名伪影检查前后的实际发言者集群数量,以及运行检查时的说明。数量相同且 fold_note: null 表示未执行合并。这些数量并不代表其余发言者已被识别。对于较早的 sidecar,这两个数量均读取其观测到的 num_speakers。
在会议工作区中,ingest --commitments 还会为每场会议写入
commitments.md / commitments.json,roll-up 会将其整合到工作区级别的
_COMMITMENTS.md / _commitments.json 语料库中——参见 Dev-commitments。

匹配置信度与阈值。 只有当集群与已知声纹的余弦相似度达到 --match-threshold(默认值为 0.50)时,自动命名才会确认名称;低于该值时,集群会保留其 SPEAKER_NN 标签——参见 name_clusters()(lib/diarize_sherpa.py)中的 >= ref_threshold 检查。默认值从 0.40 调高到 0.50,因为在真实会议音频中,TitaNet-small 在 0.40–0.53 区间内会错误确认身份,而真正的同一说话人匹配得分则高得多——在端到端测试中,已登记的参考声纹与其集群的匹配得分为 0.986,而与最接近的陌生人仅为 0.194,因此 0.50 落在一个很大的空档中。使用 registry_matches 查看每个未达阈值的近似匹配究竟有多接近;如果确有说话人被漏掉,再有意降低阈值。

故障排除

  • 曾经可以使用,后来出现 whosaid: command not found。 whosaid install 会让 ~/.local/bin/whosaid 成为指向检出目录的符号链接,而不是复制实现。如果检出目录位于临时目录(/tmp、/private/tmp、$TMPDIR,例如克隆到 /private/tmp/whosaid-latest/),操作系统最终会清理该目录,符号链接就会指向空处,因此 shell 只会报告 "command not found",不会说明原因。现在,除非传入 --force,否则 whosaid install (以及 ./bootstrap.sh)一开始就会拒绝从临时目录中的检出目录安装。要修复已经失效的安装:将检出目录克隆或移至稳定位置(例如 ~/code/whosaid),然后在该目录中再次运行 whosaid install。由于出问题的是已安装的符号链接本身,无法通过命令名运行 whosaid doctor 来确认此问题——请改为在新的检出目录中运行 ./whosaid doctor;发现此问题时,它会报告一条 DANGLING 安装记录。
  • Enroll/record 没有声音。 这几乎总是 macOS 麦克风权限问题:前往“系统设置”→“隐私与安全性”→“麦克风”,并授予终端应用访问权限。macOS 会向未经授权的应用提供静音零值而不是报错,因此 whosaid 会检查录制音量,而不是依赖正常的退出代码来检测此问题。
  • 无法在我的 Intel Mac 上运行。 MLX 仅支持 Apple Silicon,因此 whosaid 需要 arm64 Mac。
  • 首次运行较慢。 首次运行 ./bootstrap.sh(或者跳过它时首次转录)会下载 Whisper 模型(约 1.5 GB)和说话人分离模型(约 30 MB)。之后每次运行都会使用本地缓存。
  • 长录音会退化为重复文本。 这是 Whisper 众所周知的幻觉/重复崩溃故障模式,最可能出现在较长或信号较弱的音频中。这就是 whosaid 直接调用 mlx-whisper 库(lib/transcribe_mlx.py)而不使用裸 CLI 的原因:裸 CLI 的单温度默认设置正是导致此问题的配置,而库调用会保留温度回退阶梯、禁用基于先前文本的条件化,并应用幻觉静音阈值。
  • 说话人显示为 SPEAKER_00 / SPEAKER_01,而不是姓名。 没有已录入的声音或注册表条目匹配度足够高。阅读 <base>.speaker-cards.txt,确认每个簇对应的人,然后运行 whosaid relabel <base> SPEAKER_01=Name——这会为其标注姓名,并记住该姓名以供下次使用。(也可以继续通过 whosaid enroll <Name> 录入。)命名使用余弦相似度阈值(--match-threshold,默认值为 0.50),因此较短或含噪的样本可能略低于阈值。查看 <base>.diarization.json 中的 registry_matches,获取每个近似匹配的确切相似度;如果确实漏掉了某位说话人,再有意调低阈值。
  • 同一个人显示为两个说话人(幽灵拆分),或已知声音仍显示为 UNIDENTIFIED。 长录音中,说话人分离器可能会将一个声音拆分到多个簇中。注册表/已录入的声音只会为其最接近的单个簇命名,因此额外的簇以前会一直没有名称。现在 whosaid 会执行吸收处理:对于任何仍未命名且声音特征与已知声音的相似度达到 --absorb-threshold(默认值为 0.85)的簇,都会将其并入该人物;说话人卡片也会将这些簇合并为一张卡片。要将此操作应用于已有转录文本,请运行 whosaid relabel <base> --auto——它会根据注册表和吸收处理重新命名,无需重新转录。
  • relabel --auto 忽略了我录入的片段,或者保留了我已删除声音特征的姓名。 现在,--auto 会像 transcribe 一样匹配 voices/ 中的录入片段(即 whosaid enroll NAME --from FILE 保存的片段),而不只是匹配注册表。如果某个簇的姓名由自动处理(注册表或吸收处理)赋予,而该姓名既没有注册表条目,也没有 voices/ 中的片段,自动处理也会移除该姓名;如果存在更匹配的候选项,则会为释放出的簇重新命名。你自行设置的姓名(SPEAKER_NN=Name、--no-save 标注)永远不会被自动移除;早于 registry_matches 的 sidecar 中的姓名也不会被移除,因为其来源未知;由 --ref 片段匹配出的姓名同样不会被移除(该片段可能位于 voices/ 之外;请使用 --forget)。whosaid relabel <base> --auto --forget NAME(可重复使用)会先从本次会议的簇中清除 NAME,包括仅存在于转录文本中的标注,然后重新命名。注册表不会受到影响,因此,如果 NAME 仍已录入且仍然匹配,它就可能再次出现;如果不希望它再出现,也请删除对应的声音特征。
  • 不同的人被合并为同一个说话人(或说话人数量过少)。 说话人嵌入模型必须与口语使用的语言匹配。whosaid 默认使用适用于英语的模型(NeMo TitaNet-small);处理英语音频时,针对中文训练的模型无法区分相似的声音,会将它们合并。对于主要为普通话的音频,请将 DIARIZE_EMB_NAME 设为同一版本中的 …zh-cn… 模型。
  • 说话人数量看起来多了一个,且有一个簇只有约 1 秒的语音。 将 --speakers N 设得过高可能会从串音中切出一个幽灵簇。省略 --speakers 以自动检测,通常更准确。
  • 长录音的自动检测报告了过多说话人(以前总是显示 20)。 当前版本已修复此问题。现在会对每段轮次的声音特征执行平均连接凝聚聚类,并在余弦相似度 0.58 处截断(环境变量为 WHOSAID_COUNT_THRESHOLD;根据真实 TitaNet-small 嵌入数据校准,同一说话人的轮次约为 0.90,不同说话人约为 0.25)。旧的估算器会将每段轮次与某个更早的轮次比较,而不是与一个簇比较,因此在任何存在真实声道变化的录音中,它都会不断新建簇,直到达到 20 的硬上限。现在,上限是边界值,而非目标值:如果估算结果达到上限,就会标记为不可信——并在 <base>.speaker-cards.txt 中说话人数量下方写入一行 WARNING:。对于可疑的估算值,系统会先尝试基于至少 1 秒的轮次进行稳定聚类截断,然后再分配较短的轮次。如果较短轮次的声音特征与这些实质性声音相差过大,就不会采用这种回退方案。count_estimate 会保留主要计数和恢复依据;count_warning 会说明采取的操作或仍存在的不确定性。在有问题的新自动运行中,whosaid 还会在聚类后检查匿名幽灵簇并尝试修复,并在 sidecar 和 MCP 结果中记录 count_before_fold、count_after_fold 和 fold_note。对于已有缓存的自动 sidecar,请运行 whosaid relabel <base> --auto --fold-unknown;它只会合并有充分把握的匿名簇,不会为它们命名。缓存修复会在 fold_evidence 中保留原始相似度依据,因此重复运行此命令不会通过平均后的声音特征形成连续合并。已命名的说话人和仅存在于转录文本中的标注会受到保护。如果你大致知道在场人数,请传入 --min-speakers/--max-speakers;如果知道确切人数,请传入 --speakers N。 当计数仍达到上限且回退方案放弃处理时,警告末尾会显示 Try --max-speakers N,sidecar 和 MCP 结果中也会包含相同的 N,作为 suggested_max_speakers。N 表示在常规截断条件下至少有 3 个实质性轮次的簇数。这只是建议:计数本身不会改变。要在自己的录音上评估估算器,请参阅 docs/count-eval.md。 估算器会在任何录音长度下运行。只有 --no-chunk 会跳过它并使用 sherpa 自己的聚类;该聚类只接受确切数量,因此除非两个值相等,否则系统会报告 --min-speakers/--max-speakers 范围未生效。
  • whosaid search --mode meaning 提示 Ollama 无法访问,或 index 跳过了嵌入。 语义搜索和混合搜索需要在 127.0.0.1:11434 上运行、并加载 nomic-embed-text 的 Ollama(运行 brew install ollama && brew services start ollama && ollama pull nomic-embed-text,或者将 WHOSAID_OLLAMA 指向其监听地址)。其他功能不会受影响:index 仍会构建全文表,search 会回退到精确模式;whosaid doctor 也会显示一条 Ollama 信息,方便你了解当前状态。Ollama 启动后,再次运行 whosaid index <ws>,为之前跳过的轮次生成嵌入。
  • no search index at <ws>/_search.db; run: whosaid index <ws>。 搜索、上下文、图视图和 MCP 工作区工具都只会读取索引;它们不会创建索引。运行一次 whosaid index <ws>(添加会议后还需再次运行,或者使用 ingest --index / roll-up --index / 监视器以保持索引最新)。如果 whosaid doctor 显示相同信息,说明 WHOSAID_WORKSPACE 指向的工作区尚未建立索引。
  • whosaid watch 从不发现新备忘录,或 watch run 以 3 退出并显示 "cannot read the source"。 专用解释器尚未获得“完全磁盘访问”权限。打开“系统设置”→“隐私与安全性”→“完全磁盘访问”,并添加 whosaid watch install 打印的确切路径(默认为 $HOME/.local/opt/whosaid-watch/bin/whosaid-watch),然后运行 launchctl kickstart -k gui/$(id -u)/<label>,或者等待下一个间隔。 whosaid watch status --into <ws> 会显示标签和 Agent 是否已加载; <ws>/.watch.log 包含逐轮详细信息。改为授予终端完全磁盘访问权限也能解决问题,但这会让你从该终端运行的所有程序都拥有此权限,而专用二进制文件正是为了避免这种情况。

测试

./test/e2e.sh 是完全离线的冒烟测试:它使用两个 macOS say 语音合成双人对话,为其中一位说话人创建仅含一个片段的语音注册,并对合成内容端到端运行真实的转录 + 说话人分离 + 命名流程,然后断言带说话人标签的转录稿正确标注已注册说话人的姓名,并为另一位说话人标注不同的名称。请先运行一次 ./bootstrap.sh,将模型缓存到本地;测试本身不会发起网络请求。

./test/version_test.sh 是快速离线单元测试,用于检查 whosaid version、--version 和 -V 是否都打印格式匹配的 whosaid X.Y.Z 行并以状态码 0 退出。

./test/install_guard_test.sh 覆盖临时目录安装保护:未使用 --force 时拒绝安装、使用该选项时安装成功、install --check-only,以及 whosaid doctor 检测悬空的安装符号链接。

./test/enroll_from_file_test.sh 隔离测试 whosaid enroll --from(时间窗口解析、15 秒 / 静音下限以及 --force 覆盖保护),使用合成的 say 音频,无需下载模型。

./test/stereo_channels_test.py 覆盖 record --app 立体声文件中的麦克风与应用音频归属:逐轮声道、将仅含麦克风音频的片段从合并的集群中拆分出来、所有者命名,以及在真实带标签文件上的标签检测。

./test/record_app_test.sh 覆盖 whosaid record --app 帮助文本和标志错误,并在安装了 swiftc 时对 Swift 采集辅助程序进行类型检查。采集本身需要显示器和屏幕录制权限,因此不会进行测试。

./test/samples_test.sh 使用手写的 sidecar 文件和合成的 say 音频测试 whosaid samples(最长片段选择、--seconds 上限、--per-speaker、命名、输出格式以及 source/--audio 回退),无需下载模型。

工作区搜索层也有自己的离线测试,所有测试都使用带占位说话人的合成工作区,并指定 --no-embed,因此绝不会联系 Ollama:

  • ./test/cli_test.sh 检查启动器:whosaid help 中的每条新增命令、用法提示和 --help 路径,以及在一个包含两场会议的工作区中端到端运行 roll-up、index、search、context、graph、wiki、 roll-up --index、doctor,以及模拟的 ingest --index。
  • ./test/search_test.sh 覆盖 lib/search.py:FTS5 构建、带筛选条件的精确查询、 context、speakers、status,以及无 Ollama 时的回退。
  • python3 test/graph_test.py 覆盖 lib/graph.py:实体表、各个视图和 wiki。
  • python3 test/teams_chat_test.py 覆盖 whosaid teams ingest:聊天日期文件夹、轮次 解析、姓名和角色映射、幂等重新导入,以及 CLI 导入 → 索引 → 搜索流程。
  • node test/teams_scraper_pills_test.js 使用模拟 DOM 运行 Teams 抓取器(不连接真实 Teams):列出聊天前会清除已按下的筛选标签,记录在 __ts.pills 中,聊天数量变化时发出警告,并在结束时恢复(#57)。
  • python3 test/teams_manifest_test.py 覆盖 roll-up 和 graph 中不含音频的文件夹: 从 sidecar 获取 created、kind 字段,以及 meetings --kind。
  • python3 test/rollup_incremental_test.py 覆盖增量式承诺事项重新折叠(#98): 仅重新折叠有更改、新增或已移除的文件夹,保留 ID 和人工整理结果,重新推导已剥离的提示和上级优先级;[commitments] 更改时仍执行完整刷新并记录最终 ID,折叠时生成的审核配对也会保留。
  • python3 test/action_items_test.py 使用模拟模型覆盖内置摘要器: 候选项选择、引文验证、章节分配和证据块。
  • python3 test/eval_test.py 使用模拟模型覆盖行动项评估框架:评分器、记录和重放,以及参考后端的隔离。
  • ./test/watch_test.sh 使用临时源文件夹和模拟的 whosaid 覆盖 lib/watch.py:稳定 mtime 等待、暂存、状态、--seed、--dry-run 和 plist。
  • python3 test/mcp_descriptions_test.py 也会检查新增的只读工作区工具。

test/eval/run_eval.py 用于衡量内置摘要器生成的草稿质量。它会在带有人工标注行动项的合成会议上运行工作流,报告精确率、召回率和 F1,并记录每次运行,以便离线重放。它还可以使用云端模型为相同会议生成草稿,以供比较。该后端是可选的开发工具,只会发送已提交的合成测试样例;whosaid 本身绝不会调用云端模型。记录的基线是默认 qwen2.5:14b 的 F1 为 0.717,而 Claude opus 参考模型为 0.921,差距主要来自精确率。请参阅 docs/eval.md。

test/diarize_eval/ 用于衡量合成会议的说话人分离效果。它使用文本转语音语音构造带有精确真实标注的模拟会议,然后评估 whosaid 找到的人数,以及它对每个轮次说话人的判断;评估条件包括盲测、使用 --speakers N,以及使用已注册语音。只有在生成语音池时,脚本中的台词才会发送给 TTS 服务商。语音由 TTS 生成,不是真人语音。请参阅 docs/diarize-eval.md。

./test/roles_test.sh 离线覆盖说话人角色标签:--role/--save-role 验证、重新保存注册表时保留角色、.speakers.txt 中的 # Role: 标题行、sidecar 的 roles 键,以及 [role] 卡片标签——无需下载模型。

./test/commitments_test.sh 离线覆盖 dev-commitments 提取器和语料:提示、否定和疑问句处理、self 角色门控、上级要求的优先级、CM-NNN roll-up 去重和手动编辑协调、WHOSAID_EMBED_FAKE=1 下的语义去重与嵌入服务不可用时的 difflib 回退,以及片段筛选(子句规范、提取和折叠时的 min_words、--min-words、[commitments] 的 min_words 键,以及被丢弃片段的审核章节);无需下载模型。 ./test/worklist_test.sh 会离线测试排名工作列表:每条分级规则、排序、原因字符串、所有者解析(--owner、me、toml 所有者、别名)、CM 与 AI 的合并及跨来源去重、作为重新生成视图的 _WORKLIST-<Owner>.md、--all-owners、--json,以及 whosaid commitments 启动命令。

客户端设置和历史记录回归也有专门的测试套件:

  • ./test/bootstrap_test.sh 使用隔离的工具模拟,测试无 Homebrew、必需工具、bottle 安装失败和校验和失败等路径。
  • python3 test/menubar_test.py 测试真实插件和 watcher CLI,包括运行中与已停止的 PID、已配置来源的权限,以及首次运行偏好设置。
  • uv run --with numpy python test/backfill_test.py 测试历史回填、语料新鲜度与整理、试运行行为、格式错误的输入,以及回滚。
  • python3 test/speaker_registry_migration_test.py 测试私有注册表的往返处理、验证、冲突策略、权限和并发创建。
运行测试套件

每个测试套件都直接运行;运行器取决于它导入的内容:

  • *.sh 测试套件:./test/<name>.sh。
  • 仅使用标准库的 Python 测试套件(graph_test.py、teams_chat_test.py 等): python3 test/<name>.py。
  • numpy 测试套件(anchor、count_eval、diarize_absorb、diarize_recovery、estimate_k、 max_speakers_hint、registry_matches 会导入 numpy;local_labels 和 backfill 则需要它来运行所驱动的 CLI): uv run --quiet --with numpy python test/<name>_test.py。
  • MCP 测试套件(mcp_descriptions_test.py、mcp_workspace_tools_test.py 会导入 mcp): uv run --with "mcp[cli]" python test/<name>.py。

修改代码前需要了解的耦合关系:

  • test/graph_test.py 会固定 meeting 表的行元组(SELECT * FROM meeting)、meetings --json 输出,以及 wiki 表格行 | folder | kind | created | ...,因此添加 meetings 列时,必须编辑这些快照。
  • 说话人注册表测试夹具中的 model 必须是 ONNX 基名("<name>.onnx");lib/speaker_registry.py 中的 _validate_model 会让 read_json 拒绝其他值。
  • macOS 不提供 GNU timeout。脚本内的截止时间使用 perl -e 'alarm shift; exec @ARGV' <secs> <cmd> (参见 test/enroll_from_file_test.sh)。

许可证

MIT — 详见 LICENSE。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。