← back to Wallco Ai
scripts/auto_variations.js
126 lines
#!/usr/bin/env node
/**
* Auto-variation fan-out — after generator_tick.js produces a base design,
* this script creates up to 5 color variations via Python+PIL hue shifts.
*
* Each variation:
* • New PNG written to data/generated/var_<parent>_<idx>.png
* • Triple-layer SAND, LLC watermark applied
* • Inserted as a new spoon_all_designs row with notes='auto-variation of #N'
* • Spawned for AI rating (graphic + interior designer Gemini critique)
*
* Usage:
* node scripts/auto_variations.js --parent 152
* node scripts/auto_variations.js --parent 152 --n 3 (cap at N)
*/
'use strict';
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
const fs = require('fs');
const path = require('path');
const { execSync, spawn, spawnSync } = require('child_process');
const DB = 'dw_unified';
const PSQL_CMD = (process.platform === 'linux')
? `sudo -n -u postgres psql ${DB} -At -q`
: `psql ${DB} -At -q`;
function psql(sql) { return execSync(PSQL_CMD, { input: sql, encoding: 'utf8' }).trim(); }
function sqlStr(v) { return v == null ? 'NULL' : "'" + String(v).replace(/'/g,"''") + "'"; }
const OUT_DIR = path.join(__dirname, '..', 'data', 'generated');
// 5 variation recipes — each is a hue/sat/value shift applied to the parent PNG
const VARIATIONS = [
{ label: 'hue+60', hue: 60, sat: 1.10, val: 1.00 },
{ label: 'hue+120', hue: 120, sat: 1.10, val: 1.00 },
{ label: 'hue+180', hue: 180, sat: 1.05, val: 1.00 },
{ label: 'hue-60', hue: -60, sat: 1.00, val: 1.00 },
{ label: 'mono', hue: 0, sat: 0.35, val: 1.05 },
];
function args() {
const a = process.argv.slice(2);
const o = { parent: null, n: 5 };
for (let i = 0; i < a.length; i++) {
if (a[i] === '--parent') o.parent = parseInt(a[++i], 10);
if (a[i] === '--n') o.n = parseInt(a[++i], 10);
}
return o;
}
const opts = args();
if (!opts.parent) { console.error('--parent <id> required'); process.exit(2); }
const parentRow = psql(`SELECT row_to_json(t) FROM (SELECT id, local_path, category, dominant_hex FROM spoon_all_designs WHERE id=${opts.parent}) t;`);
if (!parentRow) { console.error('parent not found'); process.exit(2); }
const parent = JSON.parse(parentRow);
if (!parent.local_path || !fs.existsSync(parent.local_path)) {
console.error('parent local_path missing:', parent.local_path); process.exit(2);
}
const variations = VARIATIONS.slice(0, Math.min(5, Math.max(1, opts.n)));
const py = `
import sys, colorsys
from PIL import Image
src, dst, hue_shift, sat_mul, val_mul = sys.argv[1], sys.argv[2], int(sys.argv[3]), float(sys.argv[4]), float(sys.argv[5])
img = Image.open(src).convert('RGB')
W, H = img.size
px = img.load()
for y in range(H):
for x in range(W):
r, g, b = px[x, y]
h, l, s = colorsys.rgb_to_hls(r/255, g/255, b/255)
h = ((h * 360 + hue_shift) % 360) / 360
s = max(0, min(1, s * sat_mul))
l = max(0, min(1, l * val_mul))
r2, g2, b2 = colorsys.hls_to_rgb(h, l, s)
px[x, y] = (int(r2*255), int(g2*255), int(b2*255))
img.save(dst, 'PNG')
print(dst)
`;
const created = [];
for (let i = 0; i < variations.length; i++) {
const v = variations[i];
const filename = `var_${opts.parent}_${v.label}_${Date.now()}.png`;
const outPath = path.join(OUT_DIR, filename);
const r = spawnSync('python3',
['-c', py, parent.local_path, outPath, String(v.hue), String(v.sat), String(v.val)],
{ encoding: 'utf8', timeout: 30000 });
if (r.status !== 0) {
console.log(JSON.stringify({event:'var_failed', label:v.label, err:r.stderr.slice(0,200)}));
continue;
}
// Watermark
try {
spawnSync('python3', [path.join(__dirname, 'watermark.py'),
'embed', outPath, '--out', outPath,
'--owner', process.env.WATERMARK_OWNER || 'SAND, LLC',
'--year', String(new Date().getFullYear())], { timeout: 20000 });
} catch {}
// Insert PG row
const id = psql(`INSERT INTO spoon_all_designs
(kind, width_in, height_in, panels, generator, prompt, seed,
pd_source_ids, local_path, dominant_hex, category, is_published, notes)
VALUES('seamless_tile', 24, 24, 1, 'replicate',
${sqlStr('auto-variation ' + v.label + ' of design #' + opts.parent)},
0, '{}', ${sqlStr(outPath)},
${parent.dominant_hex ? sqlStr(parent.dominant_hex) : 'NULL'},
${parent.category ? sqlStr(parent.category) : 'NULL'},
FALSE, ${sqlStr('auto-variation ' + v.label + ' of design #' + opts.parent)})
RETURNING id;`);
const newId = parseInt(id, 10);
created.push({ id: newId, label: v.label, filename });
console.log(JSON.stringify({event:'variation', parent:opts.parent, id:newId, label:v.label}));
// Fire AI rating (detached)
try {
const rater = spawn('node',
[path.join(__dirname, 'rate_design.js'), '--id', String(newId)],
{ detached: true, stdio: 'ignore', env: process.env });
rater.unref();
} catch {}
}
console.log(JSON.stringify({event:'done', parent:opts.parent, created:created.length, ids: created.map(x=>x.id)}));