[object Object]

← back to Designer Wallcoverings

auto-save: 2026-06-23T12:21:34 (3 files) — shopify/scripts/tt-room-dbg2.js shopify/tres-tintas-room-backups/_done.ledger shopify/tres-tintas-spec-backups/_done.ledger

3c95a884ff83ee8356d1fe92cd8e7d6b21fd8c8b · 2026-06-23 12:21:39 -0700 · Steve Abrams

Files touched

Diff

commit 3c95a884ff83ee8356d1fe92cd8e7d6b21fd8c8b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 23 12:21:39 2026 -0700

    auto-save: 2026-06-23T12:21:34 (3 files) — shopify/scripts/tt-room-dbg2.js shopify/tres-tintas-room-backups/_done.ledger shopify/tres-tintas-spec-backups/_done.ledger
---
 shopify/scripts/tt-room-dbg2.js               | 175 --------------------------
 shopify/tres-tintas-room-backups/_done.ledger | 166 ++++++++++++++++++++++++
 shopify/tres-tintas-spec-backups/_done.ledger |  22 ++++
 3 files changed, 188 insertions(+), 175 deletions(-)

diff --git a/shopify/scripts/tt-room-dbg2.js b/shopify/scripts/tt-room-dbg2.js
deleted file mode 100644
index 48694948..00000000
--- a/shopify/scripts/tt-room-dbg2.js
+++ /dev/null
@@ -1,175 +0,0 @@
-#!/usr/bin/env node
-/**
- * tres-tintas-room-settings.js
- *
- * Generates ONE photorealistic room-setting image per Tres Tintas product that
- * lacks one, using the EXACT catalog pattern (Replicate SDXL inpainting with the
- * pattern wall mask-LOCKED so SDXL can never alter the pattern — it only renders
- * the furniture/floor/light around it).
- *
- * Pipeline per product:
- *   1. Pull the product's pattern image + repeat (custom.pattern_repeat) from Shopify.
- *   2. Build a one-point-perspective room: exact pattern tiled on the back wall
- *      (with molding frame + baseboard), neutral floor/ceiling/side walls.
- *      Mask = black over the pattern wall (keep), white elsewhere (generate).
- *      -> delegates to build-room-composite.py (PIL).
- *   3. Replicate SDXL inpainting renders the room around the locked pattern.
- *   4. Upload the result as a 2nd product image (alt "Room Setting").
- *
- * Idempotent (ledger), error-tolerant (one product's failure is skipped, not
- * fatal), shows a running $ total. DRY RUN by default. APPLY=1 to write.
- *
- *   LIMIT=1 node tres-tintas-room-settings.js          # dry run, 1
- *   LIMIT=1 APPLY=1 node tres-tintas-room-settings.js  # write 1 (pilot)
- *   APPLY=1 node tres-tintas-room-settings.js          # full ~557 run
- */
-const fs = require('fs');
-const path = require('path');
-const { execFileSync } = require('child_process');
-
-const STORE = 'designer-laboratory-sandbox.myshopify.com';
-const API = '2024-01';
-const APPLY = process.env.APPLY === '1';
-const LIMIT = process.env.LIMIT ? parseInt(process.env.LIMIT, 10) : Infinity;
-const ROOM = process.env.ROOM || 'living_room';
-const REPLICATE_VERSION = 'a5b13068cc81a89a4fbeefeccc774869fcb34df4dbc92c1555e0f2771d49dde7';
-const COST_PER_IMG = 0.02;            // ~$0.02 per SDXL inpaint run (observed)
-const PY = fs.existsSync('/opt/homebrew/bin/python3') ? '/opt/homebrew/bin/python3' : 'python3';
-const COMPOSITE_PY = path.join(__dirname, 'build-room-composite.py');
-
-function envval(key) {
-  const env = fs.readFileSync(path.join(process.env.HOME, 'Projects/secrets-manager/.env'), 'utf8');
-  const m = env.match(new RegExp(`^${key}=["']?([^"'\\n]+)`, 'm'));
-  return m ? m[1].trim() : null;
-}
-const TOKEN = envval('SHOPIFY_DRAFT_TOKEN');
-const RTOKEN = envval('REPLICATE_API_TOKEN');
-const H = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
-
-const TMP = '/tmp/tt-rooms';
-fs.mkdirSync(TMP, { recursive: true });
-const BACKUP_DIR = path.join(__dirname, '..', 'tres-tintas-room-backups');
-const LEDGER = path.join(BACKUP_DIR, '_done.ledger');
-fs.mkdirSync(BACKUP_DIR, { recursive: true });
-const done = new Set(fs.existsSync(LEDGER) ? fs.readFileSync(LEDGER, 'utf8').split('\n').filter(Boolean) : []);
-
-const sleep = (ms) => new Promise(r => setTimeout(r, ms));
-// Per-request timeout so a half-open socket can never wedge the batch (the
-// 2026-06-23 25-min hang). Default 60s; uploads/large posts get more headroom.
-async function fetchRetry(url, opts, tries = 3, timeoutMs = 60000) {
-  let e;
-  for (let i = 0; i < tries; i++) {
-    try {
-      return await fetch(url, { ...opts, signal: AbortSignal.timeout(timeoutMs) });
-    } catch (err) { e = err; await sleep(800 * (i + 1)); }
-  }
-  throw e;
-}
-async function gql(query, variables) {
-  // Route through fetchRetry so the paginated scan can't wedge on a half-open
-  // socket (the bare-fetch hole that hung the 2026-06-23 relaunch with 0 output).
-  const r = await fetchRetry(`https://${STORE}/admin/api/${API}/graphql.json`, { method: 'POST', headers: H, body: JSON.stringify({ query, variables }) });
-  const j = await r.json(); if (j.errors) throw new Error(JSON.stringify(j.errors)); return j.data;
-}
-
-// inches from "23.6 in / 60 cm" -> 23.6 ; default 19.7
-function repeatInches(v) { const m = /([\d.]+)\s*in/i.exec(v || ''); return m ? parseFloat(m[1]) : 19.7; }
-
-async function replicateInpaint(initPath, maskPath, outPath) {
-  const dataUri = (p) => 'data:image/png;base64,' + fs.readFileSync(p).toString('base64');
-  const body = { version: REPLICATE_VERSION, input: {
-    image: dataUri(initPath), mask: dataUri(maskPath),
-    prompt: 'photorealistic modern living room interior, comfortable fabric sofa, wooden coffee table, area rug, floor lamps, large window with soft natural daylight, the back feature wall has geometric patterned wallcovering framed by white crown molding and baseboard, warm professional architectural interior photography, high detail, realistic shadows and lighting',
-    negative_prompt: 'blurry, low quality, distorted, cartoon, painting, sketch, oversaturated, warped walls, extra walls, text, watermark',
-    strength: 0.99, steps: 30, guidance_scale: 8, num_outputs: 1 } };
-  let j = await (await fetchRetry('https://api.replicate.com/v1/predictions', { method: 'POST', headers: { Authorization: `Bearer ${RTOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) })).json();
-  if (j.error) throw new Error('replicate create: ' + JSON.stringify(j.error));
-  const url = j.urls.get;
-  const deadline = Date.now() + 5 * 60 * 1000; // hard 5-min wall-clock cap per prediction
-  for (let i = 0; i < 90; i++) {
-    if (Date.now() > deadline) throw new Error('replicate wall-clock timeout');
-    await sleep(2000);
-    const pr = await (await fetchRetry(url, { headers: { Authorization: `Bearer ${RTOKEN}` } })).json();
-    if (pr.status === 'succeeded') { const out = Array.isArray(pr.output) ? pr.output[0] : pr.output; const img = await (await fetchRetry(out, {}, 3, 120000)).arrayBuffer(); fs.writeFileSync(outPath, Buffer.from(img)); return; }
-    if (pr.status === 'failed' || pr.status === 'canceled') throw new Error('replicate ' + pr.status + ': ' + JSON.stringify(pr.error));
-  }
-  throw new Error('replicate poll timeout');
-}
-
-async function uploadImage(productId, filePath, alt) {
-  const b64 = fs.readFileSync(filePath).toString('base64');
-  const r = await fetchRetry(`https://${STORE}/admin/api/${API}/products/${productId}/images.json`, {
-    method: 'POST', headers: H, body: JSON.stringify({ image: { attachment: b64, filename: `${productId}-room-${ROOM}.png`, alt } }) }, 3, 120000);
-  const j = await r.json();
-  if (!j.image) throw new Error('shopify upload: ' + JSON.stringify(j.errors || j));
-  return j.image.id;
-}
-
-async function main() {
-  if (!TOKEN || !RTOKEN) throw new Error('missing SHOPIFY_DRAFT_TOKEN or REPLICATE_API_TOKEN');
-  console.log(`MODE: ${APPLY ? 'APPLY (live)' : 'DRY RUN'}  room=${ROOM}${LIMIT !== Infinity ? `  LIMIT=${LIMIT}` : ''}  est $${COST_PER_IMG}/img`);
-  // first:100 (matches the desc/spec scripts that enumerate all 592 cleanly).
-  // first:60 hit a Shopify frozen-endCursor pagination bug -> only saw 120.
-  const q = `query($c:String){products(first:100,query:"vendor:'Tres Tintas'",after:$c){pageInfo{hasNextPage endCursor}
-    edges{node{id title images(first:10){edges{node{url}}}} } }}`;
-
-  let cursor = null, scanned = 0, need = 0, processed = 0, ok = 0, errs = 0, spend = 0;
-  while (true) {
-    const conn = (await gql(q, { cursor })).products;
-    for (const e of conn.edges) {
-      const n = e.node; scanned++;
-      const id = n.id.split('/').pop();
-      const imgs = n.images.edges.map(x => x.node.url);
-      const hasRoom = imgs.some(u => /interior|_int|room|ambiente|setting/i.test(u));
-      if (imgs.length > 1 || hasRoom) continue;   // already has a 2nd/room image
-      need++;
-      if (done.has(id)) continue;
-      if (processed >= LIMIT) continue;
-
-      const patternUrl = imgs[0];
-      if (!patternUrl) { console.log(`  !! ${id} no pattern image — SKIP`); continue; }
-
-      try {
-        // fetch repeat for tile scale
-        const mfRes = await fetchRetry(`https://${STORE}/admin/api/${API}/products/${id}/metafields.json`, { headers: H });
-        const mfs = (await mfRes.json()).metafields || [];
-        const rep = repeatInches((mfs.find(m => m.namespace === 'custom' && m.key === 'pattern_repeat') || {}).value);
-
-        // download pattern
-        const patPath = `${TMP}/${id}-pat.jpg`;
-        const pbuf = await (await fetchRetry(patternUrl, {})).arrayBuffer();
-        fs.writeFileSync(patPath, Buffer.from(pbuf));
-
-        const initPath = `${TMP}/${id}-init.png`, maskPath = `${TMP}/${id}-mask.png`, outPath = `${TMP}/${id}-room.png`;
-        execFileSync(PY, [COMPOSITE_PY, patPath, initPath, maskPath, String(rep)], { stdio: 'pipe' });
-
-        processed++;
-        if (!APPLY) { console.log(`  [dry] ${id} ${n.title}  repeat=${rep}in  -> would inpaint + upload`); continue; }
-
-        await replicateInpaint(initPath, maskPath, outPath);
-        spend += COST_PER_IMG;
-        const imgId = await uploadImage(id, outPath, `Room Setting - ${n.title.replace(/\s*\|.*$/, '')}`);
-        ok++;
-        fs.appendFileSync(LEDGER, id + '\n'); done.add(id);
-        console.log(`  ✓ ${id} ${n.title}  img=${imgId}  [${ok} done · ~$${spend.toFixed(2)}]`);
-        await sleep(300);
-      } catch (err) {
-        errs++;
-        console.log(`  ✗ ${id} ${n.title}: ${err.message} — SKIP (retry next run)`);
-        await sleep(500);
-      }
-    }
-    // Shopify can return hasNextPage:true with a FROZEN endCursor at the
-    // result-set boundary -> infinite same-page loop (the 2026-06-23 wedge).
-    // Break if the cursor doesn't advance.
-    const next = conn.pageInfo.endCursor;
-    console.error("DBG page: edges="+conn.edges.length+" hasNext="+conn.pageInfo.hasNextPage+" scanned="+scanned+" frozen="+(next===cursor));
-    if (conn.pageInfo.hasNextPage && processed < LIMIT && next && next !== cursor) cursor = next; else break;
-  }
-
-  console.log(`\n--- SUMMARY ---`);
-  console.log(`scanned: ${scanned} | need-room: ${need} | processed: ${processed} | uploaded: ${ok} | errors: ${errs}`);
-  console.log(`Replicate spend: ~$${spend.toFixed(2)} (${ok} imgs × $${COST_PER_IMG})`);
-  if (APPLY) console.log(`ledger -> ${LEDGER}`);
-}
-main().catch(e => { console.error(e); process.exit(1); });
diff --git a/shopify/tres-tintas-room-backups/_done.ledger b/shopify/tres-tintas-room-backups/_done.ledger
index b6efb93d..10436686 100644
--- a/shopify/tres-tintas-room-backups/_done.ledger
+++ b/shopify/tres-tintas-room-backups/_done.ledger
@@ -24,3 +24,169 @@
 7863633543219
 7863633575987
 7863636197427
+7863633608755
+7863633641523
+7863633674291
+7863633707059
+7863633739827
+7863633772595
+7863633805363
+7863633870899
+7863633903667
+7863633936435
+7863633969203
+7863634001971
+7863634034739
+7863634067507
+7863634100275
+7863634133043
+7863634165811
+7863634198579
+7863634231347
+7863634329651
+7863634362419
+7863634395187
+7863634427955
+7863634460723
+7863634493491
+7863634526259
+7863634559027
+7863634591795
+7863634624563
+7863634657331
+7863634722867
+7863634755635
+7863634788403
+7863634821171
+7863634853939
+7863634886707
+7863634919475
+7863634952243
+7863634985011
+7863635017779
+7863635050547
+7863635083315
+7863635116083
+7863635148851
+7863635181619
+7863635214387
+7863635247155
+7863635279923
+7863635312691
+7863635345459
+7863635378227
+7863635410995
+7863635443763
+7863635476531
+7863635509299
+7863635574835
+7863635607603
+7863635640371
+7863635673139
+7863635705907
+7863635738675
+7863635771443
+7863635804211
+7863635836979
+7863635869747
+7863635935283
+7863635968051
+7863636000819
+7863636033587
+7863636066355
+7863636099123
+7863636131891
+7863636164659
+7863636230195
+7863636262963
+7863636295731
+7863636328499
+7863636361267
+7863636426803
+7863636459571
+7863636492339
+7863636525107
+7863636557875
+7863636590643
+7863636656179
+7863636688947
+7863636721715
+7863636754483
+7863636787251
+7863636820019
+7863636852787
+7863636885555
+7863636918323
+7863636951091
+7863636983859
+7863637016627
+7863637049395
+7863637082163
+7863637114931
+7863637147699
+7863637180467
+7863637213235
+7863637278771
+7863637377075
+7863637475379
+7863637540915
+7863637639219
+7863637671987
+7863637737523
+7863637770291
+7863637803059
+7863637835827
+7863637868595
+7863637901363
+7863637934131
+7863637966899
+7863637999667
+7863638032435
+7863638065203
+7863638097971
+7863638130739
+7863638163507
+7863638229043
+7863638261811
+7863638294579
+7863638327347
+7863638360115
+7863638392883
+7863638458419
+7863638491187
+7863638523955
+7863638556723
+7863638589491
+7863638622259
+7863638655027
+7863638687795
+7863638720563
+7863638753331
+7863638786099
+7863638818867
+7863638851635
+7863638884403
+7863638917171
+7863638949939
+7863638982707
+7863639015475
+7863639048243
+7863639081011
+7863639146547
+7863639179315
+7863639212083
+7863639244851
+7863639310387
+7863639343155
+7863639375923
+7863639408691
+7863639441459
+7863639474227
+7863639506995
+7863639539763
+7863639572531
+7863639605299
+7863639638067
+7863639670835
+7863639703603
+7863639736371
diff --git a/shopify/tres-tintas-spec-backups/_done.ledger b/shopify/tres-tintas-spec-backups/_done.ledger
index c4569b01..bdaf1a60 100644
--- a/shopify/tres-tintas-spec-backups/_done.ledger
+++ b/shopify/tres-tintas-spec-backups/_done.ledger
@@ -552,3 +552,25 @@
 7866357907507
 7866357940275
 7866357973043
+7433013624883
+7433013690419
+7433013821491
+7433014050867
+7433014149171
+7433014313011
+7433014345779
+7433014640691
+7433014673459
+7433014706227
+7433014738995
+7433014771763
+7433014804531
+7433014837299
+7433014870067
+7433014902835
+7433014935603
+7433014968371
+7433015001139
+7433015066675
+7433015558195
+7866125025331

← 72762a60 Add gated live-push + rollback scripts + candidate snapshot  ·  back to Designer Wallcoverings  ·  Audit: repeat+match are OPTIONAL specs (plain/free-match rol d7042d8c →