视频链接 → 飞书妙记 + 飞书文档
前置条件:先读
../lark-shared/SKILL.md。 已经登录过 lark-cli,且 scope 至少包含:drive:drive、docs:document、minutes:minutes:readonly、minutes:minutes.artifacts:read、minutes:minutes.transcript:export。 缺 scope 时按 lark-shared 的提示让用户授权后再继续。
触发场景
仅在以下两种情况触发本 skill:
- 用户明确表达要把视频转成笔记:消息里包含视频 URL(或分享口令,如「2.33 复制打开抖音…https://v.douyin.com/xxx/」)**且**伴随明确动词——"转成笔记"、"留档"、"归档"、"整理这个视频"、"转成逐字稿/文稿"、"沉淀一下"、"帮我看里面讲了啥" 等
- 显式触发:用户使用
/video2note <url>或$video2note <url>
⚠️ 仅看到 URL 而无明确意图时不要触发。按全局 CLAUDE.md「URL 链接处理」规则先做 triage——看一眼是什么内容,问用户想做什么,再决定是否调用本 skill。
配置
skill 的归档目标飞书云盘文件夹 token 存在 config.json 里(首次使用前请运行 bash scripts/setup.sh)。
Step 0 时务必先读取:
SKILL_ROOT=~/.claude/skills/lark-video2note
[ -f "$SKILL_ROOT/config.json" ] || { echo "缺少 config.json,请先跑 bash $SKILL_ROOT/scripts/setup.sh" >&2; exit 1; }
FOLDER_TOKEN=$(python3 -c "import json;print(json.load(open('$SKILL_ROOT/config.json'))['folder_token'])")
之后所有 lark-cli drive +upload --folder-token 和 lark-cli docs +create --parent-token 都用 $FOLDER_TOKEN 而不是写死的字符串。
- 工作目录:
/tmp/video2note/<timestamp>/(每次新建,跑完保留 mp4 直到归档完成)
工具
| 平台 | 下载方式 | 逐字稿来源 |
|---|---|---|
| 抖音 | iesdouyin.com/share/video/<id>/ 解析 play_addr | 妙记 ASR |
| B站 | yt-dlp | 优先平台字幕(默认带 Chrome cookies,B站字幕需登录态)→ 妙记兜底 |
| YouTube | yt-dlp | 优先 auto-captions(默认带 Chrome cookies)→ 妙记兜底 |
| 小红书 | yt-dlp --cookies-from-browser chrome | 妙记 ASR |
两个核心脚本:
scripts/download.sh:下载视频 mp4。输入 URL + 输出目录,输出一行 JSON:
{"file":"...","title":"...","platform":"douyin","author":"...","duration_s":485,"source_url":"...","size_bytes":...}
scripts/get-captions.sh:尝试从 B站 / YouTube 抓现成字幕。成功输出:
{"transcript_file":"$WORK/transcript.txt","language":"zh-CN","lines":243}
失败 exit 1(适用任何没字幕的视频或非 B站/YT 平台)。
流程
# Step 0: 加载配置 + 准备工作目录(静默初始化,正常时不向 stdout 输出)
SKILL_ROOT=~/.claude/skills/lark-video2note
[ -f "$SKILL_ROOT/config.json" ] || {
echo "❌ 缺少 config.json,请先运行:bash $SKILL_ROOT/scripts/setup.sh" >&2; exit 1;
}
FOLDER_TOKEN=$(python3 -c "import json;print(json.load(open('$SKILL_ROOT/config.json'))['folder_token'])")
TS=$(date +%s)
WORK="/tmp/video2note/$TS"
mkdir -p "$WORK"
# 把状态写到约定路径,后续 Step 用 `WORK=$(cat /tmp/video2note_current_work)` /
# `FOLDER_TOKEN=$(cat /tmp/video2note_current_folder)` 读回。每个 Bash 工具调用
# 是独立 shell 进程,环境变量不持续——只能通过文件或 inline 重新赋值传递。
# ⚠️ 不要 `echo "WORK=$WORK"` 当回显——空载 init 应该完全安静,输出留给真有事时。
echo "$WORK" > /tmp/video2note_current_work
echo "$FOLDER_TOKEN" > /tmp/video2note_current_folder
# Step 1: 下载视频(自动识别平台)
# 关键:捕获 exit code
set +e
META=$(bash ~/.claude/skills/lark-video2note/scripts/download.sh "<URL_OR_SHARE_TEXT>" "$WORK" 2>"$WORK/.dl-err")
DL_EXIT=$?
set -e
# Exit code 2 = "这不是视频"。立即停止本 skill 流程,路由到 defuddle
if [ $DL_EXIT -eq 2 ]; then
# 把 .dl-err 里 NOT_A_VIDEO 那段原样转给用户,并主动建议改走 defuddle
# 不要继续 Step 2-4,不要上传、不要建妙记、不要建 docx
cat "$WORK/.dl-err" >&2
# 然后用 defuddle 抓那条 URL 的文本内容,按用户原意做总结/归档
exit 0
fi
# 其它非零 exit 是真失败(网络/cookies/格式),按"失败回退"小节处理
# ⚠️ 不要写 `[ $DL_EXIT -ne 0 ] && { ... }` 形式——当 DL_EXIT=0 时 test 退 1,
# 会变成整个 Bash 工具调用的退出码,触发假阳性 "Error: Exit code 1"。
# 用 if 块包起来,避免把判断结果当退出码。
if [ $DL_EXIT -ne 0 ]; then
cat "$WORK/.dl-err" >&2
exit $DL_EXIT
fi
# 解析 META:file / title / platform / author / duration_s / source_url
# Step 2: 拿逐字稿 —— 先试平台字幕,没有再走妙记
# 关键判断:抖音 / 小红书直接跳过,进 Step 2b。B站 / YouTube 先试 Step 2a
PLATFORM=$(echo "$META" | python3 -c 'import sys,json;print(json.load(sys.stdin)["platform"])')
# Step 2a: 尝试平台字幕(仅 B站 / YouTube 有意义)
TRANSCRIPT_FILE=""
MINUTE_URL=""
if [ "$PLATFORM" = "bilibili" ] || [ "$PLATFORM" = "youtube" ]; then
if CAPTIONS=$(bash ~/.claude/skills/lark-video2note/scripts/get-captions.sh "<URL>" "$WORK" 2>/dev/null); then
TRANSCRIPT_FILE=$(echo "$CAPTIONS" | python3 -c 'import sys,json;print(json.load(sys.stdin)["transcript_file"])')
# 拿到字幕 → 跳过妙记,直奔 Step 3
fi
fi
# Step 2b: 没拿到字幕 → 走完整妙记 ASR 流程(上传云盘 → 生成妙记 → 拉产物)
if [ -z "$TRANSCRIPT_FILE" ]; then
# 2b-i: 上传到飞书云盘 视频笔记 文件夹
# 注意:drive +upload 要求 --file 是相对于 CWD 的相对路径
cd "$WORK"
FILE_BASENAME=$(basename "$(echo "$META" | python3 -c 'import sys,json;print(json.load(sys.stdin)["file"])')")
TITLE=$(echo "$META" | python3 -c 'import sys,json;print(json.load(sys.stdin)["title"])')
lark-cli drive +upload \
--file "./$FILE_BASENAME" \
--folder-token $FOLDER_TOKEN \
--name "$TITLE.mp4"
# → 记录 data.file_token 为 FILE_TOKEN
# 2b-ii: 生成妙记(异步)
lark-cli minutes +upload --file-token "$FILE_TOKEN"
# → 返回 data.minute_url;从 URL 末段取 minute_token
# 2b-iii: 等妙记 ASR 转写完成,拉逐字稿
# 用 wait-for-minute.sh 轮询,不要再写固定 sleep。
# 短视频 ~30-60s 退出;长视频自适应;最长 10 分钟兜底。
#
# 就绪信号:transcript.txt 里出现「说话人」标签(ASR 真实产出的结构标记)。
# 不等 artifacts.summary——飞书 AI 总结是独立流水线,比 ASR 慢 3-10 分钟,
# 而本 skill 用不到它(自己基于逐字稿写更密的版本,见 Step 3)。
#
# ⚠️ vc +notes 会把 transcript 下载到调用时的 CWD 下(minutes/<token>/transcript.txt),
# 不是绝对路径。脚本内部已 cd 到 $WORK 之类的环境时仍要确保 CWD 正确。
cd "$WORK"
bash ~/.claude/skills/lark-video2note/scripts/wait-for-minute.sh "$MINUTE_TOKEN" > _notes.json
# → _notes.json 是 vc +notes 的完整响应
# → data.notes[0].artifacts.transcript_file 是相对 CWD 的逐字稿路径
# → AI summary 字段忽略(我们自己写更密的)
TRANSCRIPT_FILE="$WORK/minutes/$MINUTE_TOKEN/transcript.txt"
fi
# 走到这里:TRANSCRIPT_FILE 一定有了
# 字幕路径下 MINUTE_URL 为空,文档里就不写「飞书妙记」那一行
# 妙记路径下 MINUTE_URL 写到元信息 callout 里
# Step 3: 由你(Claude)基于逐字稿撰写结构化文档,并落成飞书 docx
# 重要:即使有妙记 summary 也不要直接用,那个总结太瘦
# 用下方「文档结构」模板,自己读完逐字稿写一版密度更高的版本
# 把内容写进 note.xml(CWD 必须是 $WORK)
# 3a. 短视频(XML < 40KB,对应大约 ≤ 20 分钟内容)— 一次性创建
cd "$WORK"
# ... 写入 note.xml(见下方模板)...
# ⚠️ 上传前必须先过 schema 校验。validator 抓常见 XML 错误(<text> 标签、
# snake_case 属性、<docx> 根标签等),失败时**立即停下,改正 note.xml 后重试**。
# 不要带着违规 XML 硬上传——服务器会静默吞掉/转义未知标签,文档表面"创建成功"
# 但渲染稀烂,bug 拖到用户打开飞书才发现。
bash ~/.claude/skills/lark-video2note/scripts/validate-docx-xml.sh ./note.xml || exit 1
lark-cli docs +create \
--api-version v2 \
--parent-token $FOLDER_TOKEN \
--content @./note.xml
# 注意:v2 用 --content(不是 --markdown)和 --parent-token(不是 --folder-token)
# --content 支持 @file,但 file 必须是 CWD 相对路径
# 文档默认 XML 格式,标题从 <title> 自动提取,不要传 --title
# → 返回 data.document.url
# 3b. 长视频(XML ≥ 40KB,对应大约 > 20 分钟内容)— 骨架 + 分段追加
# 长内容一次性 POST 容易超字数限制或被截。建议拆 3 段:
# ① 创建骨架:标题 + 元信息 callout + 执行摘要 + 核心论点 + 占位 heading
# ② 用 docs +update --command append 追加:完整论证链路 + Demo + 名词解释 + 金句
# ③ 再 append:逐字稿章节
# 每一段 .xml 都要先过 validate-docx-xml.sh 再 append。
# 例:
# bash ~/.claude/skills/lark-video2note/scripts/validate-docx-xml.sh ./part2.xml || exit 1
# lark-cli docs +update --api-version v2 --doc <url_or_token> \
# --mode append --content @./part2.xml
# 写入前用 wc -c note.xml 看大小,> 40KB 走 3b
# 参考:飞书 docs v2 推荐"先建骨架再 append",详见 lark-doc skill
# Step 4: 给用户回结果
# 输出 docx URL(绝对路径形式,方便 ⌘+Click)
# 如果走的是妙记路径,附带妙记 URL;走字幕路径则不提
文档结构(11 大块,固定顺序)
使用飞书 docx XML 格式(不要用 markdown,markdown 不支持 callout)。
模板和示例见 references/doc-template.xml。
⚠️ XML schema 是硬约束,不允许"发挥"。常见踩坑:
| 错的写法 | 正确写法 |
|---|---|
<docx> / <?xml...?> 当根标签 | 直接 <title>...</title> 开头,无外层根 |
<text>正文</text> | <p>正文</p> ——飞书 docx 没有 <text> 标签,写出来会被原样转义成可见字符串 |
background_color="light-blue"(下划线) | background-color="light-blue"(连字符) |
emoji_id="round_pushpin" | emoji="📍"(直接放 emoji 字符,不是名称 id) |
把所有内容塞进一个 <callout> 包起来 | 用 <h1> / <p> / <ul> / <callout> 按结构拼,11 大块各就各位 |
属性名一律 kebab-case;段落标签只能用 <p>;emoji 必须是字符不是名称。不在 references/doc-template.xml 出现的标签或属性都视为非法。生成完 note.xml 后 scripts/validate-docx-xml.sh 会做一次硬性校验,发现违规直接 fail,必须先改对再上传。
1. <title>{视频标题}</title>
2. 📍 来源信息 callout(light-blue)— 平台/作者/时长/原视频/妙记/归档日期
3. ✨ 执行摘要 callout(light-yellow)— 一段话讲清作者真正想说什么,3-5 行
4. 🎯 核心论点 — 一句话立场
5. 🧭 完整论证链路 — 按视频推进逻辑切 4-6 个 h2 小节,每节用「叙述段落 + 必要时穿插 bullet」的方式写
6. 🎬 Demo 实操(如视频里有 demo)— 几段叙述讲清楚步骤
7. 📚 名词解释 — 只解释 2-4 个真正生僻的(如 Ontology、FDE、AIP),用大白话 + 类比
8. 💬 金句原文 callo