glytch/fktry/tools/optimize-assets.mjs
type-two 20d2bcbbfe [orchestrator] round 7 phase 0: 27.3 MB -> 7.0 MB, and the build can no longer forget
All 27 shipped GLBs run through gltf-transform (meshopt + webp + 1024 textures).
Measured per-asset, no failures: 27,304 KB -> 7,132 KB (74% off) with PBR intact.
The biggest wins are the wave-1/wave-2 assets that shipped raw (artifact-bottler
1453->329, chroma-keyer 1928->301, mv-extractor 1376->190); the six baked through
the new farm script were already lean and barely moved, which is the check that
the farm-side step is doing its job.

tools/optimize-assets.mjs: idempotent gate wired into deploy.sh BEFORE vite build.
Skips anything already carrying EXT_meshopt_compression (so it costs ~0s when
nothing is new) and FAILS THE BUILD on any asset over 900 KB — a farm regression
that emits trellis2-dense meshes now stops the deploy instead of shipping 36 MB.

deploy.sh verify gap closed: it tested exactly one .js file, so a deploy that
shipped ZERO models would have reported success. Now checks the JS bundle, a real
GLB endpoint, and asserts live model count == built model count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:21:20 +10:00

70 lines
2.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Idempotent GLB optimization gate. Runs from deploy.sh BEFORE vite build.
*
* Why this exists: the farm emits 18 MB PBR GLBs. Shipped raw, 27 of them were
* 27.3 MB; through `gltf-transform optimize` they are 7.0 MB with the same PBR.
* That step used to live in my head, which is not a build system. An asset added
* by hand — or by a farm run that skipped the post-step — would silently ship raw.
*
* Skips anything already carrying EXT_meshopt_compression, so it is safe to run
* every deploy and costs ~0s when there is nothing new.
*/
import { execFileSync } from 'node:child_process';
import { readdirSync, readFileSync, statSync, renameSync, rmSync } from 'node:fs';
import { join } from 'node:path';
const DIR = new URL('../public/assets/models/', import.meta.url).pathname;
const MAX_KB = 900; // per-asset ceiling; a farm regression should fail the build
/** Read the GLB JSON chunk without a glTF library. */
function meta(path) {
const b = readFileSync(path);
if (b.length < 20 || b.subarray(0, 4).toString() !== 'glTF') return null;
const jsonLen = b.readUInt32LE(12);
try {
return JSON.parse(b.subarray(20, 20 + jsonLen).toString());
} catch {
return null;
}
}
const files = readdirSync(DIR).filter((f) => f.endsWith('.glb'));
let optimized = 0, skipped = 0, before = 0, after = 0;
const oversize = [];
for (const f of files) {
const p = join(DIR, f);
const sizeBefore = statSync(p).size;
before += sizeBefore;
const m = meta(p);
if (m?.extensionsRequired?.includes('EXT_meshopt_compression')) {
skipped++;
after += sizeBefore;
} else {
const tmp = `${p}.opt`;
execFileSync('npx', ['--yes', '@gltf-transform/cli@latest', 'optimize', p, tmp,
'--texture-compress', 'webp', '--texture-size', '1024',
'--simplify-error', '0.002', '--compress', 'meshopt'],
{ stdio: 'pipe', timeout: 900_000 });
rmSync(p);
renameSync(tmp, p);
optimized++;
after += statSync(p).size;
console.log(` optimized ${f}: ${(sizeBefore / 1024) | 0} KB -> ${(statSync(p).size / 1024) | 0} KB`);
}
const kb = (statSync(p).size / 1024) | 0;
if (kb > MAX_KB) oversize.push(`${f} (${kb} KB)`);
}
console.log(`assets: ${files.length} files, ${optimized} optimized, ${skipped} already lean — ` +
`${(before / 1048576).toFixed(1)} MB -> ${(after / 1048576).toFixed(1)} MB`);
if (oversize.length) {
console.error(`\nFAIL: ${oversize.length} asset(s) over ${MAX_KB} KB after optimization:`);
for (const o of oversize) console.error(` ${o}`);
console.error('A mesh this heavy usually means the bake used too dense a pipeline ' +
'(trellis2 1024_cascade produces ~487k tris; hunyuan3d_mlx is the machine default).');
process.exit(1);
}