CommunityVidéo et animationgithub.com

eternova/long-video-course-notes

Claude Code skill: 把长视频课程自动化生成图文飞书文档笔记。video→canvas 抓帧 + AI 识别 + lark-cli 推送。

Compatible avecClaude Code~Codex CLICursor
npx add-skill eternova/long-video-course-notes

name: long-video-course-notes description: 把 1-3 小时的长视频课程(直播回放、网课、Bilibili 讲座等)自动化生成图文并茂的飞书文档笔记。流程是:登录 → 提取 video 元素 → 用 video→canvas 抓取关键时间点的帧(绕过 HLS DRM)→ AI 看帧识别 PPT vs 讲师人头 → 在 PPT 密集段补抓 → 写带时间戳的 markdown 文章 → 推到飞书文档(自动建文件夹+插图)。当用户说「把这个视频整理成笔记」「长视频图文笔记」「视频课图文摘要发飞书」时使用。

长视频课程图文笔记 skill

何时用

  • 用户给了一个长视频 URL(>30 分钟),希望生成图文摘要而不是只看一遍
  • 视频是直播回放 / 网课 / 讲座这种主体是 PPT + 讲师讲解的内容(非娱乐短片)
  • 输出目标是 飞书文档(默认)或本地 markdown
  • 视频平台支持任何用 <video> HTML5 元素的网站(小鹅通、生财有术、Bilibili、B站、网易云课堂、抖音教育等)

何时不用

  • 视频用 Flash / Silverlight / 私有播放器(无 HTML5 video 元素)
  • 视频 < 15 分钟(直接看完更快)
  • 视频内容是纯娱乐(音乐、Vlog、动画)—— 抓帧没价值
  • 用户希望保留口播文字稿 —— 这个 skill 不做 ASR,需另用 whisper 或飞书妙记

核心原理(为什么这套行)

视频流通常是 HLS / MPEG-DASH,加上 token 鉴权——直接 ffmpeg 下载会 403。但浏览器内的 video 元素已经把流解密为帧缓冲,用 canvas.drawImage(video, ...) 可以拿到任意时间点的 PNG,绕开整个版权链。

video 元素 (DRM-free 解密后) → canvas.drawImage → toDataURL → 本地 jpg

流程总览

Step 1: 评估视频可访问性(登录态 / 是否 HTML5 video)
Step 2: 探针——拿到 duration / resolution / src
Step 3: 粗采样(每 8-10 分钟一帧)
Step 4: AI 看帧分类(PPT vs 人头 vs 编辑器 vs Q&A)
Step 5: 在 PPT 密集区补采样
Step 6: 写 markdown 文章
Step 7: 推到飞书(建文件夹 + 创建文档 + 逐张插图)

Step 1:登录 / 可访问性检查

用 Playwright MCP 浏览器打开视频 URL:

mcp__playwright__browser_navigate(url=视频URL)

检查

  • 如果跳转到登录页 → 看下面「登录策略」
  • 如果直接看到课程页 → 跳到 Step 2

登录策略(按优先级)

A. 平台已登录(最常见,免操作) — Playwright MCP 浏览器在同一会话内会保留 cookie,如果用户最近用 Playwright 访问过同一域名并登录,cookie 还在。直接试导航。

B. 用户在自己 Chrome 里登录 → cookie 导入 — 用 ~/.claude/skills/gstack/browsecookie-import-browser

cd ~/.claude/skills/gstack/browse
bun run src/cli.ts goto "https://target.com/"  # 必须先访问目标域
bun run src/cli.ts cookie-import-browser chrome --domain target.com

⚠️ 失败常见原因:用户 Chrome 里 cookie 已过期(看 expires_utc 字段)。

C. 无人值守登录不可行 → 让用户做 — 验证码 / 滑块 / 微信扫码这些永远做不到自动化,老老实实让用户登录:

  • 给清晰的 1 句话指引:「在 Chrome 里打开 X,登录,然后告诉我」
  • 不要发 2 段话以上,不要给 4 个选项

绝对不要陷入:试错登录→cookie 过期→换工具→工具又挂→再换工具的死循环。最多尝试 2 次后直接降级到 Plan B(用户自己截图发我)。

Step 2:视频元素探针

打开视频页后,运行:

() => {
  const v = document.querySelector('video');
  if (!v) return {error: 'no video element'};
  return {
    src: v.src || v.currentSrc,
    duration: v.duration,
    width: v.videoWidth,
    height: v.videoHeight,
    paused: v.paused,
    readyState: v.readyState,
    title: document.title,
    url: location.href
  };
}

判断

  • duration > 0readyState >= 3 → 可以抓帧
  • srcblob:... URL → 用的 MSE,正是我们要的
  • videoWidth >= 720 → 帧质量够
  • readyState < 3 → 视频未加载,可能需要点播放按钮先触发加载(跳过 muted 自动播放更稳)

Step 3:粗采样关键时间点

对一个 N 分钟的视频,先采样 ceil(N/8) 帧(每 8 分钟一帧),覆盖整体节奏:

async () => {
  const v = document.querySelector('video');
  v.pause(); v.muted = true;
  
  const duration = v.duration;
  const interval = 480; // 8 分钟
  const targets = [];
  for (let t = 30; t < duration - 30; t += interval) targets.push(Math.floor(t));
  
  const seekTo = (t) => new Promise((resolve) => {
    const onSeeked = () => { v.removeEventListener('seeked', onSeeked); resolve(); };
    v.addEventListener('seeked', onSeeked);
    v.currentTime = t;
    setTimeout(() => { v.removeEventListener('seeked', onSeeked); resolve(); }, 8000);
  });
  
  const canvas = document.createElement('canvas');
  canvas.width = v.videoWidth;
  canvas.height = v.videoHeight;
  const ctx = canvas.getContext('2d');
  
  window.__frames = window.__frames || {};
  const out = [];
  for (const t of targets) {
    await seekTo(t);
    v.pause();
    await new Promise(r => setTimeout(r, 800)); // 等帧缓冲
    ctx.drawImage(v, 0, 0, canvas.width, canvas.height);
    window.__frames[t] = canvas.toDataURL('image/jpeg', 0.8);
    out.push({t: v.currentTime, target: t, kb: (window.__frames[t].length / 1024) | 0});
  }
  return out;
}

关键细节

  • v.muted = true:避免 autoplay 检测拦截
  • v.pause() 在每次 seek 后再调一次(有些播放器在 seek 后会自动 resume)
  • 800ms 等待:seeked 事件触发不代表帧已渲染,要给 GPU buffer 一点时间,否则会拍到上一个时间点的帧
  • 都 stash 在 window.__frames,避免一次返回 30MB 巨型 base64

Step 4:取出 base64 写盘

每次 evaluate 调用最多返回 ~5MB JSON。分批:

() => {
  const keys = Object.keys(window.__frames).sort((a,b) => +a - +b);
  const out = {};
  for (const k of keys.slice(START, START+4)) {
    out[k] = window.__frames[k].split(',')[1]; // 去掉 data:image/jpeg;base64,
  }
  return out;
}

mcp__playwright__browser_evaluatefilename 参数把 JSON 写到 ~/.playwright-mcp/~/不能直接写 /tmp(沙箱限制)。

落盘脚本:

import json, base64, os
os.makedirs(os.path.expanduser("~/course-frames"), exist_ok=True)
for batch in glob("~/course-frames/batch*.json"):
    d = json.load(open(batch))
    for t, b64 in d.items():
        secs = int(t)
        fn = f"frame-{secs:04d}s-{secs//60:03d}m{secs%60:02d}s.jpg"
        open(f"~/course-frames/{fn}", "wb").write(base64.b64decode(b64))

Step 5:AI 看帧分类 + 补采样

Read 工具逐帧查看(图片会自动显示给你)。给每帧打标:

  • slide —— 有 PPT 文字、可读 → 必抓
  • editor —— Cursor / VS Code / 终端 → (演示场景有用)
  • talking_head —— 只是讲师人脸 → 跳过
  • qa / chat —— 弹幕互动 → 选 1-2 张作 Q&A 代表

密集补采样:如果连续 2 帧之间跨度 8 分钟但内容完全不同(如从 Principle 01 跳到 Principle 05),说明这段 PPT 切换密集,回头补采样:

// 在 [start, end] 范围按 1-2 分钟间隔补采
const targets = [];
for (let t = START; t <= END; t += 90) targets.push(t);

Step 6:写 markdown 文章

文章结构(根据课程类型微调):

# 课程标题 · 完整复盘笔记

> 课程信息:讲师 / 日期 / 时长 / 平台 / 原视频链接

## 这篇文章给谁看
(1 段,直接说明读者画像 + 学到什么)

## 课程整体结构

00:00 - 08:00 开场 08:00 - 25:00 Part 1 ... ...


## Part 1 · ...
> ⏱ 时间码:**16:00 – 25:00**

### 子章节 1
> ⏱ **16:00** · 课件 9/38
![placeholder-frame-XXXXs.jpg](placeholder-frame-XXXXs.jpg)
(关键观点 + 钉子句 + 检查清单)

...

## 给小白的可执行下一步
(按周拆 Action Items)

## 附录 · 关键资源链接

写作规则

  • 每个 image placeholder 用唯一文件名 placeholder-frame-{seconds}s.jpg,便于后面 selection-with-ellipsis 定位
  • 每个 PPT 必标精确时间码(hh:mm 或 mm:ss)+ 课件页码
  • 课件 PPT 内容直接抄到表格里,不要总结(用户要看原文)
  • 反例/正例 / 检查清单这些结构化内容用 markdown 表格或列表
  • 文末必有「给小白的可执行下一步」按周拆,否则文章只是 read-only

Step 7:推飞书

7.1 检查 lark-cli 授权状态

lark-cli auth status

第一次跑这个 skill 需要授权这些 scope(按需,逐个加):

  • space:folder:create — 建文件夹
  • docx:document:create — 创建文档
  • docx:document:write_only + docx:document:readonly — 修改文档(媒体插入需要)
  • docs:document.media:upload — 上传图片到文档

授权命令:

nohup lark-cli auth login --scope "<scope>" > /tmp/lark-auth.log 2>&1 &
sleep 5
cat /tmp/lark-auth.log  # 拿到 device-flow URL
open "<URL>"  # 让用户在浏览器授权
# 等用户完成
until grep -q "授权成功" /tmp/lark-auth.log; do sleep 3; done

⚠️ scope 必须一次只加一个——多 scope 一起请求会报 invalid scope。

7.2 建文件夹(默认在用户个人空间根目录)

lark-cli drive +create-folder --name "<文件夹名>"
# 返回 folder_token,记下来

7.3 准备干净版 markdown(无图片占位)

sed '/!\[.*\](placeholder-frame/d' article.md > article-clean.md

7.4 创建文档

cd ~/course-frames/article  # markdown 路径必须 cd 进去(CLI 限制)
lark-cli docs +create \
  --markdown @article-clean.md \
  --folder-token "<folder_token>" \
  --title "<文档标题>" \
  > /tmp/doc-create.json 2>&1
# 提取 doc_id

7.5 逐张插入图片

每张图按章节标题或独特文本作 anchor:

lark-cli docs +media-insert \
  --doc "<doc_id>" \
  --file "frame-XXXXs-MMMmSSs.jpg" \
  --type image \
  --selection-with-ellipsis "<unique heading text>" \
  --align center \
  --caption "<时间码 · 描述>"

Anchor 规则

  • 用章节标题前 15 个字(够 unique 即可)
  • 同一章节多张图 → 用不同段落的独特短语
  • 失败的话用 start...end 格式:--selection-with-ellipsis "首段...末段"

7.6 失败处理

  • 服务器超时 → 重试 1 次。超过 16k 字符的文档创建容易超时,文章拆成两段也行
  • "必须在 root folder 创建" 错误 → folder-token 拼错或没传
  • 图片占位仍存在 → markdown 里 placeholder-frame-XXX 残留,先用 --mode overwrite 重写文档干净版再插

反模式(避免踩坑)

❌ 反模式 1:试图 ffmpeg 直接下载视频

HLS + token 几乎一定会 403。即使能下,也要花 20 分钟下载完整视频,再花 5 分钟 ffmpeg 抽帧。video → canvas 用 5 秒抓 16 帧

❌ 反模式 2:让 Claude 看完整视频

你没有那个能力。即使能(多模态视频 API),成本也是抓帧的 50 倍。抓帧 + 看图 + 时间戳定位才是杠杆。

❌ 反模式 3:seek 没等够时间就 drawImage

seeked 事件触发 ≠ 帧渲染完成。GPU buffer 有 200-500ms 延迟。至少 800ms,1 秒更稳。表现是「前两帧 OK,第三帧开始全是同一张图」。

❌ 反模式 4:scope 一把抓

lark-cli auth login --scope "scope1 scope2 scope3" 报 invalid scope。一次一个

❌ 反模式 5:试图用 gstack browse 同时跑视频和飞书操作

gstack browse daemon 在 bash session 切换时不稳定。Playwright MCP 抓视频 + lark-cli 操作飞书 最干净。

❌ 反模式 6:cookie 过期还在硬试

检查 expires_utc < 当前时间 → 直接告诉用户「重新登录」,不要换 5 种方案试。

常见用户问题预案

「为什么不直接给我视频字幕?」 答:可以,但需要单独跑 whisper(额外 5-10 分钟)。如果用户明确要文字稿就跑;否则图文笔记是默认。

「为什么不抓更多帧?」 答:每帧 ~200KB,飞书文档插入有体积限制 + 视觉疲劳。12-18 张是甜点,覆盖一节 2 小时课。

「能不能不要飞书?」 答:默认输出 ~/course-frames/article/article.md + 同目录 *.jpg,用户可手动发任何地方。

输出规范

每次成功跑完,最终响应必须包括:

  1. 飞书文档 URL(直接可点击)
  2. 抓帧总数 + 总时长
  3. 章节小目录(让用户预览)
  4. 一句**「下一步建议」**(如「评论里告诉我哪段要扩写」)

配套脚本

scripts/capture-frames.js —— Playwright evaluate 用的探针 + 抓帧函数(粘到 mcp__playwright__browser_evaluate 里)

scripts/decode-batches.py —— 落盘批量 base64 → jpg

scripts/insert-images.sh —— 飞书 lark-cli 批量插图模板

参考一个完整跑通的 case:examples/deepsea-2026-04-23.md

Skills associés