[object Object]

← back to Wallco Ai

stoned-animals quality purge — unpublish 227 alert-mood designs

91d3a60de24e4ab4b333d8141a37742ad8b3f6f2 · 2026-05-31 19:40:48 -0700 · Steve Abrams

Steve 2026-05-31: 'review stoned-animals next' (after the drunk-animals
purge succeeded). Same playbook, mood-criterion instead of subject+prop:
the collection's intent is 'animal in a relaxed/sleepy state', so the
failure mode is alert/active eyes that contradict the theme.

Gemini 2.0 Flash vision audit of 712 live stoned-animals (was 837 in
snapshot, 712 by the time the queue loaded after the drunk purge):
  - 485 KEEP (68.1%) — recognizable animal + neutral-or-sleepy mood
  - 227 KILL (31.9%) — almost all 'mood:alert' (226), 1 'animal indistinct'
  -   0 ERROR (no broken images this time)
  = 227 unpublished

Notable finding: 450 title drifts (63%) — Mac2 title-claimed species
('Deer Daydream', 'Owls Idyll', 'Bears Slumber') often doesn't match
what Gemini actually saw (iguana, koala, otter, sloth). This is a
TITLER problem not a quality problem, not part of this kill but worth
fixing as a follow-up.

Top actual species (by Gemini): koala 75, lemur 56, otter 55, fox 55,
sloth 53, raccoon 52. Heavy on lounge-prone mammals — makes sense.

Audit cost: $0.11 (gemini-2.0-flash). Script:
scripts/audit-stoned-animals-mood.js. Verdict logic computed in JS
(KILL if animal=indistinct OR mood matches alert/active/hunting), so
the prompt just gathers ANIMAL + MOOD descriptors and lets the
threshold be tunable without re-querying Gemini.

Execution (matched drunk-animals playbook):
  1. POST /api/cactus-decision/bulk action=unpublish → 227 applied,
     0 errored, 227 dropped_from_index (0.6s)
  2. scripts/refresh_designs_snapshot.py → 35,011 designs in snapshot
  3. ./deploy-kamatera.sh → HTTP 200 health, count 3984→3928

Customer-facing verification:
  226 of 227 KILLs absent from wallco.ai/designs?category=stoned-animals
  712 → 487 designs (drop of 225, matches 227 ± 2)
  One holdout: #7466 'Noir Sundrift' (on prod; titled 'Owls Idyll in
  Charcoal' on Mac2). Has curator-force-publish tag + metadata
  appears to differ between Mac2 and prod. Steve decided to skip
  the chase — 99.6% propagation accepted.

Reversible via UPDATE all_designs SET is_published=TRUE WHERE id IN (...)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 91d3a60de24e4ab4b333d8141a37742ad8b3f6f2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 31 19:40:48 2026 -0700

    stoned-animals quality purge — unpublish 227 alert-mood designs
    
    Steve 2026-05-31: 'review stoned-animals next' (after the drunk-animals
    purge succeeded). Same playbook, mood-criterion instead of subject+prop:
    the collection's intent is 'animal in a relaxed/sleepy state', so the
    failure mode is alert/active eyes that contradict the theme.
    
    Gemini 2.0 Flash vision audit of 712 live stoned-animals (was 837 in
    snapshot, 712 by the time the queue loaded after the drunk purge):
      - 485 KEEP (68.1%) — recognizable animal + neutral-or-sleepy mood
      - 227 KILL (31.9%) — almost all 'mood:alert' (226), 1 'animal indistinct'
      -   0 ERROR (no broken images this time)
      = 227 unpublished
    
    Notable finding: 450 title drifts (63%) — Mac2 title-claimed species
    ('Deer Daydream', 'Owls Idyll', 'Bears Slumber') often doesn't match
    what Gemini actually saw (iguana, koala, otter, sloth). This is a
    TITLER problem not a quality problem, not part of this kill but worth
    fixing as a follow-up.
    
    Top actual species (by Gemini): koala 75, lemur 56, otter 55, fox 55,
    sloth 53, raccoon 52. Heavy on lounge-prone mammals — makes sense.
    
    Audit cost: $0.11 (gemini-2.0-flash). Script:
    scripts/audit-stoned-animals-mood.js. Verdict logic computed in JS
    (KILL if animal=indistinct OR mood matches alert/active/hunting), so
    the prompt just gathers ANIMAL + MOOD descriptors and lets the
    threshold be tunable without re-querying Gemini.
    
    Execution (matched drunk-animals playbook):
      1. POST /api/cactus-decision/bulk action=unpublish → 227 applied,
         0 errored, 227 dropped_from_index (0.6s)
      2. scripts/refresh_designs_snapshot.py → 35,011 designs in snapshot
      3. ./deploy-kamatera.sh → HTTP 200 health, count 3984→3928
    
    Customer-facing verification:
      226 of 227 KILLs absent from wallco.ai/designs?category=stoned-animals
      712 → 487 designs (drop of 225, matches 227 ± 2)
      One holdout: #7466 'Noir Sundrift' (on prod; titled 'Owls Idyll in
      Charcoal' on Mac2). Has curator-force-publish tag + metadata
      appears to differ between Mac2 and prod. Steve decided to skip
      the chase — 99.6% propagation accepted.
    
    Reversible via UPDATE all_designs SET is_published=TRUE WHERE id IN (...)
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 scripts/audit-stoned-animals-mood.js | 203 +++++++++++++++++++++++++++++++++++
 1 file changed, 203 insertions(+)

diff --git a/scripts/audit-stoned-animals-mood.js b/scripts/audit-stoned-animals-mood.js
new file mode 100644
index 0000000..37fa605
--- /dev/null
+++ b/scripts/audit-stoned-animals-mood.js
@@ -0,0 +1,203 @@
+#!/usr/bin/env node
+/**
+ * audit-stoned-animals-mood.js — Gemini vision pass over the live
+ * stoned-animals cohort flagging designs where the animal isn't recognizable
+ * OR isn't actually in a relaxed/sleepy/hazy mood (the collection's intent).
+ *
+ * Steve 2026-05-31: same playbook as audit-drunk-animals-abstraction.js
+ * but tuned for stoned-animals which uses "Slumber/Idyll/Daydream/Haze/
+ * Repose/Drift/Lullaby" verbs — NO prop required (no joint/pipe/drink),
+ * the mood IS the motif. Failure modes: AI-stretched shapes that don't
+ * read as an animal, or an animal with alert/active eyes that contradicts
+ * the "stoned" theme.
+ *
+ * Bonus signal: title-vs-visual mismatch. The snapshot title declares a
+ * species ("Foxes / Otters / Sloths / Deer / Bears / Owls / Rabbits") but
+ * the image may show something else entirely — captured as TITLE_DRIFT in
+ * the audit row for Steve to spot-check.
+ *
+ * Outputs:
+ *   /tmp/stoned-animals-audit-results.jsonl  (resumable, every row)
+ *   /tmp/stoned-animals-audit-kill-list.json (KILL ids only)
+ *   /tmp/stoned-animals-audit-keep-list.json (KEEP ids only)
+ *
+ * Model: gemini-2.0-flash (~$0.12 for ~700 published designs).
+ */
+'use strict';
+
+require('dotenv').config();
+const fs = require('fs');
+const path = require('path');
+const http = require('http');
+const https = require('https');
+
+const API_KEY = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
+if (!API_KEY) { console.error('GEMINI_API_KEY not set'); process.exit(1); }
+
+const MODEL = process.env.AUDIT_MODEL || 'gemini-2.0-flash';
+const SERVER = process.env.AUDIT_SERVER || 'http://127.0.0.1:9905';
+const CATEGORY = process.env.AUDIT_CATEGORY || 'stoned-animals';
+const OUT_JSONL = '/tmp/stoned-animals-audit-results.jsonl';
+const KILL_OUT = '/tmp/stoned-animals-audit-kill-list.json';
+const KEEP_OUT = '/tmp/stoned-animals-audit-keep-list.json';
+const CONCURRENCY = parseInt(process.env.AUDIT_CONCURRENCY, 10) || 6;
+
+const PROMPT = `Describe what you see in this wallpaper pattern. Answer in EXACTLY this format on two lines, nothing else:
+
+ANIMAL: name the animal species you see (e.g. fox, owl, sloth, otter, bear, rabbit, deer, raccoon, lemur), or "indistinct" if it's just smeared/stretched shapes you can't identify without being told. Be honest — if the body looks AI-stretched, dismembered, elongated, or amorphous, say "indistinct".
+MOOD: pick ONE single word that best describes the animal's expression/pose: "sleepy" (closed/half-closed eyes, lying down, lounging, calm, languid, drowsy, dreamy), "neutral" (no strong mood signal either way), "alert" (wide-open eyes, staring, ears pricked, standing upright, hunting/active pose), or "indistinct" (can't tell because the figure is too abstract).`;
+
+// Verdict logic — applied in JS so we can tune the threshold without rebuilding
+// the audit. The collection is "stoned-animals" so mood matters but neutral is
+// acceptable (many stylized illustrations have placid expressions Gemini labels
+// "neutral"); only clear "alert"/"active" contradicts the theme.
+function computeVerdict(animal, mood) {
+  const a = (animal || '').toLowerCase();
+  const m = (mood || '').toLowerCase();
+  if (!a || a === 'indistinct' || a === 'none') return { verdict: 'KILL', reason: 'animal indistinct' };
+  if (m === 'alert' || m === 'active' || m.includes('alert') || m.includes('active') ||
+      m.includes('hunting') || m.includes('aggressive')) {
+    return { verdict: 'KILL', reason: 'mood:' + m };
+  }
+  if (m === 'indistinct') return { verdict: 'KILL', reason: 'mood indistinct' };
+  return { verdict: 'KEEP', reason: '' };
+}
+
+function loadQueue() {
+  const snap = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'data', 'designs.json'), 'utf8'));
+  return snap.filter(d => d.category === CATEGORY && d.is_published !== false && !d.user_removed)
+             .map(d => ({ id: d.id, title: d.title || '', created_at: d.created_at || null }));
+}
+
+function loadDone() {
+  if (!fs.existsSync(OUT_JSONL)) return new Set();
+  const lines = fs.readFileSync(OUT_JSONL, 'utf8').split('\n').filter(Boolean);
+  const done = new Set();
+  for (const l of lines) {
+    try { const r = JSON.parse(l); if (r.id) done.add(r.id); } catch {}
+  }
+  return done;
+}
+
+function fetchImage(id) {
+  return new Promise((resolve, reject) => {
+    const req = http.get(`${SERVER}/designs/img/by-id/${id}`, res => {
+      if (res.statusCode !== 200) { res.resume(); return reject(new Error(`img HTTP ${res.statusCode}`)); }
+      const c = []; res.on('data', x => c.push(x));
+      res.on('end', () => resolve({ buf: Buffer.concat(c), mime: res.headers['content-type'] || 'image/png' }));
+      res.on('error', reject);
+    });
+    req.on('error', reject);
+    req.setTimeout(15000, () => req.destroy(new Error('img timeout')));
+  });
+}
+
+function callGemini(imgB64, mime) {
+  return new Promise((resolve, reject) => {
+    const body = JSON.stringify({
+      contents: [{ parts: [
+        { inline_data: { mime_type: mime.split(';')[0].trim(), data: imgB64 } },
+        { text: PROMPT },
+      ]}],
+      generationConfig: { temperature: 0.1, maxOutputTokens: 100 },
+    });
+    const req = https.request({
+      hostname: 'generativelanguage.googleapis.com',
+      path: `/v1beta/models/${MODEL}:generateContent?key=${API_KEY}`,
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
+    }, res => {
+      const c = []; res.on('data', x => c.push(x));
+      res.on('end', () => {
+        const raw = Buffer.concat(c).toString('utf8');
+        if (res.statusCode !== 200) return reject(new Error(`gemini HTTP ${res.statusCode}: ${raw.slice(0,200)}`));
+        try {
+          const j = JSON.parse(raw);
+          const text = j?.candidates?.[0]?.content?.parts?.[0]?.text || '';
+          resolve({ text, usage: j?.usageMetadata });
+        } catch (e) { reject(new Error('gemini parse: ' + e.message)); }
+      });
+    });
+    req.on('error', reject);
+    req.setTimeout(45000, () => req.destroy(new Error('gemini timeout')));
+    req.write(body); req.end();
+  });
+}
+
+function parseVerdict(text, title) {
+  const animal = (text.match(/ANIMAL:\s*([^\n]+)/i) || [, ''])[1].trim().slice(0, 80);
+  const mood   = (text.match(/MOOD:\s*([^\n]+)/i)   || [, ''])[1].trim().slice(0, 80);
+  // Strip parenthetical hedges Gemini sometimes adds, e.g. "Sleepy (eyes half-closed)"
+  const moodClean = mood.replace(/\s*\(.+\)$/, '').trim();
+  const { verdict, reason } = computeVerdict(animal, moodClean);
+  // Title-drift detector
+  const titleAnimal = (title.match(/^([A-Z][a-z]+)s?\s+/) || [, ''])[1].toLowerCase();
+  const visualAnimal = animal.toLowerCase();
+  const titleDrift = titleAnimal && visualAnimal && visualAnimal !== 'indistinct' &&
+    !visualAnimal.includes(titleAnimal) && !titleAnimal.includes(visualAnimal.split(/[\s,]/)[0]);
+  return { verdict, kill_reason: reason, animal, mood: moodClean, title_drift: titleDrift, title_animal: titleAnimal };
+}
+
+async function auditOne(item) {
+  const t0 = Date.now();
+  try {
+    const { buf, mime } = await fetchImage(item.id);
+    const { text, usage } = await callGemini(buf.toString('base64'), mime);
+    const v = parseVerdict(text, item.title);
+    return { id: item.id, title: item.title, ...v, raw: text.slice(0, 200),
+             tokens_in: usage?.promptTokenCount || 0, tokens_out: usage?.candidatesTokenCount || 0,
+             ms: Date.now() - t0 };
+  } catch (e) {
+    return { id: item.id, title: item.title, verdict: 'ERROR', error: String(e.message || e).slice(0, 160), ms: Date.now() - t0 };
+  }
+}
+
+async function main() {
+  const queue = loadQueue();
+  const done = loadDone();
+  const work = queue.filter(it => !done.has(it.id));
+  console.log(`category: ${CATEGORY}, queue: ${queue.length}, done: ${done.size}, to do: ${work.length}`);
+  console.log(`model: ${MODEL}, concurrency: ${CONCURRENCY}`);
+
+  const out = fs.createWriteStream(OUT_JSONL, { flags: 'a' });
+  let idx = 0, processed = 0;
+  const total = work.length;
+  const tally = { KEEP: 0, KILL: 0, ERROR: 0, UNKNOWN: 0 };
+  let drifts = 0, tin = 0, tout = 0;
+  const t0 = Date.now();
+
+  async function worker() {
+    while (true) {
+      const i = idx++;
+      if (i >= work.length) break;
+      const r = await auditOne(work[i]);
+      tally[r.verdict] = (tally[r.verdict] || 0) + 1;
+      if (r.title_drift) drifts++;
+      tin += r.tokens_in || 0; tout += r.tokens_out || 0;
+      out.write(JSON.stringify(r) + '\n');
+      processed++;
+      if (processed % 25 === 0 || processed === total) {
+        const elapsed = ((Date.now() - t0) / 1000).toFixed(0);
+        const rate = (processed / Math.max(1, (Date.now() - t0) / 1000)).toFixed(2);
+        const cost = (tin * 0.10 / 1e6) + (tout * 0.40 / 1e6);
+        console.log(`  [${processed}/${total}] ${elapsed}s @ ${rate}/s — KEEP=${tally.KEEP} KILL=${tally.KILL} ERR=${tally.ERROR} drifts=${drifts} | $${cost.toFixed(4)}`);
+      }
+    }
+  }
+  await Promise.all(Array.from({ length: CONCURRENCY }, worker));
+  out.end();
+
+  const allRows = fs.readFileSync(OUT_JSONL, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l));
+  const kill = allRows.filter(r => r.verdict === 'KILL');
+  const keep = allRows.filter(r => r.verdict === 'KEEP');
+  fs.writeFileSync(KILL_OUT, JSON.stringify(kill, null, 2));
+  fs.writeFileSync(KEEP_OUT, JSON.stringify(keep, null, 2));
+  console.log(`\n=== DONE ===`);
+  console.log(`kill: ${KILL_OUT} — ${kill.length} ids`);
+  console.log(`keep: ${KEEP_OUT} — ${keep.length} ids`);
+  console.log(`errors: ${allRows.filter(r => r.verdict === 'ERROR').length}`);
+  console.log(`title drifts: ${allRows.filter(r => r.title_drift).length}`);
+  console.log(`cost: $${((tin * 0.10 / 1e6) + (tout * 0.40 / 1e6)).toFixed(4)}`);
+}
+
+main().catch(e => { console.error('FATAL:', e); process.exit(1); });

← 724ffb9 Generate + publish GRASSCLOTH/LINEN/SILK drunk-animals sets  ·  back to Wallco Ai  ·  Add curator-excluded-ids helper — publish loop respects cact be9b245 →