
Bilibili Analyzer
- 1k installs
- 84 repo stars
- Updated January 28, 2026
- aidotnet/moyucode
bilibili-analyzer is a Python agent skill that extracts structured insights, keyframes, faces, objects, and scene data from Bilibili videos for developers who need training data, research samples, or automated content an
About
bilibili-analyzer is a MoyuCode skill under skills/tools/bilibili-analyzer/scripts that analyzes Bilibili videos via `python main.py` with a BV or full URL. Flags control frame interval (-i), max frames (-m), output facets (-f text,objects,faces,scene), worker parallelism (-w 8), scene detection toggles, and custom output directories. Example workflows sample tutorial videos every 15 seconds up to 100 frames, quick-preview long videos at 120-second intervals with 20 frames, or interview analysis pulling faces, text, and scenes. Developers reach for bilibili-analyzer when building Chinese video datasets, multimodal training corpora, or automated scene summaries from Bilibili sources.
- Extracts keyframes at custom intervals with scene detection
- Supports multiple output formats including faces, text, objects, and scene
- Parallel processing with configurable worker count for faster analysis
- Generates Markdown reports, frame images, logs, and checkpoints
- Handles both full URLs and short b23.tv links
Bilibili Analyzer by the numbers
- 1,033 all-time installs (skills.sh)
- +11 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,012 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aidotnet/moyucode --skill bilibili-analyzerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1k |
|---|---|
| repo stars | ★ 84 |
| Security audit | 2 / 3 scanners passed |
| Last updated | January 28, 2026 |
| Repository | aidotnet/moyucode ↗ |
How do you extract frames and objects from Bilibili videos?
Extract structured insights, keyframes, faces, objects, and scene data from Bilibili videos for training data, research, or content analysis.
Who is it for?
ML engineers and researchers building datasets from Bilibili tutorial, interview, or long-form videos with configurable frame sampling.
Skip if: Developers analyzing YouTube or offline files without Bilibili URLs or teams needing real-time streaming inference instead of batch extraction.
When should I use this skill?
User provides a Bilibili BV URL and wants keyframes, faces, objects, scenes, or text extracted for training or content analysis.
What you get
Structured video analysis with keyframes, text, faces, objects, and scene detection output files
- Extracted keyframes
- Object and face labels
- Scene and text analysis output
Files
Bilibili Video Analyzer
Description
B站视频内容分析工具。提供视频URL后,自动下载视频、拆解成帧图片,然后使用AI分析内容,最终生成高质量的专题文档或实操教程。
核心特点:
- 不是简单的时间线记录,而是重新组织整理成一篇完整的文档
- 实操类视频 → 生成可直接使用的操作教程
- 知识类视频 → 生成结构化的专题文档
- 报告中插入关键截图,使用
格式
Source & Documentation
| 工具 | 用途 | 文档 |
|---|---|---|
| FFmpeg | 视频拆帧 | 官网 / 文档 |
| Bilibili API | 视频下载 | API文档 |
Installation
1. 安装 .NET 10 SDK
脚本使用 .NET 10 单文件执行功能,需要安装 .NET 10 SDK。
下载地址: https://dotnet.microsoft.com/download/dotnet/10.0
验证安装:
dotnet --version2. 安装 FFmpeg
Windows:
# Chocolatey
choco install ffmpeg
# 或 Scoop
scoop install ffmpeg
# 或手动下载: https://ffmpeg.org/download.htmlmacOS:
brew install ffmpegLinux:
# Ubuntu/Debian
sudo apt install ffmpeg
# CentOS/RHEL
sudo yum install ffmpeg验证安装:
ffmpeg -versionTrigger
/bilibili-analyzer命令- 用户请求分析B站视频
- 用户提供B站视频链接并要求分析
Provided Script
本 skill 提供了 scripts/prepare.cs 脚本用于下载视频和提取帧图片。
脚本位置: skills/tools/bilibili-analyzer/scripts/prepare.cs
运行方式: 使用 .NET 10 单文件执行功能
使用方法
# 基本用法
dotnet run scripts/prepare.cs "<视频URL>" -o <输出目录>
# 示例
dotnet run scripts/prepare.cs "https://www.bilibili.com/video/BV1xx411c7mD" -o ./output
# 长视频(降低帧率)
dotnet run scripts/prepare.cs "https://www.bilibili.com/video/BV1xx411c7mD" -o ./output --fps 0.5参数说明
| 参数 | 说明 | 默认值 |
|---|---|---|
url | B站视频URL(必需) | - |
-o, --output | 输出目录 | 当前目录 |
--fps | 每秒提取帧数 | 1.0 |
--similarity | 相似度阈值(0-1),超过此值的相邻帧会被去重 | 0.80 |
--no-dedup | 禁用相似帧去重 | false |
--video-only | 只下载视频,不提取帧 | false |
--frames-only | 只提取帧(需已有video.mp4) | false |
相似帧去重
脚本会自动对相邻帧进行相似度检测,去除相似度超过阈值(默认80%)的重复帧:
- 使用 ffmpeg 的 SSIM/PSNR 算法计算相似度
- 只比较相邻帧,不会跨帧比较
- 去重后自动重新编号(frame_0001.jpg, frame_0002.jpg, ...)
- 可通过
--similarity 0.85调整阈值 - 可通过
--no-dedup禁用去重
输出结构
<输出目录>/
├── video.mp4 # 下载的视频文件
└── images/ # 帧图片目录
├── frame_0001.jpg
├── frame_0002.jpg
├── frame_0003.jpg
└── ...Workflow (Prompt)
你是一个视频内容分析助手。当用户提供B站视频链接时,按以下步骤执行:
Step 1: 下载视频并拆帧
使用提供的脚本下载视频并拆解成帧图片:
dotnet run skills/tools/bilibili-analyzer/scripts/prepare.cs "<视频URL>" -o <输出目录>注意事项:
- 短视频(<10分钟): 使用默认
--fps 1 - 中等视频(10-30分钟): 使用
--fps 0.5 - 长视频(>30分钟): 使用
--fps 0.2
Step 2: 分析帧图片
使用 Task 工具分批并行分析 images/ 目录中的图片。
分批策略(根据总图片数动态计算):
| 总图片数 | 分批数量 | 每批图片数 |
|---|---|---|
| 1-30 | 1 批 | 全部 |
| 31-60 | 2 批 | 约 15-30 张/批 |
| 61-120 | 3 批 | 约 20-40 张/批 |
| 121-200 | 4 批 | 约 30-50 张/批 |
| 200+ | 5 批 | 平均分配 |
计算公式:
总图片数 <= 30: 1 批
总图片数 <= 60: 2 批
总图片数 <= 120: 3 批
总图片数 <= 200: 4 批
总图片数 > 200: 5 批
每批图片数 = 总图片数 / 分批数量(向上取整)示例:假设有 85 张图片 → 分 3 批
Task 1: 分析 frame_0001.jpg ~ frame_0029.jpg(29张)
Task 2: 分析 frame_0030.jpg ~ frame_0058.jpg(29张)
Task 3: 分析 frame_0059.jpg ~ frame_0085.jpg(27张)Task Prompt 模板:
读取并分析 <输出目录>/images/ 目录下的 frame_0001.jpg 到 frame_0020.jpg(共20张图片)。
【重要要求】
你的响应必须是这些图片内容的【完整详细报告】,不要省略任何信息。
对每张图片,详细记录:
1. **帧号**: frame_xxxx.jpg
2. **场景类型**: 代码编辑器/终端/浏览器/PPT/对话/其他
3. **界面内容**:
- 窗口标题、菜单、按钮等UI元素
- 当前打开的文件/页面
4. **文字内容**:
- 完整转录屏幕上的所有文字
- 代码内容(完整复制,保留格式)
- 终端命令和输出
- 注释和说明文字
5. **操作动作**:
- 鼠标位置、点击目标
- 正在进行的操作
6. **关键信息**:
- 重要的配置项
- 关键步骤说明
- 错误信息或警告
【输出格式】
## frame_0001.jpg
- 场景: [场景类型]
- 内容: [详细描述]
- 文字/代码:[完整的文字或代码内容]
- 操作: [正在进行的操作]
- 要点: [关键信息]
## frame_0002.jpg
...
【注意】
- 不要省略任何图片
- 代码和文字必须完整转录
- 信息越详细越好分析要点: 1. 完整转录所有文字和代码内容 2. 详细描述界面元素和操作步骤 3. 记录每张图片的关键信息 4. 标注重要的截图帧号(如 frame_0042.jpg)
Step 3: 生成文档
根据视频类型,将分析结果重新组织整理成 视频分析.md:
判断视频类型:
- 实操类: 编程教程、软件操作、配置演示等
- 知识类: 概念讲解、原理分析、经验分享等
【关键】图片与内容必须严格对应:
错误示例 ❌:
### 安装 Node.js
首先下载 Node.js...
 ← 图片可能是其他内容
正确示例 ✅:
### 安装 Node.js
首先下载 Node.js...
 ← 图片确实是下载页面生成文档的正确流程:
1. 先整理所有 Task 返回的分析结果
- 汇总所有帧的分析内容
- 建立「帧号 → 内容」的对应关系
2. 按主题重新组织内容(不是按时间顺序)
- 将相关内容归类到同一章节
- 确定每个章节需要哪些帧的信息
3. 插入图片时必须核对
- 只插入与当前内容直接相关的图片
- 图片描述要准确反映图片实际内容
- 使用格式:

4. 代码必须来自图片中的实际代码
- 不要自己编造代码
- 代码块标注来源:
<!-- 来自 frame_0025 -->
重要原则: 1. 图文对应 - 每张图片必须与其上下文内容匹配 2. 不要时间线流水账 - 重新组织内容,像写文章一样 3. 结构清晰 - 有章节划分和逻辑顺序 4. 代码真实 - 只使用图片中出现的代码,不要编造 5. 独立可读 - 不看视频也能完全理解
Output Format
实操教程类
# {教程主题}
## 简介
{教程目标}
{前置条件和要求}
## 环境准备
{需要安装的软件}
{配置要求}
## 操作步骤
### 1. {步骤标题}
{详细说明,内容必须与下方图片对应}

<!-- 代码来自 frame_xxxx -->2. {步骤标题}
{详细说明}
!frame_xxxx: 准确描述
...
完整代码
<!-- 汇总自 frame_xxxx, frame_xxxx, frame_xxxx --> {汇总所有代码片段,标注来源帧号}
常见问题
{可能遇到的问题和解决方案}
总结
{核心要点回顾} {延伸学习建议}
### 知识文档类
{主题}
概述
{主题背景介绍} {为什么重要}
{章节1标题}
{内容,必须与配图对应}
!frame_xxxx: 图片实际内容描述
{章节2标题}
{内容}
!frame_xxxx: 图片实际内容描述
核心要点
- 要点1
- 要点2
- 要点3
延伸阅读
{相关资源和建议}
### 图片插入规范
| 规则 | 说明 |
|------|------|
| **帧号必须标注** | `` |
| **描述必须准确** | 描述图片的实际内容,不是期望内容 |
| **内容必须匹配** | 图片上方/下方的文字必须与图片内容相关 |
| **代码标注来源** | `<!-- 代码来自 frame_0025 -->` |
| **不要乱插图** | 没有合适的图就不插,不要强行配图 |
## API Reference
### Bilibili API
脚本使用 Bilibili 官方 API 下载视频:
获取视频信息
GET https://api.bilibili.com/x/web-interface/view?bvid=BV1xx411c7mD
获取播放地址
GET https://api.bilibili.com/x/player/playurl?bvid=BV1xx411c7mD&cid={cid}&qn=80&fnval=1
API 文档: https://github.com/SocialSisterYi/bilibili-API-collect
### FFmpeg 拆帧命令
每秒1帧
ffmpeg -i video.mp4 -vf "fps=1" -q:v 2 images/frame_%04d.jpg
每秒0.5帧(每2秒1帧)
ffmpeg -i video.mp4 -vf "fps=0.5" -q:v 2 images/frame_%04d.jpg
指定时间范围
ffmpeg -i video.mp4 -ss 00:01:00 -to 00:05:00 -vf "fps=1" -q:v 2 images/frame_%04d.jpg
提取关键帧(场景变化)
ffmpeg -i video.mp4 -vf "select='gt(scene,0.3)'" -vsync vfr -q:v 2 images/frame_%04d.jpg
更多选项: https://ffmpeg.org/ffmpeg.html
## Examples
### 示例1: 分析编程教程
1. 下载并拆帧
dotnet run scripts/prepare.cs "https://www.bilibili.com/video/BV1xx411c7mD" -o ./react-tutorial
2. 分析图片(使用 Task 工具)
3. 生成 react-tutorial/视频分析.md
### 示例2: 分析长视频
降低帧率,减少图片数量
dotnet run scripts/prepare.cs "https://www.bilibili.com/video/BV1xx411c7mD" -o ./long-video --fps 0.2
### 示例3: 只下载视频
dotnet run scripts/prepare.cs "https://www.bilibili.com/video/BV1xx411c7mD" -o ./output --video-only
## Quality Checklist
生成文档前,**逐项检查**以下要求:
### 内容质量
- [ ] 内容重新组织,不是时间线流水账
- [ ] 章节结构清晰,有逻辑顺序
- [ ] 不看视频也能理解全部内容
- [ ] 包含环境准备/前置条件说明
- [ ] 包含总结和核心要点
### 图文对应(重要!)
- [ ] 每张图片都标注了帧号: ``
- [ ] 图片描述准确反映图片**实际内容**
- [ ] 图片上下文的文字与图片内容**直接相关**
- [ ] 没有随意插入不相关的图片
- [ ] 代码块标注了来源帧号: `<!-- 来自 frame_xxxx -->`
### 代码质量
- [ ] 代码来自图片中的实际代码,不是编造的
- [ ] 代码片段完整,可直接复制使用
- [ ] 完整代码章节汇总了所有代码并标注来源
## Tags
`bilibili`, `video-analysis`, `ai`, `frame-extraction`, `markdown`, `tutorial`, `ffmpeg`, `dotnet`
## Compatibility
- Codex: Yes
- Claude Code: Yes
基本使用示例
快速开始
cd skills/tools/bilibili-analyzer/scripts
python main.py "https://www.bilibili.com/video/BV1xx411c7mD"常用场景
1. 分析教程视频(提取更多帧)
python main.py "https://www.bilibili.com/video/BV1xx411c7mD" \
-i 15 \
-m 100 \
-f text,objects2. 快速预览长视频
python main.py "https://www.bilibili.com/video/BV1xx411c7mD" \
-i 120 \
-m 20 \
--no-scene-detection3. 分析人物访谈
python main.py "https://www.bilibili.com/video/BV1xx411c7mD" \
-i 30 \
-f faces,text,scene4. 高并行分析(快速完成)
python main.py "https://www.bilibili.com/video/BV1xx411c7mD" \
-w 8 \
-m 305. 指定输出目录
python main.py "https://www.bilibili.com/video/BV1xx411c7mD" \
-o ./my-video-reports6. 使用短链接
python main.py "https://b23.tv/xxxxx"7. 详细日志模式(调试用)
python main.py "https://www.bilibili.com/video/BV1xx411c7mD" -v8. 静默模式(只显示错误)
python main.py "https://www.bilibili.com/video/BV1xx411c7mD" -q输出示例
分析完成后,输出目录结构:
./bilibili/视频标题/
├── report.md # Markdown分析报告
├── frames/ # 关键帧图片
│ ├── frame_001_00-00-00.jpg
│ ├── frame_002_00-00-30.jpg
│ └── ...
├── analysis.log # 执行日志
└── checkpoints/ # 检查点数据常见问题 (FAQ)
基础问题
Q: 为什么需要FFmpeg?
A: FFmpeg是一个强大的多媒体处理工具,本工具使用它从视频中提取关键帧。没有FFmpeg,帧提取功能将无法工作。
Q: 分析一个视频需要多长时间?
A: 取决于多个因素:
- 视频长度
- 帧提取间隔和最大帧数
- 网络速度(下载阶段)
- AI分析并行度
一般来说,10分钟视频(默认设置)约需5-10分钟完成分析。
Q: 支持哪些视频格式?
A: 支持B站所有可播放的视频格式。系统使用yt-dlp下载,会自动选择最佳质量。
错误处理
Q: 遇到"URL无效"错误怎么办?
A: 请确保URL格式正确:
- 标准格式:
https://www.bilibili.com/video/BV1xx411c7mD - 短链接:
https://b23.tv/xxxxx - 不支持番剧、直播等其他类型链接
Q: 下载失败怎么办?
A: 可能的原因和解决方案: 1. 网络问题: 检查网络连接,使用--retries增加重试次数 2. 视频不存在: 确认视频未被删除或设为私有 3. 地区限制: 某些视频可能有地区限制
Q: FFmpeg报错怎么办?
A: 1. 确认FFmpeg已正确安装: ffmpeg -version 2. 确认FFmpeg在系统PATH中 3. 尝试重新安装FFmpeg
Q: AI分析失败怎么办?
A: 系统会自动重试失败的分析任务。如果仍然失败: 1. 检查Claude Code配置 2. 查看日志文件了解详细错误 3. 部分结果会保存到partial_report.json
高级用法
Q: 如何只分析视频的特定部分?
A: 目前不支持指定时间范围,但可以通过调整参数间接实现:
- 增大
--interval跳过更多内容 - 减小
--max-frames限制分析帧数
Q: 如何提高分析速度?
A: 1. 增加并行worker: -w 8 2. 减少帧数: -m 20 3. 增大间隔: -i 60 4. 禁用场景检测: --no-scene-detection
Q: 如何获取更详细的分析?
A: 1. 减小帧间隔: -i 10 2. 增加最大帧数: -m 100 3. 启用所有分析焦点: -f text,objects,faces,actions,scene
Q: 分析中断后如何恢复?
A: 系统会自动保存检查点。目前需要重新运行命令,未来版本将支持从检查点恢复。
输出相关
Q: 报告中的图片路径是什么格式?
A: 使用相对路径,格式为./frames/frame_XXX_HH-MM-SS.jpg,确保报告和frames目录在同一位置时图片可正常显示。
Q: 日志文件在哪里?
A: 日志文件analysis.log位于输出目录中,与report.md同级。
错误代码
| 错误 | 原因 | 解决方案 |
|---|---|---|
URLValidationError | URL格式不正确 | 使用正确的B站视频URL |
MetadataFetchError | 无法获取视频信息 | 检查视频是否存在/私有 |
DownloadError | 下载失败 | 检查网络,增加重试次数 |
FFmpegError | FFmpeg问题 | 安装/重装FFmpeg |
AnalysisError | AI分析失败 | 检查Claude Code配置 |
ReportGenerationError | 报告生成失败 | 检查输出目录权限 |
安装指南
系统依赖
FFmpeg(必需)
FFmpeg用于从视频中提取关键帧,是必需依赖。
Windows
# 使用 Chocolatey
choco install ffmpeg
# 或使用 Scoop
scoop install ffmpeg
# 或手动下载
# 访问 https://ffmpeg.org/download.html 下载并添加到PATHmacOS
brew install ffmpegLinux (Ubuntu/Debian)
sudo apt update
sudo apt install ffmpegLinux (CentOS/RHEL)
sudo yum install epel-release
sudo yum install ffmpeg验证安装
ffmpeg -versionPython依赖
# 创建虚拟环境(推荐)
python -m venv venv
source venv/bin/activate # Linux/macOS
.\venv\Scripts\activate # Windows
# 安装依赖
pip install requests yt-dlp依赖说明
| 包 | 版本 | 用途 |
|---|---|---|
requests | >=2.28.0 | HTTP请求,获取视频元数据 |
yt-dlp | >=2023.0.0 | 视频下载 |
开发依赖(可选)
pip install hypothesis pytest| 包 | 版本 | 用途 |
|---|---|---|
hypothesis | >=6.0.0 | 属性测试 |
pytest | >=7.0.0 | 单元测试 |
#!/usr/bin/env dotnet run
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
// Parse arguments (args is already available in top-level statements)
if (args.Length == 0 || args.Contains("-h") || args.Contains("--help"))
{
PrintHelp();
return;
}
var url = args[0];
var outputDir = GetArgValue(args, "-o", "--output") ?? ".";
var fpsStr = GetArgValue(args, "--fps") ?? "1.0";
if (!double.TryParse(fpsStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var fps))
{
Console.WriteLine($"[ERROR] Invalid fps value: {fpsStr}");
Environment.Exit(1);
}
var similarityStr = GetArgValue(args, "--similarity") ?? "0.80";
if (!double.TryParse(similarityStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var similarityThreshold))
{
Console.WriteLine($"[ERROR] Invalid similarity value: {similarityStr}");
Environment.Exit(1);
}
var videoOnly = args.Contains("--video-only");
var framesOnly = args.Contains("--frames-only");
var noDedup = args.Contains("--no-dedup");
var videoPath = Path.Combine(outputDir, "video.mp4");
var imagesDir = Path.Combine(outputDir, "images");
// Create output directory
Directory.CreateDirectory(outputDir);
Console.WriteLine(new string('=', 50));
Console.WriteLine("Bilibili Video Analyzer - Prepare Script");
Console.WriteLine(new string('=', 50));
Console.WriteLine($"URL: {url}");
Console.WriteLine($"Output: {outputDir}");
Console.WriteLine($"FPS: {fps}");
Console.WriteLine($"Similarity Threshold: {similarityThreshold:P0}");
Console.WriteLine($"Deduplication: {(noDedup ? "Disabled" : "Enabled")}");
Console.WriteLine(new string('=', 50));
// Download video
if (!framesOnly)
{
if (!await DownloadBilibiliVideoAsync(url, videoPath))
{
Environment.Exit(1);
}
}
// Extract frames
if (!videoOnly)
{
if (!File.Exists(videoPath))
{
Console.WriteLine($"[ERROR] Video file not found: {videoPath}");
Environment.Exit(1);
}
if (!await ExtractFramesAsync(videoPath, imagesDir, fps))
{
Environment.Exit(1);
}
// Deduplicate similar frames
if (!noDedup)
{
await DeduplicateFramesAsync(imagesDir, similarityThreshold);
}
}
Console.WriteLine();
Console.WriteLine(new string('=', 50));
Console.WriteLine("[OK] Done!");
Console.WriteLine($" Video: {videoPath}");
Console.WriteLine($" Images: {imagesDir}/");
Console.WriteLine(new string('=', 50));
// === Functions ===
async Task<bool> DownloadBilibiliVideoAsync(string url, string outputPath)
{
Console.WriteLine($"[INFO] Downloading video: {url}");
try
{
// Extract BV ID from URL
var bvid = ExtractBvid(url);
if (string.IsNullOrEmpty(bvid))
{
Console.WriteLine("[ERROR] Invalid Bilibili URL, cannot extract BV ID");
return false;
}
Console.WriteLine($"[INFO] BV ID: {bvid}");
using var client = CreateHttpClient();
// Step 1: Get video info to obtain cid
Console.WriteLine("[INFO] Fetching video info...");
var infoUrl = $"https://api.bilibili.com/x/web-interface/view?bvid={bvid}";
var infoJson = await client.GetStringAsync(infoUrl);
using var infoDoc = JsonDocument.Parse(infoJson);
var code = infoDoc.RootElement.GetProperty("code").GetInt32();
if (code != 0)
{
var message = infoDoc.RootElement.GetProperty("message").GetString();
Console.WriteLine($"[ERROR] Failed to get video info: {message}");
return false;
}
var data = infoDoc.RootElement.GetProperty("data");
var title = data.GetProperty("title").GetString();
var cid = data.GetProperty("cid").GetInt64();
Console.WriteLine($"[INFO] Title: {title}");
Console.WriteLine($"[INFO] CID: {cid}");
// Step 2: Get playback URL
Console.WriteLine("[INFO] Fetching playback URL...");
var playUrl = $"https://api.bilibili.com/x/player/playurl?bvid={bvid}&cid={cid}&qn=80&fnval=1";
var playJson = await client.GetStringAsync(playUrl);
using var playDoc = JsonDocument.Parse(playJson);
var playCode = playDoc.RootElement.GetProperty("code").GetInt32();
if (playCode != 0)
{
var message = playDoc.RootElement.GetProperty("message").GetString();
Console.WriteLine($"[ERROR] Failed to get playback URL: {message}");
return false;
}
var playData = playDoc.RootElement.GetProperty("data");
var durl = playData.GetProperty("durl")[0];
var videoUrl = durl.GetProperty("url").GetString();
var size = durl.GetProperty("size").GetInt64();
Console.WriteLine($"[INFO] Video size: {size / 1024.0 / 1024.0:F1} MB");
// Step 3: Download video
Console.WriteLine("[INFO] Downloading video file...");
using var request = new HttpRequestMessage(HttpMethod.Get, videoUrl);
request.Headers.Add("Referer", $"https://www.bilibili.com/video/{bvid}");
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength ?? size;
if (totalBytes <= 0) totalBytes = size > 0 ? size : 1; // Prevent division by zero
await using var contentStream = await response.Content.ReadAsStreamAsync();
await using var fileStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.None, 81920, true);
var buffer = new byte[81920]; // Larger buffer for better performance
var totalRead = 0L;
var lastProgress = -1;
int bytesRead;
while ((bytesRead = await contentStream.ReadAsync(buffer)) > 0)
{
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead));
totalRead += bytesRead;
var progress = (int)(totalRead * 100 / totalBytes);
if (progress != lastProgress && (progress % 10 == 0 || progress == 100))
{
Console.WriteLine($"[INFO] Progress: {progress}%");
lastProgress = progress;
}
}
// Ensure 100% is shown
if (lastProgress != 100)
{
Console.WriteLine($"[INFO] Progress: 100%");
}
Console.WriteLine($"[OK] Video downloaded: {outputPath}");
return true;
}
catch (HttpRequestException ex)
{
Console.WriteLine($"[ERROR] Network error: {ex.Message}");
return false;
}
catch (JsonException ex)
{
Console.WriteLine($"[ERROR] Failed to parse API response: {ex.Message}");
return false;
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Download failed: {ex.Message}");
return false;
}
}
string? ExtractBvid(string url)
{
// Match BV ID from various URL formats
// https://www.bilibili.com/video/BV1xx411c7mD
// https://www.bilibili.com/video/BV1xx411c7mD?p=1
// https://b23.tv/BV1xx411c7mD
// BV1xx411c7mD
// BV ID is exactly 12 characters: BV + 10 alphanumeric chars
var match = Regex.Match(url, @"BV[a-zA-Z0-9]{10}");
return match.Success ? match.Value : null;
}
HttpClient CreateHttpClient()
{
var handler = new HttpClientHandler
{
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
};
var client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
client.DefaultRequestHeaders.Add("Referer", "https://www.bilibili.com");
client.DefaultRequestHeaders.Add("Accept", "application/json, text/plain, */*");
client.Timeout = TimeSpan.FromMinutes(30);
return client;
}
async Task<bool> ExtractFramesAsync(string videoPath, string outputDir, double fps)
{
Console.WriteLine($"[INFO] Extracting frames (fps={fps})");
var ffmpeg = FindExecutable("ffmpeg", "ffmpeg.exe");
if (ffmpeg == null)
{
Console.WriteLine("[ERROR] ffmpeg not found!");
Console.WriteLine(" Windows: choco install ffmpeg / scoop install ffmpeg");
Console.WriteLine(" macOS: brew install ffmpeg");
Console.WriteLine(" Linux: sudo apt install ffmpeg");
return false;
}
Directory.CreateDirectory(outputDir);
var outputPattern = Path.Combine(outputDir, "frame_%04d.jpg");
try
{
Console.WriteLine($"[INFO] Running ffmpeg: {ffmpeg}");
var psi = new ProcessStartInfo
{
FileName = ffmpeg,
Arguments = $"-i \"{videoPath}\" -vf \"fps={fps.ToString(CultureInfo.InvariantCulture)}\" -q:v 2 -y \"{outputPattern}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null)
{
Console.WriteLine("[ERROR] Failed to start ffmpeg");
return false;
}
// Read stderr asynchronously to prevent deadlock
var stderrTask = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
var stderr = await stderrTask;
// ffmpeg returns 0 on success, non-zero on failure
if (process.ExitCode != 0)
{
// Only show last few lines of error
var errorLines = stderr.Split('\n').TakeLast(5);
Console.WriteLine($"[ERROR] Frame extraction failed (exit code {process.ExitCode}):");
foreach (var line in errorLines)
{
if (!string.IsNullOrWhiteSpace(line))
Console.WriteLine($" {line.Trim()}");
}
return false;
}
var frameCount = Directory.GetFiles(outputDir, "frame_*.jpg").Length;
if (frameCount == 0)
{
Console.WriteLine("[ERROR] No frames extracted. Check if video file is valid.");
return false;
}
Console.WriteLine($"[OK] Frames extracted: {frameCount} images saved to {outputDir}/");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Frame extraction failed: {ex.Message}");
return false;
}
}
async Task DeduplicateFramesAsync(string imagesDir, double threshold)
{
Console.WriteLine($"[INFO] Deduplicating similar frames (threshold: {threshold:P0})...");
var files = Directory.GetFiles(imagesDir, "frame_*.jpg")
.OrderBy(f => f)
.ToList();
if (files.Count < 2)
{
Console.WriteLine("[INFO] Not enough frames to deduplicate");
return;
}
var toDelete = new List<string>();
var ffmpeg = FindExecutable("ffmpeg", "ffmpeg.exe");
if (ffmpeg == null)
{
Console.WriteLine("[WARN] ffmpeg not found, skipping deduplication");
return;
}
// Compare consecutive frames only (not cross-frame comparison)
for (int i = 0; i < files.Count - 1; i++)
{
var current = files[i];
var next = files[i + 1];
// Skip if current frame is already marked for deletion
if (toDelete.Contains(current))
continue;
var similarity = await CalculateFrameSimilarityAsync(ffmpeg, current, next);
if (similarity >= threshold)
{
// Keep the first frame, mark the next one for deletion
toDelete.Add(next);
}
}
// Delete similar frames
foreach (var file in toDelete)
{
try
{
File.Delete(file);
}
catch { }
}
// Renumber remaining frames
var remainingFiles = Directory.GetFiles(imagesDir, "frame_*.jpg")
.OrderBy(f => f)
.ToList();
for (int i = 0; i < remainingFiles.Count; i++)
{
var newName = Path.Combine(imagesDir, $"frame_{i + 1:D4}.jpg");
if (remainingFiles[i] != newName)
{
// Use temp name to avoid conflicts
var tempName = Path.Combine(imagesDir, $"temp_{i + 1:D4}.jpg");
File.Move(remainingFiles[i], tempName);
}
}
// Rename temp files to final names
var tempFiles = Directory.GetFiles(imagesDir, "temp_*.jpg").OrderBy(f => f).ToList();
for (int i = 0; i < tempFiles.Count; i++)
{
var finalName = Path.Combine(imagesDir, $"frame_{i + 1:D4}.jpg");
File.Move(tempFiles[i], finalName);
}
var finalCount = Directory.GetFiles(imagesDir, "frame_*.jpg").Length;
Console.WriteLine($"[OK] Deduplication complete: {files.Count} -> {finalCount} frames (removed {toDelete.Count} similar frames)");
}
async Task<double> CalculateFrameSimilarityAsync(string ffmpeg, string file1, string file2)
{
try
{
// Use ffmpeg to calculate PSNR (Peak Signal-to-Noise Ratio) between two images
// Higher PSNR = more similar images
var psi = new ProcessStartInfo
{
FileName = ffmpeg,
Arguments = $"-i \"{file1}\" -i \"{file2}\" -lavfi \"psnr\" -f null -",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null) return 0;
var stderrTask = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
var stderr = await stderrTask;
// Parse PSNR value from output
// Format: [Parsed_psnr_0 @ ...] PSNR y:XX.XX u:XX.XX v:XX.XX average:XX.XX min:XX.XX max:XX.XX
var match = Regex.Match(stderr, @"average:(\d+\.?\d*)", RegexOptions.IgnoreCase);
if (match.Success && double.TryParse(match.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var psnr))
{
// Convert PSNR to similarity percentage
// PSNR > 40 dB is considered very similar (>95%)
// PSNR > 30 dB is considered similar (>80%)
// PSNR = infinity means identical images
if (psnr > 100) return 1.0; // Identical or near-identical
if (psnr > 40) return 0.95 + (psnr - 40) * 0.001;
if (psnr > 30) return 0.80 + (psnr - 30) * 0.015;
if (psnr > 20) return 0.50 + (psnr - 20) * 0.03;
return psnr * 0.025;
}
// Fallback: use simpler SSIM if PSNR parsing fails
return await CalculateSSIMAsync(ffmpeg, file1, file2);
}
catch
{
return 0;
}
}
async Task<double> CalculateSSIMAsync(string ffmpeg, string file1, string file2)
{
try
{
var psi = new ProcessStartInfo
{
FileName = ffmpeg,
Arguments = $"-i \"{file1}\" -i \"{file2}\" -lavfi \"ssim\" -f null -",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null) return 0;
var stderrTask = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
var stderr = await stderrTask;
// Parse SSIM value: All:0.XXXXX
var match = Regex.Match(stderr, @"All:(\d+\.?\d*)", RegexOptions.IgnoreCase);
if (match.Success && double.TryParse(match.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var ssim))
{
return ssim; // SSIM is already 0-1 range
}
return 0;
}
catch
{
return 0;
}
}
string? FindExecutable(params string[] names)
{
var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? "";
var paths = pathEnv.Split(Path.PathSeparator);
foreach (var name in names)
{
foreach (var path in paths)
{
try
{
var fullPath = Path.Combine(path, name);
if (File.Exists(fullPath)) return fullPath;
}
catch { } // Ignore invalid paths
}
}
// Common Windows paths
if (OperatingSystem.IsWindows())
{
var commonPaths = new[]
{
@"C:\ffmpeg\bin",
@"C:\Program Files\ffmpeg\bin",
@"C:\tools\ffmpeg\bin",
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "scoop", "shims"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "WinGet", "Links"),
};
foreach (var basePath in commonPaths)
{
foreach (var name in names)
{
try
{
var fullPath = Path.Combine(basePath, name);
if (File.Exists(fullPath)) return fullPath;
}
catch { }
}
}
}
// Try where/which command
try
{
var cmd = OperatingSystem.IsWindows() ? "where" : "which";
var psi = new ProcessStartInfo
{
FileName = cmd,
Arguments = names[0],
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process != null)
{
process.WaitForExit(5000); // 5 second timeout
var output = process.StandardOutput.ReadToEnd()?.Trim();
if (!string.IsNullOrEmpty(output))
{
var firstLine = output.Split('\n')[0].Trim();
if (File.Exists(firstLine)) return firstLine;
}
}
}
catch { }
return null;
}
string? GetArgValue(string[] args, params string[] names)
{
for (int i = 0; i < args.Length - 1; i++)
{
if (names.Contains(args[i]))
{
return args[i + 1];
}
}
return null;
}
void PrintHelp()
{
Console.WriteLine(@"
Bilibili Video Analyzer - Prepare Script
Usage:
dotnet run prepare.cs <url> [options]
Arguments:
url Bilibili video URL (required)
Options:
-o, --output <dir> Output directory (default: current)
--fps <value> Frames per second (default: 1.0)
--similarity <value> Similarity threshold for deduplication (default: 0.80)
--no-dedup Disable frame deduplication
--video-only Only download video, skip frame extraction
--frames-only Only extract frames (requires existing video.mp4)
-h, --help Show this help
Examples:
dotnet run prepare.cs ""https://www.bilibili.com/video/BV1xx411c7mD""
dotnet run prepare.cs ""https://www.bilibili.com/video/BV1xx411c7mD"" --fps 0.5
dotnet run prepare.cs ""https://www.bilibili.com/video/BV1xx411c7mD"" -o ./output
dotnet run prepare.cs ""https://www.bilibili.com/video/BV1xx411c7mD"" --similarity 0.85
dotnet run prepare.cs ""https://www.bilibili.com/video/BV1xx411c7mD"" --no-dedup
Deduplication:
Consecutive frames with similarity >= threshold will be deduplicated.
Only compares adjacent frames (not cross-frame comparison).
Uses ffmpeg SSIM/PSNR for accurate similarity calculation.
Requirements:
- .NET 10 SDK
- ffmpeg: https://ffmpeg.org/download.html
");
}
Related skills
FAQ
How do you run bilibili-analyzer on a video?
bilibili-analyzer runs from skills/tools/bilibili-analyzer/scripts with python main.py plus a Bilibili URL, using flags like -i for frame interval, -m for max frames, and -f for text, objects, faces, or scene outputs.
Can bilibili-analyzer speed up long videos?
bilibili-analyzer supports quick previews with -i 120 -m 20 --no-scene-detection, or high parallelism with -w 8 to finish analysis faster on long Bilibili uploads.
Is Bilibili Analyzer safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.