← back to Wallco Ai
lib/design-references.js
227 lines
/**
* Design reference picker — pulls art-historical anchors from
* (a) Met Museum public-domain artworks (dw_unified.met_museum_art)
* (b) prior wallco designs already in the catalog (spoon_all_designs)
* for use as style/composition anchors inside drunk-animal /
* drunk-zoo / mural generator prompts.
*
* Steve directive 2026-05-25 — "designs pulled from the postgresql
* and met museum images" + "this is our llm". We use real PD artwork
* and our own prior winners as the inspiration layer for every new
* prompt, instead of relying purely on the diffusion model's training.
*
* USAGE:
* const refs = require('./lib/design-references');
* const met = refs.pickMetReference(['bacchanal', 'wine']);
* const own = refs.pickPriorDesignReference('drunk-animals');
* // append to prompt body:
* // `inspired by ${met.compactCitation}, composition lineage from ${own.compactCitation}`
*
* Both pickers are SYNCHRONOUS (they use execSync against a pre-warmed
* in-memory cache) so the generator hot paths stay simple. Cache
* refresh happens at module-load + once every 6 hours (configurable
* via DESIGN_REFS_TTL_SEC).
*
* If PG is unreachable, fall back to a tiny hardcoded list so the
* generators never throw — just less variety.
*/
'use strict';
const { execSync } = require('child_process');
// ── Theme pools — Met PD query templates keyed by drunk-animal vibe ───
const MET_THEMES = {
bacchanal: { sql: "LOWER(title) ~ 'bacchanal|bacchus|dionys|reveler|carous|silen|priap' OR (LOWER(title) ~ 'feast' AND classification IN ('Prints','Paintings','Drawings'))", weight: 3 },
wine: { sql: "(LOWER(title) ~ 'wine|cup|goblet|chalice|amphora|krater|kantharos|tankard|tavern|drinking') AND classification IN ('Prints','Paintings','Drawings','Ceramics','Glass','Vases')", weight: 2 },
engraving: { sql: "classification ILIKE 'Prints' AND object_begin_date BETWEEN 1700 AND 1899", weight: 1 },
animal: { sql: "LOWER(title) ~ 'monkey|giraffe|elephant|orangutan|lemur|sloth|capybara|frog|macaw|parrot|peacock|owl|panda' AND classification IN ('Prints','Paintings','Drawings')", weight: 2 },
chinoiserie: { sql: "(department = 'Asian Art' AND LOWER(medium) ~ 'silk|cloth|textile') OR LOWER(title) ~ 'chinoiserie|wallpaper'", weight: 1 },
};
// In-memory cache. Key = theme name → array of Met rows.
const METROW_CACHE = {};
let MET_CACHE_AT = 0;
const TTL_SEC = parseInt(process.env.DESIGN_REFS_TTL_SEC || '21600', 10); // 6 hr
// Cap per-theme pool size to keep memory + query cost bounded.
const POOL_SIZE = parseInt(process.env.DESIGN_REFS_POOL_SIZE || '120', 10);
function psqlJson(sql) {
try {
const cmd = `psql dw_unified -At -F'\t' -c ${JSON.stringify(sql)}`;
return execSync(cmd, { encoding: 'utf8', timeout: 12000 });
} catch (e) {
return '';
}
}
function refreshMetCache() {
const now = Date.now();
if (now - MET_CACHE_AT < TTL_SEC * 1000 && Object.keys(METROW_CACHE).length) return;
for (const [theme, cfg] of Object.entries(MET_THEMES)) {
const sql =
`SELECT object_id, title, COALESCE(artist,'Unknown') AS artist, ` +
`COALESCE(NULLIF(date_display,''), object_date, 'date unknown') AS date_display, ` +
`COALESCE(NULLIF(classification,''), 'Artwork') AS classification, ` +
`COALESCE(NULLIF(met_url,''), 'https://www.metmuseum.org/art/collection/search/' || object_id) AS met_url, ` +
`COALESCE(NULLIF(primary_image_small,''), NULLIF(image_url,''), NULLIF(primary_image,'')) AS image ` +
`FROM met_museum_art ` +
`WHERE is_public_domain AND active AND (${cfg.sql}) ` +
`ORDER BY random() LIMIT ${POOL_SIZE};`;
const raw = psqlJson(sql);
const rows = raw.split('\n').filter(Boolean).map(line => {
const [object_id, title, artist, date_display, classification, met_url, image] = line.split('\t');
return {
source: 'met',
theme,
object_id,
title: (title || '').slice(0, 120),
artist: (artist || 'Unknown').split('|')[0].slice(0, 80),
date_display: (date_display || '').slice(0, 40),
classification: classification || '',
met_url, image,
};
});
METROW_CACHE[theme] = rows;
}
MET_CACHE_AT = now;
}
// Fallback list — used only when PG is unreachable.
const MET_FALLBACK = [
{ source: 'met', theme: 'bacchanal', object_id: '345721', title: 'A Bacchanal (Silenus & bacchants)', artist: 'Marcantonio Raimondi', date_display: 'ca. 1510–13', classification: 'Prints' },
{ source: 'met', theme: 'wine', object_id: '436106', title: 'Still Life with Wine Bottle and Apples', artist: 'Henri Fantin-Latour', date_display: '1876', classification: 'Paintings' },
{ source: 'met', theme: 'engraving', object_id: '720507', title: 'Autumn (Wine Harvest)', artist: 'Aignan Thomas Desfriches', date_display: '1768', classification: 'Prints' },
{ source: 'met', theme: 'animal', object_id: '435716', title: 'Studies of Monkeys', artist: 'Antoine Watteau', date_display: 'ca. 1714–16', classification: 'Drawings' },
];
function pickRand(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
function compactCitation(row) {
if (!row) return null;
const yr = row.date_display ? `, ${row.date_display}` : '';
return `the Met's "${row.title}" (${row.artist}${yr})`;
}
/**
* Pick a Met Museum PD reference matching one or more themes.
* @param {string[]} themes e.g. ['bacchanal', 'wine']. Defaults to all.
* @returns {object} { theme, title, artist, date_display, met_url, compactCitation }
*/
function pickMetReference(themes) {
refreshMetCache();
const pool = [];
const want = (themes && themes.length) ? themes : Object.keys(MET_THEMES);
for (const t of want) {
const rows = METROW_CACHE[t] || [];
const weight = (MET_THEMES[t] && MET_THEMES[t].weight) || 1;
for (let i = 0; i < weight; i++) pool.push(...rows);
}
const row = pool.length ? pickRand(pool) : pickRand(MET_FALLBACK);
return { ...row, compactCitation: compactCitation(row) };
}
// ── Prior wallco designs from PG (composition reference) ──────────────
const PRIOR_CACHE = {};
let PRIOR_CACHE_AT = 0;
function refreshPriorCache() {
const now = Date.now();
if (now - PRIOR_CACHE_AT < TTL_SEC * 1000 && Object.keys(PRIOR_CACHE).length) return;
// Pull the top-rated published prior designs per category (stars desc),
// then random within ties.
const sql =
`SELECT id, ` +
`CASE WHEN category LIKE 'drunk-zoo-36%' THEN 'drunk-zoo-36' ELSE category END AS bucket, ` +
`COALESCE(LEFT(prompt, 160), '') AS prompt_head, ` +
`COALESCE(user_stars, 0) AS stars, ` +
`COALESCE(dominant_hex, '#888888') AS dominant_hex ` +
`FROM spoon_all_designs ` +
`WHERE is_published AND (category IN ('drunk-animals','drunk-monkeys-v2','drunk-animals-designer') ` +
`OR category LIKE 'drunk-zoo-36%') ` +
`ORDER BY stars DESC NULLS LAST, random() LIMIT 600;`;
const raw = psqlJson(sql);
const buckets = {};
for (const line of raw.split('\n').filter(Boolean)) {
const [id, bucket, prompt_head, stars, dominant_hex] = line.split('\t');
if (!buckets[bucket]) buckets[bucket] = [];
buckets[bucket].push({
source: 'prior',
id: parseInt(id, 10),
category: bucket,
prompt_head: (prompt_head || '').slice(0, 160),
stars: parseInt(stars, 10) || 0,
dominant_hex,
});
}
Object.assign(PRIOR_CACHE, buckets);
PRIOR_CACHE_AT = now;
}
function pickPriorDesignReference(category) {
refreshPriorCache();
const bucket =
(category && category.startsWith('drunk-zoo-36')) ? 'drunk-zoo-36' :
category || 'drunk-animals';
const rows = PRIOR_CACHE[bucket] || PRIOR_CACHE['drunk-animals'] || [];
if (!rows.length) return null;
const row = pickRand(rows);
return {
...row,
compactCitation: `composition lineage from wallco internal design #${row.id} (${row.dominant_hex} dominant)`,
};
}
// ── Combined helper — return both refs ready-to-paste into a prompt ──
function pickReferences(category) {
const m = pickMetReference(themesForCategory(category));
const p = pickPriorDesignReference(category);
return {
met: m,
prior: p,
promptClause: [
m && `inspired by ${m.compactCitation}`,
p && p.compactCitation,
].filter(Boolean).join(', '),
};
}
function themesForCategory(category) {
// Map each drunk-animal sub-category to a sensible Met theme mix.
if (!category) return ['bacchanal', 'wine', 'animal'];
if (category.startsWith('drunk-zoo-36')) return ['bacchanal', 'animal', 'engraving'];
if (category === 'drunk-monkeys-v2') return ['animal', 'bacchanal'];
if (category === 'drunk-animals-designer') return ['chinoiserie', 'wine', 'engraving'];
return ['bacchanal', 'wine', 'animal'];
}
module.exports = {
pickMetReference,
pickPriorDesignReference,
pickReferences,
themesForCategory,
MET_THEMES,
};
// CLI smoke-test
if (require.main === module) {
console.log('Met cache warming...');
refreshMetCache();
for (const t of Object.keys(MET_THEMES)) {
console.log(` ${t}: ${(METROW_CACHE[t]||[]).length} rows`);
}
console.log('\nPrior cache warming...');
refreshPriorCache();
for (const b of Object.keys(PRIOR_CACHE)) {
console.log(` ${b}: ${PRIOR_CACHE[b].length} rows`);
}
console.log('\nSample reference pairs:');
for (const cat of ['drunk-animals', 'drunk-zoo-36', 'drunk-monkeys-v2', 'drunk-animals-designer']) {
const r = pickReferences(cat);
console.log(`\n [${cat}]`);
console.log(` met: ${r.met && r.met.compactCitation}`);
console.log(` prior: ${r.prior && r.prior.compactCitation}`);
console.log(` clause: ${r.promptClause}`);
}
}