Youtube and Bilibili Subtitle Extraction and Summary

从YouTube或Bilibili提取字幕,生成结构化摘要并保存原始文本。

已扫描
适合谁
内容创作者、知识管理者
不适合谁
无需字幕处理的普通用户、无网络环境的离线使用者
国内可用性
需网络配置。可能需要网络配置或第三方服务可访问。
安装难度
新手友好(★☆☆)。基于终端操作、依赖、API Key 和本地环境要求的初步判断。

安装与下载

openclaw skills install @xiapuyang/youtube-subtitle-summary

Skill 说明

命令、参数、文件名以原文为准

视频字幕提取器(YouTube + Bilibili)

检测平台 → 下载字幕 → 清理 → 保存原始内容 → 生成摘要


步骤 1 — 确保 yt-dlp 可用

if ! command -v yt-dlp &>/dev/null; then
  echo "yt-dlp 未找到,正在安装..."
  pip install -q yt-dlp || pip3 install -q yt-dlp
fi
yt-dlp -U --quiet 2>/dev/null || true

如果安装失败,请停止并提示用户手动安装 yt-dlp(使用 pip install yt-dlpbrew install yt-dlp)。


步骤 2 — 检测平台并下载字幕

根据 URL 判断是 Bilibili 还是 YouTube,并采用相应的策略。

URL="<用户提供的 URL>"
TMPDIR=$(mktemp -d)
SUB_FILE=""
SUBTITLE_LANG=""

# 检测平台
if echo "$URL" | grep -qE '(bilibili\.com|b23\.tv)'; then
  PLATFORM="bilibili"
  SITE_NAME="Bilibili"
  SITE_DOMAIN="bilibili.com"
else
  PLATFORM="youtube"
  SITE_NAME="YouTube"
  SITE_DOMAIN="youtube.com"
fi

Bilibili 分支

Bilibili 字幕需要登录凭据。始终使用 cookies 文件——若文件缺失或已过期(超过 30 天),则从 Chrome 中刷新:

if [ "$PLATFORM" = "bilibili" ]; then
  BILI_COOKIES="${BILIBILI_COOKIES_FILE:-$HOME/bilibili_cookies.txt}"

  NEED_REFRESH=false
  if [ ! -f "$BILI_COOKIES" ]; then
    NEED_REFRESH=true
  elif [ "$(find "$BILI_COOKIES" -mtime +30 2>/dev/null | wc -l | tr -d ' ')" -gt 0 ]; then
    echo "Bilibili cookies 已超过 30 天,正在刷新..."
    NEED_REFRESH=true
  fi

  if [ "$NEED_REFRESH" = true ]; then
    echo "从 Chrome 读取 cookies(一次性的密钥链提示)..."
    yt-dlp --cookies-from-browser chrome --cookies "$BILI_COOKIES" \
      --skip-download -i "https://www.bilibili.com/" 2>/dev/null
  fi

  COOKIE_ARGS="--cookies $BILI_COOKIES"

  # 获取可用字幕语言列表 —— 捕获 stderr 以检测登录失败
  LIST_OUTPUT=$(yt-dlp --list-subs $COOKIE_ARGS "$URL" 2>&1)
  if echo "$LIST_OUTPUT" | grep -qi "login\|not logged\|需要登录\|please log"; then
    echo ""
    echo "❌ Bilibili cookies 已过期或无效。"
    echo "   修复方法:删除 cookies 文件后重试 —— 它将重新从 Chrome 读取。"
    echo "   rm \"$BILI_COOKIES\""
    rm -rf "$TMPDIR"
    exit 1
  fi
  AVAIL_LANGS=$(echo "$LIST_OUTPUT" | awk '/^[a-z]/{print $1}' | grep -v "^Language$")

  # 优先尝试 ai-zh,然后是 zh 变体,最后是 en
  for lang in ai-zh zh-Hans zh-CN zh en; do
    if echo "$AVAIL_LANGS" | grep -q "^${lang}$"; then
      yt-dlp \
        --write-sub \
        --sub-langs "$lang" \
        --skip-download \
        --retries 3 \
        -o "$TMPDIR/bili_%(id)s" \
        $COOKIE_ARGS \
        "$URL" 2>/dev/null
      SUB_FILE=$(ls "$TMPDIR"/*.${lang}.* 2>/dev/null | head -1)
      if [ -n "$SUB_FILE" ]; then
        SUBTITLE_LANG="$lang"
        break
      fi
    fi
  done
fi

YouTube 分支

if [ "$PLATFORM" = "youtube" ]; then
  for lang in zh-Hans zh-CN zh en; do
    yt-dlp \
      --write-subs \
      --write-auto-subs \
      --sub-langs "$lang" \
      --skip-download \
      --sub-format vtt \
      --retries 3 \
      --sleep-requests 1 \
      -o "$TMPDIR/yt_%(id)s" \
      "$URL" 2>/dev/null
    SUB_FILE=$(ls "$TMPDIR"/*.${lang}.vtt 2>/dev/null | head -1)
    if [ -n "$SUB_FILE" ]; then
      SUBTITLE_LANG="$lang"
      break
    fi
    sleep 1
  done
fi

若未获取到字幕则失败

if [ -z "$SUB_FILE" ]; then
  echo "此视频未找到字幕。"
  echo "  - 无手动上传的字幕"
  echo "  - 无自动生成的字幕"
  echo "缺少字幕无法继续处理。"
  rm -rf "$TMPDIR"
  exit 1
fi

步骤 3 — 清理字幕文件 → 纯文本

检测格式(SRT 或 VTT),并进行相应清理:

EXT="${SUB_FILE##*.}"

if [ "$EXT" = "srt" ]; then
  # SRT:移除序号、时间戳、HTML 标签、去重
  grep -v "^[0-9]*$" "$SUB_FILE" \
    | grep -v "^[0-9][0-9]:[0-9][0-9]:[0-9][0-9],[0-9]* --> " \
    | sed 's/<[^>]*>//g' \
    | grep -v "^$" \
    | python3 -c "
import sys, html
seen = set()
for line in sys.stdin:
    line = html.unescape(line).strip()
    if line and line not in seen:
        seen.add(line)
        print(line)
        print()
" > "$TMPDIR/cleaned.txt"
else
  # VTT
  sed 's/<[^>]*>//g' "$SUB_FILE" \
    | grep -v "^WEBVTT" \
    | grep -v "^NOTE" \
    | grep -v "^Kind:" \
    | grep -v "^Language:" \
    | grep -v "^[0-9][0-9]:[0-9][0-9]:[0-9][0-9]" \
    | grep -v "^$" \
    | python3 -c "
import sys, html
seen = set()
for line in sys.stdin:
    line = html.unescape(line).strip()
    if line and line not in seen:
        seen.add(line)
        print(line)
        print()
" > "$TMPDIR/cleaned.txt"
fi

步骤 4 — 确定输出目录并设置文件名

OUTPUT_DIR="${YOUTUBE_SUBTITLES_DIR:-.}"
mkdir -p "$OUTPUT_DIR"

使用原视频标题作为文件名。仅移除 macOS 不支持的字符(/ 和 ASCII 冒号 :);保留其他所有字符,包括全角标点符号(如 《》)。限制长度为 100 个字符:

SLUG=$(echo "<title>" | python3 -c "
import sys
title = sys.stdin.read().strip()
title = title.replace('/', '').replace(':', '')
print(title[:100])
")

步骤 5 — 获取视频元数据

使用 $COOKIE_ARGS(在步骤 2 中设置;YouTube 为空,Bilibili 为 --cookies FILE

yt-dlp --dump-json --no-playlist $COOKIE_ARGS "$URL" 2>/dev/null \
  | python3 -c "
import json, sys
d = json.load(sys.stdin)
desc = d.get('description','')
first_para = desc.split('\n\n')[0].replace('\n',' ')[:300]
chapters = d.get('chapters') or []
chapter_lines = '\n'.join(f'  - \"{int(c[\"start_time\"]//60)}:{int(c[\"start_time\"]%60):02d} {c[\"title\"]}\"' for c in chapters)
cats = d.get('categories') or []
print('TITLE:', d.get('title',''))
print('CHANNEL:', d.get('uploader',''))
print('DURATION:', d.get('duration_string',''))
print('DATE:', d.get('upload_date',''))
print('DESCRIPTION:', first_para)
print('CATEGORY:', cats[0] if cats else '')
print('CHAPTERS:')
print(chapter_lines)
"

步骤 6 — 保存原始字幕文本

写入文件 $OUTPUT_DIR/$SLUG.md

NOW=$(date +"%Y-%m-%dT%H:%M")
WORDS=$(wc -w < "$TMPDIR/cleaned.txt" | tr -d ' ')
---
title: "<title>"
source: "<URL>"
author:
  - "[[<channel>]]"
published: "<YYYYMMDD>"
description: "<DESCRIPTION>"
tags:
  - "<PLATFORM>"
ctime: "<NOW>"
mtime: "<NOW>"
words: "<WORDS>"
site: "<SITE_NAME>"
domain: "<SITE_DOMAIN>"
channel: "<channel>"
duration: "<duration>"
category: "<CATEGORY>"
subtitle_lang: "<SUBTITLE_LANG>"
chapters:
<CHAPTERS or empty>
type: "source"
---

<完整的清洗后字幕文本>

步骤 7 — 检查长度并准备摘要输入

CHARS=$(wc -c < "$TMPDIR/cleaned.txt" | tr -d ' ')
  • 若字符数 ≤ 120000:直接使用完整清洗后的文本作为摘要输入。
  • 若字符数 > 120000:需先执行分治处理(步骤 8),再将合并后的要点作为摘要输入。

步骤 8 — 长字幕的分治处理(仅当字幕超过 12 万字符时)

将文本拆分为约每块 10,000 字符的片段:

python3 - <<'EOF'
text = open("$TMPDIR/cleaned.txt").read()
size = 10000
chunks = [text[i:i+size] for i in range(0, len(text), size)]
for i, chunk in enumerate(chunks):
    print(f"=== CHUNK {i+1}/{len(chunks)} ===")
    print(chunk)
EOF

对每个片段,提取结构化笔记,格式如下:

TOPIC: [该片段的推断主题名称,例如“AI泡沫评估”]
- [关键观点或事实 — 2 句话:内容陈述 + 支持细节]
- [关键观点或事实 — 2 句话]
...
QUOTES: [1–3 条值得保留的原文引用]
DATA: [任何具体数字或指标]

每块提取 8–12 个条目。收集所有结构化笔记,按主题归类,作为步骤 9 的摘要输入。在传递给步骤 9 前,需合并相同主题下的跨块笔记。


步骤 9 — 生成摘要

确定摘要语言:

if [ -n "$YOUTUBE_SUBTITLES_SUMMARY_LANG" ]; then
  SUMMARY_LANG="$YOUTUBE_SUBTITLES_SUMMARY_LANG"
elif [[ "$SUBTITLE_LANG" == zh* ]]; then
  SUMMARY_LANG="zh"
else
  _SYS_LANG="${LANG:-${LANGUAGE:-}}"
  case "$_SYS_LANG" in
    zh*) SUMMARY_LANG="zh" ;;
    *)   SUMMARY_LANG="en" ;;
  esac
fi

使用步骤 7 或 8 提供的摘要输入,生成摘要正文(所有章节),并先写入临时文件:

cat > "$TMPDIR/summary_body.md" <<'SUMMARY_EOF'
<生成的摘要章节内容>
SUMMARY_EOF
SUMMARY_WORDS=$(wc -w < "$TMPDIR/summary_body.md" | tr -d ' ')

然后将前文和正文合并,写入最终文件 $OUTPUT_DIR/$SLUG-summary.md,语言为 $SUMMARY_LANG

cat > "$OUTPUT_DIR/$SLUG-summary.md" <<EOF
<前文信息>
EOF
cat "$TMPDIR/summary_body.md" >> "$OUTPUT_DIR/$SLUG-summary.md"

前文模板:

---
title: "<title>"
source: "<URL>"
author:
published: "<YYYYMMDD>"
description: "<DESCRIPTION>"
tags:
  - "<PLATFORM>"
ctime: "<NOW>"
mtime: "<NOW>"
words: "<SUMMARY_WORDS>"
site: "<SITE_NAME>"
domain: "<SITE_DOMAIN>"
channel: "<channel>"
duration: "<duration>"
category: "<CATEGORY>"
subtitle_lang: "<SUBTITLE_LANG>"
chapters:
<CHAPTERS or empty>
type: "source"
lang: "<SUMMARY_LANG>"
---

章节标题必须与摘要语言一致。根据语言选择对应标题:

**英文 (en):**

## Overall Summary / ## Topic Chapters / ## Key Quotes / ## Novel Ideas / ## Counter-intuitive Views / ## Core Tensions / ## Methodology / ## Key Data

**中文 (zh):**

## 总体摘要 / ## 话题章节 / ## 关键引用 / ## 新颖观点 / ## 反直觉观点 / ## 核心张力 / ## 方法论 / ## 关键数据

语气与归属说明

直接陈述内容。禁止在每条要点前添加归属语句,如“X 认为”“X 指出”“X 表示”“据 X 所述”等,无论何种语言。说话人已在前文中明确标识,重复其姓名是冗余信息。应直接陈述观点本身。

错误示例:“演讲者认为 API 业务没有护城河,因为用户毫无忠诚度。”

正确示例:“API 业务没有护城河——用户会立即转向更便宜或更好的替代方案。”

深度要求(适用于所有章节中的所有要点)

每个要点必须包含 2–3 句话以上

  1. 主张:明确陈述具体观点或事实
  2. 理由 / 证据:提供支持依据、举例或数据(如有必要可引用原文)
  3. 细微之处或影响:指出限制条件、后果或“所以呢”——读者需要理解的关键点

单句要点不可接受。若无法写出 2–3 句话来阐述某一点,说明该点过于薄弱,应直接省略,而非强行填充。

各章节内容规则

  • 总体摘要 / 总体摘要:用 3–5 句话综合全文内容,随后列出“最值得关注的几个点 / Key Highlights:”的要点列表(3–5 项)。每项需包含 2–3 句话,具体且非泛泛而谈,体现对忙碌读者最有价值的惊人发现或核心洞见。
  • 话题章节: 识别4–8个主要讨论话题。每个话题下撰写一个 ### [话题名称] 小节,包含3–5个要点,每条2–3句话。话题应反映实际对话流程(例如“OpenAI战略”、“AI泡沫评估”、“中美对比”、“投资哲学”)。涵盖所有重要话题——不要因某话题看似次要就忽略。即使是短暂或个人化的片段(如电视节目类比、提及的爱好),只要承载有意义的思想,也应单独设立小节。
  • 关键引用: 从转录文本中摘录原文引语,使用块引用格式。目标为10–15条引语;优先选择生动、具体或能体现说话人立场的原话。每条引语后附加一句上下文说明(谁说的,在何种情境下)。
  • 新颖观点: 提出新颖、少见或重新诠释常见概念的想法。每条包含:明确陈述该观点(2–3句,含理由)。不包括已主流化的内容。
  • 反直觉观点: 与普遍认知相悖的主张。格式为:普遍认知:[X] → 真实主张:[Y] —— 然后用1–2句话解释为何Y不明显,并说明支持它的依据。
  • 核心张力: 对立力量、未解决的争论或结构性矛盾。每项张力需命名双方(每方2–3句),并注明说话者是否解决该矛盾,或保持开放。
  • 方法论: 框架、决策流程、启发式方法或分步操作方式。每项描述:说明其内容、如何应用、解决什么问题。多步骤过程可使用子列表展开。
  • 关键数据: 具体数字、统计数据、指标或明确比较。需注明来源背景(谁提到、关于什么)。不得虚构数据——仅收录转录文本中明确出现的数值。

省略源内容无关的任何部分。不得编造信息。


第10步 —— 清理并生成报告

rm -rf "$TMPDIR"
原始文件: $OUTPUT_DIR/$SLUG.md
摘要文件: $OUTPUT_DIR/$SLUG-summary.md
X
@xiapuyang

已收录 1 个 Skill

相关推荐