Docs/Skills and code/skill video-visual-production

.claude/skills/video-visual-production/scripts/lib/util.js

Code77 lines3.9 KBSkill: video-visual-production

What the file says about itself

Shared helpers for the video-visual-production scripts. Node only, no dependencies.

// Shared helpers for the video-visual-production scripts. Node only, no dependencies.
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { spawnSync } = require('child_process');

const ROOT = path.resolve(__dirname, '..', '..', '..', '..', '..');
const SKILL = path.resolve(__dirname, '..', '..');
const FPS = 30;

const readJSON = (p) => JSON.parse(fs.readFileSync(p, 'utf8'));
const writeJSON = (p, o) => { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(o, null, 2)); };
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
const sha256File = (p) => sha256(fs.readFileSync(p));
const toFrame = (sec) => Math.round(sec * FPS);
const fromFrame = (f) => f / FPS;
const arg = (name, def) => { const i = process.argv.indexOf('--' + name); return i > -1 && i + 1 < process.argv.length && !process.argv[i + 1].startsWith('--') ? process.argv[i + 1] : def; };
const flag = (name) => process.argv.includes('--' + name);

// Heavy output lives outside OneDrive. Override with VIDEO_BUILD_DIR.
function buildDir(slug, beat) {
  const base = process.env.VIDEO_BUILD_DIR || 'C:/video-build';
  const d = path.join(base, slug, beat || '');
  fs.mkdirSync(d, { recursive: true });
  return d;
}

function run(cmd, args, opt = {}) {
  const r = spawnSync(cmd, args, { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, ...opt });
  if (r.error) throw r.error;
  return r;
}
function mustRun(cmd, args, opt = {}) {
  const r = run(cmd, args, opt);
  if (r.status !== 0) throw new Error(`${cmd} ${args.slice(0, 6).join(' ')} ... failed (${r.status}): ${(r.stderr || '').slice(-600)}`);
  return r;
}

const say = (m) => console.log(m);
function die(m) { console.error('FAIL: ' + m); process.exit(1); }

// Resolve a repo-relative path from the beat sheet.
const fromRoot = (p) => (path.isAbsolute(p) ? p : path.join(ROOT, p));

// Word times for a beat sheet. words.json times beats on one global timeline; `time_offset_s` in the sheet is where this beat starts there.
function loadWords(sheet) {
  const off = sheet.time_offset_s || 0;
  return readJSON(fromRoot(sheet.words)).words.map((w) => (w.beat === sheet.beat && off ? { ...w, start: +(w.start - off).toFixed(3), end: +(w.end - off).toFixed(3) } : w));
}

// Build slots: the machine has about 800 MB free, so parallel agents queue for a slot (default 2). Slots are lock folders.
async function withSlot(name, fn) {
  const n = +(process.env.VIDEO_BUILD_SLOTS || 2), dir = path.join(process.env.VIDEO_BUILD_DIR || 'C:/video-build', '.slots');
  fs.mkdirSync(dir, { recursive: true });
  const t0 = Date.now();
  for (;;) {
    for (let i = 0; i < n; i++) {
      const d = path.join(dir, 'slot' + i);
      try { fs.mkdirSync(d); fs.writeFileSync(path.join(d, 'owner.txt'), name + ' pid ' + process.pid + ' ' + new Date().toISOString()); return await run1(d, fn); } catch (e) {
        if (e.code !== 'EEXIST') throw e;
        try {
          const owner = fs.readFileSync(path.join(d, 'owner.txt'), 'utf8'), pid = +(owner.match(/pid (d+)/) || [])[1];
          let alive = true;
          if (pid) { try { process.kill(pid, 0); } catch (k) { alive = k.code === 'EPERM'; } }
          if (!alive || Date.now() - fs.statSync(d).mtimeMs > 12 * 60 * 1000) fs.rmSync(d, { recursive: true, force: true });
        } catch (_) { /* gone, or not yet written */ }
      }
    }
    if (Date.now() - t0 > 60 * 60 * 1000) throw new Error('waited an hour for a build slot');
    if (!withSlot.said) { console.log('   waiting for a build slot (' + n + ' at a time) ...'); withSlot.said = true; }
    await new Promise((r) => setTimeout(r, 4000));
  }
}
async function run1(d, fn) { try { return await fn(); } finally { fs.rmSync(d, { recursive: true, force: true }); } }

module.exports = { withSlot, loadWords, ROOT, SKILL, FPS, readJSON, writeJSON, sha256, sha256File, toFrame, fromFrame, arg, flag, buildDir, run, mustRun, say, die, fromRoot };

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.