Video Transcript Extraction
When to use
User shares a video URL and wants the spoken content transcribed to text. Works for Bilibili, YouTube, and other platforms by downloading audio and running ASR.
User Preference
Always use this workflow when the user shares a video link. The user explicitly requested: "以后我给你视频你就按照这样识别出给我". Do NOT ask which method to use — just do it.
Workflow
Step 1: Get video info
For Bilibili short URLs (b23.tv), resolve first:
curl -sL -o /dev/null -w "%{url_effective}" "https://b23.tv/xxx"
Extract BV ID from the redirect URL.
Get cid:
curl -s "https://api.bilibili.com/x/web-interface/view?bvid=BVxxx" \
-H "Referer: https://www.bilibili.com" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" \
| python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(d['cid'])"
Step 2: Download audio
Get audio stream URL from Bilibili playurl API:
curl -s "https://api.bilibili.com/x/player/playurl?bvid=BVxxx&cid=XXX&fnval=16&qn=64" \
-H "Referer: https://www.bilibili.com" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" \
| python3 -c "import json,sys; a=json.load(sys.stdin)['data']['dash']['audio']; print(sorted(a,key=lambda x:x['bandwidth'],reverse=True)[0]['baseUrl'])"
Download and convert:
curl -L -o /tmp/bili_audio.m4s "$AUDIO_URL" \
-H "Referer: https://www.bilibili.com" \
-H "User-Agent: Mozilla/5.0 ..."
ffmpeg -i /tmp/bili_audio.m4s -vn -acodec libmp3lame -q:a 2 /tmp/bili_audio.mp3 -y
Pitfall: yt-dlp does NOT work with Bilibili — returns 412 Precondition Failed. Always use the Bilibili API directly.
For Douyin (抖音)
Pitfall: yt-dlp does NOT work with Douyin — requires fresh cookies and still fails. Douyin web also blocks bots aggressively. Use SnapAny as primary method.
See also: references/douyin-transcript-workflow.md for full validated workflow.
PRIMARY: SnapAny + MiMo ASR (most reliable)
SnapAny (snapany.com) parses Douyin links and provides direct download URLs.
- Navigate to
https://snapany.com/zh/tiktok - Paste the Douyin URL into textbox (both
v.douyin.comshort links AND fulldouyin.com/video/URLs work — do NOT waste time converting to short links) - Click "提取视频图片", wait for results to load (button re-enables, download links appear)
- Extract download URLs via
browser_console:const dl = []; document.querySelectorAll('a').forEach(l => { if (l.href && (l.href.includes('douyinvod') || l.href.includes('douyinstatic') || l.href.includes('douyinpic'))) dl.push({text: l.textContent.trim(), href: l.href}); }); JSON.stringify(dl); - Fast path — direct audio link: If you see a "下载音频" link pointing to
douyinstatic.com(e.g.lf9-music-east.douyinstatic.com/obj/ies-music-hj/*.mp3), download it directly — no video download or ffmpeg needed:
⚠️ SSL pitfall: Some audio CDN domains (e.g.curl -L -o /tmp/douyin_audio.mp3 "$AUDIO_CDN_URL"sf6-cdn-tos.douyinstatic.com) may fail withSSL_ERROR_SYSCALLfrom certain networks. If curl hangs or fails, fall back to the video download path immediately — don't retry the same URL. - Fallback — video download + extract: Get "下载视频" URL (from
douyinvod.com), download and extract audio:
Note: thecurl -L -o /tmp/douyin_video.mp4 "$VIDEO_URL" -H "Referer: https://www.douyin.com/" ffmpeg -i /tmp/douyin_video.mp4 -vn -acodec libmp3lame -q:a 2 /tmp/douyin_audio.mp3 -y-H "Referer: https://www.douyin.com/"header is sometimes needed for douyinvod.com URLs. - Proceed to MiMo ASR section below
Pitfall: Douyin web page may show "你要观看的视频不存在" — this means the video is mobile-app-only, NOT deleted. SnapAny can still parse it.
FALLBACK: browser-act network interception
If SnapAny fails, try browser-act:
- Open
https://www.douyin.com/video/{VIDEO_ID}in browser-act - If page shows "视频不存在" or "视频数据加载中" forever → switch back to SnapAny
- Otherwise intercept video URLs from network requests
Then proceed to MiMo ASR section below.
Step 3: Verify API key (CRITICAL — do this before ASR)
MiMo API keys can expire. Always verify before starting a long transcription:
curl -s "https://token-plan-cn.xiaomimimo.com/v1/models" -H "api-key: YOUR_KEY...
If this returns Invalid API Key, ask the user for a fresh key immediately. Do NOT proceed with transcription attempts that will all fail.
Step 4: ASR with MiMo
⚠️ CRITICAL: Always chunk audio first! MiMo ASR truncates transcripts for files >2MB MP3 (~3min). A 7-minute video returned only ~500 chars without chunking. See references/douyin-transcript-workflow.md Method 3 for chunking commands.
Use execute_code to call MiMo ASR API:
- Endpoint:
https://token-plan-cn.xiaomimimo.com/v1/chat/completions - Model:
mimo-v2.5-asr - API Key: stored in memory (MiMo ASR key)
- Audio format: base64-encoded MP3 in
input_audiocontent block - Header:
Authorization: Bearer <key>(also works withapi-key: <key>) - Base64 size limit: 10MB
ASR payload structure:
{
"model": "mimo-v2.5-asr",
"messages": [{
"role": "user",
"content": [{
"type": "input_audio",
"input_audio": {
"data": f"data:audio/mpeg;base64,{audio_b64}",
"format": "mp3"
}
}]
}]
}
Pitfall: Omit asr_options.language on retry if first attempt has repetition issues — removing the language hint sometimes improves accuracy.
Pitfall: MiMo ASR uses /v1/chat/completions endpoint, NOT a dedicated /audio/transcriptions endpoint. Trying /audio/transcriptions returns 404.
Step 5: Format and deliver
Present the transcript with:
- Video title as heading
- Clean paragraph breaks
- Bold section markers where the speaker numbers items (一、二、三...)
For YouTube
YouTube is aggressively blocking automated transcript access. Multiple approaches fail:
youtube-transcript-apiPython package →RequestBlocked(IP banned)yt-dlp --write-auto-sub→Sign in to confirm you're not a bot- Direct
youtube.com/api/timedtext→ Empty response - Third-party services (downsub.com) → Cloudflare blocked
PRIMARY: browser-act + saveanyyoutube.com (verified working)
This is the most reliable method. It uses browser-act to intercept the subtitle API call.
- Start browser-act session:
browser-act --session yt browser open <browser_id> "https://www.saveanyyoutube.com/watch?v={VIDEO_ID}" - Wait for page to load:
browser-act --session yt wait stable - Scroll down to find subtitle section:
browser-act --session yt scroll down - Find and click the SRT download button (look for "Subtitle" → "SRT" → "Download")
- Intercept the API call via
browser-act --session yt network requests --filter subtitle --type xhr,fetch - Get the subtitle URL from the API response:
browser-act --session yt network request <request_id> - The response contains
subtitles[0].url— this is the YouTube timedtext URL - Fetch the URL directly with Python requests to get the transcript
- Parse JSON3 format: extract
events[].segs[].utf8and join - Save to
~/youtube-scripts/{video_title}_字幕.txt
Key API details:
- saveanyyoutube.com calls
https://service.saveanyyoutube.com./api/video/subtitleswith POST body{"url": "https://www.youtube.com/watch?v={VIDEO_ID}" - Response contains
subtitles[0].urlwhich is a YouTube timedtext URL - The timedtext URL returns JSON3 format with
events[].segs[].utf8text segments
Pitfall: browser-act requires API Key. Run browser-act auth set <KEY> first.
FALLBACK: youtube-transcript.io
If browser-act is unavailable, use the browser to extract transcripts via youtube-transcript.io:
browser_navigatetohttps://www.youtube-transcript.io/?v={VIDEO_ID}- Paste the YouTube URL and click "Extract transcript"
- Wait for loading, then extract text via
browser_consolewithdocument.body.innerText - Save to
~/youtube-scripts/{video_title}_字幕.txt
LAST RESORT: Ask user to download manually
If both methods fail, ask the user to:
- Open saveanyyoutube.com in their browser
- Paste the YouTube link
- Click Caption Downloader → English (auto-generated) → SRT → Download
- Save the SRT file to
~/youtube-scripts/
When to use which method
| Scenario | Method |
|---|---|
| YouTube video, transcript API works | youtube-content skill |
| YouTube video, transcript API blocked | youtube-transcript.io via browser |
| YouTube video, all APIs blocked | Ask user to download SRT manually |
| Bilibili video | Bilibili API + MiMo ASR (this skill) |
| Douyin video | SnapAny + MiMo ASR (this skill) — full URLs work fine |
Dependencies
ffmpeg(pre-installed on macOS)- No Python packages needed (uses stdlib
urllib+base64)
⚠️ Proxy Rule
When using mihomo for proxy, do NOT set system global proxy via networksetup. Use rule-based routing only. Global proxy causes other services to lose connection. User explicitly said: "开代理不要开全局,按规则连接,不然你会断开连接,不想失去你".
Proxy Setup for yt-dlp (YouTube)
yt-dlp needs proxy to access YouTube from this machine. mihomo is installed but has no launchd service — must be started manually.
Start mihomo (if not running)
# Check if already running
lsof -i :7890 2>/dev/null | head -3
# If not running, start in background
mihomo -d /Users/elham/.config/mihomo &>/dev/null &
sleep 2 && lsof -i :7890 2>/dev/null | head -3
- Config:
~/.config/mihomo/config.yaml - Mixed-port:
7890 - Binary:
/opt/homebrew/bin/mihomo
Use proxy with yt-dlp
yt-dlp --proxy socks5://127.0.0.1:7890 <URL>
yt-dlp YouTube download (ASR fallback)
When YouTube subtitles are unavailable, yt-dlp can download the video/audio for ASR transcription:
# Download audio only
yt-dlp --proxy socks5://127.0.0.1:7890 -x --audio-format mp3 -o "/tmp/%(title)s.%(ext)s" "URL"
# Download video (for audio extraction)
yt-dlp --proxy socks5://127.0.0.1:7890 -f "best[height<=720]" -o "/tmp/%(title)s.%(ext)s" "URL"
Then extract audio with ffmpeg and run MiMo ASR as usual.
Output
Present the full transcript directly in chat. No need to save to file unless user asks.