Merge remote-tracking branch 'origin/master' into hermes/wechat
# Conflicts: # .hermes/shared-memory/pitfalls.md # .hermes/todos/【后端架构】api-server能力模块化与图片资产Adapter收口计划-2026-05-14.md
This commit is contained in:
@@ -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}"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
480
scripts/generate-taonier-logo-concepts.mjs
Normal file
480
scripts/generate-taonier-logo-concepts.mjs
Normal 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,
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user