← back to Wallco Ai
extend-luxe-curator — backfill C + queue A/E for reverted-blanket roots
aceea323bd85c243f92d8c339dcf77bc9f904523 · 2026-05-25 09:11:51 -0700 · Steve Abrams
Steve-authorized $20.72 run: for the 259 drunk-animals roots that have
exactly ONE existing luxe-v2 child (the original variant-C from the
reverted blanket roll-luxe-c rollout 2026-05-25), extend the curator
queue with A + E so Steve can pick from all 3 heritage variants per
root instead of regenerating C from scratch.
Two phases:
1. Backfill — for each target root, append a synthetic queue entry
marking the existing C luxe as { variant:'C', new_id:<existing_id>,
ground:<reconstructed-from-rotation>, ok:true, backfill:true }.
Ground reconstructed via the same `root_id % 5` rotation that
scripts/roll-luxe-c.js used (BLANKET_GROUNDS = silk/cork/grasscloth/
linen/raffia) — PG doesn't store the ground, but the original
rotation was deterministic so we can compute it.
2. Generate — for each target root, fan parallel gen-luxe.js calls for
variants A (raffia) and E (grasscloth) at concurrency 4. Idempotent
per (root, variant): skips combos where an OK entry already exists
in luxe-curator-queue.jsonl. Failed entries are NOT counted as
"done" so retries pick them up.
Skips roots with 0, 2, 3, or 4 existing luxe children:
- 0 = handled by queue-luxe-curator.js (different runner)
- 2 = edge cases needing manual cleanup
- 3 = already have A+C+E from overnight batch1
- 4 = the regen test root, already covered
Cost: ~$0.04 × 518 calls = ~$20.72 estimate. Wall ~19 min at
concurrency 4 / Gemini Flash Image ~9s median.
After this run completes, /luxe-curator.html will show 305 rows
(259 newly-extended + 46 already-complete) with full A/C/E triples.
Files touched
A scripts/extend-luxe-curator.js
Diff
commit aceea323bd85c243f92d8c339dcf77bc9f904523
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon May 25 09:11:51 2026 -0700
extend-luxe-curator — backfill C + queue A/E for reverted-blanket roots
Steve-authorized $20.72 run: for the 259 drunk-animals roots that have
exactly ONE existing luxe-v2 child (the original variant-C from the
reverted blanket roll-luxe-c rollout 2026-05-25), extend the curator
queue with A + E so Steve can pick from all 3 heritage variants per
root instead of regenerating C from scratch.
Two phases:
1. Backfill — for each target root, append a synthetic queue entry
marking the existing C luxe as { variant:'C', new_id:<existing_id>,
ground:<reconstructed-from-rotation>, ok:true, backfill:true }.
Ground reconstructed via the same `root_id % 5` rotation that
scripts/roll-luxe-c.js used (BLANKET_GROUNDS = silk/cork/grasscloth/
linen/raffia) — PG doesn't store the ground, but the original
rotation was deterministic so we can compute it.
2. Generate — for each target root, fan parallel gen-luxe.js calls for
variants A (raffia) and E (grasscloth) at concurrency 4. Idempotent
per (root, variant): skips combos where an OK entry already exists
in luxe-curator-queue.jsonl. Failed entries are NOT counted as
"done" so retries pick them up.
Skips roots with 0, 2, 3, or 4 existing luxe children:
- 0 = handled by queue-luxe-curator.js (different runner)
- 2 = edge cases needing manual cleanup
- 3 = already have A+C+E from overnight batch1
- 4 = the regen test root, already covered
Cost: ~$0.04 × 518 calls = ~$20.72 estimate. Wall ~19 min at
concurrency 4 / Gemini Flash Image ~9s median.
After this run completes, /luxe-curator.html will show 305 rows
(259 newly-extended + 46 already-complete) with full A/C/E triples.
---
scripts/extend-luxe-curator.js | 235 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 235 insertions(+)
diff --git a/scripts/extend-luxe-curator.js b/scripts/extend-luxe-curator.js
new file mode 100644
index 0000000..dd754d4
--- /dev/null
+++ b/scripts/extend-luxe-curator.js
@@ -0,0 +1,235 @@
+#!/usr/bin/env node
+// extend-luxe-curator — for drunk-animals roots that have EXACTLY ONE
+// existing luxe-v2 child (the original variant-C from the reverted
+// blanket roll-luxe-c rollout), backfill that C into the curator queue
+// file then generate the missing variants A + E. Lets Steve curate
+// the full A/C/E triple per root instead of regenerating the existing C.
+//
+// Skips:
+// - roots with 0 luxe children (covered by queue-luxe-curator.js)
+// - roots with 2+ luxe children (already had partial overnight coverage)
+//
+// Cost: ~$0.08 per root × ~259 = ~$20.72. Time at concurrency 4 ≈ 19 min.
+//
+// Usage: node scripts/extend-luxe-curator.js [--category=drunk-animals]
+// [--concurrency=4] [--dry-run]
+// [--limit=N]
+
+const fs = require('fs');
+const path = require('path');
+const { spawn, spawnSync } = require('child_process');
+const { extractSubjectsFromPrompt, detectParentReference } = require('../lib/subject-detector');
+
+const ARG = (k, dflt) => {
+ const a = process.argv.find(x => x.startsWith(`--${k}=`));
+ return a ? a.split('=')[1] : dflt;
+};
+const FLAG = (k) => process.argv.includes(`--${k}`);
+
+const CATEGORY = ARG('category', 'drunk-animals');
+const LIMIT = parseInt(ARG('limit', '0'), 10);
+const CONCURRENCY = parseInt(ARG('concurrency', '4'), 10);
+const DRY = FLAG('dry-run');
+
+const ROOT_DIR = path.join(__dirname, '..');
+const LOG = path.join(ROOT_DIR, 'data', 'luxe-curator-queue.jsonl');
+
+// The grounds rotation that scripts/roll-luxe-c.js used. Reconstructed
+// here so backfilled C entries have an accurate ground field even though
+// PG doesn't store it. Source: roll-luxe-c.js GROUNDS const + the rotation
+// `GROUNDS[r.rootId % GROUNDS.length]`.
+const BLANKET_GROUNDS = ['silk', 'cork', 'grasscloth', 'linen', 'raffia'];
+
+// Canonical ground mapping for new A/E variants (mirrors queue-luxe-curator.js
+// VARIANT_GROUND map). A goes on raffia, E on grasscloth.
+const NEW_VARIANT_GROUND = { A: 'raffia', E: 'grasscloth' };
+const NEW_VARIANTS = ['A', 'E'];
+
+function psql(sql) {
+ const r = spawnSync('psql', ['dw_unified', '-At', '-q', '-F|'], { input: sql, encoding: 'utf8' });
+ if (r.status !== 0) throw new Error(r.stderr || 'psql failed');
+ return r.stdout.trim();
+}
+
+// Find drunk-animals roots that have exactly ONE luxe-v2 child + return
+// the existing child id so we can backfill it.
+function pickRoots() {
+ const sql = `
+ WITH per_root AS (
+ SELECT parent_design_id AS root_id,
+ MIN(id) AS existing_luxe_id,
+ COUNT(*) AS n_luxe
+ FROM spoon_all_designs
+ WHERE generator = 'gemini-2.5-flash-image-luxe-v2'
+ AND parent_design_id IS NOT NULL
+ GROUP BY parent_design_id
+ HAVING COUNT(*) = 1
+ )
+ SELECT pr.root_id, pr.existing_luxe_id,
+ regexp_replace(s.prompt, E'\\n|\\r', ' ', 'g') AS prompt
+ FROM per_root pr
+ JOIN spoon_all_designs s ON s.id = pr.root_id
+ WHERE s.category = '${CATEGORY.replace(/'/g, "''")}'
+ AND s.prompt IS NOT NULL
+ ORDER BY pr.root_id
+ ${LIMIT > 0 ? `LIMIT ${LIMIT}` : ''}
+ `;
+ const raw = psql(sql);
+ if (!raw) return [];
+ return raw.split('\n').map(line => {
+ const [rootId, existingLuxeId, ...promptParts] = line.split('|');
+ return {
+ rootId: parseInt(rootId, 10),
+ existingLuxeId: parseInt(existingLuxeId, 10),
+ prompt: promptParts.join('|'),
+ };
+ }).filter(r => Number.isFinite(r.rootId));
+}
+
+// Same as queue-luxe-curator: walk meta-prompt references ("auto-variation
+// hue+60 of design #N") up to the real subject prompt before extracting motif.
+function resolvePrompt(prompt, depth = 0) {
+ if (depth >= 4) return prompt;
+ const parent = detectParentReference(prompt);
+ if (!parent) return prompt;
+ const out = psql(`SELECT prompt FROM spoon_all_designs WHERE id=${parent}`);
+ if (!out) return prompt;
+ return resolvePrompt(out, depth + 1);
+}
+
+function extractMotif(rootPrompt, len = 300) {
+ let m = rootPrompt.trim().slice(0, len);
+ const dot = m.indexOf('. ');
+ if (dot > 60 && dot < m.length - 1) m = m.slice(0, dot);
+ return m.trim();
+}
+
+// Read existing queue file once into a Set of (root_id|variant) keys we
+// already have entries for — used to skip both backfill and new-gen on
+// already-queued combos.
+function loadExistingPairs() {
+ // Only count SUCCESSFUL (root,variant) pairs. Failed entries should be
+ // retried on the next run, not skipped.
+ const pairs = new Set();
+ if (!fs.existsSync(LOG)) return pairs;
+ for (const line of fs.readFileSync(LOG, 'utf8').split('\n')) {
+ if (!line.trim()) continue;
+ try {
+ const r = JSON.parse(line);
+ if (r.ok && r.root_id && r.variant) pairs.add(`${r.root_id}|${r.variant}`);
+ } catch {}
+ }
+ return pairs;
+}
+
+function appendQueueEntry(entry) {
+ fs.appendFileSync(LOG, JSON.stringify(entry) + '\n');
+}
+
+function runGenLuxe(rootId, motif, variant, ground) {
+ return new Promise((resolve) => {
+ const args = [
+ path.join(__dirname, 'gen-luxe.js'),
+ String(rootId), motif,
+ `--variant=${variant}`,
+ `--texture-cat=${ground}`,
+ `--curator-mode`,
+ ];
+ const t0 = Date.now();
+ const child = spawn('node', args, { cwd: ROOT_DIR, env: process.env });
+ let stdout = '', stderr = '';
+ child.stdout.on('data', d => { stdout += d; });
+ child.stderr.on('data', d => { stderr += d; });
+ child.on('close', (code) => {
+ const durationMs = Date.now() - t0;
+ const m = stdout.match(/\[gen-luxe\] PG id: (\d+)/);
+ const newId = m ? parseInt(m[1], 10) : null;
+ resolve({ ok: code === 0 && newId !== null, code, newId, durationMs, stderr: stderr.slice(-300) });
+ });
+ });
+}
+
+(async () => {
+ const roots = pickRoots();
+ const existingPairs = loadExistingPairs();
+ const backfillNeeded = roots.filter(r => !existingPairs.has(`${r.rootId}|C`));
+ const genJobs = [];
+ for (const r of roots) {
+ for (const v of NEW_VARIANTS) {
+ if (!existingPairs.has(`${r.rootId}|${v}`)) {
+ genJobs.push({ rootId: r.rootId, variant: v, ground: NEW_VARIANT_GROUND[v], prompt: r.prompt });
+ }
+ }
+ }
+ const estCost = (genJobs.length * 0.04).toFixed(2);
+ console.log(`[extend-luxe-curator] category=${CATEGORY} target_roots=${roots.length}`);
+ console.log(` backfill C entries needed: ${backfillNeeded.length}`);
+ console.log(` new gen jobs (A+E): ${genJobs.length} → est cost $${estCost}`);
+
+ if (DRY) {
+ console.log('\n--- DRY RUN sample (first 4) ---');
+ for (const r of roots.slice(0, 4)) {
+ const ground = BLANKET_GROUNDS[r.rootId % BLANKET_GROUNDS.length];
+ const resolvedPrompt = resolvePrompt(r.prompt);
+ const motif = extractMotif(resolvedPrompt, 90);
+ console.log(` root #${r.rootId} (existing C luxe #${r.existingLuxeId}, ground=${ground})`);
+ console.log(` motif: "${motif}…"`);
+ console.log(` → will queue A(raffia) + E(grasscloth)`);
+ }
+ return;
+ }
+
+ // Step 1: backfill existing C entries
+ for (const r of backfillNeeded) {
+ const ground = BLANKET_GROUNDS[r.rootId % BLANKET_GROUNDS.length];
+ const resolvedPrompt = resolvePrompt(r.prompt);
+ const motif = extractMotif(resolvedPrompt);
+ appendQueueEntry({
+ ts: new Date().toISOString(),
+ backfill: true, // marker — not from a real run
+ root_id: r.rootId,
+ variant: 'C',
+ ground,
+ motif: motif.slice(0, 200),
+ ok: true,
+ new_id: r.existingLuxeId,
+ duration_ms: 0,
+ });
+ }
+ console.log(`[extend-luxe-curator] backfilled ${backfillNeeded.length} existing C entries into queue`);
+
+ // Step 2: fan out gen jobs
+ if (!genJobs.length) {
+ console.log('[extend-luxe-curator] no new variants to generate');
+ return;
+ }
+ const startedAt = Date.now();
+ let okCount = 0, errCount = 0, i = 0;
+
+ async function worker(wid) {
+ while (genJobs.length) {
+ const j = genJobs.shift();
+ if (!j) return;
+ const idx = ++i;
+ const resolvedPrompt = resolvePrompt(j.prompt);
+ const motif = extractMotif(resolvedPrompt);
+ const res = await runGenLuxe(j.rootId, motif, j.variant, j.ground);
+ const entry = {
+ ts: new Date().toISOString(),
+ worker: wid, idx,
+ root_id: j.rootId, variant: j.variant, ground: j.ground,
+ motif: motif.slice(0, 200),
+ ok: res.ok, new_id: res.newId, duration_ms: res.durationMs,
+ ...(res.ok ? {} : { code: res.code, stderr: res.stderr }),
+ };
+ if (res.ok) okCount++; else errCount++;
+ appendQueueEntry(entry);
+ const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
+ console.log(`[w${wid}] ${idx} root=${j.rootId} ${j.variant} (${j.ground}) → ${res.ok ? '✓ #' + res.newId : '✗ code=' + res.code} (${(res.durationMs/1000).toFixed(1)}s) · totals ok=${okCount} err=${errCount} · ${elapsed}s wall`);
+ }
+ }
+
+ await Promise.all(Array.from({ length: CONCURRENCY }, (_, k) => worker(k + 1)));
+ const totalSec = ((Date.now() - startedAt) / 1000).toFixed(1);
+ console.log(`\n[extend-luxe-curator] done · ok=${okCount} err=${errCount} actual_cost≈$${(okCount * 0.04).toFixed(2)} · ${totalSec}s wall`);
+})().catch(e => { console.error('FATAL', e.stack || e.message); process.exit(1); });
← 467c1ec audit-room-seam: tighter masks + pattern-vs-seam disambiguat
·
back to Wallco Ai
·
16 product-page layout variations w/ live modals at /layouts be77282 →