|
| 1 | +/** |
| 2 | + * generate-color-stories.mjs |
| 3 | + * Run once: node scripts/generate-color-stories.mjs |
| 4 | + * Requires GOOGLE_AI_API_KEY in environment (or .env.local) |
| 5 | + * Outputs: src/data/color-stories.json |
| 6 | + */ |
| 7 | + |
| 8 | +import { GoogleGenerativeAI } from "@google/generative-ai"; |
| 9 | +import { writeFileSync, readFileSync, existsSync } from "fs"; |
| 10 | +import { join, dirname } from "path"; |
| 11 | +import { fileURLToPath } from "url"; |
| 12 | + |
| 13 | +const __dirname = dirname(fileURLToPath(import.meta.url)); |
| 14 | + |
| 15 | +// Load .env.local if available |
| 16 | +const envPath = join(__dirname, "../.env.local"); |
| 17 | +if (existsSync(envPath)) { |
| 18 | + const env = readFileSync(envPath, "utf8"); |
| 19 | + for (const line of env.split("\n")) { |
| 20 | + const [k, ...v] = line.split("="); |
| 21 | + if (k && v.length) process.env[k.trim()] = v.join("=").trim(); |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +const API_KEY = process.env.GOOGLE_AI_API_KEY; |
| 26 | +if (!API_KEY) { |
| 27 | + console.error("Missing GOOGLE_AI_API_KEY"); |
| 28 | + process.exit(1); |
| 29 | +} |
| 30 | + |
| 31 | +const FAMILIES = [ |
| 32 | + { slug: "red", name: "Red", hex: "#e63946", hue: "warm reds and crimsons" }, |
| 33 | + { slug: "orange", name: "Orange", hex: "#f4a261", hue: "vibrant oranges and ambers" }, |
| 34 | + { slug: "yellow", name: "Yellow", hex: "#e9c46a", hue: "golden yellows and saffrons" }, |
| 35 | + { slug: "lime", name: "Lime", hex: "#90be6d", hue: "fresh lime greens and chartreuses" }, |
| 36 | + { slug: "green", name: "Green", hex: "#2a9d8f", hue: "deep greens and forest tones" }, |
| 37 | + { slug: "teal", name: "Teal", hex: "#264653", hue: "teal blues and cyan tones" }, |
| 38 | + { slug: "blue", name: "Blue", hex: "#4361ee", hue: "rich blues and cobalts" }, |
| 39 | + { slug: "purple", name: "Purple", hex: "#7209b7", hue: "purples and violets" }, |
| 40 | + { slug: "pink", name: "Pink", hex: "#f72585", hue: "pinks and magentas" }, |
| 41 | +]; |
| 42 | + |
| 43 | +const genAI = new GoogleGenerativeAI(API_KEY); |
| 44 | +const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); |
| 45 | + |
| 46 | +async function generateStory(family) { |
| 47 | + const prompt = `You are a color historian and design writer. Write a rich, engaging article about the color family "${family.name}" (${family.hue}). |
| 48 | +
|
| 49 | +The article should have these sections: |
| 50 | +1. "origin" — The historical and cultural origins of this color family (2-3 sentences) |
| 51 | +2. "psychology" — The psychological effects and emotional associations (2-3 sentences) |
| 52 | +3. "design" — How designers and artists use this color family effectively (2-3 sentences) |
| 53 | +4. "brands" — 3-4 well-known brands or artworks that famously use this color, and why (2-3 sentences) |
| 54 | +5. "palette_tip" — One practical tip for using this color in a palette (1-2 sentences) |
| 55 | +
|
| 56 | +Also provide: |
| 57 | +- "headline": A compelling 5-8 word headline for this article |
| 58 | +- "summary": A 1-sentence description for SEO meta description (max 20 words) |
| 59 | +
|
| 60 | +Respond ONLY with valid JSON: |
| 61 | +{ |
| 62 | + "headline": "...", |
| 63 | + "summary": "...", |
| 64 | + "origin": "...", |
| 65 | + "psychology": "...", |
| 66 | + "design": "...", |
| 67 | + "brands": "...", |
| 68 | + "palette_tip": "..." |
| 69 | +} |
| 70 | +
|
| 71 | +No markdown, pure JSON.`; |
| 72 | + |
| 73 | + try { |
| 74 | + const result = await model.generateContent(prompt); |
| 75 | + const text = result.response.text(); |
| 76 | + let parsed; |
| 77 | + try { |
| 78 | + parsed = JSON.parse(text.trim()); |
| 79 | + } catch { |
| 80 | + const match = text.match(/\{[\s\S]*\}/); |
| 81 | + if (match) parsed = JSON.parse(match[0]); |
| 82 | + else throw new Error("Could not parse"); |
| 83 | + } |
| 84 | + console.log(`✓ ${family.name}`); |
| 85 | + return { ...family, ...parsed }; |
| 86 | + } catch (err) { |
| 87 | + console.error(`✗ ${family.name}:`, err.message); |
| 88 | + return { |
| 89 | + ...family, |
| 90 | + headline: `The World of ${family.name}`, |
| 91 | + summary: `Explore the history, psychology, and design applications of ${family.name.toLowerCase()} tones.`, |
| 92 | + origin: "", |
| 93 | + psychology: "", |
| 94 | + design: "", |
| 95 | + brands: "", |
| 96 | + palette_tip: "", |
| 97 | + }; |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +const outPath = join(__dirname, "../src/data/color-stories.json"); |
| 102 | + |
| 103 | +// Load existing to allow resuming |
| 104 | +let existing = {}; |
| 105 | +if (existsSync(outPath)) { |
| 106 | + existing = JSON.parse(readFileSync(outPath, "utf8")); |
| 107 | + console.log(`Resuming — ${Object.keys(existing).length} already generated`); |
| 108 | +} |
| 109 | + |
| 110 | +const stories = { ...existing }; |
| 111 | + |
| 112 | +for (const family of FAMILIES) { |
| 113 | + if (stories[family.slug]) { |
| 114 | + console.log(`⏭ ${family.name} (already exists)`); |
| 115 | + continue; |
| 116 | + } |
| 117 | + const story = await generateStory(family); |
| 118 | + stories[family.slug] = story; |
| 119 | + // Save after each to allow resuming |
| 120 | + writeFileSync(outPath, JSON.stringify(stories, null, 2)); |
| 121 | + // Rate limit |
| 122 | + await new Promise((r) => setTimeout(r, 1500)); |
| 123 | +} |
| 124 | + |
| 125 | +console.log(`\nDone! Saved to src/data/color-stories.json`); |
0 commit comments