// VERIFY SWEEP worker (A, 28 Oct): node _tools/verify_kill.cjs [outDir] // per kill: motionQA g5 (with the face check, warn-only data) + g0, mapCheck over the 17 dry maps, a 10-frame strip (trigger -> final, g5) // + the XT and L finals -> /.jpg, one JSON line in /results.jsonl; then /_sheet.jpg (every kill, labelled) const fs = require('fs'), os = require('os'), path = require('path'); let chromium; try { ({ chromium } = require('playwright')); } catch (e) { ({ chromium } = require('/Users/basha/node_modules/playwright')); } const [w, keysArg, outArg] = process.argv.slice(2), keys = keysArg.split(',').filter(Boolean), out = outArg || '/Volumes/Workspace/Scratch/coord/verify'; fs.mkdirSync(out, { recursive: true }); const ROOT = path.resolve(__dirname, '..'), CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; (async () => { const udd = fs.mkdtempSync(path.join(os.tmpdir(), 'k58v-')); const ctx = await chromium.launchPersistentContext(udd, { headless: true, executablePath: CHROME, args: ['--headless=new', '--allow-file-access-from-files'], viewport: { width: 1200, height: 900 } }); const page = await ctx.newPage(), errs = []; page.on('pageerror', (e) => errs.push(e.message)); try { // (a page-load timeout is a FAILURE: tried twice (150 s, then 150 s again), then exit 2 with no rows; never a silent DONE) let loaded = false; for (let tr = 0; tr < 2 && !loaded; tr++) { try { await page.goto('file://' + path.join(ROOT, '58k_' + w + '.html'), { timeout: 150000, waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => document.documentElement.classList.contains('k58-loaded'), null, { timeout: 180000 }); loaded = true; } catch (e) { console.error('LOAD FAIL ' + w + ' try ' + (tr + 1) + ': ' + String(e.message).split('\n')[0]); } } if (!loaded) { console.error('WORKER ' + w + ' page never loaded'); process.exitCode = 2; return; } const cells = []; for (const key of keys) { const t0 = Date.now(), e0 = errs.length; let r; try { r = await page.evaluate(async (key) => { const d = K58.get(key); if (!d) return { key, err: 'no kill' }; // (a kill that is not built is not rateable: a ready:false plan stub (e.g. 58k_w_flamer_plan.js) or a film with no bodies: a FAILURE row, never a blank strip) if (d.ready !== true) return { key, name: d.name, err: 'not built (ready: ' + d.ready + ', src ' + (d.src58 || '?') + ')' }; { const v = K58.offscreen(64, 30, {}); let nb = 0, fk = 0; try { v.load(d, { gore: 5 }); nb = (v.scene && v.scene.bodies || []).length; const Ph = v.phase58(); fk = Ph ? Ph.fK : 0; } catch (e) { /* */ } K58.offRelease(v); if (!nb) return { key, name: d.name, err: 'blank film (no bodies, fK ' + fk + ')' }; } const REAL = K58.REAL, top = (R) => R ? R.list.filter((e) => e.kind !== 'face').sort((a, b) => (a.sev === b.sev ? 0 : a.sev === 'fail' ? -1 : 1)).slice(0, 3).map((e) => (e.sev === 'fail' ? 'FAIL ' : 'warn ') + e.f.toFixed(2) + ' ' + e.who + ': ' + e.msg) : []; const q5 = REAL.motionQA(d, { gore: 5 }), q0 = REAL.motionQA(d, { gore: 0, face: false }); const nf = (R) => R ? { fail: R.list.filter((e) => e.sev === 'fail' && e.kind !== 'face').length, warn: R.list.filter((e) => e.sev !== 'fail' && e.kind !== 'face').length } : null; const face = q5 ? q5.list.filter((e) => e.kind === 'face').map((e) => ({ f: +e.f.toFixed(2), f1: +(e.f1 || e.f).toFixed(2), who: e.who, cls: /\[face check in calibration/.test(e.msg) ? 'FAILCLASS' : 'warn', msg: e.msg.replace(' [face check in calibration: warn only]', '') })) : []; const maps = (K58.mapIds ? K58.mapIds() : []).filter((m) => m !== 'deep'); let plays = 0; const skip = []; for (const m of maps) { let mc = null; try { mc = K58.mapCheck(d, m); } catch (e) { /* */ } if (mc && mc.mode === 'skip') skip.push(m); else plays++; } await K58.plateFor(d, { gore: 5 }); const o5 = { gore: 5 }, m = K58.measure(d, o5), fT5 = K58.finalT(d, o5), ts = Array.from({ length: 10 }, (_, i) => Math.max(0, m.fK - 0.3) + (fT5 - Math.max(0, m.fK - 0.3)) * i / 9); const W = 200, H = 92, fr = K58.still(d, ts, W, H, { dpr: 1, opts: o5 }), fx = K58.still(d, [fT5], W, H, { dpr: 1, opts: o5 })[0], o1 = { gore: 1 }, fl = K58.still(d, [K58.finalT(d, o1)], W, H, { dpr: 1, opts: o1 })[0]; const cv = document.createElement('canvas'); cv.width = W * 6; cv.height = (H + 14) * 2 + 18; const g = cv.getContext('2d'); g.fillStyle = '#111'; g.fillRect(0, 0, cv.width, cv.height); g.fillStyle = q5 && q5.fails ? '#FF5570' : '#5CE08A'; g.font = 'bold 13px sans-serif'; g.fillText(key + ' ' + (d.name || '') + ' g5 ' + (q5 ? q5.fails + 'f/' + q5.warns + 'w' : '-') + ' maps ' + plays + '/' + maps.length + (face.some((x) => x.cls === 'FAILCLASS') ? ' FACE!' : ''), 4, 13); fr.forEach((c, i) => { const X = (i % 5) * W, Y = 18 + Math.floor(i / 5) * (H + 14); g.drawImage(c, X, Y + 12); g.fillStyle = '#ccc'; g.font = '10px sans-serif'; g.fillText('f' + ts[i].toFixed(2), X + 3, Y + 10); }); g.drawImage(fx, 5 * W, 18 + 12); g.drawImage(fl, 5 * W, 18 + H + 14 + 12); g.fillStyle = '#FFD23F'; g.font = 'bold 10px sans-serif'; g.fillText('XT final', 5 * W + 3, 18 + 10); g.fillText('L final', 5 * W + 3, 18 + H + 14 + 10); return { key, name: d.name, film: +m.film.toFixed(2), fK: +m.fK.toFixed(2), g5: nf(q5), g0: nf(q0), top5: top(q5), top0: top(q0), face, maps: plays + '/' + maps.length, skip, url: cv.toDataURL('image/jpeg', 0.8) }; }, key); } catch (e) { r = { key, err: String(e.message || e).slice(0, 200) }; } r.pageErrors = errs.slice(e0, e0 + 3).map((s) => s.slice(0, 160)); r.sec = Math.round((Date.now() - t0) / 1000); r.w = w; if (r.url) { fs.writeFileSync(path.join(out, key + '.jpg'), Buffer.from(r.url.split(',')[1], 'base64')); cells.push(r.url); delete r.url; } if (r.err) process.exitCode = 3; fs.appendFileSync(path.join(out, 'results.jsonl'), JSON.stringify(r) + '\n'); console.log(key, r.err ? 'ERR ' + r.err : 'g5 ' + JSON.stringify(r.g5) + ' g0 ' + JSON.stringify(r.g0) + ' maps ' + r.maps + ' face ' + r.face.length + ' (' + r.sec + ' s)'); } if (cells.length) { const url = await page.evaluate(async (cells) => { const ims = await Promise.all(cells.map((u) => new Promise((res) => { const im = new Image(); im.onload = () => res(im); im.src = u; }))); const W = ims[0].width, H = ims[0].height, cols = 2, cv = document.createElement('canvas'); cv.width = W * cols; cv.height = H * Math.ceil(ims.length / cols); const g = cv.getContext('2d'); g.fillStyle = '#111'; g.fillRect(0, 0, cv.width, cv.height); ims.forEach((im, i) => g.drawImage(im, (i % cols) * W, Math.floor(i / cols) * H)); return cv.toDataURL('image/jpeg', 0.75); }, cells); fs.writeFileSync(path.join(out, w + '_sheet.jpg'), Buffer.from(url.split(',')[1], 'base64')); } } finally { await ctx.close(); fs.rmSync(udd, { recursive: true, force: true }); } })().catch((e) => { console.error('WORKER', w, e.message); process.exitCode = 1; }).then(() => process.exit(process.exitCode || 0));