
Video Copy Analyzer
- 140 installs
- 204 repo stars
- Updated February 21, 2026
- albedo-tabai/video-copy-analyzer
video-copy-analyzer is a skill that downloads online videos, transcribes Chinese speech with FunASR, and runs a three-dimension analysis of the video copy.
About
video-copy-analyzer is a Chinese-language tool that downloads an online video, transcribes its Chinese speech with FunASR, corrects the transcript, and runs a three-dimension analysis of the copy across TextContent, Viral, and Brainstorming lenses. It follows a strict 5-stage pipeline from download to a structured transcript. A developer or content creator uses it to extract and study short-video scripts and viral copywriting techniques. It supports Bilibili, YouTube, and Douyin.
- Downloads online videos (Bilibili, YouTube, Douyin), transcribes Chinese speech with FunASR, and corrects the transcript
- Runs a 5-stage pipeline ending in three-dimension copy analysis (TextContent, Viral, Brainstorming)
- Uses a three-tier subtitle strategy: embedded subtitles, RapidOCR burned-in subtitles, then FunASR transcription
Video Copy Analyzer by the numbers
- 140 all-time installs (skills.sh)
- Ranked #739 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
video-copy-analyzer capabilities & compatibility
Free; FunASR runs locally after a one-time 2-3GB model download
- Capabilities
- transcription · copywriting
- Use cases
- transcription · copywriting · marketing
- Platforms
- macOS
- Pricing
- Free
What video-copy-analyzer says it does
一站式视频内容提取与文案分析,支持 B站、YouTube、抖音 等平台。
转录速度极快:10 分钟音频约 22 秒完成
npx skills add https://github.com/albedo-tabai/video-copy-analyzer --skill video-copy-analyzerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 140 |
|---|---|
| repo stars | ★ 204 |
| Last updated | February 21, 2026 |
| Repository | albedo-tabai/video-copy-analyzer ↗ |
What it does
Download a video, transcribe its speech with FunASR, and analyze the copy across three dimensions.
Who is it for?
Extracting and analyzing short-video scripts and viral copywriting techniques
Skip if: Analyzing video visuals or scene quality (it focuses on the spoken/text copy)
When should I use this skill?
A user needs to analyze short-video copy, extract video content, or study viral copywriting
What you get
A corrected transcript and a three-dimension copy analysis report are produced from an online video.
- Corrected text transcript (.md)
- Three-dimension analysis report
- Structured transcript
By the numbers
- 5-stage pipeline
- 3 analysis frameworks (TextContent, Viral, Brainstorming)
- supports 3 platforms (Bilibili, YouTube, Douyin)
Files
视频文案分析工具
一站式视频内容提取与文案分析,支持 B站、YouTube、抖音 等平台。
安装部署
系统要求
- Python 3.9+
- FFmpeg(用于音视频处理)
- 约 3GB 磁盘空间(FunASR 模型缓存)
一键安装
# 1. 基础工具
brew install ffmpeg # macOS
pip install yt-dlp requests pysrt python-dotenv
# 2. FunASR(核心 ASR 引擎,中文语音转录)
pip install funasr modelscope torch torchaudio
# 3. RapidOCR(烧录字幕识别,可选)
pip install rapidocr-onnxruntime⚠️ FunASR 首次运行注意事项
FunASR 首次运行时会自动下载约 2-3GB 模型文件到 ~/.cache/modelscope/:
| 模型 | 大小 | 用途 |
|---|---|---|
| paraformer-zh | ~1.05GB | 中文语音识别(ASR) |
| fsmn-vad | ~20MB | 语音活动检测(长音频分段) |
| ct-punc | ~1GB | 标点恢复 |
- 首次下载可能需要 1-5 分钟(取决于网速),期间看起来像是卡住,请耐心等待
- 下载完成后会缓存到本地,后续运行秒级加载
- 如果下载失败,可手动从 ModelScope 下载模型放到
~/.cache/modelscope/hub/models/iic/目录
环境验证
# 验证所有依赖
python scripts/check_environment.py
# 或手动检查关键组件
yt-dlp --version
ffmpeg -version
python -c "from funasr import AutoModel; print('FunASR OK')"
python -c "from rapidocr_onnxruntime import RapidOCR; print('RapidOCR OK')"首次使用设置
首次使用时,询问用户:
"请设置默认工作目录(用于保存下载的视频和分析报告):
>
A. 使用默认目录:~/video-analysis/B. 每次手动指定目录
C. 指定一个固定目录:[请输入路径]"
保存用户选择供后续使用。
---
工作流程(5 阶段)
重要:你必须严格按照以下 5 个阶段顺序执行,每个阶段完成后再进入下一个阶段。不要跳过任何阶段。
阶段 1: 下载视频
目标:将用户提供的视频 URL 下载为本地 MP4 文件。
执行步骤: 1. 获取用户提供的视频 URL 和输出目录 2. 如果输出目录不存在,创建它:mkdir -p <输出目录> 3. 判断视频平台并选择下载方式:
抖音视频(URL 包含 douyin.com 或 v.douyin.com)
使用专用下载脚本:
python scripts/download_douyin.py "<抖音链接>" "<输出目录>/<文件名>.mp4"支持的链接格式:v.douyin.com/xxx、www.douyin.com/video/xxx、douyin.com/jingxuan?modal_id=xxx
其他平台(B站、YouTube 等)
使用 yt-dlp:
yt-dlp -f "bestvideo[height<=1080]+bestaudio/best[height<=1080]" \
--merge-output-format mp4 \
-o "<输出目录>/%(id)s.%(ext)s" \
"<视频URL>"4. 确认下载成功:检查 MP4 文件是否存在且大小 > 0
---
阶段 2: 字幕提取
目标:从视频中提取 SRT 格式字幕文件。
执行步骤:
⚠️ 重要:不要调用 `extract_subtitle_funasr.py` 的 `main()` 函数或直接运行整个脚本(它包含 B站 API 调用会因 cookies 问题卡住)。直接调用 `extract_with_funasr` 函数。
使用以下 Python 代码直接调用 FunASR 提取字幕:
import sys, os
sys.path.insert(0, '<skill_scripts_目录的绝对路径>')
from extract_subtitle_funasr import extract_with_funasr
success = extract_with_funasr('<视频文件绝对路径>', '<输出SRT文件绝对路径>')执行方式:将上述代码写入临时 Python 脚本文件(如 /tmp/run_funasr.py),然后运行:
python3 -u /tmp/run_funasr.py 2>&1 | tee /tmp/funasr_output.log注意事项:
- 必须使用绝对路径,不要使用相对路径
- 首次运行需下载 2-3GB 模型,耐心等待(后续秒级加载)
- 转录速度极快:10 分钟音频约 22 秒完成
- 命令运行后需要等待 30-120 秒(取决于视频长度)
确认成功:检查 SRT 文件是否存在且大小 > 0
字幕提取的内部逻辑(三层优先级,脚本自动处理):
| 优先级 | 方法 | 适用场景 | 准确度 | 速度 |
|---|---|---|---|---|
| L1 | 内嵌字幕提取 | 视频自带字幕流 | ⭐⭐⭐⭐⭐ | ⚡ 极快 |
| L2 | RapidOCR 烧录字幕识别 | 字幕烧录在画面中 | ⭐⭐⭐⭐ | 🚀 快 |
| L3 | FunASR 语音转录 | 无字幕,纯语音 | ⭐⭐⭐⭐ | ⚡ 极快 |
---
阶段 3: 文稿校正
目标:将 SRT 字幕合并为连续文本,基于语义进行校正,输出 <视频ID>_文字稿.md。
执行步骤: 1. 读取 SRT 字幕文件 2. 提取所有文本行(跳过序号和时间戳行) 3. 合并为连续文本 4. 基于上下文语义进行智能校正:
- 修正 ASR 产生的同音字错误(如"旗下"→"棋下")
- 修正专业术语和人名
- 确保标点符号正确
5. 保存为 Markdown 文件
输出文件:<输出目录>/<视频ID>_文字稿.md
输出格式:
# <视频ID> 原始文字稿
<校正后的完整文本>---
阶段 4: 三维度综合分析
目标:对文字稿内容进行深度分析,应用三个分析框架,输出 <视频ID>_分析报告.md。
执行步骤: 1. 读取阶段 3 生成的文字稿内容 2. 依次应用以下三个分析框架 3. 将分析结果保存为 Markdown 文件
三个分析框架:
4.1 TextContent Analysis 视角
你必须分析以下维度:
- 叙事结构分析:开场、发展、高潮、转折、结尾的结构
- 叙事声音分析:基调、节奏、独特金句
- 修辞手法识别:比喻、反转、呼应、隐喻等
- 词库提取:关键词列表
4.2 Viral-Abstract-Script 视角
你必须分析以下维度:
- Viral-5D 框架诊断:对 Hook、Emotion、爆点、CTA、社交货币 分别给出⭐评分和分析
- 风格定位:该视频的内容风格标签
- 爆款潜力评估:完播率、互动率、转发率预期
- 优化建议:1-3 条具体可执行的改进建议
4.3 Brainstorming 视角
你必须分析以下维度:
- 核心价值拆解:该文案的核心传播价值是什么
- 创意方向探索:2-3 种衍生创意方向
- 增量验证点:可测试的优化实验
输出文件:<输出目录>/<视频ID>_分析报告.md
输出格式:
# 视频文案综合分析报告(三维度)
## 一、TextContent Analysis 视角
[叙事结构、修辞手法、词库]
## 二、Viral-Abstract-Script 视角
[Viral-5D诊断、风格定位、优化建议]
## 三、Brainstorming 视角
[价值拆解、创意方向、验证点]---
阶段 5: 结构化文字稿
目标:基于阶段 3 的文字稿,按照视频内容的叙事逻辑重新分段整理,输出格式清晰、层次分明的 <视频ID>_结构化文字稿.md。
执行步骤: 1. 读取阶段 3 生成的文字稿,以及阶段 4 分析报告中的叙事结构分析 2. 按照视频内容的自然叙事段落进行切分 3. 为每个段落添加简洁的小标题(## 一、xxx、## 二、xxx 格式) 4. 在段落内部按语义进行合理分行(每段不宜过长,3-5 句为宜) 5. 修正 ASR 错误的同时保留口语化风格 6. 对关键金句或重点内容使用加粗标注 7. 保存为 Markdown 文件
输出文件:<输出目录>/<视频ID>_结构化文字稿.md
输出格式:
# <视频标题或ID> 结构化文字稿
## 一、<第一段小标题>
<分行后的段落文本,3-5 句一段>
<继续...>
## 二、<第二段小标题>
<分行后的段落文本>
...注意事项:
- 小标题应简洁有力,概括该段核心内容
- 保留视频原始的口语表达风格,不要过度书面化
- 关键金句或亮点用加粗突出
- 每个段落之间用空行分隔,提高可读性
---
完成后输出
完成所有 5 个阶段后,向用户播报:
✅ 视频文案分析完成!
📁 输出目录: <用户指定的目录>
📄 生成文件:
- <视频ID>.mp4 (原始视频)
- <视频ID>.srt (原始字幕)
- <视频ID>_文字稿.md (校正后纯文本文字稿)
- <视频ID>_分析报告.md (三维度分析报告)
- <视频ID>_结构化文字稿.md (按叙事逻辑分段的结构化文字稿)
🔗 快速打开:
[文字稿](<文字稿路径>)
[分析报告](<分析报告路径>)
[结构化文字稿](<结构化文字稿路径>)---
参考文件
| 文件 | 说明 | 状态 |
|---|---|---|
| download_douyin.py | 抖音视频下载脚本 | ✅ 可用 |
| extract_subtitle_funasr.py | 智能字幕提取(FunASR + RapidOCR) | ✅ 可用 |
| check_environment.py | 环境依赖检测 | ✅ 可用 |
| analysis-frameworks.md | 三个分析框架详解 | ✅ 参考 |
<!-- 以下脚本暂未启用,需要浏览器 cookies 支持,目前因 macOS 钥匙串加密问题暂不可用 --> <!-- | fetch_bilibili_subtitle.py | B站API字幕获取(需cookies) | ⏸️ 暂停 | --> <!-- | extract_subtitle.py | Whisper 字幕提取(已被 FunASR 替代) | ⏸️ 暂停 | --> <!-- | transcribe_audio.py | Whisper 音频转录(已被 FunASR 替代) | ⏸️ 暂停 | -->
---
FunASR 技术细节
本 skill 使用 FunASR 的 Paraformer 系列模型组合:
from funasr import AutoModel
model = AutoModel(
model="paraformer-zh", # 中文 ASR(含 SeACo 增强)
vad_model="fsmn-vad", # 语音活动检测(自动分段长音频)
vad_kwargs={"max_single_segment_time": 60000}, # 每段最长 60 秒
punc_model="ct-punc", # 标点恢复
disable_update=True, # 禁用版本检查
)
result = model.generate(
input="audio.wav",
batch_size_s=300, # 动态 batch
cache={}, # 官方推荐参数
)性能参考(MacBook Pro M 系列 CPU):
| 音频时长 | 转录耗时 | RTF | 字幕条数 |
|---|---|---|---|
| 42 秒 | 2 秒 | 0.049 | ~11 条 |
| 9 分钟 | 22 秒 | 0.036 | ~137 条 |
| 10 分钟 | 22 秒 | 0.035 | ~142 条 |
---
故障排除
FunASR 首次运行很慢 / 看起来卡住
首次运行需下载约 2-3GB 模型文件,这是正常现象。在网速 30MB/s 的环境下约需 1-2 分钟。下载完成后后续运行秒级加载。
FunASR 模型下载失败
如果 ModelScope 下载速度慢或中断,可手动从 ModelScope 下载以下模型到 ~/.cache/modelscope/hub/models/iic/ 目录:
speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorchspeech_fsmn_vad_zh-cn-16k-common-pytorchpunc_ct-transformer_cn-en-common-vocab471067-large
torch 版本不兼容
FunASR 需要 PyTorch 2.0+,建议:
pip install torch torchaudio --upgrade字幕提取脚本直接运行卡住
不要直接运行 python extract_subtitle_funasr.py video.mp4 output.srt(它会调用 B站 API 获取字幕并因 cookies 问题卡住)。请按照阶段 2的说明,直接调用 extract_with_funasr() 函数。
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
.eggs/
# Environment
.env
.venv
env/
venv/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Video files (don't commit large media)
*.mp4
*.mkv
*.avi
*.mov
*.srt
# Temporary
*.tmp
*.temp
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
#!/usr/bin/env python3
"""
视频文案分析工具 - 主入口脚本
用法: python main.py <视频URL或文件路径> [输出目录]
"""
import os
import sys
import subprocess
import re
from pathlib import Path
def get_script_dir():
"""获取脚本所在目录"""
return Path(__file__).parent.resolve()
def get_venv_python():
"""获取虚拟环境中的 Python 路径"""
venv_python = get_script_dir() / "venv" / "bin" / "python"
if venv_python.exists():
return str(venv_python)
return sys.executable
def get_venv_ytdlp():
"""获取虚拟环境中的 yt-dlp 路径"""
venv_ytdlp = get_script_dir() / "venv" / "bin" / "yt-dlp"
if venv_ytdlp.exists():
return str(venv_ytdlp)
return "yt-dlp"
def is_url(path):
"""判断是否为 URL"""
return path.startswith("http://") or path.startswith("https://")
def extract_video_id(url):
"""从 URL 提取视频 ID"""
# B站
bilibili_match = re.search(r'(BV[\w]+|av\d+)', url)
if bilibili_match:
return bilibili_match.group(1)
# YouTube
youtube_match = re.search(r'(?:v=|youtu\.be/)([a-zA-Z0-9_-]{11})', url)
if youtube_match:
return youtube_match.group(1)
# 默认使用 URL 的哈希
return hex(hash(url) & 0xFFFFFFFF)[2:]
def download_video(url, output_dir):
"""下载视频"""
video_id = extract_video_id(url)
output_template = str(output_dir / f"{video_id}.%(ext)s")
cmd = [
get_venv_ytdlp(),
"-f", "bestvideo[height<=1080]+bestaudio/best[height<=1080]",
"--merge-output-format", "mp4",
"-o", output_template,
url
]
print(f"📥 正在下载视频: {url}")
print(f" 输出路径: {output_dir}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"❌ 下载失败: {result.stderr}")
return None
# 查找下载的文件
for ext in ["mp4", "mkv", "webm"]:
video_file = output_dir / f"{video_id}.{ext}"
if video_file.exists():
print(f"✅ 视频下载成功: {video_file}")
return video_file
print("❌ 未找到下载的视频文件")
return None
def transcribe_video(video_path, output_dir, model="medium", language="auto"):
"""使用 Whisper 转录视频"""
video_path = Path(video_path)
video_id = video_path.stem
srt_path = output_dir / f"{video_id}.srt"
script_path = get_script_dir() / "scripts" / "transcribe_audio.py"
cmd = [
get_venv_python(),
str(script_path),
str(video_path),
str(srt_path),
model,
language,
"cpu" # 默认使用 CPU,MacOS 上更稳定
]
print(f"🎤 正在转录视频: {video_path}")
print(f" 使用模型: {model}, 语言: {language}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"❌ 转录失败: {result.stderr}")
if result.stdout:
print(f" 输出: {result.stdout}")
return None
if srt_path.exists():
print(f"✅ 转录成功: {srt_path}")
return srt_path
print("❌ 未找到生成的字幕文件")
return None
def read_srt_as_text(srt_path):
"""读取 SRT 字幕并提取纯文本"""
with open(srt_path, "r", encoding="utf-8") as f:
content = f.read()
# 移除时间戳和序号,只保留文本
lines = []
for line in content.split("\n"):
line = line.strip()
# 跳过空行、序号和时间戳
if not line:
continue
if line.isdigit():
continue
if "-->" in line:
continue
lines.append(line)
return " ".join(lines)
def main():
if len(sys.argv) < 2 or sys.argv[1] in ["-h", "--help"]:
print("用法: python main.py <视频URL或文件路径> [输出目录] [whisper模型] [语言]")
print()
print("参数:")
print(" 视频URL或文件路径 - B站/YouTube URL 或本地视频文件路径")
print(" 输出目录 - 可选,默认为 ~/video-analysis/")
print(" whisper模型 - 可选,tiny/base/small/medium/large,默认 medium")
print(" 语言 - 可选,zh/en/auto,默认 auto")
print()
print("示例:")
print(" python main.py https://www.bilibili.com/video/BVxxxx")
print(" python main.py ./my_video.mp4 ./output medium zh")
sys.exit(0 if len(sys.argv) > 1 else 1)
input_path = sys.argv[1]
output_dir = Path(sys.argv[2] if len(sys.argv) > 2 else os.path.expanduser("~/video-analysis"))
model = sys.argv[3] if len(sys.argv) > 3 else "medium"
language = sys.argv[4] if len(sys.argv) > 4 else "auto"
# 创建输出目录
output_dir.mkdir(parents=True, exist_ok=True)
print("=" * 60)
print("🎬 视频文案分析工具")
print("=" * 60)
# 阶段 1: 获取视频
if is_url(input_path):
video_path = download_video(input_path, output_dir)
if not video_path:
sys.exit(1)
else:
video_path = Path(input_path)
if not video_path.exists():
print(f"❌ 视频文件不存在: {video_path}")
sys.exit(1)
print(f"📁 使用本地视频: {video_path}")
# 阶段 2: Whisper 转录
srt_path = transcribe_video(video_path, output_dir, model, language)
if not srt_path:
sys.exit(1)
# 阶段 3: 提取文本
text_content = read_srt_as_text(srt_path)
transcript_path = output_dir / f"{video_path.stem}_文字稿.md"
with open(transcript_path, "w", encoding="utf-8") as f:
f.write(f"# 视频语音转录文字稿\n\n")
f.write(f"**视频来源**: {input_path}\n")
f.write(f"**视频文件**: {video_path}\n")
f.write(f"**转录模型**: Whisper {model}\n\n")
f.write("---\n\n")
f.write("## 完整文字稿\n\n")
f.write(text_content)
f.write("\n")
print(f"✅ 文字稿已保存: {transcript_path}")
# 完成报告
print()
print("=" * 60)
print("✅ 视频文案分析完成!")
print("=" * 60)
print()
print(f"📁 输出目录: {output_dir}")
print()
print("📄 生成文件:")
print(f" - {video_path.name} (视频)")
print(f" - {srt_path.name} (字幕)")
print(f" - {transcript_path.name} (文字稿)")
print()
print("💡 提示: 文字稿已准备好,可以进行三维度分析了!")
if __name__ == "__main__":
main()
Video Copy Analyzer
中文文档 | English
<div align="center">
🤖 Claude Skill | AI-Powered Video Analysis
 
</div>
🎬 One-stop video content extraction and copywriting analysis tool. Download videos, smart subtitle extraction (embedded/burned/audio), and analyze scripts using three AI frameworks.
✨ Features
| Stage | Function | Description |
|---|---|---|
| 1️⃣ | Video Download | Download from Bilibili/YouTube/Douyin (yt-dlp + custom downloader) |
| 2️⃣ | Smart Subtitle Extraction | Three-tier priority: Embedded → OCR (RapidOCR) → ASR (FunASR/Whisper) |
| 3️⃣ | Smart Correction | Context-based auto-correction of transcription errors |
| 4️⃣ | Three-Dimensional Analysis | TextContent + Viral + Brainstorming |
🚀 Quick Start
Prerequisites
# 1. yt-dlp (video downloader)
pip install yt-dlp
# 2. FFmpeg (must be installed and in PATH)
ffmpeg -version
# 3. Python dependencies
pip install pysrt python-dotenv
# 4. FunASR (Recommended for Chinese, lightweight & accurate)
pip install funasr modelscope
# 5. RapidOCR (ONNX lightweight, for burned subtitle detection)
pip install rapidocr-onnxruntime
# 6. Whisper (Alternative for English/multilingual)
pip install openai-whisper
# 7. requests (for Douyin download)
pip install requestsUsage
This is a Claude Skill designed for AI agents. Install it in your .agent/skills/ directory:
git clone https://github.com/ALBEDO-TABAI/video-copy-analyzer.git .agent/skills/video-copy-analyzerThen use it with Claude:
"Analyze this video: https://www.bilibili.com/video/BV1xxxxx"
🎯 Smart Subtitle Extraction (3-Tier Priority)
The skill automatically selects the best extraction method:
Video Input
↓
[1️⃣ Embedded Subtitle] ──→ Detected ──→ Direct Extract (Highest Accuracy)
↓ Not detected
[2️⃣ Burned Subtitle OCR] ──→ RapidOCR Frame Sampling ──→ Detected ──→ Full Video OCR
↓ Not detected
[3️⃣ Audio Transcription] ──→ FunASR (Chinese optimized) / Whisper (Multilingual)
↓
Output SRT SubtitlesExtraction Methods Comparison
| Tier | Method | Use Case | Accuracy | Speed |
|---|---|---|---|---|
| L1 | Embedded Extract | Video has subtitle stream | ⭐⭐⭐⭐⭐ | ⚡ Fastest |
| L2 | RapidOCR | Subtitles burned into video | ⭐⭐⭐⭐ | 🚀 Fast |
| L3 | FunASR Nano | Chinese audio transcription | ⭐⭐⭐⭐ | � Medium |
| L3 | Whisper | English/multilingual audio | ⭐⭐⭐ | 🐢 Medium |
Tech Stack
- RapidOCR (ONNX): Lightweight OCR for burned subtitle detection
- 🚀 Lightweight: ONNX Runtime, no GPU required
- 🎯 Cross-platform: Windows/Linux/Mac
- 📦 Easy deploy: Single pip install
- ✨ High accuracy: Based on PaddleOCR
- FunASR Nano: Alibaba open-source Chinese ASR model
- 🚀 Lightweight: ~100MB vs Whisper Large ~1.5GB
- 🎯 Chinese optimized: Better than Whisper for Chinese
- ⏱️ Timestamp: Word-level timestamps
- 💨 Fast: Runs well on CPU
�📊 Three-Dimensional Analysis Framework
1. TextContent Analysis
- Narrative structure breakdown
- Rhetorical device identification
- Keyword extraction
2. Viral-Abstract-Script Framework
- Viral-5D Diagnosis: Hook / Emotion / Peaks / CTA / Social Currency
- Style positioning
- Optimization suggestions
3. Brainstorming Framework
- Core value decomposition
- 2-3 creative direction exploration
- Incremental verification points
📁 Project Structure
video-copy-analyzer/
├── SKILL.md # Core skill instructions
├── scripts/
│ ├── download_douyin.py # Douyin video downloader (watermark-free)
│ ├── extract_subtitle_funasr.py # Smart subtitle extraction (FunASR + RapidOCR)
│ ├── extract_subtitle.py # Whisper-based extraction
│ ├── transcribe_audio.py # Audio transcription script
│ └── check_environment.py # Environment verification
└── references/
└── analysis-frameworks.md # Analysis framework details🔧 Configuration
On first use, the skill will prompt you to set a default output directory:
- Option A: Use default
~/video-analysis/ - Option B: Specify each time
- Option C: Set a fixed custom directory
📄 Output Files
After analysis, you'll receive:
| File | Content |
|---|---|
{video_id}.mp4 | Original video |
{video_id}.srt | Raw subtitles |
{video_id}_transcript.md / {video_id}_文字稿.md | Corrected transcript |
{video_id}_analysis.md / {video_id}_分析报告.md | Three-dimensional analysis report |
🎯 Supported Environments
This is a Claude Skill that works with AI coding assistants:
| Environment | Model | Status |
|---|---|---|
| Antigravity | Gemini 3 Pro | ✅ Supported |
| Cursor | Claude 4.5 Opus | ✅ Tested & Recommended |
| Claude Code | Claude 4.5 Opus | ✅ Supported |
| Windsurf | Any Claude model | ✅ Supported |
| Trae | Claude 3.5/4 | ✅ Supported |
💡 Best Performance: Tested with Claude 4.5 Opus, achieving optimal results in transcription correction and three-dimensional analysis.
📝 License
MIT License
视频文案分析工具
English | 中文文档
<div align="center">
🤖 Claude Skill | AI 驱动的视频分析工具
 
</div>
🎬 一站式视频内容提取与文案分析工具。下载视频、智能字幕提取(内嵌/烧录/语音)、三维度 AI 框架分析文案。
✨ 功能特性
| 阶段 | 功能 | 说明 |
|---|---|---|
| 1️⃣ | 视频下载 | 支持 B站/YouTube/抖音(yt-dlp + 专用下载器) |
| 2️⃣ | 智能字幕提取 | 三层优先级:内嵌字幕 → OCR识别 → 语音转录 |
| 3️⃣ | 智能校正 | 基于上下文自动校正转录错误 |
| 4️⃣ | 三维度分析 | TextContent + Viral + Brainstorming |
🚀 快速开始
环境要求
# 1. yt-dlp(视频下载器)
pip install yt-dlp
# 2. FFmpeg(必须安装并添加到 PATH)
ffmpeg -version
# 3. Python 基础依赖
pip install pysrt python-dotenv
# 4. FunASR(中文语音转录,推荐,轻量且效果好)
pip install funasr modelscope
# 5. RapidOCR(ONNX轻量版,用于烧录字幕识别)
pip install rapidocr-onnxruntime
# 6. Whisper(英文/多语言备选方案)
pip install openai-whisper
# 7. requests(抖音下载需要)
pip install requests使用方法
这是一个 Claude Skill,专为 AI 代理设计。将其安装到 .agent/skills/ 目录:
git clone https://github.com/ALBEDO-TABAI/video-copy-analyzer.git .agent/skills/video-copy-analyzer然后与 Claude 对话使用:
"分析这个视频:https://www.bilibili.com/video/BV1xxxxx"
🎯 智能字幕提取(三层优先级)
Skill 会自动选择最佳提取方案:
视频输入
↓
[1️⃣ 内嵌字幕检测] ──→ 检测到字幕流 ──→ 直接提取(准确度最高)
↓ 未检测到
[2️⃣ 烧录字幕检测] ──→ RapidOCR 采样帧识别 ──→ 检测到文字 ──→ 全视频 OCR 提取
↓ 未检测到
[3️⃣ 语音转录] ──→ FunASR(中文优化)/ Whisper(多语言)
↓
输出 SRT 字幕提取方式对比
| 层级 | 方法 | 适用场景 | 准确度 | 速度 |
|---|---|---|---|---|
| L1 | 内嵌字幕提取 | 视频自带字幕流 | ⭐⭐⭐⭐⭐ | ⚡ 极快 |
| L2 | RapidOCR 烧录识别 | 字幕烧录在画面中 | ⭐⭐⭐⭐ | 🚀 快 |
| L3 | FunASR Nano | 中文语音转录 | ⭐⭐⭐⭐ | 🐢 中等 |
| L3 | Whisper | 英文/多语言语音 | ⭐⭐⭐ | 🐢 中等 |
技术栈说明
- RapidOCR (ONNX): 用于检测和提取烧录在视频画面中的字幕
- 🚀 轻量级:ONNX Runtime 推理,无需 GPU
- 🎯 跨平台:Windows/Linux/Mac 均支持
- 📦 易部署:单 pip 安装,无复杂依赖
- ✨ 高精度:基于 PaddleOCR 模型优化
- FunASR Nano: 阿里开源中文语音识别模型
- 🚀 轻量级:~100MB vs Whisper Large ~1.5GB
- 🎯 中文优化:针对中文语音专门训练,效果优于 Whisper
- ⏱️ 时间戳:支持字级别时间戳
- 💨 速度快:CPU 上也能快速运行
📊 三维度分析框架
1. TextContent Analysis(文本内容分析)
- 叙事结构拆解
- 修辞手法识别
- 关键词提取
2. Viral-Abstract-Script(病毒传播框架)
- Viral-5D 诊断:Hook / Emotion / 爆点 / CTA / 社交货币
- 风格定位
- 优化建议
3. Brainstorming(头脑风暴框架)
- 核心价值拆解
- 2-3 种创意方向探索
- 增量验证点
📁 项目结构
video-copy-analyzer/
├── SKILL.md # 核心技能说明
├── scripts/
│ ├── download_douyin.py # 抖音视频下载(无水印)
│ ├── extract_subtitle_funasr.py # 智能字幕提取(FunASR + RapidOCR)
│ ├── extract_subtitle.py # 基于 Whisper 的提取
│ ├── transcribe_audio.py # 音频转录脚本
│ └── check_environment.py # 环境检测脚本
└── references/
└── analysis-frameworks.md # 分析框架详解🔧 配置说明
首次使用时,skill 会引导你设置默认输出目录:
- 选项 A:使用默认目录
~/video-analysis/ - 选项 B:每次手动指定
- 选项 C:设置一个固定的自定义目录
📄 输出文件
分析完成后,你将获得:
| 文件 | 内容 |
|---|---|
{视频ID}.mp4 | 原始视频 |
{视频ID}.srt | 原始字幕 |
{视频ID}_文字稿.md | 校正后文字稿 |
{视频ID}_分析报告.md | 三维度分析报告 |
🎯 支持环境
这是一个 Claude Skill,可在以下 AI 编程助手中使用:
| 环境 | 模型 | 状态 |
|---|---|---|
| Antigravity | Gemini 3 Pro | ✅ 支持 |
| Cursor | Claude 4.5 Opus | ✅ 已测试,推荐 |
| Claude Code | Claude 4.5 Opus | ✅ 支持 |
| Windsurf | 任意 Claude 模型 | ✅ 支持 |
| Trae | Claude 3.5/4 | ✅ 支持 |
💡 最佳效果:在 Claude 4.5 Opus 下测试,转录校正和三维度分析效果理想。
📝 许可证
MIT License
三维度分析框架详解
本文件详细说明 video-copy-analyzer 使用的三个分析框架。
一、TextContent Analysis 框架
1.1 叙事结构分析(Stage 7)
按时间线拆解视频内容:
| 结构层 | 说明 | 典型时长 |
|---|---|---|
| 开场钩子 | 吸引注意力的开头 | 0-5s |
| 核心卖点 | 主要内容展开 | 5-30s |
| 场景延伸 | 应用场景描述 | 30-45s |
| 行动号召 | CTA引导 | 最后5s |
1.2 叙事声音分析
- 人称视角:第一/第二/第三人称
- 语气特征:正式/口语/专业/亲切
- 节奏特点:句子平均时长、语速变化
1.3 修辞手法识别
| 手法 | 说明 | 效果 |
|---|---|---|
| 类比 | 将抽象概念具象化 | 降低理解门槛 |
| 数据论证 | 使用具体数字 | 增强可信度 |
| 对偶 | 对仗工整的句式 | 朗朗上口 |
| 呼应 | 首尾呼应 | 结构闭环 |
| 排比 | 并列结构 | 增强气势 |
1.4 词库提取
分类提取关键词:
- 核心词:产品/服务核心特征
- 场景词:使用场景描述
- 卖点词:差异化卖点
- 行动词:引导用户行动
---
二、Viral-Abstract-Script 框架
2.1 Viral-5D 诊断
| 维度 | 说明 | 评分标准 |
|---|---|---|
| Hook | 黄金3秒开头 | 是否立即抓住注意力 |
| Emotion | 情感触发 | 是否引发共鸣/笑点/惊讶 |
| 爆点结构 | 节奏设计 | 是否有高潮点 |
| CTA | 行动引导 | 是否有明确下一步 |
| 社交货币 | 传播价值 | 是否值得分享/讨论 |
2.2 风格定位
| 风格 | 特点 | 适合场景 |
|---|---|---|
| 抽象鬼畜 | 意义消解、荒诞 | 谐音梗、整活 |
| 反转剧情 | 悬念→意外结局 | 故事类 |
| 共鸣治愈 | 真实感、普通人视角 | 生活记录 |
| 科技种草 | 理性、信息密度高 | 产品测评 |
2.3 Hook 类型参考
1. 痛点切入:"你是不是也遇到过..." 2. 反常识:"90%的人都不知道..." 3. 数字开头:"3个方法让你..." 4. 悬念设置:"结局没想到..." 5. 情绪共鸣:"太真实了..."
---
三、Brainstorming 框架
3.1 核心价值拆解
根本需求 → 表层痛点 → 深层需求
↓ ↓ ↓
为什么 遇到什么 真正想要3.2 多方向探索
每次提出 2-3 种不同方向:
| 方向 | 核心概念 | 差异化 |
|---|---|---|
| A | 技术极客向 | 强调技术/材料 |
| B | 生活方式向 | 场景化叙事 |
| C | 情感共鸣向 | 亲情/友情牌 |
3.3 增量验证点
每个关键决策点都需验证:
- [ ] 开场钩子有效?
- [ ] 核心卖点差异化?
- [ ] 场景覆盖目标用户?
- [ ] 行动引导明确?
---
四、综合评分体系
| 维度 | 权重 | 评分标准 |
|---|---|---|
| 结构完整性 | 20% | 是否有完整叙事结构 |
| 开场吸引力 | 20% | 3秒是否抓住注意力 |
| 信息密度 | 15% | 信息量与时长比 |
| 情感共鸣 | 15% | 是否引发情感反应 |
| 传播潜力 | 15% | 是否值得分享 |
| CTA有效性 | 15% | 是否有明确行动引导 |
#!/usr/bin/env python3
"""
环境检测和依赖安装脚本
检测 video-copy-analyzer 所需的所有依赖
"""
import subprocess
import sys
import shutil
def check_command(cmd: str, version_arg: str = "--version") -> tuple[bool, str]:
"""检查命令行工具是否可用"""
try:
result = subprocess.run(
[cmd, version_arg],
capture_output=True,
text=True,
timeout=10
)
version = result.stdout.strip() or result.stderr.strip()
return True, version.split('\n')[0]
except (FileNotFoundError, subprocess.TimeoutExpired):
return False, ""
def check_python_package(package: str) -> bool:
"""检查 Python 包是否已安装"""
try:
__import__(package)
return True
except ImportError:
return False
def main():
print("=" * 50)
print("🔍 Video Copy Analyzer 环境检测")
print("=" * 50)
print()
all_ok = True
missing = []
# 1. 检查 yt-dlp
print("1️⃣ 检查 yt-dlp...")
ok, version = check_command("yt-dlp")
if ok:
print(f" ✅ yt-dlp: {version}")
else:
print(" ❌ yt-dlp 未安装")
missing.append("pip install yt-dlp")
all_ok = False
# 2. 检查 FFmpeg
print("2️⃣ 检查 FFmpeg...")
ok, version = check_command("ffmpeg", "-version")
if ok:
print(f" ✅ FFmpeg: {version[:50]}...")
else:
print(" ❌ FFmpeg 未安装")
missing.append("下载 FFmpeg: https://ffmpeg.org/download.html")
all_ok = False
# 3. 检查 Python 依赖
print("3️⃣ 检查 Python 依赖...")
packages = {
"whisper": "openai-whisper",
"pysrt": "pysrt",
"dotenv": "python-dotenv",
"torch": "torch"
}
pip_install = []
for import_name, pip_name in packages.items():
if check_python_package(import_name):
print(f" ✅ {pip_name}")
else:
print(f" ❌ {pip_name} 未安装")
pip_install.append(pip_name)
all_ok = False
# 4. 检查 CUDA (可选)
print("4️⃣ 检查 CUDA (可选)...")
try:
import torch
if torch.cuda.is_available():
print(f" ✅ CUDA 可用: {torch.cuda.get_device_name(0)}")
else:
print(" ⚠️ CUDA 不可用,将使用 CPU(转录速度较慢)")
except ImportError:
print(" ⚠️ PyTorch 未安装,无法检测 CUDA")
print()
print("=" * 50)
if all_ok:
print("✅ 所有依赖已满足,可以使用 video-copy-analyzer!")
else:
print("❌ 存在缺失依赖,请执行以下命令安装:")
print()
if pip_install:
print(f" pip install {' '.join(pip_install)}")
for cmd in missing:
if not cmd.startswith("pip"):
print(f" {cmd}")
print("=" * 50)
return 0 if all_ok else 1
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
抖音视频下载脚本
支持从抖音分享链接提取并下载视频(无水印版本)
使用方法:
python download_douyin.py <抖音链接> <输出路径>
示例:
python download_douyin.py "https://v.douyin.com/xxxxx" ./video.mp4
python download_douyin.py "https://www.douyin.com/video/xxxxx" ./video.mp4
"""
import requests
import re
import json
import sys
import os
from urllib.parse import unquote, urlparse
def is_douyin_url(url: str) -> bool:
"""检查是否为抖音链接"""
douyin_patterns = [
r'v\.douyin\.com',
r'www\.douyin\.com',
r'm\.douyin\.com',
r'douyin\.com/video/',
r'douyin\.com/jingxuan',
]
return any(re.search(pattern, url) for pattern in douyin_patterns)
def extract_video_id(url: str) -> str:
"""从抖音链接中提取视频ID"""
# 尝试从各种格式的链接中提取ID
patterns = [
r'/video/(\d+)',
r'modal_id=(\d+)',
r'share/video/(\d+)',
]
for pattern in patterns:
match = re.search(pattern, url)
if match:
return match.group(1)
# 如果是短链接,返回None,需要获取重定向后的URL
return None
def get_redirect_url(short_url: str) -> tuple:
"""获取重定向后的完整URL"""
headers = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9',
}
try:
response = requests.get(short_url, headers=headers, allow_redirects=True, timeout=10)
return response.url, headers['User-Agent'], response.text
except Exception as e:
print(f"✗ 获取重定向URL失败: {e}")
return None, None, None
def extract_render_data(html: str) -> dict:
"""从HTML中提取RENDER_DATA"""
# 尝试多种可能的模式
patterns = [
r'<script id="RENDER_DATA" type="application/json">([^<]+)</script>',
r'window\._ROUTER_DATA\s*=\s*(\{.+?\});?\s*</script>',
r'window\._SSR_DATA\s*=\s*(\{.+?\});?\s*</script>',
r'window\._SSR_HYDRATED_DATA\s*=\s*(\{.+?\});?\s*</script>',
]
for pattern in patterns:
matches = re.findall(pattern, html, re.DOTALL)
if matches:
data_str = matches[0]
# URL解码
if '%' in data_str:
data_str = unquote(data_str)
try:
return json.loads(data_str)
except json.JSONDecodeError:
continue
return None
def extract_video_url(data: dict) -> str:
"""从RENDER_DATA中提取视频URL"""
def get_nested(obj, path):
"""安全地获取嵌套字典/列表值"""
current = obj
for key in path:
if isinstance(current, dict) and key in current:
current = current[key]
elif isinstance(current, list) and isinstance(key, int) and key < len(current):
current = current[key]
else:
return None
return current
# 尝试多种可能的路径
possible_paths = [
['loaderData', 'video_(id)/page', 'videoInfoRes', 'item_list', 0, 'video', 'play_addr', 'url_list'],
['loaderData', 'video_(id)/page', 'aweme_detail', 'video', 'play_addr', 'url_list'],
['videoInfoRes', 'item_list', 0, 'video', 'play_addr', 'url_list'],
['app', 'videoInfoRes', 'item_list', 0, 'video', 'play_addr', 'url_list'],
['app', 'videoDetail', 'video', 'play_addr', 'url_list'],
['video', 'play_addr', 'url_list'],
['aweme_detail', 'video', 'play_addr', 'url_list'],
]
for path in possible_paths:
url_list = get_nested(data, path)
if url_list and isinstance(url_list, list) and len(url_list) > 0:
video_url = url_list[0]
# 替换playwm为play获取无水印版本
video_url = video_url.replace('playwm', 'play')
return video_url
# 如果路径查找失败,尝试正则搜索
json_str = json.dumps(data)
play_patterns = [
r'"play_addr":\s*\{[^}]*"url_list":\s*\["([^"]+)"',
r'"playAddr":\s*\["([^"]+)"',
r'"download_addr":\s*\{[^}]*"url_list":\s*\["([^"]+)"',
]
for pattern in play_patterns:
matches = re.findall(pattern, json_str)
if matches:
video_url = matches[0].replace('playwm', 'play')
return video_url
return None
def download_video(video_url: str, output_path: str, user_agent: str) -> bool:
"""下载视频"""
headers = {
'User-Agent': user_agent,
'Referer': 'https://www.douyin.com/',
'Accept': '*/*',
'Accept-Language': 'zh-CN,zh;q=0.9',
}
try:
response = requests.get(video_url, headers=headers, stream=True, timeout=60)
if response.status_code not in [200, 206]:
print(f"✗ 下载失败,状态码: {response.status_code}")
return False
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
percent = (downloaded / total_size) * 100
print(f"\r进度: {percent:.1f}% ({downloaded:,}/{total_size:,} bytes)", end='', flush=True)
print() # 换行
return True
except Exception as e:
print(f"✗ 下载视频时出错: {e}")
return False
def download_douyin_video(url: str, output_path: str) -> bool:
"""
下载抖音视频的主函数
Args:
url: 抖音视频链接(支持短链接和长链接)
output_path: 输出文件路径
Returns:
bool: 下载是否成功
"""
print(f"🎬 开始下载抖音视频")
print(f" 链接: {url}")
print(f" 输出: {output_path}")
print()
# 步骤1: 获取重定向URL和页面内容
print("步骤 1/4: 获取页面信息...")
full_url, user_agent, html = get_redirect_url(url)
if not full_url:
return False
print(f"✓ 获取到页面 ({len(html):,} 字符)")
# 步骤2: 提取RENDER_DATA
print("\n步骤 2/4: 提取视频数据...")
render_data = extract_render_data(html)
if not render_data:
print("✗ 无法提取视频数据")
return False
print("✓ 提取到视频数据")
# 步骤3: 提取视频URL
print("\n步骤 3/4: 解析视频地址...")
video_url = extract_video_url(render_data)
if not video_url:
print("✗ 无法获取视频下载地址")
return False
print(f"✓ 获取到视频地址")
# 步骤4: 下载视频
print("\n步骤 4/4: 下载视频...")
success = download_video(video_url, output_path, user_agent)
if success:
file_size = os.path.getsize(output_path)
print(f"✓ 下载完成: {file_size:,} bytes")
return True
else:
return False
def main():
if len(sys.argv) < 3:
print("用法: python download_douyin.py <抖音链接> <输出路径>")
print("示例: python download_douyin.py 'https://v.douyin.com/xxxxx' ./video.mp4")
sys.exit(1)
url = sys.argv[1]
output_path = sys.argv[2]
# 检查是否为抖音链接
if not is_douyin_url(url):
print(f"✗ 不是有效的抖音链接: {url}")
sys.exit(1)
success = download_douyin_video(url, output_path)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
智能字幕提取脚本 - FunASR + RapidOCR 版本
流程:B站API字幕 → 内嵌字幕 → 烧录字幕检测(RapidOCR) → FunASR语音转录
技术栈:
- B站 API: 直接获取平台字幕(需 cookies)
- RapidOCR (ONNX): 轻量级 OCR,用于提取烧录字幕
- FunASR: 中文语音转录,配合 VAD 分段和标点模型
"""
import subprocess
import sys
import os
import re
import tempfile
from pathlib import Path
import json
# ============================================================
# L0: B站 API 字幕获取(最高优先级)
# ============================================================
def extract_bvid(video_url_or_path: str) -> str:
"""从 URL 或文件名中提取 B站 BV 号"""
# 匹配 BV 号模式(BV + 10位字母数字)
match = re.search(r'(BV[a-zA-Z0-9]{10})', video_url_or_path)
if match:
return match.group(1)
return ""
def get_bilibili_subtitle(bvid: str, output_srt: str) -> bool:
"""
通过 B站 API 获取字幕
自动从浏览器读取 cookies,无需手动配置
优先级: yt-dlp cookies > browser_cookie3 > 配置文件/环境变量
"""
# 调用独立的字幕获取脚本
script_dir = os.path.dirname(os.path.abspath(__file__))
fetch_script = os.path.join(script_dir, "fetch_bilibili_subtitle.py")
if os.path.exists(fetch_script):
try:
cmd = [sys.executable, fetch_script, bvid, output_srt]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
print(result.stdout)
if result.stderr:
print(result.stderr)
if result.returncode == 0 and os.path.exists(output_srt):
# 检查文件是否有实际内容
if os.path.getsize(output_srt) > 10:
return True
return False
except subprocess.TimeoutExpired:
print(" ⚠️ 字幕获取超时")
return False
except Exception as e:
print(f" ⚠️ 调用字幕获取脚本失败: {e}")
return False
else:
print(f" ⚠️ 未找到 fetch_bilibili_subtitle.py 脚本")
# 回退到简单的无 cookies 尝试
return _simple_bilibili_fetch(bvid, output_srt)
def _simple_bilibili_fetch(bvid: str, output_srt: str) -> bool:
"""简单的 B站字幕获取(无 cookies,通常会失败但不影响流程)"""
try:
import requests
except ImportError:
return False
headers = {
"User-Agent": "Mozilla/5.0",
"Referer": "https://www.bilibili.com",
}
try:
resp = requests.get(
f"https://api.bilibili.com/x/player/pagelist?bvid={bvid}",
headers=headers, timeout=10
)
data = resp.json()
if data.get("code") != 0 or not data.get("data"):
return False
cid = data["data"][0]["cid"]
resp = requests.get(
f"https://api.bilibili.com/x/web-interface/view?bvid={bvid}",
headers=headers, timeout=10
)
aid = resp.json()["data"]["aid"]
resp = requests.get(
f"https://api.bilibili.com/x/player/wbi/v2?aid={aid}&cid={cid}",
headers=headers, timeout=10
)
subtitles = resp.json().get("data", {}).get("subtitle", {}).get("subtitles", [])
if not subtitles:
return False
sub_url = subtitles[0].get("subtitle_url", "")
if sub_url.startswith("//"):
sub_url = "https:" + sub_url
resp = requests.get(sub_url, headers=headers, timeout=10)
body = resp.json().get("body", [])
if not body:
return False
with open(output_srt, 'w', encoding='utf-8') as f:
for i, item in enumerate(body, 1):
start = format_timestamp(item.get("from", 0))
end = format_timestamp(item.get("to", 0))
content = item.get("content", "").strip()
if content:
f.write(f"{i}\n{start} --> {end}\n{content}\n\n")
return True
except Exception:
return False
# ============================================================
# L1: 内嵌字幕检测
# ============================================================
def check_embedded_subtitle(video_path: str) -> tuple[bool, str]:
"""
检查视频是否包含内嵌字幕流
返回: (是否有内嵌字幕, 字幕文件路径或错误信息)
"""
try:
cmd = [
"ffprobe", "-v", "quiet", "-print_format", "json",
"-show_streams", video_path
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
data = json.loads(result.stdout)
streams = data.get("streams", [])
subtitle_streams = [s for s in streams if s.get("codec_type") == "subtitle"]
if subtitle_streams:
output_srt = video_path.rsplit(".", 1)[0] + "_embedded.srt"
cmd = [
"ffmpeg", "-y", "-i", video_path,
"-map", f"0:s:0", output_srt
]
subprocess.run(cmd, capture_output=True, check=True)
return True, output_srt
else:
return False, "无内嵌字幕流"
except Exception as e:
return False, f"检测失败: {e}"
# ============================================================
# L2: 烧录字幕检测与提取 (RapidOCR)
# ============================================================
def capture_frame(video_path: str, timestamp: str = "00:00:05") -> str:
"""截取视频指定时间的帧"""
try:
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
frame_path = tmp.name
cmd = [
"ffmpeg", "-y", "-ss", timestamp, "-i", video_path,
"-vframes", "1", "-q:v", "2", frame_path
]
subprocess.run(cmd, capture_output=True, check=True)
return frame_path
except Exception as e:
return ""
def _format_time_hms(seconds: int) -> str:
"""将秒数格式化为 HH:MM:SS 格式(用于 ffmpeg 时间戳)"""
h = seconds // 3600
m = (seconds % 3600) // 60
s = seconds % 60
return f"{h:02d}:{m:02d}:{s:02d}"
def check_burned_subtitle(frame_path: str) -> bool:
"""使用 RapidOCR 检测画面是否有烧录字幕"""
try:
from rapidocr_onnxruntime import RapidOCR
ocr = RapidOCR()
result = ocr(frame_path)
# 如果检测到文字,认为有烧录字幕
if result and result[0]:
text_count = len([line for line in result[0] if line])
# 检测到至少2行文字,认为是字幕
return text_count >= 2
return False
except ImportError:
print("⚠️ RapidOCR 未安装,跳过烧录字幕检测")
print(" 安装命令: pip install rapidocr-onnxruntime")
return False
except Exception as e:
print(f"⚠️ OCR 检测失败: {e}")
return False
def extract_burned_subtitle_ocr(video_path: str, output_srt: str) -> bool:
"""使用 RapidOCR 提取烧录字幕"""
try:
from rapidocr_onnxruntime import RapidOCR
print("🔍 使用 RapidOCR 提取烧录字幕...")
cmd = [
"ffprobe", "-v", "error", "-show_entries",
"format=duration", "-of", "default=noprint_wrappers=1:nokey=1",
video_path
]
result = subprocess.run(cmd, capture_output=True, text=True)
duration = float(result.stdout.strip())
ocr = RapidOCR()
# 每隔2秒截取一帧进行 OCR(减少计算量)
subtitles = []
for t in range(0, int(duration), 2):
timestamp = _format_time_hms(t)
frame_path = capture_frame(video_path, timestamp)
if not frame_path:
continue
result = ocr(frame_path)
if result and result[0]:
# 提取文字
texts = []
for line in result[0]:
if line:
text = line[1]
confidence = line[2]
# 修复: confidence 可能是 str 类型,统一转为 float
try:
conf = float(confidence)
except (ValueError, TypeError):
conf = 0.0
if conf > 0.7: # 置信度阈值
texts.append(text)
if texts:
start_ts = format_timestamp(t)
end_ts = format_timestamp(t + 2)
subtitles.append({
'index': len(subtitles) + 1,
'start': start_ts,
'end': end_ts,
'text': ' '.join(texts)
})
os.unlink(frame_path)
# 写入 SRT 文件
with open(output_srt, 'w', encoding='utf-8') as f:
for sub in subtitles:
f.write(f"{sub['index']}\n")
f.write(f"{sub['start']} --> {sub['end']}\n")
f.write(f"{sub['text']}\n\n")
print(f"✅ OCR 提取完成: {len(subtitles)} 条字幕")
return True
except Exception as e:
print(f"❌ OCR 提取失败: {e}")
return False
# ============================================================
# L3: FunASR 语音转录
# ============================================================
def extract_audio(video_path: str, audio_path: str) -> bool:
"""从视频中提取音频"""
try:
cmd = [
"ffmpeg", "-y", "-i", video_path,
"-vn", "-acodec", "pcm_s16le",
"-ar", "16000", "-ac", "1",
audio_path
]
subprocess.run(cmd, capture_output=True, check=True)
return True
except subprocess.CalledProcessError as e:
print(f"❌ 音频提取失败: {e}")
return False
def format_timestamp(seconds: float) -> str:
"""格式化时间戳为 SRT 格式"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
millis = int((seconds % 1) * 1000)
return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
def _split_text_by_punctuation(text: str, timestamps: list) -> list:
"""
按标点符号切分带字级时间戳的文本为自然句
timestamps: [[start_ms, end_ms], ...] 每个字/词的时间戳
返回: [{'text': str, 'start_ms': int, 'end_ms': int}, ...]
"""
# 句末标点
sentence_endings = set('。!?!?;;…')
# 次级切分标点(逗号等,仅在句子过长时切)
clause_breaks = set(',,、')
sentences = []
current_chars = []
current_start_idx = 0
ts_len = len(timestamps)
text_len = len(text)
for char_idx, char in enumerate(text):
current_chars.append(char)
# 映射字符位置到时间戳位置
ts_idx = min(int(char_idx / text_len * ts_len), ts_len - 1) if ts_len > 0 else 0
is_end = char in sentence_endings
is_clause = char in clause_breaks and len(current_chars) > 25 # 逗号切分仅在 >25 字时
is_last = char_idx == text_len - 1
if is_end or is_clause or is_last:
sent_text = ''.join(current_chars).strip()
if sent_text:
start_ts_idx = min(int(current_start_idx / text_len * ts_len), ts_len - 1) if ts_len > 0 else 0
end_ts_idx = ts_idx
start_ms = timestamps[start_ts_idx][0] if ts_len > 0 else 0
end_ms = timestamps[end_ts_idx][1] if ts_len > 0 else 0
sentences.append({
'text': sent_text,
'start_ms': start_ms,
'end_ms': end_ms,
})
current_chars = []
current_start_idx = char_idx + 1
return sentences
def extract_with_funasr(video_path: str, output_srt: str) -> bool:
"""
使用 FunASR 进行语音转录
配合 VAD 分段模型 + 标点模型,正确处理长音频
调用方式参照 FunASR 官方 demo:
https://github.com/modelscope/FunASR/blob/main/examples/industrial_data_pretraining/paraformer/demo.py
"""
try:
from funasr import AutoModel
print("🎤 使用 FunASR 进行语音转录...")
print(" ASR 模型: paraformer-zh (含 VAD + 标点)")
print(" ⚠️ 首次运行需下载约 2-3GB 模型文件,请耐心等待")
# 提取音频
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
audio_path = tmp.name
if not extract_audio(video_path, audio_path):
return False
# 加载 FunASR 模型(官方推荐的短名称 + VAD + 标点)
model = AutoModel(
model="paraformer-zh",
vad_model="fsmn-vad",
vad_kwargs={"max_single_segment_time": 60000},
punc_model="ct-punc",
disable_update=True,
)
# 转录(VAD 自动分段,标点自动恢复,cache={} 是官方推荐参数)
result = model.generate(
input=audio_path,
batch_size_s=300,
cache={},
)
# 生成 SRT
subtitle_count = 0
with open(output_srt, 'w', encoding='utf-8') as f:
for res in result:
text = res.get('text', '').strip()
timestamps = res.get('timestamp', [])
sentence_info = res.get('sentence_info', [])
if sentence_info:
# 方案A: 使用句级时间戳(最佳,如果模型返回了)
for sent in sentence_info:
sent_text = sent.get('text', '').strip()
if sent_text:
subtitle_count += 1
start = format_timestamp(sent.get('start', 0) / 1000)
end = format_timestamp(sent.get('end', 0) / 1000)
f.write(f"{subtitle_count}\n{start} --> {end}\n{sent_text}\n\n")
elif timestamps and text:
# 方案B: 按标点符号切分 + 字级时间戳映射
sentences = _split_text_by_punctuation(text, timestamps)
for sent in sentences:
subtitle_count += 1
start = format_timestamp(sent['start_ms'] / 1000)
end = format_timestamp(sent['end_ms'] / 1000)
f.write(f"{subtitle_count}\n{start} --> {end}\n{sent['text']}\n\n")
elif text:
# 方案C: 无时间戳,仅输出文本
subtitle_count += 1
f.write(f"{subtitle_count}\n00:00:00,000 --> 00:00:00,000\n{text}\n\n")
# 清理临时文件
os.unlink(audio_path)
print(f"✅ FunASR 转录完成: {subtitle_count} 条字幕")
return subtitle_count > 0
except ImportError:
print("❌ FunASR 未安装")
print(" 安装命令: pip install funasr modelscope torchaudio")
return False
except Exception as e:
print(f"❌ FunASR 转录失败: {e}")
import traceback
traceback.print_exc()
return False
# ============================================================
# 主流程
# ============================================================
def smart_subtitle_extraction(video_path: str, output_srt: str, video_url: str = "") -> tuple[bool, str]:
"""
智能字幕提取主函数
流程: B站API字幕 → 内嵌字幕 → 烧录字幕(RapidOCR) → FunASR语音转录
返回: (是否成功, 使用的模式)
"""
print("=" * 50)
print("🎬 智能字幕提取 (B站API + RapidOCR + FunASR)")
print("=" * 50)
print(f"视频: {video_path}")
print()
# 步骤0: 尝试从B站API获取字幕(最优先)
bvid = extract_bvid(video_url) or extract_bvid(video_path)
if bvid:
print("步骤 0/4: 尝试B站API字幕获取...")
if get_bilibili_subtitle(bvid, output_srt):
return True, "bilibili_api"
print()
# 步骤1: 检查内嵌字幕
print("步骤 1/3: 检查内嵌字幕...")
has_embedded, result = check_embedded_subtitle(video_path)
if has_embedded:
print(f"✅ 发现内嵌字幕,已提取: {result}")
if result != output_srt:
import shutil
shutil.copy(result, output_srt)
return True, "embedded"
else:
print(f"⚠️ {result}")
# 步骤2: 检测烧录字幕
print("\n步骤 2/3: 检测烧录字幕 (RapidOCR)...")
frame_path = capture_frame(video_path, "00:00:05")
if frame_path:
has_burned = check_burned_subtitle(frame_path)
os.unlink(frame_path)
if has_burned:
print("✅ 检测到烧录字幕,使用 RapidOCR 提取...")
if extract_burned_subtitle_ocr(video_path, output_srt):
return True, "ocr"
else:
print("⚠️ 未检测到烧录字幕")
# 步骤3: 使用 FunASR
print("\n步骤 3/3: 使用 FunASR 语音转录...")
if extract_with_funasr(video_path, output_srt):
return True, "funasr"
return False, "failed"
def main():
if len(sys.argv) < 3:
print("用法: python extract_subtitle_funasr.py <视频路径> <输出SRT路径> [视频URL]")
print()
print("参数说明:")
print(" 视频路径 - 本地视频文件路径")
print(" 输出SRT - 输出的 SRT 字幕文件路径")
print(" 视频URL - 可选,原始视频URL(用于B站API字幕获取)")
sys.exit(1)
video_path = sys.argv[1]
output_srt = sys.argv[2]
video_url = sys.argv[3] if len(sys.argv) > 3 else ""
if not os.path.exists(video_path):
print(f"❌ 视频文件不存在: {video_path}")
sys.exit(1)
success, mode = smart_subtitle_extraction(video_path, output_srt, video_url)
if success:
print(f"\n✅ 字幕提取成功!")
print(f" 模式: {mode}")
print(f" 输出: {output_srt}")
sys.exit(0)
else:
print(f"\n❌ 字幕提取失败")
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
智能字幕提取脚本
流程:内嵌字幕 → 烧录字幕检测(OCR) → Whisper语音转录
"""
import subprocess
import sys
import os
import tempfile
from pathlib import Path
import json
def check_embedded_subtitle(video_path: str) -> tuple[bool, str]:
"""
检查视频是否包含内嵌字幕流
返回: (是否有内嵌字幕, 字幕文件路径或错误信息)
"""
try:
# 使用 ffprobe 检查字幕流
cmd = [
"ffprobe", "-v", "quiet", "-print_format", "json",
"-show_streams", video_path
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
data = json.loads(result.stdout)
streams = data.get("streams", [])
subtitle_streams = [s for s in streams if s.get("codec_type") == "subtitle"]
if subtitle_streams:
# 提取第一个字幕流
output_srt = video_path.rsplit(".", 1)[0] + "_embedded.srt"
cmd = [
"ffmpeg", "-y", "-i", video_path,
"-map", f"0:s:0", output_srt
]
subprocess.run(cmd, capture_output=True, check=True)
return True, output_srt
else:
return False, "无内嵌字幕流"
except Exception as e:
return False, f"检测失败: {e}"
def capture_frame(video_path: str, timestamp: str = "00:00:05") -> str:
"""
截取视频指定时间的帧
默认截取第5秒(通常有字幕出现)
"""
try:
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
frame_path = tmp.name
cmd = [
"ffmpeg", "-y", "-ss", timestamp, "-i", video_path,
"-vframes", "1", "-q:v", "2", frame_path
]
subprocess.run(cmd, capture_output=True, check=True)
return frame_path
except Exception as e:
return ""
def check_burned_subtitle(frame_path: str) -> bool:
"""
使用 OCR 检测画面是否有烧录字幕
返回: 是否检测到字幕
"""
try:
from paddleocr import PaddleOCR
ocr = PaddleOCR(
use_angle_cls=True,
lang='ch',
show_log=False,
use_gpu=False # CPU 运行
)
result = ocr.ocr(frame_path, cls=True)
# 如果检测到文字,认为有烧录字幕
if result and result[0]:
text_count = len([line for line in result[0] if line])
# 检测到至少3行文字,认为是字幕
return text_count >= 3
return False
except ImportError:
print("⚠️ PaddleOCR 未安装,跳过烧录字幕检测")
return False
except Exception as e:
print(f"⚠️ OCR 检测失败: {e}")
return False
def extract_burned_subtitle_ocr(video_path: str, output_srt: str) -> bool:
"""
使用 OCR 提取烧录字幕
策略:每隔1秒截取一帧,OCR识别文字,合并为SRT
"""
try:
from paddleocr import PaddleOCR
import cv2
print("🔍 使用 OCR 提取烧录字幕...")
# 获取视频时长
cmd = [
"ffprobe", "-v", "error", "-show_entries",
"format=duration", "-of", "default=noprint_wrappers=1:nokey=1",
video_path
]
result = subprocess.run(cmd, capture_output=True, text=True)
duration = float(result.stdout.strip())
ocr = PaddleOCR(
use_angle_cls=True,
lang='ch',
show_log=False,
use_gpu=False
)
# 每隔1秒截取一帧进行 OCR
subtitles = []
for t in range(0, int(duration), 1):
frame_path = capture_frame(video_path, f"00:00:{t:02d}")
if not frame_path:
continue
result = ocr.ocr(frame_path, cls=True)
if result and result[0]:
# 提取文字
texts = []
for line in result[0]:
if line:
text = line[1][0]
confidence = line[1][1]
if confidence > 0.7: # 置信度阈值
texts.append(text)
if texts:
subtitles.append({
'index': len(subtitles) + 1,
'start': f"00:00:{t:02d},000",
'end': f"00:00:{t+1:02d},000",
'text': ' '.join(texts)
})
os.unlink(frame_path)
# 写入 SRT 文件
with open(output_srt, 'w', encoding='utf-8') as f:
for sub in subtitles:
f.write(f"{sub['index']}\n")
f.write(f"{sub['start']} --> {sub['end']}\n")
f.write(f"{sub['text']}\n\n")
print(f"✅ OCR 提取完成: {len(subtitles)} 条字幕")
return True
except Exception as e:
print(f"❌ OCR 提取失败: {e}")
return False
def extract_with_whisper(video_path: str, output_srt: str, model: str = "large") -> bool:
"""
使用 Whisper 进行语音转录
"""
try:
import whisper
import torch
print(f"🎤 使用 Whisper {model} 进行语音转录...")
# 检查 CUDA
device = "cuda" if torch.cuda.is_available() else "cpu"
if device == "cpu":
print("⚠️ CUDA 不可用,使用 CPU(速度较慢)")
# 加载模型
model = whisper.load_model(model, device=device)
# 转录
result = model.transcribe(
video_path,
language="zh",
task="transcribe",
verbose=False
)
# 生成 SRT
def format_timestamp(seconds: float) -> str:
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
millis = int((seconds % 1) * 1000)
return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
with open(output_srt, 'w', encoding='utf-8') as f:
for i, segment in enumerate(result["segments"], 1):
start = format_timestamp(segment["start"])
end = format_timestamp(segment["end"])
text = segment["text"].strip()
f.write(f"{i}\n{start} --> {end}\n{text}\n\n")
print(f"✅ Whisper 转录完成: {len(result['segments'])} 条字幕")
return True
except Exception as e:
print(f"❌ Whisper 转录失败: {e}")
return False
def smart_subtitle_extraction(video_path: str, output_srt: str) -> tuple[bool, str]:
"""
智能字幕提取主函数
流程: 内嵌字幕 → 烧录字幕(OCR) → Whisper语音转录
返回: (是否成功, 使用的模式)
"""
print("=" * 50)
print("🎬 智能字幕提取")
print("=" * 50)
print(f"视频: {video_path}")
print()
# 步骤1: 检查内嵌字幕
print("步骤 1/3: 检查内嵌字幕...")
has_embedded, result = check_embedded_subtitle(video_path)
if has_embedded:
print(f"✅ 发现内嵌字幕,已提取: {result}")
# 复制到目标路径
if result != output_srt:
import shutil
shutil.copy(result, output_srt)
return True, "embedded"
else:
print(f"⚠️ {result}")
# 步骤2: 检测烧录字幕
print("\n步骤 2/3: 检测烧录字幕...")
frame_path = capture_frame(video_path, "00:00:05")
if frame_path:
has_burned = check_burned_subtitle(frame_path)
os.unlink(frame_path)
if has_burned:
print("✅ 检测到烧录字幕,使用 OCR 提取...")
if extract_burned_subtitle_ocr(video_path, output_srt):
return True, "ocr"
else:
print("⚠️ 未检测到烧录字幕")
# 步骤3: 使用 Whisper
print("\n步骤 3/3: 使用 Whisper 语音转录...")
if extract_with_whisper(video_path, output_srt, "large"):
return True, "whisper"
return False, "failed"
def main():
if len(sys.argv) < 3:
print("用法: python extract_subtitle.py <视频路径> <输出SRT路径>")
sys.exit(1)
video_path = sys.argv[1]
output_srt = sys.argv[2]
if not os.path.exists(video_path):
print(f"❌ 视频文件不存在: {video_path}")
sys.exit(1)
success, mode = smart_subtitle_extraction(video_path, output_srt)
if success:
print(f"\n✅ 字幕提取成功!")
print(f" 模式: {mode}")
print(f" 输出: {output_srt}")
sys.exit(0)
else:
print(f"\n❌ 字幕提取失败")
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
B站字幕获取脚本 - 自动从浏览器读取 cookies 并获取视频字幕
功能:
- 自动检测已登录的浏览器并获取 B站 cookies
- 通过 B站 API 直接获取 AI 生成字幕(比本地 ASR 更快更准)
- 输出标准 SRT 格式字幕文件
Cookies 获取优先级:
1. yt-dlp --cookies-from-browser(最可靠,持续跟进浏览器加密更新)
2. browser_cookie3 Python 库(备选)
3. 手动配置 ~/.bilibili_cookies.txt 或环境变量(兜底)
用法:
python fetch_bilibili_subtitle.py <BV号或URL> <输出SRT路径> [--browser chrome|firefox|safari|edge]
示例:
python fetch_bilibili_subtitle.py BV1vdZ6BJEcQ output.srt
python fetch_bilibili_subtitle.py "https://www.bilibili.com/video/BV1vdZ6BJEcQ/" output.srt
python fetch_bilibili_subtitle.py BV1vdZ6BJEcQ output.srt --browser firefox
"""
import subprocess
import sys
import os
import re
import json
import tempfile
import argparse
from pathlib import Path
try:
import requests
except ImportError:
print("❌ requests 未安装: pip install requests")
sys.exit(1)
# ============================================================
# BV号/URL 解析
# ============================================================
def extract_bvid(input_str: str) -> str:
"""从 URL、BV号 或文件名中提取 BV号"""
# 直接是 BV号
match = re.search(r'(BV[a-zA-Z0-9]{10})', input_str)
if match:
return match.group(1)
# 短链需要解析重定向
if 'b23.tv' in input_str:
try:
resp = requests.head(input_str, allow_redirects=True, timeout=10)
match = re.search(r'(BV[a-zA-Z0-9]{10})', resp.url)
if match:
return match.group(1)
except Exception:
pass
return ""
# ============================================================
# Cookies 获取策略
# ============================================================
def get_cookies_via_ytdlp(browser: str = "chrome") -> dict:
"""
策略1: 通过 yt-dlp 从浏览器获取 cookies(最可靠)
yt-dlp 持续跟进浏览器加密更新,比第三方库更稳定
"""
print(f" 🔑 尝试从 {browser} 获取 cookies (via yt-dlp)...")
try:
# 用 yt-dlp 导出 cookies 到临时文件
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode='w') as tmp:
cookies_file = tmp.name
cmd = [
"yt-dlp",
"--cookies-from-browser", browser,
"--cookies", cookies_file,
"--skip-download",
"--no-warnings",
"-q",
"https://www.bilibili.com/video/BV1xx411c7mD/", # 任意有效BV号
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30,
)
if os.path.exists(cookies_file) and os.path.getsize(cookies_file) > 0:
cookies = _parse_netscape_cookies(cookies_file, ".bilibili.com")
os.unlink(cookies_file)
if cookies.get("SESSDATA"):
print(f" ✅ 成功获取 cookies (SESSDATA={cookies['SESSDATA'][:8]}...)")
return cookies
else:
print(f" ⚠️ cookies 中无 SESSDATA(可能未登录B站)")
return {}
else:
os.unlink(cookies_file) if os.path.exists(cookies_file) else None
print(f" ⚠️ yt-dlp 未能导出 cookies")
return {}
except FileNotFoundError:
print(f" ⚠️ yt-dlp 未安装,跳过")
return {}
except subprocess.TimeoutExpired:
print(f" ⚠️ yt-dlp 超时(可能需要系统钥匙串权限)")
return {}
except Exception as e:
print(f" ⚠️ yt-dlp 获取失败: {e}")
return {}
def get_cookies_via_browser_cookie3(browser: str = "chrome") -> dict:
"""
策略2: 通过 browser_cookie3 库获取 cookies
注意: Chrome 2024年后新加密可能导致部分 cookies 值为空
"""
print(f" 🔑 尝试从 {browser} 获取 cookies (via browser_cookie3)...")
try:
import browser_cookie3
browser_map = {
"chrome": browser_cookie3.chrome,
"firefox": browser_cookie3.firefox,
"edge": browser_cookie3.edge,
"opera": browser_cookie3.opera,
}
if browser not in browser_map:
print(f" ⚠️ browser_cookie3 不支持 {browser}")
return {}
cj = browser_map[browser](domain_name=".bilibili.com")
cookies = {}
for cookie in cj:
if cookie.domain and ".bilibili.com" in cookie.domain:
cookies[cookie.name] = cookie.value
if cookies.get("SESSDATA"):
print(f" ✅ 成功获取 cookies (SESSDATA={cookies['SESSDATA'][:8]}...)")
return cookies
else:
print(f" ⚠️ cookies 中无 SESSDATA(可能未登录或加密问题)")
return {}
except ImportError:
print(f" ⚠️ browser_cookie3 未安装,跳过 (pip install browser_cookie3)")
return {}
except Exception as e:
print(f" ⚠️ browser_cookie3 获取失败: {e}")
return {}
def get_cookies_from_config() -> dict:
"""
策略3: 从配置文件或环境变量获取 cookies(兜底方案)
支持:
- 环境变量: BILIBILI_SESSDATA, BILIBILI_BILI_JCT
- cookies 文件: ~/.bilibili_cookies.txt (Netscape 格式)
"""
print(" 🔑 尝试从配置文件/环境变量获取 cookies...")
cookies = {}
# 方式A: 环境变量
sessdata = os.environ.get("BILIBILI_SESSDATA", "")
if sessdata:
cookies["SESSDATA"] = sessdata
bili_jct = os.environ.get("BILIBILI_BILI_JCT", "")
if bili_jct:
cookies["bili_jct"] = bili_jct
print(f" ✅ 从环境变量获取 (SESSDATA={sessdata[:8]}...)")
return cookies
# 方式B: Netscape cookies 文件
cookies_file = os.path.expanduser("~/.bilibili_cookies.txt")
if os.path.exists(cookies_file):
cookies = _parse_netscape_cookies(cookies_file, ".bilibili.com")
if cookies.get("SESSDATA"):
print(f" ✅ 从 {cookies_file} 获取 (SESSDATA={cookies['SESSDATA'][:8]}...)")
return cookies
print(" ⚠️ 未找到配置的 cookies")
return {}
def _parse_netscape_cookies(filepath: str, domain_filter: str = "") -> dict:
"""解析 Netscape 格式 cookies 文件"""
cookies = {}
try:
with open(filepath, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
parts = line.split('\t')
if len(parts) >= 7:
domain = parts[0]
name = parts[5]
value = parts[6]
if not domain_filter or domain_filter in domain:
cookies[name] = value
except Exception:
pass
return cookies
def get_bilibili_cookies(preferred_browser: str = "chrome") -> dict:
"""
按优先级尝试获取 B站 cookies
优先级:yt-dlp > browser_cookie3 > 配置文件/环境变量
"""
print("\n📦 获取 B站 cookies...")
# 要尝试的浏览器列表
browsers = [preferred_browser]
for b in ["chrome", "firefox", "edge", "safari"]:
if b not in browsers:
browsers.append(b)
# 策略1: yt-dlp(按浏览器优先级)
for browser in browsers:
cookies = get_cookies_via_ytdlp(browser)
if cookies.get("SESSDATA"):
return cookies
# 策略2: browser_cookie3(按浏览器优先级)
for browser in browsers:
cookies = get_cookies_via_browser_cookie3(browser)
if cookies.get("SESSDATA"):
return cookies
# 策略3: 配置文件/环境变量
cookies = get_cookies_from_config()
if cookies.get("SESSDATA"):
return cookies
return {}
# ============================================================
# B站 API 字幕获取
# ============================================================
def fetch_subtitle(bvid: str, cookies: dict, output_srt: str) -> bool:
"""通过 B站 API 获取字幕并保存为 SRT"""
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"Referer": "https://www.bilibili.com",
}
try:
# 步骤1: BV号 → CID
print("\n📡 调用 B站 API...")
url = f"https://api.bilibili.com/x/player/pagelist?bvid={bvid}"
resp = requests.get(url, headers=headers, cookies=cookies, timeout=10)
data = resp.json()
if data.get("code") != 0 or not data.get("data"):
print(f" ❌ 获取视频信息失败: {data.get('message', '未知错误')}")
return False
cid = data["data"][0]["cid"]
part_name = data["data"][0].get("part", "")
duration = data["data"][0].get("duration", 0)
print(f" 📺 视频: {part_name} (时长: {duration}s, CID: {cid})")
# 步骤2: 获取 AID
url = f"https://api.bilibili.com/x/web-interface/view?bvid={bvid}"
resp = requests.get(url, headers=headers, cookies=cookies, timeout=10)
data = resp.json()
if data.get("code") != 0:
print(f" ❌ 获取AID失败: {data.get('message', '未知错误')}")
return False
aid = data["data"]["aid"]
title = data["data"].get("title", "")
print(f" 📝 标题: {title}")
# 步骤3: 获取字幕列表
url = f"https://api.bilibili.com/x/player/wbi/v2?aid={aid}&cid={cid}"
resp = requests.get(url, headers=headers, cookies=cookies, timeout=10)
data = resp.json()
if data.get("code") != 0:
print(f" ❌ 获取字幕信息失败: {data.get('message', '未知错误')}")
return False
subtitles = data.get("data", {}).get("subtitle", {}).get("subtitles", [])
if not subtitles:
print(" ⚠️ 该视频无字幕(未开启AI字幕或需要登录)")
return False
# 显示可用字幕
print(f" 📋 可用字幕: {len(subtitles)} 条")
for s in subtitles:
print(f" - {s.get('lan_doc', '?')} ({s.get('lan', '?')})")
# 选择中文字幕(优先 ai-zh)
chosen = subtitles[0]
for s in subtitles:
if s.get("lan") in ("ai-zh", "zh-Hans", "zh-CN", "zh"):
chosen = s
break
subtitle_url = chosen.get("subtitle_url", "")
if not subtitle_url:
print(" ❌ 字幕URL为空")
return False
if subtitle_url.startswith("//"):
subtitle_url = "https:" + subtitle_url
# 步骤4: 下载字幕 JSON
resp = requests.get(subtitle_url, headers=headers, timeout=10)
subtitle_data = resp.json()
body = subtitle_data.get("body", [])
if not body:
print(" ❌ 字幕内容为空")
return False
# 步骤5: 转换为 SRT 格式
with open(output_srt, 'w', encoding='utf-8') as f:
for i, item in enumerate(body, 1):
start = item.get("from", 0)
end = item.get("to", 0)
content = item.get("content", "").strip()
if content:
start_ts = _format_srt_timestamp(start)
end_ts = _format_srt_timestamp(end)
f.write(f"{i}\n{start_ts} --> {end_ts}\n{content}\n\n")
print(f"\n✅ 字幕获取成功!")
print(f" 条数: {len(body)}")
print(f" 语言: {chosen.get('lan_doc', '未知')}")
print(f" 输出: {output_srt}")
return True
except requests.exceptions.RequestException as e:
print(f" ❌ 网络请求失败: {e}")
return False
except Exception as e:
print(f" ❌ 字幕获取失败: {e}")
import traceback
traceback.print_exc()
return False
def _format_srt_timestamp(seconds: float) -> str:
"""格式化时间戳为 SRT 格式 HH:MM:SS,mmm"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
millis = int((seconds % 1) * 1000)
return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
# ============================================================
# 主入口
# ============================================================
def main():
parser = argparse.ArgumentParser(
description="从 B站获取视频字幕(自动读取浏览器 cookies)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python fetch_bilibili_subtitle.py BV1vdZ6BJEcQ output.srt
python fetch_bilibili_subtitle.py "https://b23.tv/W2ot8As" output.srt
python fetch_bilibili_subtitle.py BV1vdZ6BJEcQ output.srt --browser firefox
Cookies 获取优先级:
1. yt-dlp --cookies-from-browser(最可靠)
2. browser_cookie3 Python 库
3. ~/.bilibili_cookies.txt 或环境变量 BILIBILI_SESSDATA
如果所有自动方式都失败,可以手动配置:
export BILIBILI_SESSDATA="你的SESSDATA值"
export BILIBILI_BILI_JCT="你的bili_jct值"
"""
)
parser.add_argument("input", help="B站 BV号 或 视频URL")
parser.add_argument("output", help="输出 SRT 文件路径")
parser.add_argument("--browser", default="chrome",
help="优先使用的浏览器 (default: chrome)")
args = parser.parse_args()
# 解析 BV号
print("=" * 50)
print("🎬 B站字幕获取工具")
print("=" * 50)
bvid = extract_bvid(args.input)
if not bvid:
print(f"❌ 无法解析 BV号: {args.input}")
sys.exit(1)
print(f"📌 BV号: {bvid}")
# 获取 cookies
cookies = get_bilibili_cookies(args.browser)
if not cookies.get("SESSDATA"):
print("\n❌ 无法获取 B站 cookies,请确保:")
print(" 1. 已在浏览器中登录 bilibili.com")
print(" 2. 已安装 yt-dlp: pip install yt-dlp")
print(" 3. 或安装 browser_cookie3: pip install browser_cookie3")
print(" 4. 或手动设置: export BILIBILI_SESSDATA='你的值'")
sys.exit(1)
# 获取字幕
success = fetch_subtitle(bvid, cookies, args.output)
if success:
sys.exit(0)
else:
print("\n💡 提示: 如果字幕获取失败,可以回退到本地转录方式:")
print(" python extract_subtitle_funasr.py <视频文件> <输出SRT>")
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Whisper 语音转录脚本
使用 OpenAI Whisper 将视频/音频转录为 SRT 字幕
"""
import sys
import os
import subprocess
import tempfile
from pathlib import Path
def check_dependencies():
"""检查必要依赖"""
try:
import whisper
return True
except ImportError:
print("❌ 缺少依赖: openai-whisper")
print("安装命令: pip install openai-whisper")
return False
def extract_audio(video_path: str, audio_path: str) -> bool:
"""从视频中提取音频"""
try:
cmd = [
"ffmpeg", "-y", "-i", video_path,
"-vn", "-acodec", "pcm_s16le",
"-ar", "16000", "-ac", "1",
audio_path
]
subprocess.run(cmd, check=True, capture_output=True)
return True
except subprocess.CalledProcessError as e:
print(f"❌ 音频提取失败: {e}")
return False
def format_timestamp(seconds: float) -> str:
"""格式化时间戳为 SRT 格式"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
millis = int((seconds % 1) * 1000)
return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
def transcribe(video_path: str, output_srt: str, model_name: str = "medium",
language: str = "auto", device: str = "cuda"):
"""
执行转录
Args:
video_path: 视频文件路径
output_srt: 输出 SRT 文件路径
model_name: Whisper 模型 (tiny/base/small/medium/large)
language: 语言 (zh/en/auto)
device: 设备 (cuda/cpu)
"""
import whisper
import torch
# 检查 CUDA 可用性
if device == "cuda" and not torch.cuda.is_available():
print("⚠️ CUDA 不可用,回退到 CPU")
device = "cpu"
print(f"📥 加载 Whisper {model_name} 模型...")
model = whisper.load_model(model_name, device=device)
# 提取音频到临时文件
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
audio_path = tmp.name
print("🎵 提取音频...")
if not extract_audio(video_path, audio_path):
return False
print("🎤 转录中...")
options = {"task": "transcribe"}
if language != "auto":
options["language"] = language
result = model.transcribe(audio_path, **options)
# 生成 SRT 文件
print("📝 生成字幕文件...")
with open(output_srt, "w", encoding="utf-8") as f:
for i, segment in enumerate(result["segments"], 1):
start = format_timestamp(segment["start"])
end = format_timestamp(segment["end"])
text = segment["text"].strip()
f.write(f"{i}\n{start} --> {end}\n{text}\n\n")
# 清理临时文件
os.unlink(audio_path)
print(f"✅ 转录完成: {output_srt}")
print(f" 检测语言: {result.get('language', 'unknown')}")
print(f" 片段数量: {len(result['segments'])}")
return True
def main():
if len(sys.argv) < 3:
print("用法: python transcribe_audio.py <视频路径> <输出SRT路径> [模型] [语言] [设备]")
print("模型: tiny/base/small/medium/large (默认: medium)")
print("语言: zh/en/auto (默认: auto)")
print("设备: cuda/cpu (默认: cuda)")
sys.exit(1)
if not check_dependencies():
sys.exit(1)
video_path = sys.argv[1]
output_srt = sys.argv[2]
model_name = sys.argv[3] if len(sys.argv) > 3 else "medium"
language = sys.argv[4] if len(sys.argv) > 4 else "auto"
device = sys.argv[5] if len(sys.argv) > 5 else "cuda"
if not os.path.exists(video_path):
print(f"❌ 视频文件不存在: {video_path}")
sys.exit(1)
success = transcribe(video_path, output_srt, model_name, language, device)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
Related skills
FAQ
Which ASR engine does video-copy-analyzer use?
It uses FunASR's Paraformer models for fast Chinese speech transcription, downloading about 2-3GB of models on first run.
What analysis frameworks does it apply?
Three: TextContent Analysis, Viral-Abstract-Script, and Brainstorming.