[object Object]

← 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

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 →