← back to Wallco Ai
drunk-animals title-drift fix — re-title 180 designs with actual species
5a5609313b5544ed514400dabb2d665ba4169f1f · 2026-05-31 19:58:17 -0700 · Steve Abrams
Steve 2026-05-31: 'review drunk-animals titles next' (after the
stoned-animals title-drift fix succeeded). Re-processed the existing
drunk-animals audit results post-hoc (the drift-detector was added
later for stoned-animals, so the drunk audit didn't capture it live).
Of 337 drunk-animals KEEPs, 180 (53%) had title-drift:
- 165 species-mismatch — title claimed wrong species
(e.g. 'Capybaras Carouse in Honey' → 'Beavers Carouse in Honey')
- 15 color-only — auto-titler punted
(e.g. 'Pewter Tipple No.41281' → 'Moths Tipple in Pewter')
Top retitled species (Gemini-identified): bird 31, monkey 15,
sloth 12, cat 10, bear 8, parrot 8, beaver 6, fish 5, dog 5.
('bird' and 'monkey' are Gemini's generic fallback — same
accuracy floor as 'dog' was on stoned-animals; still better
than the wrong claim it replaced.)
Mechanism: scripts/generate-drunk-title-rewrites.js. Drunk-animals
verb vocabulary differs from stoned-animals (Soirée/Folly/Spree/Toast/
Carouse/Bacchanal/Nightcap/Tipple vs Slumber/Idyll/Daydream/Haze),
but the rewrite shape and pipeline are identical. Reused:
- /api/admin/titles/bulk endpoint (from stoned-animals fix)
- refresh_designs_snapshot.py ai_title preference (same fix)
- Pluralizer with irregular dict (wolf→wolves, fox→foxes, etc.)
Execution:
1. POST 180 renames → applied=180 errored=0 patched_in_memory=180 (0.6s)
2. snapshot rebuild → 35,028 rows, 5/5 spot-checks ✓
3. ./deploy-kamatera.sh → 5/5 prod verifications ✓
Reversible: /tmp/drunk-animals-title-rewrites.json has old_title for
every row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
A scripts/generate-drunk-title-rewrites.js
Diff
commit 5a5609313b5544ed514400dabb2d665ba4169f1f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun May 31 19:58:17 2026 -0700
drunk-animals title-drift fix — re-title 180 designs with actual species
Steve 2026-05-31: 'review drunk-animals titles next' (after the
stoned-animals title-drift fix succeeded). Re-processed the existing
drunk-animals audit results post-hoc (the drift-detector was added
later for stoned-animals, so the drunk audit didn't capture it live).
Of 337 drunk-animals KEEPs, 180 (53%) had title-drift:
- 165 species-mismatch — title claimed wrong species
(e.g. 'Capybaras Carouse in Honey' → 'Beavers Carouse in Honey')
- 15 color-only — auto-titler punted
(e.g. 'Pewter Tipple No.41281' → 'Moths Tipple in Pewter')
Top retitled species (Gemini-identified): bird 31, monkey 15,
sloth 12, cat 10, bear 8, parrot 8, beaver 6, fish 5, dog 5.
('bird' and 'monkey' are Gemini's generic fallback — same
accuracy floor as 'dog' was on stoned-animals; still better
than the wrong claim it replaced.)
Mechanism: scripts/generate-drunk-title-rewrites.js. Drunk-animals
verb vocabulary differs from stoned-animals (Soirée/Folly/Spree/Toast/
Carouse/Bacchanal/Nightcap/Tipple vs Slumber/Idyll/Daydream/Haze),
but the rewrite shape and pipeline are identical. Reused:
- /api/admin/titles/bulk endpoint (from stoned-animals fix)
- refresh_designs_snapshot.py ai_title preference (same fix)
- Pluralizer with irregular dict (wolf→wolves, fox→foxes, etc.)
Execution:
1. POST 180 renames → applied=180 errored=0 patched_in_memory=180 (0.6s)
2. snapshot rebuild → 35,028 rows, 5/5 spot-checks ✓
3. ./deploy-kamatera.sh → 5/5 prod verifications ✓
Reversible: /tmp/drunk-animals-title-rewrites.json has old_title for
every row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
scripts/generate-drunk-title-rewrites.js | 172 +++++++++++++++++++++++++++++++
1 file changed, 172 insertions(+)
diff --git a/scripts/generate-drunk-title-rewrites.js b/scripts/generate-drunk-title-rewrites.js
new file mode 100644
index 0000000..8c127f9
--- /dev/null
+++ b/scripts/generate-drunk-title-rewrites.js
@@ -0,0 +1,172 @@
+#!/usr/bin/env node
+/**
+ * generate-drunk-title-rewrites.js — Re-process the drunk-animals audit
+ * results to detect title-vs-visual drifts (the audit didn't capture
+ * title_drift live, since the drift-detector was added later for stoned-animals).
+ * Then emit proposed renames for the 337 KEEPs with drift.
+ *
+ * Same target format as stoned-animals: `[ActualSpeciesPlural] [Verb] in [Color]`
+ * but with drunk-animals verb vocabulary (drinking-themed words).
+ *
+ * Output: /tmp/drunk-animals-title-rewrites.json
+ */
+'use strict';
+
+const fs = require('fs');
+
+const RESULTS = '/tmp/drunk-animals-audit-results.jsonl';
+const OUT = '/tmp/drunk-animals-title-rewrites.json';
+
+// Drunk-animals verb vocabulary — drinking + party adjacent words.
+const VERBS = new Set([
+ 'Soirée', 'Soiree', 'Folly', 'Spree', 'Toast', 'Saunter', 'Revel',
+ 'Bacchanal', 'Nightcap', 'Carouse', 'Carousing', 'Bender',
+ 'Reverie', 'Frolic', 'Saunter', 'Tipple', 'Tippler',
+ // Some shared with stoned (the titler reused verbs across collections)
+ 'Drift', 'Haze', 'Lullaby', 'Halcyon', 'Vespers', 'Repose', 'Slumber',
+ 'Idyll', 'Daydream',
+]);
+
+const MULTI_COLORS = [
+ 'Bottle Green', 'Antique Brass', 'Antique Tan', 'Bone Ivory',
+ 'Bordeaux Velvet', 'Champagne Linen', 'Deep Plum', 'Forest Loden',
+ 'Greenhouse Glass', 'Indigo Library', 'Manor Saddle', 'Midnight Navy',
+ 'Saddle Mocha', 'Slate Mist', 'Smoke Ash', 'Stone Pewter',
+];
+
+const PLURAL_EXCEPTIONS = {
+ fox: 'foxes', wolf: 'wolves', mouse: 'mice', ox: 'oxen',
+ deer: 'deer', sheep: 'sheep', moose: 'moose', fish: 'fish',
+ goose: 'geese', child: 'children', octopus: 'octopuses',
+ bunny: 'bunnies', puppy: 'puppies',
+};
+
+function pluralize(s) {
+ s = (s || '').trim().toLowerCase();
+ if (!s) return '';
+ if (PLURAL_EXCEPTIONS[s]) return PLURAL_EXCEPTIONS[s];
+ if (s.endsWith('s') || s.endsWith('x') || s.endsWith('z') ||
+ s.endsWith('ch') || s.endsWith('sh')) return s + 'es';
+ if (s.endsWith('y') && !'aeiou'.includes(s[s.length - 2])) return s.slice(0, -1) + 'ies';
+ if (s.endsWith('f')) return s.slice(0, -1) + 'ves';
+ if (s.endsWith('fe')) return s.slice(0, -2) + 'ves';
+ return s + 's';
+}
+
+function titleCase(s) { return s.replace(/\b\w/g, c => c.toUpperCase()); }
+
+function parseTitle(title) {
+ if (!title) return null;
+ const t = title.trim();
+ // Format B: "X Verb in Color"
+ const mB = t.match(/^(\S+)\s+(\S+)\s+in\s+(.+)$/);
+ if (mB) {
+ const [, first, verb, colorRaw] = mB;
+ if (VERBS.has(verb)) {
+ return { kind: 'species-mismatch', old_animal_plural: first, verb, color: colorRaw.trim() };
+ }
+ }
+ // Format A: "Color Verb No.ID"
+ for (const mc of MULTI_COLORS) {
+ if (t.startsWith(mc + ' ')) {
+ const rest = t.slice(mc.length + 1);
+ const mA = rest.match(/^(\S+)\s+No\.\d+/);
+ if (mA && VERBS.has(mA[1])) {
+ return { kind: 'color-only', color: mc, verb: mA[1] };
+ }
+ }
+ }
+ const mAs = t.match(/^(\S+)\s+(\S+)\s+No\.\d+/);
+ if (mAs && VERBS.has(mAs[2])) {
+ return { kind: 'color-only', color: mAs[1], verb: mAs[2] };
+ }
+ return null;
+}
+
+const MULTI_WORD_OK = ['red panda', 'polar bear', 'snow leopard', 'sea otter',
+ 'tree frog', 'flying fox', 'kookaburra', 'sloth bear'];
+
+function cleanAnimal(s) {
+ s = (s || '').toLowerCase().trim();
+ if (!s || s === 'indistinct' || s === 'none') return '';
+ s = s.replace(/^(a|an|the)\s+/, '');
+ s = s.split(/[,/]|\sand\s/)[0].trim();
+ if (MULTI_WORD_OK.includes(s)) return s;
+ const parts = s.split(/\s+/);
+ return parts[parts.length - 1];
+}
+
+// Post-hoc title-drift detection for drunk-animals audit results.
+// title_animal extraction: first capitalized word in title before a verb.
+function detectDrift(title, animal) {
+ const cleaned = cleanAnimal(animal);
+ if (!cleaned) return { drift: false };
+ const tm = (title || '').match(/^([A-Z][a-z]+)s?\s+/);
+ const titleAnimal = tm ? tm[1].toLowerCase() : '';
+ // No animal at all in title (color-only or color-prefixed) = color-injection drift
+ // (we want to add the actual species). Color names like "Olive", "Bottle"
+ // start with capital but aren't species — distinguish by checking if it's a verb.
+ // Simplest: if the FIRST word can be parsed as a color, then no species was claimed.
+ const parsed = parseTitle(title);
+ if (!parsed) return { drift: false };
+ if (parsed.kind === 'color-only') return { drift: true, title_animal: '', kind: 'color-only' };
+ if (parsed.kind === 'species-mismatch') {
+ const tAn = parsed.old_animal_plural.toLowerCase().replace(/s$/, '');
+ if (tAn !== cleaned && !tAn.includes(cleaned) && !cleaned.includes(tAn)) {
+ return { drift: true, title_animal: tAn, kind: 'species-mismatch' };
+ }
+ }
+ return { drift: false };
+}
+
+function main() {
+ const rows = fs.readFileSync(RESULTS, 'utf8').split('\n').filter(Boolean).map(JSON.parse);
+ const keeps = rows.filter(r => r.verdict === 'KEEP');
+ console.log(`drunk-animals KEEPs: ${keeps.length}`);
+
+ const drifted = [];
+ for (const r of keeps) {
+ const drift = detectDrift(r.title, r.animal);
+ if (drift.drift) drifted.push({ ...r, ...drift });
+ }
+ console.log(`drifted (need re-title): ${drifted.length}`);
+
+ const rewrites = [];
+ const skipped = [];
+ for (const r of drifted) {
+ const parsed = parseTitle(r.title);
+ if (!parsed) { skipped.push({ id: r.id, reason: 'unparseable' }); continue; }
+ const animal = cleanAnimal(r.animal);
+ if (!animal) { skipped.push({ id: r.id, reason: 'animal blank' }); continue; }
+ const plural = titleCase(pluralize(animal));
+ const newTitle = `${plural} ${parsed.verb} in ${parsed.color}`;
+ if (newTitle === r.title) continue;
+ rewrites.push({ id: r.id, old_title: r.title, new_title: newTitle,
+ animal, verb: parsed.verb, color: parsed.color, kind: parsed.kind });
+ }
+
+ console.log(`generated rewrites: ${rewrites.length}`);
+ console.log(`skipped: ${skipped.length}`);
+
+ fs.writeFileSync(OUT, JSON.stringify(rewrites, null, 2));
+ console.log(`saved → ${OUT}`);
+ console.log('\n=== 12 SAMPLE REWRITES ===');
+ rewrites.slice(0, 6).forEach(r =>
+ console.log(` #${r.id.toString().padStart(5)} '${r.old_title}'\n → '${r.new_title}' [${r.kind}]`));
+ console.log(' ...');
+ rewrites.slice(-6).forEach(r =>
+ console.log(` #${r.id.toString().padStart(5)} '${r.old_title}'\n → '${r.new_title}' [${r.kind}]`));
+
+ const kinds = {};
+ rewrites.forEach(r => kinds[r.kind] = (kinds[r.kind] || 0) + 1);
+ console.log(`\n=== KIND DISTRIBUTION ===`);
+ Object.entries(kinds).forEach(([k, v]) => console.log(` ${k}: ${v}`));
+
+ const species = {};
+ rewrites.forEach(r => species[r.animal] = (species[r.animal] || 0) + 1);
+ console.log(`\n=== TOP SPECIES (post-rewrite) ===`);
+ Object.entries(species).sort((a, b) => b[1] - a[1]).slice(0, 12).forEach(([k, v]) =>
+ console.log(` ${v.toString().padStart(3)}× ${k}`));
+}
+
+main();
← 021e9d8 admin: rescue-curator — per-species drunk-animals review w/
·
back to Wallco Ai
·
Color-targeted llama+crane colorways (54992-54998) replace r 549ea46 →