[object Object]

← back to Wallco Ai

wallco: fliepaper-bugs tick generator script

7d5c3579f8a069f657979310f1eb9ee5df6e1200 · 2026-05-13 17:05:17 -0700 · SteveStudio2

Settlement-gated Gemini 2.5 Flash text-to-image runner for the 36-cell
fliepaper-bugs collection. Picks PENDING cells in order, gates each
prompt via the inline settlementCheck (binding-rule implementation from
~/.claude/skills/settlement/SKILL.md), persists PNGs to both the
canonical data/fliepaper-bugs/<pattern>/<colorway>.png path and the
served data/generated/ path, inserts spoon_all_designs rows
(is_published=false; Steve handles Shopify push separately), flips the
cell to GENERATED, and commits per-cell.

Reserved for SS27 drop and skipped naturally (not in spec.json):
patterns praying-mantis-garden + cicada-terracotta, colorway
vintage-rose.

This tick: 36/36 cells generated, 0 SETTLEMENT_BLOCKED, 0 FAILED.

Files touched

Diff

commit 7d5c3579f8a069f657979310f1eb9ee5df6e1200
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 13 17:05:17 2026 -0700

    wallco: fliepaper-bugs tick generator script
    
    Settlement-gated Gemini 2.5 Flash text-to-image runner for the 36-cell
    fliepaper-bugs collection. Picks PENDING cells in order, gates each
    prompt via the inline settlementCheck (binding-rule implementation from
    ~/.claude/skills/settlement/SKILL.md), persists PNGs to both the
    canonical data/fliepaper-bugs/<pattern>/<colorway>.png path and the
    served data/generated/ path, inserts spoon_all_designs rows
    (is_published=false; Steve handles Shopify push separately), flips the
    cell to GENERATED, and commits per-cell.
    
    Reserved for SS27 drop and skipped naturally (not in spec.json):
    patterns praying-mantis-garden + cicada-terracotta, colorway
    vintage-rose.
    
    This tick: 36/36 cells generated, 0 SETTLEMENT_BLOCKED, 0 FAILED.
---
 scripts/fliepaper_bugs_tick.js | 270 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 270 insertions(+)

diff --git a/scripts/fliepaper_bugs_tick.js b/scripts/fliepaper_bugs_tick.js
new file mode 100644
index 0000000..3760767
--- /dev/null
+++ b/scripts/fliepaper_bugs_tick.js
@@ -0,0 +1,270 @@
+#!/usr/bin/env node
+/**
+ * fliepaper-bugs single-tick generator.
+ *
+ * Run for ONE tick (≤8 minutes), generate up to N PENDING cells, then exit.
+ * Wraps a generation loop driven by the same architecture as
+ * imperial_garden_colorways_batch.js, with these differences:
+ *
+ *   - Text-to-image (no source image to recolor); each pattern × colorway is
+ *     synthesized from the composed prompt in fliepaper-bugs.js.
+ *   - Settlement gate runs BEFORE every Gemini call — the inline check
+ *     from src/fliepaper-bugs.js implements the binding rules from
+ *     ~/.claude/skills/settlement/SKILL.md (butterflies/birds/bananas/grapes
+ *     are BLOCK; foliage is NEEDS_REVIEW; insect-only prompts are OK).
+ *   - Patterns 7 and 8 (Praying Mantis Garden, Cicada Terracotta) and the
+ *     "Vintage Rose" palette alt are RESERVED for an SS27 drop and are NOT
+ *     present in spec.json, so the spec walk skips them naturally.
+ *   - PNGs persist to BOTH:
+ *       data/fliepaper-bugs/<pattern>/<colorway>.png  (canonical per spec)
+ *       data/generated/fliepaper-bugs_<pattern>_<colorway>.png  (served at /designs/img/)
+ *   - On success: insert spoon_all_designs row (kind=seamless_tile,
+ *     category=fliepaper-bugs, vendor/owned_by_steve via tags), update the
+ *     fliepaper_bugs_cells row (status=GENERATED, design_id, thumbnail_url,
+ *     generated_at), and commit with the user-specified message.
+ *   - NEVER pushes to Shopify. Steve handles Shopify push separately.
+ *
+ * Tick budget: 8 minutes total. Each Gemini call ≈ 15-40s. Default N=3.
+ *
+ * Usage:
+ *   cd ~/Projects/wallco-ai && node scripts/fliepaper_bugs_tick.js [N]
+ */
+'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 { settlementCheck, composeCellPrompt } = require(path.join(__dirname, '..', 'src', 'fliepaper-bugs.js'));
+
+const KEY = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
+if (!KEY) { console.error('GEMINI_API_KEY / GOOGLE_API_KEY missing'); process.exit(1); }
+
+const ROOT      = path.join(__dirname, '..');
+const SPEC_PATH = path.join(ROOT, 'data', 'fliepaper-bugs', 'spec.json');
+const DATA_DIR  = path.join(ROOT, 'data', 'fliepaper-bugs');
+const IMG_DIR   = path.join(ROOT, 'data', 'generated');
+fs.mkdirSync(IMG_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 SPEC = JSON.parse(fs.readFileSync(SPEC_PATH, 'utf8'));
+const RESERVED_PATTERN_SLUGS  = new Set(['praying-mantis-garden', 'cicada-terracotta']);
+const RESERVED_COLORWAY_SLUGS = new Set(['vintage-rose']);
+
+const SHARED_TAIL = [
+  'Seamless tile repeat with no edge artifacts.',
+  'Luxury wallcovering / fashion-print quality, archival, no cartoon style.',
+  'No watermark, no signature, no text, no logos.',
+  'Square aspect ratio, even illumination, flat-paper rendering (no perspective, no room mockup).',
+].join(' ');
+
+function pickPendingCells(limit) {
+  const sql = `
+    SELECT row_to_json(t) FROM (
+      SELECT id, pattern_slug, colorway_slug
+        FROM fliepaper_bugs_cells
+       WHERE collection_slug='fliepaper-bugs'
+         AND status='PENDING'
+         AND design_id IS NULL
+       ORDER BY pattern_slug, colorway_slug
+       LIMIT ${limit}
+    ) t;
+  `;
+  const raw = psql(sql);
+  if (!raw) return [];
+  return raw.split('\n').filter(Boolean).map(l => JSON.parse(l));
+}
+
+async function generateOne(cell) {
+  const pattern  = SPEC.patterns.find(p => p.slug === cell.pattern_slug);
+  const colorway = SPEC.palette .find(c => c.slug === cell.colorway_slug);
+  if (!pattern || !colorway) throw new Error(`spec mismatch for cell ${cell.id}`);
+  if (RESERVED_PATTERN_SLUGS .has(pattern.slug))  throw new Error(`pattern ${pattern.slug} is RESERVED for SS27`);
+  if (RESERVED_COLORWAY_SLUGS.has(colorway.slug)) throw new Error(`colorway ${colorway.slug} is RESERVED for SS27`);
+
+  const promptBody = composeCellPrompt(pattern, colorway);
+  const verdict = settlementCheck(promptBody);
+
+  psql(`UPDATE fliepaper_bugs_cells
+           SET settlement_verdict=${esc(JSON.stringify(verdict))}::jsonb,
+               updated_at=now()
+         WHERE id=${cell.id};`);
+  if (verdict.verdict === 'BLOCK') {
+    psql(`UPDATE fliepaper_bugs_cells
+             SET status='SETTLEMENT_BLOCKED',
+                 last_error=${esc('Settlement BLOCK: ' + verdict.reason)},
+                 updated_at=now()
+           WHERE id=${cell.id};`);
+    throw new Error(`SETTLEMENT BLOCK — ${verdict.reason}`);
+  }
+  if (verdict.verdict !== 'OK') {
+    console.log(`    settlement ${verdict.verdict} — ${verdict.reason}`);
+  }
+
+  const fullPrompt = promptBody + ' ' + SHARED_TAIL;
+  const body = {
+    contents: [{ parts: [{ 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(`Gemini 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: `fliepaper-bugs/${pattern.slug}/${colorway.slug}`, 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)}`);
+  }
+
+  // ── Persist both canonical path + served path ────────────────────────
+  const canonicalDir  = path.join(DATA_DIR, pattern.slug);
+  fs.mkdirSync(canonicalDir, { recursive: true });
+  const canonicalPath = path.join(canonicalDir, `${colorway.slug}.png`);
+  const buf = Buffer.from(data, 'base64');
+  fs.writeFileSync(canonicalPath, buf);
+
+  const servedFilename = `fliepaper-bugs_${pattern.slug}_${colorway.slug}.png`;
+  const servedPath = path.join(IMG_DIR, servedFilename);
+  fs.writeFileSync(servedPath, buf);
+
+  const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
+
+  // ── Palette extraction ───────────────────────────────────────────────
+  const py = spawnSync('python3', ['-c', `
+from PIL import Image
+from collections import Counter
+import json, sys
+img = Image.open(sys.argv[1]).convert('RGB')
+img.thumbnail((300, 300))
+pal = img.quantize(colors=5, method=Image.Quantize.MEDIANCUT).convert('RGB')
+pixels = list(pal.getdata())
+cnt = Counter(pixels)
+total = sum(cnt.values())
+palette = [{'hex':'#{:02x}{:02x}{:02x}'.format(*rgb), 'pct':round(n/total*100,2)} for rgb,n in cnt.most_common(5)]
+print(json.dumps(palette))
+`, servedPath], { encoding: 'utf8' });
+  let palette = []; let dominant = null;
+  try { palette = JSON.parse(py.stdout.trim()); dominant = palette[0]?.hex; } catch {}
+
+  // ── Insert spoon_all_designs row (is_published=false; Steve reviews/pushes) ──
+  const seed = crypto.randomInt(1, 2 ** 31 - 1);
+  const title = `Fliepaper Bugs · ${pattern.name} · ${colorway.name}`;
+  const promptDesc = (title + ' — ' + fullPrompt).slice(0, 1800);
+  const palJsonSql = "'" + JSON.stringify(palette).replace(/'/g, "''") + "'::jsonb";
+  const tags = [
+    'fliepaper-bugs', pattern.slug, colorway.slug, pattern.bug,
+    'gucci-coord', 'owned-by-steve', 'designer-wallcoverings', colorway.role,
+  ];
+  const tagSql = 'ARRAY[' + tags.map(t => esc(t)).join(',') + ']::text[]';
+  const motifsSql = `ARRAY[${esc(pattern.bug)}, 'insect', 'wallcovering']::text[]`;
+
+  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',
+      ${esc(promptDesc)}, ${seed},
+      ${dominant ? "'" + dominant + "'" : 'NULL'},
+      ${palJsonSql},
+      ${esc(servedPath)}, ${esc('/designs/img/' + servedFilename)},
+      'fliepaper-bugs',
+      ${motifsSql},
+      ${tagSql},
+      FALSE, ${esc(`fliepaper-bugs ${pattern.slug} × ${colorway.slug}`)})
+    RETURNING id;`;
+  const designId = parseInt(psql(insertSql), 10);
+  if (!Number.isFinite(designId)) throw new Error('spoon_all_designs insert returned no id');
+
+  // ── Update the cell row ──────────────────────────────────────────────
+  psql(`UPDATE fliepaper_bugs_cells
+           SET status='GENERATED',
+               design_id=${designId},
+               thumbnail_url=${esc('/designs/img/' + servedFilename)},
+               generated_at=now(),
+               last_error=NULL,
+               updated_at=now()
+         WHERE id=${cell.id};`);
+
+  return {
+    cellId: cell.id, designId, pattern, colorway,
+    canonicalPath, servedPath, servedFilename, elapsed, dominant,
+  };
+}
+
+function gitCommit(pattern, colorway) {
+  const cwd = ROOT;
+  try {
+    execSync(`git -C ${cwd} add data/fliepaper-bugs/${pattern.slug}/${colorway.slug}.png data/generated/fliepaper-bugs_${pattern.slug}_${colorway.slug}.png 2>/dev/null || true`, { stdio: 'pipe' });
+    // Allow empty in case the generated/ file is in .gitignore — we still want
+    // the canonical data/fliepaper-bugs/ tree committed.
+    const msg = `wallco: fliepaper-bugs ${pattern.slug} ${colorway.slug} generated`;
+    execSync(`git -C ${cwd} commit -m ${JSON.stringify(msg)} 2>&1`, { stdio: 'pipe' });
+    return msg;
+  } catch (e) {
+    return null;
+  }
+}
+
+(async () => {
+  const N = Math.max(1, Math.min(8, parseInt(process.argv[2] || '3', 10)));
+  const TICK_BUDGET_MS = 8 * 60 * 1000;
+  const t0 = Date.now();
+  const deadline = t0 + TICK_BUDGET_MS - 30_000; // leave 30s for commit + exit
+
+  console.log(`\nfliepaper-bugs tick — target ${N} cells, budget ${(TICK_BUDGET_MS/1000)|0}s.`);
+
+  const cells = pickPendingCells(N);
+  if (cells.length === 0) {
+    console.log('  no PENDING cells — nothing to do this tick.');
+    process.exit(0);
+  }
+  console.log(`  ${cells.length} PENDING cells queued: ${cells.map(c => `${c.pattern_slug}/${c.colorway_slug}`).join(', ')}`);
+
+  const results = [];
+  for (let i = 0; i < cells.length; i++) {
+    if (Date.now() > deadline) {
+      console.log(`  budget exhausted after ${i} cells — exiting cleanly.`);
+      break;
+    }
+    const c = cells[i];
+    console.log(`  [${i + 1}/${cells.length}] ${c.pattern_slug} × ${c.colorway_slug}`);
+    try {
+      const out = await generateOne(c);
+      const commitMsg = gitCommit(out.pattern, out.colorway);
+      console.log(`      → design id=${out.designId}  ${out.elapsed}s  dom=${out.dominant || '—'}  commit=${commitMsg ? 'ok' : 'skipped'}`);
+      results.push(out);
+    } catch (e) {
+      console.error(`      FAIL — ${String(e.message).slice(0, 300)}`);
+      try {
+        psql(`UPDATE fliepaper_bugs_cells
+                 SET status='FAILED',
+                     last_error=${esc(String(e.message).slice(0, 1000))},
+                     updated_at=now()
+               WHERE id=${c.id};`);
+      } catch {}
+    }
+  }
+
+  const total = ((Date.now() - t0) / 1000).toFixed(1);
+  const remaining = parseInt(psql(`SELECT COUNT(*) FROM fliepaper_bugs_cells WHERE collection_slug='fliepaper-bugs' AND status='PENDING';`), 10);
+  console.log(`\n  ${results.length} generated this tick · ${total}s · ${remaining} PENDING cells remain.`);
+  process.exit(0);
+})().catch(e => { console.error(e); process.exit(1); });

← b5d6362 fliepaper-bugs: API + server.js mount — settlement-gated reg  ·  back to Wallco Ai  ·  settlement: add 'palm leaves' to negative-prompt language + 746cf5e →