[object Object]

← back to Wallco Ai

drunk-curator: inch ruler + stretch tile to 24/36/52in print width (buttons+slider, persists); room wall repeat scales to match

66faa13a3c2401a3e4498fa10c8ff9eeb1f040c5 · 2026-05-27 16:08:02 -0700 · Steve Abrams

Files touched

Diff

commit 66faa13a3c2401a3e4498fa10c8ff9eeb1f040c5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 27 16:08:02 2026 -0700

    drunk-curator: inch ruler + stretch tile to 24/36/52in print width (buttons+slider, persists); room wall repeat scales to match
---
 data/auto-decide-ledger.jsonl   | 75 +++++++++++++++++++++++++++++++++++++++++
 lib/composition-detector.js     | 23 +++++++++++--
 public/admin/drunk-curator.html | 64 ++++++++++++++++++++++++++++++++++-
 scripts/auto-decide-drunk.js    |  7 ++--
 4 files changed, 162 insertions(+), 7 deletions(-)

diff --git a/data/auto-decide-ledger.jsonl b/data/auto-decide-ledger.jsonl
index c50cd2f..c186d2c 100644
--- a/data/auto-decide-ledger.jsonl
+++ b/data/auto-decide-ledger.jsonl
@@ -2768,3 +2768,78 @@
 {"t":"2026-05-27T23:04:09.876Z","id":42289,"verdict":"hot_candidate","n":9,"see":"A repeating pattern of bird and floral motifs on a brown background."}
 {"t":"2026-05-27T23:04:12.789Z","id":42291,"verdict":"hot_candidate","n":9,"see":"A repeating floral motif on a blue background."}
 {"t":"2026-05-27T23:04:15.824Z","id":42292,"verdict":"hot_candidate","n":12,"see":"A repeating pattern of birds and foliage."}
+{"t":"2026-05-27T23:04:18.911Z","id":42293,"verdict":"hot_candidate","n":6,"see":"A repeating pattern of bird and flower motifs."}
+{"t":"2026-05-27T23:04:21.914Z","id":42294,"verdict":"hot_candidate","n":9,"see":"A repeating pattern of birds and flowers."}
+{"t":"2026-05-27T23:04:24.964Z","id":42295,"verdict":"hot_candidate","n":6,"see":"A repeating floral motif in a diamond grid."}
+{"t":"2026-05-27T23:04:27.988Z","id":42296,"verdict":"hot_candidate","n":6,"see":"A repeating floral motif in vertical stripes."}
+{"t":"2026-05-27T23:04:31.341Z","id":42297,"verdict":"hot_candidate","n":7,"see":"A repeating floral motif in vertical rows."}
+{"t":"2026-05-27T23:04:34.317Z","id":42298,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of birds on branches."}
+{"t":"2026-05-27T23:04:37.057Z","id":42299,"verdict":"hot_candidate","n":9,"see":"A repeating floral damask pattern."}
+{"t":"2026-05-27T23:04:40.041Z","id":42300,"verdict":"hot_candidate","n":6,"see":"A repeating floral motif on a gray background."}
+{"t":"2026-05-27T23:04:43.329Z","id":42301,"verdict":"hot_candidate","n":9,"see":"A repeating floral pattern with a brown background."}
+{"t":"2026-05-27T23:04:46.278Z","id":42302,"verdict":"hot_candidate","n":12,"see":"A repeating wallpaper pattern of birds on branches."}
+{"t":"2026-05-27T23:04:49.262Z","id":42303,"verdict":"hot_candidate","n":6,"see":"A repeating floral motif on a brown background."}
+{"t":"2026-05-27T23:04:52.775Z","id":42304,"verdict":"hot_candidate","n":4,"see":"A wallpaper tile with a repeating bird and floral motif."}
+{"t":"2026-05-27T23:04:55.768Z","id":42305,"verdict":"hot_candidate","n":7,"see":"A repeating floral motif on a brown background."}
+{"t":"2026-05-27T23:04:58.811Z","id":42306,"verdict":"hot_candidate","n":4,"see":"This is a wallpaper tile with a repeating bird and floral motif."}
+{"t":"2026-05-27T23:05:01.549Z","id":42307,"verdict":"hot_candidate","n":9,"see":"A repeating floral motif covers the tile."}
+{"t":"2026-05-27T23:05:05.351Z","id":42308,"verdict":"hot_candidate","n":7,"see":"A repeating floral damask pattern."}
+{"t":"2026-05-27T23:05:08.637Z","id":42309,"verdict":"hot_candidate","n":5,"see":"A repeating pattern of birds on branches with flowers."}
+{"t":"2026-05-27T23:05:11.513Z","id":42310,"verdict":"hot_candidate","n":6,"see":"A repeating floral pattern with a brown background."}
+{"t":"2026-05-27T23:05:14.473Z","id":42311,"verdict":"hot_candidate","n":6,"see":"A repeating pattern of birds and flowers."}
+{"t":"2026-05-27T23:05:17.374Z","id":42312,"verdict":"hot_candidate","n":4,"see":"A repeating floral motif within a diamond grid."}
+{"t":"2026-05-27T23:05:20.675Z","id":42313,"verdict":"hot_candidate","n":6,"see":"A repeating pattern of birds and floral motifs."}
+{"t":"2026-05-27T23:05:23.868Z","id":42314,"verdict":"hot_candidate","n":6,"see":"I see a repeating floral pattern."}
+{"t":"2026-05-27T23:05:26.929Z","id":42315,"verdict":"hot_candidate","n":6,"see":"I see a repeating floral pattern."}
+{"t":"2026-05-27T23:05:29.658Z","id":42319,"verdict":"hot_candidate","n":6,"see":"I see a repeating floral pattern."}
+{"t":"2026-05-27T23:05:32.443Z","id":42320,"verdict":"hot_candidate","n":6,"see":"A repeating floral motif on a gray background."}
+{"t":"2026-05-27T23:05:35.319Z","id":42321,"verdict":"hot_candidate","n":6,"see":"A repeating floral motif on a blue background."}
+{"t":"2026-05-27T23:05:38.181Z","id":42322,"verdict":"hot_candidate","n":5,"see":"I see a repeating pattern of birds on branches."}
+{"t":"2026-05-27T23:05:40.989Z","id":42323,"verdict":"hot_candidate","n":12,"see":"A repeating floral pattern with a diamond grid."}
+{"t":"2026-05-27T23:05:44.108Z","id":42324,"verdict":"hot_candidate","n":7,"see":"A repeating floral pattern with a gray background."}
+{"t":"2026-05-27T23:05:46.931Z","id":42325,"verdict":"hot_candidate","n":6,"see":"A repeating pattern of leafy branches."}
+{"t":"2026-05-27T23:05:49.947Z","id":42326,"verdict":"hot_candidate","n":25,"see":"A repeating floral pattern with a diamond grid."}
+{"t":"2026-05-27T23:05:52.997Z","id":42328,"verdict":"hot_candidate","n":4,"see":"A repeating floral pattern with blue and beige flowers."}
+{"t":"2026-05-27T23:05:56.140Z","id":42329,"verdict":"hot_candidate","n":6,"see":"A repeating floral damask pattern."}
+{"t":"2026-05-27T23:05:59.477Z","id":42330,"verdict":"hot_candidate","n":6,"see":"A repeating wallpaper pattern with birds and floral motifs."}
+{"t":"2026-05-27T23:06:02.331Z","id":42332,"verdict":"hot_candidate","n":4,"see":"A repeating wallpaper pattern with birds and flowers."}
+{"t":"2026-05-27T23:06:05.332Z","id":42333,"verdict":"hot_candidate","n":9,"see":"A repeating floral damask pattern."}
+{"t":"2026-05-27T23:06:08.458Z","id":42334,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of birds on branches with flowers."}
+{"t":"2026-05-27T23:06:11.741Z","id":42335,"verdict":"hot_candidate","n":8,"see":"A repeating floral and bird motif wallpaper."}
+{"t":"2026-05-27T23:06:14.576Z","id":42337,"verdict":"hot_candidate","n":9,"see":"A repeating floral bouquet pattern."}
+{"t":"2026-05-27T23:06:17.724Z","id":42338,"verdict":"hot_candidate","n":9,"see":"A repeating floral damask pattern."}
+{"t":"2026-05-27T23:06:20.673Z","id":42339,"verdict":"hot_candidate","n":4,"see":"I see a repeating pattern of birds on branches."}
+{"t":"2026-05-27T23:06:23.490Z","id":42340,"verdict":"hot_candidate","n":7,"see":"A repeating floral bouquet pattern."}
+{"t":"2026-05-27T23:06:26.414Z","id":42341,"verdict":"hot_candidate","n":6,"see":"A repeating pattern of floral motifs."}
+{"t":"2026-05-27T23:06:29.337Z","id":42344,"verdict":"hot_candidate","n":6,"see":"A wallpaper tile with a repeating bird and floral motif."}
+{"t":"2026-05-27T23:06:32.384Z","id":42345,"verdict":"hot_candidate","n":6,"see":"I see a repeating floral and bird pattern."}
+{"t":"2026-05-27T23:06:35.371Z","id":42346,"verdict":"hot_candidate","n":6,"see":"A repeating pattern of birds and flowers."}
+{"t":"2026-05-27T23:06:38.555Z","id":42347,"verdict":"hot_candidate","n":6,"see":"A repeating floral motif on a blue background."}
+{"t":"2026-05-27T23:06:41.523Z","id":42348,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of birds and flowers."}
+{"t":"2026-05-27T23:06:44.376Z","id":42349,"verdict":"hot_candidate","n":5,"see":"A wallpaper tile with a repeating bird and floral motif."}
+{"t":"2026-05-27T23:06:47.366Z","id":42350,"verdict":"hot_candidate","n":9,"see":"A repeating floral motif on a beige background."}
+{"t":"2026-05-27T23:06:50.303Z","id":42351,"verdict":"hot_candidate","n":4,"see":"A repeating wallpaper tile with birds on branches."}
+{"t":"2026-05-27T23:06:53.278Z","id":42352,"verdict":"hot_candidate","n":7,"see":"A repeating floral motif in a wallpaper tile."}
+{"t":"2026-05-27T23:06:56.309Z","id":42354,"verdict":"hot_candidate","n":7,"see":"A repeating floral bouquet pattern."}
+{"t":"2026-05-27T23:06:59.270Z","id":42355,"verdict":"hot_candidate","n":4,"see":"A repeating wallpaper pattern with birds and floral motifs."}
+{"t":"2026-05-27T23:07:02.614Z","id":42356,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of birds on branches."}
+{"t":"2026-05-27T23:07:05.596Z","id":42357,"verdict":"hot_candidate","n":8,"see":"A repeating pattern of leafy branches."}
+{"t":"2026-05-27T23:07:08.448Z","id":42358,"verdict":"hot_candidate","n":5,"see":"A repeating floral motif on a brown background."}
+{"t":"2026-05-27T23:07:11.317Z","id":42359,"verdict":"hot_candidate","n":4,"see":"I see a repeating pattern of birds and flowers."}
+{"t":"2026-05-27T23:07:14.163Z","id":42360,"verdict":"hot_candidate","n":7,"see":"A repeating floral bouquet pattern."}
+{"t":"2026-05-27T23:07:16.976Z","id":42361,"verdict":"hot_candidate","n":6,"see":"A repeating floral motif on a brown background."}
+{"t":"2026-05-27T23:07:19.954Z","id":42363,"verdict":"hot_candidate","n":4,"see":"A repeating wallpaper pattern with birds and floral motifs."}
+{"t":"2026-05-27T23:07:22.938Z","id":42364,"verdict":"hot_candidate","n":12,"see":"A repeating pattern of flowers, birds, and butterflies within a diamond grid."}
+{"t":"2026-05-27T23:07:26.089Z","id":42366,"verdict":"hot_candidate","n":6,"see":"A repeating floral motif on a brown background."}
+{"t":"2026-05-27T23:07:29.000Z","id":42367,"verdict":"hot_candidate","n":9,"see":"A repeating floral bouquet pattern."}
+{"t":"2026-05-27T23:07:32.010Z","id":42369,"verdict":"hot_candidate","n":9,"see":"A repeating floral motif on a green background."}
+{"t":"2026-05-27T23:07:34.932Z","id":42370,"verdict":"hot_candidate","n":6,"see":"A repeating floral damask pattern."}
+{"t":"2026-05-27T23:07:37.695Z","id":42371,"verdict":"hot_candidate","n":7,"see":"A repeating floral pattern with bouquets."}
+{"t":"2026-05-27T23:07:40.617Z","id":42372,"verdict":"hot_candidate","n":5,"see":"A repeating pattern of birds and flowers."}
+{"t":"2026-05-27T23:07:43.552Z","id":42373,"verdict":"hot_candidate","n":9,"see":"A repeating floral motif on a green background."}
+{"t":"2026-05-27T23:07:46.556Z","id":42374,"verdict":"hot_candidate","n":9,"see":"A repeating floral pattern with a diamond grid."}
+{"t":"2026-05-27T23:07:50.126Z","id":42375,"verdict":"hot_candidate","n":6,"see":"A repeating wallpaper pattern with birds and flowers."}
+{"t":"2026-05-27T23:07:53.130Z","id":42376,"verdict":"hot_candidate","n":12,"see":"A repeating floral motif within a diamond grid."}
+{"t":"2026-05-27T23:07:56.052Z","id":42377,"verdict":"hot_candidate","n":6,"see":"A repeating wallpaper pattern with birds and flowers."}
+{"t":"2026-05-27T23:07:58.999Z","id":42379,"verdict":"hot_candidate","n":6,"see":"A repeating floral motif in a diamond grid."}
+{"t":"2026-05-27T23:08:01.997Z","id":42380,"verdict":"hot_candidate","n":6,"see":"A repeating floral pattern with a brick background."}
diff --git a/lib/composition-detector.js b/lib/composition-detector.js
index e36d239..f91bb8a 100644
--- a/lib/composition-detector.js
+++ b/lib/composition-detector.js
@@ -61,8 +61,10 @@ CENTERED HERO (the defect):
 
 Count the visible instances of the dominant motif. A partial/cropped motif near an edge counts as 1 if you can see it's the same motif. Don't count background florals or small filler — count the primary repeating motif only.
 
+Also judge LARGE EMPTY AREA: TRUE if a big contiguous region (roughly a third or more of the tile) is flat empty background — a blank band, a void, or a large gap with no motif in it — so the tile reads as half-empty rather than a full all-over field. A textured/tone-on-tone background densely populated with motifs is NOT a large empty area. A single broad blank diagonal/horizontal band IS.
+
 Return JSON ONLY (no markdown):
-{"is_repeating_field": <bool>, "instance_count": <int>, "hero_centered": <bool>, "confidence": <0..1>, "what_you_see": "<one short sentence>"}
+{"is_repeating_field": <bool>, "instance_count": <int>, "hero_centered": <bool>, "large_empty_area": <bool>, "confidence": <0..1>, "what_you_see": "<one short sentence>"}
 
 Decision rule for hero_centered: TRUE if there's ONE clearly-dominant motif at or near the centre that's larger than the others (or all alone), with corners showing wrap-fragments or empty ground.
 
@@ -140,12 +142,14 @@ async function analyzeComposition(input, opts = {}) {
   // Coerce field types; tolerate missing fields with sane defaults
   const instance_count = Number.isFinite(parsed.instance_count) ? Math.max(0, parseInt(parsed.instance_count, 10)) : 0;
   const hero_centered  = parsed.hero_centered === true;
+  const large_empty_area = parsed.large_empty_area === true;
   // Recompute is_repeating_field locally — don't trust Gemini's boolean math
   const is_repeating_field = instance_count >= 3 && !hero_centered;
   return {
     is_repeating_field,
     instance_count,
     hero_centered,
+    large_empty_area,
     confidence:   Number(parsed.confidence) || 0,
     what_you_see: String(parsed.what_you_see || '').slice(0, 250),
     cost_estimate: 0.0005,
@@ -191,6 +195,9 @@ async function analyzeCompositionStable(input, opts = {}) {
   // Mode hero_centered
   const heroTrue = votes.filter(v => v.hero_centered).length;
   const hero_centered = heroTrue > votes.length / 2;
+  // Mode large_empty_area
+  const voidTrue = votes.filter(v => v.large_empty_area).length;
+  const large_empty_area = voidTrue > votes.length / 2;
   const avgConf = votes.reduce((s, v) => s + v.confidence, 0) / votes.length;
   // Reason = whichever winning-side vote has highest confidence
   const winners = votes.filter(v => v.is_repeating_field === is_repeating_field);
@@ -199,6 +206,7 @@ async function analyzeCompositionStable(input, opts = {}) {
     is_repeating_field,
     instance_count: median,
     hero_centered,
+    large_empty_area,
     confidence: avgConf,
     consensus:  Math.max(trueVotes, falseVotes) / votes.length,  // 1.0 unanimous, 0.67 split
     unanimous,
@@ -218,8 +226,17 @@ async function gateComposition(input, opts = {}) {
   const r = stable ? await analyzeCompositionStable(input, opts) : await analyzeComposition(input, opts);
   // Scenic exemption always passes the gate
   if (r.scenic_exempt) return { ok: true, result: r, reason: 'scenic-exempt' };
-  const ok = !r.hero_centered && r.instance_count >= minInstances;
-  return { ok, result: r, reason: ok ? 'pass' : (r.hero_centered ? `centered-hero (n=${r.instance_count})` : `too-few-instances (n=${r.instance_count}<${minInstances})`) };
+  // Open-space guard is OPT-IN (opts.rejectOpenSpace) so the generate/smart-fix
+  // retry loop keeps its original behavior; the triage decider passes it true.
+  const voidReject = opts.rejectOpenSpace === true && r.large_empty_area === true;
+  const ok = !r.hero_centered && r.instance_count >= minInstances && !voidReject;
+  let reason = 'pass';
+  if (!ok) {
+    if (r.hero_centered) reason = `centered-hero (n=${r.instance_count})`;
+    else if (r.instance_count < minInstances) reason = `too-few-instances (n=${r.instance_count}<${minInstances})`;
+    else if (voidReject) reason = `large-empty-void`;
+  }
+  return { ok, result: r, reason };
 }
 
 module.exports = { analyzeComposition, analyzeCompositionStable, gateComposition, isScenicCategory, SCENIC_CATEGORIES, GEMINI_MODEL };
diff --git a/public/admin/drunk-curator.html b/public/admin/drunk-curator.html
index a1d2b0e..ece0679 100644
--- a/public/admin/drunk-curator.html
+++ b/public/admin/drunk-curator.html
@@ -49,6 +49,18 @@
   .done{text-align:center;color:var(--mut);margin-top:18vh;} .done h2{color:var(--ink);}
   .flash{position:fixed;inset:0;pointer-events:none;opacity:0;transition:opacity .25s;font-size:140px;display:flex;align-items:center;justify-content:center;z-index:9;}
   .flash.show{opacity:1;}
+  /* repeat-scale control: stretch the tile to a real print width + inch ruler */
+  .scalebar{display:flex;align-items:center;gap:10px;font-size:12px;color:var(--mut);}
+  .scalebar .sb{background:#2a2a2a;color:var(--ink);border:1px solid #444;padding:6px 13px;font-size:12px;font-weight:700;border-radius:8px;}
+  .scalebar .sb.on{background:var(--ink);color:#111;border-color:var(--ink);}
+  .scalebar input[type=range]{width:150px;accent-color:var(--hot);}
+  .scalewrap{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;}
+  .scaleimg{position:relative;background:#000;flex:0 0 auto;box-shadow:0 0 0 1px #333;}
+  .scaleimg .tile{width:100%;height:100%;}
+  .ruler{position:relative;height:15px;flex:0 0 auto;}
+  .ruler .tick{position:absolute;top:0;width:1px;height:5px;background:#7a7a7a;}
+  .ruler .tick.maj{height:10px;background:#cfcfcf;}
+  .ruler .lab{position:absolute;top:8px;transform:translateX(-50%);font:700 8.5px sans-serif;color:#cfcfcf;white-space:nowrap;}
 </style>
 </head>
 <body>
@@ -86,6 +98,43 @@ const ADMIN=new URLSearchParams(location.search).get('admin');
 const CATEGORY=new URLSearchParams(location.search).get('category')||'';
 const q=s=>ADMIN?(s+(s.includes('?')?'&':'?')+'admin='+encodeURIComponent(ADMIN)):s;
 let QUEUE=[], i=0, hot=0, not=0, streak=0, decided={};
+// Repeat-scale preview: show the flat tile stretched to a real print width with an
+// inch ruler. 24/36/52″ are the standard wallpaper/panel widths. Persists.
+let scaleInches = Math.min(52, Math.max(24, parseInt(localStorage.getItem('drunk_scale_in'))||36));
+const WALL_INCHES = 96;   // the room pane represents ~an 8ft wall, so the repeat shows at true relative size
+function setScale(n){
+  scaleInches = Math.min(52, Math.max(24, parseInt(n)||36));
+  localStorage.setItem('drunk_scale_in', scaleInches);
+  applyScale();
+}
+// Size the tile so its width = scaleInches at a constant px/inch (52″ fills the pane,
+// smaller widths render proportionally narrower — it literally "stretches"), then
+// lay an inch ruler beneath it and scale the room wall's repeat to match.
+function applyScale(){
+  const slider=document.getElementById('scaleslider'); if(slider) slider.value=scaleInches;
+  document.querySelectorAll('.scalebar .sb').forEach(b=>b.classList.toggle('on', +b.dataset.in===scaleInches));
+  const num=document.getElementById('scalenum'); if(num) num.textContent=scaleInches;
+  const sv=document.getElementById('scaleval'); if(sv) sv.textContent=scaleInches;
+  const wall=document.querySelector('.wall'); if(wall) wall.style.backgroundSize=(scaleInches/WALL_INCHES*100).toFixed(1)+'% auto';
+  const wrap=document.querySelector('.scalewrap'); if(!wrap) return;
+  const pane=wrap.closest('.pane'); if(!pane) return;
+  const avail=Math.max(120, Math.min(pane.clientWidth-12, pane.clientHeight-30));  // square fits both dims, leave room for ruler
+  const ppi=avail/52;                       // 52″ = the widest, fills the pane
+  const W=Math.round(scaleInches*ppi);
+  const img=document.getElementById('scaleimg'); const ruler=document.getElementById('ruler');
+  if(img){ img.style.width=W+'px'; img.style.height=W+'px'; }
+  if(ruler){
+    ruler.style.width=W+'px';
+    let h='';
+    for(let inch=0; inch<=scaleInches; inch++){
+      const x=(inch*ppi).toFixed(1), maj=(inch%12===0)||inch===scaleInches;
+      h+=`<div class="tick${maj?' maj':''}" style="left:${x}px"></div>`;
+      if(maj) h+=`<div class="lab" style="left:${x}px">${inch}″</div>`;
+    }
+    ruler.innerHTML=h;
+  }
+}
+addEventListener('resize',()=>{ if(document.querySelector('.scalewrap')) applyScale(); });
 // Move through the queue WITHOUT deciding — lets Steve go back/forward to review
 // or fix items. Clamps to [0, length] (length = the cleared screen).
 function go(delta){ if(busy) return; const ni=i+delta; if(ni<0||ni>QUEUE.length) return; i=ni; render(); }
@@ -100,7 +149,12 @@ function render(){
   const d=QUEUE[i], src=`/designs/img/by-id/${d.id}`;
   $('#stage').innerHTML=`
     <div class="panes">
-      <div class="pane"><span class="tag">flat tile</span><img class="tile" src="${src}" onerror="this.style.opacity=.25"><div class="eboxes" id="eboxes"></div></div>
+      <div class="pane"><span class="tag">flat tile · <span id="scaleval">${scaleInches}</span>″ wide</span>
+        <div class="scalewrap">
+          <div class="scaleimg" id="scaleimg"><img class="tile" src="${src}" onerror="this.style.opacity=.25"><div class="eboxes" id="eboxes"></div></div>
+          <div class="ruler" id="ruler"></div>
+        </div>
+      </div>
       <div class="pane"><span class="tag">room setting</span>
         <div class="room">
           <div class="wall" style="background-image:url('${src}')"></div>
@@ -109,11 +163,19 @@ function render(){
         </div>
       </div>
     </div>
+    <div class="scalebar">📏 Width
+      <button class="sb" data-in="24" onclick="setScale(24)">24″</button>
+      <button class="sb" data-in="36" onclick="setScale(36)">36″</button>
+      <button class="sb" data-in="52" onclick="setScale(52)">52″</button>
+      <input type="range" id="scaleslider" min="24" max="52" step="1" value="${scaleInches}" oninput="setScale(this.value)">
+      <span><b id="scalenum">${scaleInches}</b>″ repeat · ruler in inches</span>
+    </div>
     <div class="meta"><span>#${d.id} · ${d.category||''}</span><span>🕓 ${fmtDate(d.created_at)}</span></div>
     <div class="prompt" onclick="this.classList.toggle('open')">${(d.prompt||'').replace(/</g,'&lt;').slice(0,240)}</div>
     <div class="nav"><button class="navb" onclick="go(-1)" ${i<=0?'disabled':''}>‹ Prev</button><span class="pos">${i+1} / ${QUEUE.length}${decided[d.id]?' · '+decided[d.id]:''}</span><button class="navb" onclick="go(1)" ${i>=QUEUE.length-1?'disabled':''}>Next ›</button></div>
     <div class="btns"><button class="hot" onclick="decide('hot')">🔥 HOT</button><button class="fix" onclick="decide('fix')">🔧 Fix</button><button class="not" onclick="decide('not')">🗑 Trash</button><button class="extract" onclick="extract()">✂️ Extract</button></div>
     <div class="hint">🔥/→ hot · 🔧/F fix · 🗑/← trash · ✂️/E extract · ‹ Prev , / [ · Next › . / ] (skip without deciding) · dotted = auto-detected errors</div>`;
+  applyScale();                   // size tile to the chosen print width + draw the inch ruler
   if(autoIdOn) autoIdCurrent();   // opt-in: only scan when the Auto-ID toggle is on
 }
 
diff --git a/scripts/auto-decide-drunk.js b/scripts/auto-decide-drunk.js
index d2c46c7..5cd1bac 100644
--- a/scripts/auto-decide-drunk.js
+++ b/scripts/auto-decide-drunk.js
@@ -65,7 +65,7 @@ function log(o) { try { fs.appendFileSync(LEDGER, JSON.stringify({ t: new Date()
         log({ id: d.id, verdict: 'not', reason: 'file_missing' }); not++; done++; continue;
       }
       let g;
-      try { g = await gateComposition(d.lp, { minInstances: 3, category: d.cat }); }
+      try { g = await gateComposition(d.lp, { minInstances: 3, category: d.cat, rejectOpenSpace: true }); }
       catch (e) {
         const msg = (e.message || '').toLowerCase();
         if (msg.includes('rate') || msg.includes('429') || msg.includes('quota') || msg.includes('overload')) {
@@ -84,11 +84,12 @@ function log(o) { try { fs.appendFileSync(LEDGER, JSON.stringify({ t: new Date()
         // publish decision; the loop just surfaces the dense winners.
         exec(`UPDATE all_designs SET notes = COALESCE(notes,'') || ' | AUTO_HOT_CANDIDATE' WHERE id=${d.id} AND (notes IS NULL OR notes NOT LIKE '%AUTO_HOT_CANDIDATE%');`);
         log({ id: d.id, verdict: 'hot_candidate', n: r.instance_count, see: r.what_you_see }); hot++;
-      } else {                                           // single-hero / sparse → NOT → registry (kept)
+      } else {                                           // single-hero / sparse / large-void → NOT → registry (kept)
+        const dtype = g.reason === 'large-empty-void' ? 'auto_decide_open_space' : 'auto_decide_single_hero';
         exec(`UPDATE all_designs SET is_published=FALSE, user_removed=TRUE WHERE id=${d.id};`);
         exec(
           "INSERT INTO wallco_defect_registry (design_id, category, generator, local_path, defect_type, defect_detail, prompt, seed, source) " +
-          `SELECT id, category, generator, local_path, 'auto_decide_single_hero', '${esc(g.reason + ' :: ' + (r.what_you_see || ''))}', prompt, seed, 'auto_decide_drunk' FROM all_designs WHERE id=${d.id} ` +
+          `SELECT id, category, generator, local_path, '${dtype}', '${esc(g.reason + ' :: ' + (r.what_you_see || ''))}', prompt, seed, 'auto_decide_drunk' FROM all_designs WHERE id=${d.id} ` +
           "ON CONFLICT (design_id, defect_type) DO NOTHING;"
         );
         log({ id: d.id, verdict: 'not', reason: g.reason, see: r.what_you_see }); not++;

← 6c3d280 drunk-curator: held-HOT toast attaches to body so render() c  ·  back to Wallco Ai  ·  add one-off re-screen of already-flagged candidates for larg 3327937 →