← back to Wallco Ai
tooling: repoint-stale-paths (350 removed designs → quarantine file locations; 5 published-broken flagged) + move-drunk-damasks (vision-classify luxe-v2 drunk-animals, moved 73 published damasks → damask collection; 43 animals/7 floral left). Gemini thinkingBudget=0 for one-word classify
6371c5c7189fed9632b8c62545a88791a45061ef · 2026-05-27 10:57:26 -0700 · Steve Abrams
Files touched
A scripts/move-drunk-damasks.jsA scripts/repoint-stale-paths.js
Diff
commit 6371c5c7189fed9632b8c62545a88791a45061ef
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 27 10:57:26 2026 -0700
tooling: repoint-stale-paths (350 removed designs → quarantine file locations; 5 published-broken flagged) + move-drunk-damasks (vision-classify luxe-v2 drunk-animals, moved 73 published damasks → damask collection; 43 animals/7 floral left). Gemini thinkingBudget=0 for one-word classify
---
scripts/move-drunk-damasks.js | 82 ++++++++++++++++++++++++++++++++++++++++++
scripts/repoint-stale-paths.js | 55 ++++++++++++++++++++++++++++
2 files changed, 137 insertions(+)
diff --git a/scripts/move-drunk-damasks.js b/scripts/move-drunk-damasks.js
new file mode 100644
index 0000000..55eb8f4
--- /dev/null
+++ b/scripts/move-drunk-damasks.js
@@ -0,0 +1,82 @@
+#!/usr/bin/env node
+/**
+ * move-drunk-damasks — the drunk-animals collection is polluted with luxe-v2
+ * heritage variants that drifted off-concept. This vision-classifies the
+ * luxe-v2 drunk-animals (damask / animals / floral / other) and MOVES the
+ * confirmed damasks to category='damask' (kept published — they're good).
+ * Animals/floral/other are left untouched (reported for follow-up).
+ *
+ * node scripts/move-drunk-damasks.js # published only, dry-run (classify + report)
+ * node scripts/move-drunk-damasks.js --apply # published only, move damasks
+ * node scripts/move-drunk-damasks.js --all --apply # include unpublished
+ * node scripts/move-drunk-damasks.js --limit 5 # quick sample
+ */
+'use strict';
+const { execFileSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
+
+const KEY = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
+if (!KEY) { console.error('GEMINI_API_KEY missing'); process.exit(1); }
+const APPLY = process.argv.includes('--apply');
+const ALL = process.argv.includes('--all');
+const LIMIT = (() => { const i = process.argv.indexOf('--limit'); return i >= 0 ? parseInt(process.argv[i + 1], 10) : 0; })();
+const CONC = 4;
+
+function psql(sql) { return execFileSync('psql', ['-d', 'dw_unified', '-tAF', '\t', '-c', sql], { encoding: 'utf8', maxBuffer: 1e9 }); }
+function psqlExec(sql) { return execFileSync('psql', ['-d', 'dw_unified', '-tAc', sql], { encoding: 'utf8' }); }
+
+const where = "category ILIKE '%drunk%' AND generator ILIKE '%luxe%'" + (ALL ? '' : ' AND is_published');
+let rows = psql(`SELECT id, local_path FROM spoon_all_designs WHERE ${where} ORDER BY is_published DESC, id`)
+ .trim().split('\n').filter(l => l.includes('\t')).map(l => { const [id, lp] = l.split('\t'); return { id: parseInt(id, 10), lp }; });
+if (LIMIT) rows = rows.slice(0, LIMIT);
+console.log(`classifying ${rows.length} luxe-v2 drunk-animals${ALL ? ' (incl unpublished)' : ' (published)'}${APPLY ? ' — APPLY' : ' — DRY-RUN'}`);
+
+const PROMPT = "Classify this wallpaper into ONE category. Answer with ONLY one word:\n"
+ + "damask = formal symmetric ornamental medallion / scroll / brocade repeat, NO animals.\n"
+ + "animals = contains animal figures (any creatures).\n"
+ + "floral = flowers / leaves / botanical, no animals.\n"
+ + "other = none of the above.\n"
+ + "One word only: damask | animals | floral | other";
+
+async function classify(lp) {
+ if (!lp || !fs.existsSync(lp)) return 'missing';
+ const b64 = fs.readFileSync(lp).toString('base64');
+ const body = { contents: [{ parts: [{ inline_data: { mime_type: 'image/png', data: b64 } }, { text: PROMPT }] }],
+ generationConfig: { temperature: 0, maxOutputTokens: 200, thinkingConfig: { thinkingBudget: 0 } } };
+ try {
+ const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${KEY}`,
+ { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+ if (!r.ok) return 'err';
+ const j = await r.json();
+ const t = (j.candidates?.[0]?.content?.parts?.[0]?.text || '').toLowerCase();
+ if (t.includes('damask')) return 'damask';
+ if (t.includes('animal')) return 'animals';
+ if (t.includes('floral')) return 'floral';
+ if (t.includes('other')) return 'other';
+ return 'unknown';
+ } catch { return 'err'; }
+}
+
+(async () => {
+ const counts = {}; const damaskIds = [];
+ let idx = 0;
+ async function worker() {
+ while (idx < rows.length) {
+ const r = rows[idx++];
+ const cls = await classify(r.lp);
+ counts[cls] = (counts[cls] || 0) + 1;
+ if (cls === 'damask') damaskIds.push(r.id);
+ if (idx % 20 === 0) process.stderr.write(` …${idx}/${rows.length}\n`);
+ }
+ }
+ await Promise.all(Array.from({ length: CONC }, worker));
+ console.log('classification:', JSON.stringify(counts));
+ console.log(`damasks found: ${damaskIds.length}`);
+ if (!damaskIds.length) return;
+ if (!APPLY) { console.log('DRY-RUN — re-run with --apply to move them to category=damask. ids:', JSON.stringify(damaskIds.slice(0, 30)) + (damaskIds.length > 30 ? '…' : '')); return; }
+ const ids = damaskIds.join(',');
+ psqlExec(`UPDATE spoon_all_designs SET category='damask' WHERE id IN (${ids});`);
+ console.log(`MOVED ${damaskIds.length} damasks → category='damask' (kept published)`);
+})();
diff --git a/scripts/repoint-stale-paths.js b/scripts/repoint-stale-paths.js
new file mode 100644
index 0000000..7c174f5
--- /dev/null
+++ b/scripts/repoint-stale-paths.js
@@ -0,0 +1,55 @@
+#!/usr/bin/env node
+/**
+ * repoint-stale-paths — fix designs whose local_path points to a missing file.
+ * When a design is marked Bad, the PNG is moved to a *_quarantine dir but
+ * local_path is never updated, leaving a stale path. This repoints those to
+ * the real quarantine location. Files that are genuinely gone (e.g. purged
+ * heal-derivatives) can't be repointed and are reported.
+ * Scope: cactus + drunk-* (the curated collections). Dry-run unless --apply.
+ */
+'use strict';
+const { execFileSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.join(__dirname, '..');
+const APPLY = process.argv.includes('--apply');
+const QDIRS = ['data/generated_cactus_quarantine', 'data/generated_ghost_quarantine'];
+
+function psql(sql) {
+ return execFileSync('psql', ['-d', 'dw_unified', '-tAF', '\t', '-c', sql], { encoding: 'utf8', maxBuffer: 1e9 });
+}
+
+const rows = psql(
+ "SELECT id, local_path, is_published FROM spoon_all_designs " +
+ "WHERE (category ILIKE '%cactus%' OR category ILIKE '%drunk%') AND local_path IS NOT NULL"
+).trim().split('\n').filter(l => l.includes('\t'));
+
+const repoint = []; // [id, newPath]
+const pubBroken = []; // published designs with no file anywhere
+let removedGone = 0;
+
+for (const l of rows) {
+ const [idS, lp, pub] = l.split('\t');
+ if (fs.existsSync(lp)) continue;
+ const bn = path.basename(lp);
+ let found = null;
+ for (const d of QDIRS) { const c = path.join(ROOT, d, bn); if (fs.existsSync(c)) { found = c; break; } }
+ if (found) repoint.push([parseInt(idS, 10), found]);
+ else if (pub === 't') pubBroken.push(parseInt(idS, 10));
+ else removedGone++;
+}
+
+console.log(`stale → repointable: ${repoint.length} | published-broken (file gone): ${pubBroken.length} | removed-gone: ${removedGone}`);
+console.log('published-broken ids:', JSON.stringify(pubBroken));
+
+if (!repoint.length) { console.log('nothing to repoint'); process.exit(0); }
+if (!APPLY) { console.log(`DRY-RUN — re-run with --apply to repoint ${repoint.length} designs`); process.exit(0); }
+
+// Batch update via a temp file (VALUES join).
+const vals = repoint.map(([id, p]) => `(${id}, ` + "'" + p.replace(/'/g, "''") + "')").join(',\n');
+const sqlFile = '/tmp/repoint-stale.sql';
+fs.writeFileSync(sqlFile, `BEGIN;\nUPDATE spoon_all_designs s SET local_path = v.p FROM (VALUES\n${vals}\n) AS v(id, p) WHERE s.id = v.id;\nCOMMIT;\n`);
+const out = execFileSync('psql', ['-d', 'dw_unified', '-f', sqlFile], { encoding: 'utf8' });
+console.log(out.trim());
+console.log(`repointed ${repoint.length} designs → quarantine paths`);
← 40ef64b drunk-animals: scrub 'label'/'labeled' positive cues from bo
·
back to Wallco Ai
·
add /admin/drunk-curator hot-or-not picker: HOT publishes, N c8123d2 →