Files
ai-florist/scripts/generate-flower-catalog.js
Chaewon Lee b50f57a6d6 feat: add flower db images
* feat: add options/map flow, dev seed, and artwork fixes

Options page, Kakao map with florist order message, dev tooling, and
create/message dummy gating — without secrets in .env.example.

Co-authored-by: Cursor <cursoragent@cursor.com>

* with generating page + art work

* with flower images

---------

Co-authored-by: 이지은 <ijieun@ijieun-ui-MacBookPro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 15:05:00 +09:00

173 lines
4.5 KiB
JavaScript

/**
* flowerDB 카탈로그 이미지 batch 생성 (1회 실행 → static/flowers/{id}.png)
*
* 사용:
* npm run generate:flowers -- --dry-run
* npm run generate:flowers -- --missing-only
* npm run generate:flowers -- --ids 7,14,18
* npm run generate:flowers -- --force --ids 14
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import OpenAI from 'openai';
import { flowerDB } from '../src/lib/server/flowerFlow/flowerDB.js';
import {
buildFlowerCardPrompt,
getPromptNameForFlower
} from '../src/lib/flowerFlow/flowerCatalogPrompt.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const OUT_DIR = join(ROOT, 'static', 'flowers');
/** @param {string} flag */
function hasFlag(flag) {
return process.argv.includes(flag);
}
/** @param {string} flag */
function readFlagValue(flag) {
const index = process.argv.indexOf(flag);
if (index === -1) return null;
return process.argv[index + 1] ?? null;
}
function loadEnvFile() {
const envPath = join(ROOT, '.env');
if (!existsSync(envPath)) return;
for (const line of readFileSync(envPath, 'utf8').split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const separator = trimmed.indexOf('=');
if (separator === -1) continue;
const key = trimmed.slice(0, separator).trim();
const value = trimmed.slice(separator + 1).trim();
// .env 값을 항상 우선 (터미널에 남은 옛 OPENAI_API_KEY 덮어씀)
if (key) {
process.env[key] = value;
}
}
}
/** @param {string} value */
function parseIdList(value) {
return value
.split(',')
.map((part) => Number(part.trim()))
.filter((id) => Number.isInteger(id) && id > 0);
}
/** @param {number} ms */
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* @param {string} prompt
* @returns {Promise<Buffer>}
*/
async function generateFlowerPng(prompt) {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error('OPENAI_API_KEY is not configured (.env)');
}
const size = process.env.OPENAI_IMAGE_CATALOG_SIZE || '1024x1536';
const quality = process.env.OPENAI_IMAGE_CATALOG_QUALITY || 'low';
const client = new OpenAI({ apiKey });
const response = await client.images.generate({
model: process.env.OPENAI_IMAGE_MODEL || 'gpt-image-1',
prompt,
size,
quality,
n: 1
});
const image = response.data?.[0];
if (image?.b64_json) {
return Buffer.from(image.b64_json, 'base64');
}
if (image?.url) {
const imageResponse = await fetch(image.url);
return Buffer.from(await imageResponse.arrayBuffer());
}
throw new Error('OpenAI image model did not return image data');
}
async function main() {
loadEnvFile();
const dryRun = hasFlag('--dry-run');
const force = hasFlag('--force');
const missingOnly = hasFlag('--missing-only');
const delayMs = Number(readFlagValue('--delay') ?? 2000);
const idsArg = readFlagValue('--ids');
/** @type {typeof flowerDB} */
let targets = [...flowerDB];
if (idsArg) {
const ids = new Set(parseIdList(idsArg));
targets = targets.filter((flower) => ids.has(flower.id));
}
if (missingOnly) {
targets = targets.filter((flower) => !existsSync(join(OUT_DIR, `${flower.id}.png`)));
}
if (targets.length === 0) {
console.log('생성할 꽃이 없습니다.');
return;
}
mkdirSync(OUT_DIR, { recursive: true });
const size = process.env.OPENAI_IMAGE_CATALOG_SIZE || '1024x1536';
const quality = process.env.OPENAI_IMAGE_CATALOG_QUALITY || 'low';
console.log(`대상: ${targets.length}종 · ${size} · quality=${quality}${dryRun ? ' (dry-run)' : ''}`);
for (const flower of targets) {
const outPath = join(OUT_DIR, `${flower.id}.png`);
const promptName = getPromptNameForFlower(flower);
const prompt = buildFlowerCardPrompt(promptName);
if (existsSync(outPath) && !force) {
console.log(`skip id=${flower.id} ${flower.name} (already exists)`);
continue;
}
console.log(`\nid=${flower.id} ${flower.name}`);
console.log(`promptName: ${promptName}`);
console.log(`prompt: ${prompt}`);
if (dryRun) continue;
try {
const bytes = await generateFlowerPng(prompt);
writeFileSync(outPath, bytes);
console.log(`saved → static/flowers/${flower.id}.png`);
} catch (err) {
console.error(`failed id=${flower.id}:`, err instanceof Error ? err.message : err);
}
if (delayMs > 0) {
await wait(delayMs);
}
}
console.log('\n완료.');
}
main().catch((err) => {
console.error(err);
process.exit(1);
});