Skip to main content

bilibili-page-reader

Get content from Bilibili videos: official subtitles, danmaku (density/peaks/sample), comments. When no subtitles exist (no 投稿字幕), fall back to downloading audio + ASR transcription via FunASR.

Aller à l'installation

Informations de source

Dépôt
ToddModica/upstream-skills
Dernière activité de la source
12 septembre 2026 à 02:06
Langue détectée de SKILL.md
anglais
Étoiles
1
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
5 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
bilibili-page-reader
description
Get content from Bilibili videos: official subtitles, danmaku (density/peaks/sample), comments. When no subtitles exist (no 投稿字幕), fall back to downloading audio + ASR transcription via FunASR.
# Bilibili Page Reader ## Core Rules - **Browser access** via `kimi-webbridge` for page state, login-only data, Bilibili Evolved (BE) providers. - **Do not click BE download buttons.** Call providers directly via evaluate. - **Always use a named session.** A session-less evaluate may land on a different tab and silently return wrong data. - **Network calls outside the browser** (audio download, playurl API) use Node.js — PowerShell's `curl.exe` and Python's requests both get blocked by Bilibili CDN TLS fingerprinting on this platform. --- ## Workflow Overview ``` ┌─ BE downloadSubtitles provider ──→ 投稿字幕 (timestamps) │ BiliBili video ───┼─ BE downloadDanmaku provider ────→ danmaku density/peaks/sample │ └─ No subtitles? ──→ Audio transcription fallback 1. Get audio stream URL (Node.js → playurl API) 2. Download .m4s audio 3. ffmpeg → .m4a 4. FunASR paraformer-zh → SRT with timestamps ``` --- ## Phase 1: Subtitles (preferred — via BE) Use the one-shot evaluate below. It returns both subtitles and danmaku in a single call. ### Setup ```bash ~/.kimi-webbridge/bin/kimi-webbridge status ``` ```json {"action":"navigate","args":{"url":"https://www.bilibili.com/video/BV.../","newTab":true},"session":"bilibili"} ``` Wait 2–3 seconds for BE to fully initialize. ### One-shot evaluate ```js (async () => { const pa = window.bilibiliEvolved.pluginApis; // ── Identifiers ── const s = window.__INITIAL_STATE__ || {}; const vd = s.videoData || {}; const bvid = vd.bvid || s.bvid || location.pathname.match(/BV[\w]+/)?.[0]; const aid = vd.aid || s.aid; const pages = vd.pages || []; const p = parseInt(new URLSearchParams(location.search).get('p') || '1') - 1; const cid = pages[p]?.cid || vd.cid || s.cid || pages[0]?.cid; const title = vd.title || document.title; // ── Register providers ── pa.registerData('downloadVideo.assets', []); // ── Poll for providers (downloadDanmaku loads async, ~1-2s) ── function getProviders() { const g = pa.getData('downloadVideo.assets'); return Array.isArray(g[0]) ? g.flat() : g; } const deadline = Date.now() + 5000; let providers = getProviders(); while (!providers.find(p => p.name === 'downloadDanmaku') && Date.now() < deadline) { await new Promise(r => setTimeout(r, 300)); providers = getProviders(); } // ── Subtitles: try 投稿字幕 first ── let subResult = { count: 0, text: '', source: 'none' }; const subProvider = providers.find(p => p.name === 'downloadSubtitles'); if (subProvider) { try { const subAssets = await subProvider.getAssets([{ input: {} }], { type: 'json', enabled: true }); const subRaw = subAssets[0].data; let subText; if (subRaw instanceof Blob) { const buf = await subRaw.arrayBuffer(); subText = new TextDecoder('utf-8').decode(buf); } else { subText = String(subRaw); } const subtitles = JSON.parse(subText); const subLines = subtitles.map(s => { const totalSec = Math.floor(s.from); const h = Math.floor(totalSec / 3600); const m = Math.floor((totalSec % 3600) / 60); const sec = String(totalSec % 60).padStart(2, '0'); if (h > 0) { return '[' + h + ':' + String(m).padStart(2, '0') + ':' + sec + '] ' + s.content; } return '[' + m + ':' + sec + '] ' + s.content; }); subResult = { count: subtitles.length, text: subLines.join('\n'), source: '投稿字幕' }; } catch(e) { subResult = { count: 0, text: '', source: '投稿字幕_error' }; } } // ── Danmaku: analyze in-page, summary only ── const dmk = providers.find(p => p.name === 'downloadDanmaku'); const dmkAssets = await dmk.getAssets( [{ input: { aid: String(aid), cid: String(cid) } }], { type: 'json', enabled: true } ); const dmkRaw = dmkAssets[0].data; let dmkText; if (dmkRaw instanceof Blob) { const buf = await dmkRaw.arrayBuffer(); dmkText = new TextDecoder('utf-8').decode(buf); } else { dmkText = String(dmkRaw); } const danmaku = JSON.parse(dmkText); // Time density: 30s buckets const bucketSize = 30; const buckets = {}; for (const d of danmaku) { const b = Math.floor(d.progress / 1000 / bucketSize) * bucketSize; buckets[b] = (buckets[b] || 0) + 1; } const density = Object.entries(buckets) .map(([t, c]) => [Number(t), c]) .sort((a, b) => a[0] - b[0]); // Top 5 peak moments const peaks = density.slice().sort((a, b) => b[1] - a[1]).slice(0, 5); // Stratified sample: up to 40 entries across full timeline const sampleCount = Math.min(40, danmaku.length); const step = Math.max(1, Math.floor(danmaku.length / sampleCount)); const danmakuSample = []; for (let i = 0; i < danmaku.length && danmakuSample.length < sampleCount; i += step) { danmakuSample.push({ t: Math.floor(danmaku[i].progress / 1000), c: danmaku[i].content }); } const totalDuration = danmaku.length > 0 ? Math.max(...danmaku.map(d => d.progress)) : 0; // ── Return ── return JSON.stringify({ ok: true, bvid, aid: Number(aid), cid: Number(cid), p: p + 1, title, sub: subResult, dmk: { count: danmaku.length, timeSpanSec: Math.floor(totalDuration / 1000), density, peakMoments: peaks.map(pk => ({ timeSec: pk[0], count: pk[1] })), sample: danmakuSample } }); })() ``` ### Response shape ```json { "sub": { "count": 243, "text": "[0:00] 大家好\n[0:01] 这个视频...", "source": "投稿字幕" }, "dmk": { "count": 247, "density": [[0,11], [30,2], ...], "peakMoments": [{"timeSec": 1170, "count": 16}, ...], "sample": [{"t": 0, "c": "辛苦惹!"}, ...] } } ``` If `sub.count === 0`, no 投稿字幕 was available. Proceed to **Phase 2** below. --- ## Phase 2: Audio Transcription Fallback (when no subtitles exist) Use when Phase 1 returns `sub.count === 0`. This replaces the old "AI subtitle API" fallback which is unreliable — the `/x/player/v2` API often returns stale/empty subtitle data. ### Why this approach The Bilibili CDN uses **TLS fingerprinting** that blocks curl and Python requests. Reliable paths: | Method | Works? | Notes | |--------|--------|-------| | `curl.exe` with browser headers | ❌ Exit code 35 | SSL blocked | | Python `requests` / `urllib` | ❌ Blocked | Same reason | | Node.js `https.get()` | ✅ | Use `User-Agent` + `Referer` headers | | Browser fetch (in-page) | ✅ but evaluate timeout <1s | Only for fast API calls | | Node.js to download audio | ✅ | Backup URL is most stable | ### Step 1: Get audio stream URL via Node.js ```bash node -e " const https = require('https'); const url = 'https://api.bilibili.com/x/player/playurl' + '?bvid=BVxxxxxxxxx&cid=xxxxxxxxx&qn=0&fnval=4048&fourk=1&platform=web'; https.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' } }, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { const d = JSON.parse(data); if (d.code === 0 && d.data && d.data.dash && d.data.dash.audio) { const audio = d.data.dash.audio; const best = audio.reduce((a,b) => a.bandwidth > b.bandwidth ? a : b); console.log(best.baseUrl + '|' + (best.backupUrl ? best.backupUrl[0] || '' : '')); } else { console.log('ERROR: no dash audio data'); } }); }).on('error', (e) => { console.log('ERROR: ' + e.message); }); " ``` The `fnval=4048` flag requests DASH format with separate audio/video streams. ### Step 2: Download audio (use backup URL — most reliable) ```bash node -e " const https = require('https'); const fs = require('fs'); const url = 'BACKUP_URL_FROM_STEP_1'; const file = fs.createWriteStream('output.m4s'); https.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 ...', 'Referer': 'https://www.bilibili.com/' }, timeout: 60000 }, (res) => { if (res.statusCode !== 200) { console.log('HTTP ' + res.statusCode); return; } res.pipe(file); res.on('end', () => { console.log('OK size=' + file.bytesWritten); }); }).on('error', (e) => { console.log('ERR: ' + e.message); }); " ``` ### Step 3: Convert to standard audio (stream copy, no re-encode) ```bash ffmpeg -i output.m4s -c copy output.m4a -y ``` ### Step 4: ASR transcription via FunASR ```bash set MODELSCOPE_CACHE=.\cache\modelscope set MODELSCOPE_CREDENTIAL_PATH=.\cache\modelscope_cred python audio2srt.py output.m4a --srt --model paraformer-zh --punc-model ct-punc --spk-model cam++ ``` ### Step 5: Cleanup — remove intermediate audio files ```bash del output.m4s output.m4a ``` The only artifacts worth keeping are the `.srt` subtitle file. The raw `.m4s`, the converted `.m4a`, and any `.json` debug output are all intermediate and should be cleaned up after the SRT is confirmed valid. **What the script does:** - Loads `paraformer-zh` (Chinese ASR, 220M params) + `fsmn-vad` + `ct-punc` + `cam++` - Outputs SRT with per-sentence timestamps via `res[0]["sentence_info"]` - CPU perf: ~15x realtime (21 min audio ≈ 90 sec) - Handles ModelScope cache/credential path setup automatically **If `cam++` is too slow to download**, omit `--spk-model` and group `res[0]["timestamp"]` (per-character ms array) by punctuation boundaries manually. **If no `sentence_info`** in the output: the `spk_model` triggers sentence segmentation. Without it, you only get raw text + per-character timestamps. ### The `audio2srt.py` script Located next to this `SKILL.md` file. Copy it into the current workspace or call it by its skill-directory path. It handles: - Model loading with ModelScope credential path override (sandbox-safe) - Multiple output formats: SRT, VTT, JSON - Parsing `sentence_info`, `timestamp`, and raw text fallback - Cleaning SenseVoice special tags (`<|zh|>`, `<|HAPPY|>`, etc.) - Segment deduplication and time-sorting
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub