← back to Wallco Ai
scripts/dig1984_facesup_smoke_test.js
134 lines
#!/usr/bin/env node
/**
* 5-variant settlement-clearance smoke test for DIG-1984 Imperial Garden.
*
* Adds the HARD directional constraint "every leaf must face UP" to defeat
* Part A1 (directional variation) of the DW Settlement gate. Recolors the
* same source image; Gemini 2.5-flash-image-edit may or may not honor the
* recompose ask — Steve eyeballs the 5 outputs to decide whether to scale.
*
* Outputs go to is_published=FALSE so they don't surface on /designs.
* Five variants chosen for spread:
* 1. tone-on-tone green (Whisper Sage)
* 2. tone-on-tone dark (Navy Ghosted)
* 3. heritage botanical (Emerald on Bone)
* 4. busy jewel-tone (Sapphire & Emerald)
* 5. extreme high contrast (Black on White ink)
*
* Cost: 5 × ~$0.001 = ~$0.005. Run:
* cd ~/Projects/wallco-ai && node scripts/dig1984_facesup_smoke_test.js
*/
'use strict';
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { execSync, spawnSync } = require('child_process');
const KEY = process.env.GEMINI_API_KEY;
if (!KEY) { console.error('GEMINI_API_KEY missing'); process.exit(1); }
const SRC = '/tmp/wallco-sources/DIG-1984.jpg';
if (!fs.existsSync(SRC)) { console.error(`source missing: ${SRC}`); process.exit(1); }
const SRC_B64 = fs.readFileSync(SRC).toString('base64');
const OUT_DIR = path.join(__dirname, '..', 'data', 'generated');
fs.mkdirSync(OUT_DIR, { recursive: true });
const DB = process.env.WALLCO_DB || 'dw_unified';
const PSQL = (process.platform === 'linux')
? `sudo -n -u postgres psql ${DB} -At -q`
: `psql ${DB} -At -q`;
function psql(sql) { return execSync(PSQL, { input: sql, encoding: 'utf8' }).trim(); }
function esc(v) { if (v == null) return 'NULL'; return "'" + String(v).replace(/'/g, "''") + "'"; }
const TESTS = [
{ name: 'Whisper Sage (FacesUp)', prompt: 'Recolor everything into the SAGE GREEN family ONLY. The ground becomes pale sage; the pattern motif becomes a slightly darker, more saturated sage with subtle gray-green undertones. No other hues anywhere. Tone-on-tone wallpaper feel.' },
{ name: 'Navy Ghosted (FacesUp)', prompt: 'Recolor into the NAVY family ONLY. Deep navy ground; near-black navy motif with subtle indigo. Mono-hue tonal.' },
{ name: 'Heritage Emerald on Bone (FacesUp)', prompt: 'Bone-ivory ground; recolor the entire pattern motif to deep emerald green. Archival watercolor feel.' },
{ name: 'Jewel Sapphire & Emerald (FacesUp)', prompt: 'Deep midnight ground; recolor motif in sapphire blue + emerald green + amethyst purple jewel tones with subtle metallic shimmer.' },
{ name: 'Ink Black on White (FacesUp)', prompt: 'Pure white ground; recolor the entire motif to pure black ink linework, no other colors. Pen-and-ink feel.' },
];
const FACES_UP = [
'CRITICAL DIRECTIONAL CONSTRAINT: rotate every leaf, vine tip, frond, and stem so ALL foliage points UPWARD toward the top of the tile.',
'No leaves rotated sideways. No leaves rotated downward. Single directional axis only.',
'This directional uniformity is a HARD constraint — more important than preserving original leaf orientations.',
'Flowers and butterflies keep their existing positions and palette logic; only the FOLIAGE re-orients to face up.',
].join(' ');
const SHARED_TAIL = [
'Preserve overall pattern composition and motif placement.',
'Seamless tile, no edge artifacts, no signature, no watermark, no text.',
'High detail, archival quality, fabric-pattern aesthetic, no cartoon style.',
].join(' ');
async function generateOne(v, idx) {
const fullPrompt = v.prompt + ' ' + FACES_UP + ' ' + SHARED_TAIL;
const body = {
contents: [{
parts: [
{ inline_data: { mime_type: 'image/jpeg', data: SRC_B64 } },
{ text: fullPrompt }
]
}],
generationConfig: { responseModalities: ['IMAGE'] }
};
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=${KEY}`;
const t0 = Date.now();
const r = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!r.ok) throw new Error(`HTTP ${r.status}: ${(await r.text()).slice(0, 300)}`);
const j = await r.json();
try {
const { logGemini } = require(require('os').homedir() + '/.claude/skills/cost-tracker/scripts/log-gemini.js');
logGemini(j, { app: 'wallco-ai', note: 'dig-1984-facesup-smoke', model: 'gemini-2.5-flash-image' });
} catch {}
const part = j.candidates?.[0]?.content?.parts?.find(p => p.inline_data || p.inlineData);
const data = part?.inline_data?.data || part?.inlineData?.data;
if (!data) {
const text = j.candidates?.[0]?.content?.parts?.find(p => p.text)?.text;
throw new Error(`no image returned (${j.candidates?.[0]?.finishReason || 'unknown'}): ${(text || '').slice(0, 200)}`);
}
const seed = crypto.randomInt(1, 2 ** 31 - 1);
const filename = `dig1984_facesup_${Date.now()}_${seed}.png`;
const outPath = path.join(OUT_DIR, filename);
fs.writeFileSync(outPath, Buffer.from(data, 'base64'));
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const labelTitle = `Imperial Garden Barone · ${v.name}`;
const promptDesc = (labelTitle + ' — ' + v.prompt + ' [FacesUp constraint applied]').slice(0, 1800);
const insertSql = `INSERT INTO spoon_all_designs
(kind, width_in, height_in, generator, prompt, seed, dominant_hex, palette,
local_path, image_url, category, motifs, tags, is_published, request_text)
VALUES ('seamless_tile', 24, 24, 'gemini-2.5-flash-image-edit',
${esc(promptDesc)}, ${seed},
NULL, '[]'::jsonb,
${esc(outPath)}, ${esc('/designs/img/' + filename)},
'colorways-imperial-garden-barone-facesup-test',
ARRAY['floral', 'garden', 'chinoiserie']::text[],
ARRAY['imperial-garden', 'barone', 'dig-1984', 'facesup-smoke-test', 'settlement-mitigation']::text[],
FALSE, ${esc('dig-1984 FacesUp smoke #' + (idx + 1) + ': ' + v.name)})
RETURNING id;`;
const id = parseInt(psql(insertSql), 10);
console.log(` [${idx + 1}/5] ${v.name.padEnd(38)} → id=${id} ${elapsed}s ${filename}`);
return { id, name: v.name, outPath };
}
(async () => {
console.log(`\nDIG-1984 FacesUp settlement-clearance smoke test (5 variants)\n`);
const results = [];
for (let i = 0; i < TESTS.length; i++) {
try { results.push(await generateOne(TESTS[i], i)); }
catch (e) { console.error(` [${i + 1}/5] FAIL — ${e.message.slice(0, 200)}`); }
}
console.log(`\nDone. ${results.length}/5 generated.`);
if (results.length) {
console.log(`\nLocal paths to open:`);
for (const r of results) console.log(` open '${r.outPath}'`);
console.log(`\nAll quarantined (is_published=false). Open each in Preview to eyeball whether Gemini honored the 'leaves face up' directional constraint.`);
}
process.exit(results.length === 5 ? 0 : 1);
})();