← back to Patterndesignlab

scripts/seed.js

159 lines

// Seed patterndesignlab from the AUTHORITATIVE catalog (scripts/build-catalog.js output).
// Every design carries its OWN correct local image — no image-pool cycling, no mismatches.
// OWNED content only. Facets derived from existing metadata; NO paid AI tagging this round.
// Dominant hex is used from source metadata or computed locally via ImageMagick ($0 local).
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const { Client } = require('pg');

const ROOT = path.join(__dirname, '..');
const PATTERN_DIR = path.join(ROOT, 'public', 'assets', 'patterns');
const SRC = JSON.parse(fs.readFileSync(path.join(ROOT, 'data', 'catalog.json'), 'utf8'));

const domCache = {};
function dominantHex(file) {
  if (domCache[file]) return domCache[file];
  try {
    const hex = execFileSync('magick', [path.join(PATTERN_DIR, file), '-resize', '1x1!', '-depth', '8', '-format', '%[hex:p{0,0}]', 'info:-'], { encoding: 'utf8' }).trim().slice(0, 6);
    const v = '#' + hex.toLowerCase();
    domCache[file] = v; return v;
  } catch { return '#cccccc'; }
}

// ---- facet derivation (keyword-driven, defendable, local) ----
const low = s => (s || '').toLowerCase();
function bag(r) {
  return low([r.title, r.styleLine, r.category, r.kind,
    (r.tags || []).join(' '), (r.motifs || []).join(' ')].join(' '));
}

function deriveStyle(t) {
  if (/moody|nocturne|noir|dark/.test(t)) return 'Dark Moody Floral';
  if (/mid-century|atomic|retro/.test(t)) return 'Mid-Century Retro';
  if (/coastal|tide|nautical|monterey/.test(t)) return 'Coastal';
  if (/stripe/.test(t)) return 'Stripe';
  if (/folk|block|geometr/.test(t)) return 'Folk Geometric';
  if (/chinoiserie/.test(t)) return 'Chinoiserie';
  if (/deco/.test(t)) return 'Art Deco';
  if (/toile/.test(t)) return 'Toile';
  if (/animal|fox|sloth|raccoon|zoo|muybridge|drunk|stoned|saddle/.test(t)) return 'Animal';
  if (/heritage|botanical|trellis|vine|ditsy|garden|calico|lattice|bough/.test(t)) return 'Heritage Botanical';
  if (/texture/.test(t)) return 'Texture';
  return 'Modern';
}
function deriveMotif(t) {
  if (/animal|fox|sloth|raccoon|zoo|muybridge|drunk|stoned|saddle/.test(t)) return 'animal';
  if (/mural/.test(t)) return 'mural';
  if (/stripe|tide|line/.test(t)) return 'stripe';
  if (/folk|block|geometr|deco|lattice|trellis|tile/.test(t)) return 'geometric';
  if (/floral|bloom|rose|camellia|magnolia|garden|meadow|botanical|vine|calico|bough|folio|carouse/.test(t)) return 'floral';
  if (/texture/.test(t)) return 'texture';
  return 'abstract';
}
const COLORWAYS = ['Greige','Chamois','Apricot','Oxblood','Celadon','Terracotta','Prussian','Honey','Bottle Green','Claret','Indigo','Teal','Crimson','Amethyst','Emerald','Rosewater','Blush','Plum','Sage','Cream'];
function deriveColorway(r, hex) {
  const t = low([r.title, r.styleLine].join(' '));
  for (const c of COLORWAYS) if (t.includes(c.toLowerCase())) return c;
  return hexFamily(hex);
}
function hexFamily(hex) {
  const m = /^#?([0-9a-f]{6})$/i.exec(hex || ''); if (!m) return 'Neutral';
  const n = parseInt(m[1], 16), r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
  if (max - min < 24) return b > 200 ? 'Cream' : (b < 70 ? 'Charcoal' : 'Greige');
  if (r >= g && r >= b) return g > 120 ? 'Ochre' : 'Terracotta';
  if (g >= r && g >= b) return b > g - 20 ? 'Teal' : 'Green';
  return r > 120 ? 'Plum' : 'Indigo';
}
function deriveTechnique(t) {
  if (/block|folk/.test(t)) return 'block-print';
  if (/wash|cottage|painterly|watercolo/.test(t)) return 'painterly';
  if (/flock|velvet/.test(t)) return 'flocked';
  return 'digital';
}
function slugify(s) { return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60); }

// credible vs novelty — mirrors build-catalog.js so the catalog can report/facet honestly
const NOVELTY_RE = /drunk|stoned|muybridge|zoo|\bdogs?\b|designer-bugs|monkey|circus|skull/i;
function classify(r) { return r.contentClass || (NOVELTY_RE.test(r.category || '') ? 'novelty' : 'credible'); }

// ---- 3 seed designer profiles (designs distributed by style family) ----
const DESIGNERS = [
  { slug: 'studio-verdant', name: 'Studio Verdant', country: 'United Kingdom',
    bio: 'Heritage botanicals and trellis work — trellises, vines and ditsy florals drawn in our own hand, all settlement-passed originals.',
    avatar: '/assets/patterns/toile-01_150dpi.png',
    match: t => /heritage|botanical|trellis|vine|ditsy|garden|calico|lattice|bough|chinoiserie|toile/.test(t) },
  { slug: 'atelier-nocturne', name: 'Atelier Nocturne', country: 'France',
    bio: 'Dark, moody florals and Art Deco geometry — saturated nocturnes for dramatic interiors.',
    avatar: '/assets/patterns/deco-01_150dpi.png',
    match: t => /moody|nocturne|noir|dark|deco|magnolia|camellia|crimson|amethyst|emerald|plum/.test(t) },
  { slug: 'field-and-fable', name: 'Field & Fable', country: 'United States',
    bio: 'Playful animals, mid-century retro, coastal and folk geometry — characterful patterns with a story.',
    avatar: '/assets/patterns/animalprint-01_150dpi.png',
    match: t => /animal|fox|sloth|raccoon|zoo|muybridge|drunk|stoned|saddle|mid-century|atomic|retro|coastal|tide|stripe|folk|block|geometr|texture/.test(t) },
];
function pickDesigner(t, i) {
  for (const d of DESIGNERS) if (d.match(t)) return d.slug;
  return DESIGNERS[i % DESIGNERS.length].slug; // deterministic fallback
}

(async () => {
  const db = new Client({ host: '127.0.0.1', user: 'dw_admin', database: 'patterndesignlab' });
  await db.connect();
  await db.query('TRUNCATE designs, designers RESTART IDENTITY CASCADE');

  const idBySlug = {};
  for (const d of DESIGNERS) {
    const r = await db.query(
      'INSERT INTO designers(slug,name,bio,country,avatar) VALUES($1,$2,$3,$4,$5) RETURNING id',
      [d.slug, d.name, d.bio, d.country, d.avatar]);
    idBySlug[d.slug] = r.rows[0].id;
  }

  const usedSlug = new Set();
  let n = 0;
  for (let i = 0; i < SRC.length; i++) {
    const r = SRC[i];
    const t = bag(r);
    const img = r.img;                       // each design's OWN correct local tile (from build-catalog)
    if (!img) continue;                      // never seed a design without its real image
    let hex = (r.dominantHex && /^#?[0-9a-fA-F]{6}$/.test(r.dominantHex)) ? (r.dominantHex[0] === '#' ? r.dominantHex : '#' + r.dominantHex) : dominantHex(img.split('/').pop());
    hex = hex.toLowerCase();
    const style = deriveStyle(t);
    const motif = deriveMotif(t);
    const colorway = deriveColorway(r, hex);
    const technique = deriveTechnique(t);
    const seamless = (r.kind || 'seamless_tile') === 'seamless_tile';
    const tiers = Array.isArray(r.licenseTiers) ? r.licenseTiers : [];
    const prices = tiers.map(x => Number(x.priceUsd)).filter(Number.isFinite);
    const priceMin = prices.length ? Math.min(...prices) : null;
    const priceMax = prices.length ? Math.max(...prices) : null;
    const tags = Array.from(new Set([style, motif, colorway, technique, r.category, seamless ? 'seamless' : 'non-seamless',
      ...(r.tags || []), ...(r.motifs || []),
      ...r.title.split(/[^a-zA-Z]+/).filter(w => w.length > 2)]
      .filter(Boolean).map(s => String(s).toLowerCase())));
    let slug = slugify(r.title) || slugify(String(r.id));
    let s2 = slug, k = 2; while (usedSlug.has(s2)) s2 = slug + '-' + (k++); slug = s2; usedSlug.add(slug);

    await db.query(
      `INSERT INTO designs(id,slug,title,designer_id,category,kind,style,motif,colorway,technique,
         seamless,dominant_hex,tags,img,room_img,style_line,provenance,license_tiers,price_min,price_max,
         pricing_status,source_id,created_at,content_class,search_tsv)
       VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,
         COALESCE($23::timestamptz, now()), $24, pdl_designs_tsv($3,$13,$7,$9,$8))`,
      [String(r.id), slug, r.title, idBySlug[pickDesigner(t, i)], r.category, r.kind || 'seamless_tile',
       style, motif, colorway, technique, seamless, hex, tags, img,
       null /* room preview is generated live via CSS tile-on-wall */, r.styleLine,
       (r.provenance && r.provenance !== 'None') ? r.provenance : 'owned · settlement-passed',
       JSON.stringify(tiers), priceMin, priceMax, r.pricingStatus || 'DRAFT', String(r.id),
       r.createdAt || null, classify(r)]);
    n++;
  }

  const counts = await db.query('SELECT d.name, count(*) FROM designs x JOIN designers d ON d.id=x.designer_id GROUP BY d.name ORDER BY 2 DESC');
  console.log(`Seeded ${n} designs across ${DESIGNERS.length} designers:`);
  counts.rows.forEach(c => console.log(`  ${c.name}: ${c.count}`));
  await db.end();
})().catch(e => { console.error(e); process.exit(1); });