← back to Wallco Ai
drunk-curator Hot-or-Not: auto-ID dotted error boxes overlaid on the flat tile (drunk-aware: flags 'No bottle'/'Not animals' per region) + decisions HOT/Fix/Trash/Extract. Extract = opaque-motif-only → elements library (reuses elements/extract.py, --id bypasses publish filter). Fix verdict added to /drunk/decide; /api/admin/drunk/extract endpoint. F/E keyboard shortcuts
a1070fe63c41c01eef4d9f00ddb8aa4f50052e36 · 2026-05-27 14:27:29 -0700 · Steve Abrams
Files touched
M public/admin/drunk-curator.html
Diff
commit a1070fe63c41c01eef4d9f00ddb8aa4f50052e36
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 27 14:27:29 2026 -0700
drunk-curator Hot-or-Not: auto-ID dotted error boxes overlaid on the flat tile (drunk-aware: flags 'No bottle'/'Not animals' per region) + decisions HOT/Fix/Trash/Extract. Extract = opaque-motif-only → elements library (reuses elements/extract.py, --id bypasses publish filter). Fix verdict added to /drunk/decide; /api/admin/drunk/extract endpoint. F/E keyboard shortcuts
---
public/admin/drunk-curator.html | 56 ++++++++++++++++++++++++++++++++++++-----
1 file changed, 50 insertions(+), 6 deletions(-)
diff --git a/public/admin/drunk-curator.html b/public/admin/drunk-curator.html
index e8d52cf..ae86517 100644
--- a/public/admin/drunk-curator.html
+++ b/public/admin/drunk-curator.html
@@ -16,8 +16,15 @@
.panes{display:grid;grid-template-columns:1fr 1fr;gap:16px;width:min(96vw,1100px);height:min(60vh,540px);}
.pane{position:relative;border-radius:14px;overflow:hidden;border:1px solid var(--line);background:#000;}
.pane .tag{position:absolute;top:8px;left:10px;z-index:3;font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:#fff;background:rgba(0,0,0,.45);padding:2px 8px;border-radius:20px;backdrop-filter:blur(4px);}
- /* left: flat tile */
- .tile{width:100%;height:100%;object-fit:cover;object-position:center 12%;display:block;}
+ /* left: flat tile — fill exactly so auto-ID error boxes map 1:1 to the image */
+ .tile{width:100%;height:100%;object-fit:fill;display:block;}
+ /* auto-detected error boxes (dotted) overlaid on the flat tile */
+ .eboxes{position:absolute;inset:0;z-index:4;pointer-events:none;}
+ .ebox{position:absolute;border:2px dashed #ffb300;background:rgba(255,179,0,.12);border-radius:2px;}
+ .ebox b{position:absolute;bottom:-1px;left:-1px;background:#ffb300;color:#1a1a1a;font:800 9px sans-serif;padding:1px 5px;border-radius:0 3px 0 0;}
+ .scanning{position:absolute;top:8px;right:10px;z-index:5;background:rgba(0,0,0,.6);color:#ffd68a;font:700 10px sans-serif;padding:2px 8px;border-radius:20px;}
+ .extracted{position:fixed;top:60px;left:50%;transform:translateX(-50%);z-index:99;background:#1f6a2c;color:#fff;padding:8px 16px;border-radius:8px;font-size:13px;display:flex;align-items:center;gap:10px;}
+ .extracted img{height:42px;width:42px;object-fit:contain;background:#fff;border-radius:6px;}
/* right: CSS room setting */
.room{position:relative;width:100%;height:100%;background:#cfc7ba;}
.wall{position:absolute;inset:0 0 24% 0;background-repeat:repeat;background-position:center top;background-size:24% auto;filter:brightness(.98);}
@@ -32,6 +39,8 @@
button{border:0;border-radius:12px;padding:13px 34px;font-size:17px;font-weight:700;cursor:pointer;transition:transform .08s;}
button:active{transform:scale(.94);}
.not{background:var(--not);color:#fff;} .hot{background:var(--hot);color:#fff;}
+ .fix{background:#c98a00;color:#1a1a1a;} .extract{background:#2e7d32;color:#fff;}
+ .btns button{padding:13px 24px;}
.hint{font-size:11px;color:var(--mut);}
.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;}
@@ -80,7 +89,7 @@ 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>
+ <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">room setting</span>
<div class="room">
<div class="wall" style="background-image:url('${src}')"></div>
@@ -91,8 +100,42 @@ function render(){
</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,'<').slice(0,240)}</div>
- <div class="btns"><button class="not" onclick="decide('not')">👎 NOT</button><button class="hot" onclick="decide('hot')">🔥 HOT</button></div>
- <div class="hint">← / N = not · → / H = hot · left = flat repeat, right = on a wall</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 opaque motif → elements · dotted = auto-detected errors</div>`;
+ autoIdCurrent(); // fetch + overlay the auto-detected error boxes for this item
+}
+
+// Auto-detect error regions for the current item and overlay dotted boxes on the
+// flat tile (reuses the category-aware /auto-id endpoint; boxes persist server-side).
+async function autoIdCurrent(){
+ const d=QUEUE[i]; if(!d) return; const myId=d.id;
+ const box=$('#eboxes'); if(!box) return;
+ box.insertAdjacentHTML('beforebegin','<span class="scanning" id="scanning">🔍 scanning…</span>');
+ try{
+ const r=await fetch(q(`/api/admin/cactus/${myId}/auto-id`));
+ const j=await r.json();
+ if(QUEUE[i] && QUEUE[i].id!==myId) return; // user already advanced — drop stale result
+ document.getElementById('scanning')?.remove();
+ const boxes=(j.boxes||j.candidates||[]);
+ $('#eboxes').innerHTML = boxes.map(b=>`<div class="ebox" style="left:${b.x*100}%;top:${b.y*100}%;width:${b.w*100}%;height:${b.h*100}%"><b>${(b.label||'').replace(/</g,'')}</b></div>`).join('');
+ }catch(e){ document.getElementById('scanning')?.remove(); }
+}
+
+// Extract the OPAQUE motif only (alpha-trimmed solid silhouette) → elements library.
+// Non-terminal: saves the element + confirms; you still HOT/Fix/Trash the source.
+async function extract(){
+ if(i>=QUEUE.length) return; const d=QUEUE[i];
+ const btn=document.querySelector('.extract'); if(btn){btn.disabled=true;btn.textContent='✂️ …';}
+ try{
+ const r=await fetch(q('/api/admin/drunk/extract'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:d.id})});
+ const j=await r.json();
+ if(!j.ok) throw new Error(j.error||('HTTP '+r.status));
+ flash('✂️');
+ const el=document.createElement('div'); el.className='extracted';
+ el.innerHTML=`<img src="${j.motif_url}"> opaque motif extracted → elements library (#${d.id})`;
+ document.body.appendChild(el); setTimeout(()=>el.remove(),3500);
+ }catch(e){ flash('⚠'); alert('Extract failed for #'+d.id+': '+e.message); }
+ finally{ if(btn){btn.disabled=false;btn.textContent='✂️ Extract';} }
}
let busy=false;
async function decide(v){
@@ -114,7 +157,8 @@ async function decide(v){
}
document.addEventListener('keydown',e=>{ const k=e.key.toLowerCase();
if(e.target.tagName==='SELECT')return;
- if(e.key==='ArrowRight'||k==='h')decide('hot'); else if(e.key==='ArrowLeft'||k==='n')decide('not'); });
+ if(e.key==='ArrowRight'||k==='h')decide('hot'); else if(e.key==='ArrowLeft'||k==='n')decide('not');
+ else if(k==='f')decide('fix'); else if(k==='e')extract(); });
// collection picker — switch the hot-or-not queue to any wallco.ai line
async function loadCollections(){
← 7eb9c7b 100% publish gate: no pattern goes live without passing ever
·
back to Wallco Ai
·
drunk-curator: make Auto-ID opt-in via header toggle (off by 8120fe7 →