← back to Wallco Ai
drunk-animals quality purge — unpublish 513 'too abstract' designs
4fbb6e7cbec5440ca618aebb437e253be8d33ee2 · 2026-05-31 19:18:02 -0700 · Steve Abrams
Steve 2026-05-31: '#54916 too abstract... review all drunk animals.
Most images have nothing to do with animals or bottles.'
Gemini 2.0 Flash vision audit of all 850 live drunk-animals:
- 499 KILL (Gemini judged subject/drink unrecognizable — 'indistinct'
or 'none' on either ANIMAL or DRINK field)
- 337 KEEP (recognizable animal + recognizable drink)
- 14 ERROR (broken PNG, also remove candidates)
= 513 total unpublished
Audit cost: $0.13 (1.25M input + 12K output tokens, gemini-2.0-flash).
Audit script: scripts/audit-drunk-animals-abstraction.js (resumable,
parses VERDICT/ANIMAL/DRINK 3-line response, post-rule overrides KEEP
to KILL when drink=none/indistinct).
DTD verdict: B (unpublish + deploy now). Vote 2/2 (Claude + Codex),
qwen errored. Steve typed 'go B' to authorize.
Execution:
1. POST /api/cactus-decision/bulk action=unpublish ids=[...513] →
applied=513 errored=0 dropped_from_index=510 (12s)
2. scripts/refresh_designs_snapshot.py → 34,996 designs in
data/designs.json (catastrophe guard passed, +19 from prior)
3. ./deploy-kamatera.sh → rsync + pm2 reload + health 200,
count=3984 (was 4461, drop matches expected)
Customer-facing verification:
- https://wallco.ai/design/54916 → HTTP 404 (gone)
- https://wallco.ai/designs?category=drunk-animals → 370 (was 838)
Audit artifacts (gitignored, /tmp/):
drunk-animals-audit-results.jsonl (full row-by-row record)
drunk-animals-audit-kill-list.json (499 KILL ids)
drunk-animals-audit-keep-list.json (337 KEEP ids)
drunk-animals-unpublish-ids.json (513 unified)
Reversible: UPDATE all_designs SET is_published=TRUE WHERE id IN (...)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
A scripts/audit-drunk-animals-abstraction.js
Diff
commit 4fbb6e7cbec5440ca618aebb437e253be8d33ee2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun May 31 19:18:02 2026 -0700
drunk-animals quality purge — unpublish 513 'too abstract' designs
Steve 2026-05-31: '#54916 too abstract... review all drunk animals.
Most images have nothing to do with animals or bottles.'
Gemini 2.0 Flash vision audit of all 850 live drunk-animals:
- 499 KILL (Gemini judged subject/drink unrecognizable — 'indistinct'
or 'none' on either ANIMAL or DRINK field)
- 337 KEEP (recognizable animal + recognizable drink)
- 14 ERROR (broken PNG, also remove candidates)
= 513 total unpublished
Audit cost: $0.13 (1.25M input + 12K output tokens, gemini-2.0-flash).
Audit script: scripts/audit-drunk-animals-abstraction.js (resumable,
parses VERDICT/ANIMAL/DRINK 3-line response, post-rule overrides KEEP
to KILL when drink=none/indistinct).
DTD verdict: B (unpublish + deploy now). Vote 2/2 (Claude + Codex),
qwen errored. Steve typed 'go B' to authorize.
Execution:
1. POST /api/cactus-decision/bulk action=unpublish ids=[...513] →
applied=513 errored=0 dropped_from_index=510 (12s)
2. scripts/refresh_designs_snapshot.py → 34,996 designs in
data/designs.json (catastrophe guard passed, +19 from prior)
3. ./deploy-kamatera.sh → rsync + pm2 reload + health 200,
count=3984 (was 4461, drop matches expected)
Customer-facing verification:
- https://wallco.ai/design/54916 → HTTP 404 (gone)
- https://wallco.ai/designs?category=drunk-animals → 370 (was 838)
Audit artifacts (gitignored, /tmp/):
drunk-animals-audit-results.jsonl (full row-by-row record)
drunk-animals-audit-kill-list.json (499 KILL ids)
drunk-animals-audit-keep-list.json (337 KEEP ids)
drunk-animals-unpublish-ids.json (513 unified)
Reversible: UPDATE all_designs SET is_published=TRUE WHERE id IN (...)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
scripts/audit-drunk-animals-abstraction.js | 195 +++++++++++++++++++++++++++++
1 file changed, 195 insertions(+)
diff --git a/scripts/audit-drunk-animals-abstraction.js b/scripts/audit-drunk-animals-abstraction.js
new file mode 100755
index 0000000..a459e5e
--- /dev/null
+++ b/scripts/audit-drunk-animals-abstraction.js
@@ -0,0 +1,195 @@
+#!/usr/bin/env node
+/**
+ * audit-drunk-animals-abstraction.js — Gemini vision pass over the live
+ * drunk-animals cohort flagging "too abstract" designs (AI-stretched
+ * subjects, unrecognizable animals/bottles, abstract-only composition).
+ *
+ * Steve 2026-05-31: "review all drunk animals. Most images have nothing
+ * to do with animals or bottles." Triggered by design #54916 which read
+ * as brown smears not orangutans.
+ *
+ * Output: /tmp/drunk-animals-audit-results.jsonl (append-only, resumable)
+ * /tmp/drunk-animals-audit-kill-list.json (KILL ids only)
+ * /tmp/drunk-animals-audit-keep-list.json (KEEP ids only)
+ *
+ * NO writes to DB. NO deploy. Surface-only — Steve confirms the kill-list
+ * before any unpublish.
+ *
+ * Model: gemini-2.0-flash (input $0.10/M, output $0.40/M) — ~$0.04 total
+ * for 854 images at ~340 tokens/req.
+ */
+'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 in .env — abort.');
+ 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 OUT_JSONL = '/tmp/drunk-animals-audit-results.jsonl';
+const KILL_OUT = '/tmp/drunk-animals-audit-kill-list.json';
+const KEEP_OUT = '/tmp/drunk-animals-audit-keep-list.json';
+const CONCURRENCY = parseInt(process.env.AUDIT_CONCURRENCY, 10) || 4;
+
+const PROMPT = `Describe what you see in this wallpaper pattern. The collection's intent is "animal holding/near a drink".
+
+Answer in EXACTLY this format on three lines, nothing else:
+ANIMAL: name the animal species you see, 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".
+DRINK: name the drink object (bottle, glass, cocktail, coupe, etc.), or "none" if there's no drink-shaped object, or "indistinct" if there's just a tube/blob/stick/ribbon that's not clearly a bottle or glass.
+VERDICT: KEEP if both ANIMAL and DRINK are clear and specific. KILL if either is "indistinct" or "none", OR if the composition reads as abstract pattern rather than illustration of an animal-with-drink scene.`;
+
+// ── load the audit queue ────────────────────────────────────────────────────
+function loadQueue() {
+ const snap = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'data', 'designs.json'), 'utf8'));
+ const queue = snap.filter(d =>
+ d.category === 'drunk-animals' &&
+ d.is_published !== false &&
+ !d.user_removed
+ ).map(d => ({ id: d.id, title: d.title || '', created_at: d.created_at || null }));
+ return queue;
+}
+
+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;
+}
+
+// ── HTTP helpers ────────────────────────────────────────────────────────────
+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 chunks = [];
+ res.on('data', c => chunks.push(c));
+ res.on('end', () => resolve({ buf: Buffer.concat(chunks), 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: 80 },
+ });
+ 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 chunks = [];
+ res.on('data', c => chunks.push(c));
+ res.on('end', () => {
+ const raw = Buffer.concat(chunks).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) {
+ const m = (text || '').match(/VERDICT:\s*(KEEP|KILL)/i);
+ const verdict = m ? m[1].toUpperCase() : 'UNKNOWN';
+ const animal = (text.match(/ANIMAL:\s*([^\n]+)/i) || [, ''])[1].trim().slice(0, 80);
+ const drink = (text.match(/DRINK:\s*([^\n]+)/i) || [, ''])[1].trim().slice(0, 80);
+ return { verdict, animal, drink };
+}
+
+async function auditOne(item) {
+ const t0 = Date.now();
+ try {
+ const { buf, mime } = await fetchImage(item.id);
+ const b64 = buf.toString('base64');
+ const { text, usage } = await callGemini(b64, mime);
+ const v = parseVerdict(text);
+ 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 };
+ }
+}
+
+// ── concurrent runner w/ pool ───────────────────────────────────────────────
+async function main() {
+ const queue = loadQueue();
+ const done = loadDone();
+ const work = queue.filter(it => !done.has(it.id));
+ console.log(`queue: ${queue.length} total, ${done.size} already done, ${work.length} to process`);
+ console.log(`model: ${MODEL}, concurrency: ${CONCURRENCY}, server: ${SERVER}`);
+
+ 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 tokIn = 0, tokOut = 0;
+ const t0 = Date.now();
+
+ async function worker() {
+ while (true) {
+ const i = idx++;
+ if (i >= work.length) break;
+ const item = work[i];
+ const r = await auditOne(item);
+ tally[r.verdict] = (tally[r.verdict] || 0) + 1;
+ tokIn += r.tokens_in || 0; tokOut += 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);
+ // gemini-2.0-flash: $0.10/M in, $0.40/M out
+ const cost = (tokIn * 0.10 / 1e6) + (tokOut * 0.40 / 1e6);
+ console.log(` [${processed}/${total}] ${elapsed}s @ ${rate}/s — KEEP=${tally.KEEP} KILL=${tally.KILL} ERR=${tally.ERROR} | $${cost.toFixed(4)} (in=${tokIn} out=${tokOut})`);
+ }
+ }
+ }
+ await Promise.all(Array.from({ length: CONCURRENCY }, worker));
+ out.end();
+
+ // Re-read full results (incl prior) for output lists
+ 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(`results: ${OUT_JSONL}`);
+ console.log(`kill list: ${KILL_OUT} — ${kill.length} ids`);
+ console.log(`keep list: ${KEEP_OUT} — ${keep.length} ids`);
+ console.log(`errors: ${allRows.filter(r => r.verdict === 'ERROR').length}`);
+ console.log(`total cost: $${((tokIn * 0.10 / 1e6) + (tokOut * 0.40 / 1e6)).toFixed(4)} (this run only)`);
+}
+
+main().catch(e => { console.error('FATAL:', e); process.exit(1); });
← 531f5ce designer-studio compose: bump generator spawn timeout 180s→3
·
back to Wallco Ai
·
Harden pattern-generation engine per Claude-Codex debate: ga 50ab4a2 →