feat: add edutainment drawing and visual package flows

This commit is contained in:
2026-05-14 14:17:10 +08:00
parent 10e8beea80
commit e444266e1e
109 changed files with 8788 additions and 996 deletions

View File

@@ -106,6 +106,11 @@ NODE
resolve_dev_stack_ports() {
local key
local value
local spacetime_port_args=()
if [[ "${SKIP_SPACETIME}" -ne 1 ]]; then
spacetime_port_args+=("spacetime:${SPACETIME_HOST}:${SPACETIME_PORT}")
fi
while IFS='=' read -r key value; do
case "${key}" in
@@ -115,7 +120,7 @@ resolve_dev_stack_ports() {
esac
done < <(
node "${REPO_ROOT}/scripts/dev-stack-port-utils.mjs" resolve-dev-stack \
"spacetime:${SPACETIME_HOST}:${SPACETIME_PORT}" \
"${spacetime_port_args[@]}" \
"api:${API_TARGET_HOST}:${API_PORT}" \
"web:${WEB_HOST}:${WEB_PORT}" \
"adminWeb:${ADMIN_WEB_TARGET_HOST}:${ADMIN_WEB_PORT}"

View File

@@ -270,6 +270,368 @@ const assetDefinitions = [
chromaKeyNote,
].join(''),
},
{
id: 'wave-cat-head-guide',
output: 'picture-book-wave-cat-head-guide-v1.png',
sourceOutput: 'picture-book-wave-cat-head-guide-v1-source.png',
size: '1024x1024',
transparent: true,
useBackgroundReference: true,
useLayoutReference: true,
layoutNormalization: {
canvasWidth: 1024,
canvasHeight: 1024,
fit: 'contain',
fillWidth: 0.76,
fillHeight: 0.76,
anchorY: 'center',
padding: 22,
},
prompt: [
'请生成儿童动作互动游戏招手提示中央使用的卡通猫猫头资产,只画猫猫头,不要身体和爪子。',
'主体是一只原创绘本卡通猫猫头,圆润、亲切、表情开心,适合夹在左右两只猫爪中间作为挥手引导。',
'猫头可以是浅米白、淡橘、柔和浅棕和浅蓝绿色高光,轮廓清晰,五官简洁可爱,不能像真实照片或具体 IP 角色。',
'资产需要轻盈半透明、水彩纸张质感,缩小后仍能清楚看出猫脸和耳朵,边缘不要有复杂毛发。',
'整体风格必须和参考背景一致:明亮、温暖、卡通绘本、草地游戏舞台气质。',
'不要文字、数字、按钮、面板、人物、全身动物、品牌符号、水印、真实照片质感、厚重阴影或科技感。',
styleReferenceNote,
noStretchNote,
chromaKeyNote,
].join(''),
},
{
id: 'wave-cat-paw-guide',
output: 'picture-book-wave-cat-paw-guide-v1.png',
sourceOutput: 'picture-book-wave-cat-paw-guide-v1-source.png',
size: '1024x1024',
transparent: true,
useBackgroundReference: true,
useLayoutReference: true,
layoutNormalization: {
canvasWidth: 1024,
canvasHeight: 1024,
fit: 'contain',
fillWidth: 0.82,
fillHeight: 0.82,
anchorY: 'center',
padding: 22,
},
prompt: [
'请生成儿童动作互动游戏的挥手引导猫爪资产,只画一只猫爪和一小段前臂,用于网页左右镜像复用。',
'主体是一段从画面下方斜向上伸出的柔软卡通猫前臂,末端是圆润猫爪,不展示手指细节,爪垫可以用几个浅色圆形简化表达。',
'猫爪需要像儿童绘本里的玩偶圆爪,简洁可爱,适合放在猫猫头左右两侧做左右摆动动画。',
'资产需要半透明、轻盈,轮廓清晰,缩小后仍能看出前臂和猫爪;边缘不要复杂毛发,不要尖爪。',
'颜色使用浅米白、淡橘、柔和草绿色和浅蓝绿色水彩高光,风格和参考背景一致,明亮、温暖、卡通绘本、轻微纸张纹理。',
'不要文字、数字、按钮、面板、人物全身、完整动物、真实照片质感、厚重阴影或科技感。',
styleReferenceNote,
noStretchNote,
chromaKeyNote,
].join(''),
},
{
id: 'wave-cat-head-guide-v2',
output: 'picture-book-wave-cat-head-guide-v2.png',
sourceOutput: 'picture-book-wave-cat-head-guide-v2-source.png',
size: '1024x1024',
transparent: true,
useBackgroundReference: true,
useLayoutReference: true,
layoutNormalization: {
canvasWidth: 1024,
canvasHeight: 1024,
fit: 'contain',
fillWidth: 0.72,
fillHeight: 0.72,
anchorY: 'center',
padding: 24,
},
prompt: [
'请重新设计一版儿童动作互动游戏招手提示中央使用的原创绘本卡通猫猫头资产,只画猫猫头,不要身体和爪子。',
'主体是一只圆润的小猫头,像贴在游戏舞台中央的柔软绘本贴纸,轮廓大而简洁,表情开心、友好、轻轻张嘴微笑。',
'五官必须更简化:大眼睛、短鼻子、小嘴巴、短胡须即可;不要长胡须伸出太远,不要复杂毛发,不要真实猫毛细节。',
'色彩使用浅奶油白、淡橘和少量浅草绿或天空蓝高光,整体更轻、更通透,适合叠在明亮草地舞台上。',
'边缘是柔和水彩描边和轻微纸张纹理,缩小到舞台中央后仍能一眼看出是可爱的猫猫头。',
'不要文字、数字、按钮、面板、人物、全身动物、品牌符号、水印、真实照片质感、厚重阴影或科技感。',
styleReferenceNote,
noStretchNote,
chromaKeyNote,
].join(''),
},
{
id: 'wave-cat-paw-guide-v2',
output: 'picture-book-wave-cat-paw-guide-v2.png',
sourceOutput: 'picture-book-wave-cat-paw-guide-v2-source.png',
size: '1024x1024',
transparent: true,
useBackgroundReference: true,
useLayoutReference: true,
layoutNormalization: {
canvasWidth: 1024,
canvasHeight: 1024,
fit: 'contain',
fillWidth: 0.74,
fillHeight: 0.78,
anchorY: 'center',
padding: 24,
},
prompt: [
'请重新设计一版儿童动作互动游戏挥手引导猫爪资产,只画一只圆润猫爪和很短一段前臂,用于网页左右镜像复用。',
'主体是大号圆猫爪,爪面朝向观众,爪垫用一个浅粉色大圆垫和几个浅粉色小圆垫简化表达,前臂只保留短短一截,不要画成长手臂。',
'猫爪要像儿童绘本贴纸或软玩具爪子,轮廓饱满、简洁、可爱,适合放在猫猫头左右两侧做挥动动画。',
'色彩与猫猫头统一:浅奶油白、淡橘、柔和浅粉或淡桃色爪垫和少量浅草绿或天空蓝高光;整体半透明、轻盈、无厚重阴影。',
'爪垫必须保持明亮柔和,禁止黑色、灰色、深棕色、深色阴影或高反差硬边。',
'缩小后必须清楚看出猫爪轮廓和爪垫;不要尖爪、不要手指细节、不要真实皮肤或真实毛发质感。',
'不要文字、数字、按钮、面板、人物全身、完整动物、真实照片质感、厚重阴影或科技感。',
styleReferenceNote,
noStretchNote,
chromaKeyNote,
].join(''),
},
{
id: 'ground-ring-v3',
output: 'picture-book-ground-ring-v3.png',
sourceOutput: 'picture-book-ground-ring-v3-source.png',
size: '1536x512',
transparent: true,
useBackgroundReference: true,
useLayoutReference: true,
layoutNormalization: {
canvasWidth: 1200,
canvasHeight: 520,
fit: 'contain',
fillWidth: 0.92,
fillHeight: 0.78,
anchorY: 'center',
padding: 24,
},
prompt: [
'请重新设计儿童动作互动游戏地面位置指示环资产,用于放在绿色草地上,必须和草皮明显区分。',
'主体是单个贴在地面上的透视椭圆指示环,不是完整背景,不要依赖网页后期压扁。',
'样式像浅蓝天空色和暖黄色软垫组成的绘本地贴:外圈为浅蓝白水彩描边,内圈有柔和暖黄色或奶油色高光,中心留空透明。',
'圆环边缘可以有少量星星光点、短虚线或纸贴边,但不要用大面积绿色草叶作为主体,避免和草地混在一起。',
'禁止使用粉紫色、品红色、紫色外圈、玫红光晕或任何接近 #ff00ff 的颜色;这些颜色会被当成透明背景删除。',
'除纯色品红背景外,主体只能使用浅蓝、白色、奶油黄、暖黄色、浅橙和极少量浅草绿。',
'整体要明亮、温暖、儿童绘本风,和草地舞台统一但有清楚视觉对比;不要科技感,不要霓虹,不要金属材质。',
styleReferenceNote,
noStretchNote,
chromaKeyNote,
].join(''),
},
{
id: 'wave-cat-torso-guide-v3',
output: 'picture-book-wave-cat-torso-guide-v3.png',
sourceOutput: 'picture-book-wave-cat-torso-guide-v3-source.png',
size: '1024x1024',
transparent: true,
useBackgroundReference: true,
useLayoutReference: true,
layoutNormalization: {
canvasWidth: 1024,
canvasHeight: 1024,
fit: 'contain',
fillWidth: 0.62,
fillHeight: 0.58,
anchorY: 'bottom',
padding: 24,
},
prompt: [
'请为输入图中的橘白绘本猫猫头补充一个可单独叠放在头部下方的猫猫上半身胸口资产。',
'只画短短的上半身胸口、脖子下沿、圆润肩膀和一点点短前肢根部;不要画头、耳朵、眼睛、嘴巴、胡须、完整爪子、腿或脚。',
'主体必须是橘白小猫身体,色彩和输入图一致:浅奶油白为主,淡橘色斑纹点缀,柔和浅棕描边,少量浅草绿或天空蓝高光。',
'形状像儿童绘本贴纸里的圆润上半身,底部自然截断,适合网页叠在猫头下面形成半身猫猫。',
'两侧不要伸出长手臂,左右猫爪会由网页单独叠加。',
'禁止黑色、黑白猫、大面积深色毛、真实毛发、尖锐漫画黑线、高反差阴影。',
'不要文字、数字、按钮、面板、人物、完整动物、品牌符号、水印、真实照片质感、厚重阴影或科技感。',
styleReferenceNote,
noStretchNote,
chromaKeyNote,
].join(''),
},
{
id: 'wave-cat-body-guide-v4',
output: 'picture-book-wave-cat-body-guide-v4.png',
sourceOutput: 'picture-book-wave-cat-body-guide-v4-source.png',
size: '1024x1024',
transparent: true,
useBackgroundReference: true,
useLayoutReference: true,
useWaveCatHeadReference: true,
layoutNormalization: {
canvasWidth: 1024,
canvasHeight: 1024,
fit: 'contain',
fillWidth: 0.78,
fillHeight: 0.88,
anchorY: 'bottom',
padding: 24,
},
prompt: [
'请按参考结构重新绘制儿童动作互动游戏中央招手提示的猫咪身体主体资源。',
'主体结构参考用户草图:正面半身猫咪,圆猫头在上方,两个三角耳朵,头下方接一个简单圆润躯干,躯干到胸口和腰部一半为止。',
'本资源只包含猫头、耳朵、脖子、躯干和肩部连接点,不要画任何手臂、前臂、手掌、猫爪、腿或脚;左右肩膀两侧要留出手臂接入空间。',
'角色必须是橘白猫:主体毛色 80% 为浅奶油白和温暖淡橘色,少量浅棕描边;只能有小面积深棕眼睛和细线五官。',
'五官简洁可爱:大眼睛、短鼻子、小嘴巴、短胡须;躯干为浅奶油白和淡橘色斑纹,边缘柔和水彩描边。',
'整体像儿童绘本贴纸,半透明、轻盈,缩小到舞台中央后仍能看清猫头和半身结构。',
'禁止画手臂或猫爪,禁止黑色、灰色、黑白猫、奶牛猫、虎斑深色块、大面积深棕毛、真实毛发、尖锐漫画黑线、高反差阴影、文字、数字、按钮、面板、水印和真实照片质感。',
styleReferenceNote,
noStretchNote,
chromaKeyNote,
].join(''),
},
{
id: 'wave-cat-arm-guide-v4',
output: 'picture-book-wave-cat-arm-guide-v4.png',
sourceOutput: 'picture-book-wave-cat-arm-guide-v4-source.png',
size: '1024x1024',
transparent: true,
useBackgroundReference: true,
useLayoutReference: true,
layoutNormalization: {
canvasWidth: 1024,
canvasHeight: 1024,
fit: 'contain',
fillWidth: 0.7,
fillHeight: 0.82,
anchorY: 'bottom',
padding: 24,
},
prompt: [
'请按参考结构重新绘制儿童动作互动游戏猫咪挥手动画用的单侧手臂资源。',
'只画一条橘白猫咪手臂:从肩膀连接处开始,弯曲向上,包含上臂、前臂和末端圆猫爪,整体像用户草图中单侧向上挥动的弯曲手臂。',
'资源需要适合网页左右镜像复用:默认绘制一条从画面下方肩部连接点向上弯到画面左上方的手臂,肩部连接点在资源下方内侧,方便 CSS 设置旋转轴。',
'猫爪末端是圆润猫爪,爪垫浅粉或淡桃色,不要尖爪;手臂粗细均匀、短而可爱,不要画成长人类手臂。',
'角色必须是橘白猫手臂:主体毛色 80% 为浅奶油白和温暖淡橘色,淡橘斑纹点缀,柔和浅棕描边,爪垫浅粉或淡桃色。',
'整体像儿童绘本贴纸,半透明、轻盈,边缘清晰,缩小后仍能看出弯曲手臂和圆猫爪。',
'不要画猫头、躯干、另一只手臂、完整动物、腿、脚、文字、数字、按钮、面板、水印、真实照片质感、黑色、灰色、黑白毛、黑灰重阴影或深色大面积毛。',
styleReferenceNote,
noStretchNote,
chromaKeyNote,
].join(''),
},
{
id: 'wave-cat-body-guide-v5',
output: 'picture-book-wave-cat-body-guide-v5.png',
sourceOutput: 'picture-book-wave-cat-body-guide-v5-source.png',
size: '1024x1024',
transparent: true,
transparencyCleanup: 'cat-guide',
useBackgroundReference: true,
useLayoutReference: true,
useWaveCatHeadReference: true,
layoutNormalization: {
canvasWidth: 1024,
canvasHeight: 1024,
fit: 'contain',
fillWidth: 0.72,
fillHeight: 0.88,
anchorY: 'bottom',
padding: 22,
},
prompt: [
'请按用户参考结构重新绘制儿童动作互动游戏中央招手提示的猫咪身体主体资源,用作动画底座。主体必须是正面纸偶结构:一个大圆猫头、两个三角耳朵、头下方连接短脖子和圆润半身躯干,画到上半身和腰部一半即可。',
'本资源只包含猫头、耳朵、五官、脖子、躯干、圆润肩膀和两侧肩部连接点;绝对不要画任何手臂、前臂、手掌、猫爪、小手、小脚、腿、脚或尾巴。左右肩膀外侧需要留出干净的手臂接入空间,方便网页单独叠加手臂动画。',
'请保持输入猫猫头的暖橘白绘本风格:头顶和耳朵外侧有淡橘色块,脸和肚子为浅奶油白,少量浅橘斑纹,五官只用小面积深棕眼睛和暖棕细线。',
'所有描边必须是柔和暖棕或浅橘棕,不要使用纯黑描边;资源自身保持清晰不透明,网页会统一设置半透明效果,不要在图片里主动降低主体透明度。',
'整体像儿童绘本贴纸或可动纸偶底座,结构简单、比例可爱,缩小到舞台中央后仍能看清大猫头、小身体和肩部挂点。',
'禁止画手臂或猫爪,禁止黑色、灰色、黑白猫、奶牛猫、虎斑深色块、大面积深棕毛、真实毛发、尖锐漫画黑线、高反差阴影、文字、数字、按钮、面板、水印和真实照片质感。',
styleReferenceNote,
noStretchNote,
chromaKeyNote,
].join(''),
},
{
id: 'wave-cat-arm-guide-v5',
output: 'picture-book-wave-cat-arm-guide-v5.png',
sourceOutput: 'picture-book-wave-cat-arm-guide-v5-source.png',
size: '1024x1024',
transparent: true,
transparencyCleanup: 'cat-guide',
useBackgroundReference: true,
useLayoutReference: true,
useWaveCatHeadReference: true,
layoutNormalization: {
canvasWidth: 1024,
canvasHeight: 1024,
fit: 'contain',
fillWidth: 0.58,
fillHeight: 0.86,
anchorY: 'bottom',
padding: 20,
},
prompt: [
'请按用户参考结构重新绘制儿童动作互动游戏猫咪挥手动画用的单侧手臂手部资源。只画一条猫咪手臂:从底部肩膀连接点开始,包含短上臂、弯曲前臂和末端圆猫爪,像可动纸偶的一条独立手臂。',
'默认绘制一条向左上方举起的手臂,肩膀连接点在画面底部偏内侧,圆猫爪在画面上方;资源需要适合网页左右镜像复用和围绕肩膀连接点旋转摆动。',
'猫爪用类似多啦A梦圆手的圆润简化形状不展示手指细节不要尖爪爪面可以有浅粉或淡桃色圆形爪垫。手臂短而可爱比例像小猫上肢不要画成人类长手臂。',
'请保持输入猫猫头的暖橘白绘本风格:手臂主体为浅奶油白和淡橘色,少量浅橘斑纹,爪垫浅粉或淡桃色,柔和暖棕描边。',
'所有描边必须是柔和暖棕或浅橘棕,不要使用纯黑描边;资源自身保持清晰不透明,网页会统一设置半透明效果,不要在图片里主动降低主体透明度。',
'不要画猫头、躯干、另一只手臂、完整动物、腿、脚、文字、数字、按钮、面板、水印、真实照片质感、黑色、灰色、黑白毛、黑灰重阴影或深色大面积毛。',
styleReferenceNote,
noStretchNote,
chromaKeyNote,
].join(''),
},
{
id: 'wave-cat-body-guide-v6',
output: 'picture-book-wave-cat-body-guide-v6.png',
sourceOutput: 'picture-book-wave-cat-body-guide-v6-source.png',
size: '1024x1024',
transparent: true,
transparencyCleanup: 'cat-guide',
useBackgroundReference: true,
useLayoutReference: true,
useWaveCatHeadReference: true,
layoutNormalization: {
canvasWidth: 1024,
canvasHeight: 1024,
fit: 'contain',
fillWidth: 0.68,
fillHeight: 0.86,
anchorY: 'bottom',
padding: 22,
},
prompt: [
'请重新绘制儿童动作互动游戏中央招手提示的猫咪身体主体资源,严格按可动纸偶拆件结构生成。主体只有一只正面橘白猫:大圆猫头、两个三角耳朵、短脖子、梨形半身躯干,底部自然截断。',
'身体两侧只允许出现圆润肩膀轮廓和一个很小的肩部连接圆点或肩窝标记;绝对不要画伸出的手臂、前臂、手掌、猫爪、小手、小脚、腿、脚或尾巴。肩膀外侧必须留空,后续网页会单独叠加两条手臂。',
'猫咪造型参考输入猫猫头的暖橘白配色:头顶、耳朵外侧和身体侧边为淡橘色,脸和肚子为浅奶油白,少量浅橘斑纹;五官使用暖棕细线和小面积深棕眼睛。',
'请避免粉色大背景、避免主体外侧彩色光晕,主体贴纸外轮廓之外必须直接是纯色背景;线条为柔和暖棕或浅橘棕,不要纯黑粗描边。',
'资源自身保持清晰不透明,半透明效果由网页 CSS 控制;整体像儿童绘本可动纸偶底座,缩小后仍能看清大猫头、短身体、肩部连接点。',
'禁止手臂、爪子、小手、脚、尾巴;禁止黑色、灰色、黑白猫、奶牛猫、虎斑深色块、大面积深棕毛、真实毛发、尖锐漫画黑线、高反差阴影、文字、数字、按钮、面板、水印和真实照片质感。',
styleReferenceNote,
noStretchNote,
chromaKeyNote,
].join(''),
},
{
id: 'wave-cat-arm-guide-v6',
output: 'picture-book-wave-cat-arm-guide-v6.png',
sourceOutput: 'picture-book-wave-cat-arm-guide-v6-source.png',
size: '1024x1024',
transparent: true,
transparencyCleanup: 'cat-guide',
useBackgroundReference: true,
useLayoutReference: true,
useWaveCatHeadReference: true,
layoutNormalization: {
canvasWidth: 1024,
canvasHeight: 1024,
fit: 'contain',
fillWidth: 0.74,
fillHeight: 0.88,
anchorY: 'bottom',
padding: 20,
},
prompt: [
'请重新绘制儿童动作互动游戏猫咪挥手动画用的单侧手臂资源,严格作为可动纸偶拆件。只画一条橘白猫手臂:底部是肩膀连接端,向左上方弯曲,末端是一只简化圆猫手。',
'猫手必须像多啦A梦式圆手或软玩具圆爪一个完整圆润手掌不画手指不画黑色或深色爪垫不画粉色爪垫点不画尖爪。手臂短而厚实像小猫上肢不要成人类长手臂。',
'资源必须适合网页左右镜像复用和围绕肩部连接点旋转:肩膀连接端在画面底部偏内侧,圆手在画面上方,四周留透明空白。',
'颜色参考输入猫猫头:浅奶油白和淡橘色为主体,少量浅橘斑纹,柔和暖棕或浅橘棕描边;不要纯黑粗描边。',
'请避免粉色大背景、避免主体外侧彩色光晕,主体贴纸外轮廓之外必须直接是纯色背景。资源自身保持清晰不透明,半透明效果由网页 CSS 控制。',
'不要画猫头、躯干、另一只手臂、完整动物、腿、脚、文字、数字、按钮、面板、水印、真实照片质感、黑色、灰色、黑白毛、黑灰重阴影或深色大面积毛。',
styleReferenceNote,
noStretchNote,
chromaKeyNote,
].join(''),
},
];
const args = new Map();
@@ -443,6 +805,12 @@ function buildRequestBody(asset, size) {
path.join(intermediateDir, layoutReferenceOutput),
);
}
if (asset.useWaveCatHeadReference) {
pushReferenceImage(
body,
path.join(assetDir, 'picture-book-wave-cat-head-guide-v2.png'),
);
}
return body;
}
@@ -670,6 +1038,93 @@ function removeCharacterOutlineChromaKey(sourcePath, finalPath) {
}
}
function removeCatGuideChromaKey(sourcePath, finalPath) {
const script = [
'from collections import deque',
'from PIL import Image',
'import sys',
'source, out = sys.argv[1], sys.argv[2]',
'im = Image.open(source).convert("RGBA")',
'px = im.load()',
'w, h = im.size',
'corner_samples = [im.getpixel((0, 0)), im.getpixel((w - 1, 0)), im.getpixel((0, h - 1)), im.getpixel((w - 1, h - 1))]',
'key = tuple(sorted([p[i] for p in corner_samples])[len(corner_samples) // 2] for i in range(3))',
'def is_magenta_bg(r, g, b):',
' if r > 170 and b > 145 and g < 185 and min(r, b) - g > 36:',
' return True',
' return r > 140 and b > 90 and r > g + 35 and b > g + 10',
'def is_bg_candidate(x, y):',
' r, g, b, a = px[x, y]',
' if a <= 10:',
' return True',
' dist = ((r - key[0]) ** 2 + (g - key[1]) ** 2 + (b - key[2]) ** 2) ** 0.5',
' if is_magenta_bg(r, g, b):',
' return True',
' if key[0] < 32 and key[1] < 32 and key[2] < 32:',
' return dist < 34 and max(r, g, b) < 55',
' if key[0] > 225 and key[1] > 225 and key[2] > 225:',
' return dist < 34 and min(r, g, b) > 210',
' return dist < 72',
'visited = bytearray(w * h)',
'queue = deque()',
'def push(x, y):',
' if x < 0 or y < 0 or x >= w or y >= h:',
' return',
' index = y * w + x',
' if visited[index] or not is_bg_candidate(x, y):',
' return',
' visited[index] = 1',
' queue.append((x, y))',
'for x in range(w):',
' push(x, 0)',
' push(x, h - 1)',
'for y in range(h):',
' push(0, y)',
' push(w - 1, y)',
'while queue:',
' x, y = queue.popleft()',
' push(x + 1, y)',
' push(x - 1, y)',
' push(x, y + 1)',
' push(x, y - 1)',
'for _ in range(3):',
' extra = []',
' for y in range(1, h - 1):',
' for x in range(1, w - 1):',
' index = y * w + x',
' if visited[index] or not is_bg_candidate(x, y):',
' continue',
' touches_bg = any(visited[(y + dy) * w + x + dx] for dy in (-1, 0, 1) for dx in (-1, 0, 1) if dx or dy)',
' if touches_bg:',
' extra.append(index)',
' if not extra:',
' break',
' for index in extra:',
' visited[index] = 1',
'for y in range(h):',
' for x in range(w):',
' r, g, b, a = px[x, y]',
' if visited[y * w + x]:',
' px[x, y] = (r, g, b, 0)',
' else:',
' if a <= 10:',
' a = 255',
' px[x, y] = (r, g, b, a)',
'im.save(out)',
].join('\n');
const result = spawnSync('python', ['-c', script, sourcePath, finalPath], {
cwd: repoRoot,
encoding: 'utf8',
});
if (result.status !== 0) {
throw new Error(
`Failed to clean cat guide transparency: ${(result.stderr || result.stdout).trim()}`,
);
}
}
function normalizeTransparentAsset(finalPath, layoutNormalization) {
if (!layoutNormalization) {
return;
@@ -857,6 +1312,8 @@ async function generateAsset(asset, env, size, force) {
removeUiPanelChromaKey(opaqueSourcePath, finalPath);
} else if (asset.transparencyCleanup === 'character-outline') {
removeCharacterOutlineChromaKey(opaqueSourcePath, finalPath);
} else if (asset.transparencyCleanup === 'cat-guide') {
removeCatGuideChromaKey(opaqueSourcePath, finalPath);
} else {
removeChromaKey(opaqueSourcePath, finalPath);
}
@@ -917,6 +1374,8 @@ async function generateAsset(asset, env, size, force) {
removeUiPanelChromaKey(opaqueSourcePath, finalPath);
} else if (asset.transparencyCleanup === 'character-outline') {
removeCharacterOutlineChromaKey(opaqueSourcePath, finalPath);
} else if (asset.transparencyCleanup === 'cat-guide') {
removeCatGuideChromaKey(opaqueSourcePath, finalPath);
} else {
removeChromaKey(opaqueSourcePath, finalPath);
}

View File

@@ -0,0 +1,480 @@
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 = 420000;
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,
),
);