Skill: vahnxu/doubao-asr (version 0.18.3)

--- name: doubao-asr（豆包语音转写） description: "Transcribe recorded audio files to text via Doubao Seed-ASR 2.0 (豆包录音文件识别模型2.0) from ByteDance/Volcengine. Best-in-class Chinese speech recognition with speaker diarization. Use this skill whenever the user wants to: convert audio/recording to text, transcribe a meeting recording or voice memo, identify who said what in a recording (说话人分离), transcribe m4a/mp3/wav/ogg/flac files, or mentions 录音转文字/豆包/火山引擎/Volcengine/Doubao ASR. Also use when the user has an audio file and needs a transcript, even if they don't explicitly say 'transcribe'. Do NOT use for real-time/streaming speech recognition, text-to-speech (TTS), live captioning, or audio format conversion." allowed-tools: "Bash(python3:*)" homepage: https://www.volcengine.com/docs/6561/1354868 metadata: { "openclaw": { "emoji": "🫘", "requires": { "bins": ["python3"], "env": ["VOLCENGINE_API_KEY", "VOLCENGINE_ACCESS_KEY_ID", "VOLCENGINE_SECRET_ACCESS_KEY", "VOLCENGINE_TOS_BUCKET", "VOLCENGINE_TOS_REGION"], "pip": ["requests"] }, "primaryEnv": "VOLCENGINE_API_KEY", "envHelp": { "VOLCENGINE_API_KEY": { "required": true, "description": "豆包 ASR API Key (UUID format). 从火山引擎语音控制台获取 / Get from Volcengine Speech console", "howToGet": "⚠️ 正确地址是 /speech/new/（新版控制台），不是 /speech/app（旧版，认证方式完全不同）\n\n1. 打开 https://console.volcengine.com/speech/new/（确认进入的是新版「豆包语音」控制台）\n2. 左侧菜单 →「语音识别」\n3. 点击「开通模型」，开通「录音文件识别2.0」\n4. 点击页面右上角「API 调用」\n5. 在 Step 1「获取 API Key」中，点击创建 API Key\n6. 复制生成的 UUID 格式 Key（如 57e620a4-179c-4b3d-bd8d-990bd1f9a1e2）\n\n⚠️ CORRECT URL is /speech/new/ (new console), NOT /speech/app (old console, completely different auth)\n\n1. Open https://console.volcengine.com/speech/new/ (make sure you are in the new 'Doubao Speech' console)\n2. Left sidebar → 'Speech Recognition'\n3. Click 'Activate Model', activate 'Audio File Recognition 2.0'\n4. Click 'API Call' button at the top-right of the page\n5. In Step 1 'Get API Key', click to create an API Key\n6. Copy the generated UUID-format key (e.g. 57e620a4-179c-4b3d-bd8d-990bd1f9a1e2)", "url": "https://console.volcengine.com/speech/new/", }, "VOLCENGINE_ACCESS_KEY_ID": { "required": true, "description": "IAM Access Key ID (starts with AKLT). 通过 IAM 子用户创建 / Create via IAM sub-user", "howToGet": "⚠️ 不要添加 TOSFullAccess 等 IAM 权限策略！权限将在 Step 3 通过 TOS 桶策略配置（最小权限）\n⚠️ DO NOT add TOSFullAccess or any IAM policy! Access will be granted via TOS bucket policy in Step 3 (least privilege)\n\n1. 打开 https://console.volcengine.com/iam/usermanage\n2. 点「新建用户」，填写用户名（如 doubao-asr）\n3. 访问方式确保勾选「编程访问」和「允许用户管理自己的API密钥」，其他选项保持默认即可\n4. 点击确定，创建成功后页面会显示 Access Key ID（以 AKLT 开头）和 Secret Access Key，复制保存\n 这一步不需要添加任何 IAM 权限策略，权限将在 Step 3 通过 TOS 桶策略授予（仅限单桶读写）\n 提示：如需再次查看密钥，进入用户列表 → 点击子用户名 → 切换到「密钥」tab\n\n1. Open https://console.volcengine.com/iam/usermanage\n2. Click 'Create User', enter username (e.g. doubao-asr)\n3. Make sure 'Programmatic Access' and 'Allow user to manage own API keys' are checked. Leave all other options as default\n4. Click confirm. The success page shows Access Key ID (starts with AKLT) and Secret Access Key — copy both\n No IAM permission policies needed here — access will be granted via TOS bucket policy in Step 3 (single-bucket read/write only)\n Tip: To view keys again, go to user list → click sub-user name → switch to 'Keys' tab", "url": "https://console.volcengine.com/iam/usermanage", }, "VOLCENGINE_SECRET_ACCESS_KEY": { "required": true, "description": "IAM Secret Access Key. 与 Access Key ID 配对 / Paired with Access Key ID", "howToGet": "在创建 Access Key ID 时一起生成（见上一步），创建后可在控制台随时查看。\n\nGenerated together with Access Key ID (see previous step). Can be viewed anytime in the console.", }, "VOLCENGINE_TOS_BUCKET": { "required": true, "description": "TOS 存储桶名称 / TOS bucket name for audio upload", "howToGet": "1. 打开 https://console.volcengine.com/tos\n2. 首次进入会看到「开通对象存储」引导页，点击确认开通\n3. 开通后如果页面没有自动跳转到管理控制台，请手动重新访问 https://console.volcengine.com/tos 进入\n4. 在左侧菜单栏找到「桶列表」。如果看不到已创建的桶，检查页面顶部的项目选择器，切换到创建桶时所用的项目\n5. 点击「创建桶」，输入桶名称，根据服务器位置选择区域：\n - 中国内地服务器 → cn-beijing\n - 海外服务器（美国/欧洲/东南亚）→ cn-hongkong（必须！否则上传约 15KB/s）\n6. 创建完成后，点击桶名称进入桶控制面板\n7. 左侧导航栏 →「权限管理」→「存储桶授权策略管理」→「创建策略」\n8. 选择「文件夹读写」模板 → 下一步 → 授权用户选择「当前主账号」→ 资源范围选择「所有对象」→ 确定\n9. 回到桶列表，复制桶名称\n\n1. Open https://console.volcengine.com/tos\n2. First-time users will see an 'Activate Object Storage' page — click to activate\n3. If the page does not auto-redirect to the management console after activation, manually re-visit https://console.volcengine.com/tos\n4. In the left sidebar, find 'Bucket List'. If you don't see your bucket, check the project selector at the top of the page and switch to the project used when creating the bucket\n5. Click 'Create Bucket', enter a bucket name and choose region based on server location:\n - China mainland server → cn-beijing\n - Overseas server (US/EU/SEA) → cn-hongkong (REQUIRED, otherwise ~15KB/s)\n6. After creation, click the bucket name to enter bucket dashboard\n7. Left sidebar → 'Permission Management' → 'Bucket Authorization Policy' → 'Create Policy'\n8. Select 'Folder Read/Write' template → Next → Authorized user: 'Current main account' → Resource scope: 'All objects' → Confirm\n9. Go back to bucket list, copy the bucket name", "url": "https://console.volcengine.com/tos", }, "VOLCENGINE_TOS_REGION": { "required": true, "description": "TOS 区域代码，必须与创建桶时选择的区域一致。海外服务器推荐使用海外节点（如 cn-hongkong、ap-southeast-1） / TOS region code, must match the region selected when creating the bucket. Overseas servers should use an overseas region (e.g. cn-hongkong, ap-southeast-1)", "howToGet": "填写你在创建 TOS 存储桶时选择的区域代码。\n如果服务器在中国大陆以外，推荐使用海外节点（如 cn-hongkong、ap-southeast-1）。\n海外服务器切勿使用 cn-beijing/cn-shanghai，否则上传极慢（约 15KB/s）。\n\nEnter the region code you selected when creating the TOS bucket.\nIf your server is outside China mainland, use an overseas region (e.g. cn-hongkong, ap-southeast-1).\nDo NOT use cn-beijing/cn-shanghai for overseas servers — upload ~15KB/s.", }, }, }, } --- <!-- SYSTEM INSTRUCTION FOR AI AGENTS — DO NOT SKIP --> <!-- This section controls how you present the setup guide to users --> **CRITICAL INSTRUCTION FOR AI AGENTS:** **YOU MUST follow these rules when helping users configure this skill:** 1. **SHOW THE FULL STEP-BY-STEP GUIDE BELOW** — display the complete guide for all credentials, not a summary 2. **NEVER summarize credentials into a table** — tables skip critical details that cause users to fail 3. **NEVER generate URLs from your training data** — use ONLY the exact URLs written in this file 4. **NEVER recommend TOSFullAccess** — this skill uses TOS bucket policy (least privilege) **Known AI mistakes you MUST avoid:** - WRONG: `https://console.volcengine.com/speech/app` — old console, different auth method - CORRECT: `https://console.volcengine.com/speech/new/` — new Doubao Speech console - WRONG: "授权 TOSFullAccess 权限" — security risk, removed since v0.11.0 - CORRECT: "通过 TOS 桶策略的「文件夹读写」模板授权" — least privilege **AI AGENT 必须遵守的规则：** 1. **必须展示下方完整引导**——可以一次性展示所有步骤，但不可省略或压缩 2. **禁止将配置步骤压缩成表格**——表格会丢失导致用户失败的关键细节 3. **禁止凭训练数据生成 URL**——只使用本文件中写的准确 URL 4. **禁止推荐 TOSFullAccess**——本 skill 使用 TOS 桶策略（最小权限） **必须避免的常见 AI 错误：** - 错误：`https://console.volcengine.com/speech/app`——旧版控制台，认证方式完全不同 - 正确：`https://console.volcengine.com/speech/new/`——新版豆包语音控制台 - 错误："授权 TOSFullAccess 权限"——安全风险，v0.11.0 起已移除 - 正确："通过 TOS 桶策略的「文件夹读写」模板授权"——最小权限 --- # Doubao ASR / 豆包语音转写 Transcribe audio files via ByteDance Volcengine's **Seed-ASR 2.0 Standard** (豆包录音文件识别模型2.0-标准版) API. Best-in-class accuracy for Chinese (Mandarin, Cantonese, Sichuan dialect, etc.) and supports 13+ languages. 调用字节跳动火山引擎**豆包录音文件识别模型2.0-标准版**（Seed-ASR 2.0 Standard）转写音频文件。中文识别（普通话、粤语、四川话等方言）准确率业界领先，支持 13+ 种语言。 ## Sending audio to OpenClaw Currently, audio files can be sent to OpenClaw via **Discord** or **WhatsApp**. Send the audio file in a chat message and ask the bot to transcribe it. 目前可通过 **Discord** 或 **WhatsApp** 向 OpenClaw 发送音频文件，发送后让 bot 转写即可。 > **Note**: Direct voice recording in the OpenClaw web UI is not yet supported. Use a messaging app to send pre-recorded audio files. > > **提示**：OpenClaw 网页端暂不支持直接录音，请通过即时通讯应用发送预录制的音频文件。 ## Quick start ```bash python3 {baseDir}/scripts/transcribe.py /path/to/audio.m4a ``` Defaults: - Model: Seed-ASR 2.0 Standard / 豆包录音文件识别模型2.0-标准版 - Speaker diarization: enabled / 说话人分离：默认开启 - Output: stdout (transcript text with speaker labels / 带说话人标签的转写文本) ## Useful flags ```bash python3 {baseDir}/scripts/transcribe.py /path/to/audio.m4a --out /tmp/transcript.txt python3 {baseDir}/scripts/transcribe.py /path/to/audio.mp3 --format mp3 python3 {baseDir}/scripts/transcribe.py /path/to/audio.m4a --json --out /tmp/result.json python3 {baseDir}/scripts/transcribe.py /path/to/audio.m4a --no-speakers # disable speaker diarization / 关闭说话人分离 python3 {baseDir}/scripts/transcribe.py https://example.com/audio.mp3 # direct URL (skip upload) ``` ## How it works The Doubao API accepts audio via URL (not direct file upload). The script: 1. **Uploads audio to Volcengine TOS** (object storage) via presigned URL — audio stays within Volcengine infrastructure, no third-party services involved 2. Submits transcription task to Seed-ASR 2.0 3. Polls until complete (typically 1-3 minutes for a 10-min audio) 4. Returns transcript text > **Privacy**: By default, audio is uploaded to your own Volcengine TOS bucket via presigned URL. No data is sent to third-party services. You can also pass a direct audio URL as the argument to skip upload entirely: ```bash python3 {baseDir}/scripts/transcribe.py https://your-bucket.tos.volces.com/audio.m4a ``` ## Dependencies - Python 3.9+ - `requests`: `pip install requests` ## Credentials You need 4 environment variables. Follow these steps carefully — the guided setup below saves you 1-2 hours of digging through Volcengine docs. 你需要设置 4 个环境变量。按以下步骤操作——这份引导能帮你节省 1-2 小时翻文档踩坑的时间。 ### Step 1: Doubao ASR API Key / 第一步：豆包 ASR API Key 1. 打开 https://console.volcengine.com/speech/new/（确认进入的是新版「豆包语音」控制台） 2. 左侧菜单 →「语音识别」 3. 点击「开通模型」，开通「录音文件识别2.0」 4. 点击页面右上角「API 调用」 5. 在 Step 1「获取 API Key」中，点击创建 API Key 6. 复制生成的 UUID 格式 Key --- 1. Open https://console.volcengine.com/speech/new/ (make sure you are in the new 'Doubao Speech' console) 2. Left sidebar → 'Speech Recognition' 3. Click 'Activate Model', activate 'Audio File Recognition 2.0' 4. Click 'API Call' button at the top-right of the page 5. In Step 1 'Get API Key', click to create an API Key 6. Copy the generated UUID-format key (e.g. [REDACTED_SECRET]) ```bash export VOLCENGINE_API_KEY=[REDACTED_SECRET] ``` ### Step 2: IAM Access Key / 第二步：创建 IAM 子用户和访问密钥 1. 打开 https://console.volcengine.com/iam/usermanage 2. 点「新建用户」，填写用户名（如 `doubao-asr`） 3. 访问方式确保勾选「编程访问」和「允许用户管理自己的API密钥」，其他选项保持默认即可 4. 点击确定，创建成功后页面会显示 Access Key ID（以 `AKLT` 开头）和 Secret Access Key，复制保存 > **提示**：这一步不需要添加任何 IAM 权限策略。权限将在 Step 3 通过 TOS 桶策略授予（仅限单桶读写）。 > 如需再次查看密钥，进入用户列表 → 点击子用户名 → 切换到「密钥」tab。 --- 1. Open https://console.volcengine.com/iam/usermanage 2. Click 'Create User', enter username (e.g. `doubao-asr`) 3. Make sure 'Programmatic Access' and 'Allow user to manage own API keys' are checked. Leave all other options as default 4. Click confirm. The success page shows Access Key ID (starts with `AKLT`) and Secret Access Key — copy both > **Note**: No IAM permission policies needed here — access will be granted via TOS bucket policy in Step 3 (single-bucket read/write only). > Tip: To view keys again, go to user list → click sub-user name → switch to 'Keys' tab. ```bash export VOLCENGINE_ACCESS_KEY_ID="AKLTxxxx..." export VOLCENGINE_SECRET_ACCESS_KEY="xxxx..." ``` ### Step 3: TOS Bucket / 第三步：开通并创建 TOS 存储桶 豆包 API 要求音频通过 URL 访问。TOS 对象存储提供安全的临时上传，数据留在火山引擎内部。 1. 打开 https://console.volcengine.com/tos 2. 首次进入会看到「开通对象存储」引导页，点击确认开通 3. 开通后如果页面没有自动跳转到管理控制台，请手动重新访问 https://console.volcengine.com/tos 进入 4. 在左侧菜单栏找到「桶列表」。如果看不到已创建的桶，检查页面顶部的项目选择器，切换到创建桶时所用的项目 5. 点击「创建桶」，输入桶名称，**根据服务器位置选择区域**（见下方表格） 6. 创建完成后，点击桶名称进入桶控制面板 7. 左侧导航栏 →「权限管理」→「存储桶授权策略管理」→「创建策略」 8. 选择「文件夹读写」模板 → 下一步 → 授权用户选择「当前主账号」→ 资源范围选择「所有对象」→ 确定 9. 回到桶列表，复制桶名称 --- 1. Open https://console.volcengine.com/tos 2. First-time users will see an 'Activate Object Storage' page — click to activate 3. If the page does not auto-redirect after activation, manually re-visit https://console.volcengine.com/tos 4. In the left sidebar, find 'Bucket List'. If you don't see your bucket, check the project selector at the top 5. Click 'Create Bucket', enter a bucket name and choose region based on server location (see table below) 6. After creation, click the bucket name to enter bucket dashboard 7. Left sidebar → 'Permission Management' → 'Bucket Authorization Policy' → 'Create Policy' 8. Select 'Folder Read/Write' template → Next → Authorized user: 'Current main account' → Resource scope: 'All objects' → Confirm 9. Go back to bucket list, copy the bucket name **Region selection / 区域选择：** | Server location / 服务器位置 | Recommended TOS region / 推荐 TOS 区域 | Region code | |---|---|---| | China mainland / 中国内地 | cn-beijing, cn-shanghai, cn-guangzhou | `cn-beijing` | | Hong Kong / 香港 | cn-hongkong | `cn-hongkong` | | Southeast Asia / 东南亚 | ap-southeast-1 (Singapore) | `ap-southeast-1` | | US, Europe, other overseas / 美国、欧洲等海外 | Any overseas region (e.g. `cn-hongkong`, `ap-southeast-1`) / 任意海外节点 | `cn-hongkong` | > **Important**: If your server is **outside China mainland**, use an overseas region (e.g. `cn-hongkong`, `ap-southeast-1`) — do NOT use `cn-beijing` / `cn-shanghai`, cross-border upload will be extremely slow (~15KB/s). > > **重要**：如果你的服务器在**中国大陆以外**，请使用海外节点（如 `cn-hongkong`、`ap-southeast-1`），不要用 `cn-beijing` / `cn-shanghai`——跨境上传会非常慢（约 15KB/s）。 ```bash export VOLCENGINE_TOS_BUCKET="your_bucket_name" export VOLCENGINE_TOS_REGION="cn-hongkong" # or other overseas region / 或其他海外节点，见上方区域表 ``` ### Summary of all environment variables / 环境变量汇总 | Variable | Required | Description | |---|---|---| | `VOLCENGINE_API_KEY` | Yes | ASR API key (UUID format) from Speech console / 语音控制台的 API Key | | `VOLCENGINE_ACCESS_KEY_ID` | Yes | IAM Access Key ID (starts with `AKLT`) / IAM 访问密钥 ID | | `VOLCENGINE_SECRET_ACCESS_KEY` | Yes | IAM Secret Access Key / IAM 访问密钥 | | `VOLCENGINE_TOS_BUCKET` | Yes | TOS bucket name / TOS 存储桶名称 | | `VOLCENGINE_TOS_REGION` | Yes | TOS region code, must match bucket region. 必须与创建桶时选择的区域一致。Overseas: e.g. `cn-hongkong`, `ap-southeast-1`; China: `cn-beijing` | ## Supported formats WAV, MP3, MP4, M4A, OGG, FLAC — up to 5 hours, 512MB max. 支持格式：WAV、MP3、MP4、M4A、OGG、FLAC——最长 5 小时，最大 512MB。 ## Troubleshooting / 常见问题 **Error**: `TOS upload failed: 403 Forbidden` **Cause**: TOS bucket policy not configured, or IAM user not authorized. / TOS 桶策略未配置，或 IAM 用户未授权。 **Solution**: Go to TOS bucket → Permission Management → Bucket Authorization Policy → Create Policy → select "Folder Read/Write" template. See Step 3 above. / 进入 TOS 桶 → 权限管理 → 存储桶授权策略管理 → 创建策略 → 选择「文件夹读写」模板。详见上方第三步。 **Error**: `TOS upload extremely slow (~15KB/s)` **Cause**: Server is outside China mainland but using `cn-beijing` region. / 服务器在中国大陆以外，但使用了 `cn-beijing` 区域。 **Solution**: Change `VOLCENGINE_TOS_REGION` to `cn-hongkong` and create a new bucket in that region. / 将 `VOLCENGINE_TOS_REGION` 改为 `cn-hongkong`，并在该区域新建存储桶。 **Error**: `API returned error: invalid API key` **Cause**: Using old Speech console API key, or key from wrong console page. / 使用了旧版语音控制台的 API Key，或从错误的控制台页面获取。 **Solution**: Get API key from the NEW Doubao Speech console at `https://console.volcengine.com/speech/new/`, NOT `/speech/app`. / 从新版豆包语音控制台 `https://console.volcengine.com/speech/new/` 获取 API Key，不是 `/speech/app`。 **Error**: `Unsupported audio format` or transcription returns empty **Cause**: Audio file is corrupted, or format not in supported list. / 音频文件损坏，或格式不在支持列表中。 **Solution**: Ensure file is one of WAV, MP3, MP4, M4A, OGG, FLAC and not corrupted. Try `--format` flag to explicitly specify format. / 确保文件是 WAV、MP3、MP4、M4A、OGG、FLAC 之一且未损坏。尝试用 `--format` 参数显式指定格式。 **Error**: `Missing: VOLCENGINE_ACCESS_KEY_ID...` after running `source .env` **Cause**: `source .env` sets variables in the current shell but does not export them to child processes. The script runs as a subprocess and cannot see unexported variables. / `source .env` 仅在当前 shell 设置变量但不导出，脚本作为子进程无法读取未导出的变量。 **Solution**: Use `set -a && source .env && set +a` to auto-export all variables, or use `export` before each variable in your `.env` file. / 使用 `set -a && source .env && set +a` 自动导出所有变量，或在 `.env` 文件中每行变量前加 `export`。

path: README.md
content: # doubao-asr

> Agent Skill for transcribing audio files via ByteDance Volcengine **Seed-ASR 2.0** (豆包录音文件识别模型2.0)

Best-in-class Chinese speech recognition — Mandarin, Cantonese, Sichuan dialect, and 13+ languages. With speaker diarization, up to 5 hours / 512MB per file.

中文语音识别准确率业界领先——支持普通话、粤语、四川话等方言及 13+ 种语言。自带说话人分离，单文件最长 5 小时 / 512MB。

## Why this skill?

Setting up Volcengine's Doubao Audio File Recognition 2.0 (豆包录音文件识别模型2.0, recorded audio → text) from scratch involves **4 environment variables across 3 different console pages** (Speech console, IAM, TOS). Without guidance, this typically takes 1-2 hours of doc-hunting and trial-and-error.

This skill provides **step-by-step bilingual setup instructions** (中英双语) baked into `SKILL.md`, so your AI agent can walk you through the entire process in ~10 minutes.

从零配置火山引擎豆包录音文件识别模型2.0（录音转文字）涉及 **3 个不同控制台页面的 4 个环境变量**。没有引导的话通常需要 1-2 小时翻文档踩坑。本 skill 在 `SKILL.md` 中内置了**中英双语分步引导**，AI agent 可以在约 10 分钟内带你完成全部配置。

## Install

### Claude Code

```bash
claude /install-skill https://github.com/vahnxu/doubao-asr
```

Or manually copy to `~/.claude/skills/doubao-asr/`.

### OpenClaw / ClawHub

Available on [ClawHub](https://clawhub.com) as `doubao-asr`.

### Manual

```bash
git clone https://github.com/vahnxu/doubao-asr.git
cd doubao-asr
pip install requests
python3 scripts/transcribe.py /path/to/audio.m4a
```

## Quick start

```bash
# Basic transcription
python3 scripts/transcribe.py /path/to/audio.m4a

# Save to file
python3 scripts/transcribe.py /path/to/audio.m4a --out /tmp/transcript.txt

# JSON output
python3 scripts/transcribe.py /path/to/audio.m4a --json --out /tmp/result.json

# Direct URL (skip upload)
python3 scripts/transcribe.py https://example.com/audio.mp3

# Disable speaker diarization
python3 scripts/transcribe.py /path/to/audio.m4a --no-speakers
```

## Credentials

See the detailed setup guide in [SKILL.md](./SKILL.md#credentials-setup) — with step-by-step instructions for each environment variable.

| Variable | Required | Description |
|---|---|---|
| `VOLCENGINE_API_KEY` | Yes | ASR API key (UUID) from Speech console |
| `VOLCENGINE_ACCESS_KEY_ID` | Yes | IAM Access Key ID (starts with `AKLT`) |
| `VOLCENGINE_SECRET_ACCESS_KEY` | Yes | IAM Secret Access Key |
| `VOLCENGINE_TOS_BUCKET` | Yes | TOS bucket name |
| `VOLCENGINE_TOS_REGION` | Yes | TOS region code, must match bucket region. Overseas: e.g. `cn-hongkong`, `ap-southeast-1`; China: `cn-beijing` |

## Supported formats

WAV, MP3, MP4, M4A, OGG, FLAC — up to 5 hours, 512MB max.

## License

Apache-2.0


path: references/skill-guide-insights.md
content: # Prompt: 基于《The Complete Guide to Building Skills for Claude》的 doubao-asr 迭代建议

> 以下内容来自另一个 session 的分析成果。该 session 完整阅读了 Anthropic 官方发布的《The Complete Guide to Building Skills for Claude》(PDF, 32页)，并结合 doubao-asr 项目现状给出了具体的迭代建议。请消化这些信息并执行改进。

---

## 一、指南核心要点摘要

### 1. Skill 文件结构规范

```
your-skill-name/          # kebab-case 命名
├── SKILL.md               # 必须，大小写敏感
├── scripts/               # 可选，可执行代码
├── references/            # 可选，按需加载的文档
└── assets/                # 可选，模板、图标等
```

- 不要在 skill 文件夹内放 README.md（README 用于 GitHub 仓库层面给人类看，不放在 skill 内部）
- SKILL.md 建议控制在 5000 词以内

### 2. 三级渐进披露（Progressive Disclosure）

这是指南最核心的设计理念：

- **第一级（YAML frontmatter）**：始终加载到 Claude 的 system prompt。只提供"这个 skill 干什么、何时触发"的最少信息。
- **第二级（SKILL.md body）**：当 Claude 判断 skill 与当前任务相关时加载。包含完整指令。
- **第三级（references/ 链接文件）**：Claude 按需导航和发现。详细文档放这里。

目的：最小化 token 消耗，同时保留专业能力。

### 3. YAML Frontmatter — description 字段的写法

description 必须同时包含：
- **做什么**（What it does）
- **何时使用**（When to use — trigger conditions）
- 具体的用户可能说的短语
- 相关文件类型（如适用）
- 不超过 1024 字符，不含 XML 标签

好的例子：
```yaml
description: "Analyzes Figma design files and generates developer handoff documentation. Use when user uploads .fig files, asks for 'design specs', 'component documentation', or 'design-to-code handoff'."
```

坏的例子：
```yaml
description: "Helps with projects."  # 太模糊
description: "Creates sophisticated multi-page documentation systems."  # 缺少触发条件
```

**负面触发（Negative Triggers）**：防止 overtriggering
```yaml
description: "...Do NOT use for simple data exploration (use data-viz skill instead)."
```

### 4. 三类 Skill 用例

| 类别 | 说明 | doubao-asr 属于 |
|------|------|----------------|
| Category 1: 文档/资产创建 | 创建一致的高质量输出 | ❌ |
| Category 2: 工作流自动化 | 多步骤流程 + 一致方法论 | ❌ |
| Category 3: MCP 增强 | 为工具访问添加工作流知识 | ✅ 最接近 |

### 5. 五种设计模式

- **Pattern 1: 顺序工作流编排** — 多步骤按特定顺序执行
- **Pattern 2: 多 MCP 协调** — 跨多个服务的工作流
- **Pattern 3: 迭代精炼** — 输出质量通过迭代提升
- **Pattern 4: 上下文感知工具选择** — 同一目标，根据上下文选不同工具（doubao-asr 的区域选择逻辑适用此模式）
- **Pattern 5: 领域知识嵌入** — skill 添加超越工具访问的专业知识（doubao-asr 的 Agent Instruction 和错误黑名单正是此模式）

### 6. 测试方法论

指南推荐三层测试：

1. **触发测试**：skill 是否在正确时机加载？（目标：90% 相关查询触发）
2. **功能测试**：输出是否正确？API 调用是否成功？
3. **性能对比**：有无 skill 的效果差异

调试方法：问 Claude "When would you use the [skill name] skill?" — Claude 会引用 description 回答，据此调整。

### 7. Troubleshooting 段的推荐格式

```markdown
## Troubleshooting
### Error: [Common error message]
Cause: [Why it happens]
Solution: [How to fix]
```

### 8. 其他要点

- `allowed-tools` 字段可限制 skill 的工具访问范围（如 `"Bash(python3:*)"`)
- 关键指令放在 SKILL.md 顶部，使用 `## Important` 或 `## Critical` 标题
- 对于关键验证，考虑用 script 而非语言指令——代码是确定性的，语言解释不是
- 对付模型"偷懒"：加 "Take your time to do this thoroughly" / "Quality is more important than speed"（但指南指出这在用户 prompt 中比在 SKILL.md 中更有效）

---

## 二、doubao-asr 现状评估

### 已经做得好的

1. ✅ description 包含触发词（"transcribe recorded audio files", "Doubao/豆包/Volcengine/火山引擎"）
2. ✅ kebab-case 命名 `doubao-asr`
3. ✅ metadata 完整（author, version, homepage, tags, requires）
4. ✅ Agent Instruction v2 + 错误黑名单 — 完全对应 Pattern 5（领域知识嵌入），这是项目亮点
5. ✅ transcribe.py 有重试机制和清晰错误信息
6. ✅ 区域选择决策树 — 对应 Pattern 4（上下文感知工具选择）

### 需要改进的

以下按优先级排序：

---

## 三、具体迭代任务（请执行）

### 任务 1: 🔴 README.md 同步 TOS_REGION 为 Required（2 分钟）

README.md 凭证表中 `VOLCENGINE_TOS_REGION` 标记为 `Required: No`，但最新 commit（8a6f2ca）已将其改为必填。需同步更新为 `Required: Yes`。

### 任务 2: 🔴 description 补充负面触发条件（5 分钟）

当前 description 没有排除条件，可能导致 overtriggering。在 description 末尾追加：

```
Do NOT use for real-time/streaming speech recognition, text-to-speech (TTS), or live captioning.
```

注意保持总长度在 1024 字符以内。

### 任务 3: 🟡 SKILL.md 添加 Troubleshooting 段（10 分钟）

在 SKILL.md 末尾（Supported formats 之后）添加：

```markdown
## Troubleshooting

### TOS upload extremely slow (~15KB/s) / TOS 上传极慢
**Cause**: Bucket region mismatch — server is overseas but bucket is in cn-beijing
**Solution**: Create a new bucket in `cn-hongkong`, update `VOLCENGINE_TOS_REGION=cn-hongkong`

原因：桶区域不匹配——服务器在海外但桶在 cn-beijing
解决：在 cn-hongkong 新建桶，更新 VOLCENGINE_TOS_REGION=cn-hongkong

### Submit failed: status code not 20000000 / 提交失败
**Cause**: Invalid or expired API key
**Solution**: Re-generate API key at https://console.volcengine.com/speech/new/ → API Call → Step 1

原因：API Key 无效或过期
解决：在 https://console.volcengine.com/speech/new/ → API 调用 → 第一步，重新创建

### "Silent audio, no transcript" / 静音音频
**Cause**: Audio file has no detectable speech
**Solution**: Verify audio plays correctly. Ensure format is supported (WAV/MP3/MP4/M4A/OGG/FLAC)

原因：音频中未检测到语音
解决：确认音频能正常播放，格式受支持

### TOS upload failed (403) / TOS 上传 403 错误
**Cause**: Bucket policy not configured or IAM key mismatch
**Solution**: Check bucket → Permission Management → verify "Folder Read/Write" policy exists and is applied to your main account

原因：桶策略未配置或 IAM 密钥不匹配
解决：进入桶 → 权限管理 → 确认已添加「文件夹读写」策略并授权给当前主账号
```

### 任务 4: 🟡 创建触发测试用例（10 分钟）

创建 `references/trigger-tests.md`：

```markdown
# doubao-asr Trigger Tests

Based on Anthropic's skill testing methodology.

## Should trigger ✅

- "帮我转写这段录音"
- "用豆包识别这个音频文件"
- "transcribe this audio file"
- "我有一段会议录音需要转成文字"
- "火山引擎语音识别"
- "doubao asr this mp3"
- "把这个 m4a 文件转成文字"
- "transcribe with speaker diarization"
- "用 Volcengine 转写"
- "帮我识别这段音频里说了什么"

## Should NOT trigger ❌

- "帮我做语音合成"（TTS, not ASR）
- "实时语音转文字"（streaming, not recorded file）
- "翻译这段文字"（translation, not transcription）
- "帮我写 Python 代码"（general programming）
- "用 Whisper 转写"（different tool）
- "今天天气怎么样"（unrelated）
- "帮我录一段音频"（recording, not transcription）

## Debugging method

Ask Claude: "When would you use the doubao-asr skill?"
Claude should quote the description back. Adjust based on what's missing.
```

### 任务 5: 🟢 考虑拆分凭证配置到 references/（20 分钟）

这是一个需要权衡的改进。指南建议 SKILL.md 控制在 5000 词以内，详细文档放 references/。但 doubao-asr 的 Agent Instruction 要求"展示完整引导"。

**折中方案**：
- 创建 `references/credentials-setup.md`，将 Step 1-3 完整内容移入
- SKILL.md 凭证段改为简短概述 + 指向 references 的链接
- Agent Instruction 改为：*"当用户需要配置凭证时，读取并展示 `references/credentials-setup.md` 的完整内容"*

**注意**：此任务需要验证 Claude 是否真的会按指令去读取 references/ 文件。如果不确定，可以先跳过，等有测试环境后再做。

### 任务 6: 🟢 添加 allowed-tools 字段（2 分钟）

在 YAML frontmatter 中添加：

```yaml
allowed-tools: "Bash(python3:*)"
```

这限制 skill 只能通过 Bash 执行 python3 命令，增加安全边界。

---

## 四、执行注意事项

1. 所有改动遵循项目现有的治理规范（AGENTS.md）
2. 改动后需写 task log（`docs/workspace/logs/YYYYMMDD_*.md`）
3. 显式 stage 文件（不要 `git add -A`）
4. 保持双语（中英文）风格一致
5. description 修改后可以用调试方法验证：问 Claude "When would you use the doubao-asr skill?"


path: references/trigger-tests.md
content: # Trigger Tests / 触发测试用例

Test cases to verify the skill triggers correctly (and does not trigger for out-of-scope requests).

## Should trigger (10 cases)

1. "帮我把这个录音转成文字" — Chinese transcription request
2. "Transcribe this audio file for me" — English transcription request
3. "用豆包把这段音频转写一下" — Explicit Doubao mention
4. "I have an M4A recording, can you convert it to text?" — Format-specific transcription
5. "帮我用火山引擎识别这段语音" — Volcengine mention
6. "Please transcribe the meeting recording at /tmp/meeting.mp3" — File path with transcription
7. "这个粤语录音能转文字吗" — Cantonese dialect mention
8. "I need speaker diarization for this audio" — Speaker diarization feature
9. "把这个 5 小时的会议录音转成文本" — Long audio transcription
10. "Use Seed-ASR to transcribe this file" — Model name mention

## Should NOT trigger (7 cases)

1. "帮我做一个实时语音识别的 demo" — Real-time/streaming ASR (out of scope)
2. "Can you convert this text to speech?" — TTS (text-to-speech, opposite direction)
3. "I need live captioning for my video stream" — Live captioning (out of scope)
4. "帮我用 Whisper 转写这段音频" — Different ASR provider (Whisper)
5. "Translate this Chinese text to English" — Translation, not transcription
6. "帮我录一段语音" — Voice recording, not transcription
7. "Generate subtitles in real-time for my livestream" — Real-time subtitles (out of scope)


path: scripts/transcribe.py
content: #!/usr/bin/env python3
"""
Doubao (豆包) Seed-ASR 2.0 Standard — audio file transcription.

API docs: https://www.volcengine.com/docs/6561/1354868
Endpoint: https://openspeech.bytedance.com/api/v3/auc/bigmodel/
Auth: x-api-key (single API key from Volcengine Speech console)
Resource-Id: volc.seedasr.auc

Audio upload: The Doubao API requires a publicly accessible URL.
This script uploads audio to Volcengine TOS (object storage) via presigned URL,
keeping data within Volcengine infrastructure. No extra SDK needed.
"""

import argparse
import hashlib
import hmac
import json
import os
import re
import sys
import time
import uuid
from datetime import datetime, timezone
from urllib.parse import quote, urlparse

try:
    import requests
except ImportError:
    sys.exit("requests is required: pip install requests")

SUBMIT_URL = "https://openspeech.bytedance.com/api/v3/auc/bigmodel/submit"
QUERY_URL = "https://openspeech.bytedance.com/api/v3/auc/bigmodel/query"
RESOURCE_ID = "volc.seedasr.auc"

FORMAT_MAP = {
    ".m4a": "m4a",
    ".mp3": "mp3",
    ".mp4": "mp4",
    ".wav": "wav",
    ".ogg": "ogg",
    ".flac": "flac",
}

MIME_MAP = {
    "m4a": "audio/mp4",
    "mp3": "audio/mpeg",
    "mp4": "audio/mp4",
    "wav": "audio/wav",
    "ogg": "audio/ogg",
    "flac": "audio/flac",
}

# --- TOS presigned URL upload (default) ---

TOS_REGION = os.environ.get("VOLCENGINE_TOS_REGION", "cn-beijing")
TOS_BUCKET = os.environ.get("VOLCENGINE_TOS_BUCKET", "")


def _tos_sign_v4(method, url, ak, sk, region, expires=3600):
    """Generate a Volcengine TOS V4 presigned URL (query-string auth)."""
    parsed = urlparse(url)
    now = datetime.now(timezone.utc)
    date_stamp = now.strftime("%Y%m%d")
    amz_date = now.strftime("%Y%m%dT%H%M%SZ")
    credential_scope = f"{date_stamp}/{region}/tos/request"
    signed_headers = "host"
    canonical_headers = f"host:{parsed.hostname}\n"

    query_params = {
        "X-Tos-Algorithm": "TOS4-HMAC-SHA256",
        "X-Tos-Credential": f"{ak}/{credential_scope}",
        "X-Tos-Date": amz_date,
        "X-Tos-Expires": str(expires),
        "X-Tos-SignedHeaders": signed_headers,
    }
    empty = ""
    canonical_qs = "&".join(
        f"{quote(k, safe=empty)}={quote(v, safe=empty)}"
        for k, v in sorted(query_params.items())
    )

    canonical_request = "\n".join([
        method,
        quote(parsed.path, safe="/"),
        canonical_qs,
        canonical_headers,
        signed_headers,
        "UNSIGNED-PAYLOAD",
    ])

    string_to_sign = "\n".join([
        "TOS4-HMAC-SHA256",
        amz_date,
        credential_scope,
        hashlib.sha256(canonical_request.encode()).hexdigest(),
    ])

    def _sign(key, msg):
        return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()

    signing_key = _sign(
        _sign(_sign(_sign(sk.encode("utf-8"), date_stamp), region), "tos"),
        "request",
    )
    signature = hmac.new(
        signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
    ).hexdigest()

    return (
        f"{parsed.scheme}://{parsed.hostname}{parsed.path}"
        f"?{canonical_qs}&X-Tos-Signature={signature}"
    )


def upload_to_tos(filepath, fmt):
    """Upload audio to Volcengine TOS and return a presigned GET URL."""
    ak = os.environ.get("VOLCENGINE_ACCESS_KEY_ID", "")
    sk = os.environ.get("VOLCENGINE_SECRET_ACCESS_KEY", "")
    bucket = TOS_BUCKET

    if not ak or not sk or not bucket:
        missing = []
        if not ak:
            missing.append("VOLCENGINE_ACCESS_KEY_ID")
        if not sk:
            missing.append("VOLCENGINE_SECRET_ACCESS_KEY")
        if not bucket:
            missing.append("VOLCENGINE_TOS_BUCKET")
        sys.exit(
            f"Missing: {', '.join(missing)}\n\n"
            "The Doubao ASR API requires audio via URL. This skill uploads to\n"
            "Volcengine TOS (object storage) — your audio stays within Volcengine.\n\n"
            "Setup (3 steps):\n"
            "  1. Create IAM Access Key: https://console.volcengine.com/iam/keymanage/\n"
            "  2. Create TOS Bucket: https://console.volcengine.com/tos/bucket/create\n"
            "  3. Set env vars:\n"
            "     export VOLCENGINE_ACCESS_KEY_ID='your_ak'\n"
            "     export VOLCENGINE_SECRET_ACCESS_KEY='your_sk'\n"
            "     export VOLCENGINE_TOS_BUCKET='your_bucket_name'"
        )

    # Validate bucket/region to prevent injection
    if not re.match(r'^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}$', bucket):
        sys.exit(f"Invalid TOS bucket name: {bucket}")
    if not re.match(r'^[a-z0-9-]+$', TOS_REGION):
        sys.exit(f"Invalid TOS region: {TOS_REGION}")

    object_key = f"doubao-asr/{uuid.uuid4()}.{fmt}"
    # Virtual-hosted style URL: bucket.endpoint/key
    url_raw = f"https://{bucket}.tos-{TOS_REGION}.volces.com/{object_key}"
    content_type = MIME_MAP.get(fmt, "application/octet-stream")

    put_url = _tos_sign_v4("PUT", url_raw, ak, sk, TOS_REGION, expires=300)

    # Retry with exponential backoff
    max_retries = 3
    for attempt in range(max_retries):
        try:
            with open(filepath, "rb") as f:
                resp = requests.put(
                    put_url, data=f,
                    headers={"Content-Type": content_type},
                    timeout=120,
                )
            if resp.status_code not in (200, 201):
                sys.exit(f"TOS upload failed ({resp.status_code}): {resp.text[:200]}")
            break
        except (requests.exceptions.SSLError, requests.exceptions.ConnectionError) as e:
            if attempt < max_retries - 1:
                wait = 2 ** (attempt + 1)
                print(f"  TOS upload error, retrying in {wait}s... ({attempt+1}/{max_retries})",
                      file=sys.stderr)
                time.sleep(wait)
            else:
                sys.exit(f"TOS upload failed after {max_retries} attempts: {e}")

    get_url = _tos_sign_v4("GET", url_raw, ak, sk, TOS_REGION, expires=3600)
    return get_url


def upload_audio(filepath, fmt):
    """Upload audio file and return an accessible URL."""
    print("  Uploading to Volcengine TOS...", file=sys.stderr)
    return upload_to_tos(filepath, fmt)


# --- Doubao ASR API ---

def get_headers(request_id, sequence=-1):
    [REDACTED_SECRET]"VOLCENGINE_API_KEY", "")
    if not [REDACTED_SECRET]
            "Missing VOLCENGINE_API_KEY\n\n"
            "Get your API key from the Volcengine Speech console:\n"
            "  https://console.volcengine.com/speech/new/\n\n"
            "Set: export VOLCENGINE_API_KEY='your_api_key'"
        )
    headers = {
        "Content-Type": "application/json",
        "x-api-key": api_key,
        "X-Api-Resource-Id": RESOURCE_ID,
        "X-Api-Request-Id": request_id,
    }
    if sequence is not None:
        headers["X-Api-Sequence"] = str(sequence)
    return headers


def submit(audio_url, fmt, speakers=True):
    """Submit a transcription task. Returns request_id."""
    request_id = str(uuid.uuid4())
    headers = get_headers(request_id, sequence=-1)
    body = {
        "user": {"uid": "openclaw-doubao-asr"},
        "audio": {"url": audio_url, "format": fmt},
        "request": {
            "model_name": "bigmodel",
            "enable_itn": True,
            "enable_punc": True,
            "enable_ddc": True,
            "show_utterances": True,
            "enable_speaker_info": speakers,
        },
    }

    max_retries = 3
    for attempt in range(max_retries):
        try:
            resp = requests.post(SUBMIT_URL, headers=headers, json=body, timeout=30)
            status = resp.headers.get("X-Api-Status-Code", "")
            message = resp.headers.get("X-Api-Message", "")
            if status != "20000000":
                sys.exit(f"Submit failed: {status} {message}")
            return request_id
        except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
            if attempt < max_retries - 1:
                wait = 2 ** (attempt + 1)
                print(f"  Submit error, retrying in {wait}s... ({attempt+1}/{max_retries})",
                      file=sys.stderr)
                time.sleep(wait)
            else:
                sys.exit(f"Submit failed after {max_retries} attempts: {e}")


def poll(request_id, timeout=600, interval=3):
    """Poll until the task completes. Returns the full result dict."""
    headers = get_headers(request_id, sequence=None)
    elapsed = 0
    net_errors = 0
    max_net_errors = 3
    while elapsed < timeout:
        try:
            resp = requests.post(QUERY_URL, headers=headers, json={}, timeout=30)
        except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
            net_errors += 1
            if net_errors >= max_net_errors:
                sys.exit(f"Poll failed after {max_net_errors} consecutive network errors: {e}")
            wait = 2 ** net_errors
            print(f"\n  Poll network error, retrying in {wait}s... ({net_errors}/{max_net_errors})",
                  file=sys.stderr)
            time.sleep(wait)
            elapsed += wait
            continue
        net_errors = 0  # reset on successful request
        status = resp.headers.get("X-Api-Status-Code", "")
        if status == "20000000":
            return resp.json()
        if status in ("20000001", "20000002"):
            print(f"\r  Transcribing... ({elapsed}s)", end="", file=sys.stderr)
            time.sleep(interval)
            elapsed += interval
            continue
        if status == "20000003":
            print("\n  Silent audio, no transcript.", file=sys.stderr)
            return {"result": {"text": "", "utterances": []}}
        message = resp.headers.get("X-Api-Message", "")
        sys.exit(f"Query failed: {status} {message}")
    sys.exit(f"Timeout after {timeout}s")


def main():
    parser = argparse.ArgumentParser(description="Doubao Seed-ASR 2.0 transcription")
    parser.add_argument("audio", help="Path to audio file (or URL)")
    parser.add_argument("--format", dest="fmt", help="Audio format (auto-detected from extension)")
    parser.add_argument("--out", help="Output file path (default: stdout)")
    parser.add_argument("--json", action="store_true", help="Output full JSON result")
    parser.add_argument("--no-speakers", action="store_true", help="Disable speaker diarization (enabled by default)")
    parser.add_argument("--timeout", type=int, default=600, help="Max wait seconds (default: 600)")
    args = parser.parse_args()

    # Support direct URL input (skip upload)
    if args.audio.startswith("http://") or args.audio.startswith("https://"):
        audio_url = args.audio
        fmt = args.fmt
        if not fmt:
            ext = os.path.splitext(urlparse(audio_url).path)[1].lower()
            fmt = FORMAT_MAP.get(ext)
        if not fmt:
            sys.exit("Cannot detect format from URL. Use --format to specify.")
    else:
        if not os.path.isfile(args.audio):
            sys.exit(f"File not found: {args.audio}")
        ext = os.path.splitext(args.audio)[1].lower()
        fmt = args.fmt or FORMAT_MAP.get(ext)
        if not fmt:
            sys.exit(f"Unknown audio format: {ext}. Use --format to specify.")
        audio_url = upload_audio(args.audio, fmt)

    # Submit task
    print("  Submitting transcription task...", file=sys.stderr)
    request_id = submit(audio_url, fmt, speakers=not args.no_speakers)

    # Poll for result
    data = poll(request_id, timeout=args.timeout)
    print("", file=sys.stderr)  # newline after progress

    result = data.get("result", {})
    utterances = result.get("utterances", [])

    # Output
    if args.json:
        output = json.dumps(data, ensure_ascii=False, indent=2)
    elif utterances and any(
        u.get("speaker") is not None or u.get("additions", {}).get("speaker") is not None
        for u in utterances
    ):
        # Format with speaker labels
        lines = []
        prev_speaker = None
        for u in utterances:
            speaker = u.get("speaker") or u.get("additions", {}).get("speaker")
            text = u.get("text", "").strip()
            if not text:
                continue
            if speaker != prev_speaker:
                label = f"Speaker {speaker}" if speaker is not None else "Speaker ?"
                lines.append(f"\n{label}:")
                prev_speaker = speaker
            lines.append(text)
        output = "\n".join(lines).strip()
    else:
        output = result.get("text", "")

    if args.out:
        out_path = os.path.realpath(args.out)
        # Block writes outside /tmp and current working directory
        cwd = os.path.realpath(os.getcwd())
        tmp = os.path.realpath("/tmp")
        if not (out_path.startswith(cwd + os.sep) or out_path.startswith(tmp + os.sep)):
            sys.exit(f"Output path not allowed (must be under working directory or /tmp): {args.out}")
        os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
        with open(out_path, "w", encoding="utf-8") as f:
            f.write(output)
        print(args.out)
    else:
        print(output)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)