locus-unity-bridge
Use when an agent needs to inspect or control a real Unity Editor through Locus, especially when Unity MCP is unavailable,…
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.
$ npx -y skills add Misaka-Mikoto-Tech/agent-skills --skill bilibili-page-reader --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/bilibili-page-readerContext preview
The summary Claude sees to decide when to auto-load this skill.
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.
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."
---
┌─ 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---
Use the one-shot evaluate below. It returns both subtitles and danmaku in a single call.
~/.kimi-webbridge/bin/kimi-webbridge status
{"action":"navigate","args":{"url":"https://www.bilibili.com/video/BV.../","newTab":true},"session":"bilibili"}Wait 2–3 seconds for BE to fully initialize.
(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
}
});
})(){
"sub": {
"count": 243,
"text": "[0:00] 大家好\n[0:01] 这个视频...",
"source": "投稿字幕"
},A personal collection of reusable AI agent skills and MCP-related utilities. 这个仓库用于收集个人日常使用中沉淀下来的 agent skills,包括网页读取、命令行调用规范、自动化流程、工具集成经验等。内容以可直接复制使用的 SKILL.md 为主,后续也可能加入 MCP server 配置和示例。
Use when an agent needs to inspect or control a real Unity Editor through Locus, especially when Unity MCP is unavailable,…
Use when writing or running PowerShell on Windows, especially native programs, quoted paths, escaping, pwsh, Start-Process, file…