关于 broll
broll 是面向视频创作者的剪辑技能,用于建立并检索带标签的 B-roll 素材库。它帮助创作者为短视频找到合适镜头,无需逐个打开视频。
主要功能
- 01整理片段元数据与标签
- 02生成联系表以供查看
- 03增量添加并标记新片段
- 04按场景、情绪和主体检索
- 05记录已用与拒用的素材
适用场景
- 01为短视频查找夜间桌面镜头
- 02将新文件夹加入素材库
- 03按转录文本选择匹配镜头
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ melvynx/ video-agent-skills --skill broll安装后在 Claude Code 中输入/broll确认已加载。
详细介绍
B-roll
B-roll = 覆盖在创作者语音上的画面:他们自己的素材(街道、夜晚的书桌、日落、健身房、手持手机),屏幕录制、照片。素材库只读取一次,之后每次剪辑都能用一次 grep 找到“我夜晚在书桌前”的画面,无需打开任何视频。
<their clips folder>/ the files, never moved or edited
video-agent/broll/
├── library.json one entry per clip: file, length, orientation, date, GPS, tags, used in
├── library.md the same, one line per clip, newest first: what agents grep
├── sheets/<id>.jpg 6 frames of the clip in a 3x2 grid (what the taggers and you look at)
└── tags/batch-NN.json what each Haiku tagger wrote
用创作者的语言与他们交流,消息简短,不用行话(说“你的 B-roll 素材库”,绝不说“JSON”)。所有命令都从项目文件夹(也就是包含 video-agent/ 的文件夹)运行。预检:ffmpeg -version && node -v;缺少工具时:/studio-setup tools。
/broll setup
- 文件夹。 用一句话询问他们的素材放在哪里。他们自己的文件夹保持原位(外接硬盘也可以)。素材分散在各处:
mkdir -p video-agent/broll/clips并把素材复制进去。 - 他们的照片(用于识别哪些素材里有他们):一张光线充足的脸部照片,保存为
video-agent/broll/me.jpg,再加 1 到 3 张,命名为me-2.jpg、me-3.jpg(一张特写、戴另一顶帽子或眼镜的照片,以及一帧他们肯定在其中的素材画面):只用一张照片,Haiku 会漏掉他们的大多数素材。没有照片:is_self保持为 false。 - 扫描(每个素材约 1 秒)。只读取新文件,因此之后可以用同一条命令添加素材:
node -e '
const fs=require("fs"),path=require("path"),{execFileSync:x}=require("child_process");
const B="video-agent/broll",L=B+"/library.json",ext=/\.(mp4|mov|m4v|mkv|webm|jpe?g|png)$/i;
fs.mkdirSync(B+"/sheets",{recursive:true});
const lib=fs.existsSync(L)?JSON.parse(fs.readFileSync(L,"utf8")):[],known=new Set(lib.map(c=>c.file));
const walk=d=>fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>e.name.startsWith(".")?[]:e.isDirectory()?walk(path.join(d,e.name)):ext.test(e.name)?[path.resolve(d,e.name)]:[]);
let n=0;
for(const f of process.argv.slice(1).flatMap(a=>fs.statSync(a).isDirectory()?walk(a):[path.resolve(a)])){
if(known.has(f))continue;
try{
const p=JSON.parse(x("ffprobe",["-v","error","-print_format","json","-show_format","-show_streams",f]).toString());
const v=p.streams.find(s=>s.codec_type==="video")||{},t=p.format.tags||{},photo=/\.(jpe?g|png)$/i.test(f);
const turn=Math.abs(+(v.tags?.rotate||(v.side_data_list||[]).find(s=>s.rotation!=null)?.rotation||0))%180===90;
const w=turn?v.height:v.width,h=turn?v.width:v.height,dur=photo?0:+p.format.duration||0;
let id=path.basename(f).replace(/\.[^.]+$/,"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")||"clip";
while(lib.some(c=>c.id===id))id+="-2";
const sheet=B+"/sheets/"+id+".jpg";
x("ffmpeg",["-v","error","-y","-i",f,"-vf",photo?"scale=720:-2":"fps=6/"+Math.max(dur,1)+",scale=360:-2,tile=3x2","-frames:v","1",sheet]);
const g=(t["com.apple.quicktime.location.ISO6709"]||t.location||"").match(/([+-][\d.]+)([+-][\d.]+)/);
lib.push({id,file:f,sheet,duration:Math.round(dur*10)/10,width:w,height:h,orientation:w<h?"portrait":w>h?"landscape":"square",
date:t["com.apple.quicktime.creationdate"]||t.creation_time||null,lat:g?+g[1]:null,lon:g?+g[2]:null,place:null,tags:null,used:[],status:"active"});
if(++n%20===0)fs.writeFileSync(L,JSON.stringify(lib,null,1));
}catch(e){console.log("skipped "+f+": "+e.message.split("\n")[0])}
}
fs.writeFileSync(L,JSON.stringify(lib,null,1));
console.log(n+" new, "+lib.length+" in the library, "+lib.filter(c=>!c.tags&&c.status==="active").length+" to tag");
' "<their clips folder>"
带 GPS 的素材:查询地点名称,每个不同地点请求一次(经纬度四舍五入到 2 位小数),每秒一个请求,然后在 library.json 中设置 place(“里斯本,葡萄牙”):curl -s "https://nominatim.openstreetmap.org/reverse?format=json&zoom=10&lat=<lat>&lon=<lon>" -H "User-Agent: video-agent-skills"。没有 GPS = 没有地点,绝不猜测。
4. 用 Haiku 添加标签(便宜又快速)。每批处理 5 个未添加标签的素材;每批用一个子 Agent,全部在同一条消息中启动,让它们并行运行,每次处理 10 批(Claude Code:使用 model: "haiku" 的 Agent 工具)。此工具没有子 Agent 时:按照相同规则自行给各批添加标签。每个标注器的提示词,填充内容后如下:
You tag B-roll clips for a video creator's library. Read each contact sheet below (6 frames of one clip, left to right then top to bottom, spread evenly over the clip) and video-agent/broll/me.jpg, me-2.jpg, me-3.jpg (the creator; skip the missing ones). Then Write ONE file, video-agent/broll/tags/batch-<NN>.json, then answer "done". Nothing else.
Clips: <id> (<duration> s) video-agent/broll/sheets/<id>.jpg ; ... (5 of them)
The file is one JSON object keyed by clip id, every id present, this shape:
{"<id>": {"title": "4 to 8 words", "description": "3 plain sentences: what is on screen, what moves, the light",
"setting": "desk|bedroom|kitchen|street|beach|car|gym|cafe|office|city|nature|screen|studio|...",
"subjects": [], "actions": [], "emotions": [{"name": "serenity", "intensity": 0.6}],
"mood": "calm|joyful|melancholic|tense|energetic|contemplative|neutral",
"time_of_day": "dawn|day|golden hour|sunset|evening|night|unknown",
"shot_type": "close-up|medium|wide|screen|aerial", "keywords": ["8 to 15 words someone would search"],
"has_people": true, "is_self": false, "quality_flags": ["blur|shaky|dark|overexposed|watermark"], "best_start": 0}}
Rules: only what is visible, never a place, brand or name you cannot read on screen. Everything in English. emotions 0 to 2, only when the image clearly carries one. is_self: compare every face with the creator photos (face shape, nose, eyebrows, hair, glasses); same person = true, even with another cap, glasses or clothes, in a mirror selfie or small in the frame; an arm's-length selfie is usually them, look twice; hands or back only = false. best_start = the second where the clean part starts (frame k of 6 is at about k/6 of the length), 0 when clean from the start. Valid JSON, no comment.
然后合并所有批次并重建 library.md(手动修改 library.json 后也要重建):
node -e '
const fs=require("fs"),B="video-agent/broll",L=B+"/library.json",lib=JSON.parse(fs.readFileSync(L,"utf8"));
fs.mkdirSync(B+"/tags",{recursive:true});
const T=Object.assign({},...fs.readdirSync(B+"/tags").filter(f=>f.endsWith(".json")).map(f=>JSON.parse(fs.readFileSync(B+"/tags/"+f,"utf8"))));
for(const c of lib)if(T[c.id])c.tags={...(c.tags||{}),...T[c.id]};
fs.writeFileSync(L,JSON.stringify(lib,null,1));
const k=c=>c.tags||{},row=c=>[c.id,c.duration+"s",c.orientation,(c.date||"").slice(0,10),c.place||"",k(c).title||"(untagged)",k(c).mood||"",k(c).time_of_day||"",
[...(k(c).keywords||[]),...(k(c).emotions||[]).map(e=>e.name),k(c).setting||""].join(" "),k(c).is_self?"me":"",(k(c).quality_flags||[]).join(" "),c.used.length?"used "+c.used.length:""].join(" | ");
const on=lib.filter(c=>c.status==="active").sort((a,b)=>(b.date||"").localeCompare(a.date||""));
fs.writeFileSync(B+"/library.md","# B-roll library\n\nid | length | orientation | date | place | title | mood | time | keywords | me | flags | used\n"+on.map(row).join("\n")+"\n");
console.log(on.length+" clips, "+on.filter(c=>!c.tags).length+" untagged, "+on.filter(c=>k(c).is_self).length+" with the creator");
'
批次文件不是有效的 JSON 或缺少 id:重新运行对应的标注器。素材库很大(300 个以上素材)时:现在先标注 100 个,说明这一点,其余的放到后台处理,或者等到首次需要视频 B-roll 时再处理。
5. 与创作者一起检查:根据他们的真实主题(见下文)搜索 3 次,阅读命中素材的说明,并展示找到的内容(“搜索‘夜晚的办公室’,我找到了这 3 段画面”)。标签错误:在 library.json 中修正并重建。创作者绝不想用的素材:拒绝它。
6. 用一句话告诉他们:“你的素材库已准备就绪:N 个素材。说‘添加这些视频’来添加更多素材,或在短视频上说‘用我的 B-roll’。”
/broll add
他们的文件或一个文件夹:将文件复制到他们的素材文件夹(或直接在原处扫描),然后对新素材执行步骤 3 和 4(扫描会跳过已知文件,标注器只会收到 tags: null)。说明新增了什么:“新增 12 个素材:4 个是 8 月在里斯本拍的,8 个在办公室”。
/broll search
grep -i -E "sunset|dusk|golden" video-agent/broll/library.md
grep -i "desk" video-agent/broll/library.md | grep -i "night" | grep "| me |"
用英文写出画面中应出现的内容;同义词用 | 连接,第二个筛选条件再用一次 grep。自行给命中结果排序:与描述相符的优先,其次是未使用过的,再其次是最近的,最后是方向合适的(短视频用竖屏;横屏素材会裁剪以填满画面)。然后阅读最好的 3 个素材说明:标签能帮你找到素材,只有你亲自看过才能确认。
/broll use
正在制作的短片(video-agent/edits/<slug>/ 或指定名称):根据短片的文字为其添加 B-roll。
- 阅读文字稿(短片的
files/words.md,时间以源视频秒数为准)。标记能表现某个时刻或地点(“我搬到了 Lisbon”)、某种感受(“我筋疲力尽”)、创作者正在做某事(“我晚上写代码”)或他们使用的物品的句子。数字、观点和证据仍用画板呈现。 - 每行搜索一次;阅读素材表。只有在画面与句子相符(平静的句子搭配平静的画面)、从
best_start开始的清晰片段时长足以覆盖整个段落,且画面没有问题(模糊、陌生人的脸、私人屏幕)时,才选用片段。每 8 到 15 秒最多添加一个 B-roll,每段 2 到 5 秒,两段 B-roll 之间要有 5 秒的创作者画面。 - 在规范文件的
media(source/spec.js)中为每个选中的片段命名:<id>: { src: '<file>', from: <best_start>, dur: <s>, fill: true }(fill会将横屏片段裁剪为 9:16,focus: [x, y]可保留画面中的某个部分)。 - 在同一规范文件的
broll中添加这些片段:{ media: '<id>', from: <source s of the first word>, to: <source s of the last word> }(参见 short-editing 的references/scene-library.md中的“全屏 B-roll”),然后再次运行prepare(它会提取帧)。 - 在每段画面的中间运行
short.mjs stills,阅读提取的帧,然后完整渲染视频(参见 short-editing 步骤 5 和 6)。 - 创作者会保留这段短片:将短片的 slug 添加到
library.json中每个片段的used,并重新生成library.md,这样后续短片就会优先使用新片段。
找不到合适的片段 = 素材库中没有能匹配的画面:保留创作者或画板画面,绝不要随便放一个片段。
/broll reject
在 library.json 中将该片段的 "status" 设为 "rejected"(设为 "active" 可恢复),然后重新生成 library.md:以后不会再出现它。文件本身不会被删除,因为这是创作者的素材。