mcpField
A
asr-transcribe-to-text

asr-transcribe-to-text

github.com/daymade创意与媒体1.5k1.2k 次安装今天更新
代码仓库

关于 asr-transcribe-to-text

asr-transcribe-to-text 是将音频或视频转为带说话人标签文本的技能。它会先检查是否已有规范转录文本,再提供云端转写或本地 ASR 路径,并支持媒体预处理和说话人分离。

主要功能

  1. 01接受本地文件和媒体 URL
  2. 02处理前检查是否已有转录文本
  3. 03支持 Feishu Minutes 和本地 ASR 路径
  4. 04预处理、合并并验证音频输入
  5. 05生成带说话人标签的文本和 CSV
  6. 06提供无时间戳的纯文本模式

适用场景

  1. 01转写尚无规范文本的录音
  2. 02转写前合并录音设备分段
  3. 03离线或隐私敏感场景使用本地 ASR
  4. 04为会议录音生成说话人时间线

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/daymade/claude-code-skills --skill asr-transcribe-to-text

安装后在 Claude Code 中输入/asr-transcribe-to-text确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.gitignore
references/
batch_vad_coverage.md
decoupled_speaker_alignment.md
execution_location_and_comparison.md
local_mlx_guide.md
overlap_merge_strategy.md
review_workbench.md
speaker_diarization.md
voiceprint_speaker_id.md
whisper_word_timestamps.md
whispercpp_gpu_runtime.md
scripts/
align_speakers.py
build_whispercpp_gpu.py
diarize_speakers.py
diarize_speakers.py.lock
fuse_whispercpp_diarization.py
generate_audit_html.py
measure_asr.py
overlap_merge_transcribe.py
prepare_asr_input.py
resolve_media_input.py
scripts/review-workbench/
prepare_project.py
server.py
scripts/review-workbench/static/
app.js
index.html
styles.css
scripts/review-workbench/tests/
test_client_integrity.cjs
test_frontend.cjs
test_playback.cjs
test_prepare_project.py

详细介绍

ASR 转写为文本

将音频/视频转写为带说话人标签的文本。本地执行有两条明确路径。长录音或无人值守录音使用带检查点的 whisper.cpp + Silero VAD 分块处理,然后再与 pyannote 说话人信息进行后期融合。短/中等长度录音可使用 Qwen3-ASR + mlx-whisper 对齐路径。两条路径都不会按说话人分段切分 ASR 输入;说话人归属在连续上下文 ASR 之后完成。

ASR 前的路径选择:转录文本才是结果,不是运行过程

开始转录前,请检查所属项目的转录目录、外部来源索引和声明的既有工作载体,按来源 ID、日期、标题和实体词查找现有的规范转录文本。经核实、由人工审阅且为最新版本的转录文本会结束此任务,除非用户明确要求进行新的独立比较。原始音频仍然存在,并不是重新生成已有文本的理由。

如果不存在规范转录文本:

  1. 对于允许云端处理的一般会议/DJI 录音,通常优先使用飞书妙记(先预处理为较小的 M4A 文件)。
  2. 如果用户要求离线/隐私处理、飞书不可用或失败,或任务明确需要独立的质量比较,则使用本地 ASR。 2b. 如需第二条独立的说话人时间线,请通过 stepfun-asr 技能的 asr_file.py 使用 StepFun 的异步文件端点。这里的供应商说话人分离路径中,只有它不与本地 pyannote 共用代码或模型,因此两者结果一致才是真正的交叉验证,而不是一个工具赞同自己。2026-09-19 在一部 10 分钟影片上的测量结果:两者对关键轮次的切分完全一致,而飞书妙记错误归属了该轮次。限制在于它需要获取一个公开 URL——不接受 base64——因此需要先暂存文件(参见 aliyun-multi-account.md);仅当归属结果至关重要时才使用,不要用于常规转录。
  3. 对于非会议媒体或明确要求使用本地/远程 ASR 的任务,请依据下方的音频位置规则选择执行位置。

不要只是为了让双路径流程看起来完整,就运行本地 ASR。

在选择本地还是远程执行前,请阅读 执行位置与比较。 优先使用现有文本和经验证的检查点,然后比较来源位置、处理权限、实际 GPU/模型身份以及端到端质量证据。 缺少测量数据时,优先选择靠近音频且符合条件的路径,并说明这样做避免了文件传输;不要声称它更快。

配置保存在 ${CLAUDE_PLUGIN_DATA}/config.json。

默认添加说话人标签。 每次运行都会生成 [start-end] SPEAKER_xx: text

  • CSV。纯文本输出为可选项(--no-diarization),适用于独白、 播客,或只需要摘要的情况——参见步骤 3。它还会移除所有时间戳: 纯文本路径只返回会话文本,不包含其他内容。需要逐行时间戳的独白 (字幕、可跳转到具体时刻的档案)应运行完整流水线,并忽略 SPEAKER_00 标签。

说话人分离的一次性设置: pyannote 是受限访问的 HuggingFace 模型—— 需要设置一次令牌(见下方的 ## Speaker Diarization & Identification)。首次运行 若未设置令牌,会失败并显示设置步骤;完成设置后,全部功能将永久可用并自动检测。

步骤 0:检测平台并加载配置

cat "${CLAUDE_PLUGIN_DATA}/config.json" 2>/dev/null

如果配置存在,读取值并继续步骤 1。

如果配置不存在,先自动检测平台:

python3 -c "
import sys, platform
is_mac_arm = sys.platform == 'darwin' and platform.machine() in ('arm64', 'aarch64')
print(f'Platform: {sys.platform} {platform.machine()}')
print(f'Apple Silicon: {is_mac_arm}')
if is_mac_arm:
    print('RECOMMEND: local-mlx')
else:
    print('RECOMMEND: remote-api')
"

然后根据平台使用带适配默认值的 AskUserQuestion:

对于 macOS Apple Silicon(推荐:本地):

ASR setup — your Mac supports local MLX; choose from data location and the execution SOP.

Q1: Transcription mode?
  A) Local MLX — runs on your Mac's GPU, no API key needed
  B) Remote API — send audio to a server (vLLM, Tailscale workstation, etc.)

Q2: Does your network have an HTTP proxy that might intercept traffic?
  A) Yes — bypass proxy for ASR traffic (Recommended if using Shadowrocket/Clash)
  B) No — direct connection

对于 其他平台(推荐:远程):

ASR setup — local MLX requires macOS Apple Silicon. Using remote API mode.

Q1: ASR Endpoint URL?
  A) https://asr.example.com/v1/audio/transcriptions (Self-hosted remote ASR)
  B) http://localhost:8002/v1/audio/transcriptions (Local ASR server)
  C) Custom URL

Q2: Proxy bypass needed?
  A) Yes (Recommended for Shadowrocket/Clash/corporate proxy)
  B) No

保存配置:

mkdir -p "${CLAUDE_PLUGIN_DATA}"
python3 -c "
import json
config = {
    'mode': 'MODE',           # 'local-mlx' or 'remote-api'
    'model': 'MODEL_ID',      # local: 'mlx-community/Qwen3-ASR-1.7B-8bit', remote: 'Qwen/Qwen3-ASR-1.7B'
    'max_tokens': 8192,       # local only; PER 20-minute chunk, not per recording
    'endpoint': 'URL',        # remote only
    'noproxy': True,
    'max_timeout': 900        # remote only
    # 'diarization_declined': True  # set only after the user explicitly declines
    #   the pyannote setup in Step 3 — every run then warns + goes plain-text
    #   until an HF token appears (auto-detected)
}
with open('${CLAUDE_PLUGIN_DATA}/config.json', 'w') as f:
    json.dump(config, f, indent=2)
print('Config saved.')
"

步骤 1:解析输入

接受本地文件、直接媒体 URL 或网页/播客单集页面。

  • 网页或播客页面 URL:先检查页面上是否有现成的转录文本。只有当用户的账户可以直接访问官方/平台转录文本时,才使用该文本。如果转录接口需要登录令牌,而当前没有可用令牌,请明确说明,并改用音频 URL 进行 ASR。
  • 本地文件、直接媒体 URL 或页面 URL 的备用方案:运行随附的解析器。它会从常见页面元数据(og:audio、媒体标签、JSON-LD、RSS 样式的 enclosure 链接)中提取媒体,通过原子临时文件替换方式下载 URL,在远程 Content-Length 存在时进行验证,计算 SHA-256,并使用 ffprobe 验证结果。
uv run ${CLAUDE_SKILL_DIR}/scripts/resolve_media_input.py \
  INPUT_FILE_OR_URL [INPUT_FILE_OR_URL2 ...] \
  --output-dir OUTPUT_DIR \
  --manifest OUTPUT_DIR/media_manifest.json

对于可疑或高价值的下载,可添加 --decode-check,让 ffmpeg 在转录前解码整个文件:

uv run ${CLAUDE_SKILL_DIR}/scripts/resolve_media_input.py \
  "https://www.xiaoyuzhoufm.com/episode/EPISODE_ID" \
  --output-dir OUTPUT_DIR \
  --manifest OUTPUT_DIR/media_manifest.json \
  --decode-check

预期输出:

Downloaded ... bytes in ...s -> OUTPUT_DIR/episode-title.m4a
OUTPUT_DIR/episode-title.m4a

在后续步骤中,将打印出的本地路径用作 INPUT_AUDIO。如果运行环境显示的是字面值 ${CLAUDE_SKILL_DIR},而不是替换后的路径,请按照本文档底部“故障排除”条目的说明解析技能目录。

对于第三方公开播客或受版权保护的媒体,请将转录文本保存为本地文件,供用户个人分析。不要在聊天中粘贴完整的长篇转录文本;请改为提供文件路径、预览、摘要或简短摘录。

步骤 2:提取音频(输入为视频时)

对于视频文件(mp4、mov、mkv、avi、webm),提取为 16kHz 单声道 WAV:

ffmpeg -i INPUT_VIDEO -vn -acodec pcm_s16le -ar 16000 -ac 1 OUTPUT.wav -y

音频文件(wav、mp3、m4a、flac、ogg)可直接使用。获取时长:

ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 INPUT_FILE

清理:转录成功后,删除提取出的 WAV 文件以节省磁盘空间。

预处理:合并片段并缩减按量计费的上传文件(可选)

在转写之前运行此步骤,适用于以下任一情况:

  • 录音是由多个片段组成的转储文件——领夹麦克风和现场录音机通常会将录音拆分为固定时长的文件(例如 TX02_MIC024_....wav、TX02_MIC025_....wav; TX01/TX02 = DJI MIC MINI 2S 内部录音——设备清单以及录音机→飞书妙记路径:meeting-ingest 技能的 meeting-ingest/references/architecture.md §①-L0)。 将它们合并一次,以保留录制顺序;随后由显式的长音频运行器(步骤 3,路径 L)负责稳定的源时间块。分别转写设备片段会丢失这些分块中的跨片段上下文。
  • 音频将提交给按量计费的 ASR(飞书妙记、任何按分钟计费的配额)——保留音高的加速可直接缩短计费时长,现代 ASR 也不受影响: 用户已在飞书妙记上验证,1.3 倍速没有可察觉的识别差异(2026-07-16);公开的 Whisper 基准测试显示,直到 2.0 倍速,词错误率(WER)才会明显下降(≤1.5 倍速 = 安全区,1.5 倍速时 WER 增加约 3%;>2 倍速则无法使用)。

使用附带的脚本——它会合并、规范化为 16 kHz 单声道、可选加速,并自行验证输出,而不是盲信 ffmpeg 的退出代码:

uv run ${CLAUDE_SKILL_DIR}/scripts/prepare_asr_input.py SEG1.wav SEG2.wav -o merged.wav   # merge only
uv run ${CLAUDE_SKILL_DIR}/scripts/prepare_asr_input.py SEG*.wav -o upload.m4a --speed 1.3  # merge + quota-saving speedup

预期输出:

Merge order:
  1. SEG1.wav  [pcm_s24le 48000Hz ch=1 1800.14s]
  2. SEG2.wav  [pcm_s24le 48000Hz ch=1 1800.15s]
[OK] duration: 4946.19s vs expected 4946.18s (delta +0.00s)
[OK] boundary 1 @ 1384.7s: max_volume -15.5 dB
[info] overall: mean_volume -38.3 dB, max_volume 0.0 dB
Wrote upload.m4a
  • 如果每个文件名中都包含 YYYYMMDD_HHMMSS 时间戳(录音机转储文件包含),片段将按时间戳排序;否则会保留给定顺序并附带说明——转写前请检查打印出的合并顺序。

  • 自我验证:输出时长必须等于 Σ片段 ÷ 速度(误差 ±1.5 秒,否则硬性失败);每个拼接处都会进行 10 秒音量抽查(边界处出现静音 = 顺序错误或缺少片段);还会打印整体响度,以便与源文件比较。

  • 加速必须采用 atempo 风格的保留音高拉伸——绝不要使用采样率技巧,否则会改变音高,并破坏 ASR 准确率和说话人日志中的声纹。

  • 根据目标选择输出格式——编解码器由文件扩展名决定:

    目标格式原因
    本地 MLX 流水线(路径 A).wav 或 .m4a两种格式都可直接输入流水线(m4a 已于 2026-07-18 验证:3 分钟片段可干净转写)。M4A 体积约小 5 倍——2 小时 49 分钟的合并文件从 324 MB WAV 降至 63 MB M4A,时长完全相同
    按量计费的上传(飞书妙记,按分钟计费的配额).m4a + --speed 1.3AAC 48k 对 ASR 来说在语音质量上几乎无损;同等语音质量下,体积比 mp3 小约 30%;加速可使计费时长缩短约 23%
    可自托管的 vLLM 端点(路径 B).ogg在 MP3 被拒绝的情况下仍可接受,体积约比 WAV 小 8 倍——这正是让长录音低于服务端 25 MB 请求上限的关键。参见路径 B 的限制部分
    无损归档.flac体积约为 WAV 的 50%,位级无损
    仅当目标不接受上述格式时.mp3兼容性回退方案
  • 保留原始文件,直到转录文本通过步骤 4 的验证。

选项:上传到飞书妙记进行转写

预处理后,如果用户需要飞书妙记,或普通会议 / DJI 录音没有权威转录文本且允许云端处理,请使用此路径。这是常规的会议音频处理路径,并非必须先在本地运行失败后才使用的回退方案。

触发短语:传到妙记 / 上传到飞书妙记 / 让妙记转写 / 根据此音频创建妙记 / 上传到飞书妙记。

上传前确认用户想要的结果:

  • 仅上传 — 用户明确表示只需要一个妙记链接,或只需要上传媒体。创建 minute_url 即为终止状态。
  • 仅需转写文本 — 用户需要转写文本/摘要,但不需要归档到项目中。上传后,按照当前版本匹配的 lark-minutes 说明执行,直到获得就绪的转写文本。
  • 交付到项目 — 用户需要校正、路由、知识库文件、项目索引或 Git 交接。meeting-ingest 负责整体任务;此技能仅参与预处理。在上传前切换到该编排器,并持续运行,直到交付回执通过验证。
  • 未指定后续处理 — 请求要求预处理/上传(例如,“上传到飞书妙记,先转成适合 ASR 的格式”),但未说明妙记创建后应如何处理。按要求完成上传,不要擅自推断转写或项目范围,然后返回 outcome_pending 及持久化令牌/URL。之后若用户要求转写或存入知识库,应继续处理同一个妙记;不得从头开始,也不得依赖最初的猜测。

不要仅仅因为用户的第一句话提到“上传到妙记”,就推断为仅上传。同一请求或项目上下文中明确指定的后续处理结果优先。

约束:

  • 不使用代理:所有 lark-cli 调用都必须使用 LARK_CLI_NO_PROXY=1。
  • 仅使用单个配置文件:只使用当前启用的飞书配置文件。不要遍历租户配置文件,也不要调用租户路由。
  • 默认不进行本地重复 ASR:此分支由飞书负责转写。只有明确要求离线处理/对比时,才运行本地 ASR。
  • 加载当前飞书指南:使用 lark-cli-router,然后阅读版本匹配的 lark-minutes 上传/详情说明。不要用此技能中的过时停止条件覆盖当前 CLI 契约。

分步操作:

  1. 尽可能使用上一节中已预处理的音频。飞书接受 MP4/MOV 封装中的 .m4a、.mp3、.wav、.aac 文件;预处理器中的“Metered upload”行已符合要求。文件大小须低于 6 GB,时长须低于 6 小时——这是飞书上传的硬性限制。

  2. 以用户身份上传到云盘:

    LARK_CLI_NO_PROXY=1 lark-cli drive +upload \
      --file '<preprocessed-media-path>' \
      --name '<basename>' \
      --as user \
      --format json
    

    从结果中记录 file_token。如果命令因路径校验或 multipart 错误而失败,不要盲目重试——更换格式或大小策略后再试一次,然后报告确切错误。

  3. 从该云盘文件创建妙记:

    LARK_CLI_NO_PROXY=1 lark-cli minutes +upload \
      --file-token '<file_token>' \
      --as user \
      --format json
    

    从结果中记录 minute_token 和 minute_url。

  4. 根据先前确定的终止处理结果选择分支:

    • 仅上传:返回 minute_url 并停止。
    • 仅需转写文本:使用当前 lark-minutes 详情命令及其就绪等待行为,并指定 --transcript。创建的 URL 只是中间结果;只有获得可读的转写文本产物才算成功。
    • 交付到项目:将以下交接信息返回给 meeting-ingest,并在同一次运行中继续:prepared_media、file_token、minute_token、minute_url、next_required_phase=minute_ready。编排器会等待云端转写文本,调用 sync-feishu-minutes 执行令牌范围内的导入/路由/委派,运行完整的 transcript-fixer,更新所有项目所属索引,完成 Git 交接,并记录交付情况。
    • 未指定后续处理:返回 minute_token、minute_url、outcome_pending 和 next_required_phase=outcome_decision。在用户说明后续处理方式之前,不要等待转写文本,也不要归档转写文本。

如果就绪等待超时或后续阶段受阻,请报告最后完成阶段的确切信息,以及持久化的 minute_token/minute_url。继续处理同一个令牌;切勿只为获取新 URL 而重新上传。 预期输出:

  • 仅上传成功:一个用户可打开的 minute_url。
  • 下游未指定的情况下成功:所请求的上传已完成,同时运行状态明确保持为可恢复的 outcome_pending;不要称其为项目交付。
  • 仅转录成功:minute_url 加上可读的转录文本文件。
  • 项目交付成功:仅提供 meeting-ingest 推送的交付回执;预处理、创建 URL 和下载转录文本都属于中间阶段。
  • 失败:提供 drive +upload 或 minutes +upload 返回的准确 API 错误,以及一项建议的后续操作。

技能选择错误时的恢复:如果此请求是在你使用 sync-feishu-minutes 时收到的,请根据结果选择:仅上传则转到此处;项目交付则转到 meeting-ingest,它会调用此预处理分支,然后继续执行基于令牌范围的飞书摄取。绝不要悄悄将项目交付降级为 Minute URL。

步骤 3:转录(默认标注说话人)

路径 L:较长的本地录音——whisper.cpp + Silero VAD(默认用于超过 30 分钟的录音)

超过 30 分钟的录音、无人值守的批量任务,或任何可能包含长时间静音/会后环境音的来源,都使用此路径。这是面向 Apple Silicon 的长音频处理方案:按源时间划分的区块会创建检查点,每个区块都会运行 whisper.cpp 的 Silero VAD,在区块交界处对 2 秒重叠部分去重,随后再融合 pyannote 说话人标签。

运行时资源和 GPU 构建清单都是明确的操作员输入。不要在批处理中静默发现或下载模型。使用此路径前, 请阅读 仅 GPU 的运行时准备。 Agent 会在现有设置授权范围内准备隔离且固定版本的构建; 未经修改的二进制文件带有 --use-gpu 标志,并不能证明 GPU VAD 已启用。运行器会在加载模型前拒绝未知的构建身份,原生防护机制也会在推理前拒绝 CPU 或未知的计算图分配。

# 1. Checkpointed ASR on the original source timeline
uv run ${CLAUDE_SKILL_DIR}/scripts/transcribe_long_whispercpp.py \
  INPUT_16K_MONO_PCM16.wav OUTPUT_DIR \
  --ffmpeg-path /absolute/path/to/ffmpeg \
  --whisper-bin /absolute/path/to/whisper-cli \
  --gpu-runtime-manifest /absolute/path/to/user-cache/gpu-runtime.json \
  --whisper-model /absolute/path/to/ggml-large-v2-or-v3.bin \
  --vad-model /absolute/path/to/ggml-silero-v6.2.0.bin

# 2. Independent speaker timeline (controlled FFmpeg decode; no TorchCodec path)
uv run --frozen ${CLAUDE_SKILL_DIR}/scripts/diarize_speakers.py \
  INPUT_16K_MONO_PCM16.wav OUTPUT_DIR/STEM.diarization.json \
  --device mps --ffmpeg-path /absolute/path/to/ffmpeg

# 3. Keep only speech-grounded ASR segments and assign speakers by time overlap
uv run ${CLAUDE_SKILL_DIR}/scripts/fuse_whispercpp_diarization.py \
  OUTPUT_DIR/STEM.whispercpp.json \
  OUTPUT_DIR/STEM.diarization.json \
  INPUT_16K_MONO_PCM16.wav OUTPUT_DIR

如果实体录音设备在业务会议结束后仍继续录音, 请向两个相关步骤都传入有证据支持的源时间戳(--end-at SECONDS)。保留原始音频和不设时长限制的 ASR 证据,但不要将会后的车内噪声或静音强行纳入会议转录文本。

完成意味着最终的 TXT/CSV/融合回执均已生成,并且已抽查开头、中间、结尾和每个外部区块交界处。仅有 N/N blocks complete 并不代表质量合格。重新运行时必须报告每个已完成区块均已缓存。

官方架构依据:whisper.cpp VAD 会在推理前提取语音;OpenAI Whisper 会重置先前文本的上下文,以避免重复循环;NeMo 的长音频指南使用带重叠的缓冲区块。项目特定的实测证据和参数说明见 references/speaker_diarization.md。

路径 A:本地 MLX(macOS Apple Silicon)——短/中音频的替代方案

运行解耦的说话人处理流水线——它会在内部处理依赖版本固定、分块生成上限、可恢复检查点、模型加载和进程树清理。

输入扩展名不能作为解码器契约。Qwen 工作进程首先使用固定版本的 MLX 解码器;如果该解码器报告了可识别的输入/容器解码失败(例如 miniaudio 无法处理 Ogg/Opus),则会使用 ffmpeg 为固定版本的默认模型创建临时的 16 kHz 单声道 PCM WAV,然后重试(自定义本地模型使用其声明的采样率)。GPU、内存及其他无关的运行时错误会原样传递,绝不会触发规范化处理。检查点、输出名称和来源信息仍与原始源字节绑定;临时 WAV 绝不会作为完成产物。

uv run ${CLAUDE_SKILL_DIR}/scripts/speaker_transcribe.py \
  INPUT_AUDIO [INPUT_AUDIO2 ...] OUTPUT_DIR

无人值守批处理:每次调用处理一个文件。任何环节发生确定性故障时(例如某段录音触发每个分块的令牌上限,导致 ASR_DETERMINISTIC_BLOCKED: ChunkTokenLimitError),整个多输入运行都会按设计退出,排在该文件之后的文件将不会启动——一次 31 个文件的批处理就曾因此在第 11 个文件处中止。逐文件调用可将影响限制在单个录音内;中间环节会缓存,因此重新运行其他文件只需执行对齐。

(每个文件的)预期输出:

Device: mps
+ uv run .../transcribe_local_mlx.py ...        (leg 1: session text)
Chunk 1/6 starting at 0.0s (max_tokens=8192)
Chunk 1/6 committed: 6310 chars, 3812 tokens
+ uv run .../word_timestamps_whisper.py ...     (leg 2: timing lattice)
... diarization ...                             (leg 3: pyannote segments)
STEM: 42 turns, speakers=['SPEAKER_00', 'SPEAKER_01'], anchored_ratio=0.93
Wrote STEM.txt, STEM.csv, STEM.alignment.json, STEM.receipt.json

每个输入对应的输出:<stem>.txt([MM:SS - MM:SS] SPEAKER_xx + 文本)、 <stem>.csv(file,start,end,duration,speaker,text——供审核 UI 和 声纹 ID 使用)、<stem>.diarization.json、<stem>.alignment.json(来源信息

  • anchored_ratio 可信度信号;低于 0.5 时会打印醒目警告——请先对照音频核验 标签,再予以信任),以及 <stem>.receipt.json(原子完成记录,绑定源文件字节、 全部四个最终产物的哈希值、生成脚本、固定版本的模型/依赖项和语义参数)。中间环节会缓存在 OUTPUT_DIR/_align/ 中,因此重新运行开销很低(--force 会重新执行最终环节)。每个中间缓存 旁车文件都会绑定源音频字节、生成脚本字节、语义参数和产物字节;仅文件存在并不构成缓存命中。下游完成状态检查要求提供最终回执,而非仅凭非空产物或对齐 JSON。Qwen 分块检查点位于其暂存目录下;运行中断后,会核验 源音频 SHA-256、生成器/拆分器/依赖项约定、不可变模型版本、生成参数和已完成分块的哈希值,然后跳过已完成的分块,而不是从头重新处理录音。一个与语言无关的 12 字符 n-gram 防护机制会在最终交付前,拒绝文本高度重复的分块或整个会话; 质量策略 ID 是检查点身份的一部分,因此较早生成且未经检查的部分无法绕过该防护。

使用 Qwen3 路线前,先对其环节进行一次冒烟测试:

uv run ${CLAUDE_SKILL_DIR}/scripts/transcribe_local_mlx.py --smoke-test

预期会看到随附脚本的依赖项验证结果和 Smoke test OK;固定版本定义在 scripts/transcribe_local_mlx.py 中。有关性能、每个分块的令牌语义、 资源限制和恢复机制,请阅读 references/local_mlx_guide.md。

工作原理(及原因): 使用整个会话的 Qwen3-ASR 文本、mlx-whisper 词级 时间戳和 pyannote 说话人片段,事后再进行对齐。这样可以避免质量受损的旧级联方案——它会分别转录每个经过说话人分离的轮次。不要将其有界的生成分块视为适用于长时间录音的保证:一段真实的多小时录音在 20、10 和 5 分钟窗口下都触及了令牌上限。架构、对齐算法和故障模式: references/decoupled_speaker_alignment.md。

首次运行:pyannote 需要一次性配置 HuggingFace 令牌。 如果脚本因显示设置提示而退出 (退出代码为 3),请停止并使用 AskUserQuestion:

Speaker diarization needs a one-time setup (gated model, free):
  1. Accept terms at https://hf.co/pyannote/speaker-diarization-3.1
  2. Run `huggingface-cli login` (or set HF_TOKEN)

Options:
A) Set it up now — I'll wait, then rerun with full speaker labels (Recommended)
B) Continue without speakers this time — plain text only
  • A → 用户确认登录后,重新运行相同命令。每次运行都会自动检测令牌;从此即可永久使用完整功能。
  • B → 持久化此选择(在 config.json 中设置 diarization_declined: true),然后重新运行相同命令。脚本会检测此标志,打印一行警告并附上两个设置步骤,然后在本次运行中自动回退到纯文本——无需传递 --no-diarization(现在会自动回退,由脚本强制执行,而非仅记录在文档中)。只要令牌仍缺失,之后每次运行都会同样发出警告并继续。令牌日后出现时,说话人分离会自动恢复(令牌存在后,此标志将被忽略)——请说明这一点,让用户知道只需完成设置即可。

纯文本快速路径(独白、播客、“只要总结一下”):

uv run ${CLAUDE_SKILL_DIR}/scripts/speaker_transcribe.py \
  INPUT_AUDIO OUTPUT_DIR --no-diarization

远程/预制 ASR 文本(例如来自 Path B 或其他 ASR 服务):跳过 Qwen3 环节,改为对该文本进行对齐。--text-file 会将一份转录文本与一个输入 wav 配对——传入多个输入会被拒绝(一份转录文本不能与多个文件对齐):

uv run ${CLAUDE_SKILL_DIR}/scripts/speaker_transcribe.py \
  INPUT_AUDIO OUTPUT_DIR --text-file TRANSCRIPT.txt

**非 Apple Silicon 设备:**whisper 计时环节仅支持 MLX。没有该环节,就没有可用于对齐说话人的时间网格——请使用 --no-diarization 运行,并告知用户说话人模式目前需要 Apple Silicon(带内置说话人分离功能的云 ASR,例如 Feishu Minutes,是无需本地 GPU 的替代方案)。

批量处理多个短文件之前(宣传片段、蒙太奇剪辑——任何可能包含纯音乐音频的内容),请阅读下文的 ## Batch Transcription (many short files):一个纯音乐片段就可能让整个批次停滞 10 分钟以上。

Path B:远程 API

远程端点只返回纯文本——之后会在本地将该文本(环节 1)与本地计时和说话人分离环节对齐,从而添加说话人。因此,Path B = 远程获取文本,再使用 --text-file 运行 Path A 的流程。

请使用随附的远程运行器和现有配置。它会检查响应语义、绑定源字节与结果字节,并在配置的服务公开相关信息时记录观测到的生产者身份;它绝不会切换共享模型。关于可自托管 GPU 的证据和性能比较,请参阅执行位置和比较。

python3 ${CLAUDE_SKILL_DIR}/scripts/transcribe_remote.py \
  INPUT_AUDIO OUTPUT.txt --config "${CLAUDE_PLUGIN_DATA}/config.json"

预期会生成 OUTPUT.txt 和 OUTPUT.txt.asr.json;回执代表纯文本环节,并不表示识别质量或说话人信息已完整。

然后在本地添加说话人(需要 Apple Silicon 和 pyannote 令牌):

uv run ${CLAUDE_SKILL_DIR}/scripts/speaker_transcribe.py \
  INPUT_AUDIO OUTPUT_DIR --text-file OUTPUT.txt
可自托管 vLLM:容易以令人困惑的方式触发的限制

**版本在这里很重要——其中两项限制在不同版本之间有过变更。**以下行为是在 vLLM 0.15.2rc1.dev68(开发版本;没有 0.15.2 正式版——PyPI 版本从 0.15.1 跳到 0.16.0)上提供 Qwen/Qwen3-ASR-1.7B 时端到端测得的,之后又对照 v0.26.0 源码重新核查。请先检查你使用的版本——运行 pip show vllm——并阅读第 1 项和第 3 项的版本说明。

**1. 发送 OGG,而不是 WAV——绝不要发送 MP3。**在 0.15.x 上,MP3 会被直接拒绝,但下意识采用的修复方法(转换为 WAV)会让你撞上第 2 项中的大小上限:

格式60 秒 @ 16 kHz 单声道,16 位是否接受(0.15.x)
WAV pcm_s16le1,920 KB是
FLAC1,092 KB是
OGG Vorbis245 KB是
MP3—否

在相同采样率下,OGG 比 WAV 小约 8 倍:

ffmpeg -nostdin -v error -i INPUT -ar 16000 -ac 1 -c:a libvorbis OUTPUT.ogg

自行比较格式时,请固定位深——解码有损源文件时,ffmpeg 可以自由提高位深;24 位 FLAC 的体积会比 16 位 PCM 更大。如果两者并非来自同一段录音,就会让人误以为“FLAC 不压缩”。请添加 -sample_fmt s16。

值得注意的是,MP3 被拒绝时,HTTP 状态码为 200,但响应正文中包含错误——只检查 %{http_code} 的判断会报告成功:

HTTP=200
{"error": {"message": "Error opening <_io.BytesIO object>: Format not recognised.", ...}}

*版本说明:*在 0.15.x 上,上传内容会通过 BytesIO 交由 librosa/soundfile 读取;即使主机上的 libsndfile 能处理磁盘上的 MP3 文件,BytesIO 中的 MP3 仍会被拒绝。v0.26.0 在 soundfile 抛出 LibsndfileError 后新增了 pyav 回退方案(multimodal/media/audio.py),因此当前版本可能可以解码 MP3/M4A;不过,基于上文所述的大小原因,OGG 仍是更好的选择。

2. 请求大小上限为 25 MB。

{"error":{"message":"Maximum file size exceeded (parameter=audio_filesize_mb, value=28.6)",...}}

VLLM_MAX_AUDIO_CLIP_FILESIZE_MB 的默认值为 25(vllm/envs.py;从 0.15.1 到 v0.26.0 均未变)。按 OGG 每分钟约 245 KB 计算,大约 100 分钟就会达到上限——足以处理一场会议,但全天录音或合并的多段录音文件就会超出上限。当任务足够长、需要调整时,请提高该值:

VLLM_MAX_AUDIO_CLIP_FILESIZE_MB=800 vllm serve <model> --port <port> ...

3. v0.26.0 新增了第二项独立限制:音频时长不得超过 10 分钟。提高大小上限不会解除这项限制——两者是彼此独立的门槛,而且超过这项时长限制会导致请求被拒绝,而非截断:

Audio exceeds maximum allowed duration of 600s (metadata reports 5998.0s).
Set VLLM_MAX_AUDIO_DECODE_DURATION_S to increase this limit.

VLLM_MAX_AUDIO_DECODE_DURATION_S 默认为 600,位于 envs.py 中大小上限设置的下一行——它在 0.15.x 中不存在,因此,在旧版服务上可正常处理的讲座长度文件,在全新安装的服务上却会遭到拒绝。在 v0.26.0+ 上,同时设置这两个值:

VLLM_MAX_AUDIO_CLIP_FILESIZE_MB=800 VLLM_MAX_AUDIO_DECODE_DURATION_S=36000 \
  vllm serve <model> --port <port> ...

4. 无法访问 huggingface.co 的主机,即使模型已缓存在本地,加载仍会失败。 vLLM 会在启动时对 config.json 发起 HEAD 请求,重试 5 次后退出——错误提示称“在缓存文件中找不到它们”,但文件明明就在那儿:

HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 vllm serve <model> ...

相同症状也可能由另一种原因导致,值得先排查:容器化服务有自己的 HF_HOME,看不到主机用户的 ~/.cache/huggingface,因此,你用 ls 能看到的模型,对容器来说确实不存在。

5. vLLM 已经能切分长音频——而且比客户端切分更好。 SpeechToTextConfig 包含 overlap_chunk_second=1 和 min_energy_split_window_size=1600,也就是说,它会在约 100 ms 的窗口内最安静的位置切分,而不是按固定偏移量切分,因此切口会落在词与词之间。解除上述上限后,100 分钟的文件可以一次请求完成。这也是下面的步骤 5 仅针对不支持此功能的服务的原因。

没有权限重启服务? #2/#3 中的上限是在服务启动时设置的,因此,如果你无法修改服务端,剩下的办法就是在客户端切分——也就是步骤 5。对于这类端点,这才是合适的工具,而不是备用方案。

⚠️ 但 overlap_merge_transcribe.py 无法直接驱动 0.15.x vLLM 端点:它会用 -acodec copy 将分块写入 chunk_NN.mp3,因此,输入本身是 MP3 时,它会输出 MP3(参见 #1,服务会拒绝);输入为其他格式时,它则会直接失败——它从不检查 ffmpeg 的退出状态,所以坏分块会在之后以 JSON 解析错误的形式暴露出来,而不是提示“ffmpeg 失败”。这些分块也只会在同一个 TemporaryDirectory 中短暂存在,因此根本没有机会转换它们。对于这类端点,请手动切分为 OGG,再逐块提交:

ffmpeg -nostdin -v error -i INPUT -f segment -segment_time 900 \
  -ar 16000 -ac 1 -c:a libvorbis chunk_%02d.ogg

请注意,这样会失去重叠合并拼接功能,因此句子可能会在接缝处断开——而这正是 #5 中服务端基于能量的切分器要避免的问题。

如果远程健康检查失败,请按顺序排查:

  1. 网络:ping -c 1 HOST 或 tailscale status | grep HOST
  2. 服务:tailscale ssh USER@HOST "curl -s localhost:PORT/v1/models"
  3. 代理:切换 --noproxy '*' 后重试

4. “现在真的有进程在监听吗?”——单靠 ss 会误导你。 它只显示当前用户的进程,因此,以其他用户身份运行或在容器内运行的服务对它不可见,却仍能正常提供流量。查询时也一并检查 Docker:

tailscale ssh USER@HOST "ss -ltn | grep -E ':(8000|8001|8002)'; \
  docker ps --format '{{.Names}}\t{{.Ports}}\t{{.Status}}'"

5. “GPU 可用于此任务吗?”——启动另一个模型之前,请先检查显存和声明的服务归属。仅凭 compute-apps 列表为空,不能证明 GPU 处于空闲状态,尤其是在 WSL 下:

tailscale ssh USER@HOST "nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv"
# under WSL nvidia-smi is often off PATH: /usr/lib/wsl/lib/nvidia-smi

请勿为了释放 GPU 或测试冷启动路径而重启或卸载其他会话的服务。检查当前归属并使用现有的兼容路由;如果执行身份不可用,请明确说明,不要悄悄回退到 CPU。

第 4 步:验证输出

转录后,检查完整性:

  1. 确认输出非空
  2. 字符数和密度(中文约 400 字/分钟,英文约 200 词/分钟)仅作为检查线索,不是最低输出要求或拒绝阈值;切勿为了达到这些指标而填充文本
  3. 对于本地 MLX,确认检查点清单显示 status: complete;当任何分块达到其令牌上限时,脚本会拒绝写入最终的 .txt
  4. 对于旧版/远程输出,检查结尾——句子中途截断的尾部可能意味着内容被截断
  5. 向用户展示开头和结尾各约 200 个字符作为预览
  6. 说话人路径:检查对齐报告——anchored_ratio 应 ≥ 0.5(低于此值时脚本会发出警告);说话人数应与录音情况相符(两人访谈却显示 5 位说话人,或独白被拆分为 2 位及以上说话人,都意味着说话人分离过度拆分——关于何时不应信任标签,请参阅 references/speaker_diarization.md)

将空结果、疑似循环结果或时间上不合乎情理的 ASR 结果保留为不可靠草稿, 并视为语音内容未知;保留原始输出,不要将其写入规范转录稿。将其纳入前, 请对照原始音频或现有的已验证来源转录稿,核实受影响的时间段。仅凭重复本身, 包括重复的词语或歌词,并不能证明 ASR 失败或不存在语音。只有在独立核实 所述时间段的原始音频后,才能标注 no speech;绝不能依据 ASR 结果或密度启发式 进行标注。

当本次运行是现有高风险转录稿的独立证据路径时,完整性意味着整个基准录音都已 到达完整检查点/最终回执。选定片段可以核实选定的发言,但不能作为整份转录稿 或“更高质量最终版”的依据。将完整输出交给 transcript-fixer;让其人工审核环节 解决尚未确定的专有名称分歧。

对于被截断或不可靠的输出,执行任务授权已涵盖的恢复操作:检查来源或复用其 已验证的转录稿,然后仅在新输入、预处理、参数或证据能够解决已观察到的问题时 重试。如果来源核实确认存在密集的真实语音,且每个分块的令牌上限导致了截断, 则在测量后将每个分块的预算提高到最高 16384。若没有此类变更,不要用相同的 来源/模型组合重复运行。保留部分输出;仅在确实需要新的许可、费用或用户自主选择时 才询问用户。

第 5 步:回退方案 — 重叠合并(仅限远程 API)

在采用此方案前,先检查你的服务端是否会在内部进行分块。 vLLM 会进行分块(路径 B 限制 #5),而且其基于能量的切分效果优于此脚本的固定偏移切分——因此,对于你控制的 vLLM 端点,应通过提高上限来解决过长文件的问题,而不是在客户端进行切分。

仅当端点无法接收整个文件时,才在客户端进行分块:它直接拒绝较长的音频 (固定上下文窗口、严格的单次请求时长限制),每次在相同输入长度下都会 OOM,或者 它会在内部进行分块,但你无权提高其上限。

超时是另一种故障,通常有更省事的解决办法——请求已被接受,而且仍在运行。 先在配置中提高 max_timeout(一个 100 分钟的文件,以约 60 倍实时速度处理仍需 几分钟,而默认值可能不够);只有在超时时限已足够宽裕的情况下仍然超时,才考虑分块, 这意味着服务端确实慢到无法一次完成处理。

适用上述任一情况时,请回退到分块转录:

python3 ${CLAUDE_SKILL_DIR}/scripts/overlap_merge_transcribe.py \
  --config "${CLAUDE_PLUGIN_DATA}/config.json" \
  INPUT_AUDIO OUTPUT.txt

将音频切分为 18 分钟的分块,每块重叠 2 分钟,并使用去除标点后的模糊匹配进行合并。算法详情请参阅 references/overlap_merge_strategy.md。

对于本地 MLX 模式,无需使用重叠合并——随附脚本使用固定版本的 Qwen 低能量切分器, 以原子方式提交每个分块,并默认将 max_tokens=8192 设为每个分块的上限。

步骤 6:建议修正转录文本

ASR 输出总会包含识别错误,例如同音词、乱码般的技术术语和不完整的句子。成功转录后,主动建议对输出运行 transcript-fixer 技能:

Transcription complete: [N] chars saved to [output_path].

ASR output typically contains recognition errors (homophones, garbled terms, broken sentences).
Would you like me to run /daymade-audio:transcript-fixer to clean up the text?

Options:
A) Yes — run daymade-audio:transcript-fixer on the output now (Recommended)
B) No — the raw transcription is good enough for my needs
C) Later — I'll run it myself when ready

如果用户选择 A,请使用输出文件路径调用 transcript-fixer 技能。这两项技能构成自然的工作流:转录 → 修正 → 审阅。

如果用户在同一轮对话中已要求修正、合并多轨内容或生成更高质量的转录文本,则该请求已代表用户选择 A。直接继续使用 transcript-fixer,无需再次询问用户是否批准同一项工作。

重新配置

rm "${CLAUDE_PLUGIN_DATA}/config.json"

然后重新运行步骤 0。

批量转录(多个短文件)

在使用外部 VAD 选择批次、解读空窗口或声称已覆盖积压内容之前,请先阅读 VAD 和批次覆盖范围。 该文档区分了源样本、候选窗口、尝试次数和已验证内容;请复用所选生产器现有的检查点,并遵循下方对应的分支。

将多个文件传递给一次 transcribe_local_mlx.py 调用十分高效(模型只需加载一次)——但前提是每个文件都包含实际语音。 如果批次中可能包含仅有音乐/背景音乐的片段(例如短宣传视频、以字幕代替旁白的蒙太奇片段),请勿在一个进程中批量处理:

  • 对于只有音乐/节奏的音频,模型可能陷入重复循环幻觉(例如无休止地输出“One, two, three, one, two, three...”)。每个分块的 8192 个 token 上限可限制资源增长,而 12 字符的 n-gram 质量门槛会拒绝重复循环,即使循环在达到上限前停止;但一个有问题的文件仍可能耗尽整个分块的超时时间,拖累整个批次。
  • 批处理任务应采用每个文件一个进程,并为每个文件设置超时(例如在每次调用外包裹 timeout 240/perl -e 'alarm 240; exec @ARGV',并保留失败记录,以便按条件恢复)。这样,卡住的文件最多耗时 4 分钟,不会拖垮整个批次。
  • 先检查卡住文件的源内容和失败原因。只有证据支持较小的输出预算时,才使用 --max-tokens 3000;将任何被截断的结果保留为草稿,并遵循步骤 4 中按条件重试的规则。
  • 应揭示不可靠的转录结果,而不是臆造内容判断:可将较低的唯一词比例、空文本或可疑重复作为检查线索。应用步骤 4 中的草稿/采纳边界;这些信号无法证明片段没有旁白,也不能作为丢弃正常重复语音或歌词的理由。只有在实际源内容支持这项独立任务时,才使用屏幕字幕 OCR。

词级时间戳(字幕、音视频对齐)

mlx-whisper 的词级时间戳是短/中等长度 Qwen 说话人流水线中的时间戳环节(第 2 环节——scripts/word_timestamps_whisper.py 会自动运行它)。本节介绍独立使用词级时间戳的场景:生成字幕、将旁白与镜头边界对齐、为单个片段生成字幕。

Qwen3-ASR 是一种 LLM 解码器 ASR:无论本地还是远程路径,它都只输出纯文本,不含对齐信息。当任务需要确定每个词的说出时间时,请使用 mlx-whisper 并设置 word_timestamps=True。对于这类任务,Whisper 的交叉注意力词级对齐是本地事实标准解决方案。

要点(完整操作方法见 references/whisper_word_timestamps.md):

  • 模型:mlx-community/whisper-large-v3-turbo(约 1.6GB)。纯转录任务中,它的中文 WER 高于 Qwen3-ASR;但对于对齐任务,完全无法使用 Qwen3-ASR;可通过 initial_prompt 提供领域术语。
  • 分段粒度陷阱:对于短视频(15–40 秒),whisper 经常会将整个片段作为一个分段返回——务必以词列表为准,并按时间中点将词分配到对应时间窗口。
  • 与 ffmpeg 场景检测(select='gt(scene,0.3)')配合处理视觉内容;对于非 ASCII 路径,请避免使用 PySceneDetect。

说话人分离与识别(谁说了什么)

说话人标签是步骤 3 的默认输出。两种本地方案都会将 ASR 与 pyannote 解耦,并按时间融合;两者都不会单独转录说话人轮次切分片段。本节介绍相关内容。

  • 短/中时长流水线 — scripts/speaker_transcribe.py 在一条命令中运行全部三个环节 + 对齐,并写入带说话人标签的转录文本 + CSV。 架构、对齐算法、可信度信号(anchored_ratio)和故障模式:references/decoupled_speaker_alignment.md。生产环境中的注意事项(过度分段、麦克风领域效应、何时不应信任标签):references/speaker_diarization.md。
  • 仅进行分离 — scripts/diarize_speakers.py 仅输出 speaker × time 片段(不进行转录)。
  • 旧版级联方案 — scripts/speaker_transcribe_cascade.py 是旧的先切分再转录方案(先分离 → 按轮次切分音频 → 对每个片段进行 ASR)。它会在每次切分时打断 ASR 上下文,降低文本质量;仅在极度嘈杂或严重重叠的音频中保留使用,此时逐片段隔离占主导的近场说话人,效果优于会话级 ASR。其他情况均使用默认的解耦方案。
  • 声纹识别 — 分离标签是匿名的(SPEAKER_00…),且仅适用于单个文件。若要将其映射到真实姓名、跨文件统一同一说话人,或合并分离时过度细分的片段,请通过 scripts/voiceprint_id.py 使用 CAM++ 声纹。使用方法以及关键的声学领域注意事项 — 用一种麦克风录制的声纹,在另一种麦克风下对同一个人的匹配效果会差得多: references/voiceprint_speaker_id.md。

一次性 pyannote 设置(受限模型):在 hf.co/pyannote/speaker-diarization-3.1 接受条款,然后运行一次 huggingface-cli login(或设置 HF_TOKEN)。之后每次运行都会自动检测。

转录审校与复核(HTML)

如需在同一处进行音频聆听、说话人标注和转录修正,请使用捆绑的 Listening Desk / 声间。它将专注的复核队列与完整转录导航、同步源音频、键盘决策、持久化本地保存、撤销以及 JSON/CSV/Markdown 导出功能结合起来。未经决策,浏览器不会将模型候选项升级为人工确认的身份。

python3 ${CLAUDE_SKILL_DIR}/scripts/review-workbench/prepare_project.py \
  --csv /absolute/output/meeting.csv --audio /absolute/audio/meeting.m4a \
  --output /absolute/review/project.json --title "Meeting review"
python3 ${CLAUDE_SKILL_DIR}/scripts/review-workbench/server.py \
  --project /absolute/review/project.json --state-dir /absolute/review/state

打开服务器打印的本地 URL。请将项目数据和决策保存在 Skill 包之外。关于多台录音设备、选定的语音样本、源时钟、状态恢复,以及与词典/队列操作的边界,请阅读Listening Desk 指南。

离线审查导出

保留现有的独立页面,以便离线或批量浏览。其浏览器本地别名和问题标记与 Listening Desk 保存的单条决策相互独立;不要将别名视为经过独立验证的身份。

说话人分离后,每个文件会生成一个 CSV(file,start,end,duration,speaker,text)。随附的审查 HTML 生成器会将这些 CSV 转换为一个以阅读为先的审查页面,其中包含音频播放、逐轮标记/备注、说话人别名和导出功能。

从说话人转录输出目录生成:

uv run ${CLAUDE_SKILL_DIR}/scripts/generate_audit_html.py \
  OUTPUT_DIR \
  --output OUTPUT_DIR/audit/index.html \
  --audio-dir /path/to/original/audio

默认情况下,假设 PROJECT_DIR 下的目录结构是扁平的:PROJECT_DIR/*.csv 转录文件、PROJECT_DIR/*.diarization.json 文件,以及与输出文件放在一起的原始音频文件。speaker_transcribe.py 本身会将 CSV、TXT 和说话人分离文件直接写入 OUTPUT_DIR。如果你的项目使用不同的结构,可以覆盖其中任意路径:

uv run ${CLAUDE_SKILL_DIR}/scripts/generate_audit_html.py \
  /path/to/project \
  --output /path/to/project/audit/index.html \
  --csv-dir /path/to/project/csv \
  --txt-dir /path/to/project/txt \
  --diarization-dir /path/to/project/diarization \
  --audio-dir /path/to/project/audio \
  --original-dir /path/to/project/original \
  --manifest /path/to/project/manifest.json \
  --title "Project Audit" \
  --subtitle "Speaker-labeled transcript review" \
  --storage-key "project-audit" \
  --known-speaker "Speaker A" \
  --known-speaker "Speaker B"

主要 CLI 选项:

选项含义
project_dir项目基础目录(必需)
--output写入 index.html 的位置
--csv-dir包含 *.csv 转录文件的目录
--txt-dir包含 *.txt 纯文本转录文件的目录(可选)
--diarization-dir包含 *.diarization.json 文件的目录
--audio-dir包含用于播放的音频文件的目录
--original-dir包含原始源媒体文件的目录(可选)
--manifest将文件 ID 映射到元数据的 JSON 清单(可选)
--title / --subtitle页面标题和副标题
--storage-key用于持久化状态的 localStorage 命名空间
--known-speaker可重复使用;"Name" 会自动分配颜色,"Name=#hex" 会显式设置颜色
--material-final / --material-rough用于筛选的可重复材料分类标签

输出是一个不依赖外部资源的独立 HTML 文件。在浏览器中打开即可审查、标记和注释各轮内容;导出按钮会生成一份报告,列出所有带标记的行及其原因和备注。

故障排除

本地 MLX 加载模型失败

如果模型加载失败,并出现类似以下错误:

AttributeError: 'str' object has no attribute '__module__'

请检查 Agent 是否正在使用当前随附的脚本。其 PEP 723 声明和运行时依赖检查负责管理版本固定。运行其 --smoke-test 并查看验证结果;验证成功之前,不要开始耗时较长的转录。

可自托管的远程端点拒绝音频

以下每种情况都可能让人误判真正原因,因此值得根据症状加以辨认。完整详情和解决方法请参阅路径 B 的“可自托管 vLLM:容易以令人困惑的方式触发的限制”部分。

症状实际原因
Maximum file size exceeded (parameter=audio_filesize_mb, ...)25 MB 上限,按字节而非分钟计算——转换为 WAV 通常会导致超限;请发送 OGG(体积约小 8 倍)
HTTP 200,但响应正文是 {"error": ... "Format not recognised."}向 0.15.x 服务发送了 MP3——而仅检查状态码的检查会将其判定为成功
Audio exceeds maximum allowed duration of 600s在 v0.26.0 中新增的第二个独立上限;提高大小上限无法解除此限制 → VLLM_MAX_AUDIO_DECODE_DURATION_S
服务无法启动:提示“couldn't find them in the cached files”,但模型确实已缓存启动时尝试访问 huggingface.co → HF_HUB_OFFLINE=1;如果使用容器,容器中的 HF_HOME 可能无法访问主机的缓存
长文件处理失败,你正准备在客户端分块vLLM 已会在低能量点分割音频——请提高上限,而不是分块;除非你无法重启服务(第 5 步会解释何时分块才是正确做法)
Whisper 时间戳步骤因 httpx.ProxyError: 503 Service Unavailable 失败

word_timestamps_whisper.py 在从本地缓存加载模型前,会先向 huggingface.co 查询模型。在代理隧道不稳定的情况下,该请求会失败,导致这一步骤退出;即使所有模型文件都已在磁盘上,也会使整个文件处理失败(某个批次中的 44 个文件里有 6 个如此)。运行时设置 HF_HUB_OFFLINE=1:离线模式会直接从缓存加载,且不会建立连接。与上方 vLLM 服务行中的设置相同,原因也相同。

${CLAUDE_SKILL_DIR} 不会被替换

此技能中的脚本路径使用 ${CLAUDE_SKILL_DIR},即技能自身的目录;Claude Code 会在技能加载时替换它。如果命令中出现字面值 ${CLAUDE_SKILL_DIR}(某些运行时不会进行替换),请按以下顺序确定技能目录:

  1. 技能加载信封:Base directory for this skill: <path> → <path> 即技能目录。
  2. 没有信封 → 查找候选目录,并选择本次会话的可用技能列表所指向的目录(已安装的副本可能落后于源代码检出目录): find ~/.claude ~/.claude-profiles ~/.codex ~/workspace -maxdepth 7 -type d -name asr-transcribe-to-text 2>/dev/null | head -5

在本文档中,将 ${CLAUDE_SKILL_DIR} 全部替换为解析出的绝对路径。

捆绑资源

脚本:

  • transcribe_remote.py — 现有的远程纯文本 ASR,包含来源/结果/生成方回执,并可选记录观测到的运行时 GPU 身份
  • measure_asr.py — 测量指定的现有 ASR 命令,检查原始语音质量,并按需比较观测结果
  • resolve_media_input.py — 将本地路径、直接媒体 URL 以及播客/网页解析为经过验证的本地媒体文件
  • prepare_asr_input.py — 合并多段录音并为 ASR 进行规范化处理(16 kHz 单声道);可选保留音高加速,以适应按量计费的上传;自动验证时长计算和拼接边界
  • transcribe_local_mlx.py — 本地 MLX 转录(macOS ARM64,PEP 723 依赖项),有界低能量分块、原子检查点/续传、绑定所有者存活状态
  • transcribe_long_whispercpp.py — 长音频 ASR 默认方案:明确的源时间块 + 重叠区段归属 + whisper.cpp/Silero VAD + 原子检查点/续传
  • build_whispercpp_gpu.py — 准备隔离且固定版本的 Metal 运行时,并检查原生 GPU 计算图;将其二进制文件/清单保存在用户缓存中
  • fuse_whispercpp_diarization.py — 将规范化的 whisper.cpp 时间片段与 pyannote 语音/说话人信息后期融合;移除缺乏依据的静音幻觉,并生成 TXT/CSV/回执
  • speaker_transcribe.py — 短/中音频解耦流水线(会话级 Qwen3-ASR + whisper 定时 + pyannote);--no-diarization 纯文本快速路径;使用 --text-file 输入远程/预制 ASR 文本
  • align_speakers.py — 解耦对齐核心(标准库):将完整转录稿映射到 whisper 词级格网 + pyannote 片段;在停顿和说话人变更处切分轮次,但绝不切开拉丁词;可独立用于调试
  • word_timestamps_whisper.py — mlx-whisper 词级时间戳 → JSON 定时格网(Apple Silicon)
  • speaker_transcribe_cascade.py — 旧版先切分再转录方案(仅适用于极度嘈杂/重叠的音频)
  • diarize_speakers.py — 仅进行说话人分离(pyannote 3.1 @ MPS)→ 每段 JSON
  • voiceprint_id.py — CAM++ 声纹注册/匹配:将匿名 SPEAKER_xx 映射到真实姓名
  • overlap_merge_transcribe.py — 使用重叠合并进行分块转录(远程 API 回退方案)
  • generate_audit_html.py — 根据 speaker-transcribe CSV 输出构建独立的 HTML 审核/复核页面

参考文档:

  • execution_location_and_comparison.md — 有条件地选择本地/远程方案,复用现有转录稿/缓存,安全识别运行时身份并比较相同来源
  • whispercpp_gpu_runtime.md — 路径 L GPU-VAD 准备、原生计算证据及检查点恢复边界
  • decoupled_speaker_alignment.md — 默认架构:解耦的原因、对齐算法、可信信号、故障模式
  • speaker_diarization.md — 生产环境中的陷阱:过度分段、麦克风域效应、何时不应相信标签;旧版级联方案说明
  • voiceprint_speaker_id.md — CAM++ 说话人识别:注册/匹配、阈值 + 间隔门槛、声学域注意事项、引导流程
  • local_mlx_guide.md — 性能基准、分块令牌/资源约定、检查点恢复、模型兼容性
  • whisper_word_timestamps.md — mlx-whisper 词级定时:短/中音频 Qwen 流水线中的定时环节;独立字幕/音视频对齐方案
  • overlap_merge_strategy.md — 朴素分块为何会失败、模糊合并算法

评论

支持 Markdown