7b92c9cAdd dataset generation scripts, ML fitting code and reference recordingsJeremy Magland 1// Renders a batch of explicit parameter sets (for the optimizer in ml/fit.py):
2// node --experimental-strip-types scripts/render-batch.ts --batch JOBS.json --out DIR [--workers W] [--duration D]
3// JOBS.json is {"jobs": [{"id": "...", "params": {PhysParams overrides in SI, "features" as text}}]}.
4// Writes DIR/clips/ID.wav and DIR/index.jsonl like generate.ts; existing clips are skipped.
6import { readFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs";
7import { join } from "node:path";
8import { chromium, type Browser, type Page } from "playwright";
9import { defaultParams, makeGrid, type PhysParams } from "../src/params.ts";
10import { initialShape, parseFeatures, shapeStats } from "../src/shape.ts";
11import { renderClip, serveDist, ARGS, type Sample } from "./generate.ts";
13const argv = process.argv.slice(2);
14const arg = (n: string, d: string) => { const k = argv.indexOf(`--${n}`); return k >= 0 && k + 1 < argv.length ? argv[k + 1] : d; };
15const OUT = arg("out", "batch");
16const WORKERS = parseInt(arg("workers", "16"));
18interface Job { id: string; params: Record<string, unknown> }
20function toSample(job: Job): Sample {
21 const p: PhysParams = { ...defaultParams, excitation: "strike", hitSequence: "single", duration: parseFloat(arg("duration", "3")) };
22 for (const [k, v] of Object.entries(job.params)) {
23 if (k === "features") (p as unknown as Record<string, unknown>).features = typeof v === "string" ? (v === "none" ? [] : parseFeatures(v) ?? []) : v;
24 else if (k in p) (p as unknown as Record<string, unknown>)[k] = v;
25 }
26 const grid = makeGrid(p);
27 const stats = shapeStats(initialShape(p, grid), grid);
28 return { id: job.id, index: -1, params: p, shapeScale: 0, peakToPeak: stats.peakToPeak, maxSlope: stats.maxSlope, nx: grid.nx, ny: grid.ny };
29}
31async function main(): Promise<void> {
32 mkdirSync(join(OUT, "clips"), { recursive: true });
33 const jobs = (JSON.parse(readFileSync(arg("batch", "jobs.json"), "utf8")).jobs as Job[]).filter((j) => !existsSync(join(OUT, "clips", `${j.id}.wav`)));
34 if (!jobs.length) { console.log("nothing to render"); return; }
35 const url = await serveDist();
36 let next = 0, done = 0, failed = 0;
37 const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
38 const worker = async (w: number) => {
39 await sleep(500 * w);
40 let browser: Browser = await chromium.launch({ headless: true, args: ARGS });
41 let page: Page = await (await browser.newContext()).newPage();
42 while (next < jobs.length) {
43 const job = jobs[next++];
44 const s = toSample(job);
45 for (let attempt = 0; ; attempt++) {
46 try {
47 const rec = await renderClip(page, url, s);
48 appendFileSync(join(OUT, "index.jsonl"), JSON.stringify(rec) + "\n");
49 done++;
50 console.log(`[w${w}] ${job.id} done in ${(rec.wallSeconds as number).toFixed(0)} s (${done} done, ${failed} failed)`);
51 break;
52 } catch (e) {
53 const msg = String(e).replace(/\n/g, " ");
54 if (/no WebGPU|closed|crashed|Protocol error|timeout/i.test(msg) && attempt < 3) {
55 try { await browser.close(); } catch { /* gone */ }
56 await sleep(3000);
57 browser = await chromium.launch({ headless: true, args: ARGS });
58 page = await (await browser.newContext()).newPage();
59 continue;
60 }
61 failed++;
62 console.log(`[w${w}] ${job.id} FAILED: ${msg}`);
63 appendFileSync(join(OUT, "index.jsonl"), JSON.stringify({ id: job.id, failed: msg }) + "\n");
64 break;
65 }
66 }
67 }
68 await browser.close();
69 };
70 await Promise.all(Array.from({ length: Math.min(WORKERS, jobs.length) }, (_, w) => worker(w)));
71 console.log(`batch finished: ${done} done, ${failed} failed`);
72 process.exit(0);
73}
75void main().catch((e) => { console.error(e); process.exit(1); });