mcpField
B
broll

broll

github.com/melvynx创意与媒体64366 次安装1 周前更新
代码仓库

关于 broll

broll 是面向视频创作者的剪辑技能,用于建立并检索带标签的 B-roll 素材库。它帮助创作者为短视频找到合适镜头,无需逐个打开视频。

主要功能

  1. 01整理片段元数据与标签
  2. 02生成联系表以供查看
  3. 03增量添加并标记新片段
  4. 04按场景、情绪和主体检索
  5. 05记录已用与拒用的素材

适用场景

  1. 01为短视频查找夜间桌面镜头
  2. 02将新文件夹加入素材库
  3. 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

  1. 文件夹。 用一句话询问他们的素材放在哪里。他们自己的文件夹保持原位(外接硬盘也可以)。素材分散在各处:mkdir -p video-agent/broll/clips 并把素材复制进去。
  2. 他们的照片(用于识别哪些素材里有他们):一张光线充足的脸部照片,保存为 video-agent/broll/me.jpg,再加 1 到 3 张,命名为 me-2.jpg、me-3.jpg(一张特写、戴另一顶帽子或眼镜的照片,以及一帧他们肯定在其中的素材画面):只用一张照片,Haiku 会漏掉他们的大多数素材。没有照片:is_self 保持为 false。
  3. 扫描(每个素材约 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。

  1. 阅读文字稿(短片的 files/words.md,时间以源视频秒数为准)。标记能表现某个时刻或地点(“我搬到了 Lisbon”)、某种感受(“我筋疲力尽”)、创作者正在做某事(“我晚上写代码”)或他们使用的物品的句子。数字、观点和证据仍用画板呈现。
  2. 每行搜索一次;阅读素材表。只有在画面与句子相符(平静的句子搭配平静的画面)、从 best_start 开始的清晰片段时长足以覆盖整个段落,且画面没有问题(模糊、陌生人的脸、私人屏幕)时,才选用片段。每 8 到 15 秒最多添加一个 B-roll,每段 2 到 5 秒,两段 B-roll 之间要有 5 秒的创作者画面。
  3. 在规范文件的 media(source/spec.js)中为每个选中的片段命名:<id>: { src: '<file>', from: <best_start>, dur: <s>, fill: true }(fill 会将横屏片段裁剪为 9:16,focus: [x, y] 可保留画面中的某个部分)。
  4. 在同一规范文件的 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(它会提取帧)。
  5. 在每段画面的中间运行 short.mjs stills,阅读提取的帧,然后完整渲染视频(参见 short-editing 步骤 5 和 6)。
  6. 创作者会保留这段短片:将短片的 slug 添加到 library.json 中每个片段的 used,并重新生成 library.md,这样后续短片就会优先使用新片段。

找不到合适的片段 = 素材库中没有能匹配的画面:保留创作者或画板画面,绝不要随便放一个片段。

/broll reject

在 library.json 中将该片段的 "status" 设为 "rejected"(设为 "active" 可恢复),然后重新生成 library.md:以后不会再出现它。文件本身不会被删除,因为这是创作者的素材。

评论

支持 Markdown