[object Object]

← 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

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 →