- Skill ID
- daymade/claude-code-skills/asr-transcribe-to-text
- Publisher
- daymade
- Repository
- claude-code-skills
- Installs
- 0
- Files
- 30
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
daymade/claude-code-skills/asr-transcribe-to-textInstalls these files- .gitignore
- SKILL.md
- references/decoupled_speaker_alignment.md
- references/local_mlx_guide.md
- references/overlap_merge_strategy.md
- references/speaker_diarization.md
- references/voiceprint_speaker_id.md
- references/whisper_word_timestamps.md
- scripts/align_speakers.py
- scripts/diarize_speakers.py
- scripts/diarize_speakers.py.lock
- scripts/fuse_whispercpp_diarization.py
- scripts/generate_audit_html.py
- scripts/overlap_merge_transcribe.py
- scripts/prepare_asr_input.py
- scripts/resolve_media_input.py
- scripts/speaker_time_contract.py
- scripts/speaker_transcribe.py
- scripts/speaker_transcribe.py.lock
- scripts/speaker_transcribe_cascade.py
- scripts/transcribe_local_mlx.py
- scripts/transcribe_long_whispercpp.py
- scripts/voiceprint_id.py
- scripts/word_timestamps_whisper.py
- tests/test_alignment_turns.py
- tests/test_long_audio_safety.py
- tests/test_skill_delivery_contract.py
- tests/test_speaker_bundle_time_contract.py
- tests/test_whispercpp_fusion.py
- tests/test_whispercpp_long_audio.py
What this skill tells the agent
ASR Transcribe to Text
Transcribe audio/video to speaker-labeled text. Local execution has two explicit routes. Long or unattended recordings use checkpointed whisper.cpp + Silero VAD blocks, then late-fuse pyannote speakers. Short/medium recordings may use the Qwen3-ASR + mlx-whisper alignment route. Neither route cuts ASR input at diarization turns; speaker attribution happens after continuous-context ASR.
Route before ASR: the transcript is the result, not the run
Before starting transcription, check the owning project's transcript catalog, external source index, and declared prior-work carriers for an existing canonical transcript using source ID, date, title, and entity terms. A verified human-reviewed/current transcript ends the task unless the user explicitly asked for a new independent comparison. Raw audio existing is not a reason to regenerate text that already exists.
When no canonical transcript exists:
- For ordinary meeting/DJI recordings where cloud processing is allowed, use Feishu Minutes as the normal primary route (preprocess to a small M4A first).
- Use local ASR when the user requires offline/privacy handling, Feishu is unavailable or failed, or the task explicitly needs an independent quality comparison.
2b. For a second, independent speaker timeline, use StepFun's async file endpoint via the stepfun-asr skill's asr_file.py. It is the only vendor diarization route here that does not share code or models with local pyannote, so agreement between the two is real corroboration rather than one tool agreeing with itself. Measured 2026-09-19 on a 10-minute film: both split the key turn identically, while Feishu Minutes mis-attributed it. The constraint is that it fetches a public URL — base64 is rejected — so it needs staging (see aliyun-multi-account.md); reach for it when an attribution is load-bearing, not for routine transcripts.
- For non-meeting media or an explicit local/remote ASR request, choose the execution location by the audio-location rule below.
Do not run local ASR merely to make a two-route process look complete.
| Mode | When | Speed | Cost |
|---|---|---|---|
| Local MLX | macOS Apple Silicon | 15-27x realtime | Free |
| Remote API | Any platform, or when local unavailable | Depends on GPU | API/self-hosted |
Choosing between them is usually not about speed — it's about where the audio already is. A remote GPU can be several times faster (a 4090 running vLLM measured ~61x realtime against ~15x for local MLX), but that gap is small change next to moving the files: transcription output is text, and text is ~10,000× smaller than the audio it came from (18.5 h of speech ≈ 330 K characters ≈ 1 MB, from ~2.6 GB of WAV). So:
Transcribe where the audio already lives, and move only the transcript.
Pulling a few hundred MB across a slow link to reach a faster GPU routinely costs more wall-clock than the entire transcription — measured once at 63 KB/s, which is over two hours for 500 MB, to save minutes of compute. If the recording is already on the remote box (it was recorded there, downloaded there, or lives in a share mounted there), run the ASR there and bring back the .txt.
Configuration persists in ${CLAUDE_PLUGIN_DATA}/config.json.
Speaker labels are the default. Every run produces[start-end] SPEAKER_xx: text+ CSV. Plain-text-only output is the opt-out (--no-diarization) for monologues, podcasts, or when you just want a summary — see Step 3. It also drops every timestamp: the plain-text path returns the session text and nothing else. A monologue that needs line-level timestamps (subtitles, jump-to-moment archives) runs the full pipeline and ignores theSPEAKER_00labels. One-time setup for diarization: pyannote is a gated HuggingFace model — it needs a token once (## Speaker Diarization & Identificationbelow). First run without it FAILS with setup steps; after setup, full capability is permanent and auto-detected.
Step 0: Detect Platform and Load Config
cat "${CLAUDE_PLUGIN_DATA}/config.json" 2>/dev/nullIf config exists, read values and proceed to Step 1.
If config does not exist, auto-detect platform first:
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')
"Then use AskUserQuestion with platform-aware defaults:
For macOS Apple Silicon (recommended: local):
ASR setup — your Mac has Apple Silicon, so local transcription is recommended.
Q1: Transcription mode?
A) Local MLX — runs on your Mac's GPU, no API key needed, 15-27x realtime (Recommended)
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 connectionFor other platforms (recommended: remote):
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) NoSave config:
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.')
"Step 1: Resolve Input
Accept local files, direct media URLs, or web/podcast episode pages.
- Web or podcast page URL: inspect the page for an existing transcript first. Use an official/platform transcript only when it is directly accessible to the user's account. If the transcript endpoint requires a login token and none is available, say that clearly and fall back to ASR from the audio URL.
- Local file, direct media URL, or page URL fallback: run the bundled resolver. It extracts media from common page metadata (
og:audio, media tags, JSON-LD, RSS-style enclosure links), downloads URLs with atomic temp-file replacement, verifies remoteContent-Lengthwhen present, computes SHA-256, and validates the result withffprobe.
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.jsonFor suspicious or high-value downloads, add --decode-check to make ffmpeg decode the whole file before transcription:
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-checkExpected output:
Downloaded ... bytes in ...s -> OUTPUT_DIR/episode-title.m4a
OUTPUT_DIR/episode-title.m4aMore from daymade
Use this skill in a project
Open any RiverX project, search the Skills panel for daymade/claude-code-skills/asr-transcribe-to-text, and install it in one click.
