Docs/Skills and code/skill video-visual-production
.claude/skills/video-visual-production/scripts/assets.js
What the file says about itself
V4 Assets: satisfy the asset requests in a beat sheet and write one record per asset. node assets.js --sheet research/<slug>/video/beat-sheet-B1.json [--only id] [--force] Kinds: commons (Wikimedia Commons file with a free licence) and…
#!/usr/bin/env node
// V4 Assets: satisfy the asset requests in a beat sheet and write one record per asset.
// node assets.js --sheet research/<slug>/video/beat-sheet-B1.json [--only id] [--force]
// Kinds: commons (Wikimedia Commons file with a free licence) and capture (headless Chrome, one passage of an article).
// Every asset gets a JSON record next to it and a row in assets.csv; an asset with no licence row is refused.
const fs = require('fs');
const path = require('path');
const U = require('./lib/util');
const cdp = require('./lib/cdp');
const UA = 'ecosystem-video-pipeline/0.1 (research project; contact via repo owner)';
const FREE = /^(CC0|Public domain|PD[- ]|CC BY(-SA)? [0-9.]+|GODL)/i;
const today = () => new Date().toISOString();
// ---------- Commons ----------
async function commons(a, outDir) {
const u = new URL('https://commons.wikimedia.org/w/api.php');
u.search = new URLSearchParams({
action: 'query', format: 'json', titles: a.title, prop: 'imageinfo', iiprop: 'url|size|mime|sha1|extmetadata',
iiurlwidth: String(a.width || 1600),
}).toString();
const j = await (await fetch(u, { headers: { 'user-agent': UA } })).json();
const page = Object.values(j.query.pages)[0];
const ii = page && page.imageinfo && page.imageinfo[0];
if (!ii) throw new Error('Commons has no file ' + a.title);
const m = ii.extmetadata || {};
const val = (k) => String((m[k] && m[k].value) || '').replace(/<[^>]+>/g, '').replace(/&/g, '&').trim();
const licence = val('LicenseShortName');
if (!licence || !FREE.test(licence)) throw new Error(`REFUSED ${a.id}: licence "${licence || 'none'}" is not in the allowlist`);
const url = ii.thumburl || ii.url;
const res = await fetch(url, { headers: { 'user-agent': UA } });
if (!res.ok) throw new Error('download failed ' + res.status);
const buf = Buffer.from(await res.arrayBuffer());
const ext = ii.mime === 'image/png' ? 'png' : 'jpg';
const file = path.join(outDir, `${a.id}.${ext}`);
fs.writeFileSync(file, buf);
return {
id: a.id, kind: 'commons', file: path.basename(file), source_url: ii.descriptionurl, download_url: url,
title: a.title, licence, licence_url: val('LicenseUrl'), author: val('Artist'), credit: val('Credit'),
original_w: ii.width, original_h: ii.height, retrieved_utc: today(), sha256: U.sha256(buf), bytes: buf.length,
use: a.use || '', changes: a.changes || 'scaled and cropped for framing',
};
}
// ---------- Capture (runs inside the page) ----------
function pageFn(args) {
const norm = (s) => s.replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/\s+/g, ' ').trim();
for (const el of document.querySelectorAll('*')) {
const p = getComputedStyle(el).position;
if (p === 'fixed' || p === 'sticky') el.style.setProperty('display', 'none', 'important');
}
const index = (root) => {
const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
let n, str = '', last = true;
const map = [];
while ((n = w.nextNode())) {
const t = n.nodeValue;
for (let i = 0; i < t.length; i++) {
let ch = t[i];
if (ch === '‘' || ch === '’') ch = "'"; else if (ch === '“' || ch === '”') ch = '"';
if (/\s/.test(ch)) { if (last) continue; ch = ' '; last = true; } else last = false;
str += ch; map.push([n, i]);
}
}
return { str, map };
};
const range = (idx, phrase, from = 0) => {
const p = norm(phrase);
const i = idx.str.indexOf(p, from);
if (i < 0) return null;
const s = idx.map[i], e = idx.map[i + p.length - 1];
const r = document.createRange();
r.setStart(s[0], s[1]); r.setEnd(e[0], e[1] + 1);
return r;
};
const A = norm(args.anchor[0]), B = norm(args.anchor[1]);
let best = null;
for (const el of document.querySelectorAll('p, div, section, article, li')) {
const t = norm(el.innerText || '');
if (t.includes(A) && t.includes(B) && (!best || t.length < best.len)) best = { el, len: t.length };
}
if (!best) return { error: 'anchor not found in page text' };
const el = best.el;
const idx = index(el);
const ra = range(idx, args.anchor[0]);
const rb = range(idx, args.anchor[1], idx.str.indexOf(norm(args.anchor[0])));
if (!ra || !rb) return { error: 'anchor range failed' };
const whole = document.createRange();
whole.setStart(ra.startContainer, ra.startOffset); whole.setEnd(rb.endContainer, rb.endOffset);
const sx = window.scrollX, sy = window.scrollY;
const lines = [...whole.getClientRects()].filter((r) => r.width > 1 && r.height > 1);
if (!lines.length) return { error: 'anchor has no visible rects (hidden or paywalled?)' };
const box = el.getBoundingClientRect();
const lineH = Math.max(...lines.map((r) => r.height));
const pad = 20;
const top = Math.min(...lines.map((r) => r.top)) - 7;
const bottom = Math.max(...lines.map((r) => r.bottom)) + 7;
const left = Math.max(0, Math.min(...lines.map((r) => r.left)) - pad * 1.5), right = Math.max(...lines.map((r) => r.right)) + pad * 1.5;
const clip = { x: left + sx, y: top + sy, width: right - left, height: bottom - top };
const hl = {};
for (const h of args.highlights) {
const r = range(idx, h);
hl[h] = r ? [...r.getClientRects()].filter((q) => q.width > 1).map((q) => ({ x: q.left - left, y: q.top - top, w: q.width, h: q.height })) : null;
}
const font = getComputedStyle(el).fontSize;
return { clip, hl, anchorText: whole.toString().replace(/\s+/g, ' ').trim(), blockText: norm(el.innerText).slice(0, 1200), title: document.title, font };
}
async function capture(a, outDir) {
const chrome = await cdp.launch(9333 + Math.floor(Math.random() * 200));
const recs = [];
try {
await chrome.send('Page.enable');
await chrome.send('Emulation.setDeviceMetricsOverride', { width: 1280, height: 900, deviceScaleFactor: 2, mobile: false });
const loaded = chrome.waitEvent('Page.loadEventFired', 45000);
await chrome.send('Page.navigate', { url: a.url });
await loaded.catch(() => null);
await cdp.sleep(3500);
for (const c of a.crops) {
const r = await chrome.send('Runtime.evaluate', {
expression: '(' + pageFn.toString() + ')(' + JSON.stringify({ anchor: c.anchor, highlights: c.highlights || [] }) + ')',
returnByValue: true,
});
const out = r.result.value;
if (!out || out.error) throw new Error(c.id + ': ' + ((out && out.error) || 'no result'));
const shot = await chrome.send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true, clip: { ...out.clip, scale: 1 } });
const buf = Buffer.from(shot.data, 'base64');
const w = buf.readUInt32BE(16), h = buf.readUInt32BE(20);
const ratio = w / out.clip.width;
const file = path.join(outDir, c.id + '.png');
fs.writeFileSync(file, buf);
const boxes = {};
for (const [k, v] of Object.entries(out.hl)) boxes[k] = v && v.map((b) => ({ x: Math.round(b.x * ratio), y: Math.round(b.y * ratio), w: Math.round(b.w * ratio), h: Math.round(b.h * ratio) }));
const missing = Object.entries(boxes).filter(([, v]) => !v).map(([k]) => k);
if (missing.length) throw new Error(c.id + ': highlight phrase(s) not found on page: ' + missing.join(' | '));
recs.push({
id: c.id, kind: 'capture', file: path.basename(file), source_url: a.url, outlet: a.outlet, published: a.published,
retrieved_utc: today(), page_title: out.title, anchor_text: out.anchorText, text_sha256: U.sha256(Buffer.from(out.anchorText)),
block_font_css_px: out.font, image_w: w, image_h: h, css_to_px: ratio, boxes, sha256: U.sha256(buf), bytes: buf.length,
captured: true, method: 'headless Chrome via DevTools; fixed and sticky overlays hidden; cropped to the lines of the quoted passage',
use: c.use || '',
});
}
return recs;
} finally {
await chrome.close();
}
}
// ---------- Quote (typeset on purpose: the source is not a web page we can capture, e.g. a PDF) ----------
function quote(a) {
return {
id: a.id, kind: 'capture', captured: false, typeset_by_choice: true, source_url: a.url, outlet: a.outlet, published: a.published,
fallback_text: a.text, highlights: a.highlights, retrieved_utc: today(), sha256: U.sha256(Buffer.from(a.text)), bytes: Buffer.byteLength(a.text),
method: 'typeset by choice from a quoted fragment recorded in the fact ledger; not a capture', use: a.use || '',
};
}
// ---------- main ----------
async function main() {
const sheetPath = U.fromRoot(U.arg('sheet', ''));
if (!fs.existsSync(sheetPath)) U.die('--sheet <beat-sheet.json> required');
const sheet = U.readJSON(sheetPath);
const only = U.arg('only', null);
const outDir = path.join(U.buildDir(sheet.slug, sheet.beat), 'assets');
fs.mkdirSync(outDir, { recursive: true });
const videoDir = path.dirname(sheetPath);
const records = [];
let failed = 0;
for (const a of sheet.assets || []) {
const ids = a.crops ? a.crops.map((c) => c.id) : [a.id];
if (a.kind === 'quote') { U.writeJSON(path.join(outDir, a.id + '.json'), quote(a)); records.push(quote(a)); U.say('ok ' + a.id + ' (quote, typeset)'); continue; }
if (only && !ids.includes(only)) continue;
const cached = ids.map((id) => path.join(outDir, id + '.json'));
if (!U.flag('force') && cached.every((p) => fs.existsSync(p) && U.readJSON(p).captured !== false)) { cached.forEach((p) => records.push(U.readJSON(p))); U.say('cached ' + ids.join(', ')); continue; }
try {
const out = a.kind === 'quote' ? [quote(a)] : a.kind === 'commons' ? [await commons(a, outDir)] : a.kind === 'capture' ? await capture(a, outDir) : (() => { throw new Error('unknown kind ' + a.kind); })();
for (const rec of out) {
U.writeJSON(path.join(outDir, rec.id + '.json'), rec);
records.push(rec);
U.say('ok ' + rec.id + ' (' + rec.kind + ') ' + Math.round(rec.bytes / 1024) + ' KB');
}
} catch (e) {
failed++;
for (const id of ids) {
const crop = (a.crops || []).find((c) => c.id === id) || {};
const rec = { id, kind: a.kind, captured: false, fallback_text: crop.fallback_text || null, highlights: crop.highlights || null, error: e.message, source_url: a.url || a.title, outlet: a.outlet, published: a.published, retrieved_utc: today(), fallback: a.fallback ? 'typeset' : null };
U.writeJSON(path.join(outDir, id + '.json'), rec);
records.push(rec);
}
U.say('FAILED ' + ids.join(', ') + ': ' + e.message + (a.fallback ? ' -> will be typeset' : ''));
if (!a.fallback) process.exitCode = 1;
}
}
// assets.csv and credits.txt
const esc = (s) => '"' + String(s == null ? '' : s).replace(/"/g, '""') + '"';
const rows = [['id', 'kind', 'source_url', 'licence', 'author', 'retrieved_utc', 'sha256', 'file', 'captured', 'use']].concat(
records.map((r) => [r.id, r.kind, r.source_url, r.licence || (r.kind === 'capture' ? 'quoted passage, short, for review (not licensed)' : ''), r.author || r.outlet || '', r.retrieved_utc, r.sha256 || '', r.file || '', r.captured === false ? 'no' : 'yes', r.use || '']));
fs.writeFileSync(path.join(videoDir, 'assets-' + sheet.beat + '.csv'), rows.map((r) => r.map(esc).join(',')).join('\n') + '\n');
const credits = ['Credits for ' + sheet.beat + ' (generated by assets.js)', ''];
for (const r of records.filter((x) => x.kind === 'commons')) {
credits.push(`Photo: ${r.title.replace(/^File:/, '')} by ${r.author}, ${r.licence} (${r.licence_url || 'licence page on Commons'}), via Wikimedia Commons (${r.source_url}). Changes: ${r.changes}.`);
}
for (const r of records.filter((x) => x.kind === 'capture' && x.typeset_by_choice)) {
credits.push(`Quoted fragment (typeset): ${r.outlet}, ${r.published} (${r.source_url}).`);
}
for (const r of records.filter((x) => x.kind === 'capture' && x.captured !== false)) {
credits.push(`Quoted passage: ${r.outlet}, ${r.published} (${r.source_url}); retrieved ${r.retrieved_utc.slice(0, 10)}.`);
}
credits.push('', 'Fonts: Poppins (SIL Open Font License 1.1). Sound effects and the placeholder bed are synthesised by the pipeline.');
fs.writeFileSync(path.join(videoDir, 'credits-' + sheet.beat + '.txt'), credits.join('\n') + '\n');
U.say(`assets: ${records.length} records, ${failed} failed; build folder ${outDir}`);
}
main().catch((e) => U.die(e.stack || e.message));
Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.