// Renders a batch of explicit parameter sets (for the optimizer in ml/fit.py): // node --experimental-strip-types scripts/render-batch.ts --batch JOBS.json --out DIR [--workers W] [--duration D] // JOBS.json is {"jobs": [{"id": "...", "params": {PhysParams overrides in SI, "features" as text}}]}. // Writes DIR/clips/ID.wav and DIR/index.jsonl like generate.ts; existing clips are skipped. import { readFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs"; import { join } from "node:path"; import { chromium, type Browser, type Page } from "playwright"; import { defaultParams, makeGrid, type PhysParams } from "../src/params.ts"; import { initialShape, parseFeatures, shapeStats } from "../src/shape.ts"; import { renderClip, serveDist, ARGS, type Sample } from "./generate.ts"; const argv = process.argv.slice(2); const arg = (n: string, d: string) => { const k = argv.indexOf(`--${n}`); return k >= 0 && k + 1 < argv.length ? argv[k + 1] : d; }; const OUT = arg("out", "batch"); const WORKERS = parseInt(arg("workers", "16")); interface Job { id: string; params: Record } function toSample(job: Job): Sample { const p: PhysParams = { ...defaultParams, excitation: "strike", hitSequence: "single", duration: parseFloat(arg("duration", "3")) }; for (const [k, v] of Object.entries(job.params)) { if (k === "features") (p as unknown as Record).features = typeof v === "string" ? (v === "none" ? [] : parseFeatures(v) ?? []) : v; else if (k in p) (p as unknown as Record)[k] = v; } const grid = makeGrid(p); const stats = shapeStats(initialShape(p, grid), grid); return { id: job.id, index: -1, params: p, shapeScale: 0, peakToPeak: stats.peakToPeak, maxSlope: stats.maxSlope, nx: grid.nx, ny: grid.ny }; } async function main(): Promise { mkdirSync(join(OUT, "clips"), { recursive: true }); const jobs = (JSON.parse(readFileSync(arg("batch", "jobs.json"), "utf8")).jobs as Job[]).filter((j) => !existsSync(join(OUT, "clips", `${j.id}.wav`))); if (!jobs.length) { console.log("nothing to render"); return; } const url = await serveDist(); let next = 0, done = 0, failed = 0; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); const worker = async (w: number) => { await sleep(500 * w); let browser: Browser = await chromium.launch({ headless: true, args: ARGS }); let page: Page = await (await browser.newContext()).newPage(); while (next < jobs.length) { const job = jobs[next++]; const s = toSample(job); for (let attempt = 0; ; attempt++) { try { const rec = await renderClip(page, url, s); appendFileSync(join(OUT, "index.jsonl"), JSON.stringify(rec) + "\n"); done++; console.log(`[w${w}] ${job.id} done in ${(rec.wallSeconds as number).toFixed(0)} s (${done} done, ${failed} failed)`); break; } catch (e) { const msg = String(e).replace(/\n/g, " "); if (/no WebGPU|closed|crashed|Protocol error|timeout/i.test(msg) && attempt < 3) { try { await browser.close(); } catch { /* gone */ } await sleep(3000); browser = await chromium.launch({ headless: true, args: ARGS }); page = await (await browser.newContext()).newPage(); continue; } failed++; console.log(`[w${w}] ${job.id} FAILED: ${msg}`); appendFileSync(join(OUT, "index.jsonl"), JSON.stringify({ id: job.id, failed: msg }) + "\n"); break; } } } await browser.close(); }; await Promise.all(Array.from({ length: Math.min(WORKERS, jobs.length) }, (_, w) => worker(w))); console.log(`batch finished: ${done} done, ${failed} failed`); process.exit(0); } void main().catch((e) => { console.error(e); process.exit(1); });