← back to Dw Universe

scripts/gen-niche-poems.js

121 lines

// Generate one short editorial line per niche via Mac1 Ollama qwen3:14b.
// Stores at ~/Projects/dw-universe/data/niche-poems.json.
// Idempotent: skips niches already present.

const fs = require('fs');
const path = require('path');
const http = require('http');

const OLLAMA = 'http://192.168.1.133:11434';
const MODEL  = 'qwen3:14b';
const OUT    = path.join(__dirname, '..', 'data', 'niche-poems.json');

const NICHES = [
  ['silkwallpaper',          'Silk',                'A pure-silk wallcovering line — finest hand-woven silk from heritage mills.'],
  ['silkwallcoverings',      'Silk Wallcoverings',  'Silk wallcoverings cut to commercial / contract spec.'],
  ['linenwallpaper',         'Linen',               'Natural linen, woven and grasscloth-style.'],
  ['jutewallpaper',          'Jute',                'Coarse, honest jute fiber wallcovering.'],
  ['raffiawallcoverings',    'Raffia',              'Hand-woven raffia palm fiber, contract spec.'],
  ['raffiawalls',            'Raffia Walls',        'Raffia residential storefront — same fiber, residential edit.'],
  ['corkwallcovering',       'Cork',                'Sustainable cork wallcovering — bark of the cork oak.'],
  ['fabricwallpaper',        'Fabric',              'Anything fabric-on-paper: textiles backed for walls.'],
  ['textilewallpaper',       'Textile',             'Woven textile-effect wallpapers.'],
  ['metallicwallpaper',      'Metallic',            'Foil and metallic-leaf grounds — silver, gold, copper, gun-metal.'],
  ['silverleafwallpaper',    'Silver Leaf',         'Pure silver leaf grounds — hand-applied.'],
  ['vinylwallpaper',         'Vinyl',               'Type II commercial vinyl — hospitality-grade durability.'],
  ['micawallpaper',          'Mica',                'Hand-applied mica — flecks of fired mineral.'],
  ['madagascarwallpaper',    'Madagascar',          'Madagascar grasscloth — woven natural fibers.'],
  ['mylarwallpaper',         'Mylar',               'Reflective mylar — dimensional sheen, mid-century moods.'],
  ['suedewallpaper',         'Suede',               'Suede-textured walls — soft, light-eating, dramatic.'],
  ['1800swallpaper',         '1800s',               'Documentary 19th-century reproduction wallpapers.'],
  ['1890swallpaper',         '1890s',               'Late-Victorian Aesthetic-movement & Arts-and-Crafts patterns.'],
  ['1900swallpaper',         '1900s',               'Edwardian and turn-of-century repeats.'],
  ['1920swallpaper',         '1920s',               'Art Deco — geometric, gold, jazz-age.'],
  ['1930swallpaper',         '1930s',               'Streamlined Moderne — late Deco bleeding into Hollywood Regency.'],
  ['1940swallpaper',         '1940s',               'Post-war floral & restraint — kitchens, bedrooms.'],
  ['1950swallpaper',         '1950s',               'Atomic-age, mid-century optimism.'],
  ['1960swallpaper',         '1960s',               'Op-art, mod, psychedelia, swirls.'],
  ['1970swallpaper',         '1970s',               'Earth-tone florals, paneling, harvest gold.'],
  ['1980swallpaper',         '1980s',               'Memphis, neon, geometric pop, brass-and-glass excess.'],
  ['retrowalls',             'Retro Walls',         'Catch-all retro/vintage — across all decades.'],
  ['wallpapersback',         'Wallpapers Back',     'Statement: wallpaper is back — broad selection.'],
  ['agedwallpaper',          'Aged',                'Patina-aged finishes, faux-distressed.'],
  ['handcraftedwallpaper',   'Handcrafted',         'Block-printed, hand-screened, artisan-made.'],
  ['museumwallpaper',        'Museum',              'Wallcoverings fit for institutional / gallery walls.'],
  ['restorationwallpaper',   'Restoration',         'Period-accurate reproductions for historic homes.'],
  ['pastelwallpaper',        'Pastel',              'Soft, light-color palette across patterns.'],
  ['glitterwalls',           'Glitter',             'Genuine glitter and glass-bead grounds.'],
  ['greenwallcoverings',     'Green',               'Sustainable, FSC, low-VOC — eco-conscious wallcovering.'],
  ['naturalwallcoverings',   'Natural',             'Plant-fiber wallcoverings: jute, sisal, abaca, raffia.'],
  ['saloonwallpaper',        'Saloon',              'Old-West, woodcut, oxblood-and-cream saloon-print aesthetic.'],
  ['contractwallpaper',      'Contract',            'Type II commercial contract wallcovering.'],
  ['hotelwallcoverings',     'Hotel',               'Hospitality-grade, lobby-and-corridor durability.'],
  ['hospitalitywallpaper',   'Hospitality',         'Hotel + restaurant + cruise — durable, designed.'],
  ['healthcarewallpaper',    'Healthcare',          'Cleanable, anti-microbial, medical-environment rated.'],
  ['restaurantwallpaper',    'Restaurant',          'Front-of-house wallcovering — moody, photogenic, washable.'],
  ['architecturalwallcoverings', 'Architectural',   'Architect-spec wallcoverings — A&D channel.'],
];

function ollama(prompt) {
  return new Promise((resolve, reject) => {
    const body = JSON.stringify({
      model: MODEL, prompt, stream: false,
      options: { temperature: 0.85, top_p: 0.95, num_predict: 80 }
    });
    const req = http.request(`${OLLAMA}/api/generate`, {
      method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': body.length },
      timeout: 60000,
    }, (res) => {
      let chunks = '';
      res.on('data', d => chunks += d);
      res.on('end', () => {
        try { resolve(JSON.parse(chunks).response || ''); } catch (e) { reject(e); }
      });
    });
    req.on('error', reject);
    req.write(body); req.end();
  });
}

const PROMPT = (label, hint) => `Write ONE single sentence — fewer than 14 words — that describes "${label}" wallcoverings as if you were a high-end interior magazine editor. Tone: literary, sensory, not salesy. Avoid clichés ("timeless", "elegance", "sophistication"). Avoid quotation marks. Hint: ${hint}

Just the sentence. Nothing else. No "Here is" or preamble. No explanation. No newlines. No <think> tags or reasoning — output ONLY the final sentence.`;

function clean(s) {
  // Strip <think> blocks (qwen3 sometimes adds reasoning)
  s = s.replace(/<think>[\s\S]*?<\/think>/gi, '');
  // Strip leading "Here is..." / quotes / leading bullets
  s = s.replace(/^["'\s\-•*]+/, '').replace(/["'\s]+$/, '').trim();
  // Take first sentence only
  const firstSentence = s.match(/^[^.!?\n]+[.!?]?/);
  return (firstSentence ? firstSentence[0] : s).trim();
}

(async () => {
  fs.mkdirSync(path.dirname(OUT), { recursive: true });
  let existing = {};
  try { existing = JSON.parse(fs.readFileSync(OUT, 'utf8')); } catch (e) {}

  let done = 0, skipped = 0;
  for (const [slug, label, hint] of NICHES) {
    if (existing[slug] && existing[slug].length > 8) { skipped++; continue; }
    const t0 = Date.now();
    try {
      const raw = await ollama(PROMPT(label, hint));
      const line = clean(raw);
      if (!line || line.length < 8) {
        console.log(`  ⚠ ${slug}: empty/short — kept raw fragment`);
        existing[slug] = raw.slice(0, 200);
      } else {
        existing[slug] = line;
        done++;
      }
      console.log(`  ${slug.padEnd(28)} (${(Date.now()-t0)/1000|0}s) → ${existing[slug]}`);
      fs.writeFileSync(OUT, JSON.stringify(existing, null, 2)); // checkpoint each
    } catch (e) {
      console.error(`  ✗ ${slug}: ${e.message}`);
    }
  }
  console.log(`done: ${done} new, ${skipped} skipped, total ${Object.keys(existing).length}`);
})();