← back to Dw Fleet Registry
batch-gen-rooms.mjs
72 lines
#!/usr/bin/env node
// Batch: for each standalone fleet site, fetch its top product (= the pattern), generate
// 3 room-setting heroes via :3075, 2x-upscale, save to viewer/static/rooms/<domain>/, and
// stage the pool (/static URLs) via the viewer. Resumable (skips sites already done).
import fs from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
const DIR = path.dirname(new URL(import.meta.url).pathname);
const map = JSON.parse(fs.readFileSync(path.join(DIR, 'data', 'deploy-map.json'), 'utf8'));
const VIEWER = 'http://localhost:9774';
const GEN = 'http://45.61.58.125:3075/api/generate-room';
const ROOMS = [
{ roomType: 'living_room', angle: 'straight_on', cameraDistance: 8 },
{ roomType: 'office', angle: 'angled_left', cameraDistance: 11 },
{ roomType: 'dining_room', angle: 'straight_on', cameraDistance: 11 },
];
const CONC = 3;
// only standalone sites that have a deploy target AND a live /api/products
const domains = Object.keys(map);
async function topPattern(domain) {
try {
const r = await fetch(`https://${domain}/api/products?limit=8`, { signal: AbortSignal.timeout(10000) });
if (!r.ok) return null;
const j = await r.json();
const items = (j.items || j.products || []).filter(p => p && p.image_url && /^https/.test(p.image_url));
return items[0]?.image_url || null;
} catch { return null; }
}
async function genRoom(b64, j) {
const r = await fetch(GEN, { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ patternBase64: b64, roomType: j.roomType, angle: j.angle, cameraDistance: j.cameraDistance, patternWidth: 36, patternHeight: 36 }), signal: AbortSignal.timeout(90000) });
const d = await r.json().catch(() => ({}));
if (!d.success || !d.image) throw new Error(d.error || ('HTTP ' + r.status));
return Buffer.from(d.image.replace(/^data:[^,]+,/, ''), 'base64');
}
async function isRedirect(domain) {
try { const r = await fetch(`https://${domain}/`, { redirect: 'manual', signal: AbortSignal.timeout(10000) }); return r.status >= 300 && r.status < 400; } catch { return false; }
}
async function doSite(domain) {
const outDir = path.join(DIR, 'viewer', 'static', 'rooms', domain);
const done = fs.existsSync(outDir) && fs.readdirSync(outDir).filter(f => f.endsWith('.png')).length >= ROOMS.length;
if (done) { console.log(`SKIP ${domain} (already has rooms)`); return; }
if (await isRedirect(domain)) { console.log(`REDIR ${domain} (skip — hero never shown)`); return; }
const pat = await topPattern(domain);
if (!pat) { console.log(`NOPAT ${domain} (no product pattern)`); return; }
let b64;
try { b64 = Buffer.from(await (await fetch(pat, { signal: AbortSignal.timeout(20000) })).arrayBuffer()).toString('base64'); }
catch { console.log(`PATFAIL ${domain}`); return; }
fs.mkdirSync(outDir, { recursive: true });
const urls = [];
for (const j of ROOMS) {
try {
const buf = await genRoom(b64, j);
const file = `${j.roomType}-${j.angle}.png`;
const out = path.join(outDir, file);
fs.writeFileSync(out, buf);
try { execFileSync('sips', ['-Z', '2368', out]); } catch {}
urls.push(`/static/rooms/${domain}/${file}`);
} catch (e) { console.log(` gen-fail ${domain}/${j.roomType}: ${String(e.message).slice(0,60)}`); }
}
if (urls.length) {
await fetch(`${VIEWER}/api/hero-pool/set`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain, images: urls }) });
console.log(`OK ${domain} -> ${urls.length} rooms staged`);
} else console.log(`EMPTY ${domain}`);
}
// pool runner
let i = 0; let ok = 0;
async function worker() { while (i < domains.length) { const d = domains[i++]; try { await doSite(d); } catch (e) { console.log(`ERR ${d}: ${e.message}`); } } }
console.log(`batch over ${domains.length} domains, conc ${CONC}`);
await Promise.all(Array.from({ length: CONC }, worker));
console.log('batch done');