视频变学习笔记 — Video to Lecture Notes
你现在运行 video-to-notes 技能。目标:用户给你一条视频链接(YouTube / Bilibili / 任意 yt-dlp 支持的源),你产出四件套学习笔记包:
lecture-notes.md— 时间戳锚点 + 嵌入关键帧 + 章节摘要 + 术语表,Obsidian/Notion 直接拖进去就能复习lecture-notes.pdf— A4 打印本,分页正确,含关键帧key-terms.json— 字段化术语表(term/termZh/definition/firstSeenSec/jumpUrl),可二次加工outline-board.html— 自包含可交互大纲,点章节直接 seek 视频到对应秒
默认零 GPU、零本地模型权重、零商业 API key。 唯一需要的凭据是在 https://clawvard.school 注册的一个 Clawvard API key,作为 Clawvard SDK 的 bearer credential。
前置条件
-
Node ≥ 18(
node -v检查) -
系统
ffmpeg:- macOS:
brew install ffmpeg - Ubuntu / Debian:
sudo apt install ffmpeg - Windows:
winget install Gyan.FFmpeg
- macOS:
-
yt-dlp:- macOS:
brew install yt-dlp - Ubuntu / Debian:
pipx install yt-dlp(或sudo apt install yt-dlp但版本通常滞后;首选 pipx) - Windows:
winget install yt-dlp.yt-dlp
- macOS:
-
Clawvard SDK(一句命令安装):
pnpm add @clawvard/sdk # 或 npm install @clawvard/sdk / yarn add @clawvard/sdk -
凭据:在 https://clawvard.school 注册账号 → mint 一个
sk-xxxClawvard API key,导出到环境变量:export CLAW_API_KEY=sk-...
默认主路径(5 步)
1. 拿字幕
按视频来源选两条小路径之一:
A. 源站本身就发布了 .srt / .vtt(archive.org / MIT OCW / TED transcripts / 任何把字幕文件挂在 CDN 上的源)——一条 curl 拿走,最稳:
curl -L -o lecture.srt '<SUBTITLE_URL>'
# 例:curl -L -o lecture.srt \
# https://archive.org/download/MIT18.06S05_MP4/01.srt
B. 源是 YouTube / Bilibili / 任意 yt-dlp 支持的网站——让 yt-dlp 拿自动字幕:
yt-dlp \
--write-auto-subs \
--sub-format vtt \
--skip-download \
--output 'lecture' \
'<VIDEO_URL>'
# -> lecture.en.vtt (或 lecture.zh-Hans.vtt / lecture.zh.vtt)
只有当 A 和 B 都拿不到字幕时,才跳到本文末的 opt-in 旁路。绝大多数公开教学视频、Conference Talk、3Blue1Brown、Karpathy 的长讲都至少落在 A 或 B 上。
2. 抽关键帧
下载视频(如果还没有本地副本);本课程包里随附了一个小脚本 extract-keyframes.mjs,把视频喂给它就能挑出代表性的板书 / 幻灯片 / 讲者切换帧,并直接产出第 4 步 SDK 要的 keyframes.json:
yt-dlp -f 'bv*+ba/b' --merge-output-format mp4 \
--output 'lecture.mp4' '<VIDEO_URL>'
# 或者源已有公开直链 mp4:
# curl -L -o lecture.mp4 '<VIDEO_FILE_URL>'
node extract-keyframes.mjs lecture.mp4 frames --max 8
# 写出 frames/*.png + frames/keyframes.json
frames/keyframes.json 是 { tSec, url }[] 形式,可以原样当作第 4 步 cv.text.lectureNotes 的 keyframes 入参。--max 8 是把全片抽到的代表性帧匀稀到 8 张;想给一节小时长讲座留更多上下文就开到 12,想做极简卡片就缩到 4。脚本本身没有第三方依赖,只需要系统 ffmpeg。
3. 解析字幕 → segments
把第 1 步拿到的 .vtt 或 .srt 解析为 { i, start, end, text }[]。这两种格式区别只是时间分隔符(. 还是 ,),一个解析器同时吃两种 ≤ 30 行 Node:
function parseSubs(raw: string) {
const blocks = raw.replace(/\r/g, "").split(/\n\n+/);
// VTT uses 00:00:00.000 ; SRT uses 00:00:00,000 — accept both.
const cueRe = /^(\d\d:\d\d:\d\d[.,]\d{1,3})\s+-->\s+(\d\d:\d\d:\d\d[.,]\d{1,3})/;
const toSec = (t: string) => {
const [hms, ms = "0"] = t.split(/[.,]/);
const [h, m, s] = hms.split(":");
return Number(h) * 3600 + Number(m) * 60 + Number(s) + Number(ms) / 1000;
};
const segments: { i: number; start: number; end: number; text: string }[] = [];
let i = 1;
for (const b of blocks) {
const lines = b.split("\n");
const cue = lines.find(l => cueRe.test(l));
if (!cue) continue;
const m = cue.match(cueRe)!;
const text = lines
.slice(lines.indexOf(cue) + 1)
.join(" ")
.replace(/<[^>]+>/g, "")
.trim();
if (!text) continue;
segments.push({ i: i++, start: toSec(m[1]), end: toSec(m[2]), text });
}
return segments;
}
(也可以 pnpm add subtitle 用现成解析器;上面的小函数适合避免新增依赖。)
4. 调 SDK — cv.text.lectureNotes
import {
Clawvard,
renderLectureNotesMarkdown,
TranscriptTooLongError,
EmptyTranscriptError,
} from "@clawvard/sdk";
import { writeFileSync } from "node:fs";
const cv = new Clawvard({ apiKey: process.env.CLAW_API_KEY! });
try {
const notes = await cv.text.lectureNotes({
segments,
keyframes,
videoTitle: "<source title>",
videoUrl: "<source URL>",
language: "en", // | "zh" | "bilingual"
noteLanguage: "en", // 输出注释 / glossary 的语言
// chapterTargetCount: 8, // 可选;省略时按视频长度自动决定
});
// ↓ 接 5 步落盘
} catch (err) {
if (err instanceof TranscriptTooLongError) {
console.error("视频太长,超出单 call 上限。按章节切两段再分别 call.");
} else if (err instanceof EmptyTranscriptError) {
console.error("字幕为空。检查 yt-dlp 是否真的拉到 --write-auto-subs 的文件。");
} else {
throw err;
}
}
SDK 服务端在 cv.text.lectureNotes 内做了多 pass:把 transcript + keyframeHints 拼成单次 LLM 调用 → 解析 JSON → 章节边界 snap 到 segment 索引 → glossary 按首次出现秒去重排序 → 关键帧按区间归到对应章节。结果里所有 [mm:ss] 锚点、&t=NNNs 跳转链接、章节区间都是服务端基于真实 segment 时间算的,不依赖 LLM 自报时间。
5. 落盘四件套
import { writeFileSync } from "node:fs";
import { execSync } from "node:child_process";
// 4-1. lecture-notes.md
writeFileSync("lecture-notes.md", renderLectureNotesMarkdown(notes, keyframes));
// 4-2. key-terms.json
writeFileSync("key-terms.json", JSON.stringify(notes.keyTerms, null, 2));
// 4-3. lecture-notes.pdf
// pandoc 路径(推荐,xelatex 排版漂亮)
execSync(
"pandoc lecture-notes.md -o lecture-notes.pdf " +
"--pdf-engine=xelatex -V mainfont='DejaVu Sans' -V geometry:margin=2cm"
);
// 纯 Node 路径(无 LaTeX 时):`pnpm add md-to-pdf` 然后
// const { mdToPdf } = await import("md-to-pdf");
// await mdToPdf({ path: "lecture-notes.md" }, { dest: "lecture-notes.pdf" });
// 4-4. outline-board.html (自包含;模板见本仓 examples/video-to-notes/outline-board.template.html)
writeFileSync("outline-board.html", renderOutlineBoardHtml(notes, keyframes));
outline-board.html 的核心交互:左侧列出每条 [mm:ss] 章节标题,点击 → 用 YouTube iframe API postMessage({event:"command",func:"seekTo",args:[seconds,true]}) 把右侧嵌入播放器 seek 到对应秒(Bilibili 用 ?t=NNNs URL 重定向也行)。
opt-in 旁路(无字幕源)
对于真的没字幕的源(很少见),把 1 步换成 whisper.cpp:
git clone https://github.com/ggerganov/whisper.cpp && cd whisper.cpp
make -j base.en # ~150 MB base.en 模型,约 2-3 分钟 CPU 解码 30 分钟视频
./main -m models/ggml-base.en.bin -f ../lecture.mp4 -ovtt -of ../lecture
这是 opt-in,不进默认主路径;不要默认要求用户装本地 ASR。
用户可见铁律
- 课程能力一律走 Clawvard SDK 的 typed method
cv.text.lectureNotes,Clawvard API key 作为 SDK 凭据。除官方域名https://clawvard.school之外,不要把课程能力指向任何其他后端。 - 链接和示例文案只用
https://clawvard.school。不要在用户文档里出现clawvard.com/clawvard.co这类拼写。 - ffmpeg / yt-dlp 仅作纯 CLI 调用;交付物只有
.md/.pdf/.json/.html四件套,不打包 ffmpeg 二进制。 - 长视频(> 2 小时 / > 6000 字幕段 / > 100K 字符)会触发
TranscriptTooLongError。按章节切两段再分别 call、最后把 sections + keyTerms 拼起来即可。 - 禁止在
.md/.pdf/.html里写「这是 mock」「placeholder」「示例数据」字样,也不要在真跑前就提交假产物。
学习完成后
告诉用户:
我已经学会了 video-to-notes。给我一条公开视频链接(YouTube / Bilibili / 任意 yt-dlp 支持的源),我会本地用 yt-dlp 抓字幕 + ffmpeg 抽关键帧 + Clawvard SDK 的
cv.text.lectureNotes多 pass 结构化,产出四件套 lecture-notes.md + lecture-notes.pdf + key-terms.json + outline-board.html。默认零 GPU、零本地模型权重、零商业 key——只用 Clawvard API key。