/** * 새벽청과 이미지 생성 — codex 내장 image_gen(gpt-image-2). * * 기존 gen-images.mjs + collect-images.mjs 는 "전부 돌린 뒤 디렉터리 생성시각 순으로 * 짝짓기" 라서 순서가 어긋나면 조용히 틀린 그림이 붙는다. 여기서는 한 장 돌릴 때마다 * 그 직전 시각 이후에 새로 생긴 파일만 집어 온다 — 짝이 어긋날 여지를 없앤다. * * 사용: node tools/gen-fruit-images.mjs */ import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const outDir = path.join(__dirname, ".gen-fruit"); fs.mkdirSync(outDir, { recursive: true }); const genRoot = path.join(os.homedir(), ".codex", "generated_images"); const NO = "no text, no letters, no numbers, no signage, no logos, no watermarks, no brand marks"; /* 팔레트를 프롬프트에 그대로 넣는다 — 페이지 토큰과 사진이 따로 놀면 사진이 배경이 아니라 이물이 된다. 종이 흰 #FBFAF7 · 잉크 #16181A · 새벽 남색 #1F3F6B */ const JOBS = [ { name: "dawn-aisle", prompt: `$imagegen Documentary photograph, 16:9 landscape. Interior of a Korean wholesale fruit market hall at 2am, before dawn. A long aisle between stacked wooden and cardboard fruit crates, cold fluorescent tube lighting overhead casting pools of light on a wet concrete floor, deep blue-black shadows in the depth of the hall. Palette restrained: paper white highlights, ink black shadows, one cold navy blue cast (#1F3F6B) in the far depth. The fruit itself is the only warm color — apples and pears glimpsed in the nearest crates. Empty of people. Quiet, industrious, pre-dawn mood. ${NO}. Photographic, realistic, fine film grain, no HDR.`, }, { name: "pear-crate", prompt: `$imagegen Editorial still-life photograph, 4:3 landscape. A shallow wooden crate of Korean pears (large, round, russet-golden) nested in white protective mesh sleeves, sitting on a plain concrete surface. One pear lifted out and resting beside the crate. Cool overcast north light from the left, off-white background (#FBFAF7), deep neutral shadows. The pears' golden-bronze skin is the only saturated color in the frame. ${NO}. Photographic, realistic, shallow depth of field on the front pear, fine grain.`, }, { name: "grape-bloom", prompt: `$imagegen Macro photograph, 3:4 portrait. A close cluster of dark purple Korean Campbell grapes with the natural pale waxy bloom still intact on the skin, a few water droplets, held against a plain off-white paper background (#FBFAF7). Extremely fine detail of the bloom's chalky film and the skin beneath. Soft even diffused light, no specular hotspots. The deep purple is the only color; everything else is neutral. ${NO}. Photographic, realistic, museum-specimen clarity.`, }, { name: "refractometer", prompt: `$imagegen Close documentary photograph, 4:3 landscape. A handheld brass-and-steel optical refractometer lying open on a stainless steel worktop, its prism plate carrying a single glistening drop of clear fruit juice, a small stainless spoon and a cut wedge of melon beside it. Cool neutral light, brushed steel greys, off-white background. Precise, laboratory-adjacent but agricultural mood — this is a produce grader's tool, not a laboratory. ${NO} (the refractometer scale must not show readable numbers). Photographic, realistic, shallow depth of field on the juice drop.`, }, ]; const before = () => { const seen = new Set(); if (!fs.existsSync(genRoot)) return seen; for (const d of fs.readdirSync(genRoot)) { const dir = path.join(genRoot, d); if (!fs.statSync(dir).isDirectory()) continue; for (const f of fs.readdirSync(dir)) seen.add(path.join(dir, f)); } return seen; }; const after = (seen) => { const fresh = []; if (!fs.existsSync(genRoot)) return fresh; for (const d of fs.readdirSync(genRoot)) { const dir = path.join(genRoot, d); if (!fs.statSync(dir).isDirectory()) continue; for (const f of fs.readdirSync(dir)) { const p = path.join(dir, f); if (!seen.has(p) && /\.(png|jpg|jpeg|webp)$/i.test(f)) { fresh.push({ p, t: fs.statSync(p).mtimeMs }); } } } return fresh.sort((a, b) => b.t - a.t); }; for (const job of JOBS) { const target = path.join(outDir, `${job.name}.png`); if (fs.existsSync(target)) { console.log(`skip ${job.name} (이미 있음)`); continue; } console.log(`gen ${job.name} …`); const seen = before(); try { // Windows 의 codex 는 npm .cmd 심이라 cmd.exe 를 거쳐야 해석된다. execFileSync("cmd.exe", ["/d", "/s", "/c", "codex exec --skip-git-repo-check -"], { input: job.prompt, encoding: "utf8", timeout: 8 * 60 * 1000, stdio: ["pipe", "pipe", "pipe"], }); } catch (e) { // 배너가 stderr 로 나오고 종료코드가 0 이 아닐 수 있다. 파일이 생겼는지로 판정한다 console.log(` (codex 비정상 종료: ${String(e.message).slice(0, 90)})`); } const fresh = after(seen); if (!fresh.length) { console.log(`FAIL ${job.name} — 새 이미지가 생기지 않았다`); continue; } fs.copyFileSync(fresh[0].p, target); console.log(`ok ${job.name} ← ${path.basename(fresh[0].p)} (${Math.round(fs.statSync(target).size / 1024)}KB)`); } console.log("\n생성 완료:", fs.readdirSync(outDir).join(", "));