broll
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseB-roll
B-roll
B-roll = the images shown over the creator's voice: their own footage (street, desk at night, sunset, gym, phone in hand), screen recordings, photos. The library is read ONCE, then every edit finds "me at my desk at night" with one grep, without opening a single video.
text
<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 wroteTalk to the creator in their language, short messages, no jargon ("ta bibliothèque de B-roll", never "JSON"). Run every command from the project folder (the one holding ). Preflight: ; missing: .
video-agent/ffmpeg -version && node -v/studio-setup toolsB-roll = 创作者旁白上叠加的画面:他们自己的素材(街头、夜间书桌、日落、健身房、手持手机)、屏幕录制和照片。素材库只需读取一次,此后每次剪辑都能用一次 grep 找到“我在夜间坐在书桌前”,无需打开任何一个视频。
text
<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
/broll setup
- The folder. Ask in one line where their clips are. Their own folder stays where it is (an external drive works). Clips spread everywhere: and copy them in.
mkdir -p video-agent/broll/clips - Photos of them (to know which clips show them): their face, well lit, as , plus 1 to 3 more as
video-agent/broll/me.jpg,me-2.jpg(a close-up, another cap or glasses, a frame of a clip where they surely are): with one photo alone, Haiku misses most of their clips. No photo:me-3.jpgstays false.is_self - The scan (about 1 s per clip). Only new files are read, so the same command adds clips later:
bash
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>"Clips with GPS: name the places, one request per distinct spot (rounded to 2 decimals), one per second, then set ("Lisbon, Portugal") in : . No GPS = no place, never a guess.
4. The tagging, by Haiku (cheap and fast). Batches of 5 untagged clips; one sub-agent per batch, all launched in ONE message so they run together, 10 at a time (Claude Code: the Agent tool with ). No sub-agents in this tool: tag the batches yourself, same rules. Prompt of each tagger, filled in:
placelibrary.jsoncurl -s "https://nominatim.openstreetmap.org/reverse?format=json&zoom=10&lat=<lat>&lon=<lon>" -H "User-Agent: video-agent-skills"model: "haiku"text
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.Then merge every batch and rebuild (also after any change by hand to ):
library.mdlibrary.jsonbash
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");
'A batch file that is not valid JSON or misses an id: run its tagger again. A big library (300+ clips): tag 100 now, say so, the rest in the background or the first time a video needs B-roll.
5. Check, with the creator: 3 searches from their real topics (below), Read the sheets of the hits, show what you found ("pour « bureau la nuit » j'ai ces 3 plans"). A wrong tag: fix it in and rebuild. A clip they never want: reject it.
6. One line to them: "ta bibliothèque est prête : N clips. Dis « ajoute ces vidéos » pour en mettre d'autres, « mets mes B-roll » sur un short."
library.json- 文件夹。 用一句话询问他们的片段存放在哪里。他们自己的文件夹保持原位(也可以使用外置硬盘)。如果片段散落在各处:执行 并将它们复制进去。
mkdir -p video-agent/broll/clips - 他们的照片(用于判断哪些片段中有他们):提供光线充足的正脸照,保存为 ,再提供 1 到 3 张照片,保存为
video-agent/broll/me.jpg、me-2.jpg(可以是特写、另一顶帽子或眼镜,或者一段确定有他们的视频画面):只有一张照片时,Haiku 会漏掉大多数包含他们的片段。没有照片:me-3.jpg保持为 false。is_self - 扫描(每个片段约 1 秒)。只读取新文件,因此之后可再次运行同一命令来添加片段:
bash
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 的片段:为地点命名,每个不同地点发起一次请求(坐标四舍五入到小数点后两位),每秒一个请求,然后在 中设置 (“里斯本,葡萄牙”):。没有 GPS 就不设置地点,绝不猜测。
4. 由 Haiku 标注(成本低且速度快)。每批处理 5 个未标注片段;每批一个 sub-agent,在一条消息中全部启动以便并行运行,每次运行 10 个(Claude Code:使用 的 Agent 工具)。此工具中没有 sub-agent 时:自行标注各批,遵循相同规则。填入每个标注 Agent 的提示词:
library.jsonplacecurl -s "https://nominatim.openstreetmap.org/reverse?format=json&zoom=10&lat=<lat>&lon=<lon>" -H "User-Agent: video-agent-skills"model: "haiku"text
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.mdlibrary.jsonbash
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 时:重新运行对应的标注 Agent。素材库较大(300 多个片段)时:先标注 100 个并告知用户,其余在后台处理,或等到首次需要 B-roll 的视频时再处理。
5. 与创作者核对:根据他们真正关心的主题执行 3 次搜索(见下文),Read 命中片段的 contact sheet,并展示找到的内容(“夜间书桌,我有这 3 个镜头”)。标签有误:在 中修正并重建。创作者不想要的片段:拒绝它。
6. 用一句话告诉创作者:“你的素材库已准备好:N 个片段。说出‘添加这些视频’即可添加更多素材;说出‘把我的 B-roll 放到这个 short 上’即可用于 short。”
library.json/broll add
/broll add
Their files or a folder: copy them into their clips folder (or scan them where they are), then steps 3 and 4 on the new ones only (the scan skips known files, the taggers get only ). Say what came in: "12 clips ajoutés : 4 à Lisbonne en août, 8 au bureau".
tags: null他们提供的文件或文件夹:将其复制到自己的片段文件夹(或在原位置扫描),然后只对新文件执行第 3 步和第 4 步(扫描会跳过已知文件,标注 Agent 只处理 的条目)。说明新增了哪些内容:“新增 12 个片段:8 月在里斯本的有 4 个,在办公室的有 8 个”。
tags: null/broll search
/broll search
bash
grep -i -E "sunset|dusk|golden" video-agent/broll/library.md
grep -i "desk" video-agent/broll/library.md | grep -i "night" | grep "| me |"English words of what the image should show; synonyms joined with , a second filter with a second . Rank the hits yourself: matches the line, then unused, then recent, then the right orientation (portrait for a short; a landscape clip is cropped to fill). Then Read the sheets of the 3 best: the tags help you find, only your eyes validate.
|grepbash
grep -i -E "sunset|dusk|golden" video-agent/broll/library.md
grep -i "desk" video-agent/broll/library.md | grep -i "night" | grep "| me |"用英文描述画面应呈现的内容;同义词用 连接,再用第二个 添加第二层筛选。自行对命中结果排序:先看整行是否匹配,再优先未使用过的、较近期的、方向合适的(short 使用竖屏;横屏片段会裁剪以铺满画面)。然后 Read 最佳 3 个结果的 contact sheet:标签帮助你找到素材,只有肉眼才能验证是否合适。
|grep/broll use
/broll use
The short in progress ( or named): its B-roll from its words.
video-agent/edits/<slug>/- Read the transcript (in the studio, source ms). Mark the lines that show a moment or a place ("I moved to Lisbon"), a feeling ("I was exhausted"), the creator doing something ("I code at night"), a thing they use. Numbers, ideas and proof stay as callouts.
public/<slug>.json - One search per line; Read the sheets. A clip goes in only if the image fits the sentence (a calm line over a calm image), the clean part from lasts the whole stretch, and nothing is wrong on screen (blur, a stranger's face, a private screen). At most one B-roll every 8 to 15 s, 2 to 5 s each, 5 s of the creator between two.
best_start - Copy each picked clip into the studio: .
mkdir -p video-agent/studio/public/broll && cp "<file>" video-agent/studio/public/broll/<id>.<ext> - Add them to :
public/<slug>.edit.json. The composition needs the"broll": [{ "src": "broll/<id>.mp4", "atMs": <startMs of the first word>, "toMs": <endMs of the last word>, "startSec": <best_start> }]layer ofbroll(itsshort-editing, section 2): a studio made before it lacks it, add it once.references/remotion-short.md - Render a still in the middle of each stretch, Read it, then the full render (steps 6 to 8).
short-editing - The creator keeps the short: add the slug to each clip's in
usedand rebuildlibrary.json, so the next shorts prefer fresh clips.library.md
No good hit = the library lacks that image: keep the creator on screen or a callout, never a random clip.
正在制作的 short( 或已命名的项目):根据其文案配置 B-roll。
video-agent/edits/<slug>/- 读取 transcript(,位于 studio 中,源时间戳使用毫秒)。标记能够呈现某个时刻或地点(“我搬到了里斯本”)、某种感受(“我筋疲力尽”)、创作者正在做的事情(“我晚上写代码”)或他们使用的东西的台词行。数字、观点和证据保留为 callout。
public/<slug>.json - 每行执行一次搜索;Read contact sheet。只有当画面与台词相符(平静的台词配平静的画面)、从 开始的干净部分能覆盖整个片段时长,且画面中没有问题(模糊、陌生人的脸、私密屏幕)时,才放入该 B-roll。每 8 到 15 秒最多插入一个 B-roll,每个持续 2 到 5 秒,两段之间保留创作者出镜 5 秒。
best_start - 将每个选中的片段复制到 studio:。
mkdir -p video-agent/studio/public/broll && cp "<file>" video-agent/studio/public/broll/<id>.<ext> - 将它们添加到 :
public/<slug>.edit.json。合成需要"broll": [{ "src": "broll/<id>.mp4", "atMs": <startMs of the first word>, "toMs": <endMs of the last word>, "startSec": <best_start> }]的short-editing图层(其broll第 2 节):此前创建的 studio 缺少该图层,只需添加一次。references/remotion-short.md - 在每段中点渲染一张静帧,Read 该静帧,然后进行完整渲染(的第 6 到第 8 步)。
short-editing - 创作者保留该 short 后:将 slug 添加到 中每个片段的
library.json里并重建used,这样后续 short 会优先使用新片段。library.md
没有合适的命中结果 = 素材库缺少该画面:保留创作者出镜或使用 callout,绝不随意选取片段。
/broll reject
/broll reject
"status": "rejected"library.json"active"library.md在 中将该片段的 (改为 可恢复),然后重建 :它将不再出现。文件本身绝不会被删除,因为它是创作者自己的素材。
library.json"status": "rejected""active"library.md