Files
Genarrative/scripts/generate-taonier-logo-concepts.mjs
高物 74fd9a33ac Increase VectorEngine timeouts and add image UI
Add VectorEngine image generation config and raise request timeouts (env + scripts) from 180000 to 1000000ms. Introduce a reusable CreativeImageInputPanel component with tests and wire up mobile keyboard-focus helpers; update generation views and related tests (CustomWorldGenerationView, BarkBattle editor, Match3D, Puzzle flows). Improve API error handling / VectorEngine request guidance (packages/shared http.ts and docs), and apply multiple backend/frontend fixes for puzzle/match3d/prompt handling. Also include extensive docs and decision-log updates describing UI/UX decisions and verification steps.
2026-05-15 02:40:59 +08:00

481 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Buffer } from 'node:buffer';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
const repoRoot = process.cwd();
const outputDir = path.join(
repoRoot,
'public',
'branding',
'taonier-logo-concepts',
);
const defaultTimeoutMs = 1000000;
const dimensionalConcepts = [
{
id: 'taonier-clay-spark',
title: '灵感陶团',
prompt:
'为中文产品“陶泥儿”设计一个无文字 Logo 图标。产品是精品 AI UGC 创作与轻休闲小游戏平台,核心理念是把脑洞、梗和小游戏像陶泥一样捏出来。图标主体是一团被轻轻捏塑的温润陶泥,内部自然形成一枚发光灵感火花和少量 AI 节点点线,整体高级、亲切、年轻、有传播感。使用暖陶土色、奶白、薄荷绿、深墨色少量点缀,居中构图,适合作为 App icon 和品牌主标。禁止文字、字母、汉字、水印、按钮、界面元素、复杂背景、儿童黏土课风格。',
},
{
id: 'taonier-play-mold',
title: '开玩模具',
prompt:
'为中文产品“陶泥儿”设计一个无文字 Logo 图标。产品强调 AI 创作、UGC、自制小游戏、玩梗传播和轻度休闲。图标主体是一枚柔软陶泥捏成的圆角播放符号播放三角像被手指压出的模具凹槽周围有两三颗精品感小星点和像素级小方块表达“捏个脑洞马上开玩”。风格是现代品牌标志柔软但不幼稚干净、可缩小识别。禁止文字、字母、汉字、水印、真实陶艺工具、UI 按钮、教程感。',
},
{
id: 'taonier-meme-bubble',
title: '造梗气泡',
prompt:
'为中文产品“陶泥儿”设计一个无文字 Logo 图标。产品是 AI UGC 创作社区,主打精品内容、梗传播、裂变分享、休闲小游戏。图标用一团软陶泥变形成聊天气泡和小表情的组合,气泡边缘像被揉捏过,中心有抽象笑脸和创意火花,但不要做儿童玩具感。品牌气质年轻、松弛、聪明、有社交传播力。配色使用陶土橙、奶白、清爽蓝绿和少量深色轮廓。禁止文字、字母、汉字、水印、复杂场景、表情包文字。',
},
{
id: 'taonier-creation-loop',
title: '共创回路',
prompt:
'为中文产品“陶泥儿”设计一个无文字 Logo 图标。产品理念是 AI 与用户共同把灵感塑形成可玩的 UGC 作品。图标主体由两条柔软陶泥带构成循环造物轨迹,形成一个抽象无限符号和手工捏塑旋涡,中间嵌入一颗小型游戏棋子或星点,表达共创、迭代、传播和精品打磨。风格简洁高级、几何清楚、移动端小尺寸仍可识别。禁止文字、字母、汉字、水印、复杂阴影、科技冷硬金属感。',
},
{
id: 'taonier-premium-seal',
title: '精品泥印',
prompt:
'为中文产品“陶泥儿”设计一个无文字 Logo 图标。产品主打精品 AI 创作、UGC 作品和轻小游戏发布。图标是一个被压印过的软陶徽章,外形像圆润印章但更现代,中间有抽象火花、小游戏方块和一处捏痕,表达“精品内容由脑洞塑形”。整体要有品牌信任感和高级手作质感,不要像儿童陶艺班。使用暖陶土、奶油白、莓红或湖蓝少量点缀,清晰居中。禁止文字、字母、汉字、水印、传统篆刻字、真实照片。',
},
];
const flatConcepts = [
{
id: 'taonier-flat-play-clay',
title: '扁平开捏',
prompt:
'为中文产品“陶泥儿”设计一个无文字扁平矢量 Logo 图标。产品是 AI UGC 创作与轻休闲小游戏平台,主张“把脑洞捏成小游戏”。图标只使用一个柔软圆润的陶泥形主轮廓,内部用极简负形播放三角表达“马上开玩”,整体像现代 App icon 的核心符号。风格要求flat vector logo, clean geometric, friendly, mainstream, memorable, high contrast, scalable, minimal shapes, solid colors, subtle 2D shadow only。配色使用暖陶土橙、奶油白、清爽薄荷绿或深墨色最多 3 个主色。禁止3D、立体、拟物、厚重阴影、渐变高光、照片质感、复杂纹理、中文字、英文字母、水印、UI 按钮、复杂背景、吉祥物。',
},
{
id: 'taonier-flat-spark-clay',
title: '灵感泥星',
prompt:
'为中文产品“陶泥儿”设计一个无文字扁平矢量 Logo 图标。产品强调 AI 创作、UGC、造梗、精品轻小游戏。图形主体是一枚圆润陶泥团中心用简洁四角星或火花负形表达灵感和 AI 生成外轮廓要一眼像“可塑形的软泥”但必须保持现代、主流、亲和、有记忆点。风格要求flat vector brand mark, simple silhouette, app icon ready, no realism, no texture, no 3D, crisp edges, 2D friendly illustration。最多 3 色,暖陶土 + 奶油白 + 少量蓝绿。禁止文字、字母、水印、复杂小节点、儿童手工课风格。',
},
{
id: 'taonier-flat-meme-smile',
title: '造梗笑泥',
prompt:
'为中文产品“陶泥儿”设计一个无文字扁平矢量 Logo 图标。产品主打 UGC、玩梗传播、裂变分享和轻休闲小游戏。图形是一团被捏成圆润聊天气泡的陶泥内部只保留极简笑脸或一颗小星点表达“造梗”和“分享快乐”。整体要像主流社交娱乐 App 的 Logo亲和、轻松、容易记住小尺寸清楚。风格要求flat vector logo, simple, bold, friendly, clean, no gradients, no 3D, no mascot complexity。配色不超过 3 色。禁止中文字、英文字母、水印、表情包文字、复杂装饰、立体高光。',
},
{
id: 'taonier-flat-loop-mold',
title: '共创泥环',
prompt:
'为中文产品“陶泥儿”设计一个无文字扁平矢量 Logo 图标。产品理念是用户与 AI 共同把灵感塑形成可玩的 UGC 作品。图形用一条柔软陶泥带形成简洁闭环或抽象无限符号中间留出小星点负形表达共创、迭代、传播和精品打磨。视觉要主流、简洁、亲和不要科技冷硬。风格要求flat vector symbol, clean loop mark, minimal, memorable, scalable, solid colors, crisp silhouette, suitable for app icon。禁止 3D、拟物、厚阴影、复杂渐变、文字、字母、水印。',
},
{
id: 'taonier-flat-seal-blocks',
title: '精品泥印',
prompt:
'为中文产品“陶泥儿”设计一个无文字扁平矢量 Logo 图标。产品强调精品 AI 作品、UGC 创作和小游戏发布。图形是一枚现代软陶印记,外形为圆角徽章或圆润印章,内部用 2 到 3 个简洁小方块和一颗星点表达“作品”“小游戏”“精品内容”。整体应像可长期使用的品牌主标主流、干净、亲和、有辨识度。风格要求flat vector logo, bold simple shapes, app icon ready, minimal color palette, no realism, no texture。禁止文字、字母、水印、传统篆刻、3D、复杂阴影、拟物陶艺。',
},
];
const v3Concepts = [
{
id: 'taonier-v3-finger-spark',
title: '灵感捏痕',
prompt:
'为中文产品“陶泥儿”设计一个无文字扁平矢量 Logo 图标。不要使用播放三角、聊天气泡、笑脸、循环无限符号、褐色陶土主色、碎片小元素。产品是 AI UGC 创作与轻休闲小游戏平台,核心是“把脑洞捏成可玩的作品”。图形主体是一个醒目的圆润软形,内部只有一枚极简指纹捏痕与小火花负形,表达“被手指一捏,灵感成型”。风格:主流 App icon、flat vector、bold simple silhouette、friendly、memorable、high contrast、可缩小识别。配色珊瑚橙或莓红作为主色奶油白负形少量青绿色投影或边缘点缀最多 3 色。画面居中留白干净。禁止文字、字母、水印、3D、拟物、厚阴影、渐变高光、照片质感、复杂纹理、表情包感、UI 按钮。',
},
{
id: 'taonier-v3-seed-pop',
title: '脑洞种子',
prompt:
'为中文产品“陶泥儿”设计一个无文字扁平矢量 Logo 图标。不要使用播放三角、聊天气泡、笑脸、无限循环、传统印章、褐色主色或多碎元素。产品主打 AI 创作、UGC、梗传播、精品轻小游戏。图标主体是一颗圆润明亮的“脑洞种子”像软泥被捏成的一颗种子/小芽顶部有一个简洁星点缺口表达灵感生长、内容生成、人人创作。风格flat vector logo, simple, mainstream, warm, lively, app icon ready, strong outline, minimal shapes。配色使用高饱和青绿、珊瑚粉、奶油白、深墨色中的 2-3 色不要大面积褐色。禁止文字、字母、水印、3D、拟物、照片、复杂渐变、表情、儿童黏土课风格。',
},
{
id: 'taonier-v3-magic-dot',
title: '一捏成型',
prompt:
'为中文产品“陶泥儿”设计一个无文字扁平矢量 Logo 图标。避开播放按钮、聊天气泡、笑脸、循环符号、褐色陶土和堆叠小图标。产品理念是用户轻轻一捏AI 把脑洞生成小游戏和 UGC 作品。图形由两个圆润手捏触点和中间一个闪光成型点组成,像“捏合灵感”的瞬间,但不要画真实手指。整体应非常简洁,有强记忆点,像主流创作娱乐 App 的标志。风格flat vector, iconic, minimal, friendly, bold shape, clear at 32px。配色亮紫红或珊瑚红主色奶油白负形青绿色小面积辅助。禁止文字、字母、水印、3D、厚阴影、渐变高光、复杂纹理。',
},
{
id: 'taonier-v3-work-gem',
title: '作品胶囊',
prompt:
'为中文产品“陶泥儿”设计一个无文字扁平矢量 Logo 图标。不要使用播放三角、聊天气泡、笑脸、循环无限符号、褐色主色、多枚小卡片或碎图标。产品强调精品 AI UGC 作品和轻小游戏创作。图形主体是一枚被捏成圆角宝石/胶囊的抽象作品符号内部只有一条柔软弧线切面和一个小星点表达“脑洞被打磨成精品”。风格flat vector logo, premium but friendly, simple, memorable, app icon, solid colors, no texture。配色湖蓝或青绿主色珊瑚橙点缀奶白负形深墨小轮廓可选。禁止文字、字母、水印、3D、复杂渐变、照片质感、游戏手柄、图片卡片、用户头像。',
},
{
id: 'taonier-v3-soft-t',
title: '软体 T 形',
prompt:
'为中文产品“陶泥儿”设计一个无文字扁平矢量 Logo 图标。尝试做一个抽象但亲和的品牌首字母符号,灵感来自 Taonier / 陶泥儿 的 T 和“被捏塑的软泥”。不要出现真实字母 T 的硬直排版而是用一笔圆润软形构成可记忆的图腾。必须避开播放三角、聊天气泡、笑脸、循环符号、褐色陶土主色和碎元素。风格flat vector brand mark, modern, friendly, bold, iconic, simple silhouette, app icon ready。配色明亮珊瑚红、奶油白、薄荷青或深墨最多 3 色。禁止文字、英文字母直出、汉字、水印、3D、拟物、厚阴影、复杂纹理。',
},
];
const magicDotConcepts = [
{
id: 'taonier-magic-dot-orbit',
title: '捏合星核',
prompt:
'围绕“陶泥儿”V3 方案“一捏成型”做 Logo 延展。设计一个无文字扁平矢量主标:两个圆润软泥触点从左右轻轻合拢,中心不是碰撞爆炸,而是一颗稳定的星核/作品核外形要形成完整、可记忆的品牌符号。必须避免播放三角、聊天气泡、笑脸、循环无限符号、褐色陶土、真实手指、括号感、爆炸特效和碎元素。风格flat vector logo, iconic, minimal, friendly, mainstream app icon, strong silhouette, clear at 32px。配色珊瑚红或莓红主形奶油白负形青绿色只做中心小面积最多 3 色。无文字、无字母、无水印、无 3D、无厚阴影、无拟物。',
},
{
id: 'taonier-magic-dot-seal',
title: '成型印记',
prompt:
'围绕“陶泥儿”V3 方案“一捏成型”做 Logo 延展。设计一个无文字扁平矢量主标:图形像一枚被两侧轻轻按压成型的软形印记,中心留出一个简洁星点或小圆孔,表达 AI 把脑洞塑形成作品。整体要比原本左右括号更完整外轮廓形成一个独特图腾。禁止播放按钮、聊天气泡、笑脸、循环符号、褐色陶土主色、多小图标、真实手、爆炸火花。风格flat vector, bold simple shape, friendly premium, memorable, app icon ready, solid colors。配色亮珊瑚、奶油白、薄荷青或深墨最多 3 色。',
},
{
id: 'taonier-magic-dot-squish',
title: '软泥合拍',
prompt:
'围绕“陶泥儿”V3 方案“一捏成型”做 Logo 延展。设计一个无文字扁平矢量 Logo两个软泥形不是分散的括号而是上下错位地挤压出中心灵感点像“啪嗒一捏作品成型”的瞬间。图形需要亲和、轻松、年轻但不做表情包。必须保持元素极少只有两块主形和一个中心成型点。禁止播放三角、聊天气泡、笑脸、无限循环、褐色主色、复杂渐变、拟物质感、真实手指、文字、字母。风格flat vector brand mark, simple, memorable, high contrast, scalable。配色莓红、奶白、青绿或明黄点缀。',
},
{
id: 'taonier-magic-dot-mold',
title: '灵感模口',
prompt:
'围绕“陶泥儿”V3 方案“一捏成型”做 Logo 延展。设计一个无文字扁平矢量主标外形像一个被捏开的柔软模口中心浮出一颗极简星点表达从软泥模口里生成作品。它应该是一眼可记住的抽象符号不像聊天框、不像播放键、不像括号。风格flat vector logo, modern, friendly, clean, bold, minimal, app icon。配色使用高识别珊瑚红或玫粉主色奶油白负形少量青绿点缀。禁止褐色陶土、真实陶艺、3D、高光、厚阴影、复杂小碎片、文字、水印。',
},
{
id: 'taonier-magic-dot-bloom',
title: '捏开灵感',
prompt:
'围绕“陶泥儿”V3 方案“一捏成型”做 Logo 延展。设计一个无文字扁平矢量 Logo用两片圆润软形夹出中央一颗灵感点整体像一个正在打开的创意容器但不要像花朵、聊天气泡或笑脸。图形要完整、主流、亲和、醒目适合 App icon 和品牌主标。禁止播放三角、聊天气泡、笑脸、循环符号、褐色陶土、碎元素、真实手、复杂花瓣。风格flat vector, minimal brand mark, strong silhouette, warm, youthful, memorable。配色珊瑚红、奶油白、青绿最多 3 色。',
},
];
const handsConcepts = [
{
id: 'taonier-hands-cradle-spark',
title: '托住灵感',
prompt:
'围绕“陶泥儿”Logo 方向 03 的“上下两只手托住灵感”的感觉继续打磨。设计一个无文字扁平矢量主标:上下两片圆润软掌状形体像手但不要画真实手指,轻轻托住中央一颗简洁灵感星核,表达用户与 AI 一起把脑洞捏成作品。整体要完整、主流、亲和、醒目,适合 App icon。避免播放三角、聊天气泡、笑脸、眼睛、花朵、循环符号、褐色陶土、多碎元素和真实手掌插画。风格flat vector logo, bold simple silhouette, friendly, memorable, premium but warm, clear at 32px。配色上方珊瑚红、下方青绿色、中央奶油白或金色小星最多 3 色。无文字、无字母、无水印、无 3D、无厚阴影。',
},
{
id: 'taonier-hands-pinched-gem',
title: '合捏成珠',
prompt:
'围绕“陶泥儿”Logo 方向 03 的“上下两只手”感觉做延展。设计一个无文字扁平矢量主标:上下一对抽象软手 / 软泥掌从两侧微微合捏中间形成一颗小圆珠或作品核。图形要像品牌符号不像手势教学图保留托举与成型的温柔感。禁止播放三角、聊天气泡、笑脸、眼睛、花朵、褐色主色、真实手指、复杂掌纹、碎小图标。风格flat vector, minimal, mainstream app logo, high contrast, iconic, friendly。配色莓红、奶白、薄荷青、少量深墨最多 3 色。',
},
{
id: 'taonier-hands-soft-bowl',
title: '创意托碗',
prompt:
'围绕“陶泥儿”Logo 方向 03 的上下手感做主标延展。设计一个无文字扁平矢量 Logo下方是一片像手掌也像软泥托碗的圆润形体上方是一片较小软形轻轻压合中间浮出星点表达“轻托脑洞、AI 捏成作品”。整体要简洁、有包容感、年轻亲和。避免像眼睛、嘴巴、聊天气泡、播放器、花朵、真实手掌、儿童黏土课。风格flat vector logo, bold simple shape, app icon ready, clean, memorable。配色青绿主托、珊瑚红上形、奶白中心最多 3 色。',
},
{
id: 'taonier-hands-formed-seal',
title: '双掌泥印',
prompt:
'围绕“陶泥儿”Logo 方向 03 的上下两只手感觉做更完整的图腾。设计一个无文字扁平矢量主标两片抽象软掌上下扣合外轮廓形成一个圆润印记中心保留一个星形负空间像“被双手捏出的创意印记”。要有主流品牌感不要像宗教手势、医疗关怀、儿童手工。禁止播放三角、聊天气泡、笑脸、眼睛、花朵、循环符号、褐色陶土、真实手指、复杂纹理。风格flat vector, iconic, simple, friendly premium, solid colors, scalable。配色珊瑚红、奶油白、青绿或深墨最多 3 色。',
},
{
id: 'taonier-hands-pop-capsule',
title: '掌心开捏',
prompt:
'围绕“陶泥儿”Logo 方向 03 的“上下两只手托住灵感”感觉做更活泼版本。设计一个无文字扁平矢量 Logo上下两片软掌像打开的胶囊中央小星点从掌心弹出表达“脑洞被捏出来”。图形需要有传播感、亲和力、记忆点但不要像表情包或聊天软件。禁止播放三角、聊天气泡、笑脸、眼睛、花朵、褐色陶土、真实手指、碎元素。风格flat vector brand mark, simple, bold, youthful, app icon, high contrast。配色亮珊瑚红、薄荷青、奶白最多 3 色。',
},
];
const args = new Map();
for (let index = 2; index < process.argv.length; index += 1) {
const raw = process.argv[index];
if (!raw.startsWith('--')) {
continue;
}
const next = process.argv[index + 1];
if (next && !next.startsWith('--')) {
args.set(raw, next);
index += 1;
} else {
args.set(raw, true);
}
}
const style = String(args.get('--style') || 'dimensional').trim();
const concepts =
style === 'flat'
? flatConcepts
: style === 'v3'
? v3Concepts
: style === 'magic'
? magicDotConcepts
: style === 'hands'
? handsConcepts
: dimensionalConcepts;
const selectedOutputDir =
style === 'flat'
? path.join(repoRoot, 'public', 'branding', 'taonier-logo-flat-concepts')
: style === 'v3'
? path.join(repoRoot, 'public', 'branding', 'taonier-logo-v3-concepts')
: style === 'magic'
? path.join(
repoRoot,
'public',
'branding',
'taonier-logo-magic-dot-concepts',
)
: style === 'hands'
? path.join(
repoRoot,
'public',
'branding',
'taonier-logo-hands-concepts',
)
: outputDir;
function readDotenv(fileName) {
const filePath = path.join(repoRoot, fileName);
if (!existsSync(filePath)) {
return {};
}
const values = {};
for (const line of readFileSync(filePath, 'utf8').split(/\r?\n/u)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
continue;
}
const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u.exec(trimmed);
if (!match) {
continue;
}
let value = match[2].trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
values[match[1]] = value;
}
return values;
}
function resolveEnv() {
const loaded = {
...readDotenv('.env.example'),
...readDotenv('.env.local'),
...readDotenv('.env.secrets.local'),
...process.env,
};
return {
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '')
.trim()
.replace(/\/+$/u, ''),
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(),
timeoutMs: Number.parseInt(
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
10,
),
};
}
function buildUrl(baseUrl) {
return baseUrl.endsWith('/v1')
? `${baseUrl}/images/generations`
: `${baseUrl}/v1/images/generations`;
}
function collectStringsByKey(value, targetKey, output) {
if (Array.isArray(value)) {
value.forEach((entry) => collectStringsByKey(entry, targetKey, output));
return;
}
if (!value || typeof value !== 'object') {
return;
}
for (const [key, nested] of Object.entries(value)) {
if (key === targetKey) {
if (typeof nested === 'string' && nested.trim()) {
output.push(nested.trim());
}
if (Array.isArray(nested)) {
nested.forEach((entry) => {
if (typeof entry === 'string' && entry.trim()) {
output.push(entry.trim());
}
});
}
}
collectStringsByKey(nested, targetKey, output);
}
}
function extractImageUrls(payload) {
const urls = [];
collectStringsByKey(payload, 'url', urls);
collectStringsByKey(payload, 'image', urls);
collectStringsByKey(payload, 'image_url', urls);
return [...new Set(urls)].filter((url) => /^https?:\/\//u.test(url));
}
function extractBase64Images(payload) {
const values = [];
collectStringsByKey(payload, 'b64_json', values);
return values;
}
function inferExtensionFromBytes(bytes) {
if (bytes.subarray(0, 8).equals(Buffer.from('\x89PNG\r\n\x1A\n', 'binary'))) {
return 'png';
}
if (bytes.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]))) {
return 'jpg';
}
if (
bytes.subarray(0, 4).toString('ascii') === 'RIFF' &&
bytes.subarray(8, 12).toString('ascii') === 'WEBP'
) {
return 'webp';
}
return 'png';
}
async function fetchJson(url, options, timeoutMs) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: abortController.signal,
});
const text = await response.text();
if (!response.ok) {
throw new Error(`VectorEngine ${response.status}: ${text.slice(0, 600)}`);
}
return JSON.parse(text);
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timer);
}
}
async function downloadUrl(url, timeoutMs) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: abortController.signal });
if (!response.ok) {
throw new Error(`download ${response.status}`);
}
return Buffer.from(await response.arrayBuffer());
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`Generated image download timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timer);
}
}
async function generateConcept(env, concept) {
const requestBody = {
model: 'gpt-image-2-all',
prompt: concept.prompt,
n: 1,
size: '1024x1024',
};
const payload = await fetchJson(
buildUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
env.timeoutMs,
);
const urls = extractImageUrls(payload);
const b64Images = extractBase64Images(payload);
let bytes;
if (urls[0]) {
bytes = await downloadUrl(urls[0], env.timeoutMs);
} else if (b64Images[0]) {
bytes = Buffer.from(b64Images[0], 'base64');
} else {
throw new Error(`VectorEngine returned no image for ${concept.id}`);
}
mkdirSync(selectedOutputDir, { recursive: true });
const extension = inferExtensionFromBytes(bytes);
const outputPath = path.join(selectedOutputDir, `${concept.id}.${extension}`);
writeFileSync(outputPath, bytes);
return outputPath;
}
const dryRun = args.has('--dry-run') || !args.has('--live');
const onlyIds = String(args.get('--only') || '')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
const limit = Number.parseInt(String(args.get('--limit') || '0'), 10);
const selected = concepts
.filter((concept) => !onlyIds.length || onlyIds.includes(concept.id))
.slice(0, limit > 0 ? limit : concepts.length);
if (dryRun) {
console.log(
JSON.stringify(
{
mode: 'dry-run',
style,
outputDir: selectedOutputDir,
count: selected.length,
requests: selected.map((concept) => ({
id: concept.id,
title: concept.title,
body: {
model: 'gpt-image-2-all',
prompt: concept.prompt,
n: 1,
size: '1024x1024',
},
})),
},
null,
2,
),
);
process.exit(0);
}
const env = resolveEnv();
if (!env.baseUrl || !env.apiKey) {
console.error(
JSON.stringify({
ok: false,
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY',
hasBaseUrl: Boolean(env.baseUrl),
hasApiKey: Boolean(env.apiKey),
}),
);
process.exit(1);
}
const generated = [];
for (const concept of selected) {
console.log(`Generating ${concept.id}...`);
generated.push(await generateConcept(env, concept));
}
console.log(
JSON.stringify(
{
ok: true,
count: generated.length,
files: generated,
},
null,
2,
),
);