diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 50476ac8..7447fd68 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,20 +16,47 @@ --- -## 2026-05-20 汪汪声浪 v1 公开闭环计划 +## 2026-05-22 敲木鱼图片创作采用双图 image2 链路 -- 背景:Bark Battle v1 需要把创作、生成、结果、发布、详情和正式运行态收成一条闭环,避免把草稿试玩、公开广场和正式成绩混在一起。 -- 决策:`bark-battle` 入口改为 6 字段表单(作品标题、简介、主题 / 竞技背景描述 `themeDescription`、玩家形象描述、对手形象描述、难度);提交后进入 `bark-battle-generating` 独立生成页,自动生成玩家形象、对手形象和竞技背景三图,部分失败也继续进入结果页。旧“角色设定 / 狗狗皮肤预设 / themePreset”统一退场,配置和文档只使用“形象描述 / themeDescription”。结果页只保留单槽重试、重新生成和上传,不再保留一次生成按钮、音频配置入口、皮肤预设入口或排名配置。发布后先跳统一作品详情页 `/works/detail?work=BB-xxxxxxxx`,再由详情页进入正式 `published` runtime;正式 runtime 必须真实麦克风,`draft` 可试玩、可 mock 且不写正式统计。公开广场统一读取 `bark_battle_gallery_view` read model。 -- 影响范围:`BarkBattleConfigEditor`、`BarkBattleGeneratingView`、`BarkBattleResultView`、`BarkBattleRuntimeShell`、`PlatformEntryFlowShellImpl`、`appPageRoutes`、Bark Battle creation/runtime client、公开广场聚合与相关交互测试。 -- 验证方式:提交表单后先进入生成页;生成页部分失败仍能落到结果页;结果页只出现单槽重试 / 重新生成 / 上传;发布后先到 `/works/detail?work=BB-xxxxxxxx` 再进正式 runtime;正式 runtime 会要求麦克风并写基础统计,草稿试玩可 mock 且不写正式 run;公开广场读取 `bark_battle_gallery_view`。 -- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 +- 背景:敲木鱼自定义题材只生成中央敲击物时,运行态缺少与新主题匹配的竖屏背景;若直接让背景 prompt 自由发挥,又容易把敲击物或木槌画进背景里。 +- 决策:敲木鱼 `compile-draft` / `regenerate-hit-object` 图片链路固定为两步 image2 edits。第一步调用 VectorEngine `/v1/images/edits` + `gpt-image-2`,以默认木鱼图作为结构和画风参考,用户上传参考图只作为同次请求的新主题参考,结合用户题材关键词或参考图主题生成 `1:1` 透明底新敲击物并写回 `hitObjectAsset`;第二步以新敲击物图作为主题和画风参考,结合用户原始题材生成 `9:16` 背景环境图并写回 `backgroundAsset`。两步 prompt 使用 PRD 中固定隐藏关键词,不追加额外 negative prompt;背景图不得包含敲击物本体或木槌互动物品。 +- 影响范围:`api-server` 木鱼图片生成编排、`wooden_fish_work_profile.background_asset_json`、shared contracts、前端结果页 / 运行态背景展示、敲木鱼 PRD 和平台链路文档。 +- 验证方式:执行 `cargo test -p api-server wooden_fish --manifest-path server-rs/Cargo.toml`、`cargo test -p spacetime-client wooden_fish --manifest-path server-rs/Cargo.toml`、`npm run spacetime:generate`、`npm run check:spacetime-schema`、`npm run typecheck`。 +- 关联文档:`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 -## 2026-05-22 汪汪声浪运行态与作品外显信息收口 +## 2026-05-21 外部 API 失败必须 OTLP 上报并落库 -- 背景:Bark Battle v1 在正式运行态、图片生成提示词和作品外部卡片上仍存在体验漂移:能量条推满后还要等计时结束、进入正式 runtime 后还要二次点击声控、角色形象 prompt 会默认注入狗主体、草稿 / 已发布卡片外部看不到创作者。 -- 决策:能量条到玩家或对手边界即结算;正式 `published` runtime 从作品详情启动后立即申请真实麦克风权限,授权成功后立刻进入倒计时,并使用 start run 返回的 `runtimeConfig` 作为本局前端规则参数;结束后弹出独立结算弹窗,运行态固定提供返回按钮。玩家 / 对手形象图提示词保持用户填写的形象描述,只要求单个完整形象、正面和透明背景,不把非狗描述改写成狗;草稿架、已发布作品架、统一作品详情和公开广场列表都展示后端返回的 `authorDisplayName`。Bark Battle 卡片封面按竞技背景、玩家形象、对手形象、入口参考图兜底;works summary 优先读取 `publishedSnapshotJson` 的最终发布素材。拟声词进入配置 JSON,未手动编辑时随主题 / 形象描述重算,手动编辑后保持创作者自定义;触发阈值降到 `0.35`、冷却降到 `150ms`,后端 `BarkBattleRuleset.min_bark_gap_ms` 同步为 `150`,局内有效触发后快速随机展示高能词池。 -- 影响范围:`BarkBattleSession`、`BarkBattleRuntimeShell`、`BarkBattleConfigEditor`、`BarkBattleConfig`、Bark Battle 生图 prompt、Bark Battle works/gallery summary、创作中心作品架卡片、公开作品码、`module-bark-battle` ruleset 和玩法链路文档。 -- 验证方式:能量条推到 `100/-100` 的领域测试应提前 finished;发布态 runtime mount 后应自动调用麦克风 sampler、登记正式 run 并使用服务端 runtimeConfig;prompt 单测应覆盖透明背景、正面和非狗描述不强注入狗;作品架测试应覆盖草稿与已发布卡片作者展示和封面兜底;拟声词测试应覆盖主题自动重算、自定义保持和随机展示。 +- 背景:图片生成等外部供应商调用失败时,仅返回 502/504 或普通日志无法支持后续按 provider、阶段和重试属性聚合排障。 +- 决策:外部 API 调用未成功时,`api-server` 必须同时发送 OTLP 失败观测并写入 `tracking_event`。当前通用 VectorEngine `gpt-image-2` 图片生成 / 编辑适配器记录 `external_api_call_failure`,`scope_kind = module`、`scope_id = provider`、`module_key = external-api`,metadata 包含 endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、latencyMs、promptChars、referenceImageCount、imageModel 和 rawExcerpt。 +- 落库方式:优先复用 tracking outbox 异步批量写入;outbox 不可写或因保护阈值拒绝时回退同步直写 SpacetimeDB。不新增 SpacetimeDB 表,不让 reducer 做外部 I/O。 +- 影响范围:`server-rs/crates/api-server/src/external_api_audit.rs`、`server-rs/crates/api-server/src/openai_image_generation.rs`、`server-rs/crates/api-server/src/telemetry.rs`、tracking outbox、后端架构文档和开发运维文档。 +- 验证方式:执行 `cargo test -p api-server external_api_audit --manifest-path server-rs/Cargo.toml -- --nocapture`、`cargo test -p api-server openai_image_generation --manifest-path server-rs/Cargo.toml -- --nocapture`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`。 +- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + +## 2026-05-21 拼图参考图主链改为 OSS assetObjectId 与只读签名 URL + +- 背景:release 上拼图图生图生成草稿时,旧链路把上传图转成 Data URL/base64 放进创作 action JSON body,容易先触发 Nginx `413 Request Entity Too Large`,也让外部模型调用前的 HTTP body 过大。 +- 决策:浏览器参考图先通过资产直传票据上传 OSS,并确认 `asset_object`;拼图 action 主链只提交 `referenceImageAssetObjectId(s)`。`api-server` 按当前登录用户校验 asset owner、bucket、kind、图片 MIME 和大小后签发 OSS 只读 URL,传给 VectorEngine 的 generation fallback 使用;需要 edits multipart 时由后端用该签名 URL 拉取字节,不再让前端把图片塞进 JSON body。 +- 兼容边界:旧 `referenceImageSrc(s)` Data URL 与历史 `/generated-*` 路径仅保留给旧草稿、旧入口和迁移期请求;调大 Nginx `client_max_body_size` 只作为兼容兜底,不是长期创作主链。 +- 影响范围:拼图创作前端、`packages/shared` / `shared-contracts` action DTO、`api-server` 拼图 VectorEngine 编排、资产确认和 `spacetime-client` 资产读取 facade。 +- 验证方式:前端 payload 中 AI 重绘优先出现 `referenceImageAssetObjectId(s)` 且 `referenceImageSrc(s)` 不再携带 Data URL;后端 `puzzle_vector_engine_generation_prefers_signed_reference_url`、`puzzle_reference_image_sources_prefer_asset_object_ids`、`puzzle_asset_object_reference_requires_matching_owner` 通过。 +- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + +## 2026-05-21 Nginx 通用 API 入口放行创作参考图请求体 + +- 背景:release 上拼图结果页重绘动作携带参考图 Data URL 时,Nginx access log 出现 `413`、`request_time=0.000`、`upstream_status=-`,说明请求被反代层默认 1 MiB 上限拦截,未进入 `api-server`。 +- 决策:发布、开发服和容器 Nginx 模板的通用 `location ~ ^/api(?:/|$)` 统一设置 `client_max_body_size 64m`。该值只作为反代放行和旧 Data URL 请求兼容兜底,具体业务请求体和图片字节上限继续由 `api-server` 路由 `DefaultBodyLimit`、OSS asset 确认和业务校验控制,不能替代接口级限制;拼图参考图长期主链见同日 `OSS assetObjectId` 决策。 +- 影响范围:`deploy/nginx/genarrative.conf`、`deploy/nginx/genarrative-dev-http.conf`、`deploy/container/nginx.conf`、Nginx README、生产运维文档和 release 排障口径。 +- 验证方式:目标机 `nginx -T 2>/dev/null | grep client_max_body_size` 应看到 `client_max_body_size 64m;`;大于 1 MiB 的参考图请求不再在 Nginx 层直接 413,access log 应出现有效 `upstream_status`。 +- 关联文档:`deploy/nginx/README.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + +## 2026-05-22 抓大鹅素材生成改为关卡整图派生三图 + +- 背景:旧抓大鹅素材链路按物品 5x5 sheet、纯背景和独立容器图分开生产,难以保证背景、UI、容器和物品风格一致,也让结果页继续暴露背景 / 容器重生成入口。 +- 决策:抓大鹅草稿生成先用 `gpt-image-2` 无参考图生成竖屏 `9:16` 完整关卡画面;关卡画面完成后,以它作为参考并发生成三张可运行资产:`1K 1:1` UI spritesheet、`1K 9:16` 关卡背景图、`2K 1:1` 物品 spritesheet。UI 与物品 spritesheet 都固定要求纯绿色绿幕背景,后端上传 OSS 前扣成真实透明 PNG。物品 spritesheet 固定 `10*10`,每行两种物品、每种五个形态。运行态和编辑器都按 alpha 连通域矩形检测解析 UI 和物品图集,不按固定像素坐标切图。 +- 兼容:新增字段继续存入现有 `generatedItemAssets[].backgroundAsset` / `generatedBackgroundAsset` JSON,不新增 SpacetimeDB schema 字段。历史 `containerImage*` 字段只作兼容;如果它与 `uiSpritesheetImage*` 同源,不得再作为运行态中心容器图。 +- 影响范围:`server-rs/crates/api-server/src/match3d/*`、`server-rs/crates/shared-contracts/src/match3d_*`、`packages/shared/src/contracts/match3dWorks.ts`、`src/components/match3d-result/Match3DResultView.tsx`、`src/components/match3d-runtime/Match3DRuntimeShell.tsx`、`src/services/match3dSpritesheetParser.ts`。 +- 验证方式:执行 `cargo test -p api-server match3d --manifest-path server-rs\Cargo.toml`、`npm run test -- src/components/match3d-result/Match3DResultView.test.tsx src/components/match3d-runtime/Match3DRuntimeShell.test.tsx src/services/match3dSpritesheetParser.test.ts src/services/match3dGeneratedModelCache.test.ts`、`npm run typecheck`、`npm run check:encoding`。 - 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 ## 2026-05-18 Rust 手写模块入口统一不用 mod.rs @@ -130,6 +157,30 @@ - 验证方式:新增玩法 PRD 必须显式声明单图资产槽位和系列素材槽位;新增工作台测试确认没有默认聊天式 Agent 输入;skill 通过 `quick_validate.py`。 - 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`.codex/skills/genarrative-play-type-integration/SKILL.md`、`.hermes/skills/genarrative-play-type-integration/SKILL.md`。 +## 2026-05-20 敲木鱼玩法按完整平台纵切接入 + +- 背景:敲木鱼玩法需要对齐拼图 / 跳一跳的创作闭环,不能做成孤立 demo 或前端本地计数工具。 +- 决策:新增 `wooden-fish` 玩法,采用表单 / 图片输入工作台、单图敲击物资产槽位、敲击音效资产槽位和最多 8 条飘字配置;公开作品号前缀为 `WF-*`;运行态只在单次 run 内累计总敲击次数和词条计数。后端新增独立 `module-wooden-fish`、shared contracts、SpacetimeDB `wooden_fish_*` 表 / public views、`spacetime-client` facade 和 `/api/creation/wooden-fish/*`、`/api/runtime/wooden-fish/*` 路由,前端接入平台入口、生成页、结果页、运行态、公开详情和推荐试玩。 +- 影响范围:`CONTEXT.md`、`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`packages/shared/src/contracts/woodenFish.ts`、`server-rs/crates/shared-contracts/src/wooden_fish.rs`、`server-rs/crates/module-wooden-fish/`、`server-rs/crates/spacetime-module/src/wooden_fish*`、`server-rs/crates/spacetime-client/src/wooden_fish.rs`、`src/components/wooden-fish-*`。 +- 验证方式:执行敲木鱼契约 / module / facade / runtime model / platform entry 定向测试、`npm run typecheck`、`npm run check:encoding`、`npm run check:spacetime-schema`、`cargo check -p api-server --manifest-path server-rs\Cargo.toml`,本地 smoke 使用 mock 短信配置后检查 `/healthz`。 +- 关联文档:`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 + +## 2026-05-21 敲木鱼敲击音效复用通用 Vidu 音效链路 + +- 背景:敲木鱼创作需要通过“敲击音效”描述生成真实短音频,不能继续由 `spacetime-client` 合成 `/generated-wooden-fish-assets/...` 假路径;同时拼图和抓大鹅音频生成入口仍需保持关闭。 +- 决策:通用 `/api/creation/audio/sound-effect` 提交 Vidu 音效任务;`/api/creation/audio/sound-effect/{task_id}/asset` 只对木鱼 `hit_sound` 目标开放,完成查询、下载、OSS 私有对象、`asset_object` 和 entity binding 写入。木鱼 `compile-draft` / `generate-hit-sound` 在 `api-server` 内复用同一 helper 生成并注入 `hitSoundAsset`,`spacetime-client` 缺少真实 `hitSoundAsset` 时拒绝编译。拼图和抓大鹅相关目标继续返回 `410 Gone`。 +- 影响范围:`server-rs/crates/api-server/src/vector_engine_audio_generation.rs`、`server-rs/crates/api-server/src/wooden_fish.rs`、`server-rs/crates/spacetime-client/src/wooden_fish.rs`、`shared-contracts` / `packages/shared` 的 `creationAudio` 契约、敲木鱼 PRD 与平台链路文档。 +- 验证方式:执行 `cargo test -p shared-contracts creation_audio --manifest-path server-rs\Cargo.toml`、`cargo test -p spacetime-client wooden_fish --manifest-path server-rs\Cargo.toml`、`cargo test -p api-server wooden_fish --manifest-path server-rs\Cargo.toml`、`cargo test -p api-server disabled_creation_audio_targets_return_gone_except_wooden_fish_sound_effects --manifest-path server-rs\Cargo.toml`、`npm run typecheck`、`npm run check:encoding`,本地 smoke 检查 `/healthz`;真实生成需同时配置 VectorEngine 与 OSS AccessKey。 +- 关联文档:`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 + +## 2026-05-21 敲木鱼默认敲击物使用内置透明 PNG + +- 背景:默认敲木鱼图案若继续用“木鱼”关键词临时生成,image2 容易语义化重画并改变用户认可的原始造型。 +- 决策:默认模板使用内置资源 `/wooden-fish/default-hit-object.png` 写回 `bundled-default` 敲击物资产;仅当用户输入自定义关键词、上传参考图或主动重生成敲击物时,才走 image2 -> OSS -> asset object -> entity binding 链路。创作入口卡片、结果页、运行态和公开列表兜底统一使用该 PNG。 +- 影响范围:敲木鱼工作台默认提示词、api-server 木鱼默认资产编排、创作入口种子与迁移、平台公开卡片兜底、PRD 与平台链路文档。 +- 验证方式:默认 `compile-draft` 返回的 `hitObjectAsset.generationProvider` 应为 `bundled-default` 且 `imageSrc=/wooden-fish/default-hit-object.png`;自定义关键词或参考图仍走 image2;前端静态资源可通过 Vite 直接访问。 +- 关联文档:`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + ## 2026-05-21 RPG publish_world 设定文本以后端草稿真相派生 - 背景:RPG 结果页发布动作只保证提交 `{ action: 'publish_world' }`;旧 agent 会话可能没有 `seed_text`,但 `draft_profile_json` 已经通过 `publish_gate` 并可发布。 @@ -407,6 +458,12 @@ - 决策:热身关全流程直接接入 `useMocapInput`,通过本地 mocap WebSocket `/stream` 消费 `general.body.center_norm` 身体中心、`actions/action/gesture/gestures/event/name/type` 动作名,以及 `hands[]`、`leftHand/rightHand`、`left_hand/right_hand` 手部坐标;位置步骤由身体中心推进,`wave_greeting`、`wave_left_hand`、`wave_right_hand` 和 `jump_once` 由 mocap 手势/轨迹推进。浏览器摄像头只作为背景层,动作数据源状态优先展示,键鼠仍作为本地调试兜底。 - 影响范围:`src/services/useMocapInput.ts`、`src/components/child-motion-demo/ChildMotionWarmupDemo.tsx`、对应单测与热身关技术文档。 - 验证方式:执行 `npx vitest run src/services/useMocapInput.test.ts src/components/child-motion-demo/ChildMotionWarmupDemo.test.tsx src/components/child-motion-demo/childMotionWarmupModel.test.ts src/services/child-motion-demo/childMotionDebugInput.test.ts src/routing/appRoutes.test.ts`、`npx eslint ...`、`npm run typecheck`、`npm run check:encoding`,并确认 `http://127.0.0.1:8876/stream` WebSocket 可握手、`http://127.0.0.1:3000/child-motion-demo` 可访问。 + +## 2026-05-18 寓教于乐频道补充热身关入口 +- 背景:用户希望在发现页的寓教于乐板块里直接看到热身关入口,而不是只依赖独立直达路由。 +- 决策:`child-motion-demo` 作为寓教于乐频道的独立卡片展示,点击后直接进入 `/child-motion-demo`;该入口与 `宝贝爱画` 并列,仍复用现有独立热身关路由,不新增新的创作模板或运行态壳层。 +- 影响范围:`src/components/rpg-entry/RpgEntryHomeView.tsx`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 +- 验证方式:执行入口回归测试、`npm run typecheck`、`npm run check:encoding`,并在发现页的寓教于乐频道确认热身关卡卡片可点击进入 `/child-motion-demo`。 - 关联文档:`docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md`。 ## 2026-05-09 GPT-image-2 图片生成统一迁移到 VectorEngine @@ -659,6 +716,14 @@ - 验证方式:开发前优先阅读 `CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`;旧 `server-node`、Express、PostgreSQL、Go 方向只允许作为迁移参考。 - 关联文档:`docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`、`AGENTS.md`。 +## 2026-05-18 寓教于乐电视端入口概念图采用横屏乐园地图方案 + +- 背景:寓教于乐板块需要面向电视端 / 横屏大屏的一组图形化入口概念图,既要像儿童乐园地图,又要和现有绘本插画风一致。 +- 决策:概念探索优先采用横屏乐园地图结构,推荐顺序为环形乐园岛、展开绘本地图、云朵空中岛、草地舞台地图;生成时优先复用 `public/child-motion-demo/picture-book-grass-stage.png` 作为风格参考,输出仅保留在 `output/imagegen/` 概念目录中,不直接进入正式资源目录。 +- 影响范围:寓教于乐板块视觉探索、后续前端入口设计、`scripts/generate-edutainment-tv-map-concepts.mjs`、相关设计文档。 +- 验证方式:概念图需保持无文字、无真实品牌 IP、无暗色科技风,并与现有草地绘本资源在配色和笔触上保持一致。 +- 关联文档:`docs/design/【前端体验】寓教于乐电视端乐园地图入口概念图-2026-05-18.md`。 + ## 2026-04-28/29 server-rs DDD 分层与契约矩阵冻结 - 背景:server-rs 模块多、上下文多,需防止领域规则、SpacetimeDB 表、HTTP BFF、前端临时逻辑互相污染。 @@ -715,6 +780,38 @@ - 影响范围:`api-server` tracking 中间件、SpacetimeDB tracking procedure、部署数据目录、OTLP 指标和运维排障。 - 验证方式:数据库不可用时公开 route 请求不失败且 outbox 文件保留;恢复后批量写入成功并删除本地 sealed 文件;关键事件仍立即影响任务 / 统计。 +## 2026-05-19 跳一跳平台公开链路采用独立玩法路由 + +- 背景:跳一跳玩法已接入平台入口、推荐、公开详情、试玩和运行态,后续继续扩展公开广场或推荐流时需要避免把它当成拼图兼容分支。 +- 决策:跳一跳公开路由统一依赖 `sourceType='jump-hop'` 和 `JH-*` public code;平台首页、推荐、公开作品列表/详情、试玩和运行态都按 `jump-hop` 独立玩法分发。后端仍是作品、运行和发布状态的业务真相,前端只做展示、交互和临时 UI 状态,不在页面层补业务规则或权限判断。 +- 影响范围:平台入口、推荐流、公开详情、试玩启动、跳一跳运行态、`api-server` / SpacetimeDB 公开投影和 shared contracts。 +- 验证方式:从平台推荐或公开详情进入跳一跳作品时,路由 source type 为 `jump-hop`、public code 为 `JH-*`,运行态启动消费后端返回的完整 profile / run 数据;后端 smoke 统一使用 `npm run dev:api-server` 启动并检查 `/healthz`。 +- 关联文档:`docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + +# 2026-05-20 陶泥儿主视觉配色回收为暖白/陶土橙 + +- 背景:用户要求只替换产品各界面的 UI 颜色,不改布局,并以两张陶泥儿主视觉图作为配色依据。 +- 决策:平台亮色主题的主色回收到暖白 / 米杏底、陶土橙主按钮、深棕正文与浅杏边框;后台管理也同步切换到同一暖橙体系。主题变量优先通过 `src/index.css` 的 `--platform-*` token 统一控制,零散组件只做必要的局部替换。 +- 影响范围:主站平台壳层、常用表单 / 按钮 / 卡片 / 背景、后台管理 UI、业务进度条和小游戏结果条的通用强调色。 +- 验证方式:优先检查 `src/index.css` 与 `apps/admin-web/src/styles/admin.css` 是否还存在旧粉色主色;再用编码检查和可执行的本地 typecheck / build 验证。 +- 关联文档:`docs/【项目基线】当前产品与工程约束-2026-05-15.md`。 + +## 2026-05-20 汪汪声浪 v1 公开闭环计划 + +- 背景:Bark Battle v1 需要把创作、生成、结果、发布、详情和正式运行态收成一条闭环,避免把草稿试玩、公开广场和正式成绩混在一起。 +- 决策:`bark-battle` 入口改为 6 字段表单(作品标题、简介、主题 / 竞技背景描述 `themeDescription`、玩家形象描述、对手形象描述、难度);提交后进入 `bark-battle-generating` 独立生成页,自动生成玩家形象、对手形象和竞技背景三图,部分失败也继续进入结果页。旧“角色设定 / 狗狗皮肤预设 / themePreset”统一退场,配置和文档只使用“形象描述 / themeDescription”。结果页只保留单槽重试、重新生成和上传,不再保留一次生成按钮、音频配置入口、皮肤预设入口或排名配置。发布后先跳统一作品详情页 `/works/detail?work=BB-xxxxxxxx`,再由详情页进入正式 `published` runtime;正式 runtime 必须真实麦克风,`draft` 可试玩、可 mock 且不写正式统计。公开广场统一读取 `bark_battle_gallery_view` read model。 +- 影响范围:`BarkBattleConfigEditor`、`BarkBattleGeneratingView`、`BarkBattleResultView`、`BarkBattleRuntimeShell`、`PlatformEntryFlowShellImpl`、`appPageRoutes`、Bark Battle creation/runtime client、公开广场聚合与相关交互测试。 +- 验证方式:提交表单后先进入生成页;生成页部分失败仍能落到结果页;结果页只出现单槽重试 / 重新生成 / 上传;发布后先到 `/works/detail?work=BB-xxxxxxxx` 再进正式 runtime;正式 runtime 会要求麦克风并写基础统计,草稿试玩可 mock 且不写正式 run;公开广场读取 `bark_battle_gallery_view`。 +- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## 2026-05-22 汪汪声浪运行态与作品外显信息收口 + +- 背景:Bark Battle v1 在正式运行态、图片生成提示词和作品外部卡片上仍存在体验漂移:能量条推满后还要等计时结束、进入正式 runtime 后还要二次点击声控、角色形象 prompt 会默认注入狗主体、草稿 / 已发布卡片外部看不到创作者。 +- 决策:能量条到玩家或对手边界即结算;正式 `published` runtime 从作品详情启动后立即申请真实麦克风权限,授权成功后立刻进入倒计时,并使用 start run 返回的 `runtimeConfig` 作为本局前端规则参数;结束后弹出独立结算弹窗,运行态固定提供返回按钮。玩家 / 对手形象图提示词保持用户填写的形象描述,只要求单个完整形象、正面和透明背景,不把非狗描述改写成狗;草稿架、已发布作品架、统一作品详情和公开广场列表都展示后端返回的 `authorDisplayName`。Bark Battle 卡片封面按竞技背景、玩家形象、对手形象、入口参考图兜底;works summary 优先读取 `publishedSnapshotJson` 的最终发布素材。拟声词进入配置 JSON,未手动编辑时随主题 / 形象描述重算,手动编辑后保持创作者自定义;触发阈值降到 `0.35`、冷却降到 `150ms`,后端 `BarkBattleRuleset.min_bark_gap_ms` 同步为 `150`,局内有效触发后快速随机展示高能词池。 +- 影响范围:`BarkBattleSession`、`BarkBattleRuntimeShell`、`BarkBattleConfigEditor`、`BarkBattleConfig`、Bark Battle 生图 prompt、Bark Battle works/gallery summary、创作中心作品架卡片、公开作品码、`module-bark-battle` ruleset 和玩法链路文档。 +- 验证方式:能量条推到 `100/-100` 的领域测试应提前 finished;发布态 runtime mount 后应自动调用麦克风 sampler、登记正式 run 并使用服务端 runtimeConfig;prompt 单测应覆盖透明背景、正面和非狗描述不强注入狗;作品架测试应覆盖草稿与已发布卡片作者展示和封面兜底;拟声词测试应覆盖主题自动重算、自定义保持和随机展示。 +- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + ## 2026-05-19 汪汪声浪默认开放并区分草稿试玩与正式运行态 - 背景:`bark-battle` 已具备草稿结果页、发布链路与运行态 API,继续在入口层标记“敬请期待”会阻断创作闭环;同时草稿试玩不应污染正式成绩统计。 diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 4299124d..04bfcdd0 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -5,6 +5,7 @@ ## 记录格式 ```md + ## 问题标题 - 现象:看到什么错误或异常行为 @@ -1289,3 +1290,51 @@ - 处理:`platformEntryTypes.ts` 必须注册 `jump-hop-workspace/generating/result/runtime/gallery-detail`;`appPageRoutes.ts` 必须补 `/creation/jump-hop/workspace`、`/creation/jump-hop/generating`、`/creation/jump-hop/result`、`/gallery/jump-hop/detail`、`/runtime/jump-hop`;`PlatformEntryFlowShellImpl.tsx` 必须持有 JumpHop session/work/run/gallery/runtimeReturnStage/generationState/error/busy,并提供 `mapJumpHopWorkToPublicWorkDetail`;`RpgEntryHomeView.tsx` 的公开卡片类型描述要给 JumpHop 单独返回 `跳一跳`。 - 验证:`npm run typecheck`,并跑 `npm test -- src/routing/appPageRoutes.test.ts` 覆盖 JumpHop 阶段路径。 - 关联:`src/components/platform-entry/platformEntryTypes.ts`、`src/routing/appPageRoutes.ts`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/rpg-entry/RpgEntryHomeView.tsx`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## image2 dry-run 带参考图时不要直接打印 data URL + +- 现象:使用 VectorEngine `gpt-image-2-all` 生成带参考图的概念图时,如果 dry-run 直接打印完整请求体,参考图会被转成超长 `data:image/png;base64,...`,终端日志会被数百万字符淹没。 +- 原因:生成请求支持 `image` 数组传入 data URL 参考图;dry-run 如果复用 live 请求体输出,就会把参考图内容完整打印。 +- 处理:dry-run 输出摘要,只保留 `imageReferenceCount`、尺寸、模型和 prompt,不输出完整 base64。live 请求仍按实际需要传 `image` 数组。 +- 验证:执行 `node scripts/generate-edutainment-tv-map-concepts.mjs --dry-run`,输出应只显示 `imageReferenceCount: 1`,不出现完整 base64。 +- 关联:`scripts/generate-edutainment-tv-map-concepts.mjs`、`docs/design/【前端体验】寓教于乐电视端乐园地图入口概念图-2026-05-18.md`。 + +## 生成图资产不能只拼 generated legacy path + +- 现象:结果页或运行态拿到 `/generated-*-assets/.../image.png` 后图片不显示;前端 `ResolvedAssetImage` 会先调用 `/api/assets/read-url?legacyPublicPath=...`,但换签后的 OSS URL 仍指向不存在对象。 +- 原因:后端只写了看起来像生成图的 legacy path,没有真正调用 image2、上传 OSS、登记 `asset_object` 并绑定实体。`/api/assets/read-url` 只负责签名读取,不会凭空生成或补写对象。 +- 处理:玩法生成链路必须在 `api-server` 完成外部副作用:调用 VectorEngine `gpt-image-2-all`,用 `GeneratedImageAssetAdapter` 准备 `PutObject`,上传 OSS 私有对象,调用 `confirm_asset_object` 和 `bind_asset_object_to_entity`,再把返回的 `legacyPublicPath` 写入玩法 profile。 +- 验证:`cargo check -p api-server --manifest-path server-rs/Cargo.toml`;契约测试应断言前端 JSON 自带的 `hitObjectAsset` 会被忽略,spacetime-client 定向测试应断言缺少服务端注入的真实 `hitObjectAsset` 时不能编译;浏览器 Network 中 generated 图片应先换签,签名 URL 指向已存在对象。 +- 关联:`server-rs/crates/api-server/src/wooden_fish.rs`、`server-rs/crates/spacetime-client/src/wooden_fish.rs`、`src/components/ResolvedAssetImage.tsx`、`src/services/assetReadUrlService.ts`。 + +## `dev:spacetime` 启动后 3101 又断开先查 publish 是否被 spacetime.json 干扰 + +- 现象:浏览器报 `Failed to initiate WebSocket connection`,目标为 `ws://127.0.0.1:3101/v1/database//subscribe`,端口检查发现 `3101` 没有长期监听;手动运行 `npm run dev:spacetime` 可看到 standalone 短暂启动后退出,发布阶段报 `No database target matches ''`。 +- 原因:SpacetimeDB CLI 会读取仓库根目录 `spacetime.json`。如果本地发布命令没有显式 `--no-config`,CLI 可能按配置文件里的 target 解析数据库,覆盖脚本已传入的 `.env.local` 数据库名和 `--server`,导致 publish 失败;`dev.mjs` 捕获错误后会清理刚启动的 standalone,于是浏览器看到 3101 被拒绝连接。 +- 处理:`scripts/dev.mjs` 的本地 publish 固定追加 `--no-config`,只使用脚本解析出的数据库名、module path 和实际 SpacetimeDB server。排查时前台运行 `npm run dev:spacetime -- --no-interactive`,若看到该错误,先确认脚本是否仍带 `--no-config`,再查 `.env.local` / `spacetime.local.json` 的数据库名。 +- 验证:`npm run test -- scripts/dev.test.ts` 覆盖 publish 参数包含 `--no-config`;`npm run dev:spacetime -- --no-interactive` 后 `http://127.0.0.1:3101/v1/ping` 应保持 200。 +- 关联:`scripts/dev.mjs`、`scripts/dev.test.ts`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + +## 本地 api-server 启动订阅 401 先查 Web identity token 注入 + +- 现象:`npm run dev` 启动到 api-server 恢复认证快照时,日志出现 `Failed to initiate WebSocket connection ... /v1/database//subscribe?compression=Brotli: HTTP error: 401 Unauthorized`。 +- 原因:SpacetimeDB SDK 订阅需要 Web API identity token;本地 `.env.local` 常把 `GENARRATIVE_SPACETIME_TOKEN` 留空,只靠 CLI 登录态 publish 成功并不能让 api-server 的 WebSocket subscribe 获得权限。 +- 处理:`scripts/dev.mjs` 在 SpacetimeDB 就绪后调用 `/v1/identity` 创建当前进程专用 Web API identity token,并只注入本次 `api-server` 环境;不要把临时 token 写进 `.env.local` 或日志。若仍报 401,先确认是否使用了项目脚本启动、日志是否出现 `已创建本地 Web identity`,以及 `GENARRATIVE_SPACETIME_SERVER_URL` / 数据库名是否指向本次启动的实例。 +- 验证:`npm run test -- scripts/dev.test.ts`;重新运行 `npm run dev` 后 api-server 启动日志不再出现上述 subscribe 401,`/healthz` 返回 200。 +- 关联:`scripts/dev.mjs`、`scripts/dev.test.ts`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + +## 创作作品架消失先查入口配置 procedure 与本地库权限 + +- 现象:寓教于乐或创作中心下草稿 / 已发布作品突然整块消失,`GET /api/creation-entry/config` 返回 `502`,details 中为 `No such procedure`。 +- 原因:本地 `.env.local` 或 `spacetime.local.json` 指向的 SpacetimeDB 库没有发布当前 `spacetime-module`,或当前 CLI 身份无权发布该库;例如旧 `xushi-p4wfr` 库缺 `get_creation_entry_config` 时,前端拿不到入口配置就不会渲染作品架。 +- 处理:优先切换到拥有目标库权限的 SpacetimeDB 身份后重新运行 `npm run dev` 完成发布;若只是本地验证,可用 gitignored 的 `spacetime.local.json` 指向可发布的本地库。debug 构建的 `api-server` 对入口配置缺 procedure 会使用后端默认入口配置兜底,避免作品架因本地库漂移整块空白。 +- 验证:`curl.exe -i http://127.0.0.1:8082/api/creation-entry/config` 返回 `200` 且包含 `baby-object-match`;前端草稿页作品架重新渲染。 +- 关联:`server-rs/crates/api-server/src/state.rs`、`server-rs/crates/api-server/src/creation_entry_config.rs`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + +## 存档选择入口不要只藏在“玩过”弹窗里 + +- 现象:用户有 RPG / 拼图运行态存档,但平台底部 `草稿` Tab 只展示作品架,个人中心只有点击 `玩过` 后才可能看到“可继续”,导致看起来没有存档选择入口。 +- 原因:`/api/profile/save-archives` 已在入口 bootstrap 加载,但前端只把 `saveEntries` 注入 `ProfilePlayedWorksModal`;没有独立的存档入口。 +- 处理:个人中心 `常用功能` 必须保留 `存档` 快捷入口,点击后打开独立存档选择弹窗并复用 `SaveArchiveCard`;恢复仍走 `/api/profile/save-archives/{worldKey}`,拼图存档继续走拼图 resume 分支,RPG 走 `handleContinueGame(snapshot)`。 +- 验证:`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "profile page exposes save archive picker"`。 +- 关联:`src/components/rpg-entry/RpgEntryHomeView.tsx`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/rpg-entry/useRpgEntryBootstrap.ts`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 diff --git a/.hermes/shared-memory/project-overview.md b/.hermes/shared-memory/project-overview.md index 040d5771..45ffc1e4 100644 --- a/.hermes/shared-memory/project-overview.md +++ b/.hermes/shared-memory/project-overview.md @@ -10,6 +10,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台,把 A - RPG / 自定义世界创作与运行时。 - 拼图玩法创作、草稿、发布、运行态和排行榜。 +- 敲木鱼玩法创作、草稿、发布、运行态、公开详情和分享码。 - 抓大鹅 Match3D 创作、2D 多视角素材生成、发布和运行态。 - 大鱼吃小鱼、方洞挑战、视觉小说、汪汪声浪和儿童向寓教于乐玩法。 - 账号、短信 / 密码 / 微信登录、个人资料、任务、钱包、邀请码、充值、反馈、法律信息和后台管理。 diff --git a/CONTEXT.md b/CONTEXT.md index 2937f24b..160d1edd 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -18,6 +18,32 @@ _Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成 ## Language +### Wooden Fish + +**敲木鱼**: +轻量点击型互动玩法,玩家在单次运行中点击非功能区敲击中央物品,触发敲击音效、敲击动画、随机飘字和本次运行内的词条计数。 +_Avoid_: 长期功德账本、排行榜玩法、全局账户累计 + +**敲击物图案**: +敲木鱼作品中被玩家点击敲击的单张物品图案;默认模板使用内置透明 PNG `/wooden-fish/default-hit-object.png`,用户自定义关键词或上传图时再使用 image2 生成最终资产,上传图只作为 image2 参考。 +_Avoid_: 直接把上传图作为运行态素材、系列素材图集 + +**敲木鱼背景环境图**: +敲木鱼作品中的竖屏 9:16 背景资产;由后端在敲击物图案生成后,以新敲击物图案作为主题和画风参考,再结合用户原始题材关键词或参考图主题调用 image2 生成。背景只适配敲击物主题和画风,不包含敲击物本体或木槌互动物品。 +_Avoid_: 把背景当封面图、在背景里重复绘制敲击物、让前端临时拼背景 + +**敲击音效**: +敲木鱼作品中每次有效敲击播放的短音频资产,可由描述生成、文件上传或麦克风录制产生,最终统一写回作品的敲击音效资产槽位。 +_Avoid_: 背景音乐、长音频轨道、运行态实时录音 + +**飘字**: +每次有效敲击后从作品配置中等概率抽取词条,并在敲击物上方以“词条+1”短暂漂浮显示的文本;配置里只保存幸运、健康、财富、姻缘、幸福、事业、成功、功德等词条名本身。 +_Avoid_: 带权重奖励、账户属性、可结算货币 + +**单次 run 计数**: +敲木鱼运行态只在当前 run 内累计总敲击次数和已出现飘字词条计数,run 结束后作为摘要保存,不形成账号级长期账本。 +_Avoid_: 用户永久功德值、跨作品累计值、排行榜积分 + ### Bark Battle **汪汪声浪大作战**: diff --git a/docs/design/【前端体验】寓教于乐电视端乐园地图入口概念图-2026-05-18.md b/docs/design/【前端体验】寓教于乐电视端乐园地图入口概念图-2026-05-18.md new file mode 100644 index 00000000..2b6c4fc1 --- /dev/null +++ b/docs/design/【前端体验】寓教于乐电视端乐园地图入口概念图-2026-05-18.md @@ -0,0 +1,36 @@ +# 寓教于乐电视端乐园地图入口概念图 + +更新时间:`2026-05-18` + +## 背景 + +寓教于乐板块需要一个面向电视端 / 横屏大屏的图形化入口,整体感觉接近主题乐园地图,但必须保留 Genarrative 现有的明亮卡通绘本插画风,不借用任何真实品牌乐园或版权角色。 + +## 目标 + +- 远看像一个完整乐园地图,近看能分辨每个玩法入口。 +- 入口区域清晰分区,后续可以叠加焦点框、按钮和中文标题。 +- 中心和下方保留足够留白,适合遥控器焦点、儿童角色或主推荐位。 +- 风格保持和寓教于乐现有草地舞台资源一致。 + +## 本次概念方向 + +1. 环形乐园岛:中央草地广场 + 外圈入口环路。 +2. 展开绘本地图:横向展开的大绘本页,左右页自然衔接。 +3. 云朵空中岛:多个浮岛通过彩虹桥和云朵步道连接。 +4. 草地舞台地图:更接近实际运行态的横屏草地入口首屏。 + +## 推荐方向 + +优先推荐 `草地舞台地图` 作为后续落地主方向,因为它与现有寓教于乐草地舞台最接近,且中央下方留白最适合后续叠加交互焦点。 + +## 生图脚本 + +- 生成脚本:`scripts/generate-edutainment-tv-map-concepts.mjs` +- 默认尺寸:`2048x1152` +- 风格参考:`public/child-motion-demo/picture-book-grass-stage.png` +- 输出目录:`output/imagegen/edutainment-tv-map-entry-concepts-20260518/` + +## 说明 + +本次结果是设计概念稿,不直接进入 `public/` 正式资源目录。后续若要继续细化,可在同一脚本里增加新的横屏变体,并保持“不写文字、不露品牌 IP、绘本插画风”这三条底线。 diff --git a/docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md b/docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md new file mode 100644 index 00000000..a660755c --- /dev/null +++ b/docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md @@ -0,0 +1,384 @@ +# 敲木鱼玩法模板 PRD 2026-05-20 + +## 1. 目标 + +新增一个可创作、可试玩、可发布的轻量休闲玩法模板: + +```text +敲木鱼 +``` + +模板按平台新增玩法 SOP 接入完整闭环: + +```text +创作入口 -> 工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态 -> 公开详情/分享 +``` + +首版默认屏幕中央展示内置卡通透明敲击物图案 `/wooden-fish/default-hit-object.png`。玩家点击运行态非功能区时触发一次敲击:播放敲击音效、敲击物图案执行被敲击动画,并在敲击物上方随机飘出一条祝福词。顶部计数器只在某条祝福词首次出现时创建,之后该词每次出现都累加。计数仅属于当前单次 run,不进入账号长期账本。 + +## 2. 模板定位 + +模板 ID: + +```text +wooden-fish +``` + +用户展示名: + +```text +敲木鱼 +``` + +公开作品号前缀: + +```text +WF-* +``` + +体验关键词: + +1. 单屏点击; +2. 轻量解压; +3. 飘字反馈; +4. 单局累计; +5. 可自定义敲击物、敲击音效和祝福词。 + +## 3. 与拼图创作流程的复用边界 + +可以复用: + +1. 创作入口配置、入口开关和作品架; +2. 表单/图片输入工作台; +3. 生成过程页和生成中恢复; +4. 结果页的返回编辑、局部重生成、试玩、发布; +5. 公开列表、公开详情、分享码和推荐流分发; +6. 平台资产对象、OSS 私有读取换签和音频资产持久化能力。 + +不复用: + +1. 拼图关卡、棋盘、拼块、排行榜和关卡推进语义; +2. 跳一跳地块图集和蓄力判定语义; +3. 抓大鹅物品消除、五视角图集和容器语义; +4. 任何长期功德账本、账号维度排行榜或全局累计。 + +## 4. 创作工具平台接入声明 + +- 工作台模式:表单/图片输入创作工作台 +- 创作链路:入口 -> 工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态 +- 单图资产槽位: + - `slotId=hit-object` + - `slotType=hit-object-image` + - `slotName=敲击物图案` + - 提示词来源:`hitObjectPrompt` 与可选 `hitObjectReferenceImageSrc` + - 写回字段:`hitObjectAsset` + - 是否允许历史图:允许 + - 是否允许 AI 重绘:允许;上传图只作为 image2 参考,最终运行态只消费 image2 生成图 + - `slotId=background` + - `slotType=background-image` + - `slotName=背景环境图` + - 提示词来源:第一步生成的敲击物图案与用户原始题材关键词 / 参考图主题 + - 写回字段:`backgroundAsset` + - 是否允许历史图:不单独选择;由敲击物图案生成链路派生 + - 是否允许 AI 重绘:允许;随敲击物图案一起重生成 +- 系列素材槽位:无;首版只有敲击物图案与背景环境图两个单图资产,不生成图集 +- 音频资产槽位: + - `slotId=hit-sound` + - `slotType=hit-sound-audio` + - `slotName=敲击音效` + - 来源:`hitSoundPrompt` 生成,或上传/麦克风录制音频 + - 写回字段:`hitSoundAsset` + - 描述生成能力:复用通用创作音频接口 `/api/creation/audio/sound-effect` 的 VectorEngine Vidu 音效生成与 OSS 持久化链路 +- API 命名空间: + - `/api/creation/wooden-fish/...` + - `/api/runtime/wooden-fish/...` +- 业务真相: + - 后端裁决并持久化 session、work profile、发布状态、run 摘要和公开投影; + - 前端只负责点击低延迟表现、音频播放、动画、飘字渲染和定期 checkpoint。 +- 创作工具模式例外:无 +- 验证命令: + - `npm run check:encoding` + - `npm run typecheck` + - `cargo test -p shared-contracts wooden_fish --manifest-path server-rs/Cargo.toml` + - `cargo test -p module-wooden-fish --manifest-path server-rs/Cargo.toml` + - `cargo check -p api-server --manifest-path server-rs/Cargo.toml` + - `npm run spacetime:generate` + - `npm run check:spacetime-schema` + - `npm run dev:api-server` 后检查 `/healthz` + +## 5. 创作输入 + +工作台提交结构化 payload,不提交聊天消息。 + +必填字段: + +1. `templateId = "wooden-fish"`; +2. `workTitle`:作品标题; +3. `hitObjectPrompt`:用户想敲的对象关键词或描述,默认“默认敲击物图案,圆润木质质感,透明背景”; +4. `floatingWords[]`:祝福词,最多 8 条,不填或清空时使用默认祝福词。 + +可选字段: + +1. `workDescription`:作品简介; +2. `themeTags[]`:最多 6 个标签; +3. `hitObjectReferenceImageSrc`:上传或历史图引用,只能作为 image2 参考,不可直接进入运行态; +4. `hitSoundPrompt`:生成音效描述; +5. `hitSoundAsset`:用户上传或录音产生的音频资产。 + +默认祝福词: + +```text +幸运 +健康 +财富 +姻缘 +幸福 +事业 +成功 +功德 +``` + +`floatingWords[]` 保存词条名本身,不保存 `+1` 后缀;运行态每次敲击时再把飘字展示为“词条+1”。 + +## 6. 生成规则 + +### 6.1 敲击物图案与背景环境图 + +默认模板在用户未自定义关键词且未上传参考图时,`compile-draft` 使用内置透明 PNG `/wooden-fish/default-hit-object.png` 写回 `hitObjectAsset`,`generationProvider="bundled-default"`。这张图来自 image2 对原始参考图的卡通风格化重绘,固定为模板默认资源,避免默认关键词在每次生成时改变造型。即使使用内置默认敲击物,首版仍需要生成 `backgroundAsset`,背景环境图使用默认敲击物作为主题和画风参考。 + +用户输入自定义关键词、上传参考图,或在结果页主动重生成敲击物时,`compile-draft` 与 `regenerate-hit-object` 必须先为敲击物图案生成 image2 单图资产,再基于新敲击物图案生成背景环境图,并由 `api-server` 注入写回 `hitObjectAsset` 与 `backgroundAsset`。前端 action 请求不得自带 `hitObjectAsset` 或 `backgroundAsset` 短路生成。如果用户上传参考图,后端只能把该图作为 image2 参考图或主题参考;运行态不得直接使用上传图。 + +敲击物图案生成流程固定为: + +1. 调用 VectorEngine `/v1/images/edits`,模型固定为 `gpt-image-2`; +2. multipart 参考图固定包含默认木鱼图 `/wooden-fish/default-hit-object.png`,作为基础结构和画风参考; +3. 若用户上传参考图,该图只作为新主题参考追加到同一次 image2 edits 请求,不直接进入运行态; +4. 尺寸固定 `1:1`,透明底; +5. 提示词严格使用: + +```text +生成敲木鱼新样式,要求结构,画风与参考图保持高度一致,新样式颜色搭配使用新主题对应的颜色。 +新主题为:(用户提供参考图或用户输入关键词) +``` + +背景环境图生成流程固定为: + +1. 调用 VectorEngine `/v1/images/edits`,模型固定为 `gpt-image-2`; +2. multipart 参考图固定为第一步新生成的敲击物图案;默认未生成新敲击物时使用内置默认敲击物图案; +3. 尺寸固定竖屏 `9:16`; +4. 背景环境图只适配新敲击物主题和画风,背景中不得包含新敲击物本体,也不得增加木槌互动物品; +5. 提示词严格使用: + +```text +生成敲木鱼背景,要求主题,画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,木鱼预设在屏幕中央位置,木鱼主体周围元素保持干净,背景氛围围绕外围设计,背景环境图中不包含新木鱼物品,背景氛围中不增加木槌互动物品。 +主题为:(用户提供参考图或用户输入关键词) +``` + +落库链路固定为:`api-server` 调用 VectorEngine `/v1/images/edits` -> 服务端上传 OSS 私有对象 -> `confirm_asset_object` 登记资产对象 -> `bind_asset_object_to_entity` 绑定到 `entityKind='wooden_fish_work'`。敲击物绑定 `slot='hit_object'`、`assetKind='wooden_fish_hit_object'`,背景绑定 `slot='background'`、`assetKind='wooden_fish_background'`。写回时把 `legacyPublicPath` 分别写入 `hitObjectAsset.imageSrc` 与 `backgroundAsset.imageSrc`。不得只拼 `/generated-wooden-fish-assets/...` 占位路径;前端会对 generated legacy path 走 `/api/assets/read-url` 换签,OSS 中没有真实对象时图片无法显示。 + +默认图案要求: + +1. 中央主体使用 `/wooden-fish/default-hit-object.png`; +2. 透明背景; +3. 适合移动端居中展示; +4. 不包含 UI、按钮、说明文字、水印或品牌标识; +5. 图片主体需留出敲击动画缩放空间。 + +### 6.2 敲击音效 + +音效统一写回 `hitSoundAsset`。 + +生成或写回规则: + +1. 若 payload 已包含上传/录音音频资产,`compile-draft` 跳过音效生成,直接持久化该资产; +2. 若 payload 只包含 `hitSoundPrompt`,`api-server` 复用通用创作音频能力提交 VectorEngine Vidu `sound_effect` 任务,轮询生成结果,下载音频,写入 OSS 私有对象,登记 `asset_object`,并绑定到 `entityKind='wooden_fish_work'`、`slot='hit_sound'`、`assetKind='wooden_fish_hit_sound'`,最后注入 `hitSoundAsset`; +3. 若两者都没有,后端生成默认“清脆短促木鱼敲击声”; +4. 音效资产必须包含可播放地址、对象键、asset object id、来源和可选时长; +5. 通用创作音频接口对 `wooden_fish` 的 `hit_sound` 目标不得返回 `410 Gone`,对应 `storagePrefix='wooden_fish_assets'`; +6. `spacetime-client` 不得自行合成 `/generated-wooden-fish-assets/...` 音效占位路径,缺少真实 `hitSoundAsset` 时必须拒绝编译。 + +### 6.3 封面 + +首版封面使用 `hitObjectAsset.imageSrc` 作为 `coverImageSrc`。背景环境图不作为封面图,不单独新增第三次图片生成。 + +## 7. 契约草案 + +`WoodenFishDraft` 至少包含: + +1. `templateId = "wooden-fish"`; +2. `templateName = "敲木鱼"`; +3. `profileId`; +4. `workTitle`; +5. `workDescription`; +6. `themeTags[]`; +7. `hitObjectPrompt`; +8. `hitObjectReferenceImageSrc`; +9. `hitSoundPrompt`; +10. `floatingWords[]`; +11. `hitObjectAsset`; +12. `backgroundAsset`; +13. `hitSoundAsset`; +14. `coverImageSrc`; +15. `generationStatus`。 + +`WoodenFishImageAsset` 至少包含: + +1. `assetId`; +2. `imageSrc`; +3. `imageObjectKey`; +4. `assetObjectId`; +5. `generationProvider`; +6. `prompt`; +7. `width`; +8. `height`。 + +`WoodenFishAudioAsset` 至少包含: + +1. `assetId`; +2. `audioSrc`; +3. `audioObjectKey`; +4. `assetObjectId`; +5. `source = generated | uploaded | recorded | placeholder`; +6. `prompt`; +7. `durationMs`。 + +`WoodenFishRunSnapshot` 至少包含: + +1. `runId`; +2. `profileId`; +3. `ownerUserId`; +4. `status = playing | finished`; +5. `totalTapCount`; +6. `wordCounters[]`; +7. `startedAtMs`; +8. `updatedAtMs`; +9. `finishedAtMs`。 + +## 8. API 草案 + +HTTP 路由: + +```text +POST /api/creation/wooden-fish/sessions +GET /api/creation/wooden-fish/sessions/{sessionId} +POST /api/creation/wooden-fish/sessions/{sessionId}/actions +GET /api/creation/wooden-fish/works/{profileId} +POST /api/creation/wooden-fish/works/{profileId}/publish +GET /api/runtime/wooden-fish/works/{profileId} +POST /api/runtime/wooden-fish/runs +POST /api/runtime/wooden-fish/runs/{runId}/checkpoint +POST /api/runtime/wooden-fish/runs/{runId}/finish +GET /api/runtime/wooden-fish/gallery +GET /api/runtime/wooden-fish/gallery/{publicWorkCode} +``` + +动作类型: + +```text +compile-draft +regenerate-hit-object +generate-hit-sound +replace-hit-sound +update-work-meta +update-floating-words +publish +start-run +checkpoint +finish +``` + +`compile-draft` 是长耗时动作。前端进入生成页后应展示可恢复进度;如果请求失败,标记失败前必须复读 session,确认后端是否已经生成并写回草稿。 + +## 9. SpacetimeDB 表和 view + +新增表: + +1. `wooden_fish_agent_session`; +2. `wooden_fish_work_profile`,其中 `background_asset_json` 保存背景环境图资产快照; +3. `wooden_fish_runtime_run`; +4. `wooden_fish_event`。 + +新增 view: + +1. `wooden_fish_gallery_card_view`:公开列表卡片投影,只暴露已发布作品; +2. `wooden_fish_gallery_view`:公开详情兼容投影,包含图案、音效和祝福词配置。 + +新增或调整表、procedure、view 后必须同步 `migration.rs`、后端表目录、生成 bindings,并执行 `npm run check:spacetime-schema`。 + +## 10. 结果页能力 + +结果页必须展示: + +1. 作品标题和简介; +2. 竖屏背景环境图预览; +3. 敲击物图案; +4. 敲击音效试听; +5. 祝福词配置; +6. 标签; +7. 试玩; +8. 发布; +9. 返回编辑。 + +结果页必须支持: + +1. 重生成敲击物图案; +2. 生成、上传或替换敲击音效; +3. 修改标题、简介和标签; +4. 修改祝福词,最多 8 条。 + +图案重生成和音效生成是独立局部生成态,不得把已有可查看结果重新变成不可打开的全局生成中。 + +## 11. 运行态规则 + +运行态采用全屏单击模型。 + +功能区: + +1. 顶部计数器; +2. 设置、暂停、返回、发布分享等按钮; +3. 结果弹层和音频授权提示。 + +点击规则: + +1. 点击非功能区才算一次敲击; +2. 每次敲击立即本地累加 `totalTapCount`; +3. 随机等概率从 `floatingWords[]` 中取一个词条; +4. 若词条首次出现,顶部创建对应计数器; +5. 后续同词条出现时对应计数器 +1; +6. 播放敲击音效; +7. 敲击物图案执行压缩、回弹或轻微震动动画; +8. 木鱼上方飘出“词条+1”并淡出。 + +音频播放: + +1. 前端使用小复音池; +2. 设置最小播放间隔,避免极端连点导致浏览器抖动; +3. 点击计数不能因为音频节流而丢失; +4. 签名 URL 未就绪时先静音表现,不请求裸 generated 私有路径。 + +后端只保存 run 摘要,不保存每次点击的完整明细;`checkpoint` 和 `finish` 都写入总敲击次数与词条计数快照。 + +## 12. 公开链路 + +平台首页推荐、发现、公开详情、搜索、已玩作品和公开试玩统一按 `sourceType='wooden-fish'` 与 `WF-*` 公开作品号识别敲木鱼作品。 + +公开列表优先消费 `wooden_fish_gallery_card_view` 订阅缓存。公开详情如果卡片摘要不足以进入运行态,必须补读完整 work profile。 + +## 13. 验收 + +1. 创作入口能看到 `敲木鱼` 模板; +2. 工作台可以填写敲击物描述、上传参考图、配置音效和祝福词; +3. 提交后按默认木鱼参考图生成 image2 敲击物图案; +4. 提交后按新敲击物图案参考图生成 9:16 背景环境图; +5. 上传图不会直接进入运行态; +6. 用户上传或录制音效时跳过音效生成并持久化该资产; +7. 结果页能看到背景、图案、试听音效、编辑祝福词并试玩; +8. 运行态功能区点击不触发敲击; +9. 非功能区点击会计数、播放音效、播放敲击动画并飘字; +10. 顶部计数器只在词条首次出现时创建; +11. 连点不丢计数; +12. `checkpoint` 和 `finish` 只保存单次 run 摘要; +13. 作品可以发布、进入公开列表和公开详情; +14. `WF-*` 公开作品号能进入分享和运行态; +15. `npm run check:encoding` 通过; +16. schema 变更后 `npm run check:spacetime-schema` 通过。 diff --git a/docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md b/docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md index 7dc401b6..9c15c72e 100644 --- a/docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md +++ b/docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md @@ -49,6 +49,8 @@ 热身结束后展示“开始游戏”按钮,用户点击后进入宝贝识物首关本地 Demo。该入口只用于热身关后的本地体验验证;正式平台体验仍必须通过“宝贝识物”创作模板发布后,在寓教于乐板块进入。 +发现页的寓教于乐频道同时提供独立热身关入口,用户可直接进入 `/child-motion-demo`。 + ### 3.3 固定流程顺序 热身关必须按照以下顺序执行: diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index afef15c4..a0cb47fe 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -44,7 +44,7 @@ npm run check:server-rs-ddd `server-rs/crates/spacetime-client/src/mapper.rs` 只作为聚合入口,负责声明 `src/mapper/` 下的领域子模块并 re-export 原有 record / mapper 能力;不要在该文件继续堆叠大段映射实现。 -当前子模块按调用领域拆分:`assets.rs`、`auth.rs`、`runtime.rs`、`runtime_profile.rs`、`custom_world.rs`、`puzzle.rs`、`match3d.rs`、`square_hole.rs`、`visual_novel.rs`、`big_fish.rs`、`story.rs`、`ai.rs`、`bark_battle.rs`、`combat.rs`、`inventory.rs`、`npc.rs`,跨领域轻量 helper 和共享 record 统一放在 `common.rs`。该拆分只改变 `spacetime-client` 文件组织,不改变 SpacetimeDB schema、生成绑定、procedure result 契约或外部 DTO;后续新增 mapper 时优先落到对应领域子模块,不得重新引入跨层 JSON 字符串兼容结构。 +当前子模块按调用领域拆分:`assets.rs`、`auth.rs`、`runtime.rs`、`runtime_profile.rs`、`custom_world.rs`、`puzzle.rs`、`match3d.rs`、`jump_hop.rs`、`wooden_fish.rs`、`square_hole.rs`、`visual_novel.rs`、`big_fish.rs`、`story.rs`、`ai.rs`、`bark_battle.rs`、`combat.rs`、`inventory.rs`、`npc.rs`,跨领域轻量 helper 和共享 record 统一放在 `common.rs`。该拆分只改变 `spacetime-client` 文件组织,不改变 SpacetimeDB schema、生成绑定、procedure result 契约或外部 DTO;后续新增 mapper 时优先落到对应领域子模块,不得重新引入跨层 JSON 字符串兼容结构。 ## API 路由分组 @@ -60,6 +60,7 @@ npm run check:server-rs-ddd - 自定义世界 / RPG:`/api/runtime/custom-world*`、`/api/story/*`、`/api/runtime/chat/*`。 - 拼图:`/api/runtime/puzzle/*`。 - 抓大鹅 Match3D:`/api/creation/match3d/*`、`/api/runtime/match3d/*`。 +- 敲木鱼:`/api/creation/wooden-fish/*`、`/api/runtime/wooden-fish/*`。 - 方洞挑战:`/api/creation/square-hole/*`、`/api/runtime/square-hole/*`。 - 视觉小说:`/api/creation/visual-novel/*`、`/api/runtime/visual-novel/*`。 - 大鱼吃小鱼:`/api/runtime/big-fish/*`。 @@ -158,8 +159,10 @@ npm run check:server-rs-ddd - 图片生成:VectorEngine / APIMart / DashScope,密钥只在后端环境变量中。 - Match3D 物品 sheet:关卡整图完成后走 VectorEngine `/v1/images/edits` multipart `image`,模型为 `gpt-image-2`,`2K 1:1` 输出 `10*10` spritesheet;物品 sheet prompt 固定要求纯绿色绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG,并把透明整图写入 `itemSpritesheetImageSrc/itemSpritesheetImageObjectKey`。后端固定从该 sheet 解析并持久化 20 个物品、每个 5 个形态;通用系列素材图集的行列索引按每行 2 个物品计算,必须落在 `1..=10`,难度只决定运行态加载 3 / 9 / 15 / 20 种。 - Match3D UI spritesheet 和背景派生图:关卡整图作为参考图并发生成 `1K 1:1` UI spritesheet 与 `1K 9:16` 背景图,模型均为 `gpt-image-2`。UI spritesheet prompt 固定要求纯绿色绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG;背景图必须合成为全画幅不透明 PNG。 +- Match3D 1:1 容器 UI:VectorEngine `/v1/images/edits` multipart 参考图。该容器参考图是后端生图协议输入,必须通过 `include_bytes!` 随 `api-server` 编译进二进制,避免 API 单独发布或运行目录缺少 `public/` 时生成失败。 +- 敲木鱼敲击物和背景环境图:VectorEngine `/v1/images/edits`,模型固定 `gpt-image-2`。敲击物支持 multipart 多参考图,第一张固定为后端内嵌默认木鱼图,用户上传图只作为新主题参考;背景环境图只使用新敲击物图作为参考。 - Hyper3D / Rodin:只保留后端安全代理和旧数据兼容;新 Match3D 草稿和批量新增不再生成 GLB。 -- 音频:视觉小说专用音频路由保留;拼图和抓大鹅生成入口暂时关闭,通用 `/api/creation/audio/*` 对相关目标返回 `410 Gone`。 +- 音频:视觉小说专用音频路由保留;拼图和抓大鹅生成入口暂时关闭,通用 `/api/creation/audio/*` 对相关目标返回 `410 Gone`;敲木鱼 `hit_sound` 目标例外开放,复用 VectorEngine Vidu 音效生成、OSS 私有对象、`asset_object` 和 entity binding 链路,目标字段固定为 `entityKind='wooden_fish_work'`、`slot='hit_sound'`、`assetKind='wooden_fish_hit_sound'`、`storagePrefix='wooden_fish_assets'`。 - OSS:私有 generated legacy path 进入浏览器前必须通过 `/api/assets/read-url` 换签;不要裸请求 `/generated-*`。 - 外部 API 失败审计:外部供应商调用未成功时,`api-server` 必须发送 OTLP 失败事件并写入 `tracking_event`。当前通用 VectorEngine `gpt-image-2-all` 图片生成 / 编辑适配器在 `request_send`、`response_body`、`upstream_status`、`response_parse`、`missing_image` 和 `image_download` 阶段失败时记录 `external_api_call_failure`,`scope_kind = module`、`scope_id = provider`、`module_key = external-api`;metadata 固定包含 provider、endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、latencyMs、promptChars、referenceImageCount 和 imageModel。入库优先复用 tracking outbox,outbox 不可写或保护阈值拒绝时回退同步写 SpacetimeDB;不得新增前端兜底或在 SpacetimeDB reducer 内做外部 I/O。 @@ -401,6 +404,41 @@ npm run check:server-rs-ddd - 源码:`server-rs/crates/spacetime-module/src/jump_hop.rs` - 说明:跳一跳公开详情兼容投影,包含作品、路径和素材字段;公开列表主路径优先使用 `jump_hop_gallery_card_view`。 +### `wooden_fish_agent_session` + +- Rust 结构体:`WoodenFishAgentSessionRow` +- 源码:`server-rs/crates/spacetime-module/src/wooden_fish/tables.rs` + +### `wooden_fish_event` + +- Rust 结构体:`WoodenFishEventRow` +- 源码:`server-rs/crates/spacetime-module/src/wooden_fish/tables.rs` + +### `wooden_fish_runtime_run` + +- Rust 结构体:`WoodenFishRuntimeRunRow` +- 源码:`server-rs/crates/spacetime-module/src/wooden_fish/tables.rs` + +### `wooden_fish_work_profile` + +- Rust 结构体:`WoodenFishWorkProfileRow` +- 源码:`server-rs/crates/spacetime-module/src/wooden_fish/tables.rs` +- 说明:敲木鱼作品 profile 真相,包含敲击物图案、背景环境图、敲击音效、飘字配置、发布状态和公开计数;`background_asset_json` 是后加入字段,保存 image2 生成的 9:16 背景环境图资产快照,旧迁移数据按 `None` 兼容。 + +### SpacetimeDB view:`wooden_fish_gallery_card_view` + +- Rust view:`wooden_fish_gallery_card_view` +- 返回类型:`Vec` +- 源码:`server-rs/crates/spacetime-module/src/wooden_fish.rs` +- 说明:敲木鱼公开广场列表卡片投影,只暴露 `publication_status = published` 的作品卡片字段;`api-server` 的 `spacetime-client` 长期订阅 `SELECT * FROM wooden_fish_gallery_card_view` 后,从本地 cache 构造敲木鱼公开列表响应。个人作品列表、详情、发布和运行态仍按 procedure 路径处理。 + +### SpacetimeDB view:`wooden_fish_gallery_view` + +- Rust view:`wooden_fish_gallery_view` +- 返回类型:`Vec` +- 源码:`server-rs/crates/spacetime-module/src/wooden_fish.rs` +- 说明:敲木鱼公开详情兼容投影,包含敲击物图案、敲击音效和飘字配置;公开列表主路径优先使用 `wooden_fish_gallery_card_view`。 + ### `match3d_agent_message` - Rust 结构体:`Match3DAgentMessageRow` diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index da405454..21bf0711 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -45,6 +45,10 @@ npm run dev:api-server 后端日志默认写入 `logs/api-server/`。后端 API smoke 使用 `npm run dev:api-server` 并检查 `/healthz`;不要使用旧 `api-server:maincloud` 或任何 `GENARRATIVE_SPACETIME_MAINCLOUD_*` 口径。 +如果本地 `GET /api/creation-entry/config` 返回 `No such procedure`,通常是 `.env.local` 指向的 SpacetimeDB 库还没有发布当前 `spacetime-module`,或当前 CLI 身份无权发布该库。debug 构建的 `api-server` 会临时使用后端默认入口配置兜底,避免创作作品架整块消失;正式修复仍应切换到拥有目标库权限的 SpacetimeDB 身份后重新运行 `npm run dev` 完成发布,或用 gitignored 的 `spacetime.local.json` 指向可发布的本地库。 + +本地 `npm run dev:spacetime` 发布模块时必须显式忽略仓库根目录的 `spacetime.json`,由脚本固定追加 `--no-config` 并使用命令参数里传入的数据库名和 `--server http://127.0.0.1:3101`。否则 CLI 可能把发布目标改写到配置文件里的其他数据库,导致 `dev:spacetime` 启动后又因发布失败自动退出,浏览器随后会在 `ws://127.0.0.1:3101/v1/database/.../subscribe` 看到连接拒绝。 + 本地 `.env`、`.env.local` 或 `.env.secrets.local` 修改后必须重启 `api-server` 才会生效;若已经通过 `npm run dev` 启动完整联调,可在该终端输入 `rs api-server`。排查 RPG / 拼图 / 抓大鹅等 VectorEngine 生图链路时,确认 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 只在本地或服务器密钥文件中配置,不能写入 Git。开局 CG 故事板、首图、背景和图集都属于长耗时图片请求;后端默认会把 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 下限收口到 `1000000`,旧进程仍可能沿用重启前的短超时。若开局 CG 故事板在 `send()` 阶段失败且日志显示 `SendRequest`,先看同一 request_id 的 `request_body_bytes`、`reference_data_url_bytes`、`sourceChain` 和 `rootSource`;当前开局 CG 会把角色图与首幕背景图压到单边 768 的 JPEG 后再作为 generations `image` 数组发送,`/v1/images/generations` 使用默认 HTTP 协商,只有 multipart `/v1/images/edits` 单独强制 HTTP/1.1。 查看本地 Rust / SpacetimeDB 日志: @@ -167,6 +171,7 @@ UI 相关修改要重点验证: 4. 身份问题先查 `spacetime login show`、`spacetime server list` 和目标库权限,不通过切回旧 Node / PostgreSQL 绕过。 5. 旧库迁移或 private 表数据保留走 `migration.rs` 的 JSON 导入导出和分片导入思路。 6. Jenkins 数据库导入 / 导出流水线会先加载 `scripts/jenkins-prepare-toolchain-env.sh`,显式补齐 Jenkins 用户的 Node、Cargo、SpacetimeDB 工具链目录;如果目标机器安装路径不同,用 `GENARRATIVE_JENKINS_TOOL_PATHS` 传入额外 `bin` 目录。 +7. 本地 `npm run dev` / `npm run dev:api-server` 若没有显式 `GENARRATIVE_SPACETIME_TOKEN`,会在 SpacetimeDB 就绪后调用 `/v1/identity` 创建当前进程专用 Web API identity token,并只注入本次 `api-server` 环境,不写回 `.env.local`。启动日志只打印 identity 前缀,禁止打印 token 明文;若仍出现 `subscribe ... 401 Unauthorized`,先确认是否绕过了项目 dev 脚本或是否连接到非本次启动的 SpacetimeDB server。 ## 生产运维 diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index b814c500..dec72bac 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -120,6 +120,28 @@ RPG / 拼图等运行态存档选择入口统一在个人中心 `常用功能 > 平台首页推荐、精选、最新、公开详情、搜索、已玩作品和公开试玩统一按 `sourceType='jump-hop'` 与 `JH-*` 公开作品号识别跳一跳作品;从公开详情或推荐流启动运行态时,若卡片摘要不足以携带角色图、地块图集和路径配置,必须先补读完整 work profile 再传入运行态。平台壳层必须同步注册 `jump-hop-workspace`、`jump-hop-generating`、`jump-hop-result`、`jump-hop-runtime`、`jump-hop-gallery-detail` 阶段,并在 `appPageRoutes.ts` 映射 `/creation/jump-hop/workspace`、`/creation/jump-hop/generating`、`/creation/jump-hop/result`、`/gallery/jump-hop/detail`、`/runtime/jump-hop`,同时持有 session、work、run、gallery、busy/error 与生成进度状态,避免只合入渲染分支但遗漏状态源或分享路径导致 typecheck 失败、刷新回首页。 +## 敲木鱼 + +对外名称:`敲木鱼`。工程域:`wooden-fish`。PRD 见 `docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`。 + +首版定位为单屏点击解压模板,链路对齐拼图的创作闭环: + +```text +创作入口 -> 工作台 -> 生成过程页 -> 结果页 -> 试玩 -> 发布 -> 运行态 +``` + +创作输入固定为: + +1. `敲什么`:敲击物单图资产槽位。默认模板使用内置透明 PNG `/wooden-fish/default-hit-object.png` 作为 `bundled-default` 敲击物资产,避免默认关键词被重新语义化改形;用户输入自定义关键词或上传参考图时,后端必须以默认木鱼图作为基础结构和画风参考,使用 image2 生成最终敲击物图案,上传图只作为新主题参考,不直接进入运行态。自定义 `compile-draft` / `regenerate-hit-object` 必须完成 image2 -> OSS 私有对象 -> asset object 登记和绑定后,再由 `api-server` 注入真实 `hitObjectAsset.imageSrc`,不能只写 `/generated-wooden-fish-assets/...` 占位路径,也不能接受前端请求自带的 `hitObjectAsset` 短路生成。 +2. `敲击音效`:音频资产槽位,支持描述生成、上传和麦克风录制,统一写回 `hitSoundAsset`。描述生成复用通用 `/api/creation/audio/sound-effect` 的 VectorEngine Vidu 音效生成、下载、OSS 私有对象、asset object 登记和 entity binding 链路;木鱼目标固定为 `entityKind='wooden_fish_work'`、`slot='hit_sound'`、`assetKind='wooden_fish_hit_sound'`、`storagePrefix='wooden_fish_assets'`,不得再返回 `410 Gone`,也不得由 `spacetime-client` 合成假音频路径。 +3. `功德有什么`:最多 8 条飘字,默认 `幸运、健康、财富、姻缘、幸福、事业、成功、功德`;创作态只保存词条名,运行态飘字展示时再追加 `+1`。 + +图片生成链路固定为双图 image2 流程:第一步用默认木鱼图作为结构和画风参考,按用户题材关键词或参考图主题生成 `1:1` 透明底新敲击物;第二步用新敲击物作为主题和画风参考生成 `9:16` 背景环境图,背景图只适配主题和画风,不能包含新敲击物本体,也不能增加木槌互动物品。两个资产分别写回 `hitObjectAsset` 与 `backgroundAsset`,并绑定到 `wooden_fish_work` 的 `hit_object` / `background` 槽位。运行态和结果页消费 `backgroundAsset` 做竖屏背景,中央再叠加 `hitObjectAsset`。 + +运行态规则真相以后端 run 摘要为准,前端只做点击低延迟表现、敲击动画、音频播放和飘字渲染。每次非功能区点击在当前 run 内累计 `totalTapCount` 和 `wordCounters`;计数不进入账号长期账本,不做排行榜。顶部计数器仅在词条首次出现时创建,后续同词条继续累加。 + +平台首页推荐、精选、最新、公开详情、搜索、已玩作品和公开试玩统一按 `sourceType='wooden-fish'` 与 `WF-*` 公开作品号识别敲木鱼作品;公开列表应走 `wooden_fish_gallery_card_view` 订阅缓存,公开详情或运行态启动时卡片摘要不足则补读完整 work profile。 + ## 抓大鹅 Match3D 对外名称:`抓大鹅`。工程域:`match3d`。 @@ -249,9 +271,9 @@ RPG / 拼图等运行态存档选择入口统一在个人中心 `常用功能 > 当前包含: -- `baby-object-match`:宝贝识物当前入口状态为 `visible=true`、`open=false`,创作 Tab 展示为“敬请期待”,不进入创作与发布链路;历史资产接口为 `/api/creation/edutainment/baby-object-match/assets`,后续重新开放时继续复用该资产链路。 +- `baby-object-match`:宝贝识物当前入口状态为 `visible=true`、`open=true`,创作 Tab 展示为“可创建”,进入宝贝识物创作、生成、结果页、试玩和发布链路;资产生成接口为 `/api/creation/edutainment/baby-object-match/assets`。入口关闭只允许通过 SpacetimeDB / 后台入口配置显式调整,默认种子和 debug 兜底都必须保持可创建。 - `baby-love-drawing`:宝贝爱画本地 demo,魔法生成接口为 `/api/creation/edutainment/baby-love-drawing/magic`。 -- `child-motion-demo`:儿童动作识别热身关。真实动作数据来自 mocap WebSocket,不要把浏览器摄像头视频流当作主动作数据源。 +- `child-motion-demo`:儿童动作识别热身关。真实动作数据来自 mocap WebSocket,不要把浏览器摄像头视频流当作主动作数据源;发现页的寓教于乐频道同时提供独立热身关入口,点击后进入 `/child-motion-demo`。 ## 创意互动 Agent diff --git a/packages/shared/src/contracts/creationAudio.ts b/packages/shared/src/contracts/creationAudio.ts index e1ad6cc3..5b820f56 100644 --- a/packages/shared/src/contracts/creationAudio.ts +++ b/packages/shared/src/contracts/creationAudio.ts @@ -39,7 +39,12 @@ export interface PublishGeneratedAudioAssetRequest { slot: string; assetKind: string; profileId?: string | null; - storagePrefix?: 'puzzle_assets' | 'match3d_assets' | 'custom_world_scenes' | null; + storagePrefix?: + | 'puzzle_assets' + | 'match3d_assets' + | 'wooden_fish_assets' + | 'custom_world_scenes' + | null; } export interface GeneratedAudioAssetResponse { diff --git a/packages/shared/src/contracts/index.ts b/packages/shared/src/contracts/index.ts index 8f8b20e8..49f37d51 100644 --- a/packages/shared/src/contracts/index.ts +++ b/packages/shared/src/contracts/index.ts @@ -5,3 +5,4 @@ export type * from './jumpHop'; export type * from './puzzleCreativeTemplate'; export type * from './visualNovel'; export type * from './barkBattle'; +export type * from './woodenFish'; diff --git a/packages/shared/src/contracts/woodenFish.ts b/packages/shared/src/contracts/woodenFish.ts new file mode 100644 index 00000000..ae035f8f --- /dev/null +++ b/packages/shared/src/contracts/woodenFish.ts @@ -0,0 +1,199 @@ +export type WoodenFishGenerationStatus = + | 'draft' + | 'generating' + | 'ready' + | 'failed'; + +export type WoodenFishActionType = + | 'compile-draft' + | 'regenerate-hit-object' + | 'generate-hit-sound' + | 'replace-hit-sound' + | 'update-work-meta' + | 'update-floating-words'; + +export type WoodenFishRunStatus = 'playing' | 'finished'; + +export interface WoodenFishImageAsset { + assetId: string; + imageSrc: string; + imageObjectKey: string; + assetObjectId: string; + generationProvider: string; + prompt: string; + width: number; + height: number; +} + +export interface WoodenFishAudioAsset { + assetId: string; + audioSrc: string; + audioObjectKey: string; + assetObjectId: string; + source: string; + prompt?: string | null; + durationMs?: number | null; +} + +export interface WoodenFishWorkspaceCreateRequest { + templateId: string; + workTitle: string; + workDescription: string; + themeTags: string[]; + hitObjectPrompt: string; + hitObjectReferenceImageSrc?: string | null; + hitSoundPrompt?: string | null; + hitSoundAsset?: WoodenFishAudioAsset | null; + floatingWords: string[]; +} + +export interface WoodenFishActionRequest { + actionType: WoodenFishActionType; + profileId?: string | null; + workTitle?: string | null; + workDescription?: string | null; + themeTags?: string[] | null; + hitObjectPrompt?: string | null; + hitObjectReferenceImageSrc?: string | null; + hitObjectAsset?: WoodenFishImageAsset | null; + backgroundAsset?: WoodenFishImageAsset | null; + hitSoundPrompt?: string | null; + hitSoundAsset?: WoodenFishAudioAsset | null; + floatingWords?: string[] | null; +} + +export interface WoodenFishWordCounter { + text: string; + count: number; +} + +export interface WoodenFishDraftResponse { + templateId: string; + templateName: string; + profileId: string | null; + workTitle: string; + workDescription: string; + themeTags: string[]; + hitObjectPrompt: string; + hitObjectReferenceImageSrc: string | null; + hitSoundPrompt: string | null; + floatingWords: string[]; + hitObjectAsset: WoodenFishImageAsset | null; + backgroundAsset: WoodenFishImageAsset | null; + hitSoundAsset: WoodenFishAudioAsset | null; + coverImageSrc: string | null; + generationStatus: WoodenFishGenerationStatus; +} + +export interface WoodenFishSessionSnapshotResponse { + sessionId: string; + ownerUserId: string; + status: WoodenFishGenerationStatus; + draft: WoodenFishDraftResponse | null; + createdAt: string; + updatedAt: string; +} + +export interface WoodenFishSessionResponse { + session: WoodenFishSessionSnapshotResponse; +} + +export interface WoodenFishActionResponse { + actionType: WoodenFishActionType; + session: WoodenFishSessionSnapshotResponse; + work: WoodenFishWorkProfileResponse | null; +} + +export interface WoodenFishWorkSummaryResponse { + runtimeKind: 'wooden-fish'; + workId: string; + profileId: string; + ownerUserId: string; + sourceSessionId: string | null; + workTitle: string; + workDescription: string; + themeTags: string[]; + coverImageSrc: string | null; + publicationStatus: string; + playCount: number; + updatedAt: string; + publishedAt: string | null; + publishReady: boolean; + generationStatus: WoodenFishGenerationStatus; +} + +export interface WoodenFishWorkProfileResponse { + summary: WoodenFishWorkSummaryResponse; + draft: WoodenFishDraftResponse; + hitObjectAsset: WoodenFishImageAsset; + backgroundAsset: WoodenFishImageAsset | null; + hitSoundAsset: WoodenFishAudioAsset; + floatingWords: string[]; +} + +export interface WoodenFishWorkDetailResponse { + item: WoodenFishWorkProfileResponse; +} + +export interface WoodenFishWorkMutationResponse { + item: WoodenFishWorkProfileResponse; +} + +export interface WoodenFishGalleryCardResponse { + publicWorkCode: string; + workId: string; + profileId: string; + ownerUserId: string; + authorDisplayName: string; + workTitle: string; + workDescription: string; + coverImageSrc: string | null; + themeTags: string[]; + publicationStatus: string; + playCount: number; + updatedAt: string; + publishedAt: string | null; + generationStatus: WoodenFishGenerationStatus; +} + +export interface WoodenFishGalleryResponse { + items: WoodenFishGalleryCardResponse[]; + hasMore: boolean; + nextCursor: string | null; +} + +export interface WoodenFishGalleryDetailResponse { + item: WoodenFishWorkProfileResponse; +} + +export interface WoodenFishRuntimeRunSnapshotResponse { + runId: string; + profileId: string; + ownerUserId: string; + status: WoodenFishRunStatus; + totalTapCount: number; + wordCounters: WoodenFishWordCounter[]; + startedAtMs: number; + updatedAtMs: number; + finishedAtMs: number | null; +} + +export interface WoodenFishRunResponse { + run: WoodenFishRuntimeRunSnapshotResponse; +} + +export interface WoodenFishStartRunRequest { + profileId: string; +} + +export interface WoodenFishCheckpointRunRequest { + totalTapCount: number; + wordCounters: WoodenFishWordCounter[]; + clientEventId: string; +} + +export interface WoodenFishFinishRunRequest { + totalTapCount: number; + wordCounters: WoodenFishWordCounter[]; + clientEventId: string; +} diff --git a/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-clay-bean.png b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-clay-bean.png new file mode 100644 index 00000000..e329f426 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-clay-bean.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-clay-bean.svg b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-clay-bean.svg new file mode 100644 index 00000000..d34d831e --- /dev/null +++ b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-clay-bean.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-clay-seed.png b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-clay-seed.png new file mode 100644 index 00000000..b204cdcb Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-clay-seed.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-clay-seed.svg b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-clay-seed.svg new file mode 100644 index 00000000..83afaa50 --- /dev/null +++ b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-clay-seed.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-dot-face.png b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-dot-face.png new file mode 100644 index 00000000..8c2a2c71 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-dot-face.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-dot-face.svg b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-dot-face.svg new file mode 100644 index 00000000..28dfbae7 --- /dev/null +++ b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-dot-face.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-mold-baby.png b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-mold-baby.png new file mode 100644 index 00000000..232354cd Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-mold-baby.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-mold-baby.svg b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-mold-baby.svg new file mode 100644 index 00000000..8e8663cc --- /dev/null +++ b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-mold-baby.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-soft-totem.png b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-soft-totem.png new file mode 100644 index 00000000..fa6b72cd Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-soft-totem.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-soft-totem.svg b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-soft-totem.svg new file mode 100644 index 00000000..12507cfc --- /dev/null +++ b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-soft-totem.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-work-puppet.png b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-work-puppet.png new file mode 100644 index 00000000..e066e780 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-work-puppet.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-work-puppet.svg b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-work-puppet.svg new file mode 100644 index 00000000..cdbce26f --- /dev/null +++ b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-abstract-mascot-work-puppet.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/branding/taonier-logo-abstract-mascot-concepts/taonier-logo-abstract-mascot-contact-sheet.png b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-logo-abstract-mascot-contact-sheet.png new file mode 100644 index 00000000..ed90840b Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-concepts/taonier-logo-abstract-mascot-contact-sheet.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-clay-pocket-token.png b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-clay-pocket-token.png new file mode 100644 index 00000000..8d5c2341 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-clay-pocket-token.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-clay-spirit-glyph.png b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-clay-spirit-glyph.png new file mode 100644 index 00000000..cc632ad9 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-clay-spirit-glyph.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-mold-blob-companion.png b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-mold-blob-companion.png new file mode 100644 index 00000000..c94a4883 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-mold-blob-companion.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-pinched-seed-mascot.png b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-pinched-seed-mascot.png new file mode 100644 index 00000000..aa532f78 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-pinched-seed-mascot.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-soft-totem-creature.png b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-soft-totem-creature.png new file mode 100644 index 00000000..eff2d606 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-soft-totem-creature.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-work-core-puppet.png b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-work-core-puppet.png new file mode 100644 index 00000000..15c01392 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-image2-work-core-puppet.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-logo-abstract-mascot-image2-contact-sheet.png b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-logo-abstract-mascot-image2-contact-sheet.png new file mode 100644 index 00000000..fd7e6242 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-image2-concepts/taonier-logo-abstract-mascot-image2-contact-sheet.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-logo-abstract-mascot-minimal-contact-sheet.png b/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-logo-abstract-mascot-minimal-contact-sheet.png new file mode 100644 index 00000000..64265b7b Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-logo-abstract-mascot-minimal-contact-sheet.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-minimal-clay-core.png b/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-minimal-clay-core.png new file mode 100644 index 00000000..ebdb8271 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-minimal-clay-core.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-minimal-clay-token.png b/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-minimal-clay-token.png new file mode 100644 index 00000000..ec9e5b81 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-minimal-clay-token.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-minimal-mold-bud.png b/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-minimal-mold-bud.png new file mode 100644 index 00000000..c4821821 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-minimal-mold-bud.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-minimal-seed-glyph.png b/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-minimal-seed-glyph.png new file mode 100644 index 00000000..02382e2a Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-minimal-concepts/taonier-minimal-seed-glyph.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-clay-orb.png b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-clay-orb.png new file mode 100644 index 00000000..d7f955b9 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-clay-orb.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-clay-orb.svg b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-clay-orb.svg new file mode 100644 index 00000000..e750a4f6 --- /dev/null +++ b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-clay-orb.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-clay-sprite.png b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-clay-sprite.png new file mode 100644 index 00000000..d30d257f Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-clay-sprite.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-clay-sprite.svg b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-clay-sprite.svg new file mode 100644 index 00000000..7cccc846 --- /dev/null +++ b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-clay-sprite.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-pinch-orbit.png b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-pinch-orbit.png new file mode 100644 index 00000000..2ebd29f3 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-pinch-orbit.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-pinch-orbit.svg b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-pinch-orbit.svg new file mode 100644 index 00000000..1868c073 --- /dev/null +++ b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-pinch-orbit.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-seed-totem.png b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-seed-totem.png new file mode 100644 index 00000000..64130ad6 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-seed-totem.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-seed-totem.svg b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-seed-totem.svg new file mode 100644 index 00000000..2c2a9848 --- /dev/null +++ b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-seed-totem.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-soft-mold.png b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-soft-mold.png new file mode 100644 index 00000000..de237d1b Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-soft-mold.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-soft-mold.svg b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-soft-mold.svg new file mode 100644 index 00000000..b5f0bc73 --- /dev/null +++ b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-soft-mold.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-work-glyph.png b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-work-glyph.png new file mode 100644 index 00000000..18ee0410 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-work-glyph.png differ diff --git a/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-work-glyph.svg b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-work-glyph.svg new file mode 100644 index 00000000..b403b89f --- /dev/null +++ b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-abstract-mascot-v2-work-glyph.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-logo-abstract-mascot-v2-contact-sheet.png b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-logo-abstract-mascot-v2-contact-sheet.png new file mode 100644 index 00000000..aef75e68 Binary files /dev/null and b/public/branding/taonier-logo-abstract-mascot-v2-concepts/taonier-logo-abstract-mascot-v2-contact-sheet.png differ diff --git a/public/branding/taonier-logo-anchor-concepts/taonier-anchor-app-token.png b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-app-token.png new file mode 100644 index 00000000..189c193d Binary files /dev/null and b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-app-token.png differ diff --git a/public/branding/taonier-logo-anchor-concepts/taonier-anchor-app-token.svg b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-app-token.svg new file mode 100644 index 00000000..60c5380f --- /dev/null +++ b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-app-token.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/branding/taonier-logo-anchor-concepts/taonier-anchor-clay-drop.png b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-clay-drop.png new file mode 100644 index 00000000..75427ba3 Binary files /dev/null and b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-clay-drop.png differ diff --git a/public/branding/taonier-logo-anchor-concepts/taonier-anchor-clay-drop.svg b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-clay-drop.svg new file mode 100644 index 00000000..85836184 --- /dev/null +++ b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-clay-drop.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/branding/taonier-logo-anchor-concepts/taonier-anchor-core.png b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-core.png new file mode 100644 index 00000000..221ab801 Binary files /dev/null and b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-core.png differ diff --git a/public/branding/taonier-logo-anchor-concepts/taonier-anchor-core.svg b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-core.svg new file mode 100644 index 00000000..fa315c43 --- /dev/null +++ b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-core.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/branding/taonier-logo-anchor-concepts/taonier-anchor-creation-base.png b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-creation-base.png new file mode 100644 index 00000000..f6455307 Binary files /dev/null and b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-creation-base.png differ diff --git a/public/branding/taonier-logo-anchor-concepts/taonier-anchor-creation-base.svg b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-creation-base.svg new file mode 100644 index 00000000..2a526a39 --- /dev/null +++ b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-creation-base.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/branding/taonier-logo-anchor-concepts/taonier-anchor-soft-slab.png b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-soft-slab.png new file mode 100644 index 00000000..b95f6d77 Binary files /dev/null and b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-soft-slab.png differ diff --git a/public/branding/taonier-logo-anchor-concepts/taonier-anchor-soft-slab.svg b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-soft-slab.svg new file mode 100644 index 00000000..945b0e32 --- /dev/null +++ b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-soft-slab.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/branding/taonier-logo-anchor-concepts/taonier-anchor-work-stack.png b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-work-stack.png new file mode 100644 index 00000000..799de34c Binary files /dev/null and b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-work-stack.png differ diff --git a/public/branding/taonier-logo-anchor-concepts/taonier-anchor-work-stack.svg b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-work-stack.svg new file mode 100644 index 00000000..78fde2f0 --- /dev/null +++ b/public/branding/taonier-logo-anchor-concepts/taonier-anchor-work-stack.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/branding/taonier-logo-anchor-concepts/taonier-logo-anchor-contact-sheet.png b/public/branding/taonier-logo-anchor-concepts/taonier-logo-anchor-contact-sheet.png new file mode 100644 index 00000000..1a9f0f77 Binary files /dev/null and b/public/branding/taonier-logo-anchor-concepts/taonier-logo-anchor-contact-sheet.png differ diff --git a/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-01-matte-clay-stamp.png b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-01-matte-clay-stamp.png new file mode 100644 index 00000000..0209b888 Binary files /dev/null and b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-01-matte-clay-stamp.png differ diff --git a/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-02-kiln-mark-core.png b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-02-kiln-mark-core.png new file mode 100644 index 00000000..c7030159 Binary files /dev/null and b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-02-kiln-mark-core.png differ diff --git a/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-03-cutout-negative-star.png b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-03-cutout-negative-star.png new file mode 100644 index 00000000..c0e800de Binary files /dev/null and b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-03-cutout-negative-star.png differ diff --git a/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-04-dry-clay-grain.png b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-04-dry-clay-grain.png new file mode 100644 index 00000000..c98ccf3a Binary files /dev/null and b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-04-dry-clay-grain.png differ diff --git a/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-05-hand-pressed-token.png b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-05-hand-pressed-token.png new file mode 100644 index 00000000..afffd04d Binary files /dev/null and b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-05-hand-pressed-token.png differ diff --git a/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-06-digital-clay-glyph.png b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-06-digital-clay-glyph.png new file mode 100644 index 00000000..47c8e465 Binary files /dev/null and b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-06-digital-clay-glyph.png differ diff --git a/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-07-premium-flat-mark.png b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-07-premium-flat-mark.png new file mode 100644 index 00000000..39db1d7e Binary files /dev/null and b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-07-premium-flat-mark.png differ diff --git a/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-08-monochrome-proof.png b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-08-monochrome-proof.png new file mode 100644 index 00000000..285a1828 Binary files /dev/null and b/public/branding/taonier-logo-anti-candy-concepts/taonier-anti-candy-08-monochrome-proof.png differ diff --git a/public/branding/taonier-logo-anti-candy-concepts/taonier-logo-anti-candy-contact-sheet.png b/public/branding/taonier-logo-anti-candy-concepts/taonier-logo-anti-candy-contact-sheet.png new file mode 100644 index 00000000..fa9cfd8f Binary files /dev/null and b/public/branding/taonier-logo-anti-candy-concepts/taonier-logo-anti-candy-contact-sheet.png differ diff --git a/public/branding/taonier-logo-anti-candy-concepts/taonier-logo-anti-candy-manifest.json b/public/branding/taonier-logo-anti-candy-concepts/taonier-logo-anti-candy-manifest.json new file mode 100644 index 00000000..0b4c3d76 --- /dev/null +++ b/public/branding/taonier-logo-anti-candy-concepts/taonier-logo-anti-candy-manifest.json @@ -0,0 +1,62 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T18:03:02.694Z", + "creativeDirection": { + "name": "陶泥儿反糖果化脑洞泥印图形标", + "textPolicy": "no Chinese, no English, no wordmark", + "palette": "灰米白、陶土白、陶土褐、深泥灰、少量暗金土黄", + "motif": "哑光软方圆陶泥印章 + 星核凹印/负形 + 极少量刻点", + "antiCandyRules": "no glossy highlight, no cream filling, no jelly, no cookie, no chocolate, no candy star" + }, + "variants": [ + { + "id": "01-matte-clay-stamp", + "title": "哑光陶泥印章", + "file": "taonier-anti-candy-01-matte-clay-stamp.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须反糖果化:第一眼必须像哑光陶泥、泥章、窑印、创作印记,绝对不能像糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力或食品包装。\n核心隐喻:脑洞泥印。一块被轻轻捏平的不规则软方圆陶泥印章,中间有一个抽象星核凹印或镂空星形泥印,表达灵感被压印成作品。\n风格:扁平矢量商标为主,低高光、低饱和、哑光粉陶质感;轮廓清楚,可后续矢量化,适合商标、App 图标、社区头像。\n主色:灰米白、未经上釉的陶土白、陶土褐、深泥灰、少量暗金土黄;颜色必须干燥、克制、低甜度。不使用亮金、糖果黄、奶油黄、粉色、青色、蓝色、紫色、荧光色或彩虹色。\n形态:保留不规则软方圆,但不要鼓胀、不要胶状、不要可食用的圆润光泽。边缘可以有少量粗糙泥料纹理、压痕、手捏不均匀。\n数字感:只允许 2 到 3 个很小的深泥灰或暗土黄刻点,像生成节点或 UGC 扩散点;不要闪亮星星,不要糖珠。\n构图:正方形画布,居中图形标,干净浅灰米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和中间星核凹印。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、旋涡环形旧稿、三色花瓣旧稿。\n强禁止食品感:不要 glossy 高光、不要果冻质感、不要奶油夹心、不要糖霜、不要撒糖粒、不要饼干边、不要巧克力流心、不要金色膨胀星糖。\n本张重点:最克制的陶泥印章。灰米白软方圆主体,中间是压进去的暗陶土星核凹印,只有 2 个微小刻点。几乎无高光。" + }, + { + "id": "02-kiln-mark-core", + "title": "窑印星核", + "file": "taonier-anti-candy-02-kiln-mark-core.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须反糖果化:第一眼必须像哑光陶泥、泥章、窑印、创作印记,绝对不能像糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力或食品包装。\n核心隐喻:脑洞泥印。一块被轻轻捏平的不规则软方圆陶泥印章,中间有一个抽象星核凹印或镂空星形泥印,表达灵感被压印成作品。\n风格:扁平矢量商标为主,低高光、低饱和、哑光粉陶质感;轮廓清楚,可后续矢量化,适合商标、App 图标、社区头像。\n主色:灰米白、未经上釉的陶土白、陶土褐、深泥灰、少量暗金土黄;颜色必须干燥、克制、低甜度。不使用亮金、糖果黄、奶油黄、粉色、青色、蓝色、紫色、荧光色或彩虹色。\n形态:保留不规则软方圆,但不要鼓胀、不要胶状、不要可食用的圆润光泽。边缘可以有少量粗糙泥料纹理、压痕、手捏不均匀。\n数字感:只允许 2 到 3 个很小的深泥灰或暗土黄刻点,像生成节点或 UGC 扩散点;不要闪亮星星,不要糖珠。\n构图:正方形画布,居中图形标,干净浅灰米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和中间星核凹印。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、旋涡环形旧稿、三色花瓣旧稿。\n强禁止食品感:不要 glossy 高光、不要果冻质感、不要奶油夹心、不要糖霜、不要撒糖粒、不要饼干边、不要巧克力流心、不要金色膨胀星糖。\n本张重点:窑印感。中间星核像烧陶后的浅浮雕窑印,用深泥灰边缘和陶土褐阴影表现,不要任何金属或糖果光泽。" + }, + { + "id": "03-cutout-negative-star", + "title": "负形星核", + "file": "taonier-anti-candy-03-cutout-negative-star.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须反糖果化:第一眼必须像哑光陶泥、泥章、窑印、创作印记,绝对不能像糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力或食品包装。\n核心隐喻:脑洞泥印。一块被轻轻捏平的不规则软方圆陶泥印章,中间有一个抽象星核凹印或镂空星形泥印,表达灵感被压印成作品。\n风格:扁平矢量商标为主,低高光、低饱和、哑光粉陶质感;轮廓清楚,可后续矢量化,适合商标、App 图标、社区头像。\n主色:灰米白、未经上釉的陶土白、陶土褐、深泥灰、少量暗金土黄;颜色必须干燥、克制、低甜度。不使用亮金、糖果黄、奶油黄、粉色、青色、蓝色、紫色、荧光色或彩虹色。\n形态:保留不规则软方圆,但不要鼓胀、不要胶状、不要可食用的圆润光泽。边缘可以有少量粗糙泥料纹理、压痕、手捏不均匀。\n数字感:只允许 2 到 3 个很小的深泥灰或暗土黄刻点,像生成节点或 UGC 扩散点;不要闪亮星星,不要糖珠。\n构图:正方形画布,居中图形标,干净浅灰米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和中间星核凹印。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、旋涡环形旧稿、三色花瓣旧稿。\n强禁止食品感:不要 glossy 高光、不要果冻质感、不要奶油夹心、不要糖霜、不要撒糖粒、不要饼干边、不要巧克力流心、不要金色膨胀星糖。\n本张重点:负形。星核用干净的镂空负形或深泥灰内孔表达,主体是单块哑光陶泥,整体更像可注册商标图形。" + }, + { + "id": "04-dry-clay-grain", + "title": "干陶颗粒", + "file": "taonier-anti-candy-04-dry-clay-grain.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须反糖果化:第一眼必须像哑光陶泥、泥章、窑印、创作印记,绝对不能像糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力或食品包装。\n核心隐喻:脑洞泥印。一块被轻轻捏平的不规则软方圆陶泥印章,中间有一个抽象星核凹印或镂空星形泥印,表达灵感被压印成作品。\n风格:扁平矢量商标为主,低高光、低饱和、哑光粉陶质感;轮廓清楚,可后续矢量化,适合商标、App 图标、社区头像。\n主色:灰米白、未经上釉的陶土白、陶土褐、深泥灰、少量暗金土黄;颜色必须干燥、克制、低甜度。不使用亮金、糖果黄、奶油黄、粉色、青色、蓝色、紫色、荧光色或彩虹色。\n形态:保留不规则软方圆,但不要鼓胀、不要胶状、不要可食用的圆润光泽。边缘可以有少量粗糙泥料纹理、压痕、手捏不均匀。\n数字感:只允许 2 到 3 个很小的深泥灰或暗土黄刻点,像生成节点或 UGC 扩散点;不要闪亮星星,不要糖珠。\n构图:正方形画布,居中图形标,干净浅灰米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和中间星核凹印。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、旋涡环形旧稿、三色花瓣旧稿。\n强禁止食品感:不要 glossy 高光、不要果冻质感、不要奶油夹心、不要糖霜、不要撒糖粒、不要饼干边、不要巧克力流心、不要金色膨胀星糖。\n本张重点:干陶质感。加入非常细微的陶土颗粒和粉陶纹理,但保持扁平图标,不要照片写实,不要脏乱。" + }, + { + "id": "05-hand-pressed-token", + "title": "手压泥币", + "file": "taonier-anti-candy-05-hand-pressed-token.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须反糖果化:第一眼必须像哑光陶泥、泥章、窑印、创作印记,绝对不能像糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力或食品包装。\n核心隐喻:脑洞泥印。一块被轻轻捏平的不规则软方圆陶泥印章,中间有一个抽象星核凹印或镂空星形泥印,表达灵感被压印成作品。\n风格:扁平矢量商标为主,低高光、低饱和、哑光粉陶质感;轮廓清楚,可后续矢量化,适合商标、App 图标、社区头像。\n主色:灰米白、未经上釉的陶土白、陶土褐、深泥灰、少量暗金土黄;颜色必须干燥、克制、低甜度。不使用亮金、糖果黄、奶油黄、粉色、青色、蓝色、紫色、荧光色或彩虹色。\n形态:保留不规则软方圆,但不要鼓胀、不要胶状、不要可食用的圆润光泽。边缘可以有少量粗糙泥料纹理、压痕、手捏不均匀。\n数字感:只允许 2 到 3 个很小的深泥灰或暗土黄刻点,像生成节点或 UGC 扩散点;不要闪亮星星,不要糖珠。\n构图:正方形画布,居中图形标,干净浅灰米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和中间星核凹印。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、旋涡环形旧稿、三色花瓣旧稿。\n强禁止食品感:不要 glossy 高光、不要果冻质感、不要奶油夹心、不要糖霜、不要撒糖粒、不要饼干边、不要巧克力流心、不要金色膨胀星糖。\n本张重点:手压泥币。像一枚被手工压平的陶泥代币,边缘不完全对称,中间星核为凹刻符号,但不要出现手或工具。" + }, + { + "id": "06-digital-clay-glyph", + "title": "数字泥符", + "file": "taonier-anti-candy-06-digital-clay-glyph.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须反糖果化:第一眼必须像哑光陶泥、泥章、窑印、创作印记,绝对不能像糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力或食品包装。\n核心隐喻:脑洞泥印。一块被轻轻捏平的不规则软方圆陶泥印章,中间有一个抽象星核凹印或镂空星形泥印,表达灵感被压印成作品。\n风格:扁平矢量商标为主,低高光、低饱和、哑光粉陶质感;轮廓清楚,可后续矢量化,适合商标、App 图标、社区头像。\n主色:灰米白、未经上釉的陶土白、陶土褐、深泥灰、少量暗金土黄;颜色必须干燥、克制、低甜度。不使用亮金、糖果黄、奶油黄、粉色、青色、蓝色、紫色、荧光色或彩虹色。\n形态:保留不规则软方圆,但不要鼓胀、不要胶状、不要可食用的圆润光泽。边缘可以有少量粗糙泥料纹理、压痕、手捏不均匀。\n数字感:只允许 2 到 3 个很小的深泥灰或暗土黄刻点,像生成节点或 UGC 扩散点;不要闪亮星星,不要糖珠。\n构图:正方形画布,居中图形标,干净浅灰米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和中间星核凹印。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、旋涡环形旧稿、三色花瓣旧稿。\n强禁止食品感:不要 glossy 高光、不要果冻质感、不要奶油夹心、不要糖霜、不要撒糖粒、不要饼干边、不要巧克力流心、不要金色膨胀星糖。\n本张重点:AI 与 UGC 暗示更强。用 3 个极小方形刻点围绕星核,像生成节点,但必须像刻在陶泥上的小孔。" + }, + { + "id": "07-premium-flat-mark", + "title": "精品扁平标", + "file": "taonier-anti-candy-07-premium-flat-mark.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须反糖果化:第一眼必须像哑光陶泥、泥章、窑印、创作印记,绝对不能像糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力或食品包装。\n核心隐喻:脑洞泥印。一块被轻轻捏平的不规则软方圆陶泥印章,中间有一个抽象星核凹印或镂空星形泥印,表达灵感被压印成作品。\n风格:扁平矢量商标为主,低高光、低饱和、哑光粉陶质感;轮廓清楚,可后续矢量化,适合商标、App 图标、社区头像。\n主色:灰米白、未经上釉的陶土白、陶土褐、深泥灰、少量暗金土黄;颜色必须干燥、克制、低甜度。不使用亮金、糖果黄、奶油黄、粉色、青色、蓝色、紫色、荧光色或彩虹色。\n形态:保留不规则软方圆,但不要鼓胀、不要胶状、不要可食用的圆润光泽。边缘可以有少量粗糙泥料纹理、压痕、手捏不均匀。\n数字感:只允许 2 到 3 个很小的深泥灰或暗土黄刻点,像生成节点或 UGC 扩散点;不要闪亮星星,不要糖珠。\n构图:正方形画布,居中图形标,干净浅灰米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和中间星核凹印。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、旋涡环形旧稿、三色花瓣旧稿。\n强禁止食品感:不要 glossy 高光、不要果冻质感、不要奶油夹心、不要糖霜、不要撒糖粒、不要饼干边、不要巧克力流心、不要金色膨胀星糖。\n本张重点:更互联网精品。减少纹理,强化几何平衡和负形,灰米白主体、深泥灰星核、陶土褐小刻痕,适合 App 图标。" + }, + { + "id": "08-monochrome-proof", + "title": "单色验证版", + "file": "taonier-anti-candy-08-monochrome-proof.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须反糖果化:第一眼必须像哑光陶泥、泥章、窑印、创作印记,绝对不能像糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力或食品包装。\n核心隐喻:脑洞泥印。一块被轻轻捏平的不规则软方圆陶泥印章,中间有一个抽象星核凹印或镂空星形泥印,表达灵感被压印成作品。\n风格:扁平矢量商标为主,低高光、低饱和、哑光粉陶质感;轮廓清楚,可后续矢量化,适合商标、App 图标、社区头像。\n主色:灰米白、未经上釉的陶土白、陶土褐、深泥灰、少量暗金土黄;颜色必须干燥、克制、低甜度。不使用亮金、糖果黄、奶油黄、粉色、青色、蓝色、紫色、荧光色或彩虹色。\n形态:保留不规则软方圆,但不要鼓胀、不要胶状、不要可食用的圆润光泽。边缘可以有少量粗糙泥料纹理、压痕、手捏不均匀。\n数字感:只允许 2 到 3 个很小的深泥灰或暗土黄刻点,像生成节点或 UGC 扩散点;不要闪亮星星,不要糖珠。\n构图:正方形画布,居中图形标,干净浅灰米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和中间星核凹印。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、旋涡环形旧稿、三色花瓣旧稿。\n强禁止食品感:不要 glossy 高光、不要果冻质感、不要奶油夹心、不要糖霜、不要撒糖粒、不要饼干边、不要巧克力流心、不要金色膨胀星糖。\n本张重点:黑白商标验证。尽量用单色深浅关系表达软方圆和星核凹印,减少装饰,确保黑白化后轮廓仍成立。" + } + ] +} diff --git a/public/branding/taonier-logo-braincore-concepts/taonier-braincore-01-minimal-braincore.png b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-01-minimal-braincore.png new file mode 100644 index 00000000..99674899 Binary files /dev/null and b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-01-minimal-braincore.png differ diff --git a/public/branding/taonier-logo-braincore-concepts/taonier-braincore-02-soft-square-clay-seal.png b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-02-soft-square-clay-seal.png new file mode 100644 index 00000000..d8f385df Binary files /dev/null and b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-02-soft-square-clay-seal.png differ diff --git a/public/branding/taonier-logo-braincore-concepts/taonier-braincore-03-warm-brown-embedded-core.png b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-03-warm-brown-embedded-core.png new file mode 100644 index 00000000..f821f583 Binary files /dev/null and b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-03-warm-brown-embedded-core.png differ diff --git a/public/branding/taonier-logo-braincore-concepts/taonier-braincore-04-subtle-pinch-marks.png b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-04-subtle-pinch-marks.png new file mode 100644 index 00000000..0b106a60 Binary files /dev/null and b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-04-subtle-pinch-marks.png differ diff --git a/public/branding/taonier-logo-braincore-concepts/taonier-braincore-05-premium-geometric-balance.png b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-05-premium-geometric-balance.png new file mode 100644 index 00000000..9292cbe7 Binary files /dev/null and b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-05-premium-geometric-balance.png differ diff --git a/public/branding/taonier-logo-braincore-concepts/taonier-braincore-06-soft-clay-texture.png b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-06-soft-clay-texture.png new file mode 100644 index 00000000..aa717de5 Binary files /dev/null and b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-06-soft-clay-texture.png differ diff --git a/public/branding/taonier-logo-braincore-concepts/taonier-braincore-07-app-icon-ready.png b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-07-app-icon-ready.png new file mode 100644 index 00000000..9994dc61 Binary files /dev/null and b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-07-app-icon-ready.png differ diff --git a/public/branding/taonier-logo-braincore-concepts/taonier-braincore-08-trademark-monochrome-ready.png b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-08-trademark-monochrome-ready.png new file mode 100644 index 00000000..d9758eba Binary files /dev/null and b/public/branding/taonier-logo-braincore-concepts/taonier-braincore-08-trademark-monochrome-ready.png differ diff --git a/public/branding/taonier-logo-braincore-concepts/taonier-logo-braincore-contact-sheet.png b/public/branding/taonier-logo-braincore-concepts/taonier-logo-braincore-contact-sheet.png new file mode 100644 index 00000000..6bdf198a Binary files /dev/null and b/public/branding/taonier-logo-braincore-concepts/taonier-logo-braincore-contact-sheet.png differ diff --git a/public/branding/taonier-logo-braincore-concepts/taonier-logo-braincore-manifest.json b/public/branding/taonier-logo-braincore-concepts/taonier-logo-braincore-manifest.json new file mode 100644 index 00000000..5fba5f9c --- /dev/null +++ b/public/branding/taonier-logo-braincore-concepts/taonier-logo-braincore-manifest.json @@ -0,0 +1,61 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T17:45:55.245Z", + "creativeDirection": { + "name": "陶泥儿脑洞星核图形标", + "textPolicy": "no Chinese, no English, no wordmark", + "palette": "奶白、米白、暖棕、陶土棕、少量暖金", + "motif": "不规则软方圆陶泥团 + 脑洞星核 + 极少量星点" + }, + "variants": [ + { + "id": "01-minimal-braincore", + "title": "极简脑洞星核", + "file": "taonier-braincore-01-minimal-braincore.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n核心隐喻:脑洞星核。一团奶白色、暖棕色调的不规则软方圆陶泥包裹一枚暖金色创意星核,表达灵感被捏造成型。\n风格:扁平矢量商标为主,带非常轻微的软陶质感;结构简洁、边缘柔和、轮廓清晰,适合商标、App 图标、社区头像。\n主色:奶白、米白、暖棕、陶土棕、少量暖金;整体温暖高级,不使用粉色、青色、蓝色、紫色、荧光色或彩虹色。\n数字感:只允许在星核周围出现 2 到 4 个极小暖金星点或像素点,暗示 AI 生成、UGC 传播和轻游戏趣味。\n构图:正方形画布,居中图形标,干净浅米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和星核。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、拟物甜品感。\n必须保持全新构图,不参考任何既有陶泥儿 logo 方案,不做旋涡环形旧稿,不做三色花瓣旧稿。\n本张重点:极简。只保留一个奶白不规则软方圆陶泥主体、一个居中的暖金四角星核、2 个极小暖金星点。不要额外装饰。" + }, + { + "id": "02-soft-square-clay-seal", + "title": "软方圆陶泥印记", + "file": "taonier-braincore-02-soft-square-clay-seal.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n核心隐喻:脑洞星核。一团奶白色、暖棕色调的不规则软方圆陶泥包裹一枚暖金色创意星核,表达灵感被捏造成型。\n风格:扁平矢量商标为主,带非常轻微的软陶质感;结构简洁、边缘柔和、轮廓清晰,适合商标、App 图标、社区头像。\n主色:奶白、米白、暖棕、陶土棕、少量暖金;整体温暖高级,不使用粉色、青色、蓝色、紫色、荧光色或彩虹色。\n数字感:只允许在星核周围出现 2 到 4 个极小暖金星点或像素点,暗示 AI 生成、UGC 传播和轻游戏趣味。\n构图:正方形画布,居中图形标,干净浅米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和星核。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、拟物甜品感。\n必须保持全新构图,不参考任何既有陶泥儿 logo 方案,不做旋涡环形旧稿,不做三色花瓣旧稿。\n本张重点:陶泥印记。主体像被轻轻按压成型的软方圆印章,边缘有自然手捏起伏,但不能像儿童玩具。星核略微偏心。" + }, + { + "id": "03-warm-brown-embedded-core", + "title": "暖棕星核嵌入", + "file": "taonier-braincore-03-warm-brown-embedded-core.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n核心隐喻:脑洞星核。一团奶白色、暖棕色调的不规则软方圆陶泥包裹一枚暖金色创意星核,表达灵感被捏造成型。\n风格:扁平矢量商标为主,带非常轻微的软陶质感;结构简洁、边缘柔和、轮廓清晰,适合商标、App 图标、社区头像。\n主色:奶白、米白、暖棕、陶土棕、少量暖金;整体温暖高级,不使用粉色、青色、蓝色、紫色、荧光色或彩虹色。\n数字感:只允许在星核周围出现 2 到 4 个极小暖金星点或像素点,暗示 AI 生成、UGC 传播和轻游戏趣味。\n构图:正方形画布,居中图形标,干净浅米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和星核。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、拟物甜品感。\n必须保持全新构图,不参考任何既有陶泥儿 logo 方案,不做旋涡环形旧稿,不做三色花瓣旧稿。\n本张重点:嵌入感。用暖棕内凹形或陶土棕阴影承托暖金星核,像灵感被嵌进陶泥里,仍保持扁平商标质感。" + }, + { + "id": "04-subtle-pinch-marks", + "title": "轻微捏痕版本", + "file": "taonier-braincore-04-subtle-pinch-marks.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n核心隐喻:脑洞星核。一团奶白色、暖棕色调的不规则软方圆陶泥包裹一枚暖金色创意星核,表达灵感被捏造成型。\n风格:扁平矢量商标为主,带非常轻微的软陶质感;结构简洁、边缘柔和、轮廓清晰,适合商标、App 图标、社区头像。\n主色:奶白、米白、暖棕、陶土棕、少量暖金;整体温暖高级,不使用粉色、青色、蓝色、紫色、荧光色或彩虹色。\n数字感:只允许在星核周围出现 2 到 4 个极小暖金星点或像素点,暗示 AI 生成、UGC 传播和轻游戏趣味。\n构图:正方形画布,居中图形标,干净浅米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和星核。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、拟物甜品感。\n必须保持全新构图,不参考任何既有陶泥儿 logo 方案,不做旋涡环形旧稿,不做三色花瓣旧稿。\n本张重点:捏痕。在陶泥主体边缘加入 2 到 3 个极轻微暖棕捏痕或凹口,表现可塑性;捏痕必须抽象、克制、可矢量化。" + }, + { + "id": "05-premium-geometric-balance", + "title": "精品几何比例", + "file": "taonier-braincore-05-premium-geometric-balance.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n核心隐喻:脑洞星核。一团奶白色、暖棕色调的不规则软方圆陶泥包裹一枚暖金色创意星核,表达灵感被捏造成型。\n风格:扁平矢量商标为主,带非常轻微的软陶质感;结构简洁、边缘柔和、轮廓清晰,适合商标、App 图标、社区头像。\n主色:奶白、米白、暖棕、陶土棕、少量暖金;整体温暖高级,不使用粉色、青色、蓝色、紫色、荧光色或彩虹色。\n数字感:只允许在星核周围出现 2 到 4 个极小暖金星点或像素点,暗示 AI 生成、UGC 传播和轻游戏趣味。\n构图:正方形画布,居中图形标,干净浅米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和星核。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、拟物甜品感。\n必须保持全新构图,不参考任何既有陶泥儿 logo 方案,不做旋涡环形旧稿,不做三色花瓣旧稿。\n本张重点:精品比例。整体更接近高级互联网 App 图标,几何平衡、负形干净、软方圆轮廓稳定,陶泥质感只保留一点点。" + }, + { + "id": "06-soft-clay-texture", + "title": "柔软陶泥质感", + "file": "taonier-braincore-06-soft-clay-texture.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n核心隐喻:脑洞星核。一团奶白色、暖棕色调的不规则软方圆陶泥包裹一枚暖金色创意星核,表达灵感被捏造成型。\n风格:扁平矢量商标为主,带非常轻微的软陶质感;结构简洁、边缘柔和、轮廓清晰,适合商标、App 图标、社区头像。\n主色:奶白、米白、暖棕、陶土棕、少量暖金;整体温暖高级,不使用粉色、青色、蓝色、紫色、荧光色或彩虹色。\n数字感:只允许在星核周围出现 2 到 4 个极小暖金星点或像素点,暗示 AI 生成、UGC 传播和轻游戏趣味。\n构图:正方形画布,居中图形标,干净浅米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和星核。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、拟物甜品感。\n必须保持全新构图,不参考任何既有陶泥儿 logo 方案,不做旋涡环形旧稿,不做三色花瓣旧稿。\n本张重点:柔软质感。在不破坏扁平矢量感的前提下,加入细腻奶油陶泥的微妙高光和暖棕渐层,不能变成 3D 玩具。" + }, + { + "id": "07-app-icon-ready", + "title": "App 图标优先", + "file": "taonier-braincore-07-app-icon-ready.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n核心隐喻:脑洞星核。一团奶白色、暖棕色调的不规则软方圆陶泥包裹一枚暖金色创意星核,表达灵感被捏造成型。\n风格:扁平矢量商标为主,带非常轻微的软陶质感;结构简洁、边缘柔和、轮廓清晰,适合商标、App 图标、社区头像。\n主色:奶白、米白、暖棕、陶土棕、少量暖金;整体温暖高级,不使用粉色、青色、蓝色、紫色、荧光色或彩虹色。\n数字感:只允许在星核周围出现 2 到 4 个极小暖金星点或像素点,暗示 AI 生成、UGC 传播和轻游戏趣味。\n构图:正方形画布,居中图形标,干净浅米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和星核。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、拟物甜品感。\n必须保持全新构图,不参考任何既有陶泥儿 logo 方案,不做旋涡环形旧稿,不做三色花瓣旧稿。\n本张重点:App 图标。图形占画面约 72%,轮廓饱满有记忆点,星核清晰醒目,适合放入圆角方形 App icon。" + }, + { + "id": "08-trademark-monochrome-ready", + "title": "商标黑白提炼", + "file": "taonier-braincore-08-trademark-monochrome-ready.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n核心隐喻:脑洞星核。一团奶白色、暖棕色调的不规则软方圆陶泥包裹一枚暖金色创意星核,表达灵感被捏造成型。\n风格:扁平矢量商标为主,带非常轻微的软陶质感;结构简洁、边缘柔和、轮廓清晰,适合商标、App 图标、社区头像。\n主色:奶白、米白、暖棕、陶土棕、少量暖金;整体温暖高级,不使用粉色、青色、蓝色、紫色、荧光色或彩虹色。\n数字感:只允许在星核周围出现 2 到 4 个极小暖金星点或像素点,暗示 AI 生成、UGC 传播和轻游戏趣味。\n构图:正方形画布,居中图形标,干净浅米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和星核。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、拟物甜品感。\n必须保持全新构图,不参考任何既有陶泥儿 logo 方案,不做旋涡环形旧稿,不做三色花瓣旧稿。\n本张重点:商标注册。优先保证黑白化后仍清楚,减少渐变和细节,用奶白主体、暖棕负形和暖金星核形成强轮廓。" + } + ] +} diff --git a/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-01-closed-curve-mark.png b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-01-closed-curve-mark.png new file mode 100644 index 00000000..35ef5de9 Binary files /dev/null and b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-01-closed-curve-mark.png differ diff --git a/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-02-friendly-geo-seed.png b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-02-friendly-geo-seed.png new file mode 100644 index 00000000..9678ad88 Binary files /dev/null and b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-02-friendly-geo-seed.png differ diff --git a/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-03-premium-soft-contour.png b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-03-premium-soft-contour.png new file mode 100644 index 00000000..bfbeedbc Binary files /dev/null and b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-03-premium-soft-contour.png differ diff --git a/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-04-playful-closed-tile.png b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-04-playful-closed-tile.png new file mode 100644 index 00000000..513ff218 Binary files /dev/null and b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-04-playful-closed-tile.png differ diff --git a/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-05-monochrome-first.png b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-05-monochrome-first.png new file mode 100644 index 00000000..0f3fac18 Binary files /dev/null and b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-05-monochrome-first.png differ diff --git a/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-06-digital-clay-accent.png b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-06-digital-clay-accent.png new file mode 100644 index 00000000..f096d762 Binary files /dev/null and b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-06-digital-clay-accent.png differ diff --git a/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-07-compact-avatar-symbol.png b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-07-compact-avatar-symbol.png new file mode 100644 index 00000000..77a78203 Binary files /dev/null and b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-07-compact-avatar-symbol.png differ diff --git a/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-08-designer-vector-ready.png b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-08-designer-vector-ready.png new file mode 100644 index 00000000..982814f4 Binary files /dev/null and b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-08-designer-vector-ready.png differ diff --git a/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-contact-sheet.png b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-contact-sheet.png new file mode 100644 index 00000000..10d4d76a Binary files /dev/null and b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-contact-sheet.png differ diff --git a/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-manifest.json b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-manifest.json new file mode 100644 index 00000000..6a013631 --- /dev/null +++ b/public/branding/taonier-logo-brief-concepts/taonier-logo-brief-manifest.json @@ -0,0 +1,88 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T20:18:49.419Z", + "logoSkillSummary": { + "requiredReview": "visual inspection, 32px readability, black-white viability", + "outputStatus": "AI concept only; final logo needs vector cleanup" + }, + "brief": { + "brand": "陶泥儿", + "logoType": "symbol/icon-only mark, no wordmark", + "product": "AI UGC 轻休闲小游戏创作与传播平台,用户像捏陶泥一样把脑洞、梗和灵感塑造成作品", + "personality": [ + "亲和", + "精品", + "创作感", + "轻松", + "年轻", + "有传播记忆点" + ], + "mustHave": [ + "闭合不规则几何底盘", + "外轮廓由流畅曲线组成", + "整体是一个完整符号而不是自由飘带", + "32px 仍能识别", + "黑白化后仍成立", + "无中文、无英文、无字标" + ], + "avoid": [ + "整体方形或圆角方块", + "中心星星或任何星形", + "自由飘带、旋涡、S/G 字母感", + "巧克力面包、甜点、饼干、糖果等食物感", + "砖块、土块、泥饼、陶片、考古印章", + "脸、表情、吉祥物、手、工具" + ] + }, + "variants": [ + { + "id": "01-closed-curve-mark", + "title": "闭合曲线标", + "file": "taonier-logo-brief-01-closed-curve-mark.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand personality: friendly, premium, creative, lightweight, young, memorable, suitable for an AI UGC casual game creation platform.\nCore metaphor: users shape imagination like clay. The logo should communicate soft creative material becoming a refined digital product symbol.\nLogo type: abstract symbol/icon only. Not an emblem with text, not a mascot, not an app-icon rounded-square background.\nMain element: one closed irregular geometric base shape made from smooth flowing curves. The outer contour must be closed, continuous, recognizable, organic, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, or loose strip. It should feel like a refined custom symbol with 5-7 soft curve turns.\nInternal design: use only 1-2 broad smooth curve partitions or negative-shape cuts to make the mark memorable. No center icon inserted. No star, no spark, no hole shaped like a star.\nStyle: modern minimalist vector logo with very subtle matte clay warmth. Clean edges, broad shapes, high contrast, no tiny details. It must look good and recognizable at 32px favicon size.\nColor: warm ceramic white, light terracotta, clay orange, warm brown, with optional small low-saturation teal, indigo-gray, or dark mud gray accent. Avoid sweet candy colors. No glossy highlights.\nFood avoidance is critical: the mark must not look like bread, chocolate bread, croissant, pastry, cookie, candy, donut, cream filling, sauce, baked dough, dessert, or food packaging.\nMaterial avoidance: do not make it look like brick, dirt clod, mud pie, pottery shard, stone, archaeological stamp, or rough handmade craft class object.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the large shape should be recognizable at 32px.\nVariant focus: the cleanest closed irregular curve mark. Use two large color areas separated by one smooth internal curve. Maximize 32px readability." + }, + { + "id": "02-friendly-geo-seed", + "title": "亲和几何种", + "file": "taonier-logo-brief-02-friendly-geo-seed.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand personality: friendly, premium, creative, lightweight, young, memorable, suitable for an AI UGC casual game creation platform.\nCore metaphor: users shape imagination like clay. The logo should communicate soft creative material becoming a refined digital product symbol.\nLogo type: abstract symbol/icon only. Not an emblem with text, not a mascot, not an app-icon rounded-square background.\nMain element: one closed irregular geometric base shape made from smooth flowing curves. The outer contour must be closed, continuous, recognizable, organic, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, or loose strip. It should feel like a refined custom symbol with 5-7 soft curve turns.\nInternal design: use only 1-2 broad smooth curve partitions or negative-shape cuts to make the mark memorable. No center icon inserted. No star, no spark, no hole shaped like a star.\nStyle: modern minimalist vector logo with very subtle matte clay warmth. Clean edges, broad shapes, high contrast, no tiny details. It must look good and recognizable at 32px favicon size.\nColor: warm ceramic white, light terracotta, clay orange, warm brown, with optional small low-saturation teal, indigo-gray, or dark mud gray accent. Avoid sweet candy colors. No glossy highlights.\nFood avoidance is critical: the mark must not look like bread, chocolate bread, croissant, pastry, cookie, candy, donut, cream filling, sauce, baked dough, dessert, or food packaging.\nMaterial avoidance: do not make it look like brick, dirt clod, mud pie, pottery shard, stone, archaeological stamp, or rough handmade craft class object.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the large shape should be recognizable at 32px.\nVariant focus: a friendly seed-like closed geometric base, but not a literal seed, not food. Rounded and approachable with one teal accent curve." + }, + { + "id": "03-premium-soft-contour", + "title": "精品软轮廓", + "file": "taonier-logo-brief-03-premium-soft-contour.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand personality: friendly, premium, creative, lightweight, young, memorable, suitable for an AI UGC casual game creation platform.\nCore metaphor: users shape imagination like clay. The logo should communicate soft creative material becoming a refined digital product symbol.\nLogo type: abstract symbol/icon only. Not an emblem with text, not a mascot, not an app-icon rounded-square background.\nMain element: one closed irregular geometric base shape made from smooth flowing curves. The outer contour must be closed, continuous, recognizable, organic, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, or loose strip. It should feel like a refined custom symbol with 5-7 soft curve turns.\nInternal design: use only 1-2 broad smooth curve partitions or negative-shape cuts to make the mark memorable. No center icon inserted. No star, no spark, no hole shaped like a star.\nStyle: modern minimalist vector logo with very subtle matte clay warmth. Clean edges, broad shapes, high contrast, no tiny details. It must look good and recognizable at 32px favicon size.\nColor: warm ceramic white, light terracotta, clay orange, warm brown, with optional small low-saturation teal, indigo-gray, or dark mud gray accent. Avoid sweet candy colors. No glossy highlights.\nFood avoidance is critical: the mark must not look like bread, chocolate bread, croissant, pastry, cookie, candy, donut, cream filling, sauce, baked dough, dessert, or food packaging.\nMaterial avoidance: do not make it look like brick, dirt clod, mud pie, pottery shard, stone, archaeological stamp, or rough handmade craft class object.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the large shape should be recognizable at 32px.\nVariant focus: premium, calm, fewer colors. Strong outer contour with a dark mud-gray internal negative curve. Very logo-like, not illustrative." + }, + { + "id": "04-playful-closed-tile", + "title": "轻玩闭合片", + "file": "taonier-logo-brief-04-playful-closed-tile.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand personality: friendly, premium, creative, lightweight, young, memorable, suitable for an AI UGC casual game creation platform.\nCore metaphor: users shape imagination like clay. The logo should communicate soft creative material becoming a refined digital product symbol.\nLogo type: abstract symbol/icon only. Not an emblem with text, not a mascot, not an app-icon rounded-square background.\nMain element: one closed irregular geometric base shape made from smooth flowing curves. The outer contour must be closed, continuous, recognizable, organic, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, or loose strip. It should feel like a refined custom symbol with 5-7 soft curve turns.\nInternal design: use only 1-2 broad smooth curve partitions or negative-shape cuts to make the mark memorable. No center icon inserted. No star, no spark, no hole shaped like a star.\nStyle: modern minimalist vector logo with very subtle matte clay warmth. Clean edges, broad shapes, high contrast, no tiny details. It must look good and recognizable at 32px favicon size.\nColor: warm ceramic white, light terracotta, clay orange, warm brown, with optional small low-saturation teal, indigo-gray, or dark mud gray accent. Avoid sweet candy colors. No glossy highlights.\nFood avoidance is critical: the mark must not look like bread, chocolate bread, croissant, pastry, cookie, candy, donut, cream filling, sauce, baked dough, dessert, or food packaging.\nMaterial avoidance: do not make it look like brick, dirt clod, mud pie, pottery shard, stone, archaeological stamp, or rough handmade craft class object.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the large shape should be recognizable at 32px.\nVariant focus: a more playful closed irregular tile with warm terracotta and ceramic white. The internal curve should suggest creation flow, not filling." + }, + { + "id": "05-monochrome-first", + "title": "黑白优先", + "file": "taonier-logo-brief-05-monochrome-first.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand personality: friendly, premium, creative, lightweight, young, memorable, suitable for an AI UGC casual game creation platform.\nCore metaphor: users shape imagination like clay. The logo should communicate soft creative material becoming a refined digital product symbol.\nLogo type: abstract symbol/icon only. Not an emblem with text, not a mascot, not an app-icon rounded-square background.\nMain element: one closed irregular geometric base shape made from smooth flowing curves. The outer contour must be closed, continuous, recognizable, organic, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, or loose strip. It should feel like a refined custom symbol with 5-7 soft curve turns.\nInternal design: use only 1-2 broad smooth curve partitions or negative-shape cuts to make the mark memorable. No center icon inserted. No star, no spark, no hole shaped like a star.\nStyle: modern minimalist vector logo with very subtle matte clay warmth. Clean edges, broad shapes, high contrast, no tiny details. It must look good and recognizable at 32px favicon size.\nColor: warm ceramic white, light terracotta, clay orange, warm brown, with optional small low-saturation teal, indigo-gray, or dark mud gray accent. Avoid sweet candy colors. No glossy highlights.\nFood avoidance is critical: the mark must not look like bread, chocolate bread, croissant, pastry, cookie, candy, donut, cream filling, sauce, baked dough, dessert, or food packaging.\nMaterial avoidance: do not make it look like brick, dirt clod, mud pie, pottery shard, stone, archaeological stamp, or rough handmade craft class object.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the large shape should be recognizable at 32px.\nVariant focus: design as if it will be converted to black and white. Use bold positive and negative shapes; color only supports the structure." + }, + { + "id": "06-digital-clay-accent", + "title": "数字陶泥点", + "file": "taonier-logo-brief-06-digital-clay-accent.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand personality: friendly, premium, creative, lightweight, young, memorable, suitable for an AI UGC casual game creation platform.\nCore metaphor: users shape imagination like clay. The logo should communicate soft creative material becoming a refined digital product symbol.\nLogo type: abstract symbol/icon only. Not an emblem with text, not a mascot, not an app-icon rounded-square background.\nMain element: one closed irregular geometric base shape made from smooth flowing curves. The outer contour must be closed, continuous, recognizable, organic, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, or loose strip. It should feel like a refined custom symbol with 5-7 soft curve turns.\nInternal design: use only 1-2 broad smooth curve partitions or negative-shape cuts to make the mark memorable. No center icon inserted. No star, no spark, no hole shaped like a star.\nStyle: modern minimalist vector logo with very subtle matte clay warmth. Clean edges, broad shapes, high contrast, no tiny details. It must look good and recognizable at 32px favicon size.\nColor: warm ceramic white, light terracotta, clay orange, warm brown, with optional small low-saturation teal, indigo-gray, or dark mud gray accent. Avoid sweet candy colors. No glossy highlights.\nFood avoidance is critical: the mark must not look like bread, chocolate bread, croissant, pastry, cookie, candy, donut, cream filling, sauce, baked dough, dessert, or food packaging.\nMaterial avoidance: do not make it look like brick, dirt clod, mud pie, pottery shard, stone, archaeological stamp, or rough handmade craft class object.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the large shape should be recognizable at 32px.\nVariant focus: include at most two tiny geometric accent dots or notches that imply AI/UGC, but they must not look like candy sprinkles or decorative confetti." + }, + { + "id": "07-compact-avatar-symbol", + "title": "头像紧凑标", + "file": "taonier-logo-brief-07-compact-avatar-symbol.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand personality: friendly, premium, creative, lightweight, young, memorable, suitable for an AI UGC casual game creation platform.\nCore metaphor: users shape imagination like clay. The logo should communicate soft creative material becoming a refined digital product symbol.\nLogo type: abstract symbol/icon only. Not an emblem with text, not a mascot, not an app-icon rounded-square background.\nMain element: one closed irregular geometric base shape made from smooth flowing curves. The outer contour must be closed, continuous, recognizable, organic, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, or loose strip. It should feel like a refined custom symbol with 5-7 soft curve turns.\nInternal design: use only 1-2 broad smooth curve partitions or negative-shape cuts to make the mark memorable. No center icon inserted. No star, no spark, no hole shaped like a star.\nStyle: modern minimalist vector logo with very subtle matte clay warmth. Clean edges, broad shapes, high contrast, no tiny details. It must look good and recognizable at 32px favicon size.\nColor: warm ceramic white, light terracotta, clay orange, warm brown, with optional small low-saturation teal, indigo-gray, or dark mud gray accent. Avoid sweet candy colors. No glossy highlights.\nFood avoidance is critical: the mark must not look like bread, chocolate bread, croissant, pastry, cookie, candy, donut, cream filling, sauce, baked dough, dessert, or food packaging.\nMaterial avoidance: do not make it look like brick, dirt clod, mud pie, pottery shard, stone, archaeological stamp, or rough handmade craft class object.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the large shape should be recognizable at 32px.\nVariant focus: compact social-avatar readability. The closed contour should be slightly fuller and more iconic, but not a rounded-square app background." + }, + { + "id": "08-designer-vector-ready", + "title": "矢量定稿感", + "file": "taonier-logo-brief-08-designer-vector-ready.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand personality: friendly, premium, creative, lightweight, young, memorable, suitable for an AI UGC casual game creation platform.\nCore metaphor: users shape imagination like clay. The logo should communicate soft creative material becoming a refined digital product symbol.\nLogo type: abstract symbol/icon only. Not an emblem with text, not a mascot, not an app-icon rounded-square background.\nMain element: one closed irregular geometric base shape made from smooth flowing curves. The outer contour must be closed, continuous, recognizable, organic, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, or loose strip. It should feel like a refined custom symbol with 5-7 soft curve turns.\nInternal design: use only 1-2 broad smooth curve partitions or negative-shape cuts to make the mark memorable. No center icon inserted. No star, no spark, no hole shaped like a star.\nStyle: modern minimalist vector logo with very subtle matte clay warmth. Clean edges, broad shapes, high contrast, no tiny details. It must look good and recognizable at 32px favicon size.\nColor: warm ceramic white, light terracotta, clay orange, warm brown, with optional small low-saturation teal, indigo-gray, or dark mud gray accent. Avoid sweet candy colors. No glossy highlights.\nFood avoidance is critical: the mark must not look like bread, chocolate bread, croissant, pastry, cookie, candy, donut, cream filling, sauce, baked dough, dessert, or food packaging.\nMaterial avoidance: do not make it look like brick, dirt clod, mud pie, pottery shard, stone, archaeological stamp, or rough handmade craft class object.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the large shape should be recognizable at 32px.\nVariant focus: make it look like a designer-ready vector concept: 2-3 flat shapes, crisp boundaries, distinctive closed outer contour, minimal material texture." + } + ] +} diff --git a/public/branding/taonier-logo-broad-concepts/taonier-broad-creation-spark.png b/public/branding/taonier-logo-broad-concepts/taonier-broad-creation-spark.png new file mode 100644 index 00000000..163fc3b3 Binary files /dev/null and b/public/branding/taonier-logo-broad-concepts/taonier-broad-creation-spark.png differ diff --git a/public/branding/taonier-logo-broad-concepts/taonier-broad-game-mold.png b/public/branding/taonier-logo-broad-concepts/taonier-broad-game-mold.png new file mode 100644 index 00000000..619b54f1 Binary files /dev/null and b/public/branding/taonier-logo-broad-concepts/taonier-broad-game-mold.png differ diff --git a/public/branding/taonier-logo-broad-concepts/taonier-broad-soft-portal.png b/public/branding/taonier-logo-broad-concepts/taonier-broad-soft-portal.png new file mode 100644 index 00000000..6442e7c4 Binary files /dev/null and b/public/branding/taonier-logo-broad-concepts/taonier-broad-soft-portal.png differ diff --git a/public/branding/taonier-logo-broad-concepts/taonier-broad-soft-totem.png b/public/branding/taonier-logo-broad-concepts/taonier-broad-soft-totem.png new file mode 100644 index 00000000..b1ec5c3f Binary files /dev/null and b/public/branding/taonier-logo-broad-concepts/taonier-broad-soft-totem.png differ diff --git a/public/branding/taonier-logo-broad-concepts/taonier-broad-work-embryo.png b/public/branding/taonier-logo-broad-concepts/taonier-broad-work-embryo.png new file mode 100644 index 00000000..6239fcc2 Binary files /dev/null and b/public/branding/taonier-logo-broad-concepts/taonier-broad-work-embryo.png differ diff --git a/public/branding/taonier-logo-broad-concepts/taonier-logo-broad-contact-sheet.png b/public/branding/taonier-logo-broad-concepts/taonier-logo-broad-contact-sheet.png new file mode 100644 index 00000000..f57edfe7 Binary files /dev/null and b/public/branding/taonier-logo-broad-concepts/taonier-logo-broad-contact-sheet.png differ diff --git a/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-01-flat-terracotta.png b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-01-flat-terracotta.png new file mode 100644 index 00000000..805815ec Binary files /dev/null and b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-01-flat-terracotta.png differ diff --git a/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-02-cream-cocoa.png b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-02-cream-cocoa.png new file mode 100644 index 00000000..f7cbc4f8 Binary files /dev/null and b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-02-cream-cocoa.png differ diff --git a/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-03-sage-clay.png b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-03-sage-clay.png new file mode 100644 index 00000000..a7b98390 Binary files /dev/null and b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-03-sage-clay.png differ diff --git a/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-04-outline-emblem.png b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-04-outline-emblem.png new file mode 100644 index 00000000..43ea32b3 Binary files /dev/null and b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-04-outline-emblem.png differ diff --git a/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-05-abstract-geometric.png b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-05-abstract-geometric.png new file mode 100644 index 00000000..2ce17d42 Binary files /dev/null and b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-05-abstract-geometric.png differ diff --git a/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-06-monochrome-first.png b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-06-monochrome-first.png new file mode 100644 index 00000000..3de160b4 Binary files /dev/null and b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-06-monochrome-first.png differ diff --git a/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-07-soft-gradient-logo.png b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-07-soft-gradient-logo.png new file mode 100644 index 00000000..e3c6d27e Binary files /dev/null and b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-07-soft-gradient-logo.png differ diff --git a/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-08-bold-avatar.png b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-08-bold-avatar.png new file mode 100644 index 00000000..e7dc79fb Binary files /dev/null and b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-capybara-jar-ref01-logo-refine-08-bold-avatar.png differ diff --git a/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-logo-capybara-jar-ref01-logo-refine-contact-sheet.png b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-logo-capybara-jar-ref01-logo-refine-contact-sheet.png new file mode 100644 index 00000000..d2c6d64a Binary files /dev/null and b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-logo-capybara-jar-ref01-logo-refine-contact-sheet.png differ diff --git a/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-logo-capybara-jar-ref01-logo-refine-manifest.json b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-logo-capybara-jar-ref01-logo-refine-manifest.json new file mode 100644 index 00000000..2f709038 --- /dev/null +++ b/public/branding/taonier-logo-capybara-jar-ref01-logo-refine-concepts/taonier-logo-capybara-jar-ref01-logo-refine-manifest.json @@ -0,0 +1,90 @@ +{ + "model": "gpt-image-2", + "endpoint": "/v1/images/edits", + "size": "1024x1024", + "referenceImage": "public\\branding\\taonier-logo-peeking-head-jar-new-animals-concepts\\taonier-peeking-head-jar-new-animals-01-capybara.png", + "generatedAt": "2026-05-18T06:24:38.693Z", + "logoSkillSummary": { + "requiredReview": "visual inspection, 32px readability, black-white viability", + "outputStatus": "AI concept only; final logo needs vector cleanup" + }, + "brief": { + "brand": "陶泥儿", + "source": "基于 peeking-head-jar-new-animals 批次 01 水豚头参考图继续收敛", + "logoType": "symbol/icon-only mascot mark, no wordmark", + "product": "AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品", + "keep": [ + "陶罐容器为主形体", + "水豚式半圆脑袋只露到眼睛位置", + "两只纯黑点眼,无高光", + "小圆耳朵与平静亲和感", + "中心构图与 32px 可读性" + ], + "explore": [ + "不同罐子颜色", + "不同动物头色彩浓度", + "更扁平、更抽象、更商标化", + "更强黑白轮廓", + "减少插画感、渐变感和材质细节" + ], + "avoid": [ + "中文或英文字", + "鼻子、嘴巴、腮红、表情高光", + "罐子表情", + "星星、闪光、手、陶艺工具", + "甜点、面包、巧克力、糖果、布丁、餐具感", + "完整动物身体、爪子、复杂场景", + "贴纸感、儿童玩具感、写实陶瓷质感" + ] + }, + "variants": [ + { + "id": "01-flat-terracotta", + "title": "扁平陶橙", + "file": "taonier-capybara-jar-ref01-logo-refine-01-flat-terracotta.png", + "prompt": "Use the uploaded reference image as the composition and pose reference. Keep the same core idea: a ceramic jar container with a capybara-like small animal peeking out only to eye level.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the reference structure: the jar is the main brand shape; the animal shows only small rounded ears, the upper half-dome head, and two matte black dot eyes above the jar rim.\nDo not show nose, mouth, cheek blush, teeth, paws, body, lower face, or a full animal head. The eyes must be pure black dots with no highlights, no white sparkle, and no glossy pupils.\nThe jar itself must have no face, no expression, no eyes, no smile, and no decorative emoji marks.\nMake the result more like a trademark and logo than an illustration: simplified silhouette, clean vector curves, broad flat color fields, fewer gradients, fewer soft shadows, no realistic ceramic rendering.\nKeep a warm ceramic-container feeling through color and silhouette only. The jar should not look like a bowl of food, cup, dessert package, pudding cup, bread, bun, chocolate, candy, jelly, or pastry.\nStyle target: premium friendly mascot-symbol logo, all-age and women-friendly, calm but memorable, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, sticker outline, background scene, and watermark.\nVariant focus: the most direct logo refinement. Use flat terracotta jar, warm caramel capybara head, minimal rim shadow, almost no gradients. Make the jar silhouette slightly more iconic and compact." + }, + { + "id": "02-cream-cocoa", + "title": "奶白可可", + "file": "taonier-capybara-jar-ref01-logo-refine-02-cream-cocoa.png", + "prompt": "Use the uploaded reference image as the composition and pose reference. Keep the same core idea: a ceramic jar container with a capybara-like small animal peeking out only to eye level.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the reference structure: the jar is the main brand shape; the animal shows only small rounded ears, the upper half-dome head, and two matte black dot eyes above the jar rim.\nDo not show nose, mouth, cheek blush, teeth, paws, body, lower face, or a full animal head. The eyes must be pure black dots with no highlights, no white sparkle, and no glossy pupils.\nThe jar itself must have no face, no expression, no eyes, no smile, and no decorative emoji marks.\nMake the result more like a trademark and logo than an illustration: simplified silhouette, clean vector curves, broad flat color fields, fewer gradients, fewer soft shadows, no realistic ceramic rendering.\nKeep a warm ceramic-container feeling through color and silhouette only. The jar should not look like a bowl of food, cup, dessert package, pudding cup, bread, bun, chocolate, candy, jelly, or pastry.\nStyle target: premium friendly mascot-symbol logo, all-age and women-friendly, calm but memorable, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, sticker outline, background scene, and watermark.\nVariant focus: cream ceramic jar with a cocoa-brown capybara head. Keep the palette soft but not edible; use graphic flat fills and a crisp rim shape to avoid dessert feeling." + }, + { + "id": "03-sage-clay", + "title": "鼠尾草陶", + "file": "taonier-capybara-jar-ref01-logo-refine-03-sage-clay.png", + "prompt": "Use the uploaded reference image as the composition and pose reference. Keep the same core idea: a ceramic jar container with a capybara-like small animal peeking out only to eye level.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the reference structure: the jar is the main brand shape; the animal shows only small rounded ears, the upper half-dome head, and two matte black dot eyes above the jar rim.\nDo not show nose, mouth, cheek blush, teeth, paws, body, lower face, or a full animal head. The eyes must be pure black dots with no highlights, no white sparkle, and no glossy pupils.\nThe jar itself must have no face, no expression, no eyes, no smile, and no decorative emoji marks.\nMake the result more like a trademark and logo than an illustration: simplified silhouette, clean vector curves, broad flat color fields, fewer gradients, fewer soft shadows, no realistic ceramic rendering.\nKeep a warm ceramic-container feeling through color and silhouette only. The jar should not look like a bowl of food, cup, dessert package, pudding cup, bread, bun, chocolate, candy, jelly, or pastry.\nStyle target: premium friendly mascot-symbol logo, all-age and women-friendly, calm but memorable, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, sticker outline, background scene, and watermark.\nVariant focus: muted sage green ceramic jar paired with a warm ochre capybara head. More mature and boutique. Keep the silhouette simple and logo-like, with only two or three main color regions." + }, + { + "id": "04-outline-emblem", + "title": "线面徽记", + "file": "taonier-capybara-jar-ref01-logo-refine-04-outline-emblem.png", + "prompt": "Use the uploaded reference image as the composition and pose reference. Keep the same core idea: a ceramic jar container with a capybara-like small animal peeking out only to eye level.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the reference structure: the jar is the main brand shape; the animal shows only small rounded ears, the upper half-dome head, and two matte black dot eyes above the jar rim.\nDo not show nose, mouth, cheek blush, teeth, paws, body, lower face, or a full animal head. The eyes must be pure black dots with no highlights, no white sparkle, and no glossy pupils.\nThe jar itself must have no face, no expression, no eyes, no smile, and no decorative emoji marks.\nMake the result more like a trademark and logo than an illustration: simplified silhouette, clean vector curves, broad flat color fields, fewer gradients, fewer soft shadows, no realistic ceramic rendering.\nKeep a warm ceramic-container feeling through color and silhouette only. The jar should not look like a bowl of food, cup, dessert package, pudding cup, bread, bun, chocolate, candy, jelly, or pastry.\nStyle target: premium friendly mascot-symbol logo, all-age and women-friendly, calm but memorable, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, sticker outline, background scene, and watermark.\nVariant focus: bolder trademark mark with clean outline plus flat fills. Use a dark warm-brown contour line around the jar and animal, but keep it soft and modern, not sticker-like." + }, + { + "id": "05-abstract-geometric", + "title": "抽象几何", + "file": "taonier-capybara-jar-ref01-logo-refine-05-abstract-geometric.png", + "prompt": "Use the uploaded reference image as the composition and pose reference. Keep the same core idea: a ceramic jar container with a capybara-like small animal peeking out only to eye level.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the reference structure: the jar is the main brand shape; the animal shows only small rounded ears, the upper half-dome head, and two matte black dot eyes above the jar rim.\nDo not show nose, mouth, cheek blush, teeth, paws, body, lower face, or a full animal head. The eyes must be pure black dots with no highlights, no white sparkle, and no glossy pupils.\nThe jar itself must have no face, no expression, no eyes, no smile, and no decorative emoji marks.\nMake the result more like a trademark and logo than an illustration: simplified silhouette, clean vector curves, broad flat color fields, fewer gradients, fewer soft shadows, no realistic ceramic rendering.\nKeep a warm ceramic-container feeling through color and silhouette only. The jar should not look like a bowl of food, cup, dessert package, pudding cup, bread, bun, chocolate, candy, jelly, or pastry.\nStyle target: premium friendly mascot-symbol logo, all-age and women-friendly, calm but memorable, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, sticker outline, background scene, and watermark.\nVariant focus: higher abstraction. Reduce the capybara head to a clean half-dome with two round ears and two black dots; reduce the jar to a distinct pot silhouette with a single rim band. Very vector-ready." + }, + { + "id": "06-monochrome-first", + "title": "黑白优先", + "file": "taonier-capybara-jar-ref01-logo-refine-06-monochrome-first.png", + "prompt": "Use the uploaded reference image as the composition and pose reference. Keep the same core idea: a ceramic jar container with a capybara-like small animal peeking out only to eye level.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the reference structure: the jar is the main brand shape; the animal shows only small rounded ears, the upper half-dome head, and two matte black dot eyes above the jar rim.\nDo not show nose, mouth, cheek blush, teeth, paws, body, lower face, or a full animal head. The eyes must be pure black dots with no highlights, no white sparkle, and no glossy pupils.\nThe jar itself must have no face, no expression, no eyes, no smile, and no decorative emoji marks.\nMake the result more like a trademark and logo than an illustration: simplified silhouette, clean vector curves, broad flat color fields, fewer gradients, fewer soft shadows, no realistic ceramic rendering.\nKeep a warm ceramic-container feeling through color and silhouette only. The jar should not look like a bowl of food, cup, dessert package, pudding cup, bread, bun, chocolate, candy, jelly, or pastry.\nStyle target: premium friendly mascot-symbol logo, all-age and women-friendly, calm but memorable, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, sticker outline, background scene, and watermark.\nVariant focus: black-and-white survival first. Design with strong positive and negative shapes; color is secondary. Use warm clay and dark umber, but the mark must remain clear if converted to pure black and white." + }, + { + "id": "07-soft-gradient-logo", + "title": "轻渐变商标", + "file": "taonier-capybara-jar-ref01-logo-refine-07-soft-gradient-logo.png", + "prompt": "Use the uploaded reference image as the composition and pose reference. Keep the same core idea: a ceramic jar container with a capybara-like small animal peeking out only to eye level.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the reference structure: the jar is the main brand shape; the animal shows only small rounded ears, the upper half-dome head, and two matte black dot eyes above the jar rim.\nDo not show nose, mouth, cheek blush, teeth, paws, body, lower face, or a full animal head. The eyes must be pure black dots with no highlights, no white sparkle, and no glossy pupils.\nThe jar itself must have no face, no expression, no eyes, no smile, and no decorative emoji marks.\nMake the result more like a trademark and logo than an illustration: simplified silhouette, clean vector curves, broad flat color fields, fewer gradients, fewer soft shadows, no realistic ceramic rendering.\nKeep a warm ceramic-container feeling through color and silhouette only. The jar should not look like a bowl of food, cup, dessert package, pudding cup, bread, bun, chocolate, candy, jelly, or pastry.\nStyle target: premium friendly mascot-symbol logo, all-age and women-friendly, calm but memorable, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, sticker outline, background scene, and watermark.\nVariant focus: allow only a very subtle premium gradient on broad shapes, like a polished app logo. Keep it much flatter than the reference and remove painterly shadows or texture." + }, + { + "id": "08-bold-avatar", + "title": "头像强识别", + "file": "taonier-capybara-jar-ref01-logo-refine-08-bold-avatar.png", + "prompt": "Use the uploaded reference image as the composition and pose reference. Keep the same core idea: a ceramic jar container with a capybara-like small animal peeking out only to eye level.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the reference structure: the jar is the main brand shape; the animal shows only small rounded ears, the upper half-dome head, and two matte black dot eyes above the jar rim.\nDo not show nose, mouth, cheek blush, teeth, paws, body, lower face, or a full animal head. The eyes must be pure black dots with no highlights, no white sparkle, and no glossy pupils.\nThe jar itself must have no face, no expression, no eyes, no smile, and no decorative emoji marks.\nMake the result more like a trademark and logo than an illustration: simplified silhouette, clean vector curves, broad flat color fields, fewer gradients, fewer soft shadows, no realistic ceramic rendering.\nKeep a warm ceramic-container feeling through color and silhouette only. The jar should not look like a bowl of food, cup, dessert package, pudding cup, bread, bun, chocolate, candy, jelly, or pastry.\nStyle target: premium friendly mascot-symbol logo, all-age and women-friendly, calm but memorable, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, sticker outline, background scene, and watermark.\nVariant focus: compact social-avatar readability. Make the jar a fuller rounded vessel and enlarge the peeking capybara head slightly, while preserving the hidden half-head rhythm and black dot eyes." + } + ] +} diff --git a/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-creator-totem.png b/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-creator-totem.png new file mode 100644 index 00000000..99c54fc5 Binary files /dev/null and b/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-creator-totem.png differ diff --git a/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-figurine-token.png b/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-figurine-token.png new file mode 100644 index 00000000..6b340f6f Binary files /dev/null and b/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-figurine-token.png differ diff --git a/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-idol-mask.png b/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-idol-mask.png new file mode 100644 index 00000000..8f79f462 Binary files /dev/null and b/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-idol-mask.png differ diff --git a/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-little-maker.png b/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-little-maker.png new file mode 100644 index 00000000..889e2ba1 Binary files /dev/null and b/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-little-maker.png differ diff --git a/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-pocket-figure.png b/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-pocket-figure.png new file mode 100644 index 00000000..fdc93722 Binary files /dev/null and b/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-pocket-figure.png differ diff --git a/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-soft-doll.png b/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-soft-doll.png new file mode 100644 index 00000000..cd1dc043 Binary files /dev/null and b/public/branding/taonier-logo-clay-mascot-concepts/taonier-clay-mascot-soft-doll.png differ diff --git a/public/branding/taonier-logo-clay-mascot-concepts/taonier-logo-clay-mascot-contact-sheet.png b/public/branding/taonier-logo-clay-mascot-concepts/taonier-logo-clay-mascot-contact-sheet.png new file mode 100644 index 00000000..c5372711 Binary files /dev/null and b/public/branding/taonier-logo-clay-mascot-concepts/taonier-logo-clay-mascot-contact-sheet.png differ diff --git a/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-01-organic-closed-badge.png b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-01-organic-closed-badge.png new file mode 100644 index 00000000..c0c2efd3 Binary files /dev/null and b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-01-organic-closed-badge.png differ diff --git a/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-02-smooth-clay-shield.png b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-02-smooth-clay-shield.png new file mode 100644 index 00000000..f25bea89 Binary files /dev/null and b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-02-smooth-clay-shield.png differ diff --git a/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-03-asymmetric-pebble-glyph.png b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-03-asymmetric-pebble-glyph.png new file mode 100644 index 00000000..9a2fe06a Binary files /dev/null and b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-03-asymmetric-pebble-glyph.png differ diff --git a/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-04-inlaid-curve-plate.png b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-04-inlaid-curve-plate.png new file mode 100644 index 00000000..9337dc00 Binary files /dev/null and b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-04-inlaid-curve-plate.png differ diff --git a/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-05-flat-vector-symbol.png b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-05-flat-vector-symbol.png new file mode 100644 index 00000000..31a79304 Binary files /dev/null and b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-05-flat-vector-symbol.png differ diff --git a/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-06-friendly-solid-form.png b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-06-friendly-solid-form.png new file mode 100644 index 00000000..8e90a44e Binary files /dev/null and b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-06-friendly-solid-form.png differ diff --git a/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-07-digital-clay-panel.png b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-07-digital-clay-panel.png new file mode 100644 index 00000000..26c06170 Binary files /dev/null and b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-07-digital-clay-panel.png differ diff --git a/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-08-trademark-ready-contour.png b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-08-trademark-ready-contour.png new file mode 100644 index 00000000..2c250fe3 Binary files /dev/null and b/public/branding/taonier-logo-closed-geo-concepts/taonier-closed-geo-08-trademark-ready-contour.png differ diff --git a/public/branding/taonier-logo-closed-geo-concepts/taonier-logo-closed-geo-contact-sheet.png b/public/branding/taonier-logo-closed-geo-concepts/taonier-logo-closed-geo-contact-sheet.png new file mode 100644 index 00000000..e1f291be Binary files /dev/null and b/public/branding/taonier-logo-closed-geo-concepts/taonier-logo-closed-geo-contact-sheet.png differ diff --git a/public/branding/taonier-logo-closed-geo-concepts/taonier-logo-closed-geo-manifest.json b/public/branding/taonier-logo-closed-geo-concepts/taonier-logo-closed-geo-manifest.json new file mode 100644 index 00000000..a7ef4ecd --- /dev/null +++ b/public/branding/taonier-logo-closed-geo-concepts/taonier-logo-closed-geo-manifest.json @@ -0,0 +1,61 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T18:57:38.302Z", + "creativeDirection": { + "name": "陶泥儿闭合不规则几何底盘图形标", + "textPolicy": "no Chinese, no English, no wordmark", + "correction": "closed irregular smooth geometry, not free ribbon, not food, not square base", + "motif": "闭合曲线几何底盘 + 内部曲线分区 + 轻陶泥温度" + }, + "variants": [ + { + "id": "01-organic-closed-badge", + "title": "有机闭合徽形", + "file": "taonier-closed-geo-01-organic-closed-badge.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次的核心要求:底盘必须是由流畅曲线组成的「闭合不规则几何体图案」。它要像一个完整的品牌徽形轮廓,而不是自由飘带、旋涡、S 形、G 形、开放环或散开的泥条。\n外轮廓:闭合、连续、平滑、非方形、非圆形、非椭圆、非圆角方块。可以有 5 到 7 个柔和转折点,像自然捏出来的抽象几何石/软陶徽形,但必须干净、可矢量化。\n内部结构:可以用 1 到 2 条平滑曲线切分色块或形成负形,但内部结构必须服务整体闭合底盘,不能出现中心星星、中心洞、脸、眼睛、嘴巴或角色感。\n识别方向:通过闭合外轮廓和内部曲线分区形成记忆点,不靠星形、不靠文字、不靠食物质感。远看要像互联网产品 logo。\n材质与风格:现代扁平矢量商标,极轻微陶泥温度;低高光、低拟物、干净边缘。不要 3D 面包感,不要巧克力、面团、糕点、糖果、饼干或烘焙质感。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主体;允许少量低饱和孔雀青、靛蓝灰或深泥灰作为内部曲线色块,占比 6% 到 15%。整体要温暖、有吸引力,但不甜、不像食品。\n构图:正方形画布,居中图形标,图形占画面约 68% 到 76%,干净浅色背景,留足安全边距。缩小到 64px 时仍能看出闭合外轮廓和内部曲线分区。\n强禁止:自由飘带、开口环、旋涡、S 形、G 形、云朵、面包、巧克力面包、可颂、甜甜圈、糖果、饼干、糕点、奶油、夹心、亮油高光、烘焙纹理、食物摄影感。\n强禁止:整体方形底、圆角方块、中心星星、任何星形、闪光、徽章文字、印章文字、脸、表情、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:最基础的闭合不规则几何体。暖陶白主体,陶土橙内部曲线切分,外轮廓像柔和抽象鹅卵石但不是圆形。" + }, + { + "id": "02-smooth-clay-shield", + "title": "柔曲陶盾", + "file": "taonier-closed-geo-02-smooth-clay-shield.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次的核心要求:底盘必须是由流畅曲线组成的「闭合不规则几何体图案」。它要像一个完整的品牌徽形轮廓,而不是自由飘带、旋涡、S 形、G 形、开放环或散开的泥条。\n外轮廓:闭合、连续、平滑、非方形、非圆形、非椭圆、非圆角方块。可以有 5 到 7 个柔和转折点,像自然捏出来的抽象几何石/软陶徽形,但必须干净、可矢量化。\n内部结构:可以用 1 到 2 条平滑曲线切分色块或形成负形,但内部结构必须服务整体闭合底盘,不能出现中心星星、中心洞、脸、眼睛、嘴巴或角色感。\n识别方向:通过闭合外轮廓和内部曲线分区形成记忆点,不靠星形、不靠文字、不靠食物质感。远看要像互联网产品 logo。\n材质与风格:现代扁平矢量商标,极轻微陶泥温度;低高光、低拟物、干净边缘。不要 3D 面包感,不要巧克力、面团、糕点、糖果、饼干或烘焙质感。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主体;允许少量低饱和孔雀青、靛蓝灰或深泥灰作为内部曲线色块,占比 6% 到 15%。整体要温暖、有吸引力,但不甜、不像食品。\n构图:正方形画布,居中图形标,图形占画面约 68% 到 76%,干净浅色背景,留足安全边距。缩小到 64px 时仍能看出闭合外轮廓和内部曲线分区。\n强禁止:自由飘带、开口环、旋涡、S 形、G 形、云朵、面包、巧克力面包、可颂、甜甜圈、糖果、饼干、糕点、奶油、夹心、亮油高光、烘焙纹理、食物摄影感。\n强禁止:整体方形底、圆角方块、中心星星、任何星形、闪光、徽章文字、印章文字、脸、表情、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:更稳定的品牌底盘。外轮廓略像柔化盾形或种子形,但没有尖角;内部一条孔雀青曲线增加识别度。" + }, + { + "id": "03-asymmetric-pebble-glyph", + "title": "非对称陶符", + "file": "taonier-closed-geo-03-asymmetric-pebble-glyph.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次的核心要求:底盘必须是由流畅曲线组成的「闭合不规则几何体图案」。它要像一个完整的品牌徽形轮廓,而不是自由飘带、旋涡、S 形、G 形、开放环或散开的泥条。\n外轮廓:闭合、连续、平滑、非方形、非圆形、非椭圆、非圆角方块。可以有 5 到 7 个柔和转折点,像自然捏出来的抽象几何石/软陶徽形,但必须干净、可矢量化。\n内部结构:可以用 1 到 2 条平滑曲线切分色块或形成负形,但内部结构必须服务整体闭合底盘,不能出现中心星星、中心洞、脸、眼睛、嘴巴或角色感。\n识别方向:通过闭合外轮廓和内部曲线分区形成记忆点,不靠星形、不靠文字、不靠食物质感。远看要像互联网产品 logo。\n材质与风格:现代扁平矢量商标,极轻微陶泥温度;低高光、低拟物、干净边缘。不要 3D 面包感,不要巧克力、面团、糕点、糖果、饼干或烘焙质感。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主体;允许少量低饱和孔雀青、靛蓝灰或深泥灰作为内部曲线色块,占比 6% 到 15%。整体要温暖、有吸引力,但不甜、不像食品。\n构图:正方形画布,居中图形标,图形占画面约 68% 到 76%,干净浅色背景,留足安全边距。缩小到 64px 时仍能看出闭合外轮廓和内部曲线分区。\n强禁止:自由飘带、开口环、旋涡、S 形、G 形、云朵、面包、巧克力面包、可颂、甜甜圈、糖果、饼干、糕点、奶油、夹心、亮油高光、烘焙纹理、食物摄影感。\n强禁止:整体方形底、圆角方块、中心星星、任何星形、闪光、徽章文字、印章文字、脸、表情、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:非对称但平衡。闭合外轮廓左右不一样,有 6 个柔和转折点,内部用深泥灰负形曲线形成品牌记忆。" + }, + { + "id": "04-inlaid-curve-plate", + "title": "嵌曲陶牌", + "file": "taonier-closed-geo-04-inlaid-curve-plate.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次的核心要求:底盘必须是由流畅曲线组成的「闭合不规则几何体图案」。它要像一个完整的品牌徽形轮廓,而不是自由飘带、旋涡、S 形、G 形、开放环或散开的泥条。\n外轮廓:闭合、连续、平滑、非方形、非圆形、非椭圆、非圆角方块。可以有 5 到 7 个柔和转折点,像自然捏出来的抽象几何石/软陶徽形,但必须干净、可矢量化。\n内部结构:可以用 1 到 2 条平滑曲线切分色块或形成负形,但内部结构必须服务整体闭合底盘,不能出现中心星星、中心洞、脸、眼睛、嘴巴或角色感。\n识别方向:通过闭合外轮廓和内部曲线分区形成记忆点,不靠星形、不靠文字、不靠食物质感。远看要像互联网产品 logo。\n材质与风格:现代扁平矢量商标,极轻微陶泥温度;低高光、低拟物、干净边缘。不要 3D 面包感,不要巧克力、面团、糕点、糖果、饼干或烘焙质感。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主体;允许少量低饱和孔雀青、靛蓝灰或深泥灰作为内部曲线色块,占比 6% 到 15%。整体要温暖、有吸引力,但不甜、不像食品。\n构图:正方形画布,居中图形标,图形占画面约 68% 到 76%,干净浅色背景,留足安全边距。缩小到 64px 时仍能看出闭合外轮廓和内部曲线分区。\n强禁止:自由飘带、开口环、旋涡、S 形、G 形、云朵、面包、巧克力面包、可颂、甜甜圈、糖果、饼干、糕点、奶油、夹心、亮油高光、烘焙纹理、食物摄影感。\n强禁止:整体方形底、圆角方块、中心星星、任何星形、闪光、徽章文字、印章文字、脸、表情、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:内部嵌色曲线。像一块闭合陶牌上嵌入一条平滑色带,色带不能像馅料、巧克力或奶油夹心。" + }, + { + "id": "05-flat-vector-symbol", + "title": "扁平矢量符", + "file": "taonier-closed-geo-05-flat-vector-symbol.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次的核心要求:底盘必须是由流畅曲线组成的「闭合不规则几何体图案」。它要像一个完整的品牌徽形轮廓,而不是自由飘带、旋涡、S 形、G 形、开放环或散开的泥条。\n外轮廓:闭合、连续、平滑、非方形、非圆形、非椭圆、非圆角方块。可以有 5 到 7 个柔和转折点,像自然捏出来的抽象几何石/软陶徽形,但必须干净、可矢量化。\n内部结构:可以用 1 到 2 条平滑曲线切分色块或形成负形,但内部结构必须服务整体闭合底盘,不能出现中心星星、中心洞、脸、眼睛、嘴巴或角色感。\n识别方向:通过闭合外轮廓和内部曲线分区形成记忆点,不靠星形、不靠文字、不靠食物质感。远看要像互联网产品 logo。\n材质与风格:现代扁平矢量商标,极轻微陶泥温度;低高光、低拟物、干净边缘。不要 3D 面包感,不要巧克力、面团、糕点、糖果、饼干或烘焙质感。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主体;允许少量低饱和孔雀青、靛蓝灰或深泥灰作为内部曲线色块,占比 6% 到 15%。整体要温暖、有吸引力,但不甜、不像食品。\n构图:正方形画布,居中图形标,图形占画面约 68% 到 76%,干净浅色背景,留足安全边距。缩小到 64px 时仍能看出闭合外轮廓和内部曲线分区。\n强禁止:自由飘带、开口环、旋涡、S 形、G 形、云朵、面包、巧克力面包、可颂、甜甜圈、糖果、饼干、糕点、奶油、夹心、亮油高光、烘焙纹理、食物摄影感。\n强禁止:整体方形底、圆角方块、中心星星、任何星形、闪光、徽章文字、印章文字、脸、表情、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:最扁平、最商标化。减少材质,只用 2 到 3 个大色块形成闭合不规则几何符号,线条极简。" + }, + { + "id": "06-friendly-solid-form", + "title": "亲和实体形", + "file": "taonier-closed-geo-06-friendly-solid-form.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次的核心要求:底盘必须是由流畅曲线组成的「闭合不规则几何体图案」。它要像一个完整的品牌徽形轮廓,而不是自由飘带、旋涡、S 形、G 形、开放环或散开的泥条。\n外轮廓:闭合、连续、平滑、非方形、非圆形、非椭圆、非圆角方块。可以有 5 到 7 个柔和转折点,像自然捏出来的抽象几何石/软陶徽形,但必须干净、可矢量化。\n内部结构:可以用 1 到 2 条平滑曲线切分色块或形成负形,但内部结构必须服务整体闭合底盘,不能出现中心星星、中心洞、脸、眼睛、嘴巴或角色感。\n识别方向:通过闭合外轮廓和内部曲线分区形成记忆点,不靠星形、不靠文字、不靠食物质感。远看要像互联网产品 logo。\n材质与风格:现代扁平矢量商标,极轻微陶泥温度;低高光、低拟物、干净边缘。不要 3D 面包感,不要巧克力、面团、糕点、糖果、饼干或烘焙质感。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主体;允许少量低饱和孔雀青、靛蓝灰或深泥灰作为内部曲线色块,占比 6% 到 15%。整体要温暖、有吸引力,但不甜、不像食品。\n构图:正方形画布,居中图形标,图形占画面约 68% 到 76%,干净浅色背景,留足安全边距。缩小到 64px 时仍能看出闭合外轮廓和内部曲线分区。\n强禁止:自由飘带、开口环、旋涡、S 形、G 形、云朵、面包、巧克力面包、可颂、甜甜圈、糖果、饼干、糕点、奶油、夹心、亮油高光、烘焙纹理、食物摄影感。\n强禁止:整体方形底、圆角方块、中心星星、任何星形、闪光、徽章文字、印章文字、脸、表情、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:亲和力。闭合底盘像一个柔软、温和、完整的小世界,但不是角色、不是脸、不是食物。" + }, + { + "id": "07-digital-clay-panel", + "title": "数字陶泥面", + "file": "taonier-closed-geo-07-digital-clay-panel.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次的核心要求:底盘必须是由流畅曲线组成的「闭合不规则几何体图案」。它要像一个完整的品牌徽形轮廓,而不是自由飘带、旋涡、S 形、G 形、开放环或散开的泥条。\n外轮廓:闭合、连续、平滑、非方形、非圆形、非椭圆、非圆角方块。可以有 5 到 7 个柔和转折点,像自然捏出来的抽象几何石/软陶徽形,但必须干净、可矢量化。\n内部结构:可以用 1 到 2 条平滑曲线切分色块或形成负形,但内部结构必须服务整体闭合底盘,不能出现中心星星、中心洞、脸、眼睛、嘴巴或角色感。\n识别方向:通过闭合外轮廓和内部曲线分区形成记忆点,不靠星形、不靠文字、不靠食物质感。远看要像互联网产品 logo。\n材质与风格:现代扁平矢量商标,极轻微陶泥温度;低高光、低拟物、干净边缘。不要 3D 面包感,不要巧克力、面团、糕点、糖果、饼干或烘焙质感。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主体;允许少量低饱和孔雀青、靛蓝灰或深泥灰作为内部曲线色块,占比 6% 到 15%。整体要温暖、有吸引力,但不甜、不像食品。\n构图:正方形画布,居中图形标,图形占画面约 68% 到 76%,干净浅色背景,留足安全边距。缩小到 64px 时仍能看出闭合外轮廓和内部曲线分区。\n强禁止:自由飘带、开口环、旋涡、S 形、G 形、云朵、面包、巧克力面包、可颂、甜甜圈、糖果、饼干、糕点、奶油、夹心、亮油高光、烘焙纹理、食物摄影感。\n强禁止:整体方形底、圆角方块、中心星星、任何星形、闪光、徽章文字、印章文字、脸、表情、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:AI/UGC 暗示。闭合底盘内有 2 到 3 个很小的几何刻点或短曲线节点,但不能像电路板,也不能像撒糖。" + }, + { + "id": "08-trademark-ready-contour", + "title": "商标轮廓款", + "file": "taonier-closed-geo-08-trademark-ready-contour.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次的核心要求:底盘必须是由流畅曲线组成的「闭合不规则几何体图案」。它要像一个完整的品牌徽形轮廓,而不是自由飘带、旋涡、S 形、G 形、开放环或散开的泥条。\n外轮廓:闭合、连续、平滑、非方形、非圆形、非椭圆、非圆角方块。可以有 5 到 7 个柔和转折点,像自然捏出来的抽象几何石/软陶徽形,但必须干净、可矢量化。\n内部结构:可以用 1 到 2 条平滑曲线切分色块或形成负形,但内部结构必须服务整体闭合底盘,不能出现中心星星、中心洞、脸、眼睛、嘴巴或角色感。\n识别方向:通过闭合外轮廓和内部曲线分区形成记忆点,不靠星形、不靠文字、不靠食物质感。远看要像互联网产品 logo。\n材质与风格:现代扁平矢量商标,极轻微陶泥温度;低高光、低拟物、干净边缘。不要 3D 面包感,不要巧克力、面团、糕点、糖果、饼干或烘焙质感。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主体;允许少量低饱和孔雀青、靛蓝灰或深泥灰作为内部曲线色块,占比 6% 到 15%。整体要温暖、有吸引力,但不甜、不像食品。\n构图:正方形画布,居中图形标,图形占画面约 68% 到 76%,干净浅色背景,留足安全边距。缩小到 64px 时仍能看出闭合外轮廓和内部曲线分区。\n强禁止:自由飘带、开口环、旋涡、S 形、G 形、云朵、面包、巧克力面包、可颂、甜甜圈、糖果、饼干、糕点、奶油、夹心、亮油高光、烘焙纹理、食物摄影感。\n强禁止:整体方形底、圆角方块、中心星星、任何星形、闪光、徽章文字、印章文字、脸、表情、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:可注册轮廓。优先保证黑白化后的闭合外轮廓和内部曲线仍有辨识度,避免渐变和复杂纹理。" + } + ] +} diff --git a/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-01-teal-core-pop.png b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-01-teal-core-pop.png new file mode 100644 index 00000000..963d4d1a Binary files /dev/null and b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-01-teal-core-pop.png differ diff --git a/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-02-indigo-cut-mark.png b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-02-indigo-cut-mark.png new file mode 100644 index 00000000..a6a10d20 Binary files /dev/null and b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-02-indigo-cut-mark.png differ diff --git a/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-03-cinnabar-clay-spark.png b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-03-cinnabar-clay-spark.png new file mode 100644 index 00000000..3ca851d9 Binary files /dev/null and b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-03-cinnabar-clay-spark.png differ diff --git a/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-04-bold-outline-token.png b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-04-bold-outline-token.png new file mode 100644 index 00000000..a6657d8a Binary files /dev/null and b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-04-bold-outline-token.png differ diff --git a/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-05-clay-pixel-seed.png b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-05-clay-pixel-seed.png new file mode 100644 index 00000000..0f952fe9 Binary files /dev/null and b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-05-clay-pixel-seed.png differ diff --git a/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-06-dynamic-squircle.png b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-06-dynamic-squircle.png new file mode 100644 index 00000000..a607f183 Binary files /dev/null and b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-06-dynamic-squircle.png differ diff --git a/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-07-app-store-icon.png b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-07-app-store-icon.png new file mode 100644 index 00000000..b770f607 Binary files /dev/null and b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-07-app-store-icon.png differ diff --git a/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-08-trademark-flat-glyph.png b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-08-trademark-flat-glyph.png new file mode 100644 index 00000000..29b08dac Binary files /dev/null and b/public/branding/taonier-logo-distinctive-concepts/taonier-distinctive-08-trademark-flat-glyph.png differ diff --git a/public/branding/taonier-logo-distinctive-concepts/taonier-logo-distinctive-contact-sheet.png b/public/branding/taonier-logo-distinctive-concepts/taonier-logo-distinctive-contact-sheet.png new file mode 100644 index 00000000..65a8389f Binary files /dev/null and b/public/branding/taonier-logo-distinctive-concepts/taonier-logo-distinctive-contact-sheet.png differ diff --git a/public/branding/taonier-logo-distinctive-concepts/taonier-logo-distinctive-manifest.json b/public/branding/taonier-logo-distinctive-concepts/taonier-logo-distinctive-manifest.json new file mode 100644 index 00000000..53148e50 --- /dev/null +++ b/public/branding/taonier-logo-distinctive-concepts/taonier-logo-distinctive-manifest.json @@ -0,0 +1,62 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T18:18:06.003Z", + "creativeDirection": { + "name": "陶泥儿高辨识可塑星核图形标", + "textPolicy": "no Chinese, no English, no wordmark", + "palette": "暖陶白/浅陶土主体 + 8%-18% 孔雀青、靛蓝灰、朱砂橙或暗金土黄点缀", + "motif": "强轮廓软方圆 + 独特可塑星核 + 少量 AI/UGC 刻点", + "correction": "avoid candy, avoid brick, avoid plain mud stamp, increase brand recognition" + }, + "variants": [ + { + "id": "01-teal-core-pop", + "title": "孔雀青星核", + "file": "taonier-distinctive-01-teal-core-pop.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次目标是高辨识度和传播吸引力:第一眼要像一个有记忆点的互联网产品 logo,而不是普通泥块、砖块、陶片、印章、饼干或糖果。\n核心隐喻:可塑星核。一个独特的「软方圆陶泥外轮廓 + 中心可塑星核」符号,星核像脑洞被捏出的一瞬间,造型要有专属轮廓,不能是普通五角星或普通四角闪光。\n风格:现代扁平矢量商标,带轻微哑光陶泥质感;边缘干净、对比清楚、轮廓强,适合 App 图标、社区头像、启动页和商标注册前期筛选。\n材质:要能看出陶泥温度,但不要过度粗糙、不要砖、不要土块、不要考古泥片。质感只做轻量表面颗粒和柔和压痕。\n配色:以暖陶白或浅陶土色为主体,加入一个高识别点缀色。允许使用低饱和孔雀青、靛蓝灰、朱砂橙、暗金土黄中的一种;点缀色只占 8% 到 18%。整体要更醒目、更年轻,但不要糖果色。\n图案:中心星核必须是品牌记忆点,可用负形、嵌色、切口、泥痕、像素刻点形成独特轮廓;周围最多 2 到 3 个小刻点暗示 AI/UGC 扩散。\n构图:正方形画布,居中图形标,图形占画面约 70% 到 78%,干净浅色背景,留足安全边距。缩小到 64px 时仍然能认出外轮廓和中心符号。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标。\n强禁止低辨识:不要普通圆泥饼、不要普通砖块、不要石头、不要考古印章、不要单纯凹星孔、不要灰扑扑单色泥片。\n强禁止食品感:不要糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力、果冻高光、亮金膨胀星糖。\n本张重点:孔雀青识别点。暖陶白软方圆主体,中间是孔雀青负形可塑星核,少量陶土褐压痕,整体清爽年轻。" + }, + { + "id": "02-indigo-cut-mark", + "title": "靛蓝切口", + "file": "taonier-distinctive-02-indigo-cut-mark.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次目标是高辨识度和传播吸引力:第一眼要像一个有记忆点的互联网产品 logo,而不是普通泥块、砖块、陶片、印章、饼干或糖果。\n核心隐喻:可塑星核。一个独特的「软方圆陶泥外轮廓 + 中心可塑星核」符号,星核像脑洞被捏出的一瞬间,造型要有专属轮廓,不能是普通五角星或普通四角闪光。\n风格:现代扁平矢量商标,带轻微哑光陶泥质感;边缘干净、对比清楚、轮廓强,适合 App 图标、社区头像、启动页和商标注册前期筛选。\n材质:要能看出陶泥温度,但不要过度粗糙、不要砖、不要土块、不要考古泥片。质感只做轻量表面颗粒和柔和压痕。\n配色:以暖陶白或浅陶土色为主体,加入一个高识别点缀色。允许使用低饱和孔雀青、靛蓝灰、朱砂橙、暗金土黄中的一种;点缀色只占 8% 到 18%。整体要更醒目、更年轻,但不要糖果色。\n图案:中心星核必须是品牌记忆点,可用负形、嵌色、切口、泥痕、像素刻点形成独特轮廓;周围最多 2 到 3 个小刻点暗示 AI/UGC 扩散。\n构图:正方形画布,居中图形标,图形占画面约 70% 到 78%,干净浅色背景,留足安全边距。缩小到 64px 时仍然能认出外轮廓和中心符号。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标。\n强禁止低辨识:不要普通圆泥饼、不要普通砖块、不要石头、不要考古印章、不要单纯凹星孔、不要灰扑扑单色泥片。\n强禁止食品感:不要糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力、果冻高光、亮金膨胀星糖。\n本张重点:靛蓝灰切口。用一条干净的靛蓝灰泥痕切出中心星核,让图形远看有强剪影,不能像旋涡旧稿。" + }, + { + "id": "03-cinnabar-clay-spark", + "title": "朱砂陶火", + "file": "taonier-distinctive-03-cinnabar-clay-spark.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次目标是高辨识度和传播吸引力:第一眼要像一个有记忆点的互联网产品 logo,而不是普通泥块、砖块、陶片、印章、饼干或糖果。\n核心隐喻:可塑星核。一个独特的「软方圆陶泥外轮廓 + 中心可塑星核」符号,星核像脑洞被捏出的一瞬间,造型要有专属轮廓,不能是普通五角星或普通四角闪光。\n风格:现代扁平矢量商标,带轻微哑光陶泥质感;边缘干净、对比清楚、轮廓强,适合 App 图标、社区头像、启动页和商标注册前期筛选。\n材质:要能看出陶泥温度,但不要过度粗糙、不要砖、不要土块、不要考古泥片。质感只做轻量表面颗粒和柔和压痕。\n配色:以暖陶白或浅陶土色为主体,加入一个高识别点缀色。允许使用低饱和孔雀青、靛蓝灰、朱砂橙、暗金土黄中的一种;点缀色只占 8% 到 18%。整体要更醒目、更年轻,但不要糖果色。\n图案:中心星核必须是品牌记忆点,可用负形、嵌色、切口、泥痕、像素刻点形成独特轮廓;周围最多 2 到 3 个小刻点暗示 AI/UGC 扩散。\n构图:正方形画布,居中图形标,图形占画面约 70% 到 78%,干净浅色背景,留足安全边距。缩小到 64px 时仍然能认出外轮廓和中心符号。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标。\n强禁止低辨识:不要普通圆泥饼、不要普通砖块、不要石头、不要考古印章、不要单纯凹星孔、不要灰扑扑单色泥片。\n强禁止食品感:不要糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力、果冻高光、亮金膨胀星糖。\n本张重点:朱砂橙活力。中心星核或侧边小泥片使用低饱和朱砂橙,像创作火花,但整体保持陶泥材质和精品克制。" + }, + { + "id": "04-bold-outline-token", + "title": "强轮廓泥符", + "file": "taonier-distinctive-04-bold-outline-token.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次目标是高辨识度和传播吸引力:第一眼要像一个有记忆点的互联网产品 logo,而不是普通泥块、砖块、陶片、印章、饼干或糖果。\n核心隐喻:可塑星核。一个独特的「软方圆陶泥外轮廓 + 中心可塑星核」符号,星核像脑洞被捏出的一瞬间,造型要有专属轮廓,不能是普通五角星或普通四角闪光。\n风格:现代扁平矢量商标,带轻微哑光陶泥质感;边缘干净、对比清楚、轮廓强,适合 App 图标、社区头像、启动页和商标注册前期筛选。\n材质:要能看出陶泥温度,但不要过度粗糙、不要砖、不要土块、不要考古泥片。质感只做轻量表面颗粒和柔和压痕。\n配色:以暖陶白或浅陶土色为主体,加入一个高识别点缀色。允许使用低饱和孔雀青、靛蓝灰、朱砂橙、暗金土黄中的一种;点缀色只占 8% 到 18%。整体要更醒目、更年轻,但不要糖果色。\n图案:中心星核必须是品牌记忆点,可用负形、嵌色、切口、泥痕、像素刻点形成独特轮廓;周围最多 2 到 3 个小刻点暗示 AI/UGC 扩散。\n构图:正方形画布,居中图形标,图形占画面约 70% 到 78%,干净浅色背景,留足安全边距。缩小到 64px 时仍然能认出外轮廓和中心符号。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标。\n强禁止低辨识:不要普通圆泥饼、不要普通砖块、不要石头、不要考古印章、不要单纯凹星孔、不要灰扑扑单色泥片。\n强禁止食品感:不要糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力、果冻高光、亮金膨胀星糖。\n本张重点:强轮廓。用深泥灰细描边或深色负形强化外轮廓和中心符号,确保黑白化后仍有高辨识度。" + }, + { + "id": "05-clay-pixel-seed", + "title": "像素创作种", + "file": "taonier-distinctive-05-clay-pixel-seed.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次目标是高辨识度和传播吸引力:第一眼要像一个有记忆点的互联网产品 logo,而不是普通泥块、砖块、陶片、印章、饼干或糖果。\n核心隐喻:可塑星核。一个独特的「软方圆陶泥外轮廓 + 中心可塑星核」符号,星核像脑洞被捏出的一瞬间,造型要有专属轮廓,不能是普通五角星或普通四角闪光。\n风格:现代扁平矢量商标,带轻微哑光陶泥质感;边缘干净、对比清楚、轮廓强,适合 App 图标、社区头像、启动页和商标注册前期筛选。\n材质:要能看出陶泥温度,但不要过度粗糙、不要砖、不要土块、不要考古泥片。质感只做轻量表面颗粒和柔和压痕。\n配色:以暖陶白或浅陶土色为主体,加入一个高识别点缀色。允许使用低饱和孔雀青、靛蓝灰、朱砂橙、暗金土黄中的一种;点缀色只占 8% 到 18%。整体要更醒目、更年轻,但不要糖果色。\n图案:中心星核必须是品牌记忆点,可用负形、嵌色、切口、泥痕、像素刻点形成独特轮廓;周围最多 2 到 3 个小刻点暗示 AI/UGC 扩散。\n构图:正方形画布,居中图形标,图形占画面约 70% 到 78%,干净浅色背景,留足安全边距。缩小到 64px 时仍然能认出外轮廓和中心符号。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标。\n强禁止低辨识:不要普通圆泥饼、不要普通砖块、不要石头、不要考古印章、不要单纯凹星孔、不要灰扑扑单色泥片。\n强禁止食品感:不要糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力、果冻高光、亮金膨胀星糖。\n本张重点:AI/UGC 暗示。中心星核周围有 3 个小方形刻点,像生成像素从陶泥里浮现,但不要复杂电路线。" + }, + { + "id": "06-dynamic-squircle", + "title": "动态软方圆", + "file": "taonier-distinctive-06-dynamic-squircle.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次目标是高辨识度和传播吸引力:第一眼要像一个有记忆点的互联网产品 logo,而不是普通泥块、砖块、陶片、印章、饼干或糖果。\n核心隐喻:可塑星核。一个独特的「软方圆陶泥外轮廓 + 中心可塑星核」符号,星核像脑洞被捏出的一瞬间,造型要有专属轮廓,不能是普通五角星或普通四角闪光。\n风格:现代扁平矢量商标,带轻微哑光陶泥质感;边缘干净、对比清楚、轮廓强,适合 App 图标、社区头像、启动页和商标注册前期筛选。\n材质:要能看出陶泥温度,但不要过度粗糙、不要砖、不要土块、不要考古泥片。质感只做轻量表面颗粒和柔和压痕。\n配色:以暖陶白或浅陶土色为主体,加入一个高识别点缀色。允许使用低饱和孔雀青、靛蓝灰、朱砂橙、暗金土黄中的一种;点缀色只占 8% 到 18%。整体要更醒目、更年轻,但不要糖果色。\n图案:中心星核必须是品牌记忆点,可用负形、嵌色、切口、泥痕、像素刻点形成独特轮廓;周围最多 2 到 3 个小刻点暗示 AI/UGC 扩散。\n构图:正方形画布,居中图形标,图形占画面约 70% 到 78%,干净浅色背景,留足安全边距。缩小到 64px 时仍然能认出外轮廓和中心符号。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标。\n强禁止低辨识:不要普通圆泥饼、不要普通砖块、不要石头、不要考古印章、不要单纯凹星孔、不要灰扑扑单色泥片。\n强禁止食品感:不要糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力、果冻高光、亮金膨胀星糖。\n本张重点:动态轮廓。外形不是静态泥块,而像正在被捏动的软方圆,有一个明显但简洁的非对称记忆点。" + }, + { + "id": "07-app-store-icon", + "title": "应用图标款", + "file": "taonier-distinctive-07-app-store-icon.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次目标是高辨识度和传播吸引力:第一眼要像一个有记忆点的互联网产品 logo,而不是普通泥块、砖块、陶片、印章、饼干或糖果。\n核心隐喻:可塑星核。一个独特的「软方圆陶泥外轮廓 + 中心可塑星核」符号,星核像脑洞被捏出的一瞬间,造型要有专属轮廓,不能是普通五角星或普通四角闪光。\n风格:现代扁平矢量商标,带轻微哑光陶泥质感;边缘干净、对比清楚、轮廓强,适合 App 图标、社区头像、启动页和商标注册前期筛选。\n材质:要能看出陶泥温度,但不要过度粗糙、不要砖、不要土块、不要考古泥片。质感只做轻量表面颗粒和柔和压痕。\n配色:以暖陶白或浅陶土色为主体,加入一个高识别点缀色。允许使用低饱和孔雀青、靛蓝灰、朱砂橙、暗金土黄中的一种;点缀色只占 8% 到 18%。整体要更醒目、更年轻,但不要糖果色。\n图案:中心星核必须是品牌记忆点,可用负形、嵌色、切口、泥痕、像素刻点形成独特轮廓;周围最多 2 到 3 个小刻点暗示 AI/UGC 扩散。\n构图:正方形画布,居中图形标,图形占画面约 70% 到 78%,干净浅色背景,留足安全边距。缩小到 64px 时仍然能认出外轮廓和中心符号。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标。\n强禁止低辨识:不要普通圆泥饼、不要普通砖块、不要石头、不要考古印章、不要单纯凹星孔、不要灰扑扑单色泥片。\n强禁止食品感:不要糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力、果冻高光、亮金膨胀星糖。\n本张重点:App Store 图标。构图饱满、中心符号强、背景干净,视觉冲击比泥章更强,但不出现文字和脸。" + }, + { + "id": "08-trademark-flat-glyph", + "title": "商标扁平符", + "file": "taonier-distinctive-08-trademark-flat-glyph.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次目标是高辨识度和传播吸引力:第一眼要像一个有记忆点的互联网产品 logo,而不是普通泥块、砖块、陶片、印章、饼干或糖果。\n核心隐喻:可塑星核。一个独特的「软方圆陶泥外轮廓 + 中心可塑星核」符号,星核像脑洞被捏出的一瞬间,造型要有专属轮廓,不能是普通五角星或普通四角闪光。\n风格:现代扁平矢量商标,带轻微哑光陶泥质感;边缘干净、对比清楚、轮廓强,适合 App 图标、社区头像、启动页和商标注册前期筛选。\n材质:要能看出陶泥温度,但不要过度粗糙、不要砖、不要土块、不要考古泥片。质感只做轻量表面颗粒和柔和压痕。\n配色:以暖陶白或浅陶土色为主体,加入一个高识别点缀色。允许使用低饱和孔雀青、靛蓝灰、朱砂橙、暗金土黄中的一种;点缀色只占 8% 到 18%。整体要更醒目、更年轻,但不要糖果色。\n图案:中心星核必须是品牌记忆点,可用负形、嵌色、切口、泥痕、像素刻点形成独特轮廓;周围最多 2 到 3 个小刻点暗示 AI/UGC 扩散。\n构图:正方形画布,居中图形标,图形占画面约 70% 到 78%,干净浅色背景,留足安全边距。缩小到 64px 时仍然能认出外轮廓和中心符号。\n禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标。\n强禁止低辨识:不要普通圆泥饼、不要普通砖块、不要石头、不要考古印章、不要单纯凹星孔、不要灰扑扑单色泥片。\n强禁止食品感:不要糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力、果冻高光、亮金膨胀星糖。\n本张重点:最终商标潜力。减少材质和阴影,以 2 到 3 个大色块形成独特符号,保留陶泥可塑感和中心可塑星核。" + } + ] +} diff --git a/public/branding/taonier-logo-flow-concepts/taonier-flow-01-soft-ribbon-loop.png b/public/branding/taonier-logo-flow-concepts/taonier-flow-01-soft-ribbon-loop.png new file mode 100644 index 00000000..d06a853e Binary files /dev/null and b/public/branding/taonier-logo-flow-concepts/taonier-flow-01-soft-ribbon-loop.png differ diff --git a/public/branding/taonier-logo-flow-concepts/taonier-flow-02-clay-wave-knot.png b/public/branding/taonier-logo-flow-concepts/taonier-flow-02-clay-wave-knot.png new file mode 100644 index 00000000..61258708 Binary files /dev/null and b/public/branding/taonier-logo-flow-concepts/taonier-flow-02-clay-wave-knot.png differ diff --git a/public/branding/taonier-logo-flow-concepts/taonier-flow-03-imagination-ripple.png b/public/branding/taonier-logo-flow-concepts/taonier-flow-03-imagination-ripple.png new file mode 100644 index 00000000..230b1387 Binary files /dev/null and b/public/branding/taonier-logo-flow-concepts/taonier-flow-03-imagination-ripple.png differ diff --git a/public/branding/taonier-logo-flow-concepts/taonier-flow-04-friendly-clay-comet.png b/public/branding/taonier-logo-flow-concepts/taonier-flow-04-friendly-clay-comet.png new file mode 100644 index 00000000..ae505519 Binary files /dev/null and b/public/branding/taonier-logo-flow-concepts/taonier-flow-04-friendly-clay-comet.png differ diff --git a/public/branding/taonier-logo-flow-concepts/taonier-flow-05-single-stroke-blob.png b/public/branding/taonier-logo-flow-concepts/taonier-flow-05-single-stroke-blob.png new file mode 100644 index 00000000..a111d64d Binary files /dev/null and b/public/branding/taonier-logo-flow-concepts/taonier-flow-05-single-stroke-blob.png differ diff --git a/public/branding/taonier-logo-flow-concepts/taonier-flow-06-two-tone-soft-flow.png b/public/branding/taonier-logo-flow-concepts/taonier-flow-06-two-tone-soft-flow.png new file mode 100644 index 00000000..fb60deeb Binary files /dev/null and b/public/branding/taonier-logo-flow-concepts/taonier-flow-06-two-tone-soft-flow.png differ diff --git a/public/branding/taonier-logo-flow-concepts/taonier-flow-07-open-clay-orbit.png b/public/branding/taonier-logo-flow-concepts/taonier-flow-07-open-clay-orbit.png new file mode 100644 index 00000000..bbdc922f Binary files /dev/null and b/public/branding/taonier-logo-flow-concepts/taonier-flow-07-open-clay-orbit.png differ diff --git a/public/branding/taonier-logo-flow-concepts/taonier-flow-08-brand-flow-glyph.png b/public/branding/taonier-logo-flow-concepts/taonier-flow-08-brand-flow-glyph.png new file mode 100644 index 00000000..d1da646a Binary files /dev/null and b/public/branding/taonier-logo-flow-concepts/taonier-flow-08-brand-flow-glyph.png differ diff --git a/public/branding/taonier-logo-flow-concepts/taonier-logo-flow-contact-sheet.png b/public/branding/taonier-logo-flow-concepts/taonier-logo-flow-contact-sheet.png new file mode 100644 index 00000000..5730f898 Binary files /dev/null and b/public/branding/taonier-logo-flow-concepts/taonier-logo-flow-contact-sheet.png differ diff --git a/public/branding/taonier-logo-flow-concepts/taonier-logo-flow-manifest.json b/public/branding/taonier-logo-flow-concepts/taonier-logo-flow-manifest.json new file mode 100644 index 00000000..e7c0fbfd --- /dev/null +++ b/public/branding/taonier-logo-flow-concepts/taonier-logo-flow-manifest.json @@ -0,0 +1,61 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T18:34:58.786Z", + "creativeDirection": { + "name": "陶泥儿连续曲线亲和图形标", + "textPolicy": "no Chinese, no English, no wordmark", + "correction": "no square base, no center star, use one continuous friendly clay curve structure", + "motif": "柔泥回环、脑洞涟漪、开放泥环、品牌曲线符" + }, + "variants": [ + { + "id": "01-soft-ribbon-loop", + "title": "柔泥回环", + "file": "taonier-flow-01-soft-ribbon-loop.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须彻底放弃方形底和中心星星:不要整体方形、不要圆角方块、不要 App 图标底板、不要中间星形、不要五角星、不要四角星、不要闪光、不要中心挖孔星。\n核心隐喻:连续的陶泥曲线。用一整块顺滑、亲和、可塑的陶泥大结构来表达「脑洞被揉开、灵感流动、内容成型」,图形要像一个完整的流动符号,而不是底板加中心图案。\n主结构:由平滑曲线组成的大轮廓,可以像柔软泥流、脑洞涟漪、捏合的回环、温和的旋拧、单线团块或开放结。整体必须连贯、圆润、有亲和力,不能分裂成多个碎片。\n风格:现代扁平矢量商标,轻微哑光陶泥质感;边缘干净、曲线饱满、负形明确,适合商标、App 图标、社区头像和启动页。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主,可加入少量低饱和孔雀青或深泥灰作为曲线内侧阴影或小连接点。颜色要温暖、有吸引力,但不要糖果色。\n识别度:远看必须能记住一个独特的大曲线轮廓;不要靠中心图案识别。缩小到 64px 时仍能看出整体曲线姿态。\n亲和力:整体像可以被揉捏的柔软小世界,有陪伴感和创作感,但不要做成脸、表情、角色或吉祥物。\n禁止:方形底、圆角方块、中心星星、任何星形、闪光、徽章、印章、砖块、泥饼、石头、陶片、饼干、糖果、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:一条宽厚柔软的陶泥带形成开放回环,像脑洞被揉开。轮廓连续,内部负形自然,不出现星形或方形底。" + }, + { + "id": "02-clay-wave-knot", + "title": "陶泥波结", + "file": "taonier-flow-02-clay-wave-knot.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须彻底放弃方形底和中心星星:不要整体方形、不要圆角方块、不要 App 图标底板、不要中间星形、不要五角星、不要四角星、不要闪光、不要中心挖孔星。\n核心隐喻:连续的陶泥曲线。用一整块顺滑、亲和、可塑的陶泥大结构来表达「脑洞被揉开、灵感流动、内容成型」,图形要像一个完整的流动符号,而不是底板加中心图案。\n主结构:由平滑曲线组成的大轮廓,可以像柔软泥流、脑洞涟漪、捏合的回环、温和的旋拧、单线团块或开放结。整体必须连贯、圆润、有亲和力,不能分裂成多个碎片。\n风格:现代扁平矢量商标,轻微哑光陶泥质感;边缘干净、曲线饱满、负形明确,适合商标、App 图标、社区头像和启动页。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主,可加入少量低饱和孔雀青或深泥灰作为曲线内侧阴影或小连接点。颜色要温暖、有吸引力,但不要糖果色。\n识别度:远看必须能记住一个独特的大曲线轮廓;不要靠中心图案识别。缩小到 64px 时仍能看出整体曲线姿态。\n亲和力:整体像可以被揉捏的柔软小世界,有陪伴感和创作感,但不要做成脸、表情、角色或吉祥物。\n禁止:方形底、圆角方块、中心星星、任何星形、闪光、徽章、印章、砖块、泥饼、石头、陶片、饼干、糖果、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:像一个温和波浪结,由 2 条互相捏合的平滑曲线组成,但必须视觉上像一个整体符号,不要碎片化。" + }, + { + "id": "03-imagination-ripple", + "title": "脑洞涟漪", + "file": "taonier-flow-03-imagination-ripple.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须彻底放弃方形底和中心星星:不要整体方形、不要圆角方块、不要 App 图标底板、不要中间星形、不要五角星、不要四角星、不要闪光、不要中心挖孔星。\n核心隐喻:连续的陶泥曲线。用一整块顺滑、亲和、可塑的陶泥大结构来表达「脑洞被揉开、灵感流动、内容成型」,图形要像一个完整的流动符号,而不是底板加中心图案。\n主结构:由平滑曲线组成的大轮廓,可以像柔软泥流、脑洞涟漪、捏合的回环、温和的旋拧、单线团块或开放结。整体必须连贯、圆润、有亲和力,不能分裂成多个碎片。\n风格:现代扁平矢量商标,轻微哑光陶泥质感;边缘干净、曲线饱满、负形明确,适合商标、App 图标、社区头像和启动页。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主,可加入少量低饱和孔雀青或深泥灰作为曲线内侧阴影或小连接点。颜色要温暖、有吸引力,但不要糖果色。\n识别度:远看必须能记住一个独特的大曲线轮廓;不要靠中心图案识别。缩小到 64px 时仍能看出整体曲线姿态。\n亲和力:整体像可以被揉捏的柔软小世界,有陪伴感和创作感,但不要做成脸、表情、角色或吉祥物。\n禁止:方形底、圆角方块、中心星星、任何星形、闪光、徽章、印章、砖块、泥饼、石头、陶片、饼干、糖果、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:用一整块陶泥曲面形成涟漪状大轮廓,中间负形像柔和水滴或脑洞入口,不能像星星。" + }, + { + "id": "04-friendly-clay-comet", + "title": "亲和泥流", + "file": "taonier-flow-04-friendly-clay-comet.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须彻底放弃方形底和中心星星:不要整体方形、不要圆角方块、不要 App 图标底板、不要中间星形、不要五角星、不要四角星、不要闪光、不要中心挖孔星。\n核心隐喻:连续的陶泥曲线。用一整块顺滑、亲和、可塑的陶泥大结构来表达「脑洞被揉开、灵感流动、内容成型」,图形要像一个完整的流动符号,而不是底板加中心图案。\n主结构:由平滑曲线组成的大轮廓,可以像柔软泥流、脑洞涟漪、捏合的回环、温和的旋拧、单线团块或开放结。整体必须连贯、圆润、有亲和力,不能分裂成多个碎片。\n风格:现代扁平矢量商标,轻微哑光陶泥质感;边缘干净、曲线饱满、负形明确,适合商标、App 图标、社区头像和启动页。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主,可加入少量低饱和孔雀青或深泥灰作为曲线内侧阴影或小连接点。颜色要温暖、有吸引力,但不要糖果色。\n识别度:远看必须能记住一个独特的大曲线轮廓;不要靠中心图案识别。缩小到 64px 时仍能看出整体曲线姿态。\n亲和力:整体像可以被揉捏的柔软小世界,有陪伴感和创作感,但不要做成脸、表情、角色或吉祥物。\n禁止:方形底、圆角方块、中心星星、任何星形、闪光、徽章、印章、砖块、泥饼、石头、陶片、饼干、糖果、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:亲和泥流。整体像一个流动的陶泥小世界,前端圆润、尾部自然收束,有动势但不尖锐。" + }, + { + "id": "05-single-stroke-blob", + "title": "单笔团块", + "file": "taonier-flow-05-single-stroke-blob.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须彻底放弃方形底和中心星星:不要整体方形、不要圆角方块、不要 App 图标底板、不要中间星形、不要五角星、不要四角星、不要闪光、不要中心挖孔星。\n核心隐喻:连续的陶泥曲线。用一整块顺滑、亲和、可塑的陶泥大结构来表达「脑洞被揉开、灵感流动、内容成型」,图形要像一个完整的流动符号,而不是底板加中心图案。\n主结构:由平滑曲线组成的大轮廓,可以像柔软泥流、脑洞涟漪、捏合的回环、温和的旋拧、单线团块或开放结。整体必须连贯、圆润、有亲和力,不能分裂成多个碎片。\n风格:现代扁平矢量商标,轻微哑光陶泥质感;边缘干净、曲线饱满、负形明确,适合商标、App 图标、社区头像和启动页。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主,可加入少量低饱和孔雀青或深泥灰作为曲线内侧阴影或小连接点。颜色要温暖、有吸引力,但不要糖果色。\n识别度:远看必须能记住一个独特的大曲线轮廓;不要靠中心图案识别。缩小到 64px 时仍能看出整体曲线姿态。\n亲和力:整体像可以被揉捏的柔软小世界,有陪伴感和创作感,但不要做成脸、表情、角色或吉祥物。\n禁止:方形底、圆角方块、中心星星、任何星形、闪光、徽章、印章、砖块、泥饼、石头、陶片、饼干、糖果、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:单笔成型。像用一笔连续曲线捏出的陶泥团,结构极简但有记忆点,适合后续矢量化。" + }, + { + "id": "06-two-tone-soft-flow", + "title": "双色软流", + "file": "taonier-flow-06-two-tone-soft-flow.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须彻底放弃方形底和中心星星:不要整体方形、不要圆角方块、不要 App 图标底板、不要中间星形、不要五角星、不要四角星、不要闪光、不要中心挖孔星。\n核心隐喻:连续的陶泥曲线。用一整块顺滑、亲和、可塑的陶泥大结构来表达「脑洞被揉开、灵感流动、内容成型」,图形要像一个完整的流动符号,而不是底板加中心图案。\n主结构:由平滑曲线组成的大轮廓,可以像柔软泥流、脑洞涟漪、捏合的回环、温和的旋拧、单线团块或开放结。整体必须连贯、圆润、有亲和力,不能分裂成多个碎片。\n风格:现代扁平矢量商标,轻微哑光陶泥质感;边缘干净、曲线饱满、负形明确,适合商标、App 图标、社区头像和启动页。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主,可加入少量低饱和孔雀青或深泥灰作为曲线内侧阴影或小连接点。颜色要温暖、有吸引力,但不要糖果色。\n识别度:远看必须能记住一个独特的大曲线轮廓;不要靠中心图案识别。缩小到 64px 时仍能看出整体曲线姿态。\n亲和力:整体像可以被揉捏的柔软小世界,有陪伴感和创作感,但不要做成脸、表情、角色或吉祥物。\n禁止:方形底、圆角方块、中心星星、任何星形、闪光、徽章、印章、砖块、泥饼、石头、陶片、饼干、糖果、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:双色曲线。暖陶白主体配少量孔雀青或陶土橙内侧曲线,让图形更吸引人,但不能变成多片拼贴。" + }, + { + "id": "07-open-clay-orbit", + "title": "开放泥环", + "file": "taonier-flow-07-open-clay-orbit.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须彻底放弃方形底和中心星星:不要整体方形、不要圆角方块、不要 App 图标底板、不要中间星形、不要五角星、不要四角星、不要闪光、不要中心挖孔星。\n核心隐喻:连续的陶泥曲线。用一整块顺滑、亲和、可塑的陶泥大结构来表达「脑洞被揉开、灵感流动、内容成型」,图形要像一个完整的流动符号,而不是底板加中心图案。\n主结构:由平滑曲线组成的大轮廓,可以像柔软泥流、脑洞涟漪、捏合的回环、温和的旋拧、单线团块或开放结。整体必须连贯、圆润、有亲和力,不能分裂成多个碎片。\n风格:现代扁平矢量商标,轻微哑光陶泥质感;边缘干净、曲线饱满、负形明确,适合商标、App 图标、社区头像和启动页。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主,可加入少量低饱和孔雀青或深泥灰作为曲线内侧阴影或小连接点。颜色要温暖、有吸引力,但不要糖果色。\n识别度:远看必须能记住一个独特的大曲线轮廓;不要靠中心图案识别。缩小到 64px 时仍能看出整体曲线姿态。\n亲和力:整体像可以被揉捏的柔软小世界,有陪伴感和创作感,但不要做成脸、表情、角色或吉祥物。\n禁止:方形底、圆角方块、中心星星、任何星形、闪光、徽章、印章、砖块、泥饼、石头、陶片、饼干、糖果、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:开放式泥环。不是闭合圆,也不是旋涡旧稿,而是一枚有缺口和呼吸感的平滑陶泥环形符号。" + }, + { + "id": "08-brand-flow-glyph", + "title": "品牌曲线符", + "file": "taonier-flow-08-brand-flow-glyph.png", + "prompt": "请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。\n产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。\n这次必须彻底放弃方形底和中心星星:不要整体方形、不要圆角方块、不要 App 图标底板、不要中间星形、不要五角星、不要四角星、不要闪光、不要中心挖孔星。\n核心隐喻:连续的陶泥曲线。用一整块顺滑、亲和、可塑的陶泥大结构来表达「脑洞被揉开、灵感流动、内容成型」,图形要像一个完整的流动符号,而不是底板加中心图案。\n主结构:由平滑曲线组成的大轮廓,可以像柔软泥流、脑洞涟漪、捏合的回环、温和的旋拧、单线团块或开放结。整体必须连贯、圆润、有亲和力,不能分裂成多个碎片。\n风格:现代扁平矢量商标,轻微哑光陶泥质感;边缘干净、曲线饱满、负形明确,适合商标、App 图标、社区头像和启动页。\n配色:暖陶白、浅陶土、陶土橙、暖棕为主,可加入少量低饱和孔雀青或深泥灰作为曲线内侧阴影或小连接点。颜色要温暖、有吸引力,但不要糖果色。\n识别度:远看必须能记住一个独特的大曲线轮廓;不要靠中心图案识别。缩小到 64px 时仍能看出整体曲线姿态。\n亲和力:整体像可以被揉捏的柔软小世界,有陪伴感和创作感,但不要做成脸、表情、角色或吉祥物。\n禁止:方形底、圆角方块、中心星星、任何星形、闪光、徽章、印章、砖块、泥饼、石头、陶片、饼干、糖果、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。\n本张重点:最终品牌符号潜力。减少材质和细节,用 1 到 2 个大曲线色块形成独特、亲和、可注册的图形标。" + } + ] +} diff --git a/public/branding/taonier-logo-fresh-concepts/taonier-fresh-dot-dice.png b/public/branding/taonier-logo-fresh-concepts/taonier-fresh-dot-dice.png new file mode 100644 index 00000000..c0aaea2b Binary files /dev/null and b/public/branding/taonier-logo-fresh-concepts/taonier-fresh-dot-dice.png differ diff --git a/public/branding/taonier-logo-fresh-concepts/taonier-fresh-mold-window.png b/public/branding/taonier-logo-fresh-concepts/taonier-fresh-mold-window.png new file mode 100644 index 00000000..c493fd70 Binary files /dev/null and b/public/branding/taonier-logo-fresh-concepts/taonier-fresh-mold-window.png differ diff --git a/public/branding/taonier-logo-fresh-concepts/taonier-fresh-pocket-world.png b/public/branding/taonier-logo-fresh-concepts/taonier-fresh-pocket-world.png new file mode 100644 index 00000000..390361bf Binary files /dev/null and b/public/branding/taonier-logo-fresh-concepts/taonier-fresh-pocket-world.png differ diff --git a/public/branding/taonier-logo-fresh-concepts/taonier-fresh-punch-hole.png b/public/branding/taonier-logo-fresh-concepts/taonier-fresh-punch-hole.png new file mode 100644 index 00000000..4cba08fc Binary files /dev/null and b/public/branding/taonier-logo-fresh-concepts/taonier-fresh-punch-hole.png differ diff --git a/public/branding/taonier-logo-fresh-concepts/taonier-fresh-stage-window.png b/public/branding/taonier-logo-fresh-concepts/taonier-fresh-stage-window.png new file mode 100644 index 00000000..fc41e804 Binary files /dev/null and b/public/branding/taonier-logo-fresh-concepts/taonier-fresh-stage-window.png differ diff --git a/public/branding/taonier-logo-fresh-concepts/taonier-fresh-wheel-imprint.png b/public/branding/taonier-logo-fresh-concepts/taonier-fresh-wheel-imprint.png new file mode 100644 index 00000000..996ff9dd Binary files /dev/null and b/public/branding/taonier-logo-fresh-concepts/taonier-fresh-wheel-imprint.png differ diff --git a/public/branding/taonier-logo-fresh-concepts/taonier-logo-fresh-contact-sheet.png b/public/branding/taonier-logo-fresh-concepts/taonier-logo-fresh-contact-sheet.png new file mode 100644 index 00000000..50b5a0cf Binary files /dev/null and b/public/branding/taonier-logo-fresh-concepts/taonier-logo-fresh-contact-sheet.png differ diff --git a/public/branding/taonier-logo-geometric-concepts/taonier-geometric-dot-gate.png b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-dot-gate.png new file mode 100644 index 00000000..4df8762f Binary files /dev/null and b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-dot-gate.png differ diff --git a/public/branding/taonier-logo-geometric-concepts/taonier-geometric-dot-gate.svg b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-dot-gate.svg new file mode 100644 index 00000000..2f2b6910 --- /dev/null +++ b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-dot-gate.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/branding/taonier-logo-geometric-concepts/taonier-geometric-dual-plate.png b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-dual-plate.png new file mode 100644 index 00000000..3063ec82 Binary files /dev/null and b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-dual-plate.png differ diff --git a/public/branding/taonier-logo-geometric-concepts/taonier-geometric-dual-plate.svg b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-dual-plate.svg new file mode 100644 index 00000000..604555ac --- /dev/null +++ b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-dual-plate.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/branding/taonier-logo-geometric-concepts/taonier-geometric-mold-chip.png b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-mold-chip.png new file mode 100644 index 00000000..b137b791 Binary files /dev/null and b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-mold-chip.png differ diff --git a/public/branding/taonier-logo-geometric-concepts/taonier-geometric-mold-chip.svg b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-mold-chip.svg new file mode 100644 index 00000000..5fbc3145 --- /dev/null +++ b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-mold-chip.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/branding/taonier-logo-geometric-concepts/taonier-geometric-offset-core.png b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-offset-core.png new file mode 100644 index 00000000..fb25bc93 Binary files /dev/null and b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-offset-core.png differ diff --git a/public/branding/taonier-logo-geometric-concepts/taonier-geometric-offset-core.svg b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-offset-core.svg new file mode 100644 index 00000000..dc99f8ca --- /dev/null +++ b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-offset-core.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/branding/taonier-logo-geometric-concepts/taonier-geometric-pinched-tile.png b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-pinched-tile.png new file mode 100644 index 00000000..6a80aa9a Binary files /dev/null and b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-pinched-tile.png differ diff --git a/public/branding/taonier-logo-geometric-concepts/taonier-geometric-pinched-tile.svg b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-pinched-tile.svg new file mode 100644 index 00000000..02c12aba --- /dev/null +++ b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-pinched-tile.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/branding/taonier-logo-geometric-concepts/taonier-geometric-work-knot.png b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-work-knot.png new file mode 100644 index 00000000..eb4ca865 Binary files /dev/null and b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-work-knot.png differ diff --git a/public/branding/taonier-logo-geometric-concepts/taonier-geometric-work-knot.svg b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-work-knot.svg new file mode 100644 index 00000000..7e0eea94 --- /dev/null +++ b/public/branding/taonier-logo-geometric-concepts/taonier-geometric-work-knot.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/branding/taonier-logo-geometric-concepts/taonier-logo-geometric-contact-sheet.png b/public/branding/taonier-logo-geometric-concepts/taonier-logo-geometric-contact-sheet.png new file mode 100644 index 00000000..c9499943 Binary files /dev/null and b/public/branding/taonier-logo-geometric-concepts/taonier-logo-geometric-contact-sheet.png differ diff --git a/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-01-berry-aqua-pop.png b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-01-berry-aqua-pop.png new file mode 100644 index 00000000..6998e267 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-01-berry-aqua-pop.png differ diff --git a/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-02-coral-lilac.png b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-02-coral-lilac.png new file mode 100644 index 00000000..f6127a50 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-02-coral-lilac.png differ diff --git a/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-03-mango-turquoise.png b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-03-mango-turquoise.png new file mode 100644 index 00000000..fe048b76 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-03-mango-turquoise.png differ diff --git a/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-04-neon-rose-mint.png b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-04-neon-rose-mint.png new file mode 100644 index 00000000..e6b51446 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-04-neon-rose-mint.png differ diff --git a/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-05-poppy-blue.png b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-05-poppy-blue.png new file mode 100644 index 00000000..bac0c919 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-05-poppy-blue.png differ diff --git a/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-06-violet-peach.png b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-06-violet-peach.png new file mode 100644 index 00000000..5a3266fe Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-06-violet-peach.png differ diff --git a/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-07-flat-duotone.png b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-07-flat-duotone.png new file mode 100644 index 00000000..d4275a70 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-07-flat-duotone.png differ diff --git a/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-08-app-icon-bright.png b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-08-app-icon-bright.png new file mode 100644 index 00000000..a633aa02 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-08-app-icon-bright.png differ diff --git a/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-contact-sheet.png b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-contact-sheet.png new file mode 100644 index 00000000..ad8e836f Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-contact-sheet.png differ diff --git a/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-manifest.json b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-manifest.json new file mode 100644 index 00000000..4208aa7e --- /dev/null +++ b/public/branding/taonier-logo-hand-spirit-bold-color-concepts/taonier-hand-spirit-bold-color-manifest.json @@ -0,0 +1,62 @@ +{ + "model": "gpt-image-2", + "endpoint": "/v1/images/edits", + "size": "1024x1024", + "referenceImage": "public\\branding\\taonier-logo-hand-spirit-ref01-logo-refine-concepts\\taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream.png", + "generatedAt": "2026-05-18T07:50:23.595Z", + "brief": { + "brand": "陶泥儿", + "goal": "更大胆、更吸引女生和年轻人的手托灵体 logo 配色探索", + "keep": "保留托举曲线与半球灵体结构,不加文字、不加脸、不加星星" + }, + "variants": [ + { + "id": "01-berry-aqua-pop", + "title": "莓粉青 aqua", + "file": "taonier-hand-spirit-bold-color-01-berry-aqua-pop.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a bolder, more attractive color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is more memorable social-platform color, more appealing to young users and women, while still premium and trademark-like.\nUse bold modern brand colors, not realistic material colors. Saturated colors are allowed, but they must read as clean brand color fields, not candy, dessert, food, jelly, bread, pastry, mochi, bun, sauce, or cosmetic packaging.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle app-logo polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette: vivid raspberry pink semi-dome, bright coral side accent, fresh aqua or mint hand support, small cream negative gap. Bold, young, energetic, not sugary." + }, + { + "id": "02-coral-lilac", + "title": "珊瑚丁香", + "file": "taonier-hand-spirit-bold-color-02-coral-lilac.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a bolder, more attractive color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is more memorable social-platform color, more appealing to young users and women, while still premium and trademark-like.\nUse bold modern brand colors, not realistic material colors. Saturated colors are allowed, but they must read as clean brand color fields, not candy, dessert, food, jelly, bread, pastry, mochi, bun, sauce, or cosmetic packaging.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle app-logo polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette: punchy coral-red semi-dome with warm pink accent, soft lilac-lavender hand support, tiny ivory separator. Feminine, fresh, and premium." + }, + { + "id": "03-mango-turquoise", + "title": "芒果松石", + "file": "taonier-hand-spirit-bold-color-03-mango-turquoise.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a bolder, more attractive color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is more memorable social-platform color, more appealing to young users and women, while still premium and trademark-like.\nUse bold modern brand colors, not realistic material colors. Saturated colors are allowed, but they must read as clean brand color fields, not candy, dessert, food, jelly, bread, pastry, mochi, bun, sauce, or cosmetic packaging.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle app-logo polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette: bright mango-orange semi-dome, hot peach accent, turquoise hand support. High contrast and cheerful, but still flat and logo-like, not food-like." + }, + { + "id": "04-neon-rose-mint", + "title": "玫红薄荷", + "file": "taonier-hand-spirit-bold-color-04-neon-rose-mint.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a bolder, more attractive color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is more memorable social-platform color, more appealing to young users and women, while still premium and trademark-like.\nUse bold modern brand colors, not realistic material colors. Saturated colors are allowed, but they must read as clean brand color fields, not candy, dessert, food, jelly, bread, pastry, mochi, bun, sauce, or cosmetic packaging.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle app-logo polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette: neon rose or magenta semi-dome, clean mint green hand support, warm ivory separator. Strong social-avatar memory, modern and playful." + }, + { + "id": "05-poppy-blue", + "title": "罂粟蓝调", + "file": "taonier-hand-spirit-bold-color-05-poppy-blue.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a bolder, more attractive color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is more memorable social-platform color, more appealing to young users and women, while still premium and trademark-like.\nUse bold modern brand colors, not realistic material colors. Saturated colors are allowed, but they must read as clean brand color fields, not candy, dessert, food, jelly, bread, pastry, mochi, bun, sauce, or cosmetic packaging.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle app-logo polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette: saturated poppy orange-red semi-dome, cobalt or sky-blue support curve, cream separator. More graphic, bold, and youth-culture oriented." + }, + { + "id": "06-violet-peach", + "title": "紫桃撞色", + "file": "taonier-hand-spirit-bold-color-06-violet-peach.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a bolder, more attractive color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is more memorable social-platform color, more appealing to young users and women, while still premium and trademark-like.\nUse bold modern brand colors, not realistic material colors. Saturated colors are allowed, but they must read as clean brand color fields, not candy, dessert, food, jelly, bread, pastry, mochi, bun, sauce, or cosmetic packaging.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle app-logo polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette: vivid violet-purple hand support with peach-orange semi-dome and pink accent. Keep the purple limited and crisp so the logo does not become a generic purple tech gradient." + }, + { + "id": "07-flat-duotone", + "title": "双色强记忆", + "file": "taonier-hand-spirit-bold-color-07-flat-duotone.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a bolder, more attractive color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is more memorable social-platform color, more appealing to young users and women, while still premium and trademark-like.\nUse bold modern brand colors, not realistic material colors. Saturated colors are allowed, but they must read as clean brand color fields, not candy, dessert, food, jelly, bread, pastry, mochi, bun, sauce, or cosmetic packaging.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle app-logo polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette and style: ultra-flat two-color version. Use one bold warm color for the spirit and one bold cool color for the hand. No highlight, no gradient, no shadow. Maximize trademark simplicity." + }, + { + "id": "08-app-icon-bright", + "title": "亮彩头像", + "file": "taonier-hand-spirit-bold-color-08-app-icon-bright.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a bolder, more attractive color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is more memorable social-platform color, more appealing to young users and women, while still premium and trademark-like.\nUse bold modern brand colors, not realistic material colors. Saturated colors are allowed, but they must read as clean brand color fields, not candy, dessert, food, jelly, bread, pastry, mochi, bun, sauce, or cosmetic packaging.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle app-logo polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette and style: brightest app-icon-friendly version. Use coral, hot pink, and aqua with only a very subtle broad gradient. Keep the mark bold and readable at small size." + } + ] +} diff --git a/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit-transparent-manifest.json b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit-transparent-manifest.json new file mode 100644 index 00000000..01fe7825 --- /dev/null +++ b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit-transparent-manifest.json @@ -0,0 +1,10 @@ +{ + "model": "gpt-image-2", + "endpoint": "/v1/images/edits", + "size": "1024x1024", + "source": "public\\branding\\taonier-logo-hand-spirit-concepts\\taonier-hand-spirit-01-gentle-hand-spirit.png", + "chromaSource": "public\\branding\\taonier-logo-hand-spirit-concepts\\taonier-hand-spirit-01-gentle-hand-spirit-transparent-source.png", + "finalOutput": "public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit-transparent.png", + "generatedAt": "2026-05-18T06:51:33.388Z", + "prompt": "Use the uploaded image as the exact edit target.\nPreserve the logo subject exactly: same abstract hand shape, same clay spirit shape, same proportions, same placement, same scale, same colors, same soft vector style.\nDo not redesign, simplify, recolor, crop, rotate, add details, remove highlights, change the hand, or change the clay spirit.\nReplace only the white/off-white background with a perfectly flat solid #00ff00 chroma-key background.\nThe background must be one uniform #00ff00 color with no shadows, gradients, texture, reflections, floor plane, border, or lighting variation.\nDo not use #00ff00 anywhere inside the logo subject.\nNo text, no watermark, no UI, no extra marks, no border.\nKeep crisp clean edges and generous safe area so the result can be converted into a transparent PNG." +} diff --git a/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit-transparent-source.png b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit-transparent-source.png new file mode 100644 index 00000000..e4749f22 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit-transparent-source.png differ diff --git a/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit-transparent.png b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit-transparent.png new file mode 100644 index 00000000..cb84c46b Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit-transparent.png differ diff --git a/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit.png b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit.png new file mode 100644 index 00000000..f7e97ca4 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit.png differ diff --git a/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-02-sharing-palm.png b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-02-sharing-palm.png new file mode 100644 index 00000000..d76ab2ea Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-02-sharing-palm.png differ diff --git a/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-03-teal-support.png b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-03-teal-support.png new file mode 100644 index 00000000..52b04e49 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-03-teal-support.png differ diff --git a/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-04-arched-spirit.png b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-04-arched-spirit.png new file mode 100644 index 00000000..84046fe0 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-04-arched-spirit.png differ diff --git a/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-05-playful-offer.png b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-05-playful-offer.png new file mode 100644 index 00000000..014e1848 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-05-playful-offer.png differ diff --git a/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-06-monochrome-first.png b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-06-monochrome-first.png new file mode 100644 index 00000000..96b07b92 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-06-monochrome-first.png differ diff --git a/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-07-avatar-readable.png b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-07-avatar-readable.png new file mode 100644 index 00000000..aaea7097 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-07-avatar-readable.png differ diff --git a/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-08-vector-ready.png b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-08-vector-ready.png new file mode 100644 index 00000000..322a19aa Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-08-vector-ready.png differ diff --git a/public/branding/taonier-logo-hand-spirit-concepts/taonier-logo-hand-spirit-contact-sheet.png b/public/branding/taonier-logo-hand-spirit-concepts/taonier-logo-hand-spirit-contact-sheet.png new file mode 100644 index 00000000..ea47da42 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-concepts/taonier-logo-hand-spirit-contact-sheet.png differ diff --git a/public/branding/taonier-logo-hand-spirit-concepts/taonier-logo-hand-spirit-manifest.json b/public/branding/taonier-logo-hand-spirit-concepts/taonier-logo-hand-spirit-manifest.json new file mode 100644 index 00000000..26d0bcab --- /dev/null +++ b/public/branding/taonier-logo-hand-spirit-concepts/taonier-logo-hand-spirit-manifest.json @@ -0,0 +1,82 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T21:30:55.045Z", + "logoSkillSummary": { + "requiredReview": "visual inspection, 32px readability, black-white viability", + "outputStatus": "AI concept only; final logo needs vector cleanup" + }, + "brief": { + "brand": "陶泥儿", + "coreBelief": "好玩会创造", + "logoType": "symbol/icon-only mark, no wordmark", + "product": "AI UGC 轻休闲小游戏创作与传播平台,用户像捏陶泥一样把脑洞、梗和灵感塑造成可分享的作品", + "metaphor": "抽象化的手托举/递出一个软萌陶泥灵体", + "intent": [ + "托举", + "分享", + "传递", + "创作被捏成一个有生命感的小作品" + ], + "spiritShape": "不规则半球形陶泥灵体,参考黑底白色半圆拱形轮廓,但不照抄", + "audience": "女性用户友好、全年龄向、年轻明亮但不低幼", + "material": "只保留陶泥温度,不追求泥土质感", + "mustHave": [ + "手必须高度抽象,像托举曲线或掌形基座", + "陶泥灵体必须是主角,软萌但不出现脸", + "画面传达分享/传递,而不是供奉/宗教/医疗", + "32px 可识别", + "黑白化仍成立" + ] + }, + "variants": [ + { + "id": "01-gentle-hand-spirit", + "title": "温柔托举灵体", + "file": "taonier-hand-spirit-01-gentle-hand-spirit.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nMain metaphor: an abstract hand gently holding up and offering a cute soft clay spirit. The logo should communicate creation, sharing, passing forward, and a small idea becoming a lovable playable object.\nLogo type: abstract symbol/icon only. Not a mascot illustration, not an app-icon rounded-square background, not an emblem with text.\nComposition: one highly simplified hand-like support curve or palm base below, holding one soft clay spirit above. The hand must be abstract, smooth, and logo-like, not realistic fingers.\nClay spirit shape: a cute irregular semi-dome / half-blob / rounded arch form, inspired by a simple white arched half-circle silhouette on dark background, but transformed into a friendly original brand symbol.\nThe spirit should be soft and lovable without a face. No eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nThe hand gesture should feel like sharing or gently presenting, not worship, religion, medical care, begging, or holding a food item.\nStyle: modern minimalist vector logo. Clean broad shapes, clear silhouette, fresh bright colors, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make the spirit look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, cream filling, sauce, dessert, or food packaging.\nShape avoidance: no square base, no rounded-square app background, no free ribbon, no swirl, no S or G letter feeling, no center star.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the hand support and semi-dome clay spirit should be recognizable at 32px.\nVariant focus: the clearest version. A simple cream abstract palm curve holds a coral-peach semi-dome clay spirit. Friendly, iconic, and readable." + }, + { + "id": "02-sharing-palm", + "title": "分享掌形", + "file": "taonier-hand-spirit-02-sharing-palm.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nMain metaphor: an abstract hand gently holding up and offering a cute soft clay spirit. The logo should communicate creation, sharing, passing forward, and a small idea becoming a lovable playable object.\nLogo type: abstract symbol/icon only. Not a mascot illustration, not an app-icon rounded-square background, not an emblem with text.\nComposition: one highly simplified hand-like support curve or palm base below, holding one soft clay spirit above. The hand must be abstract, smooth, and logo-like, not realistic fingers.\nClay spirit shape: a cute irregular semi-dome / half-blob / rounded arch form, inspired by a simple white arched half-circle silhouette on dark background, but transformed into a friendly original brand symbol.\nThe spirit should be soft and lovable without a face. No eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nThe hand gesture should feel like sharing or gently presenting, not worship, religion, medical care, begging, or holding a food item.\nStyle: modern minimalist vector logo. Clean broad shapes, clear silhouette, fresh bright colors, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make the spirit look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, cream filling, sauce, dessert, or food packaging.\nShape avoidance: no square base, no rounded-square app background, no free ribbon, no swirl, no S or G letter feeling, no center star.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the hand support and semi-dome clay spirit should be recognizable at 32px.\nVariant focus: sharing intention. The abstract hand is slightly forward-facing, like offering the clay spirit outward, but still very simplified and not realistic." + }, + { + "id": "03-teal-support", + "title": "青绿托线", + "file": "taonier-hand-spirit-03-teal-support.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nMain metaphor: an abstract hand gently holding up and offering a cute soft clay spirit. The logo should communicate creation, sharing, passing forward, and a small idea becoming a lovable playable object.\nLogo type: abstract symbol/icon only. Not a mascot illustration, not an app-icon rounded-square background, not an emblem with text.\nComposition: one highly simplified hand-like support curve or palm base below, holding one soft clay spirit above. The hand must be abstract, smooth, and logo-like, not realistic fingers.\nClay spirit shape: a cute irregular semi-dome / half-blob / rounded arch form, inspired by a simple white arched half-circle silhouette on dark background, but transformed into a friendly original brand symbol.\nThe spirit should be soft and lovable without a face. No eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nThe hand gesture should feel like sharing or gently presenting, not worship, religion, medical care, begging, or holding a food item.\nStyle: modern minimalist vector logo. Clean broad shapes, clear silhouette, fresh bright colors, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make the spirit look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, cream filling, sauce, dessert, or food packaging.\nShape avoidance: no square base, no rounded-square app background, no free ribbon, no swirl, no S or G letter feeling, no center star.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the hand support and semi-dome clay spirit should be recognizable at 32px.\nVariant focus: use a clear soft teal support curve as the hand and a warm peach clay spirit above. Strong color memory, no food look." + }, + { + "id": "04-arched-spirit", + "title": "拱形泥灵", + "file": "taonier-hand-spirit-04-arched-spirit.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nMain metaphor: an abstract hand gently holding up and offering a cute soft clay spirit. The logo should communicate creation, sharing, passing forward, and a small idea becoming a lovable playable object.\nLogo type: abstract symbol/icon only. Not a mascot illustration, not an app-icon rounded-square background, not an emblem with text.\nComposition: one highly simplified hand-like support curve or palm base below, holding one soft clay spirit above. The hand must be abstract, smooth, and logo-like, not realistic fingers.\nClay spirit shape: a cute irregular semi-dome / half-blob / rounded arch form, inspired by a simple white arched half-circle silhouette on dark background, but transformed into a friendly original brand symbol.\nThe spirit should be soft and lovable without a face. No eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nThe hand gesture should feel like sharing or gently presenting, not worship, religion, medical care, begging, or holding a food item.\nStyle: modern minimalist vector logo. Clean broad shapes, clear silhouette, fresh bright colors, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make the spirit look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, cream filling, sauce, dessert, or food packaging.\nShape avoidance: no square base, no rounded-square app background, no free ribbon, no swirl, no S or G letter feeling, no center star.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the hand support and semi-dome clay spirit should be recognizable at 32px.\nVariant focus: emphasize the irregular semi-dome clay spirit shape from the reference: simple arched top, flatter base, slightly organic, no face." + }, + { + "id": "05-playful-offer", + "title": "轻玩递出", + "file": "taonier-hand-spirit-05-playful-offer.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nMain metaphor: an abstract hand gently holding up and offering a cute soft clay spirit. The logo should communicate creation, sharing, passing forward, and a small idea becoming a lovable playable object.\nLogo type: abstract symbol/icon only. Not a mascot illustration, not an app-icon rounded-square background, not an emblem with text.\nComposition: one highly simplified hand-like support curve or palm base below, holding one soft clay spirit above. The hand must be abstract, smooth, and logo-like, not realistic fingers.\nClay spirit shape: a cute irregular semi-dome / half-blob / rounded arch form, inspired by a simple white arched half-circle silhouette on dark background, but transformed into a friendly original brand symbol.\nThe spirit should be soft and lovable without a face. No eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nThe hand gesture should feel like sharing or gently presenting, not worship, religion, medical care, begging, or holding a food item.\nStyle: modern minimalist vector logo. Clean broad shapes, clear silhouette, fresh bright colors, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make the spirit look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, cream filling, sauce, dessert, or food packaging.\nShape avoidance: no square base, no rounded-square app background, no free ribbon, no swirl, no S or G letter feeling, no center star.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the hand support and semi-dome clay spirit should be recognizable at 32px.\nVariant focus: more playful and lively. The hand support suggests passing the spirit forward, with one broad curve only. Avoid decorative tiny details." + }, + { + "id": "06-monochrome-first", + "title": "黑白优先", + "file": "taonier-hand-spirit-06-monochrome-first.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nMain metaphor: an abstract hand gently holding up and offering a cute soft clay spirit. The logo should communicate creation, sharing, passing forward, and a small idea becoming a lovable playable object.\nLogo type: abstract symbol/icon only. Not a mascot illustration, not an app-icon rounded-square background, not an emblem with text.\nComposition: one highly simplified hand-like support curve or palm base below, holding one soft clay spirit above. The hand must be abstract, smooth, and logo-like, not realistic fingers.\nClay spirit shape: a cute irregular semi-dome / half-blob / rounded arch form, inspired by a simple white arched half-circle silhouette on dark background, but transformed into a friendly original brand symbol.\nThe spirit should be soft and lovable without a face. No eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nThe hand gesture should feel like sharing or gently presenting, not worship, religion, medical care, begging, or holding a food item.\nStyle: modern minimalist vector logo. Clean broad shapes, clear silhouette, fresh bright colors, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make the spirit look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, cream filling, sauce, dessert, or food packaging.\nShape avoidance: no square base, no rounded-square app background, no free ribbon, no swirl, no S or G letter feeling, no center star.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the hand support and semi-dome clay spirit should be recognizable at 32px.\nVariant focus: design for black-and-white survival first. Use strong positive and negative shapes so the hand and spirit remain readable without color." + }, + { + "id": "07-avatar-readable", + "title": "头像可读", + "file": "taonier-hand-spirit-07-avatar-readable.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nMain metaphor: an abstract hand gently holding up and offering a cute soft clay spirit. The logo should communicate creation, sharing, passing forward, and a small idea becoming a lovable playable object.\nLogo type: abstract symbol/icon only. Not a mascot illustration, not an app-icon rounded-square background, not an emblem with text.\nComposition: one highly simplified hand-like support curve or palm base below, holding one soft clay spirit above. The hand must be abstract, smooth, and logo-like, not realistic fingers.\nClay spirit shape: a cute irregular semi-dome / half-blob / rounded arch form, inspired by a simple white arched half-circle silhouette on dark background, but transformed into a friendly original brand symbol.\nThe spirit should be soft and lovable without a face. No eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nThe hand gesture should feel like sharing or gently presenting, not worship, religion, medical care, begging, or holding a food item.\nStyle: modern minimalist vector logo. Clean broad shapes, clear silhouette, fresh bright colors, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make the spirit look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, cream filling, sauce, dessert, or food packaging.\nShape avoidance: no square base, no rounded-square app background, no free ribbon, no swirl, no S or G letter feeling, no center star.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the hand support and semi-dome clay spirit should be recognizable at 32px.\nVariant focus: social avatar and favicon readability. Compact, bold silhouette, thicker hand curve, larger semi-dome spirit, no small parts." + }, + { + "id": "08-vector-ready", + "title": "矢量定稿感", + "file": "taonier-hand-spirit-08-vector-ready.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nMain metaphor: an abstract hand gently holding up and offering a cute soft clay spirit. The logo should communicate creation, sharing, passing forward, and a small idea becoming a lovable playable object.\nLogo type: abstract symbol/icon only. Not a mascot illustration, not an app-icon rounded-square background, not an emblem with text.\nComposition: one highly simplified hand-like support curve or palm base below, holding one soft clay spirit above. The hand must be abstract, smooth, and logo-like, not realistic fingers.\nClay spirit shape: a cute irregular semi-dome / half-blob / rounded arch form, inspired by a simple white arched half-circle silhouette on dark background, but transformed into a friendly original brand symbol.\nThe spirit should be soft and lovable without a face. No eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nThe hand gesture should feel like sharing or gently presenting, not worship, religion, medical care, begging, or holding a food item.\nStyle: modern minimalist vector logo. Clean broad shapes, clear silhouette, fresh bright colors, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make the spirit look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, cream filling, sauce, dessert, or food packaging.\nShape avoidance: no square base, no rounded-square app background, no free ribbon, no swirl, no S or G letter feeling, no center star.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the hand support and semi-dome clay spirit should be recognizable at 32px.\nVariant focus: designer-ready vector concept. 2-3 flat shapes, crisp boundaries, distinctive hand-support and clay-spirit silhouette, minimal material cue." + } + ] +} diff --git a/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-01-dusty-rose-sage.png b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-01-dusty-rose-sage.png new file mode 100644 index 00000000..5f7f0199 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-01-dusty-rose-sage.png differ diff --git a/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-02-smoke-blue-apricot.png b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-02-smoke-blue-apricot.png new file mode 100644 index 00000000..cc05337c Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-02-smoke-blue-apricot.png differ diff --git a/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-03-misty-lilac-clay.png b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-03-misty-lilac-clay.png new file mode 100644 index 00000000..8fb67ab9 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-03-misty-lilac-clay.png differ diff --git a/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-04-butter-rose-tea.png b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-04-butter-rose-tea.png new file mode 100644 index 00000000..4c40e54e Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-04-butter-rose-tea.png differ diff --git a/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-05-clay-blue-mint.png b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-05-clay-blue-mint.png new file mode 100644 index 00000000..c10a8134 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-05-clay-blue-mint.png differ diff --git a/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-06-powder-berry-cloud.png b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-06-powder-berry-cloud.png new file mode 100644 index 00000000..5098f639 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-06-powder-berry-cloud.png differ diff --git a/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-07-sand-violet.png b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-07-sand-violet.png new file mode 100644 index 00000000..80c5d6ab Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-07-sand-violet.png differ diff --git a/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-08-muted-duotone.png b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-08-muted-duotone.png new file mode 100644 index 00000000..be7d9c0d Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-08-muted-duotone.png differ diff --git a/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-contact-sheet.png b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-contact-sheet.png new file mode 100644 index 00000000..c31e9b03 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-contact-sheet.png differ diff --git a/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-manifest.json b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-manifest.json new file mode 100644 index 00000000..335ee780 --- /dev/null +++ b/public/branding/taonier-logo-hand-spirit-muted-color-concepts/taonier-hand-spirit-muted-color-manifest.json @@ -0,0 +1,62 @@ +{ + "model": "gpt-image-2", + "endpoint": "/v1/images/edits", + "size": "1024x1024", + "referenceImage": "public\\branding\\taonier-logo-hand-spirit-ref01-logo-refine-concepts\\taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream.png", + "generatedAt": "2026-05-18T08:25:13.194Z", + "brief": { + "brand": "陶泥儿", + "goal": "低饱和度但不寡淡的年轻向颜色探索", + "keep": "保留托举曲线与半球灵体结构,不加文字、不加脸、不加星星" + }, + "variants": [ + { + "id": "01-dusty-rose-sage", + "title": "雾玫鼠尾草", + "file": "taonier-hand-spirit-muted-color-01-dusty-rose-sage.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a low-saturation color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is softer, more tasteful, more wearable brand color. The palette should feel like a modern lifestyle brand, not a toy, candy, dessert, or cosmetics ad.\nUse muted but attractive colors: dusty rose, misty lavender, sage green, smoke blue, butter cream, muted coral, terracotta clay, pale apricot. Keep the image warm and youthful without becoming loud.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette: dusty rose semi-dome, sage green support curve, warm ivory gap. Soft, modern, and feminine without being sweet." + }, + { + "id": "02-smoke-blue-apricot", + "title": "烟蓝杏橙", + "file": "taonier-hand-spirit-muted-color-02-smoke-blue-apricot.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a low-saturation color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is softer, more tasteful, more wearable brand color. The palette should feel like a modern lifestyle brand, not a toy, candy, dessert, or cosmetics ad.\nUse muted but attractive colors: dusty rose, misty lavender, sage green, smoke blue, butter cream, muted coral, terracotta clay, pale apricot. Keep the image warm and youthful without becoming loud.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette: smoke blue support curve, pale apricot or muted peach semi-dome, cream separator. Calm, fresh, and suitable for young users." + }, + { + "id": "03-misty-lilac-clay", + "title": "雾紫陶土", + "file": "taonier-hand-spirit-muted-color-03-misty-lilac-clay.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a low-saturation color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is softer, more tasteful, more wearable brand color. The palette should feel like a modern lifestyle brand, not a toy, candy, dessert, or cosmetics ad.\nUse muted but attractive colors: dusty rose, misty lavender, sage green, smoke blue, butter cream, muted coral, terracotta clay, pale apricot. Keep the image warm and youthful without becoming loud.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette: misty lilac support, soft terracotta clay spirit, off-white negative space. More boutique and refined, not purple-tech." + }, + { + "id": "04-butter-rose-tea", + "title": "黄油玫瑰茶", + "file": "taonier-hand-spirit-muted-color-04-butter-rose-tea.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a low-saturation color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is softer, more tasteful, more wearable brand color. The palette should feel like a modern lifestyle brand, not a toy, candy, dessert, or cosmetics ad.\nUse muted but attractive colors: dusty rose, misty lavender, sage green, smoke blue, butter cream, muted coral, terracotta clay, pale apricot. Keep the image warm and youthful without becoming loud.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette: butter cream spirit, muted rose support curve, faint tea-green accent. Gentle, cozy, and premium with low saturation." + }, + { + "id": "05-clay-blue-mint", + "title": "陶蓝薄荷", + "file": "taonier-hand-spirit-muted-color-05-clay-blue-mint.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a low-saturation color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is softer, more tasteful, more wearable brand color. The palette should feel like a modern lifestyle brand, not a toy, candy, dessert, or cosmetics ad.\nUse muted but attractive colors: dusty rose, misty lavender, sage green, smoke blue, butter cream, muted coral, terracotta clay, pale apricot. Keep the image warm and youthful without becoming loud.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette: clay orange or muted coral semi-dome, powder blue support, tiny mint accent. Softly playful but not heavy." + }, + { + "id": "06-powder-berry-cloud", + "title": "粉雾浆果", + "file": "taonier-hand-spirit-muted-color-06-powder-berry-cloud.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a low-saturation color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is softer, more tasteful, more wearable brand color. The palette should feel like a modern lifestyle brand, not a toy, candy, dessert, or cosmetics ad.\nUse muted but attractive colors: dusty rose, misty lavender, sage green, smoke blue, butter cream, muted coral, terracotta clay, pale apricot. Keep the image warm and youthful without becoming loud.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette: powder berry semi-dome, cloud pink support curve, warm cream gap. Youthful, gentle, and more like a boutique brand than a toy." + }, + { + "id": "07-sand-violet", + "title": "砂紫奶雾", + "file": "taonier-hand-spirit-muted-color-07-sand-violet.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a low-saturation color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is softer, more tasteful, more wearable brand color. The palette should feel like a modern lifestyle brand, not a toy, candy, dessert, or cosmetics ad.\nUse muted but attractive colors: dusty rose, misty lavender, sage green, smoke blue, butter cream, muted coral, terracotta clay, pale apricot. Keep the image warm and youthful without becoming loud.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette: sand beige or pale almond spirit, muted violet support curve, soft cream separator. Quiet, tasteful, and logo-ready." + }, + { + "id": "08-muted-duotone", + "title": "低饱双色", + "file": "taonier-hand-spirit-muted-color-08-muted-duotone.png", + "prompt": "Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.\nCreate a low-saturation color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.\nPreserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.\nThe goal is softer, more tasteful, more wearable brand color. The palette should feel like a modern lifestyle brand, not a toy, candy, dessert, or cosmetics ad.\nUse muted but attractive colors: dusty rose, misty lavender, sage green, smoke blue, butter cream, muted coral, terracotta clay, pale apricot. Keep the image warm and youthful without becoming loud.\nMake it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle polish.\nKeep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nPalette and style: two-color muted duotone only. Use one subdued warm hue and one subdued cool hue. No shiny gloss, no intense contrast, no candy feeling." + } + ] +} diff --git a/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-01-thin-outline-small-eyes.png b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-01-thin-outline-small-eyes.png new file mode 100644 index 00000000..9337613e Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-01-thin-outline-small-eyes.png differ diff --git a/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-02-medium-outline-round-eyes.png b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-02-medium-outline-round-eyes.png new file mode 100644 index 00000000..3e064048 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-02-medium-outline-round-eyes.png differ diff --git a/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-03-bold-outline-higher-eyes.png b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-03-bold-outline-higher-eyes.png new file mode 100644 index 00000000..98d05f8a Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-03-bold-outline-higher-eyes.png differ diff --git a/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-04-warm-cocoa-outline.png b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-04-warm-cocoa-outline.png new file mode 100644 index 00000000..aaf00a1a Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-04-warm-cocoa-outline.png differ diff --git a/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-05-compact-avatar-cute.png b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-05-compact-avatar-cute.png new file mode 100644 index 00000000..fffabed2 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-05-compact-avatar-cute.png differ diff --git a/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-06-black-white-first.png b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-06-black-white-first.png new file mode 100644 index 00000000..52923768 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-06-black-white-first.png differ diff --git a/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-07-soft-feminine-cute.png b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-07-soft-feminine-cute.png new file mode 100644 index 00000000..1b1cc20b Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-07-soft-feminine-cute.png differ diff --git a/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-08-vector-ready-cute.png b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-08-vector-ready-cute.png new file mode 100644 index 00000000..a70e5074 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-08-vector-ready-cute.png differ diff --git a/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-contact-sheet.png b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-contact-sheet.png new file mode 100644 index 00000000..73cce156 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-contact-sheet.png differ diff --git a/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-manifest.json b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-manifest.json new file mode 100644 index 00000000..26fc6d1a --- /dev/null +++ b/public/branding/taonier-logo-hand-spirit-outline-eye-concepts/taonier-hand-spirit-outline-eye-manifest.json @@ -0,0 +1,62 @@ +{ + "model": "gpt-image-2", + "endpoint": "/v1/images/edits", + "size": "1024x1024", + "referenceImage": "public\\branding\\taonier-logo-hand-spirit-ref01-logo-refine-concepts\\taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream.png", + "generatedAt": "2026-05-18T08:56:23.597Z", + "brief": { + "brand": "陶泥儿", + "goal": "在上轮 01 的基础上加入描边和黑点眼睛,让标志更可爱", + "keep": "保留托举曲线与半球灵体结构,不加文字、不加星星、不改骨架" + }, + "variants": [ + { + "id": "01-thin-outline-small-eyes", + "title": "细描边小眼", + "file": "taonier-hand-spirit-outline-eye-01-thin-outline-small-eyes.png", + "prompt": "Use the uploaded logo as the exact structural reference. Keep the same icon-only composition: a soft semi-dome spirit above a smooth abstract hand-like support curve.\nCreate a cuter, more logo-like refinement. Add a clean outline around the whole symbol, and add two pure matte black dot eyes to the upper semi-dome part only.\nThe eyes must belong to the upper half-circle spirit, not the lower support curve. No nose, no mouth, no eyebrows, no blush, no limbs, no character body, no star, no spark, no text.\nDo not redesign the overall structure. Keep the same proportions, centered layout, and soft brand silhouette. Only make it more cute, more memorable, and more trademark-like.\nThe outline should feel clean, friendly, and brand-ready, not sticker-like, not cartoonish, not too thick unless the variant asks for it.\nUse the same soft warm palette family as the reference, but allow subtle low-saturation tweaks to improve charm and contrast. Keep it elegant and youthful, not loud.\nKeep 32px readability and black-white viability. It must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nVariant focus: the most restrained cute version. Use a thin warm outline and small matte black dot eyes with calm spacing." + }, + { + "id": "02-medium-outline-round-eyes", + "title": "中描边圆眼", + "file": "taonier-hand-spirit-outline-eye-02-medium-outline-round-eyes.png", + "prompt": "Use the uploaded logo as the exact structural reference. Keep the same icon-only composition: a soft semi-dome spirit above a smooth abstract hand-like support curve.\nCreate a cuter, more logo-like refinement. Add a clean outline around the whole symbol, and add two pure matte black dot eyes to the upper semi-dome part only.\nThe eyes must belong to the upper half-circle spirit, not the lower support curve. No nose, no mouth, no eyebrows, no blush, no limbs, no character body, no star, no spark, no text.\nDo not redesign the overall structure. Keep the same proportions, centered layout, and soft brand silhouette. Only make it more cute, more memorable, and more trademark-like.\nThe outline should feel clean, friendly, and brand-ready, not sticker-like, not cartoonish, not too thick unless the variant asks for it.\nUse the same soft warm palette family as the reference, but allow subtle low-saturation tweaks to improve charm and contrast. Keep it elegant and youthful, not loud.\nKeep 32px readability and black-white viability. It must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nVariant focus: medium outline thickness and slightly rounder dot eyes. Make the face read a touch more openly cute, but still minimal." + }, + { + "id": "03-bold-outline-higher-eyes", + "title": "粗描边高眼", + "file": "taonier-hand-spirit-outline-eye-03-bold-outline-higher-eyes.png", + "prompt": "Use the uploaded logo as the exact structural reference. Keep the same icon-only composition: a soft semi-dome spirit above a smooth abstract hand-like support curve.\nCreate a cuter, more logo-like refinement. Add a clean outline around the whole symbol, and add two pure matte black dot eyes to the upper semi-dome part only.\nThe eyes must belong to the upper half-circle spirit, not the lower support curve. No nose, no mouth, no eyebrows, no blush, no limbs, no character body, no star, no spark, no text.\nDo not redesign the overall structure. Keep the same proportions, centered layout, and soft brand silhouette. Only make it more cute, more memorable, and more trademark-like.\nThe outline should feel clean, friendly, and brand-ready, not sticker-like, not cartoonish, not too thick unless the variant asks for it.\nUse the same soft warm palette family as the reference, but allow subtle low-saturation tweaks to improve charm and contrast. Keep it elegant and youthful, not loud.\nKeep 32px readability and black-white viability. It must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nVariant focus: stronger bold outline and eyes placed a little higher on the upper dome, creating a sweeter peeking expression." + }, + { + "id": "04-warm-cocoa-outline", + "title": "暖可可描边", + "file": "taonier-hand-spirit-outline-eye-04-warm-cocoa-outline.png", + "prompt": "Use the uploaded logo as the exact structural reference. Keep the same icon-only composition: a soft semi-dome spirit above a smooth abstract hand-like support curve.\nCreate a cuter, more logo-like refinement. Add a clean outline around the whole symbol, and add two pure matte black dot eyes to the upper semi-dome part only.\nThe eyes must belong to the upper half-circle spirit, not the lower support curve. No nose, no mouth, no eyebrows, no blush, no limbs, no character body, no star, no spark, no text.\nDo not redesign the overall structure. Keep the same proportions, centered layout, and soft brand silhouette. Only make it more cute, more memorable, and more trademark-like.\nThe outline should feel clean, friendly, and brand-ready, not sticker-like, not cartoonish, not too thick unless the variant asks for it.\nUse the same soft warm palette family as the reference, but allow subtle low-saturation tweaks to improve charm and contrast. Keep it elegant and youthful, not loud.\nKeep 32px readability and black-white viability. It must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nVariant focus: use a warm cocoa or deep beige outline that makes the logo feel softer and more plush, with small centered black eyes." + }, + { + "id": "05-compact-avatar-cute", + "title": "头像可爱款", + "file": "taonier-hand-spirit-outline-eye-05-compact-avatar-cute.png", + "prompt": "Use the uploaded logo as the exact structural reference. Keep the same icon-only composition: a soft semi-dome spirit above a smooth abstract hand-like support curve.\nCreate a cuter, more logo-like refinement. Add a clean outline around the whole symbol, and add two pure matte black dot eyes to the upper semi-dome part only.\nThe eyes must belong to the upper half-circle spirit, not the lower support curve. No nose, no mouth, no eyebrows, no blush, no limbs, no character body, no star, no spark, no text.\nDo not redesign the overall structure. Keep the same proportions, centered layout, and soft brand silhouette. Only make it more cute, more memorable, and more trademark-like.\nThe outline should feel clean, friendly, and brand-ready, not sticker-like, not cartoonish, not too thick unless the variant asks for it.\nUse the same soft warm palette family as the reference, but allow subtle low-saturation tweaks to improve charm and contrast. Keep it elegant and youthful, not loud.\nKeep 32px readability and black-white viability. It must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nVariant focus: compact avatar readability. Enlarge the upper dome slightly, keep the hand support bold, and make the eyes more visible without adding any mouth." + }, + { + "id": "06-black-white-first", + "title": "黑白优先", + "file": "taonier-hand-spirit-outline-eye-06-black-white-first.png", + "prompt": "Use the uploaded logo as the exact structural reference. Keep the same icon-only composition: a soft semi-dome spirit above a smooth abstract hand-like support curve.\nCreate a cuter, more logo-like refinement. Add a clean outline around the whole symbol, and add two pure matte black dot eyes to the upper semi-dome part only.\nThe eyes must belong to the upper half-circle spirit, not the lower support curve. No nose, no mouth, no eyebrows, no blush, no limbs, no character body, no star, no spark, no text.\nDo not redesign the overall structure. Keep the same proportions, centered layout, and soft brand silhouette. Only make it more cute, more memorable, and more trademark-like.\nThe outline should feel clean, friendly, and brand-ready, not sticker-like, not cartoonish, not too thick unless the variant asks for it.\nUse the same soft warm palette family as the reference, but allow subtle low-saturation tweaks to improve charm and contrast. Keep it elegant and youthful, not loud.\nKeep 32px readability and black-white viability. It must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nVariant focus: black-and-white survival first. Make the outline and the eyes work clearly even if all color is removed. Very strong logo readability." + }, + { + "id": "07-soft-feminine-cute", + "title": "柔和少女感", + "file": "taonier-hand-spirit-outline-eye-07-soft-feminine-cute.png", + "prompt": "Use the uploaded logo as the exact structural reference. Keep the same icon-only composition: a soft semi-dome spirit above a smooth abstract hand-like support curve.\nCreate a cuter, more logo-like refinement. Add a clean outline around the whole symbol, and add two pure matte black dot eyes to the upper semi-dome part only.\nThe eyes must belong to the upper half-circle spirit, not the lower support curve. No nose, no mouth, no eyebrows, no blush, no limbs, no character body, no star, no spark, no text.\nDo not redesign the overall structure. Keep the same proportions, centered layout, and soft brand silhouette. Only make it more cute, more memorable, and more trademark-like.\nThe outline should feel clean, friendly, and brand-ready, not sticker-like, not cartoonish, not too thick unless the variant asks for it.\nUse the same soft warm palette family as the reference, but allow subtle low-saturation tweaks to improve charm and contrast. Keep it elegant and youthful, not loud.\nKeep 32px readability and black-white viability. It must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nVariant focus: a softer feminine-cute version. Keep the outline elegant and the eyes gentle; the whole mark should feel like a friendly brand mascot symbol." + }, + { + "id": "08-vector-ready-cute", + "title": "矢量定稿感", + "file": "taonier-hand-spirit-outline-eye-08-vector-ready-cute.png", + "prompt": "Use the uploaded logo as the exact structural reference. Keep the same icon-only composition: a soft semi-dome spirit above a smooth abstract hand-like support curve.\nCreate a cuter, more logo-like refinement. Add a clean outline around the whole symbol, and add two pure matte black dot eyes to the upper semi-dome part only.\nThe eyes must belong to the upper half-circle spirit, not the lower support curve. No nose, no mouth, no eyebrows, no blush, no limbs, no character body, no star, no spark, no text.\nDo not redesign the overall structure. Keep the same proportions, centered layout, and soft brand silhouette. Only make it more cute, more memorable, and more trademark-like.\nThe outline should feel clean, friendly, and brand-ready, not sticker-like, not cartoonish, not too thick unless the variant asks for it.\nUse the same soft warm palette family as the reference, but allow subtle low-saturation tweaks to improve charm and contrast. Keep it elegant and youthful, not loud.\nKeep 32px readability and black-white viability. It must still work as an app icon, social avatar, and trademark symbol.\nClean light background, generous safe area. Image-only logo concept.\nVariant focus: designer-ready vector concept. Clean crisp outline, balanced eye spacing, no decorative detail, very easy to trace into an SVG mark." + } + ] +} diff --git a/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream.png b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream.png new file mode 100644 index 00000000..ec552a7d Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream.png differ diff --git a/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-02-warm-clay-premium.png b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-02-warm-clay-premium.png new file mode 100644 index 00000000..dd5f35f2 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-02-warm-clay-premium.png differ diff --git a/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-03-mint-support.png b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-03-mint-support.png new file mode 100644 index 00000000..8b3e5d06 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-03-mint-support.png differ diff --git a/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-04-outline-vector.png b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-04-outline-vector.png new file mode 100644 index 00000000..2b8aba02 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-04-outline-vector.png differ diff --git a/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-05-abstract-two-shape.png b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-05-abstract-two-shape.png new file mode 100644 index 00000000..5237ee53 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-05-abstract-two-shape.png differ diff --git a/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-06-monochrome-first.png b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-06-monochrome-first.png new file mode 100644 index 00000000..713b2e33 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-06-monochrome-first.png differ diff --git a/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-07-soft-gradient-premium.png b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-07-soft-gradient-premium.png new file mode 100644 index 00000000..49f19aff Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-07-soft-gradient-premium.png differ diff --git a/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-08-compact-avatar.png b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-08-compact-avatar.png new file mode 100644 index 00000000..49a54015 Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-hand-spirit-ref01-logo-refine-08-compact-avatar.png differ diff --git a/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-logo-hand-spirit-ref01-logo-refine-contact-sheet.png b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-logo-hand-spirit-ref01-logo-refine-contact-sheet.png new file mode 100644 index 00000000..11fdd96a Binary files /dev/null and b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-logo-hand-spirit-ref01-logo-refine-contact-sheet.png differ diff --git a/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-logo-hand-spirit-ref01-logo-refine-manifest.json b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-logo-hand-spirit-ref01-logo-refine-manifest.json new file mode 100644 index 00000000..0a6922b8 --- /dev/null +++ b/public/branding/taonier-logo-hand-spirit-ref01-logo-refine-concepts/taonier-logo-hand-spirit-ref01-logo-refine-manifest.json @@ -0,0 +1,89 @@ +{ + "model": "gpt-image-2", + "endpoint": "/v1/images/edits", + "size": "1024x1024", + "referenceImage": "public\\branding\\taonier-logo-hand-spirit-concepts\\taonier-hand-spirit-01-gentle-hand-spirit.png", + "generatedAt": "2026-05-18T07:29:31.615Z", + "logoSkillSummary": { + "requiredReview": "visual inspection, 32px readability, black-white viability", + "outputStatus": "AI concept only; final logo needs vector cleanup" + }, + "brief": { + "brand": "陶泥儿", + "source": "基于 taonier-hand-spirit-01-gentle-hand-spirit 做商标化探索", + "logoType": "symbol/icon-only mark, no wordmark", + "product": "AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品", + "keep": [ + "上方软萌半球陶泥灵体", + "下方抽象托举手势/掌形曲线", + "托举、传递、分享的动作语义", + "无脸、无文字、无星星", + "亲和、精品、可用于商标和 App 图标" + ], + "explore": [ + "更扁平的纯色块版本", + "更精品的低饱和陶器色版本", + "更强线面结构版本", + "更抽象、更少形状的版本", + "更适合 32px 和黑白化的版本" + ], + "avoid": [ + "中文或英文字", + "眼睛、嘴巴、表情、角色身体", + "星星、闪光、魔法符号", + "真实手指、宗教托举、医疗护理感", + "面包、甜点、糖果、果冻、奶油、食物包装", + "复杂背景、边框、UI、按钮、水印" + ] + }, + "variants": [ + { + "id": "01-flat-coral-cream", + "title": "扁平珊瑚奶白", + "file": "taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream.png", + "prompt": "Use the uploaded reference image as the composition and shape reference. Keep the same core structure: an abstract hand-like support curve below, gently holding a soft semi-dome clay spirit above.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the brand idea: fun creation, sharing, passing a small playable idea forward. The mark should feel like a small creative object being gently offered, not a scene or illustration.\nKeep the subject face-free: no eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nMake it more like a trademark and logo than the reference: fewer shapes, cleaner silhouette, clearer positive/negative structure, more vector-friendly curves, broad flat color regions, less glossy highlight, less painterly shadow.\nThe hand must remain abstract: a smooth palm-like support curve or bowl-like gesture, not realistic fingers, not a real human hand, not worship, not medical care.\nThe spirit must remain a soft irregular semi-dome or rounded clay core. It should not become a planet, helmet, bread, bun, mochi, dumpling, pastry, candy, jelly, sauce, dessert, or food package.\nStyle target: premium friendly logo mark, all-age and women-friendly, warm but not childish, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid UI, border, sticker outline unless explicitly requested by the variant, background scene, watermark, extra marks, and any text.\nVariant focus: the most direct flat-logo refinement. Use coral-orange clay spirit and cream hand support. Reduce the glossy highlight to nearly zero. Use 3-4 crisp flat shapes only." + }, + { + "id": "02-warm-clay-premium", + "title": "暖陶精品色", + "file": "taonier-hand-spirit-ref01-logo-refine-02-warm-clay-premium.png", + "prompt": "Use the uploaded reference image as the composition and shape reference. Keep the same core structure: an abstract hand-like support curve below, gently holding a soft semi-dome clay spirit above.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the brand idea: fun creation, sharing, passing a small playable idea forward. The mark should feel like a small creative object being gently offered, not a scene or illustration.\nKeep the subject face-free: no eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nMake it more like a trademark and logo than the reference: fewer shapes, cleaner silhouette, clearer positive/negative structure, more vector-friendly curves, broad flat color regions, less glossy highlight, less painterly shadow.\nThe hand must remain abstract: a smooth palm-like support curve or bowl-like gesture, not realistic fingers, not a real human hand, not worship, not medical care.\nThe spirit must remain a soft irregular semi-dome or rounded clay core. It should not become a planet, helmet, bread, bun, mochi, dumpling, pastry, candy, jelly, sauce, dessert, or food package.\nStyle target: premium friendly logo mark, all-age and women-friendly, warm but not childish, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid UI, border, sticker outline unless explicitly requested by the variant, background scene, watermark, extra marks, and any text.\nVariant focus: warmer boutique clay palette. Muted terracotta, soft sand, and warm ivory. More mature and premium, with a compact iconic silhouette and no candy gloss." + }, + { + "id": "03-mint-support", + "title": "青绿托举线", + "file": "taonier-hand-spirit-ref01-logo-refine-03-mint-support.png", + "prompt": "Use the uploaded reference image as the composition and shape reference. Keep the same core structure: an abstract hand-like support curve below, gently holding a soft semi-dome clay spirit above.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the brand idea: fun creation, sharing, passing a small playable idea forward. The mark should feel like a small creative object being gently offered, not a scene or illustration.\nKeep the subject face-free: no eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nMake it more like a trademark and logo than the reference: fewer shapes, cleaner silhouette, clearer positive/negative structure, more vector-friendly curves, broad flat color regions, less glossy highlight, less painterly shadow.\nThe hand must remain abstract: a smooth palm-like support curve or bowl-like gesture, not realistic fingers, not a real human hand, not worship, not medical care.\nThe spirit must remain a soft irregular semi-dome or rounded clay core. It should not become a planet, helmet, bread, bun, mochi, dumpling, pastry, candy, jelly, sauce, dessert, or food package.\nStyle target: premium friendly logo mark, all-age and women-friendly, warm but not childish, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid UI, border, sticker outline unless explicitly requested by the variant, background scene, watermark, extra marks, and any text.\nVariant focus: stronger color memory. Use a clear muted teal or mint support curve as the hand and a warm peach clay spirit above. Keep it flat, balanced, and not cosmetic." + }, + { + "id": "04-outline-vector", + "title": "线面商标", + "file": "taonier-hand-spirit-ref01-logo-refine-04-outline-vector.png", + "prompt": "Use the uploaded reference image as the composition and shape reference. Keep the same core structure: an abstract hand-like support curve below, gently holding a soft semi-dome clay spirit above.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the brand idea: fun creation, sharing, passing a small playable idea forward. The mark should feel like a small creative object being gently offered, not a scene or illustration.\nKeep the subject face-free: no eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nMake it more like a trademark and logo than the reference: fewer shapes, cleaner silhouette, clearer positive/negative structure, more vector-friendly curves, broad flat color regions, less glossy highlight, less painterly shadow.\nThe hand must remain abstract: a smooth palm-like support curve or bowl-like gesture, not realistic fingers, not a real human hand, not worship, not medical care.\nThe spirit must remain a soft irregular semi-dome or rounded clay core. It should not become a planet, helmet, bread, bun, mochi, dumpling, pastry, candy, jelly, sauce, dessert, or food package.\nStyle target: premium friendly logo mark, all-age and women-friendly, warm but not childish, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid UI, border, sticker outline unless explicitly requested by the variant, background scene, watermark, extra marks, and any text.\nVariant focus: bolder trademark construction. Use a clean warm-brown contour line combined with flat fills. The outline should clarify the hand and spirit silhouette, modern rather than sticker-like." + }, + { + "id": "05-abstract-two-shape", + "title": "双形抽象", + "file": "taonier-hand-spirit-ref01-logo-refine-05-abstract-two-shape.png", + "prompt": "Use the uploaded reference image as the composition and shape reference. Keep the same core structure: an abstract hand-like support curve below, gently holding a soft semi-dome clay spirit above.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the brand idea: fun creation, sharing, passing a small playable idea forward. The mark should feel like a small creative object being gently offered, not a scene or illustration.\nKeep the subject face-free: no eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nMake it more like a trademark and logo than the reference: fewer shapes, cleaner silhouette, clearer positive/negative structure, more vector-friendly curves, broad flat color regions, less glossy highlight, less painterly shadow.\nThe hand must remain abstract: a smooth palm-like support curve or bowl-like gesture, not realistic fingers, not a real human hand, not worship, not medical care.\nThe spirit must remain a soft irregular semi-dome or rounded clay core. It should not become a planet, helmet, bread, bun, mochi, dumpling, pastry, candy, jelly, sauce, dessert, or food package.\nStyle target: premium friendly logo mark, all-age and women-friendly, warm but not childish, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid UI, border, sticker outline unless explicitly requested by the variant, background scene, watermark, extra marks, and any text.\nVariant focus: higher abstraction. Reduce the mark to two dominant shapes: one semi-dome spirit and one sweeping hand support. Remove highlight details. Make the silhouette distinctive and vector-ready." + }, + { + "id": "06-monochrome-first", + "title": "黑白优先", + "file": "taonier-hand-spirit-ref01-logo-refine-06-monochrome-first.png", + "prompt": "Use the uploaded reference image as the composition and shape reference. Keep the same core structure: an abstract hand-like support curve below, gently holding a soft semi-dome clay spirit above.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the brand idea: fun creation, sharing, passing a small playable idea forward. The mark should feel like a small creative object being gently offered, not a scene or illustration.\nKeep the subject face-free: no eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nMake it more like a trademark and logo than the reference: fewer shapes, cleaner silhouette, clearer positive/negative structure, more vector-friendly curves, broad flat color regions, less glossy highlight, less painterly shadow.\nThe hand must remain abstract: a smooth palm-like support curve or bowl-like gesture, not realistic fingers, not a real human hand, not worship, not medical care.\nThe spirit must remain a soft irregular semi-dome or rounded clay core. It should not become a planet, helmet, bread, bun, mochi, dumpling, pastry, candy, jelly, sauce, dessert, or food package.\nStyle target: premium friendly logo mark, all-age and women-friendly, warm but not childish, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid UI, border, sticker outline unless explicitly requested by the variant, background scene, watermark, extra marks, and any text.\nVariant focus: black-and-white survival first. Design with strong positive and negative shapes; color may be warm clay, but the mark must remain clear as a pure monochrome logo." + }, + { + "id": "07-soft-gradient-premium", + "title": "轻渐变精品", + "file": "taonier-hand-spirit-ref01-logo-refine-07-soft-gradient-premium.png", + "prompt": "Use the uploaded reference image as the composition and shape reference. Keep the same core structure: an abstract hand-like support curve below, gently holding a soft semi-dome clay spirit above.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the brand idea: fun creation, sharing, passing a small playable idea forward. The mark should feel like a small creative object being gently offered, not a scene or illustration.\nKeep the subject face-free: no eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nMake it more like a trademark and logo than the reference: fewer shapes, cleaner silhouette, clearer positive/negative structure, more vector-friendly curves, broad flat color regions, less glossy highlight, less painterly shadow.\nThe hand must remain abstract: a smooth palm-like support curve or bowl-like gesture, not realistic fingers, not a real human hand, not worship, not medical care.\nThe spirit must remain a soft irregular semi-dome or rounded clay core. It should not become a planet, helmet, bread, bun, mochi, dumpling, pastry, candy, jelly, sauce, dessert, or food package.\nStyle target: premium friendly logo mark, all-age and women-friendly, warm but not childish, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid UI, border, sticker outline unless explicitly requested by the variant, background scene, watermark, extra marks, and any text.\nVariant focus: a polished but still logo-like version. Allow only very subtle broad gradients for premium softness. Remove small glossy highlights and avoid 3D rendering." + }, + { + "id": "08-compact-avatar", + "title": "头像强识别", + "file": "taonier-hand-spirit-ref01-logo-refine-08-compact-avatar.png", + "prompt": "Use the uploaded reference image as the composition and shape reference. Keep the same core structure: an abstract hand-like support curve below, gently holding a soft semi-dome clay spirit above.\nCreate a refined icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nPreserve the brand idea: fun creation, sharing, passing a small playable idea forward. The mark should feel like a small creative object being gently offered, not a scene or illustration.\nKeep the subject face-free: no eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.\nMake it more like a trademark and logo than the reference: fewer shapes, cleaner silhouette, clearer positive/negative structure, more vector-friendly curves, broad flat color regions, less glossy highlight, less painterly shadow.\nThe hand must remain abstract: a smooth palm-like support curve or bowl-like gesture, not realistic fingers, not a real human hand, not worship, not medical care.\nThe spirit must remain a soft irregular semi-dome or rounded clay core. It should not become a planet, helmet, bread, bun, mochi, dumpling, pastry, candy, jelly, sauce, dessert, or food package.\nStyle target: premium friendly logo mark, all-age and women-friendly, warm but not childish, suitable for app icon, social avatar, and trademark review.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.\nAvoid UI, border, sticker outline unless explicitly requested by the variant, background scene, watermark, extra marks, and any text.\nVariant focus: compact social-avatar readability. Enlarge the clay spirit slightly, thicken the hand support curve, reduce thin gaps, and keep the total mark bold at 32px." + } + ] +} diff --git a/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-bowl.png b/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-bowl.png new file mode 100644 index 00000000..14b9e2d4 Binary files /dev/null and b/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-bowl.png differ diff --git a/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-clap.png b/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-clap.png new file mode 100644 index 00000000..36f5f5d0 Binary files /dev/null and b/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-clap.png differ diff --git a/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-cradle.png b/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-cradle.png new file mode 100644 index 00000000..c4cfa074 Binary files /dev/null and b/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-cradle.png differ diff --git a/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-pop.png b/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-pop.png new file mode 100644 index 00000000..41682d9b Binary files /dev/null and b/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-pop.png differ diff --git a/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-seal.png b/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-seal.png new file mode 100644 index 00000000..49e3363f Binary files /dev/null and b/public/branding/taonier-logo-hands-concepts/taonier-hands-v2-seal.png differ diff --git a/public/branding/taonier-logo-hands-concepts/taonier-logo-hands-contact-sheet.png b/public/branding/taonier-logo-hands-concepts/taonier-logo-hands-contact-sheet.png new file mode 100644 index 00000000..9a7867b0 Binary files /dev/null and b/public/branding/taonier-logo-hands-concepts/taonier-logo-hands-contact-sheet.png differ diff --git a/public/branding/taonier-logo-mascot-symbol-concepts/taonier-logo-mascot-symbol-contact-sheet.png b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-logo-mascot-symbol-contact-sheet.png new file mode 100644 index 00000000..e4257fac Binary files /dev/null and b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-logo-mascot-symbol-contact-sheet.png differ diff --git a/public/branding/taonier-logo-mascot-symbol-concepts/taonier-logo-mascot-symbol-manifest.json b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-logo-mascot-symbol-manifest.json new file mode 100644 index 00000000..e9e220cd --- /dev/null +++ b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-logo-mascot-symbol-manifest.json @@ -0,0 +1,84 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T21:47:40.073Z", + "logoSkillSummary": { + "requiredReview": "visual inspection, 32px readability, black-white viability", + "outputStatus": "AI concept only; final logo needs vector cleanup" + }, + "brief": { + "brand": "陶泥儿", + "coreBelief": "好玩会创造", + "logoType": "symbol/icon-only mascot mark, no wordmark", + "product": "AI UGC 轻休闲小游戏创作与传播平台,用户像捏陶泥一样把脑洞、梗和灵感塑造成可分享的作品", + "direction": "抽象吉祥物符号,从人形、精灵、怪物、动物等形态提炼", + "audience": "女性用户友好、全年龄向、年轻明亮但不低幼", + "mascotRules": [ + "必须是 logo 符号级别,不是完整角色立绘", + "轮廓要有记忆点,32px 可读", + "表情最多极简两点或无表情", + "身体结构要高度抽象、可矢量化", + "保留一点陶泥被捏出的柔软感" + ], + "avoid": [ + "复杂角色", + "儿童玩具感", + "怪物恐怖感", + "真实动物", + "食品感", + "文字", + "星星", + "写实泥土纹理" + ] + }, + "variants": [ + { + "id": "01-clay-humanoid", + "title": "抽象人形", + "file": "taonier-mascot-symbol-01-clay-humanoid.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nLogo type: abstract mascot symbol only. It must be a simple brand mark, not a full character illustration, not a scene, not a sticker sheet, not an app-icon rounded-square background.\nMascot meaning: a soft clay-born little being that represents playful creation, shareable ideas, and a friendly AI/UGC companion.\nStyle: modern minimalist vector mascot mark, compact silhouette, broad simple shapes, fresh bright palette, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nCharacter abstraction: use a highly simplified head/body silhouette or creature glyph. No detailed anatomy. No clothing. No props. No complex limbs. Optional face should be extremely minimal, at most two small eyes; no mouth unless it is a tiny simple mark.\nShape language: soft, rounded, slightly irregular, as if gently pinched from clay, but clean and designer-ready. Friendly and memorable, not childish.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance: do not make it look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, dessert, or food packaging.\nAvoid scary monster, aggressive teeth, claws, realistic animal, pet-shop icon, emoji face, toy mascot, chibi over-detailing, or generic blob.\nNo star, no spark, no halo, no magic wand, no hand, no pottery tool, no UI, no border, no watermark.\nComposition: centered on a clean light background, generous safe area. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the mascot silhouette should be recognizable at 32px.\nVariant focus: abstract humanoid mascot. A tiny soft clay person-like glyph with rounded head and merged body, no limbs or very minimal arms, friendly but not childish." + }, + { + "id": "02-clay-sprite", + "title": "陶泥精灵", + "file": "taonier-mascot-symbol-02-clay-sprite.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nLogo type: abstract mascot symbol only. It must be a simple brand mark, not a full character illustration, not a scene, not a sticker sheet, not an app-icon rounded-square background.\nMascot meaning: a soft clay-born little being that represents playful creation, shareable ideas, and a friendly AI/UGC companion.\nStyle: modern minimalist vector mascot mark, compact silhouette, broad simple shapes, fresh bright palette, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nCharacter abstraction: use a highly simplified head/body silhouette or creature glyph. No detailed anatomy. No clothing. No props. No complex limbs. Optional face should be extremely minimal, at most two small eyes; no mouth unless it is a tiny simple mark.\nShape language: soft, rounded, slightly irregular, as if gently pinched from clay, but clean and designer-ready. Friendly and memorable, not childish.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance: do not make it look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, dessert, or food packaging.\nAvoid scary monster, aggressive teeth, claws, realistic animal, pet-shop icon, emoji face, toy mascot, chibi over-detailing, or generic blob.\nNo star, no spark, no halo, no magic wand, no hand, no pottery tool, no UI, no border, no watermark.\nComposition: centered on a clean light background, generous safe area. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the mascot silhouette should be recognizable at 32px.\nVariant focus: clay sprite. A small semi-dome spirit with a gentle lifted silhouette, like a friendly creative helper, no wings, no magic stars, no fantasy clutter." + }, + { + "id": "03-soft-monster", + "title": "软萌怪物", + "file": "taonier-mascot-symbol-03-soft-monster.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nLogo type: abstract mascot symbol only. It must be a simple brand mark, not a full character illustration, not a scene, not a sticker sheet, not an app-icon rounded-square background.\nMascot meaning: a soft clay-born little being that represents playful creation, shareable ideas, and a friendly AI/UGC companion.\nStyle: modern minimalist vector mascot mark, compact silhouette, broad simple shapes, fresh bright palette, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nCharacter abstraction: use a highly simplified head/body silhouette or creature glyph. No detailed anatomy. No clothing. No props. No complex limbs. Optional face should be extremely minimal, at most two small eyes; no mouth unless it is a tiny simple mark.\nShape language: soft, rounded, slightly irregular, as if gently pinched from clay, but clean and designer-ready. Friendly and memorable, not childish.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance: do not make it look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, dessert, or food packaging.\nAvoid scary monster, aggressive teeth, claws, realistic animal, pet-shop icon, emoji face, toy mascot, chibi over-detailing, or generic blob.\nNo star, no spark, no halo, no magic wand, no hand, no pottery tool, no UI, no border, no watermark.\nComposition: centered on a clean light background, generous safe area. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the mascot silhouette should be recognizable at 32px.\nVariant focus: soft friendly monster glyph. Cute but not scary, no teeth, no claws, one distinctive head shape, perhaps tiny horn-like soft bumps but not devilish." + }, + { + "id": "04-animal-abstract", + "title": "抽象动物", + "file": "taonier-mascot-symbol-04-animal-abstract.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nLogo type: abstract mascot symbol only. It must be a simple brand mark, not a full character illustration, not a scene, not a sticker sheet, not an app-icon rounded-square background.\nMascot meaning: a soft clay-born little being that represents playful creation, shareable ideas, and a friendly AI/UGC companion.\nStyle: modern minimalist vector mascot mark, compact silhouette, broad simple shapes, fresh bright palette, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nCharacter abstraction: use a highly simplified head/body silhouette or creature glyph. No detailed anatomy. No clothing. No props. No complex limbs. Optional face should be extremely minimal, at most two small eyes; no mouth unless it is a tiny simple mark.\nShape language: soft, rounded, slightly irregular, as if gently pinched from clay, but clean and designer-ready. Friendly and memorable, not childish.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance: do not make it look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, dessert, or food packaging.\nAvoid scary monster, aggressive teeth, claws, realistic animal, pet-shop icon, emoji face, toy mascot, chibi over-detailing, or generic blob.\nNo star, no spark, no halo, no magic wand, no hand, no pottery tool, no UI, no border, no watermark.\nComposition: centered on a clean light background, generous safe area. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the mascot silhouette should be recognizable at 32px.\nVariant focus: abstract animal-like mascot. Suggest a small rounded creature through ears or tail-like curves, but not a specific real animal and not pet logo." + }, + { + "id": "05-clay-orb-being", + "title": "泥团小灵", + "file": "taonier-mascot-symbol-05-clay-orb-being.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nLogo type: abstract mascot symbol only. It must be a simple brand mark, not a full character illustration, not a scene, not a sticker sheet, not an app-icon rounded-square background.\nMascot meaning: a soft clay-born little being that represents playful creation, shareable ideas, and a friendly AI/UGC companion.\nStyle: modern minimalist vector mascot mark, compact silhouette, broad simple shapes, fresh bright palette, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nCharacter abstraction: use a highly simplified head/body silhouette or creature glyph. No detailed anatomy. No clothing. No props. No complex limbs. Optional face should be extremely minimal, at most two small eyes; no mouth unless it is a tiny simple mark.\nShape language: soft, rounded, slightly irregular, as if gently pinched from clay, but clean and designer-ready. Friendly and memorable, not childish.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance: do not make it look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, dessert, or food packaging.\nAvoid scary monster, aggressive teeth, claws, realistic animal, pet-shop icon, emoji face, toy mascot, chibi over-detailing, or generic blob.\nNo star, no spark, no halo, no magic wand, no hand, no pottery tool, no UI, no border, no watermark.\nComposition: centered on a clean light background, generous safe area. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the mascot silhouette should be recognizable at 32px.\nVariant focus: orb-like clay being. A simple irregular rounded body with minimal face or no face, strong silhouette, playful creation companion." + }, + { + "id": "06-playful-creature", + "title": "轻玩小怪", + "file": "taonier-mascot-symbol-06-playful-creature.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nLogo type: abstract mascot symbol only. It must be a simple brand mark, not a full character illustration, not a scene, not a sticker sheet, not an app-icon rounded-square background.\nMascot meaning: a soft clay-born little being that represents playful creation, shareable ideas, and a friendly AI/UGC companion.\nStyle: modern minimalist vector mascot mark, compact silhouette, broad simple shapes, fresh bright palette, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nCharacter abstraction: use a highly simplified head/body silhouette or creature glyph. No detailed anatomy. No clothing. No props. No complex limbs. Optional face should be extremely minimal, at most two small eyes; no mouth unless it is a tiny simple mark.\nShape language: soft, rounded, slightly irregular, as if gently pinched from clay, but clean and designer-ready. Friendly and memorable, not childish.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance: do not make it look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, dessert, or food packaging.\nAvoid scary monster, aggressive teeth, claws, realistic animal, pet-shop icon, emoji face, toy mascot, chibi over-detailing, or generic blob.\nNo star, no spark, no halo, no magic wand, no hand, no pottery tool, no UI, no border, no watermark.\nComposition: centered on a clean light background, generous safe area. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the mascot silhouette should be recognizable at 32px.\nVariant focus: more playful creature mark. Dynamic but compact, one distinctive asymmetric curve, readable at 32px, still premium and not a toy brand." + }, + { + "id": "07-avatar-readable", + "title": "头像可读", + "file": "taonier-mascot-symbol-07-avatar-readable.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nLogo type: abstract mascot symbol only. It must be a simple brand mark, not a full character illustration, not a scene, not a sticker sheet, not an app-icon rounded-square background.\nMascot meaning: a soft clay-born little being that represents playful creation, shareable ideas, and a friendly AI/UGC companion.\nStyle: modern minimalist vector mascot mark, compact silhouette, broad simple shapes, fresh bright palette, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nCharacter abstraction: use a highly simplified head/body silhouette or creature glyph. No detailed anatomy. No clothing. No props. No complex limbs. Optional face should be extremely minimal, at most two small eyes; no mouth unless it is a tiny simple mark.\nShape language: soft, rounded, slightly irregular, as if gently pinched from clay, but clean and designer-ready. Friendly and memorable, not childish.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance: do not make it look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, dessert, or food packaging.\nAvoid scary monster, aggressive teeth, claws, realistic animal, pet-shop icon, emoji face, toy mascot, chibi over-detailing, or generic blob.\nNo star, no spark, no halo, no magic wand, no hand, no pottery tool, no UI, no border, no watermark.\nComposition: centered on a clean light background, generous safe area. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the mascot silhouette should be recognizable at 32px.\nVariant focus: social avatar and favicon readability. Bold compact mascot head/body silhouette, minimal inner detail, high black-and-white clarity." + }, + { + "id": "08-vector-ready", + "title": "矢量符号感", + "file": "taonier-mascot-symbol-08-vector-ready.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.\nLogo type: abstract mascot symbol only. It must be a simple brand mark, not a full character illustration, not a scene, not a sticker sheet, not an app-icon rounded-square background.\nMascot meaning: a soft clay-born little being that represents playful creation, shareable ideas, and a friendly AI/UGC companion.\nStyle: modern minimalist vector mascot mark, compact silhouette, broad simple shapes, fresh bright palette, very light clay warmth only. It must look good and recognizable at 32px favicon size.\nCharacter abstraction: use a highly simplified head/body silhouette or creature glyph. No detailed anatomy. No clothing. No props. No complex limbs. Optional face should be extremely minimal, at most two small eyes; no mouth unless it is a tiny simple mark.\nShape language: soft, rounded, slightly irregular, as if gently pinched from clay, but clean and designer-ready. Friendly and memorable, not childish.\nColor direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.\nAvoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance: do not make it look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, dessert, or food packaging.\nAvoid scary monster, aggressive teeth, claws, realistic animal, pet-shop icon, emoji face, toy mascot, chibi over-detailing, or generic blob.\nNo star, no spark, no halo, no magic wand, no hand, no pottery tool, no UI, no border, no watermark.\nComposition: centered on a clean light background, generous safe area. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the mascot silhouette should be recognizable at 32px.\nVariant focus: designer-ready vector mascot concept. 2-3 flat shapes, crisp boundaries, distinctive silhouette, minimal material cue, no illustration shading." + } + ] +} diff --git a/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-01-clay-humanoid.png b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-01-clay-humanoid.png new file mode 100644 index 00000000..e6c9da82 Binary files /dev/null and b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-01-clay-humanoid.png differ diff --git a/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-02-clay-sprite.png b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-02-clay-sprite.png new file mode 100644 index 00000000..be1c3a66 Binary files /dev/null and b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-02-clay-sprite.png differ diff --git a/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-03-soft-monster.png b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-03-soft-monster.png new file mode 100644 index 00000000..2e1f677c Binary files /dev/null and b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-03-soft-monster.png differ diff --git a/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-04-animal-abstract.png b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-04-animal-abstract.png new file mode 100644 index 00000000..1a8a23cc Binary files /dev/null and b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-04-animal-abstract.png differ diff --git a/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-05-clay-orb-being.png b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-05-clay-orb-being.png new file mode 100644 index 00000000..9553a46a Binary files /dev/null and b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-05-clay-orb-being.png differ diff --git a/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-06-playful-creature.png b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-06-playful-creature.png new file mode 100644 index 00000000..b3f88980 Binary files /dev/null and b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-06-playful-creature.png differ diff --git a/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-07-avatar-readable.png b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-07-avatar-readable.png new file mode 100644 index 00000000..c4daf21e Binary files /dev/null and b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-07-avatar-readable.png differ diff --git a/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-08-vector-ready.png b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-08-vector-ready.png new file mode 100644 index 00000000..b87294ec Binary files /dev/null and b/public/branding/taonier-logo-mascot-symbol-concepts/taonier-mascot-symbol-08-vector-ready.png differ diff --git a/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-logo-pair-ears-jar-contact-sheet.png b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-logo-pair-ears-jar-contact-sheet.png new file mode 100644 index 00000000..7a453a00 Binary files /dev/null and b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-logo-pair-ears-jar-contact-sheet.png differ diff --git a/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-logo-pair-ears-jar-manifest.json b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-logo-pair-ears-jar-manifest.json new file mode 100644 index 00000000..f8cebda2 --- /dev/null +++ b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-logo-pair-ears-jar-manifest.json @@ -0,0 +1,85 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T22:54:25.492Z", + "logoSkillSummary": { + "requiredReview": "visual inspection, 32px readability, black-white viability", + "outputStatus": "AI concept only; final logo needs vector cleanup" + }, + "brief": { + "brand": "陶泥儿", + "coreBelief": "好玩会创造", + "logoType": "symbol/icon-only mascot mark, no wordmark", + "product": "AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品", + "direction": "陶罐容器 + 只露出动物耳朵的小灵体,神秘又可爱,罐子无表情", + "audience": "女性用户友好、全年龄向、年轻明亮但不低幼", + "structureRules": [ + "主形体是一个陶罐或陶罐容器,强调器皿感和包裹感", + "罐中只露出耳朵,不露完整脸部,不露完整身体", + "耳朵可以是兔、猫、狐狸、熊、狗等动物耳朵,但只能露耳朵", + "罐子可以带短手短脚,但不是必须;若有,也要极简抽象", + "整体必须是 logo 符号级别,不是完整插画角色" + ], + "avoid": [ + "中文或英文字", + "表情元素", + "星星或闪光", + "手托举元素", + "写实陶瓷高光", + "脏泥土或砖块", + "面团、汤圆、甜点、面包、巧克力、糖果、布丁", + "完整动物脸", + "恐怖怪物、牙齿、爪子" + ] + }, + "variants": [ + { + "id": "01-rabbit-jar", + "title": "兔耳陶罐", + "file": "taonier-pair-ears-jar-01-rabbit-jar.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with only animal ears peeking out from inside. The jar should feel mysterious and cute at the same time. Do not show a full face or full body, only ears.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nHidden animal presence: only the ears emerge from the jar opening or upper body. The ears may be rabbit, cat, fox, bear, or dog ears depending on the variant, but no face, no eyes, no nose, no mouth, no full head. The ears must feel playful and alive, but still simple and logo-friendly.\nOptional tiny feet or short hand nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Ear color direction: use natural animal colors that fit the variant, but keep them soft and not neon.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid any jar face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: rabbit ears. Long soft rabbit ears rise from the jar opening with a gentle curve, while the jar remains compact and premium." + }, + { + "id": "02-cat-jar", + "title": "猫耳陶罐", + "file": "taonier-pair-ears-jar-02-cat-jar.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with only animal ears peeking out from inside. The jar should feel mysterious and cute at the same time. Do not show a full face or full body, only ears.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nHidden animal presence: only the ears emerge from the jar opening or upper body. The ears may be rabbit, cat, fox, bear, or dog ears depending on the variant, but no face, no eyes, no nose, no mouth, no full head. The ears must feel playful and alive, but still simple and logo-friendly.\nOptional tiny feet or short hand nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Ear color direction: use natural animal colors that fit the variant, but keep them soft and not neon.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid any jar face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: cat ears. Small pointed cat ears peek from the jar opening, giving a slightly sly but still very cute feeling." + }, + { + "id": "03-fox-jar", + "title": "狐耳陶罐", + "file": "taonier-pair-ears-jar-03-fox-jar.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with only animal ears peeking out from inside. The jar should feel mysterious and cute at the same time. Do not show a full face or full body, only ears.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nHidden animal presence: only the ears emerge from the jar opening or upper body. The ears may be rabbit, cat, fox, bear, or dog ears depending on the variant, but no face, no eyes, no nose, no mouth, no full head. The ears must feel playful and alive, but still simple and logo-friendly.\nOptional tiny feet or short hand nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Ear color direction: use natural animal colors that fit the variant, but keep them soft and not neon.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid any jar face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: fox ears. Slender fox-like ears with a warm orange accent, a little more clever and playful than the rabbit version." + }, + { + "id": "04-bear-jar", + "title": "熊耳陶罐", + "file": "taonier-pair-ears-jar-04-bear-jar.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with only animal ears peeking out from inside. The jar should feel mysterious and cute at the same time. Do not show a full face or full body, only ears.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nHidden animal presence: only the ears emerge from the jar opening or upper body. The ears may be rabbit, cat, fox, bear, or dog ears depending on the variant, but no face, no eyes, no nose, no mouth, no full head. The ears must feel playful and alive, but still simple and logo-friendly.\nOptional tiny feet or short hand nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Ear color direction: use natural animal colors that fit the variant, but keep them soft and not neon.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid any jar face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: bear ears. Two small rounded bear ears emerging from the top, very soft and sleepy, with a sturdy jar silhouette." + }, + { + "id": "05-dog-jar", + "title": "狗耳陶罐", + "file": "taonier-pair-ears-jar-05-dog-jar.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with only animal ears peeking out from inside. The jar should feel mysterious and cute at the same time. Do not show a full face or full body, only ears.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nHidden animal presence: only the ears emerge from the jar opening or upper body. The ears may be rabbit, cat, fox, bear, or dog ears depending on the variant, but no face, no eyes, no nose, no mouth, no full head. The ears must feel playful and alive, but still simple and logo-friendly.\nOptional tiny feet or short hand nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Ear color direction: use natural animal colors that fit the variant, but keep them soft and not neon.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid any jar face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: dog ears. Slightly floppy dog ears peeking from the vessel, friendly and lively, but still only ears, no face." + }, + { + "id": "06-dual-ears", + "title": "双耳组合", + "file": "taonier-pair-ears-jar-06-dual-ears.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with only animal ears peeking out from inside. The jar should feel mysterious and cute at the same time. Do not show a full face or full body, only ears.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nHidden animal presence: only the ears emerge from the jar opening or upper body. The ears may be rabbit, cat, fox, bear, or dog ears depending on the variant, but no face, no eyes, no nose, no mouth, no full head. The ears must feel playful and alive, but still simple and logo-friendly.\nOptional tiny feet or short hand nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Ear color direction: use natural animal colors that fit the variant, but keep them soft and not neon.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid any jar face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: two different ear shapes on one jar, such as one rabbit ear and one cat ear, but still harmonized into a single mascot symbol." + }, + { + "id": "07-tall-jar", + "title": "高罐长耳", + "file": "taonier-pair-ears-jar-07-tall-jar.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with only animal ears peeking out from inside. The jar should feel mysterious and cute at the same time. Do not show a full face or full body, only ears.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nHidden animal presence: only the ears emerge from the jar opening or upper body. The ears may be rabbit, cat, fox, bear, or dog ears depending on the variant, but no face, no eyes, no nose, no mouth, no full head. The ears must feel playful and alive, but still simple and logo-friendly.\nOptional tiny feet or short hand nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Ear color direction: use natural animal colors that fit the variant, but keep them soft and not neon.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid any jar face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: taller jar silhouette with more vertical ears, so the ear read is clearer at favicon size and the vessel feels more iconic." + }, + { + "id": "08-jar-mark", + "title": "商标定稿感", + "file": "taonier-pair-ears-jar-08-jar-mark.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with only animal ears peeking out from inside. The jar should feel mysterious and cute at the same time. Do not show a full face or full body, only ears.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nHidden animal presence: only the ears emerge from the jar opening or upper body. The ears may be rabbit, cat, fox, bear, or dog ears depending on the variant, but no face, no eyes, no nose, no mouth, no full head. The ears must feel playful and alive, but still simple and logo-friendly.\nOptional tiny feet or short hand nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Ear color direction: use natural animal colors that fit the variant, but keep them soft and not neon.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid any jar face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: strongest trademark readability. Use a compact jar silhouette, very simple ears, minimal details, excellent black-and-white legibility." + } + ] +} diff --git a/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-01-rabbit-jar.png b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-01-rabbit-jar.png new file mode 100644 index 00000000..b8f09238 Binary files /dev/null and b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-01-rabbit-jar.png differ diff --git a/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-02-cat-jar.png b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-02-cat-jar.png new file mode 100644 index 00000000..1b16cf8a Binary files /dev/null and b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-02-cat-jar.png differ diff --git a/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-03-fox-jar.png b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-03-fox-jar.png new file mode 100644 index 00000000..5c636752 Binary files /dev/null and b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-03-fox-jar.png differ diff --git a/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-04-bear-jar.png b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-04-bear-jar.png new file mode 100644 index 00000000..bf708a98 Binary files /dev/null and b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-04-bear-jar.png differ diff --git a/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-05-dog-jar.png b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-05-dog-jar.png new file mode 100644 index 00000000..6a1a4481 Binary files /dev/null and b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-05-dog-jar.png differ diff --git a/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-06-dual-ears.png b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-06-dual-ears.png new file mode 100644 index 00000000..359074cf Binary files /dev/null and b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-06-dual-ears.png differ diff --git a/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-07-tall-jar.png b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-07-tall-jar.png new file mode 100644 index 00000000..fd948bd9 Binary files /dev/null and b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-07-tall-jar.png differ diff --git a/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-08-jar-mark.png b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-08-jar-mark.png new file mode 100644 index 00000000..7339c707 Binary files /dev/null and b/public/branding/taonier-logo-pair-ears-jar-concepts/taonier-pair-ears-jar-08-jar-mark.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-logo-peeking-head-jar-blackdot-eye-contact-sheet.png b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-logo-peeking-head-jar-blackdot-eye-contact-sheet.png new file mode 100644 index 00000000..4cc0017d Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-logo-peeking-head-jar-blackdot-eye-contact-sheet.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-logo-peeking-head-jar-blackdot-eye-manifest.json b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-logo-peeking-head-jar-blackdot-eye-manifest.json new file mode 100644 index 00000000..f340af24 --- /dev/null +++ b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-logo-peeking-head-jar-blackdot-eye-manifest.json @@ -0,0 +1,88 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T23:38:04.256Z", + "logoSkillSummary": { + "requiredReview": "visual inspection, 32px readability, black-white viability", + "outputStatus": "AI concept only; final logo needs vector cleanup" + }, + "brief": { + "brand": "陶泥儿", + "coreBelief": "好玩会创造", + "logoType": "symbol/icon-only mascot mark, no wordmark", + "product": "AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品", + "direction": "在保持“半头探出”的节奏下继续拓展,但眼睛必须是纯黑点无高光", + "audience": "女性用户友好、全年龄向、年轻明亮但不低幼", + "structureRules": [ + "主形体仍然是陶罐容器,罐子负责陶器和包裹感", + "动物只露出耳朵、上半个脑袋和两只黑点眼睛", + "眼睛不能有高光、不能有白点反光、不能有玻璃感", + "不露鼻子、嘴巴、身体、爪子或完整动物脸", + "罐子绝对不能有表情元素" + ], + "avoid": [ + "中文或英文字", + "罐子表情", + "动物嘴巴或鼻子", + "眼睛高光", + "白眼球高光", + "星星或闪光", + "手托举元素", + "写实陶瓷高光", + "脏泥土或砖块", + "面团、汤圆、甜点、面包、巧克力、糖果、布丁", + "完整动物身体", + "恐怖怪物、牙齿、爪子" + ] + }, + "variants": [ + { + "id": "01-clay-rabbit", + "title": "陶粉兔头", + "file": "taonier-peeking-head-jar-blackdot-eye-01-clay-rabbit.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: tall slim clay jar with rabbit ears and a cream rabbit head. Use pale clay beige jar and soft peach ear interiors, eyes are black dots only." + }, + { + "id": "02-ash-cat", + "title": "灰陶猫头", + "file": "taonier-peeking-head-jar-blackdot-eye-02-ash-cat.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: squat ash-clay jar with cat ears and a gray-white cat head. Use muted ash beige jar and compact triangular ears, eyes are black dots only." + }, + { + "id": "03-terracotta-fox", + "title": "陶红狐头", + "file": "taonier-peeking-head-jar-blackdot-eye-03-terracotta-fox.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: flared terracotta jar with fox ears and orange fox head. Use muted terracotta jar, cream face area, and warm orange ear tips, eyes are black dots only." + }, + { + "id": "04-striped-bear", + "title": "横纹熊头", + "file": "taonier-peeking-head-jar-blackdot-eye-04-striped-bear.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: jar with subtle ceramic stripe bands, bear ears, and a cocoa-brown bear head. The eyes remain black dots only, no extra facial marks." + }, + { + "id": "05-long-neck-dog", + "title": "长颈狗头", + "file": "taonier-peeking-head-jar-blackdot-eye-05-long-neck-dog.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: long-neck jar with floppy dog ears and a tan dog head. Use a soft gray-beige jar and warm tan ears, eyes are black dots only." + }, + { + "id": "06-low-mouse", + "title": "低矮鼠头", + "file": "taonier-peeking-head-jar-blackdot-eye-06-low-mouse.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: low wide jar with mouse ears and a pale gray mouse head. Add tiny pink ear interiors, eyes are black dots only, cute and slightly mischievous." + }, + { + "id": "07-asym-deer", + "title": "偏心鹿头", + "file": "taonier-peeking-head-jar-blackdot-eye-07-asym-deer.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: slightly asymmetrical jar with deer ears and a soft brown deer head. Keep the ears upright and slender, with a calm cream-beige jar, eyes are black dots only." + }, + { + "id": "08-compact-panda", + "title": "紧凑熊猫", + "file": "taonier-peeking-head-jar-blackdot-eye-08-compact-panda.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: compact trademark jar with panda ears and a black-and-cream panda head. Keep the silhouette bold and simple for strongest brand recognition, eyes are black dots only." + } + ] +} diff --git a/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-01-clay-rabbit.png b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-01-clay-rabbit.png new file mode 100644 index 00000000..a91f0dda Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-01-clay-rabbit.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-02-ash-cat.png b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-02-ash-cat.png new file mode 100644 index 00000000..da0b60c1 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-02-ash-cat.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-03-terracotta-fox.png b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-03-terracotta-fox.png new file mode 100644 index 00000000..8aee11d9 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-03-terracotta-fox.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-04-striped-bear.png b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-04-striped-bear.png new file mode 100644 index 00000000..3ead2d36 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-04-striped-bear.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-05-long-neck-dog.png b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-05-long-neck-dog.png new file mode 100644 index 00000000..45f4e627 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-05-long-neck-dog.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-06-low-mouse.png b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-06-low-mouse.png new file mode 100644 index 00000000..2e6bdf23 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-06-low-mouse.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-07-asym-deer.png b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-07-asym-deer.png new file mode 100644 index 00000000..80bfe286 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-07-asym-deer.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-08-compact-panda.png b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-08-compact-panda.png new file mode 100644 index 00000000..08b08bd8 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-blackdot-eye-concepts/taonier-peeking-head-jar-blackdot-eye-08-compact-panda.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-logo-peeking-head-jar-broad-contact-sheet.png b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-logo-peeking-head-jar-broad-contact-sheet.png new file mode 100644 index 00000000..eca01319 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-logo-peeking-head-jar-broad-contact-sheet.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-logo-peeking-head-jar-broad-manifest.json b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-logo-peeking-head-jar-broad-manifest.json new file mode 100644 index 00000000..b989b632 --- /dev/null +++ b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-logo-peeking-head-jar-broad-manifest.json @@ -0,0 +1,88 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T23:50:57.428Z", + "logoSkillSummary": { + "requiredReview": "visual inspection, 32px readability, black-white viability", + "outputStatus": "AI concept only; final logo needs vector cleanup" + }, + "brief": { + "brand": "陶泥儿", + "coreBelief": "好玩会创造", + "logoType": "symbol/icon-only mascot mark, no wordmark", + "product": "AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品", + "direction": "保持“半头探出”节奏,继续拓展更丰富的罐型、配色和动物类别,眼睛仍是黑点", + "audience": "女性用户友好、全年龄向、年轻明亮但不低幼", + "structureRules": [ + "主形体仍然是陶罐容器,罐子负责陶器和包裹感", + "动物只露出耳朵、上半个脑袋和两只黑点眼睛", + "眼睛不能有高光、不能有白点反光、不能有玻璃感", + "不露鼻子、嘴巴、身体、爪子或完整动物脸", + "罐子绝对不能有表情元素" + ], + "avoid": [ + "中文或英文字", + "罐子表情", + "动物嘴巴或鼻子", + "眼睛高光", + "白眼球高光", + "星星或闪光", + "手托举元素", + "写实陶瓷高光", + "脏泥土或砖块", + "面团、汤圆、甜点、面包、巧克力、糖果、布丁", + "完整动物身体", + "恐怖怪物、牙齿、爪子" + ] + }, + "variants": [ + { + "id": "01-olive-rabbit", + "title": "橄榄兔头", + "file": "taonier-peeking-head-jar-broad-01-olive-rabbit.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: tall slim jar with a muted olive-clay body and rabbit ears. The rabbit head is cream colored with soft peach inner ears, eyes are black dots only." + }, + { + "id": "02-sand-cat", + "title": "砂陶猫头", + "file": "taonier-peeking-head-jar-broad-02-sand-cat.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: squat sand-colored jar with cat ears and a gray-white cat head. Make the rim compact and the body broad, eyes are black dots only." + }, + { + "id": "03-apricot-fox", + "title": "杏陶狐头", + "file": "taonier-peeking-head-jar-broad-03-apricot-fox.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: flared apricot-terracotta jar with fox ears and a warm orange fox head. Use a cream face area and strong ear silhouette, eyes are black dots only." + }, + { + "id": "04-banded-bear", + "title": "双带熊头", + "file": "taonier-peeking-head-jar-broad-04-banded-bear.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: jar with two subtle ceramic bands, bear ears, and a cocoa-brown bear head. Keep the vessel sturdy and broad, eyes are black dots only." + }, + { + "id": "05-necked-dog", + "title": "细颈狗头", + "file": "taonier-peeking-head-jar-broad-05-necked-dog.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: tall narrow-neck jar with floppy dog ears and a tan dog head. Use a warm gray-beige jar and slightly longer ear shapes, eyes are black dots only." + }, + { + "id": "06-flat-mouse", + "title": "扁罐鼠头", + "file": "taonier-peeking-head-jar-broad-06-flat-mouse.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: low flat jar with mouse ears and a pale gray mouse head. Add tiny pink ear interiors and a wider mouth rim, eyes are black dots only." + }, + { + "id": "07-tilted-deer", + "title": "斜肩鹿头", + "file": "taonier-peeking-head-jar-broad-07-tilted-deer.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: slightly tilted jar with deer ears and a soft brown deer head. Use a calm cream-beige jar with a subtle shoulder shift, eyes are black dots only." + }, + { + "id": "08-compact-panda", + "title": "紧凑熊猫", + "file": "taonier-peeking-head-jar-broad-08-compact-panda.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: compact trademark jar with panda ears and a black-and-cream panda head. Keep the silhouette bold, simple, and easy to read at 32px, eyes are black dots only." + } + ] +} diff --git a/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-01-olive-rabbit.png b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-01-olive-rabbit.png new file mode 100644 index 00000000..2c549b04 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-01-olive-rabbit.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-02-sand-cat.png b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-02-sand-cat.png new file mode 100644 index 00000000..de7d786e Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-02-sand-cat.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-03-apricot-fox.png b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-03-apricot-fox.png new file mode 100644 index 00000000..531e0905 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-03-apricot-fox.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-04-banded-bear.png b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-04-banded-bear.png new file mode 100644 index 00000000..ff708e12 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-04-banded-bear.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-05-necked-dog.png b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-05-necked-dog.png new file mode 100644 index 00000000..fc438cde Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-05-necked-dog.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-06-flat-mouse.png b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-06-flat-mouse.png new file mode 100644 index 00000000..7e8be0b5 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-06-flat-mouse.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-07-tilted-deer.png b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-07-tilted-deer.png new file mode 100644 index 00000000..c4591efc Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-07-tilted-deer.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-08-compact-panda.png b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-08-compact-panda.png new file mode 100644 index 00000000..44c041fd Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-broad-concepts/taonier-peeking-head-jar-broad-08-compact-panda.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-logo-peeking-head-jar-contact-sheet.png b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-logo-peeking-head-jar-contact-sheet.png new file mode 100644 index 00000000..b3356744 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-logo-peeking-head-jar-contact-sheet.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-logo-peeking-head-jar-manifest.json b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-logo-peeking-head-jar-manifest.json new file mode 100644 index 00000000..69575d32 --- /dev/null +++ b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-logo-peeking-head-jar-manifest.json @@ -0,0 +1,86 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T23:08:17.118Z", + "logoSkillSummary": { + "requiredReview": "visual inspection, 32px readability, black-white viability", + "outputStatus": "AI concept only; final logo needs vector cleanup" + }, + "brief": { + "brand": "陶泥儿", + "coreBelief": "好玩会创造", + "logoType": "symbol/icon-only mascot mark, no wordmark", + "product": "AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品", + "direction": "陶罐容器 + 小动物从罐中露出耳朵、半个脑袋和眼睛,神秘又可爱", + "audience": "女性用户友好、全年龄向、年轻明亮但不低幼", + "structureRules": [ + "主形体仍然是陶罐容器,罐子负责陶器和包裹感", + "动物只露出耳朵、上半个脑袋和两只眼睛", + "不露鼻子、嘴巴、身体、爪子或完整动物脸", + "罐子绝对不能有表情元素", + "整体必须是 logo 符号级别,不是完整插画角色" + ], + "avoid": [ + "中文或英文字", + "罐子表情", + "动物嘴巴或鼻子", + "星星或闪光", + "手托举元素", + "写实陶瓷高光", + "脏泥土或砖块", + "面团、汤圆、甜点、面包、巧克力、糖果、布丁", + "完整动物身体", + "恐怖怪物、牙齿、爪子" + ] + }, + "variants": [ + { + "id": "01-rabbit-peek", + "title": "兔头探出", + "file": "taonier-peeking-head-jar-01-rabbit-peek.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nOptional tiny jar feet or short side nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Animal head and ears should use soft natural animal colors that fit the variant.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: rabbit. Long soft rabbit ears, upper half of rabbit head and two simple eyes peeking above the jar rim, gentle and premium." + }, + { + "id": "02-cat-peek", + "title": "猫头探出", + "file": "taonier-peeking-head-jar-02-cat-peek.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nOptional tiny jar feet or short side nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Animal head and ears should use soft natural animal colors that fit the variant.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: cat. Small triangular cat ears, upper half of cat head and two simple eyes peeking out, clever and cute, no whiskers." + }, + { + "id": "03-fox-peek", + "title": "狐头探出", + "file": "taonier-peeking-head-jar-03-fox-peek.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nOptional tiny jar feet or short side nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Animal head and ears should use soft natural animal colors that fit the variant.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: fox. Slender fox ears, warm orange upper head with two simple eyes, playful but not sharp or aggressive." + }, + { + "id": "04-bear-peek", + "title": "熊头探出", + "file": "taonier-peeking-head-jar-04-bear-peek.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nOptional tiny jar feet or short side nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Animal head and ears should use soft natural animal colors that fit the variant.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: bear. Rounded bear ears and rounded upper head, two simple eyes, cozy and calm, no muzzle." + }, + { + "id": "05-dog-peek", + "title": "狗头探出", + "file": "taonier-peeking-head-jar-05-dog-peek.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nOptional tiny jar feet or short side nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Animal head and ears should use soft natural animal colors that fit the variant.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: dog. Soft floppy dog ears and upper head peeking from the jar, friendly but not a pet logo, no nose or mouth." + }, + { + "id": "06-mixed-peek", + "title": "混合小灵", + "file": "taonier-peeking-head-jar-06-mixed-peek.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nOptional tiny jar feet or short side nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Animal head and ears should use soft natural animal colors that fit the variant.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: ambiguous animal spirit. Ear shapes sit between rabbit and cat, upper head and eyes only, more original and less species-specific." + }, + { + "id": "07-tall-peek", + "title": "高罐探头", + "file": "taonier-peeking-head-jar-07-tall-peek.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nOptional tiny jar feet or short side nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Animal head and ears should use soft natural animal colors that fit the variant.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: taller jar silhouette with animal head peeking to eye level. Make the jar and head relationship clear at favicon size." + }, + { + "id": "08-trademark-peek", + "title": "商标探头", + "file": "taonier-peeking-head-jar-08-trademark-peek.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nOptional tiny jar feet or short side nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Animal head and ears should use soft natural animal colors that fit the variant.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: strongest trademark readability. Compact jar, simple half-head, two eyes, very few details, excellent black-and-white legibility." + } + ] +} diff --git a/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-01-rabbit-peek.png b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-01-rabbit-peek.png new file mode 100644 index 00000000..8bfa6fc3 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-01-rabbit-peek.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-02-cat-peek.png b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-02-cat-peek.png new file mode 100644 index 00000000..d7ea6d93 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-02-cat-peek.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-03-fox-peek.png b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-03-fox-peek.png new file mode 100644 index 00000000..1784d4b6 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-03-fox-peek.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-04-bear-peek.png b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-04-bear-peek.png new file mode 100644 index 00000000..87eb8e7c Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-04-bear-peek.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-05-dog-peek.png b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-05-dog-peek.png new file mode 100644 index 00000000..fa70abd1 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-05-dog-peek.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-06-mixed-peek.png b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-06-mixed-peek.png new file mode 100644 index 00000000..3b07c1f3 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-06-mixed-peek.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-07-tall-peek.png b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-07-tall-peek.png new file mode 100644 index 00000000..07f4cac6 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-07-tall-peek.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-08-trademark-peek.png b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-08-trademark-peek.png new file mode 100644 index 00000000..8c0b4f5e Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-concepts/taonier-peeking-head-jar-08-trademark-peek.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-logo-peeking-head-jar-expanded-contact-sheet.png b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-logo-peeking-head-jar-expanded-contact-sheet.png new file mode 100644 index 00000000..81bcf28d Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-logo-peeking-head-jar-expanded-contact-sheet.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-logo-peeking-head-jar-expanded-manifest.json b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-logo-peeking-head-jar-expanded-manifest.json new file mode 100644 index 00000000..4e230103 --- /dev/null +++ b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-logo-peeking-head-jar-expanded-manifest.json @@ -0,0 +1,86 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T23:21:04.261Z", + "logoSkillSummary": { + "requiredReview": "visual inspection, 32px readability, black-white viability", + "outputStatus": "AI concept only; final logo needs vector cleanup" + }, + "brief": { + "brand": "陶泥儿", + "coreBelief": "好玩会创造", + "logoType": "symbol/icon-only mascot mark, no wordmark", + "product": "AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品", + "direction": "在保持“半头探出”的节奏下,拓展罐型、口沿、底座、动物原色和耳型搭配", + "audience": "女性用户友好、全年龄向、年轻明亮但不低幼", + "structureRules": [ + "主形体仍然是陶罐容器,罐子负责陶器和包裹感", + "动物只露出耳朵、上半个脑袋和两只眼睛", + "不露鼻子、嘴巴、身体、爪子或完整动物脸", + "罐子绝对不能有表情元素", + "整体必须是 logo 符号级别,不是完整插画角色" + ], + "avoid": [ + "中文或英文字", + "罐子表情", + "动物嘴巴或鼻子", + "星星或闪光", + "手托举元素", + "写实陶瓷高光", + "脏泥土或砖块", + "面团、汤圆、甜点、面包、巧克力、糖果、布丁", + "完整动物身体", + "恐怖怪物、牙齿、爪子" + ] + }, + "variants": [ + { + "id": "01-tall-rabbit", + "title": "高罐兔耳", + "file": "taonier-peeking-head-jar-expanded-01-tall-rabbit.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: tall slim jar with soft rabbit ears and a cream rabbit head peeking out. Use pale beige jar and soft peach inner ears, elegant and light." + }, + { + "id": "02-squat-cat", + "title": "矮罐猫头", + "file": "taonier-peeking-head-jar-expanded-02-squat-cat.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: squat round jar with cat ears and a gray-white cat head. Use a warmer taupe jar and small triangular ears, compact and cozy." + }, + { + "id": "03-flared-fox", + "title": "阔口狐头", + "file": "taonier-peeking-head-jar-expanded-03-flared-fox.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: flared-rim jar with fox ears and orange fox head. Use muted terracotta jar, cream face area, and warm orange ear tips." + }, + { + "id": "04-double-band-bear", + "title": "双圈熊头", + "file": "taonier-peeking-head-jar-expanded-04-double-band-bear.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: double-band jar with bear ears and a cocoa-brown bear head. The jar can have two subtle horizontal rings for ceramic rhythm." + }, + { + "id": "05-long-neck-dog", + "title": "长颈狗头", + "file": "taonier-peeking-head-jar-expanded-05-long-neck-dog.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: long-neck jar with floppy dog ears and a tan dog head. Use a soft gray-beige jar and warm tan ears, friendly and upright." + }, + { + "id": "06-low-mouse", + "title": "低矮鼠头", + "file": "taonier-peeking-head-jar-expanded-06-low-mouse.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: low wide jar with mouse ears and a pale gray mouse head. Add tiny pink ear interiors, cute and slightly mischievous." + }, + { + "id": "07-asym-deer", + "title": "偏心鹿头", + "file": "taonier-peeking-head-jar-expanded-07-asym-deer.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: slightly asymmetrical jar with deer ears and a soft brown deer head. Keep the ears upright and slender, with a calm cream-beige jar." + }, + { + "id": "08-compact-panda", + "title": "紧凑熊猫", + "file": "taonier-peeking-head-jar-expanded-08-compact-panda.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: compact trademark jar with panda ears and a black-and-cream panda head. Keep the silhouette bold and simple for strongest brand recognition." + } + ] +} diff --git a/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-01-tall-rabbit.png b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-01-tall-rabbit.png new file mode 100644 index 00000000..d8699d86 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-01-tall-rabbit.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-02-squat-cat.png b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-02-squat-cat.png new file mode 100644 index 00000000..6e35da28 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-02-squat-cat.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-03-flared-fox.png b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-03-flared-fox.png new file mode 100644 index 00000000..96bf9c1c Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-03-flared-fox.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-04-double-band-bear.png b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-04-double-band-bear.png new file mode 100644 index 00000000..b62800d8 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-04-double-band-bear.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-05-long-neck-dog.png b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-05-long-neck-dog.png new file mode 100644 index 00000000..9967c060 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-05-long-neck-dog.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-06-low-mouse.png b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-06-low-mouse.png new file mode 100644 index 00000000..9f49662c Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-06-low-mouse.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-07-asym-deer.png b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-07-asym-deer.png new file mode 100644 index 00000000..352b6878 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-07-asym-deer.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-08-compact-panda.png b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-08-compact-panda.png new file mode 100644 index 00000000..a9ddfec0 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-expanded-concepts/taonier-peeking-head-jar-expanded-08-compact-panda.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-logo-peeking-head-jar-new-animals-contact-sheet.png b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-logo-peeking-head-jar-new-animals-contact-sheet.png new file mode 100644 index 00000000..5790a162 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-logo-peeking-head-jar-new-animals-contact-sheet.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-logo-peeking-head-jar-new-animals-manifest.json b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-logo-peeking-head-jar-new-animals-manifest.json new file mode 100644 index 00000000..41bb360a --- /dev/null +++ b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-logo-peeking-head-jar-new-animals-manifest.json @@ -0,0 +1,88 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-15T00:04:07.210Z", + "logoSkillSummary": { + "requiredReview": "visual inspection, 32px readability, black-white viability", + "outputStatus": "AI concept only; final logo needs vector cleanup" + }, + "brief": { + "brand": "陶泥儿", + "coreBelief": "好玩会创造", + "logoType": "symbol/icon-only mascot mark, no wordmark", + "product": "AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品", + "direction": "保持当前“半头探出”的状态,但把动物类型真正拓宽到新物种", + "audience": "女性用户友好、全年龄向、年轻明亮但不低幼", + "structureRules": [ + "主形体仍然是陶罐容器,罐子负责陶器和包裹感", + "动物只露出耳朵、上半个脑袋和两只黑点眼睛", + "眼睛不能有高光、不能有白点反光、不能有玻璃感", + "不露鼻子、嘴巴、身体、爪子或完整动物脸", + "罐子绝对不能有表情元素" + ], + "avoid": [ + "中文或英文字", + "罐子表情", + "动物嘴巴或鼻子", + "眼睛高光", + "白眼球高光", + "星星或闪光", + "手托举元素", + "写实陶瓷高光", + "脏泥土或砖块", + "面团、汤圆、甜点、面包、巧克力、糖果、布丁", + "完整动物身体", + "恐怖怪物、牙齿、爪子" + ] + }, + "variants": [ + { + "id": "01-capybara", + "title": "水豚头", + "file": "taonier-peeking-head-jar-new-animals-01-capybara.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: capybara. Use a broad calm jar with a warm beige body and a capybara head peeking out. The capybara has simple rounded ears and a very gentle expression made only from black-dot eyes." + }, + { + "id": "02-hamster", + "title": "仓鼠头", + "file": "taonier-peeking-head-jar-new-animals-02-hamster.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: hamster. Use a squat round jar with a pale sand body and a hamster head. Slightly fuller cheeks are allowed only as shape, but no mouth or nose; eyes are black dots only." + }, + { + "id": "03-koala", + "title": "考拉头", + "file": "taonier-peeking-head-jar-new-animals-03-koala.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: koala. Use a muted eucalyptus-gray jar and a gray-white koala head with round fuzzy ears. Keep the head soft and calm, eyes are black dots only." + }, + { + "id": "04-otter", + "title": "水獭头", + "file": "taonier-peeking-head-jar-new-animals-04-otter.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: otter. Use a smooth river-stone jar and a warm brown otter head. The ears can be tiny and round, the head is compact and playful, eyes are black dots only." + }, + { + "id": "05-squirrel", + "title": "松鼠头", + "file": "taonier-peeking-head-jar-new-animals-05-squirrel.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: squirrel. Use a light clay jar and a reddish-brown squirrel head with small upright ears. The head should feel energetic but still only half exposed, eyes are black dots only." + }, + { + "id": "06-raccoon", + "title": "浣熊头", + "file": "taonier-peeking-head-jar-new-animals-06-raccoon.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: raccoon. Use a muted taupe jar and a gray raccoon head with a darker mask shape implied by color, but no nose or mouth; eyes are black dots only." + }, + { + "id": "07-lamb", + "title": "小羊头", + "file": "taonier-peeking-head-jar-new-animals-07-lamb.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: lamb. Use a soft cream jar and a fluffy off-white lamb head with small curled ears. Keep the silhouette gentle and soft, eyes are black dots only." + }, + { + "id": "08-hedgehog", + "title": "刺猬头", + "file": "taonier-peeking-head-jar-new-animals-08-hedgehog.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nCore idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.\nThe jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.\nBrand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.\nAnimal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.\nEye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.\nJar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.\nStyle: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nJar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.\nAnimal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.\nAvoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.\nFood avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.\nAvoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.\nVariant focus: hedgehog. Use a compact jar with a warm sand body and a hedgehog head hinted by a rounded spiky silhouette, but keep the spikes soft and logo-simple, eyes are black dots only." + } + ] +} diff --git a/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-01-capybara.png b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-01-capybara.png new file mode 100644 index 00000000..b3b9a05b Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-01-capybara.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-02-hamster.png b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-02-hamster.png new file mode 100644 index 00000000..04c89370 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-02-hamster.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-03-koala.png b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-03-koala.png new file mode 100644 index 00000000..b171e38e Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-03-koala.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-04-otter.png b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-04-otter.png new file mode 100644 index 00000000..a4ae927a Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-04-otter.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-05-squirrel.png b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-05-squirrel.png new file mode 100644 index 00000000..13cdfc34 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-05-squirrel.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-06-raccoon.png b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-06-raccoon.png new file mode 100644 index 00000000..c80d7832 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-06-raccoon.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-07-lamb.png b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-07-lamb.png new file mode 100644 index 00000000..f8904714 Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-07-lamb.png differ diff --git a/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-08-hedgehog.png b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-08-hedgehog.png new file mode 100644 index 00000000..5b772baf Binary files /dev/null and b/public/branding/taonier-logo-peeking-head-jar-new-animals-concepts/taonier-peeking-head-jar-new-animals-08-hedgehog.png differ diff --git a/public/branding/taonier-logo-playful-bean-concepts/taonier-logo-playful-bean-contact-sheet.png b/public/branding/taonier-logo-playful-bean-concepts/taonier-logo-playful-bean-contact-sheet.png new file mode 100644 index 00000000..675ba418 Binary files /dev/null and b/public/branding/taonier-logo-playful-bean-concepts/taonier-logo-playful-bean-contact-sheet.png differ diff --git a/public/branding/taonier-logo-playful-bean-concepts/taonier-logo-playful-bean-manifest.json b/public/branding/taonier-logo-playful-bean-concepts/taonier-logo-playful-bean-manifest.json new file mode 100644 index 00000000..48c343bb --- /dev/null +++ b/public/branding/taonier-logo-playful-bean-concepts/taonier-logo-playful-bean-manifest.json @@ -0,0 +1,85 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T21:01:55.557Z", + "logoSkillSummary": { + "requiredReview": "visual inspection, 32px readability, black-white viability", + "outputStatus": "AI concept only; final logo needs vector cleanup" + }, + "brief": { + "brand": "陶泥儿", + "coreBelief": "好玩会创造", + "logoType": "symbol/icon-only mark, no wordmark", + "product": "AI UGC 轻休闲小游戏创作与传播平台,用户像捏陶泥一样把脑洞、梗和灵感塑造成可玩的作品", + "coreMetaphor": "已经成形的可玩作品胚", + "audience": "女性用户友好、全年龄向、年轻明亮但不低幼", + "visualLanguage": "抽象但有玩性的软几何玩具感", + "material": "只保留陶泥温度,不追求泥土质感", + "shape": "闭合不规则圆润豆形,外轮廓流畅、亲和、有玩性", + "colors": [ + "珊瑚橙", + "蜜桃粉", + "奶油白", + "清透青绿", + "少量暖黄或柔紫可选" + ], + "mustHave": [ + "无中文、无英文、无字标", + "无星星、无脸、无表情", + "无方形底盘", + "无食物感", + "32px 可识别", + "黑白化仍成立" + ] + }, + "variants": [ + { + "id": "01-fresh-bean-mark", + "title": "清新豆形标", + "file": "taonier-playful-bean-01-fresh-bean-mark.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works.\nLogo meaning: an already-formed playable creation embryo. It should feel like a small friendly finished creative object, not a process diagram and not raw clay.\nLogo type: abstract symbol/icon only. No wordmark, no mascot, no face, no app-icon rounded-square background.\nMain element: one closed irregular rounded bean-like soft geometric shape. The outer contour must be smooth, friendly, memorable, complete, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, loose strip, badge, stamp, or tile.\nThe symbol should be abstract but playful: it should feel like a soft geometric toy-like creation object, mature enough for a premium brand and not childish.\nInternal design: optional 1-2 broad curved color fields, soft cut-ins, or abstract playful inlays. Do not show a transformation process. Do not add a center icon.\nNo star, no spark, no star-shaped negative space, no eyes, no mouth, no character body, no hand, no pottery tool.\nStyle: modern minimalist vector logo. Flat, crisp, simple broad shapes, very light clay warmth only. No realistic texture. It must look good and recognizable at 32px favicon size.\nColor direction: fresh bright palette for women-friendly and all-age users: coral orange, peach pink, cream white, clear soft teal or mint green. Use brand-color flat shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make it look like bread, chocolate bread, pastry, cookie, candy, jelly, donut, cream filling, sauce, baked dough, dessert, fruit candy, or food packaging.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the closed bean-like silhouette should be recognizable at 32px.\nVariant focus: the cleanest fresh bean mark. Use coral orange and cream white with a tiny soft teal accent. Strong closed irregular bean silhouette, very readable." + }, + { + "id": "02-peach-soft-geometry", + "title": "蜜桃软几何", + "file": "taonier-playful-bean-02-peach-soft-geometry.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works.\nLogo meaning: an already-formed playable creation embryo. It should feel like a small friendly finished creative object, not a process diagram and not raw clay.\nLogo type: abstract symbol/icon only. No wordmark, no mascot, no face, no app-icon rounded-square background.\nMain element: one closed irregular rounded bean-like soft geometric shape. The outer contour must be smooth, friendly, memorable, complete, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, loose strip, badge, stamp, or tile.\nThe symbol should be abstract but playful: it should feel like a soft geometric toy-like creation object, mature enough for a premium brand and not childish.\nInternal design: optional 1-2 broad curved color fields, soft cut-ins, or abstract playful inlays. Do not show a transformation process. Do not add a center icon.\nNo star, no spark, no star-shaped negative space, no eyes, no mouth, no character body, no hand, no pottery tool.\nStyle: modern minimalist vector logo. Flat, crisp, simple broad shapes, very light clay warmth only. No realistic texture. It must look good and recognizable at 32px favicon size.\nColor direction: fresh bright palette for women-friendly and all-age users: coral orange, peach pink, cream white, clear soft teal or mint green. Use brand-color flat shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make it look like bread, chocolate bread, pastry, cookie, candy, jelly, donut, cream filling, sauce, baked dough, dessert, fruit candy, or food packaging.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the closed bean-like silhouette should be recognizable at 32px.\nVariant focus: peach pink and coral soft geometry. Feminine-friendly but not cosmetic, not candy. One smooth inner color field supports the closed bean shape." + }, + { + "id": "03-mint-creation-embryo", + "title": "青绿创作胚", + "file": "taonier-playful-bean-03-mint-creation-embryo.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works.\nLogo meaning: an already-formed playable creation embryo. It should feel like a small friendly finished creative object, not a process diagram and not raw clay.\nLogo type: abstract symbol/icon only. No wordmark, no mascot, no face, no app-icon rounded-square background.\nMain element: one closed irregular rounded bean-like soft geometric shape. The outer contour must be smooth, friendly, memorable, complete, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, loose strip, badge, stamp, or tile.\nThe symbol should be abstract but playful: it should feel like a soft geometric toy-like creation object, mature enough for a premium brand and not childish.\nInternal design: optional 1-2 broad curved color fields, soft cut-ins, or abstract playful inlays. Do not show a transformation process. Do not add a center icon.\nNo star, no spark, no star-shaped negative space, no eyes, no mouth, no character body, no hand, no pottery tool.\nStyle: modern minimalist vector logo. Flat, crisp, simple broad shapes, very light clay warmth only. No realistic texture. It must look good and recognizable at 32px favicon size.\nColor direction: fresh bright palette for women-friendly and all-age users: coral orange, peach pink, cream white, clear soft teal or mint green. Use brand-color flat shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make it look like bread, chocolate bread, pastry, cookie, candy, jelly, donut, cream filling, sauce, baked dough, dessert, fruit candy, or food packaging.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the closed bean-like silhouette should be recognizable at 32px.\nVariant focus: clear mint or teal as the memory accent, with warm cream and coral. The mark should feel like a playable creation object, not a leaf or seed." + }, + { + "id": "04-female-bright-mark", + "title": "女性向明亮款", + "file": "taonier-playful-bean-04-female-bright-mark.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works.\nLogo meaning: an already-formed playable creation embryo. It should feel like a small friendly finished creative object, not a process diagram and not raw clay.\nLogo type: abstract symbol/icon only. No wordmark, no mascot, no face, no app-icon rounded-square background.\nMain element: one closed irregular rounded bean-like soft geometric shape. The outer contour must be smooth, friendly, memorable, complete, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, loose strip, badge, stamp, or tile.\nThe symbol should be abstract but playful: it should feel like a soft geometric toy-like creation object, mature enough for a premium brand and not childish.\nInternal design: optional 1-2 broad curved color fields, soft cut-ins, or abstract playful inlays. Do not show a transformation process. Do not add a center icon.\nNo star, no spark, no star-shaped negative space, no eyes, no mouth, no character body, no hand, no pottery tool.\nStyle: modern minimalist vector logo. Flat, crisp, simple broad shapes, very light clay warmth only. No realistic texture. It must look good and recognizable at 32px favicon size.\nColor direction: fresh bright palette for women-friendly and all-age users: coral orange, peach pink, cream white, clear soft teal or mint green. Use brand-color flat shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make it look like bread, chocolate bread, pastry, cookie, candy, jelly, donut, cream filling, sauce, baked dough, dessert, fruit candy, or food packaging.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the closed bean-like silhouette should be recognizable at 32px.\nVariant focus: brighter women-friendly palette, soft coral, peach, cream, and one clean mint accent. Keep it premium and avoid beauty-brand cliché." + }, + { + "id": "05-all-age-play-mark", + "title": "全龄轻玩款", + "file": "taonier-playful-bean-05-all-age-play-mark.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works.\nLogo meaning: an already-formed playable creation embryo. It should feel like a small friendly finished creative object, not a process diagram and not raw clay.\nLogo type: abstract symbol/icon only. No wordmark, no mascot, no face, no app-icon rounded-square background.\nMain element: one closed irregular rounded bean-like soft geometric shape. The outer contour must be smooth, friendly, memorable, complete, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, loose strip, badge, stamp, or tile.\nThe symbol should be abstract but playful: it should feel like a soft geometric toy-like creation object, mature enough for a premium brand and not childish.\nInternal design: optional 1-2 broad curved color fields, soft cut-ins, or abstract playful inlays. Do not show a transformation process. Do not add a center icon.\nNo star, no spark, no star-shaped negative space, no eyes, no mouth, no character body, no hand, no pottery tool.\nStyle: modern minimalist vector logo. Flat, crisp, simple broad shapes, very light clay warmth only. No realistic texture. It must look good and recognizable at 32px favicon size.\nColor direction: fresh bright palette for women-friendly and all-age users: coral orange, peach pink, cream white, clear soft teal or mint green. Use brand-color flat shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make it look like bread, chocolate bread, pastry, cookie, candy, jelly, donut, cream filling, sauce, baked dough, dessert, fruit candy, or food packaging.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the closed bean-like silhouette should be recognizable at 32px.\nVariant focus: all-age casual play. More energetic and memorable, but still simple. Use two or three flat color fields, no small decorative details." + }, + { + "id": "06-monochrome-first", + "title": "黑白优先款", + "file": "taonier-playful-bean-06-monochrome-first.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works.\nLogo meaning: an already-formed playable creation embryo. It should feel like a small friendly finished creative object, not a process diagram and not raw clay.\nLogo type: abstract symbol/icon only. No wordmark, no mascot, no face, no app-icon rounded-square background.\nMain element: one closed irregular rounded bean-like soft geometric shape. The outer contour must be smooth, friendly, memorable, complete, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, loose strip, badge, stamp, or tile.\nThe symbol should be abstract but playful: it should feel like a soft geometric toy-like creation object, mature enough for a premium brand and not childish.\nInternal design: optional 1-2 broad curved color fields, soft cut-ins, or abstract playful inlays. Do not show a transformation process. Do not add a center icon.\nNo star, no spark, no star-shaped negative space, no eyes, no mouth, no character body, no hand, no pottery tool.\nStyle: modern minimalist vector logo. Flat, crisp, simple broad shapes, very light clay warmth only. No realistic texture. It must look good and recognizable at 32px favicon size.\nColor direction: fresh bright palette for women-friendly and all-age users: coral orange, peach pink, cream white, clear soft teal or mint green. Use brand-color flat shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make it look like bread, chocolate bread, pastry, cookie, candy, jelly, donut, cream filling, sauce, baked dough, dessert, fruit candy, or food packaging.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the closed bean-like silhouette should be recognizable at 32px.\nVariant focus: design for black-and-white survival first. Bold positive and negative shapes, color only supports the structure. No delicate gradients." + }, + { + "id": "07-avatar-readable", + "title": "头像小尺寸款", + "file": "taonier-playful-bean-07-avatar-readable.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works.\nLogo meaning: an already-formed playable creation embryo. It should feel like a small friendly finished creative object, not a process diagram and not raw clay.\nLogo type: abstract symbol/icon only. No wordmark, no mascot, no face, no app-icon rounded-square background.\nMain element: one closed irregular rounded bean-like soft geometric shape. The outer contour must be smooth, friendly, memorable, complete, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, loose strip, badge, stamp, or tile.\nThe symbol should be abstract but playful: it should feel like a soft geometric toy-like creation object, mature enough for a premium brand and not childish.\nInternal design: optional 1-2 broad curved color fields, soft cut-ins, or abstract playful inlays. Do not show a transformation process. Do not add a center icon.\nNo star, no spark, no star-shaped negative space, no eyes, no mouth, no character body, no hand, no pottery tool.\nStyle: modern minimalist vector logo. Flat, crisp, simple broad shapes, very light clay warmth only. No realistic texture. It must look good and recognizable at 32px favicon size.\nColor direction: fresh bright palette for women-friendly and all-age users: coral orange, peach pink, cream white, clear soft teal or mint green. Use brand-color flat shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make it look like bread, chocolate bread, pastry, cookie, candy, jelly, donut, cream filling, sauce, baked dough, dessert, fruit candy, or food packaging.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the closed bean-like silhouette should be recognizable at 32px.\nVariant focus: social avatar and favicon readability. Full, compact closed bean silhouette with one distinctive broad internal curve; no tiny dots." + }, + { + "id": "08-vector-ready", + "title": "矢量定稿感款", + "file": "taonier-playful-bean-08-vector-ready.png", + "prompt": "Create an icon-only brand logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nBrand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works.\nLogo meaning: an already-formed playable creation embryo. It should feel like a small friendly finished creative object, not a process diagram and not raw clay.\nLogo type: abstract symbol/icon only. No wordmark, no mascot, no face, no app-icon rounded-square background.\nMain element: one closed irregular rounded bean-like soft geometric shape. The outer contour must be smooth, friendly, memorable, complete, and vector-friendly.\nThe shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, loose strip, badge, stamp, or tile.\nThe symbol should be abstract but playful: it should feel like a soft geometric toy-like creation object, mature enough for a premium brand and not childish.\nInternal design: optional 1-2 broad curved color fields, soft cut-ins, or abstract playful inlays. Do not show a transformation process. Do not add a center icon.\nNo star, no spark, no star-shaped negative space, no eyes, no mouth, no character body, no hand, no pottery tool.\nStyle: modern minimalist vector logo. Flat, crisp, simple broad shapes, very light clay warmth only. No realistic texture. It must look good and recognizable at 32px favicon size.\nColor direction: fresh bright palette for women-friendly and all-age users: coral orange, peach pink, cream white, clear soft teal or mint green. Use brand-color flat shapes, not candy rendering.\nAvoid muted mud colors as the dominant palette. Avoid dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.\nFood avoidance is critical: do not make it look like bread, chocolate bread, pastry, cookie, candy, jelly, donut, cream filling, sauce, baked dough, dessert, fruit candy, or food packaging.\nComposition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.\nValidation targets: black-and-white version should still read clearly; the closed bean-like silhouette should be recognizable at 32px.\nVariant focus: designer-ready vector concept. 2-3 flat shapes, crisp boundaries, distinctive closed rounded-bean contour, minimal material cue." + } + ] +} diff --git a/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-01-fresh-bean-mark.png b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-01-fresh-bean-mark.png new file mode 100644 index 00000000..20caf56a Binary files /dev/null and b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-01-fresh-bean-mark.png differ diff --git a/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-02-peach-soft-geometry.png b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-02-peach-soft-geometry.png new file mode 100644 index 00000000..d1bcf08a Binary files /dev/null and b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-02-peach-soft-geometry.png differ diff --git a/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-03-mint-creation-embryo.png b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-03-mint-creation-embryo.png new file mode 100644 index 00000000..b72c679c Binary files /dev/null and b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-03-mint-creation-embryo.png differ diff --git a/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-04-female-bright-mark.png b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-04-female-bright-mark.png new file mode 100644 index 00000000..1ce817be Binary files /dev/null and b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-04-female-bright-mark.png differ diff --git a/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-05-all-age-play-mark.png b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-05-all-age-play-mark.png new file mode 100644 index 00000000..6eebdacc Binary files /dev/null and b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-05-all-age-play-mark.png differ diff --git a/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-06-monochrome-first.png b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-06-monochrome-first.png new file mode 100644 index 00000000..5552f400 Binary files /dev/null and b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-06-monochrome-first.png differ diff --git a/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-07-avatar-readable.png b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-07-avatar-readable.png new file mode 100644 index 00000000..14aabbf5 Binary files /dev/null and b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-07-avatar-readable.png differ diff --git a/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-08-vector-ready.png b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-08-vector-ready.png new file mode 100644 index 00000000..91a55e96 Binary files /dev/null and b/public/branding/taonier-logo-playful-bean-concepts/taonier-playful-bean-08-vector-ready.png differ diff --git a/public/branding/taonier-logo-punch-hole-concepts/taonier-logo-punch-hole-contact-sheet.png b/public/branding/taonier-logo-punch-hole-concepts/taonier-logo-punch-hole-contact-sheet.png new file mode 100644 index 00000000..c0eef79f Binary files /dev/null and b/public/branding/taonier-logo-punch-hole-concepts/taonier-logo-punch-hole-contact-sheet.png differ diff --git a/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-app-token.png b/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-app-token.png new file mode 100644 index 00000000..8f87935f Binary files /dev/null and b/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-app-token.png differ diff --git a/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-color-inlay.png b/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-color-inlay.png new file mode 100644 index 00000000..e20e4c8f Binary files /dev/null and b/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-color-inlay.png differ diff --git a/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-hole-balance.png b/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-hole-balance.png new file mode 100644 index 00000000..5b5439b0 Binary files /dev/null and b/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-hole-balance.png differ diff --git a/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-locked-shape.png b/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-locked-shape.png new file mode 100644 index 00000000..d1d4ec6d Binary files /dev/null and b/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-locked-shape.png differ diff --git a/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-mono-test.png b/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-mono-test.png new file mode 100644 index 00000000..452fc1dc Binary files /dev/null and b/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-mono-test.png differ diff --git a/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-stable-icon.png b/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-stable-icon.png new file mode 100644 index 00000000..b7cbdaaa Binary files /dev/null and b/public/branding/taonier-logo-punch-hole-concepts/taonier-punch-stable-icon.png differ diff --git a/public/branding/taonier-logo-punch04-color-concepts/taonier-logo-punch04-color-contact-sheet.png b/public/branding/taonier-logo-punch04-color-concepts/taonier-logo-punch04-color-contact-sheet.png new file mode 100644 index 00000000..dce1af6a Binary files /dev/null and b/public/branding/taonier-logo-punch04-color-concepts/taonier-logo-punch04-color-contact-sheet.png differ diff --git a/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-clay-gradient-flat.png b/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-clay-gradient-flat.png new file mode 100644 index 00000000..72063022 Binary files /dev/null and b/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-clay-gradient-flat.png differ diff --git a/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-cream-window.png b/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-cream-window.png new file mode 100644 index 00000000..c983a866 Binary files /dev/null and b/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-cream-window.png differ diff --git a/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-mint-shadow.png b/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-mint-shadow.png new file mode 100644 index 00000000..e66d5b79 Binary files /dev/null and b/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-mint-shadow.png differ diff --git a/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-navy-game-core.png b/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-navy-game-core.png new file mode 100644 index 00000000..47eb02ba Binary files /dev/null and b/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-navy-game-core.png differ diff --git a/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-negative-tile.png b/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-negative-tile.png new file mode 100644 index 00000000..78ffb418 Binary files /dev/null and b/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-negative-tile.png differ diff --git a/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-warm-ink-core.png b/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-warm-ink-core.png new file mode 100644 index 00000000..b825fada Binary files /dev/null and b/public/branding/taonier-logo-punch04-color-concepts/taonier-punch04-warm-ink-core.png differ diff --git a/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-logo-ref04-locked-color-contact-sheet.png b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-logo-ref04-locked-color-contact-sheet.png new file mode 100644 index 00000000..bc0e7101 Binary files /dev/null and b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-logo-ref04-locked-color-contact-sheet.png differ diff --git a/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-blue-ink.png b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-blue-ink.png new file mode 100644 index 00000000..45a4952d Binary files /dev/null and b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-blue-ink.png differ diff --git a/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-green-ink.png b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-green-ink.png new file mode 100644 index 00000000..3e7761ef Binary files /dev/null and b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-green-ink.png differ diff --git a/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-plum-ink.png b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-plum-ink.png new file mode 100644 index 00000000..f9ce68df Binary files /dev/null and b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-plum-ink.png differ diff --git a/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-shrink-core.png b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-shrink-core.png new file mode 100644 index 00000000..c9d9c197 Binary files /dev/null and b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-shrink-core.png differ diff --git a/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-soft-charcoal.png b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-soft-charcoal.png new file mode 100644 index 00000000..c04cee0d Binary files /dev/null and b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-soft-charcoal.png differ diff --git a/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-warm-ink.png b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-warm-ink.png new file mode 100644 index 00000000..3b0528a5 Binary files /dev/null and b/public/branding/taonier-logo-ref04-locked-color-concepts/taonier-ref04-locked-warm-ink.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-logo-ref04-palette-refine-contact-sheet.png b/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-logo-ref04-palette-refine-contact-sheet.png new file mode 100644 index 00000000..e735051d Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-logo-ref04-palette-refine-contact-sheet.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-ref04-palette-refine-biscuit.png b/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-ref04-palette-refine-biscuit.png new file mode 100644 index 00000000..8c410b47 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-ref04-palette-refine-biscuit.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-ref04-palette-refine-butter.png b/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-ref04-palette-refine-butter.png new file mode 100644 index 00000000..456824fc Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-ref04-palette-refine-butter.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-ref04-palette-refine-cream.png b/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-ref04-palette-refine-cream.png new file mode 100644 index 00000000..8f577dc9 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-ref04-palette-refine-cream.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-ref04-palette-refine-milk.png b/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-ref04-palette-refine-milk.png new file mode 100644 index 00000000..d9851534 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-ref04-palette-refine-milk.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-sparkle-reference-crop.png b/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-sparkle-reference-crop.png new file mode 100644 index 00000000..2a22d000 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-concepts/taonier-sparkle-reference-crop.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v2-concepts/taonier-logo-ref04-palette-refine-v2-contact-sheet.png b/public/branding/taonier-logo-ref04-palette-refine-v2-concepts/taonier-logo-ref04-palette-refine-v2-contact-sheet.png new file mode 100644 index 00000000..62f0e8c2 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v2-concepts/taonier-logo-ref04-palette-refine-v2-contact-sheet.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v2-concepts/taonier-ref04-palette-refine-v2-light-vanilla.png b/public/branding/taonier-logo-ref04-palette-refine-v2-concepts/taonier-ref04-palette-refine-v2-light-vanilla.png new file mode 100644 index 00000000..3b585f4d Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v2-concepts/taonier-ref04-palette-refine-v2-light-vanilla.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v2-concepts/taonier-ref04-palette-refine-v2-pale-cream.png b/public/branding/taonier-logo-ref04-palette-refine-v2-concepts/taonier-ref04-palette-refine-v2-pale-cream.png new file mode 100644 index 00000000..d57ac416 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v2-concepts/taonier-ref04-palette-refine-v2-pale-cream.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v2-concepts/taonier-ref04-palette-refine-v2-soft-butter.png b/public/branding/taonier-logo-ref04-palette-refine-v2-concepts/taonier-ref04-palette-refine-v2-soft-butter.png new file mode 100644 index 00000000..83b328e6 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v2-concepts/taonier-ref04-palette-refine-v2-soft-butter.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v3-concepts/taonier-logo-ref04-palette-refine-v3-contact-sheet.png b/public/branding/taonier-logo-ref04-palette-refine-v3-concepts/taonier-logo-ref04-palette-refine-v3-contact-sheet.png new file mode 100644 index 00000000..e5ec8913 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v3-concepts/taonier-logo-ref04-palette-refine-v3-contact-sheet.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v3-concepts/taonier-ref04-palette-refine-v3-butter-soft.png b/public/branding/taonier-logo-ref04-palette-refine-v3-concepts/taonier-ref04-palette-refine-v3-butter-soft.png new file mode 100644 index 00000000..1f6dd27d Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v3-concepts/taonier-ref04-palette-refine-v3-butter-soft.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v3-concepts/taonier-ref04-palette-refine-v3-milk-cream.png b/public/branding/taonier-logo-ref04-palette-refine-v3-concepts/taonier-ref04-palette-refine-v3-milk-cream.png new file mode 100644 index 00000000..a3680d8e Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v3-concepts/taonier-ref04-palette-refine-v3-milk-cream.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v3-concepts/taonier-ref04-palette-refine-v3-soft-vanilla.png b/public/branding/taonier-logo-ref04-palette-refine-v3-concepts/taonier-ref04-palette-refine-v3-soft-vanilla.png new file mode 100644 index 00000000..d2aaf01f Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v3-concepts/taonier-ref04-palette-refine-v3-soft-vanilla.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-logo-ref04-palette-refine-v4-contact-sheet.png b/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-logo-ref04-palette-refine-v4-contact-sheet.png new file mode 100644 index 00000000..d1ed73b5 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-logo-ref04-palette-refine-v4-contact-sheet.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-ref04-palette-refine-v4-cream-paper.png b/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-ref04-palette-refine-v4-cream-paper.png new file mode 100644 index 00000000..34aedd36 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-ref04-palette-refine-v4-cream-paper.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-ref04-palette-refine-v4-pale-butter.png b/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-ref04-palette-refine-v4-pale-butter.png new file mode 100644 index 00000000..c3456100 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-ref04-palette-refine-v4-pale-butter.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-ref04-palette-refine-v4-soft-champagne.png b/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-ref04-palette-refine-v4-soft-champagne.png new file mode 100644 index 00000000..81e1f6f0 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-ref04-palette-refine-v4-soft-champagne.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-ref04-palette-refine-v4-warm-ivory.png b/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-ref04-palette-refine-v4-warm-ivory.png new file mode 100644 index 00000000..e80a3fb2 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v4-concepts/taonier-ref04-palette-refine-v4-warm-ivory.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-logo-ref04-palette-refine-v5-contact-sheet.png b/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-logo-ref04-palette-refine-v5-contact-sheet.png new file mode 100644 index 00000000..a9500de5 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-logo-ref04-palette-refine-v5-contact-sheet.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-ref04-palette-refine-v5-balanced-bright-spark.png b/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-ref04-palette-refine-v5-balanced-bright-spark.png new file mode 100644 index 00000000..10509d6e Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-ref04-palette-refine-v5-balanced-bright-spark.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-ref04-palette-refine-v5-filled-centered-spark.png b/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-ref04-palette-refine-v5-filled-centered-spark.png new file mode 100644 index 00000000..ebd57523 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-ref04-palette-refine-v5-filled-centered-spark.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-ref04-palette-refine-v5-smooth-left-small-spark.png b/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-ref04-palette-refine-v5-smooth-left-small-spark.png new file mode 100644 index 00000000..dffaf239 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-ref04-palette-refine-v5-smooth-left-small-spark.png differ diff --git a/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-ref04-palette-refine-v5-solid-core-no-hole.png b/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-ref04-palette-refine-v5-solid-core-no-hole.png new file mode 100644 index 00000000..7914250b Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-refine-v5-concepts/taonier-ref04-palette-refine-v5-solid-core-no-hole.png differ diff --git a/public/branding/taonier-logo-ref04-palette-transfer/taonier-logo-ref04-palette-transfer-contact-sheet.png b/public/branding/taonier-logo-ref04-palette-transfer/taonier-logo-ref04-palette-transfer-contact-sheet.png new file mode 100644 index 00000000..fd8ff342 Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-transfer/taonier-logo-ref04-palette-transfer-contact-sheet.png differ diff --git a/public/branding/taonier-logo-ref04-palette-transfer/taonier-ref04-palette-transfer-warm-yellow-sparkle.png b/public/branding/taonier-logo-ref04-palette-transfer/taonier-ref04-palette-transfer-warm-yellow-sparkle.png new file mode 100644 index 00000000..a3dd55bf Binary files /dev/null and b/public/branding/taonier-logo-ref04-palette-transfer/taonier-ref04-palette-transfer-warm-yellow-sparkle.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-logo-ref04-warm-sparkle-contact-sheet.png b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-logo-ref04-warm-sparkle-contact-sheet.png new file mode 100644 index 00000000..e60e1621 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-logo-ref04-warm-sparkle-contact-sheet.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-caramel.png b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-caramel.png new file mode 100644 index 00000000..7e61d5f2 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-caramel.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-clay-quiet.png b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-clay-quiet.png new file mode 100644 index 00000000..6f1d8d84 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-clay-quiet.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-cocoa.png b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-cocoa.png new file mode 100644 index 00000000..e21e08e1 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-cocoa.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-plum.png b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-plum.png new file mode 100644 index 00000000..da9fa07a Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-plum.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-rust.png b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-rust.png new file mode 100644 index 00000000..f07ba90a Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-rust.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-terracotta.png b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-terracotta.png new file mode 100644 index 00000000..80658b35 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-concepts/taonier-ref04-warm-sparkle-terracotta.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-logo-ref04-warm-sparkle-v2-contact-sheet.png b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-logo-ref04-warm-sparkle-v2-contact-sheet.png new file mode 100644 index 00000000..e60e1621 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-logo-ref04-warm-sparkle-v2-contact-sheet.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-caramel.png b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-caramel.png new file mode 100644 index 00000000..7e61d5f2 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-caramel.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-clay-quiet.png b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-clay-quiet.png new file mode 100644 index 00000000..6f1d8d84 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-clay-quiet.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-cocoa.png b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-cocoa.png new file mode 100644 index 00000000..e21e08e1 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-cocoa.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-plum.png b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-plum.png new file mode 100644 index 00000000..da9fa07a Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-plum.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-rust.png b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-rust.png new file mode 100644 index 00000000..f07ba90a Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-rust.png differ diff --git a/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-terracotta.png b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-terracotta.png new file mode 100644 index 00000000..80658b35 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-sparkle-v2-concepts/taonier-ref04-warm-sparkle-terracotta.png differ diff --git a/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-logo-ref04-warm-star-contact-sheet.png b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-logo-ref04-warm-star-contact-sheet.png new file mode 100644 index 00000000..efc7f4c4 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-logo-ref04-warm-star-contact-sheet.png differ diff --git a/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-caramel.png b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-caramel.png new file mode 100644 index 00000000..58060a02 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-caramel.png differ diff --git a/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-cocoa.png b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-cocoa.png new file mode 100644 index 00000000..90d9dcc7 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-cocoa.png differ diff --git a/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-olive.png b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-olive.png new file mode 100644 index 00000000..2075da25 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-olive.png differ diff --git a/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-plum.png b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-plum.png new file mode 100644 index 00000000..68420c98 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-plum.png differ diff --git a/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-rust.png b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-rust.png new file mode 100644 index 00000000..2cf515d0 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-rust.png differ diff --git a/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-terracotta.png b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-terracotta.png new file mode 100644 index 00000000..fcb5e381 Binary files /dev/null and b/public/branding/taonier-logo-ref04-warm-star-concepts/taonier-ref04-warm-star-terracotta.png differ diff --git a/public/branding/taonier-logo-short-foot-creature-concepts/taonier-logo-short-foot-creature-contact-sheet.png b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-logo-short-foot-creature-contact-sheet.png new file mode 100644 index 00000000..946f9817 Binary files /dev/null and b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-logo-short-foot-creature-contact-sheet.png differ diff --git a/public/branding/taonier-logo-short-foot-creature-concepts/taonier-logo-short-foot-creature-manifest.json b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-logo-short-foot-creature-manifest.json new file mode 100644 index 00000000..7ffea9ad --- /dev/null +++ b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-logo-short-foot-creature-manifest.json @@ -0,0 +1,84 @@ +{ + "model": "gpt-image-2-all", + "size": "1024x1024", + "generatedAt": "2026-05-14T22:27:21.056Z", + "logoSkillSummary": { + "requiredReview": "visual inspection, 32px readability, black-white viability", + "outputStatus": "AI concept only; final logo needs vector cleanup" + }, + "brief": { + "brand": "陶泥儿", + "coreBelief": "好玩会创造", + "logoType": "symbol/icon-only mascot mark, no wordmark", + "product": "AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品", + "direction": "低重心短脚泥团小灵体 / 小怪物:参考图只用于造型,不继承写实陶瓷质感", + "audience": "女性用户友好、全年龄向、年轻明亮但不低幼", + "shapeRules": [ + "主体是坐在地上的闭合泥团生物,像一个稳定的软陶泥胚", + "底部有 3-5 个短短的圆脚或脚趾状支点,但不能变成爪子", + "头顶可以有弯角、小尖、软芽、卷曲或捏起的造型,作为记忆点", + "整体必须是 logo 符号级别,不是完整角色插画", + "32px 下仍能看出低重心泥团、短脚和头顶造型" + ], + "avoid": [ + "中文或英文字", + "星星或闪光", + "手托举元素", + "写实陶瓷高光", + "脏泥土或砖块", + "面团、汤圆、甜点、面包、巧克力、糖果、布丁", + "恐怖怪物、牙齿、爪子", + "儿童玩具、表情包贴纸" + ] + }, + "variants": [ + { + "id": "01-curled-tip", + "title": "弯角泥团", + "file": "taonier-short-foot-creature-01-curled-tip.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nThe reference idea is only shape language: a squat soft clay lump creature sitting on the ground, with several very short rounded feet and a distinctive top tuft or curved horn. Do not copy realistic ceramic rendering.\nBrand core: fun creation. The mark should feel like a tiny clay-born creative spirit: soft, friendly, shareable, and able to turn ideas into playful AI/UGC casual game works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain silhouette: low center of gravity, closed mound body, slightly irregular but clean outline, broad base, 3 to 5 stubby rounded feet at the bottom. Feet must be short and cute, never claws, toes, paws, or realistic animal legs.\nTop silhouette: one memorable soft sculpted feature on the head, such as a curled clay tip, small pinched sprout, rounded horn, or soft wave tuft. The top feature should be part of the same clay body, not a separate hat or accessory.\nFace policy: face is optional; if used, only two tiny simple eye dots. No mouth, no blush, no emoji expression, no detailed face. The silhouette should work even without the face.\nStyle: modern minimalist vector mascot logo, flat brand-color shapes, clean curves, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nColor direction: earthy but fresh, using warm clay beige, terracotta, cream white, soft coral, and a very small teal or mint accent if helpful. Avoid dirty mud, brick red dominance, candy gradients, glossy dessert rendering.\nFood avoidance is critical: do not make it look like mochi, dumpling, bun, bread, cake, cookie, candy, pudding, chocolate, jelly, or any edible mascot.\nAvoid pottery class object, ceramic figurine, handmade toy, rough mud texture, archaeology stamp, realistic glaze, 3D clay render, or shiny ceramic highlight.\nAvoid scary monster, teeth, claws, spikes, realistic animal, pet logo, chibi over-detailing, generic blob, star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on clean light background, generous safe area, strong readable silhouette first. It should look recognizable at 32px and still work in black and white.\nVariant focus: a squat clay lump creature with one soft curled tip leaning gently forward, four tiny rounded feet, calm premium silhouette." + }, + { + "id": "02-soft-sprout", + "title": "软芽泥团", + "file": "taonier-short-foot-creature-02-soft-sprout.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nThe reference idea is only shape language: a squat soft clay lump creature sitting on the ground, with several very short rounded feet and a distinctive top tuft or curved horn. Do not copy realistic ceramic rendering.\nBrand core: fun creation. The mark should feel like a tiny clay-born creative spirit: soft, friendly, shareable, and able to turn ideas into playful AI/UGC casual game works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain silhouette: low center of gravity, closed mound body, slightly irregular but clean outline, broad base, 3 to 5 stubby rounded feet at the bottom. Feet must be short and cute, never claws, toes, paws, or realistic animal legs.\nTop silhouette: one memorable soft sculpted feature on the head, such as a curled clay tip, small pinched sprout, rounded horn, or soft wave tuft. The top feature should be part of the same clay body, not a separate hat or accessory.\nFace policy: face is optional; if used, only two tiny simple eye dots. No mouth, no blush, no emoji expression, no detailed face. The silhouette should work even without the face.\nStyle: modern minimalist vector mascot logo, flat brand-color shapes, clean curves, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nColor direction: earthy but fresh, using warm clay beige, terracotta, cream white, soft coral, and a very small teal or mint accent if helpful. Avoid dirty mud, brick red dominance, candy gradients, glossy dessert rendering.\nFood avoidance is critical: do not make it look like mochi, dumpling, bun, bread, cake, cookie, candy, pudding, chocolate, jelly, or any edible mascot.\nAvoid pottery class object, ceramic figurine, handmade toy, rough mud texture, archaeology stamp, realistic glaze, 3D clay render, or shiny ceramic highlight.\nAvoid scary monster, teeth, claws, spikes, realistic animal, pet logo, chibi over-detailing, generic blob, star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on clean light background, generous safe area, strong readable silhouette first. It should look recognizable at 32px and still work in black and white.\nVariant focus: a low mound creature with a pinched sprout-like top made from the same clay body, three short feet, fresh and memorable." + }, + { + "id": "03-wave-tuft", + "title": "波浪小怪", + "file": "taonier-short-foot-creature-03-wave-tuft.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nThe reference idea is only shape language: a squat soft clay lump creature sitting on the ground, with several very short rounded feet and a distinctive top tuft or curved horn. Do not copy realistic ceramic rendering.\nBrand core: fun creation. The mark should feel like a tiny clay-born creative spirit: soft, friendly, shareable, and able to turn ideas into playful AI/UGC casual game works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain silhouette: low center of gravity, closed mound body, slightly irregular but clean outline, broad base, 3 to 5 stubby rounded feet at the bottom. Feet must be short and cute, never claws, toes, paws, or realistic animal legs.\nTop silhouette: one memorable soft sculpted feature on the head, such as a curled clay tip, small pinched sprout, rounded horn, or soft wave tuft. The top feature should be part of the same clay body, not a separate hat or accessory.\nFace policy: face is optional; if used, only two tiny simple eye dots. No mouth, no blush, no emoji expression, no detailed face. The silhouette should work even without the face.\nStyle: modern minimalist vector mascot logo, flat brand-color shapes, clean curves, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nColor direction: earthy but fresh, using warm clay beige, terracotta, cream white, soft coral, and a very small teal or mint accent if helpful. Avoid dirty mud, brick red dominance, candy gradients, glossy dessert rendering.\nFood avoidance is critical: do not make it look like mochi, dumpling, bun, bread, cake, cookie, candy, pudding, chocolate, jelly, or any edible mascot.\nAvoid pottery class object, ceramic figurine, handmade toy, rough mud texture, archaeology stamp, realistic glaze, 3D clay render, or shiny ceramic highlight.\nAvoid scary monster, teeth, claws, spikes, realistic animal, pet logo, chibi over-detailing, generic blob, star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on clean light background, generous safe area, strong readable silhouette first. It should look recognizable at 32px and still work in black and white.\nVariant focus: a playful clay creature with a single wave-shaped top tuft, broad sitting base, 4 tiny feet, more dynamic but still logo-simple." + }, + { + "id": "04-round-horn", + "title": "圆角小怪", + "file": "taonier-short-foot-creature-04-round-horn.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nThe reference idea is only shape language: a squat soft clay lump creature sitting on the ground, with several very short rounded feet and a distinctive top tuft or curved horn. Do not copy realistic ceramic rendering.\nBrand core: fun creation. The mark should feel like a tiny clay-born creative spirit: soft, friendly, shareable, and able to turn ideas into playful AI/UGC casual game works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain silhouette: low center of gravity, closed mound body, slightly irregular but clean outline, broad base, 3 to 5 stubby rounded feet at the bottom. Feet must be short and cute, never claws, toes, paws, or realistic animal legs.\nTop silhouette: one memorable soft sculpted feature on the head, such as a curled clay tip, small pinched sprout, rounded horn, or soft wave tuft. The top feature should be part of the same clay body, not a separate hat or accessory.\nFace policy: face is optional; if used, only two tiny simple eye dots. No mouth, no blush, no emoji expression, no detailed face. The silhouette should work even without the face.\nStyle: modern minimalist vector mascot logo, flat brand-color shapes, clean curves, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nColor direction: earthy but fresh, using warm clay beige, terracotta, cream white, soft coral, and a very small teal or mint accent if helpful. Avoid dirty mud, brick red dominance, candy gradients, glossy dessert rendering.\nFood avoidance is critical: do not make it look like mochi, dumpling, bun, bread, cake, cookie, candy, pudding, chocolate, jelly, or any edible mascot.\nAvoid pottery class object, ceramic figurine, handmade toy, rough mud texture, archaeology stamp, realistic glaze, 3D clay render, or shiny ceramic highlight.\nAvoid scary monster, teeth, claws, spikes, realistic animal, pet logo, chibi over-detailing, generic blob, star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on clean light background, generous safe area, strong readable silhouette first. It should look recognizable at 32px and still work in black and white.\nVariant focus: a friendly abstract little monster with one rounded horn-like bump and a second smaller bump, stubby feet, no scary details." + }, + { + "id": "05-low-squat", + "title": "低趴泥团", + "file": "taonier-short-foot-creature-05-low-squat.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nThe reference idea is only shape language: a squat soft clay lump creature sitting on the ground, with several very short rounded feet and a distinctive top tuft or curved horn. Do not copy realistic ceramic rendering.\nBrand core: fun creation. The mark should feel like a tiny clay-born creative spirit: soft, friendly, shareable, and able to turn ideas into playful AI/UGC casual game works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain silhouette: low center of gravity, closed mound body, slightly irregular but clean outline, broad base, 3 to 5 stubby rounded feet at the bottom. Feet must be short and cute, never claws, toes, paws, or realistic animal legs.\nTop silhouette: one memorable soft sculpted feature on the head, such as a curled clay tip, small pinched sprout, rounded horn, or soft wave tuft. The top feature should be part of the same clay body, not a separate hat or accessory.\nFace policy: face is optional; if used, only two tiny simple eye dots. No mouth, no blush, no emoji expression, no detailed face. The silhouette should work even without the face.\nStyle: modern minimalist vector mascot logo, flat brand-color shapes, clean curves, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nColor direction: earthy but fresh, using warm clay beige, terracotta, cream white, soft coral, and a very small teal or mint accent if helpful. Avoid dirty mud, brick red dominance, candy gradients, glossy dessert rendering.\nFood avoidance is critical: do not make it look like mochi, dumpling, bun, bread, cake, cookie, candy, pudding, chocolate, jelly, or any edible mascot.\nAvoid pottery class object, ceramic figurine, handmade toy, rough mud texture, archaeology stamp, realistic glaze, 3D clay render, or shiny ceramic highlight.\nAvoid scary monster, teeth, claws, spikes, realistic animal, pet logo, chibi over-detailing, generic blob, star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on clean light background, generous safe area, strong readable silhouette first. It should look recognizable at 32px and still work in black and white.\nVariant focus: extra low and stable clay mound, wide base, five tiny rounded feet, top feature is a subtle pinched crest, very favicon-readable." + }, + { + "id": "06-asymmetric-charm", + "title": "偏心灵体", + "file": "taonier-short-foot-creature-06-asymmetric-charm.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nThe reference idea is only shape language: a squat soft clay lump creature sitting on the ground, with several very short rounded feet and a distinctive top tuft or curved horn. Do not copy realistic ceramic rendering.\nBrand core: fun creation. The mark should feel like a tiny clay-born creative spirit: soft, friendly, shareable, and able to turn ideas into playful AI/UGC casual game works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain silhouette: low center of gravity, closed mound body, slightly irregular but clean outline, broad base, 3 to 5 stubby rounded feet at the bottom. Feet must be short and cute, never claws, toes, paws, or realistic animal legs.\nTop silhouette: one memorable soft sculpted feature on the head, such as a curled clay tip, small pinched sprout, rounded horn, or soft wave tuft. The top feature should be part of the same clay body, not a separate hat or accessory.\nFace policy: face is optional; if used, only two tiny simple eye dots. No mouth, no blush, no emoji expression, no detailed face. The silhouette should work even without the face.\nStyle: modern minimalist vector mascot logo, flat brand-color shapes, clean curves, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nColor direction: earthy but fresh, using warm clay beige, terracotta, cream white, soft coral, and a very small teal or mint accent if helpful. Avoid dirty mud, brick red dominance, candy gradients, glossy dessert rendering.\nFood avoidance is critical: do not make it look like mochi, dumpling, bun, bread, cake, cookie, candy, pudding, chocolate, jelly, or any edible mascot.\nAvoid pottery class object, ceramic figurine, handmade toy, rough mud texture, archaeology stamp, realistic glaze, 3D clay render, or shiny ceramic highlight.\nAvoid scary monster, teeth, claws, spikes, realistic animal, pet logo, chibi over-detailing, generic blob, star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on clean light background, generous safe area, strong readable silhouette first. It should look recognizable at 32px and still work in black and white.\nVariant focus: asymmetrical friendly spirit mark, body leans slightly to one side, curled top balances the shape, short feet stay grounded." + }, + { + "id": "07-avatar-bold", + "title": "头像强识别", + "file": "taonier-short-foot-creature-07-avatar-bold.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nThe reference idea is only shape language: a squat soft clay lump creature sitting on the ground, with several very short rounded feet and a distinctive top tuft or curved horn. Do not copy realistic ceramic rendering.\nBrand core: fun creation. The mark should feel like a tiny clay-born creative spirit: soft, friendly, shareable, and able to turn ideas into playful AI/UGC casual game works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain silhouette: low center of gravity, closed mound body, slightly irregular but clean outline, broad base, 3 to 5 stubby rounded feet at the bottom. Feet must be short and cute, never claws, toes, paws, or realistic animal legs.\nTop silhouette: one memorable soft sculpted feature on the head, such as a curled clay tip, small pinched sprout, rounded horn, or soft wave tuft. The top feature should be part of the same clay body, not a separate hat or accessory.\nFace policy: face is optional; if used, only two tiny simple eye dots. No mouth, no blush, no emoji expression, no detailed face. The silhouette should work even without the face.\nStyle: modern minimalist vector mascot logo, flat brand-color shapes, clean curves, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nColor direction: earthy but fresh, using warm clay beige, terracotta, cream white, soft coral, and a very small teal or mint accent if helpful. Avoid dirty mud, brick red dominance, candy gradients, glossy dessert rendering.\nFood avoidance is critical: do not make it look like mochi, dumpling, bun, bread, cake, cookie, candy, pudding, chocolate, jelly, or any edible mascot.\nAvoid pottery class object, ceramic figurine, handmade toy, rough mud texture, archaeology stamp, realistic glaze, 3D clay render, or shiny ceramic highlight.\nAvoid scary monster, teeth, claws, spikes, realistic animal, pet logo, chibi over-detailing, generic blob, star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on clean light background, generous safe area, strong readable silhouette first. It should look recognizable at 32px and still work in black and white.\nVariant focus: bold social avatar readability, thick simple silhouette, two tiny eye dots allowed, top tuft and feet readable at 32px." + }, + { + "id": "08-vector-outline", + "title": "商标轮廓", + "file": "taonier-short-foot-creature-08-vector-outline.png", + "prompt": "Create an icon-only mascot logo symbol for the Chinese product named \"陶泥儿\"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.\nThe reference idea is only shape language: a squat soft clay lump creature sitting on the ground, with several very short rounded feet and a distinctive top tuft or curved horn. Do not copy realistic ceramic rendering.\nBrand core: fun creation. The mark should feel like a tiny clay-born creative spirit: soft, friendly, shareable, and able to turn ideas into playful AI/UGC casual game works.\nLogo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.\nMain silhouette: low center of gravity, closed mound body, slightly irregular but clean outline, broad base, 3 to 5 stubby rounded feet at the bottom. Feet must be short and cute, never claws, toes, paws, or realistic animal legs.\nTop silhouette: one memorable soft sculpted feature on the head, such as a curled clay tip, small pinched sprout, rounded horn, or soft wave tuft. The top feature should be part of the same clay body, not a separate hat or accessory.\nFace policy: face is optional; if used, only two tiny simple eye dots. No mouth, no blush, no emoji expression, no detailed face. The silhouette should work even without the face.\nStyle: modern minimalist vector mascot logo, flat brand-color shapes, clean curves, premium but warm. Slight clay warmth through color and form only; no realistic texture.\nColor direction: earthy but fresh, using warm clay beige, terracotta, cream white, soft coral, and a very small teal or mint accent if helpful. Avoid dirty mud, brick red dominance, candy gradients, glossy dessert rendering.\nFood avoidance is critical: do not make it look like mochi, dumpling, bun, bread, cake, cookie, candy, pudding, chocolate, jelly, or any edible mascot.\nAvoid pottery class object, ceramic figurine, handmade toy, rough mud texture, archaeology stamp, realistic glaze, 3D clay render, or shiny ceramic highlight.\nAvoid scary monster, teeth, claws, spikes, realistic animal, pet logo, chibi over-detailing, generic blob, star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.\nComposition: centered on clean light background, generous safe area, strong readable silhouette first. It should look recognizable at 32px and still work in black and white.\nVariant focus: designer-ready vector mark. Use 2-3 flat shapes, crisp boundaries, very strong black-and-white silhouette, minimal inner detail." + } + ] +} diff --git a/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-01-curled-tip.png b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-01-curled-tip.png new file mode 100644 index 00000000..4461557f Binary files /dev/null and b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-01-curled-tip.png differ diff --git a/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-02-soft-sprout.png b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-02-soft-sprout.png new file mode 100644 index 00000000..5a970cb6 Binary files /dev/null and b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-02-soft-sprout.png differ diff --git a/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-03-wave-tuft.png b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-03-wave-tuft.png new file mode 100644 index 00000000..124731c0 Binary files /dev/null and b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-03-wave-tuft.png differ diff --git a/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-04-round-horn.png b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-04-round-horn.png new file mode 100644 index 00000000..8f79afce Binary files /dev/null and b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-04-round-horn.png differ diff --git a/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-05-low-squat.png b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-05-low-squat.png new file mode 100644 index 00000000..809c978f Binary files /dev/null and b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-05-low-squat.png differ diff --git a/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-06-asymmetric-charm.png b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-06-asymmetric-charm.png new file mode 100644 index 00000000..48fd9dde Binary files /dev/null and b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-06-asymmetric-charm.png differ diff --git a/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-07-avatar-bold.png b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-07-avatar-bold.png new file mode 100644 index 00000000..81e3b441 Binary files /dev/null and b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-07-avatar-bold.png differ diff --git a/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-08-vector-outline.png b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-08-vector-outline.png new file mode 100644 index 00000000..bc1b3799 Binary files /dev/null and b/public/branding/taonier-logo-short-foot-creature-concepts/taonier-short-foot-creature-08-vector-outline.png differ diff --git a/public/branding/taonier-logo-spiral-reference-concepts/taonier-logo-spiral-reference-contact-sheet.png b/public/branding/taonier-logo-spiral-reference-concepts/taonier-logo-spiral-reference-contact-sheet.png new file mode 100644 index 00000000..081680b1 Binary files /dev/null and b/public/branding/taonier-logo-spiral-reference-concepts/taonier-logo-spiral-reference-contact-sheet.png differ diff --git a/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-bouncy-clay.png b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-bouncy-clay.png new file mode 100644 index 00000000..be26c140 Binary files /dev/null and b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-bouncy-clay.png differ diff --git a/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-candy-roll.png b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-candy-roll.png new file mode 100644 index 00000000..a15757c2 Binary files /dev/null and b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-candy-roll.png differ diff --git a/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-creation-whirl.png b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-creation-whirl.png new file mode 100644 index 00000000..8e44674b Binary files /dev/null and b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-creation-whirl.png differ diff --git a/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-reference.jpg b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-reference.jpg new file mode 100644 index 00000000..909668d0 Binary files /dev/null and b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-reference.jpg differ diff --git a/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-soft-squish.png b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-soft-squish.png new file mode 100644 index 00000000..6bd10dd0 Binary files /dev/null and b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-soft-squish.png differ diff --git a/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-soft-token.png b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-soft-token.png new file mode 100644 index 00000000..a03c8eb0 Binary files /dev/null and b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-soft-token.png differ diff --git a/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-star-core.png b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-star-core.png new file mode 100644 index 00000000..25d974b5 Binary files /dev/null and b/public/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-star-core.png differ diff --git a/public/branding/taonier-logo-squish-concepts/taonier-logo-squish-contact-sheet.png b/public/branding/taonier-logo-squish-concepts/taonier-logo-squish-contact-sheet.png new file mode 100644 index 00000000..8d49544d Binary files /dev/null and b/public/branding/taonier-logo-squish-concepts/taonier-logo-squish-contact-sheet.png differ diff --git a/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-bounce.png b/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-bounce.png new file mode 100644 index 00000000..619ae0b7 Binary files /dev/null and b/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-bounce.png differ diff --git a/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-comet.png b/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-comet.png new file mode 100644 index 00000000..eaf7d61d Binary files /dev/null and b/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-comet.png differ diff --git a/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-pulse.png b/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-pulse.png new file mode 100644 index 00000000..ffb8f935 Binary files /dev/null and b/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-pulse.png differ diff --git a/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-spark-gap.png b/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-spark-gap.png new file mode 100644 index 00000000..b82ae240 Binary files /dev/null and b/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-spark-gap.png differ diff --git a/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-token.png b/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-token.png new file mode 100644 index 00000000..a80c05e4 Binary files /dev/null and b/public/branding/taonier-logo-squish-concepts/taonier-squish-v2-token.png differ diff --git a/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-bubble-q.png b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-bubble-q.png new file mode 100644 index 00000000..ad325f91 Binary files /dev/null and b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-bubble-q.png differ diff --git a/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-candy-mint.png b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-candy-mint.png new file mode 100644 index 00000000..83f2cb53 Binary files /dev/null and b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-candy-mint.png differ diff --git a/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-contact-sheet.png b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-contact-sheet.png new file mode 100644 index 00000000..882d871b Binary files /dev/null and b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-contact-sheet.png differ diff --git a/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-coral-soda.png b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-coral-soda.png new file mode 100644 index 00000000..7add634e Binary files /dev/null and b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-coral-soda.png differ diff --git a/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-original-plus.png b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-original-plus.png new file mode 100644 index 00000000..02bf0268 Binary files /dev/null and b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-original-plus.png differ diff --git a/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-peach-jelly.png b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-peach-jelly.png new file mode 100644 index 00000000..382cf5a8 Binary files /dev/null and b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-peach-jelly.png differ diff --git a/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-pop-bright.png b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-pop-bright.png new file mode 100644 index 00000000..89a21434 Binary files /dev/null and b/public/branding/taonier-logo-squish-recolor-variants/taonier-squish-recolor-pop-bright.png differ diff --git a/public/branding/taonier-logo-squish-variants/taonier-squish-berry-mint.svg b/public/branding/taonier-logo-squish-variants/taonier-squish-berry-mint.svg new file mode 100644 index 00000000..b4b67dd5 --- /dev/null +++ b/public/branding/taonier-logo-squish-variants/taonier-squish-berry-mint.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/branding/taonier-logo-squish-variants/taonier-squish-bubble-bright.svg b/public/branding/taonier-logo-squish-variants/taonier-squish-bubble-bright.svg new file mode 100644 index 00000000..2a5469d3 --- /dev/null +++ b/public/branding/taonier-logo-squish-variants/taonier-squish-bubble-bright.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/branding/taonier-logo-squish-variants/taonier-squish-candy-pop.svg b/public/branding/taonier-logo-squish-variants/taonier-squish-candy-pop.svg new file mode 100644 index 00000000..cf308506 --- /dev/null +++ b/public/branding/taonier-logo-squish-variants/taonier-squish-candy-pop.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/branding/taonier-logo-squish-variants/taonier-squish-jelly-cream.svg b/public/branding/taonier-logo-squish-variants/taonier-squish-jelly-cream.svg new file mode 100644 index 00000000..27a41413 --- /dev/null +++ b/public/branding/taonier-logo-squish-variants/taonier-squish-jelly-cream.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/branding/taonier-logo-squish-variants/taonier-squish-neon-cute.svg b/public/branding/taonier-logo-squish-variants/taonier-squish-neon-cute.svg new file mode 100644 index 00000000..d0086711 --- /dev/null +++ b/public/branding/taonier-logo-squish-variants/taonier-squish-neon-cute.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/branding/taonier-logo-squish-variants/taonier-squish-sunny-coral.svg b/public/branding/taonier-logo-squish-variants/taonier-squish-sunny-coral.svg new file mode 100644 index 00000000..a7bb0a14 --- /dev/null +++ b/public/branding/taonier-logo-squish-variants/taonier-squish-sunny-coral.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/branding/taonier-logo-squish-variants/taonier-squish-variants-contact-sheet.svg b/public/branding/taonier-logo-squish-variants/taonier-squish-variants-contact-sheet.svg new file mode 100644 index 00000000..536f2a86 --- /dev/null +++ b/public/branding/taonier-logo-squish-variants/taonier-squish-variants-contact-sheet.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 01 莓果薄荷 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 02 糖果桃青 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 03 奶油果冻 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 04 亮彩泡泡 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 05 暖日珊瑚 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 06 霓虹可爱 + \ No newline at end of file diff --git a/public/child-motion-demo/picture-book-character-outline-v3.png b/public/child-motion-demo/picture-book-character-outline-v3.png new file mode 100644 index 00000000..5076be7d Binary files /dev/null and b/public/child-motion-demo/picture-book-character-outline-v3.png differ diff --git a/public/child-motion-demo/picture-book-character-outline-v4.png b/public/child-motion-demo/picture-book-character-outline-v4.png new file mode 100644 index 00000000..6acd9536 Binary files /dev/null and b/public/child-motion-demo/picture-book-character-outline-v4.png differ diff --git a/public/child-motion-demo/picture-book-wave-cat-arm-guide-v7.png b/public/child-motion-demo/picture-book-wave-cat-arm-guide-v7.png new file mode 100644 index 00000000..218ef9b3 Binary files /dev/null and b/public/child-motion-demo/picture-book-wave-cat-arm-guide-v7.png differ diff --git a/public/child-motion-demo/picture-book-wave-cat-body-guide-v7.png b/public/child-motion-demo/picture-book-wave-cat-body-guide-v7.png new file mode 100644 index 00000000..74fa51b0 Binary files /dev/null and b/public/child-motion-demo/picture-book-wave-cat-body-guide-v7.png differ diff --git a/public/child-motion-demo/picture-book-wave-cat-paw-left-v1.png b/public/child-motion-demo/picture-book-wave-cat-paw-left-v1.png new file mode 100644 index 00000000..cc039e9b Binary files /dev/null and b/public/child-motion-demo/picture-book-wave-cat-paw-left-v1.png differ diff --git a/public/child-motion-demo/picture-book-wave-cat-paw-right-v1.png b/public/child-motion-demo/picture-book-wave-cat-paw-right-v1.png new file mode 100644 index 00000000..dd50f224 Binary files /dev/null and b/public/child-motion-demo/picture-book-wave-cat-paw-right-v1.png differ diff --git a/public/edutainment-baby-object/default-left-hand.png b/public/edutainment-baby-object/default-left-hand.png new file mode 100644 index 00000000..03c336cf Binary files /dev/null and b/public/edutainment-baby-object/default-left-hand.png differ diff --git a/public/edutainment-baby-object/default-right-hand.png b/public/edutainment-baby-object/default-right-hand.png new file mode 100644 index 00000000..69f60995 Binary files /dev/null and b/public/edutainment-baby-object/default-right-hand.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-hands-1x2-v3.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-hands-1x2-v3.png new file mode 100644 index 00000000..e9ff9af3 Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-hands-1x2-v3.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-hands-1x2-v4.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-hands-1x2-v4.png new file mode 100644 index 00000000..1ff47b8f Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-hands-1x2-v4.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-hands-1x2-v8-green-preview.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-hands-1x2-v8-green-preview.png new file mode 100644 index 00000000..26af2e2c Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-hands-1x2-v8-green-preview.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-hands-1x2-v8-transparent.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-hands-1x2-v8-transparent.png new file mode 100644 index 00000000..cdc0134f Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-hands-1x2-v8-transparent.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-clean-transparent.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-clean-transparent.png new file mode 100644 index 00000000..62980beb Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-clean-transparent.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-defringed-strong.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-defringed-strong.png new file mode 100644 index 00000000..5f80b985 Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-defringed-strong.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-defringed.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-defringed.png new file mode 100644 index 00000000..44fe9098 Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-defringed.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-final-clean.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-final-clean.png new file mode 100644 index 00000000..03c336cf Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-final-clean.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-final-transparent.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-final-transparent.png new file mode 100644 index 00000000..e072f085 Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-final-transparent.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-transparent.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-transparent.png new file mode 100644 index 00000000..e0be70c2 Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v4-transparent.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v8-transparent.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v8-transparent.png new file mode 100644 index 00000000..d86abccb Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v8-transparent.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-clean-transparent.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-clean-transparent.png new file mode 100644 index 00000000..6d6fa6cb Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-clean-transparent.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-defringed-strong.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-defringed-strong.png new file mode 100644 index 00000000..c3433271 Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-defringed-strong.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-defringed.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-defringed.png new file mode 100644 index 00000000..af490122 Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-defringed.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-final-clean.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-final-clean.png new file mode 100644 index 00000000..69f60995 Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-final-clean.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-final-transparent.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-final-transparent.png new file mode 100644 index 00000000..99abe77b Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-final-transparent.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-transparent.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-transparent.png new file mode 100644 index 00000000..60d923c4 Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v4-transparent.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v8-transparent.png b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v8-transparent.png new file mode 100644 index 00000000..7f7ff2b6 Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v8-transparent.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/left-hand-picture-book-v1.png b/public/edutainment-baby-object/image2-picture-book-hands/left-hand-picture-book-v1.png new file mode 100644 index 00000000..34f5c1c5 Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/left-hand-picture-book-v1.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/left-hand-picture-book-v2-transparent.png b/public/edutainment-baby-object/image2-picture-book-hands/left-hand-picture-book-v2-transparent.png new file mode 100644 index 00000000..65d8633f Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/left-hand-picture-book-v2-transparent.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/left-hand-picture-book-v2.png b/public/edutainment-baby-object/image2-picture-book-hands/left-hand-picture-book-v2.png new file mode 100644 index 00000000..43748d0a Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/left-hand-picture-book-v2.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/right-hand-picture-book-v1.png b/public/edutainment-baby-object/image2-picture-book-hands/right-hand-picture-book-v1.png new file mode 100644 index 00000000..c3bf647b Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/right-hand-picture-book-v1.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/right-hand-picture-book-v2-transparent.png b/public/edutainment-baby-object/image2-picture-book-hands/right-hand-picture-book-v2-transparent.png new file mode 100644 index 00000000..45cbdbac Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/right-hand-picture-book-v2-transparent.png differ diff --git a/public/edutainment-baby-object/image2-picture-book-hands/right-hand-picture-book-v2.png b/public/edutainment-baby-object/image2-picture-book-hands/right-hand-picture-book-v2.png new file mode 100644 index 00000000..5483d93d Binary files /dev/null and b/public/edutainment-baby-object/image2-picture-book-hands/right-hand-picture-book-v2.png differ diff --git a/public/wooden-fish/default-hit-object.png b/public/wooden-fish/default-hit-object.png new file mode 100644 index 00000000..44a6002c Binary files /dev/null and b/public/wooden-fish/default-hit-object.png differ diff --git a/scripts/dev.mjs b/scripts/dev.mjs index 3fc39c60..ef4fe055 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -675,7 +675,17 @@ class DevRunner { } if (!this.options.skipPublish) { - await this.publishSpacetimeModule(); + try { + await this.publishSpacetimeModule(); + } catch (error) { + if (isSpacetimePublishPermissionError(error)) { + console.warn( + `[dev:spacetime] 本地发布被当前 identity 拒绝,保留已启动的 standalone: ${error.message}`, + ); + } else { + throw error; + } + } } } @@ -769,20 +779,11 @@ class DevRunner { const env = {...this.baseEnv}; this.prepareMigrationBootstrapSecret(env); - const args = [ - 'publish', - this.options.database, - '--server', - this.state.spacetimeServer, - '--module-path', - modulePath, - '--build-options=--debug', - ]; - - if (!this.options.preserveDatabase) { - args.push('-c=on-conflict'); - } - args.push('--yes'); + const args = buildSpacetimePublishArgs({ + database: this.options.database, + preserveDatabase: this.options.preserveDatabase, + server: this.state.spacetimeServer, + }); console.log(`[dev:spacetime] 发布模块: ${this.options.database}`); await runForeground('spacetime', args, { @@ -815,7 +816,28 @@ class DevRunner { console.log(`[dev:spacetime] 迁移引导密钥: ${this.options.migrationBootstrapSecret}`); } - startApiServer(service) { + async ensureApiServerSpacetimeToken() { + const existingToken = String(this.baseEnv.GENARRATIVE_SPACETIME_TOKEN ?? '').trim(); + if (existingToken && shouldTrustExistingSpacetimeToken(existingToken, this.state.spacetimeServer)) { + return; + } + + const identityUrl = buildUrl(this.state.spacetimeServer, '/v1/identity'); + if (!identityUrl) { + throw new Error(`无法构造 SpacetimeDB identity 地址: ${this.state.spacetimeServer}`); + } + + const response = await fetchSpacetimeIdentity(identityUrl); + this.baseEnv.GENARRATIVE_SPACETIME_TOKEN = response.token; + this.state.spacetimeIdentity = response.identity; + console.log( + `[dev:spacetime] 已创建本地 Web identity: ${response.identity.slice(0, 12)}...`, + ); + } + + async startApiServer(service) { + await this.ensureApiServerSpacetimeToken(); + const mergedEnv = { ...this.baseEnv, GENARRATIVE_API_HOST: this.options.apiHost, @@ -1412,6 +1434,75 @@ async function isHttpReady(url, timeoutMs = 1000) { } } +async function fetchSpacetimeIdentity(url) { + let response; + try { + response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }); + } catch (error) { + throw new Error( + `SpacetimeDB identity 请求失败: ${url}; ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + + const text = await response.text(); + if (!response.ok) { + throw new Error(`SpacetimeDB identity HTTP ${response.status}: ${trimPreview(text)}`); + } + + let payload; + try { + payload = JSON.parse(text); + } catch (error) { + throw new Error( + `SpacetimeDB identity 响应不是合法 JSON: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + + const identity = + payload.identity ?? payload.Identity ?? payload.identity_hex ?? payload.identityHex; + const token = payload.token ?? payload.Token; + if (typeof identity !== 'string' || typeof token !== 'string') { + throw new Error(`SpacetimeDB identity 响应缺少 identity/token: ${trimPreview(text)}`); + } + + return {identity, token}; +} + +function shouldTrustExistingSpacetimeToken(existingToken, serverUrl) { + const shellToken = String(process.env.GENARRATIVE_SPACETIME_TOKEN ?? '').trim(); + if (shellToken && shellToken === existingToken) { + return true; + } + + return !isLoopbackSpacetimeServer(serverUrl); +} + +function isLoopbackSpacetimeServer(serverUrl) { + try { + const url = new URL(serverUrl); + return ['127.0.0.1', 'localhost', '::1'].includes(url.hostname); + } catch { + return false; + } +} + +function trimPreview(text, maxLength = 300) { + const normalized = String(text ?? '').replace(/\s+/gu, ' ').trim(); + return normalized.length > maxLength + ? `${normalized.slice(0, maxLength)}...` + : normalized; +} + function runForeground(command, args, {cwd, env, label}) { return new Promise((resolveRun, rejectRun) => { const child = spawn(command, args, { @@ -1459,10 +1550,40 @@ function normalizePath(path) { return path.replace(/\\/gu, '/'); } +function buildSpacetimePublishArgs({database, server, preserveDatabase}) { + const args = [ + 'publish', + database, + '--server', + server, + '--module-path', + modulePath, + '--build-options=--debug', + ]; + + if (!preserveDatabase) { + args.push('-c=on-conflict'); + } + + args.push('--yes', '--no-config'); + return args; +} + +function isSpacetimePublishPermissionError(error) { + const message = String(error?.message ?? error ?? ''); + return ( + message.includes('Pre-publish check failed with status 403 Forbidden') || + message.includes('not authorized to perform action on database') || + message.includes('is not authorized to perform action on database') + ); +} + export { DevRunner, + buildSpacetimePublishArgs, createDevServerSpawnOptions, createWatchConfigs, + isSpacetimePublishPermissionError, parseArgs, shouldAcceptWatchEvent, }; diff --git a/scripts/dev.test.ts b/scripts/dev.test.ts index 72f5661a..06015c59 100644 --- a/scripts/dev.test.ts +++ b/scripts/dev.test.ts @@ -6,8 +6,10 @@ import {afterEach, describe, expect, test, vi} from 'vitest'; import { DevRunner, + buildSpacetimePublishArgs, createDevServerSpawnOptions, createWatchConfigs, + isSpacetimePublishPermissionError, parseArgs, shouldAcceptWatchEvent, } from './dev.mjs'; @@ -210,6 +212,34 @@ describe('dev scheduler watch routing', () => { }); describe('dev scheduler spacetime refresh', () => { + test('本地发布 403 时识别为身份权限问题,避免误杀 standalone', () => { + const error = new Error( + 'Pre-publish check failed with status 403 Forbidden: c200... is not authorized to perform action on database c200...: update database', + ); + + expect(isSpacetimePublishPermissionError(error)).toBe(true); + expect(isSpacetimePublishPermissionError(new Error('No database target matches'))).toBe(false); + }); + + test('发布 spacetime-module 时忽略 spacetime.json 以免覆盖显式数据库', () => { + const args = buildSpacetimePublishArgs({ + database: 'xushi-p4wfr', + preserveDatabase: false, + server: 'http://127.0.0.1:3101', + }); + + expect(args).toContain('--no-config'); + expect(args).toEqual( + expect.arrayContaining([ + 'publish', + 'xushi-p4wfr', + '--server', + 'http://127.0.0.1:3101', + '-c=on-conflict', + ]), + ); + }); + test('手动刷新 spacetime 只重新发布模块,不重启 standalone 进程', async () => { const {explicitOptions, options} = parseArgs([], {}); const runner = new DevRunner(options, {}, explicitOptions); @@ -241,4 +271,62 @@ describe('dev scheduler spacetime refresh', () => { expect(runner.waitForSpacetime).not.toHaveBeenCalled(); expect(runner.publishSpacetimeModule).not.toHaveBeenCalled(); }); + + test('启动 api-server 前为空 token 自动创建本地 Web identity', async () => { + const {explicitOptions, options} = parseArgs([], { + GENARRATIVE_SPACETIME_TOKEN: '', + }); + const runner = new DevRunner(options, {}, explicitOptions); + runner.state.spacetimeServer = 'http://127.0.0.1:3101'; + globalThis.fetch = vi.fn(async () => ({ + ok: true, + status: 200, + text: async () => + JSON.stringify({ + identity: 'c200localidentity', + token: 'local-web-token', + }), + })) as unknown as typeof fetch; + + await runner.ensureApiServerSpacetimeToken(); + + expect(runner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBe('local-web-token'); + expect(globalThis.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:3101/v1/identity', + expect.objectContaining({ + method: 'POST', + }), + ); + }); + + test('本地 SpacetimeDB 不信任 env 文件中的陈旧 token', async () => { + const originalToken = process.env.GENARRATIVE_SPACETIME_TOKEN; + delete process.env.GENARRATIVE_SPACETIME_TOKEN; + try { + const {explicitOptions, options} = parseArgs([], { + GENARRATIVE_SPACETIME_TOKEN: 'stale-env-file-token', + }); + const runner = new DevRunner(options, {GENARRATIVE_SPACETIME_TOKEN: 'stale-env-file-token'}, explicitOptions); + runner.state.spacetimeServer = 'http://127.0.0.1:3101'; + globalThis.fetch = vi.fn(async () => ({ + ok: true, + status: 200, + text: async () => + JSON.stringify({ + identity: 'c200freshidentity', + token: 'fresh-web-token', + }), + })) as unknown as typeof fetch; + + await runner.ensureApiServerSpacetimeToken(); + + expect(runner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBe('fresh-web-token'); + } finally { + if (originalToken === undefined) { + delete process.env.GENARRATIVE_SPACETIME_TOKEN; + } else { + process.env.GENARRATIVE_SPACETIME_TOKEN = originalToken; + } + } + }); }); diff --git a/scripts/generate-edutainment-tv-map-concepts.mjs b/scripts/generate-edutainment-tv-map-concepts.mjs new file mode 100644 index 00000000..6131839d --- /dev/null +++ b/scripts/generate-edutainment-tv-map-concepts.mjs @@ -0,0 +1,394 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; + +const repoRoot = process.cwd(); +const outDir = path.join( + repoRoot, + 'output', + 'imagegen', + 'edutainment-tv-map-entry-concepts-20260518', +); +const styleReferencePath = path.join( + repoRoot, + 'public', + 'child-motion-demo', + 'picture-book-grass-stage.png', +); +const defaultTimeoutMs = 1000000; +const commonStyle = [ + '横屏 16:9 电视端寓教于乐板块交互入口概念图。', + '画面像儿童主题乐园地图,不是现实品牌乐园,不出现迪士尼、环球影城、城堡商标、影视 IP、真实品牌或可识别版权角色。', + '整体保持 Genarrative 寓教于乐现有明亮卡通绘本插画风:柔和水彩笔触、轻微纸张纹理、温暖草地、浅蓝天空、圆润可爱、低噪声、儿童友好。', + '地图分为 5 到 6 个清晰区域,每个区域像可点击玩法入口:宝贝识物、宝贝爱画、动作热身、拼图启蒙、声音节奏、自然探索;用图形和场景暗示模块,不写文字。', + '需要有主路径、分叉小路、入口节点、空白安全区和明显的焦点层级,适合后续在网页上叠加按钮、焦点光圈和中文标题。', + '构图为电视大屏横屏,远看是完整乐园地图,近看每个区域可作为独立交互入口;边缘留出安全裁切,不要把重要入口贴边。', + '不要出现文字、数字、字母、按钮文案、UI 面板、教程说明、水印、logo、真实照片质感、暗色科技风、过度商业广告感。', +].join(''); + +const concepts = [ + { + id: 'edutainment-tv-map-01-ring-park', + title: '环形乐园岛', + prompt: [ + commonStyle, + '版式方向:俯视略带透视的环形乐园岛,中央是柔软草地广场,外圈有一条蜿蜒小路串联 6 个入口区域。', + '区域暗示:左侧水果与小篮子区域代表宝贝识物;左下彩色画笔和画纸区域代表宝贝爱画;下方开阔草地圆环代表动作热身;右下拼图积木和图块小屋代表拼图启蒙;右侧小舞台和音符形花朵代表声音节奏;上方小树林和放大镜步道代表自然探索。', + '每个入口用圆润小建筑、道具和地形分区表现,入口节点尺寸接近,主路清楚,中央保留可叠加推荐焦点的位置。', + ].join(''), + }, + { + id: 'edutainment-tv-map-02-open-book', + title: '展开绘本地图', + prompt: [ + commonStyle, + '版式方向:一本巨大的横向展开绘本变成乐园地图,左右两页自然连接,中缝是一条小河或小路。', + '左页偏认知与绘画:果园篮子、动物剪影卡片、彩色蜡笔丘陵、画纸小屋;右页偏运动与探索:草地热身舞台、拼图桥、小音符剧场、树林观察台。', + '入口像从纸页上立起来的立体绘本机关,边缘有轻微纸张纹理和翻页层次,整体仍是干净可交互背景,不要文字。', + ].join(''), + }, + { + id: 'edutainment-tv-map-03-floating-islands', + title: '云朵空中岛', + prompt: [ + commonStyle, + '版式方向:浅蓝天空中的多个漂浮小岛,岛与岛之间由彩虹桥、云朵步道和藤蔓小路连接,横向展开适合电视端选择入口。', + '每个小岛是一个玩法模块入口:水果识物岛、画笔创作岛、草地运动岛、拼图机械小岛、声音花园岛、自然观察岛。', + '中央主岛最大,左右分布保持平衡,背景云层干净明亮,入口岛轮廓清晰,适合后续做焦点放大和悬停动效。', + ].join(''), + }, + { + id: 'edutainment-tv-map-04-stage-garden', + title: '草地舞台地图', + prompt: [ + commonStyle, + '版式方向:把现有寓教于乐草地舞台扩展成横屏互动乐园,前景是开阔草地,远景是小山、树木和柔和天空。', + '入口沿一条 S 形小路从左到右铺开:篮子果园、画画帐篷、动作圆环舞台、拼图桥、声音小剧场、探索小树林。', + '整体更接近实际运行态背景,可直接想象成电视端页面首屏;中心下方需要留空,给遥控器焦点框、入口标题或儿童角色站位使用。', + ].join(''), + }, +]; + +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 generationUrl(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 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'; +} + +function inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/jpeg') { + return 'jpg'; + } + return 'png'; +} + +function toDataUrl(filePath) { + if (!existsSync(filePath)) { + return null; + } + const bytes = readFileSync(filePath); + const extension = inferExtensionFromBytes(bytes); + const mime = extension === 'jpg' ? 'image/jpeg' : `image/${extension}`; + return `data:${mime};base64,${bytes.toString('base64')}`; +} + +function buildRequestBody(concept, size) { + const body = { + model: 'gpt-image-2-all', + prompt: concept.prompt, + n: 1, + size, + }; + const styleReference = toDataUrl(styleReferencePath); + if (styleReference) { + body.image = [styleReference]; + } + return body; +} + +function buildDryRunRequestBody(concept, size, hasStyleReference) { + return { + model: 'gpt-image-2-all', + prompt: concept.prompt, + n: 1, + size, + imageReferenceCount: hasStyleReference ? 1 : 0, + }; +} + +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 { + bytes: Buffer.from(await response.arrayBuffer()), + extension: inferExtensionFromContentType( + response.headers.get('content-type') || '', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, concept, size) { + const payload = await fetchJson( + generationUrl(env.baseUrl), + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(buildRequestBody(concept, size)), + }, + env.timeoutMs, + ); + + const urls = []; + const b64Images = []; + collectStringsByKey(payload, 'url', urls); + collectStringsByKey(payload, 'image', urls); + collectStringsByKey(payload, 'image_url', urls); + collectStringsByKey(payload, 'b64_json', b64Images); + + let image; + const imageUrl = [...new Set(urls)].find((url) => /^https?:\/\//u.test(url)); + if (imageUrl) { + image = await downloadUrl(imageUrl, env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${concept.id}`); + } + + mkdirSync(outDir, { recursive: true }); + const outputPath = path.join(outDir, `${concept.id}.${image.extension}`); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +const args = new Map(); +for (let index = 2; index < process.argv.length; index += 1) { + const raw = process.argv[index]; + if (raw.startsWith('--')) { + const next = process.argv[index + 1]; + if (next && !next.startsWith('--')) { + args.set(raw, next); + index += 1; + } else { + args.set(raw, true); + } + } +} + +const size = String(args.get('--size') || '2048x1152'); +const dryRun = args.has('--dry-run') || !args.has('--live'); +const selectedIds = String(args.get('--only') || '') + .split(',') + .map((value) => value.trim()) + .filter(Boolean); +const selectedConcepts = concepts.filter( + (concept) => selectedIds.length === 0 || selectedIds.includes(concept.id), +); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outDir, + size, + hasStyleReference: existsSync(styleReferencePath), + count: selectedConcepts.length, + requests: selectedConcepts.map((concept) => ({ + id: concept.id, + title: concept.title, + body: buildDryRunRequestBody( + concept, + size, + existsSync(styleReferencePath), + ), + })), + }, + 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 files = []; +for (const concept of selectedConcepts) { + console.log(`Generating ${concept.id}...`); + files.push(await generateOne(env, concept, size)); +} + +writeFileSync( + path.join(outDir, 'generation-metadata.json'), + JSON.stringify( + { + model: 'gpt-image-2-all', + size, + generatedAt: new Date().toISOString(), + styleReference: existsSync(styleReferencePath) + ? styleReferencePath + : null, + files: selectedConcepts.map((concept, index) => ({ + id: concept.id, + title: concept.title, + file: files[index], + prompt: concept.prompt, + })), + }, + null, + 2, + ), +); + +console.log(JSON.stringify({ ok: true, count: files.length, files }, null, 2)); diff --git a/scripts/generate-taonier-abstract-mascot-image2-contact-sheet.py b/scripts/generate-taonier-abstract-mascot-image2-contact-sheet.py new file mode 100644 index 00000000..64dbcd62 --- /dev/null +++ b/scripts/generate-taonier-abstract-mascot-image2-contact-sheet.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-abstract-mascot-image2-concepts" +) +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-abstract-mascot-image2-contact-sheet.png" + +ITEMS = [ + ("01 泥灵符号", "taonier-image2-clay-spirit-glyph.png"), + ("02 捏胚小偶", "taonier-image2-pinched-seed-mascot.png"), + ("03 软陶图灵", "taonier-image2-soft-totem-creature.png"), + ("04 口袋泥符", "taonier-image2-clay-pocket-token.png"), + ("05 作品泥偶", "taonier-image2-work-core-puppet.png"), + ("06 模团伙伴", "taonier-image2-mold-blob-companion.png"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def main() -> None: + cell_size = 330 + label_height = 58 + gap = 28 + columns = 3 + rows = 2 + width = columns * cell_size + (columns + 1) * gap + height = rows * (cell_size + label_height) + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#eee9df") + draw = ImageDraw.Draw(sheet) + font = load_font(24) + + for index, (label, filename) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_size + label_height + gap) + + source_path = OUTPUT_DIR / filename + if not source_path.exists(): + continue + source = Image.open(source_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=10, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=font) + + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-abstract-mascot-image2-logo-concepts.mjs b/scripts/generate-taonier-abstract-mascot-image2-logo-concepts.mjs new file mode 100644 index 00000000..ba3adb51 --- /dev/null +++ b/scripts/generate-taonier-abstract-mascot-image2-logo-concepts.mjs @@ -0,0 +1,330 @@ +import { Buffer } from 'node:buffer'; +import { + existsSync, + mkdirSync, + readFileSync, + readdirSync, + writeFileSync, +} from 'node:fs'; +import path from 'node:path'; + +const repoRoot = process.cwd(); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-abstract-mascot-image2-concepts', +); +const defaultTimeoutMs = 420000; + +const concepts = [ + { + id: 'taonier-image2-clay-spirit-glyph', + title: '泥灵符号', + prompt: + '为中文产品“陶泥儿”设计一个无文字 Logo 图标。方向:陶泥人、陶泥手办、抽象角色 / 吉祥物,但不要做人体形象,也不要做完整角色插画。主体像一块被轻轻捏出生命感的陶泥符号:单一圆润剪影,内部一个极简星核或负形孔洞,只有很少的点状生命感。必须是扁平矢量、主流 App icon、简单几何、亲和、醒目、可记忆、小尺寸清晰。配色:奶油白主形、暖陶土橙点缀、深墨背景、少量金色星核。禁止文字、字母、汉字、水印、真实人形、手脚、复杂五官、聊天气泡、播放三角、儿童黏土课、3D、厚阴影、贴纸感。', + }, + { + id: 'taonier-image2-pinched-seed-mascot', + title: '捏胚小偶', + prompt: + '为“陶泥儿”设计无文字品牌主标。把“陶泥小人 / 手办 / 吉祥物”的精神压缩成一个非人形的几何泥胚:像一颗被捏过的种子或软陶坯,带一点抽象生命感,但没有手、脚、头发、衣服。中心有一颗极简四角闪光星,表达 AI 把脑洞捏成作品。风格:flat vector mascot mark, modern, friendly, geometric, logo-ready, not illustration。颜色控制在 3 到 4 色:深墨、奶白、陶土橙、暖黄。禁止文字、字母、汉字、真实陶艺工具、儿童玩具、emoji 表情、聊天气泡、播放按钮、复杂装饰、立体渲染。', + }, + { + id: 'taonier-image2-soft-totem-creature', + title: '软陶图灵', + prompt: + '为“陶泥儿”设计无文字 Logo。主体是一个抽象陶泥角色图腾,不是人,也不是动物,而是一枚软陶手办被极简化后的品牌符号:上窄下稳、边缘像手捏过,中央有一个圆形作品核和一个小泥点。要有吉祥物的亲和力,但更像成熟平台主标。风格:bold flat vector, iconic silhouette, playful premium, highly memorable, simple enough for favicon。配色:深色背景、奶油白大形、陶土橙作品核、薄荷青或金色小点。禁止中文英文、复杂脸、两只眼睛加嘴的头像感、人体、手、脚、聊天气泡、播放三角、3D 质感。', + }, + { + id: 'taonier-image2-clay-pocket-token', + title: '口袋泥符', + prompt: + '为“陶泥儿”设计一个能做 App icon 的无文字主 Logo。方向是“抽象口袋陶泥手办”:它像一个可以被收藏的小软陶 token,但不能是具体人物。主图形由一个圆角几何泥块、一处被捏出的缺口、一枚小星核组成,轮廓要一眼能记住。气质年轻、Q、可爱但不幼稚,适合 AI UGC 轻休闲小游戏平台。扁平矢量感,少色,高对比。禁止文字、字母、水印、真实人脸、手脚、表情包、聊天气泡、播放按钮、过多小元素、3D、照片感。', + }, + { + id: 'taonier-image2-work-core-puppet', + title: '作品泥偶', + prompt: + '为“陶泥儿”设计无文字 Logo 图标。把陶泥手办的收藏感、AI 创作的作品核、UGC 玩梗传播的轻松感融合成一个抽象泥偶符号。不要画成人体,只用几何软块和负形孔洞表现“像有生命的陶泥作品”。主体简单、厚实、圆润,中心一枚四角星或小圆核,最多两个辅助泥点。风格:minimal vector mascot logo, clean, premium cute, mainstream consumer app icon。颜色:奶白、陶土橙、深墨、暖黄,可少量青绿。禁止文字、英文、汉字、复杂背景、复杂五官、真实手办、玩具包装、儿童黏土、3D 厚重阴影。', + }, + { + id: 'taonier-image2-mold-blob-companion', + title: '模团伙伴', + prompt: + '为“陶泥儿”做一个无文字 Logo 主标。方向:非人形抽象吉祥物,像一团被模具轻轻压出的陶泥伙伴。整体是一个简单几何软形,带一个偏心孔洞和一个小闪光星,让人感觉它能承载用户脑洞、生成小游戏作品。要求主流、亲和、可爱、扁平、矢量、识别强,不能像插画或头像。配色:深墨底、奶油白主体、陶土橙和暖黄点缀,最多 4 色。禁止文字、字母、汉字、水印、手脚、五官表情、聊天气泡、播放三角、复杂碎片、3D、真实陶泥照片。', + }, +]; + +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); + } +} + +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', + quality: String(args.get('--quality') || 'low'), + 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(outputDir, { recursive: true }); + const extension = inferExtensionFromBytes(bytes); + const outputPath = path.join(outputDir, `${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', + outputDir, + count: selected.length, + requests: selected.map((concept) => ({ + id: concept.id, + title: concept.title, + body: { + model: 'gpt-image-2-all', + quality: String(args.get('--quality') || 'low'), + 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, + verifiedFiles: readdirSync(outputDir).sort(), + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-abstract-mascot-logo-concepts.py b/scripts/generate-taonier-abstract-mascot-logo-concepts.py new file mode 100644 index 00000000..40629582 --- /dev/null +++ b/scripts/generate-taonier-abstract-mascot-logo-concepts.py @@ -0,0 +1,295 @@ +from __future__ import annotations + +import math +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-abstract-mascot-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-abstract-mascot-contact-sheet.png" + +SIZE = 1024 +SCALE = 4 + +INK = "#121212" +INK_BLUE = "#101418" +CREAM = "#fff5df" +CLAY = "#d77750" +CLAY_DARK = "#b95f3f" +GOLD = "#ffd25d" +MINT = "#31c7a9" +CORAL = "#ff6a5f" + +VARIANTS = [ + ("taonier-abstract-mascot-clay-bean", "陶泥豆偶", "clay_bean"), + ("taonier-abstract-mascot-mold-baby", "模胚小灵", "mold_baby"), + ("taonier-abstract-mascot-dot-face", "泥点面偶", "dot_face"), + ("taonier-abstract-mascot-soft-totem", "软陶图腾", "soft_totem"), + ("taonier-abstract-mascot-clay-seed", "陶泥种子", "clay_seed"), + ("taonier-abstract-mascot-work-puppet", "作品泥灵", "work_puppet"), +] + + +def hex_to_rgb(value: str) -> tuple[int, int, int]: + value = value.removeprefix("#") + return tuple(int(value[index : index + 2], 16) for index in (0, 2, 4)) + + +def s(value: float) -> int: + return round(value * SCALE) + + +def circle(draw: ImageDraw.ImageDraw, cx: float, cy: float, radius: float, fill: str) -> None: + draw.ellipse((s(cx - radius), s(cy - radius), s(cx + radius), s(cy + radius)), fill=hex_to_rgb(fill)) + + +def rounded_rect( + draw: ImageDraw.ImageDraw, + box: tuple[float, float, float, float], + radius: float, + fill: str, +) -> None: + draw.rounded_rectangle(tuple(s(value) for value in box), radius=s(radius), fill=hex_to_rgb(fill)) + + +def polygon(cx: float, cy: float, radius: float, sides: int, rotation: float) -> list[tuple[int, int]]: + return [ + (s(cx + math.cos(rotation + math.tau * index / sides) * radius), s(cy + math.sin(rotation + math.tau * index / sides) * radius)) + for index in range(sides) + ] + + +def star_points(cx: float, cy: float, outer: float, inner: float, count: int = 4) -> list[tuple[int, int]]: + points = [] + for index in range(count * 2): + radius = outer if index % 2 == 0 else inner + angle = -math.pi / 2 + index * math.pi / count + points.append((s(cx + math.cos(angle) * radius), s(cy + math.sin(angle) * radius))) + return points + + +def draw_clay_bean(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#101010")) + rounded_rect(draw, (270, 214, 754, 820), 228, CREAM) + circle(draw, 650, 330, 112, CLAY) + circle(draw, 676, 354, 86, CREAM) + circle(draw, 430, 470, 34, INK) + circle(draw, 590, 470, 34, INK) + draw.polygon(star_points(512, 618, 66, 28), fill=hex_to_rgb(GOLD)) + rounded_rect(draw, (360, 742, 664, 790), 24, CLAY) + + +def draw_mold_baby(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(INK_BLUE)) + draw.polygon(polygon(512, 498, 304, 8, math.pi / 8), fill=hex_to_rgb(CREAM)) + circle(draw, 512, 398, 126, "#101418") + circle(draw, 512, 398, 62, GOLD) + circle(draw, 390, 570, 30, "#101418") + circle(draw, 634, 570, 30, "#101418") + rounded_rect(draw, (380, 704, 644, 758), 27, MINT) + rounded_rect(draw, (318, 268, 460, 326), 29, CORAL) + + +def draw_dot_face(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#17110e")) + rounded_rect(draw, (254, 254, 770, 770), 190, CLAY) + rounded_rect(draw, (330, 320, 694, 706), 140, CREAM) + circle(draw, 440, 478, 30, "#17110e") + circle(draw, 584, 478, 30, "#17110e") + rounded_rect(draw, (458, 594, 566, 638), 22, CLAY) + circle(draw, 512, 254, 54, GOLD) + circle(draw, 512, 254, 24, "#17110e") + + +def draw_soft_totem(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#111111")) + rounded_rect(draw, (340, 188, 684, 836), 172, CREAM) + circle(draw, 512, 336, 112, CLAY) + rounded_rect(draw, (390, 442, 634, 722), 122, "#111111") + circle(draw, 512, 582, 58, GOLD) + circle(draw, 432, 332, 24, INK) + circle(draw, 592, 332, 24, INK) + rounded_rect(draw, (404, 782, 620, 842), 30, CREAM) + + +def draw_clay_seed(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#101418")) + draw.pieslice((s(236), s(176), s(788), s(832)), start=218, end=578, fill=hex_to_rgb(CREAM)) + circle(draw, 618, 326, 72, "#101418") + circle(draw, 618, 326, 34, GOLD) + circle(draw, 438, 488, 30, "#101418") + rounded_rect(draw, (506, 548, 632, 594), 23, CLAY) + draw.polygon(star_points(512, 682, 58, 24), fill=hex_to_rgb(GOLD)) + + +def draw_work_puppet(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#101010")) + rounded_rect(draw, (286, 300, 738, 756), 150, CREAM) + circle(draw, 286, 530, 88, "#101010") + circle(draw, 738, 530, 88, "#101010") + circle(draw, 512, 300, 76, GOLD) + circle(draw, 430, 474, 24, "#101010") + circle(draw, 594, 474, 24, "#101010") + draw.polygon(star_points(512, 604, 68, 28), fill=hex_to_rgb("#101010")) + draw.polygon(star_points(512, 604, 34, 14), fill=hex_to_rgb(GOLD)) + rounded_rect(draw, (360, 744, 664, 798), 27, CLAY) + + +DRAWERS = { + "clay_bean": draw_clay_bean, + "mold_baby": draw_mold_baby, + "dot_face": draw_dot_face, + "soft_totem": draw_soft_totem, + "clay_seed": draw_clay_seed, + "work_puppet": draw_work_puppet, +} + + +def render_variant(style: str) -> Image.Image: + image = Image.new("RGB", (SIZE * SCALE, SIZE * SCALE), hex_to_rgb("#111111")) + draw = ImageDraw.Draw(image) + DRAWERS[style](draw) + return image.resize((SIZE, SIZE), Image.Resampling.LANCZOS) + + +def build_svg(style: str) -> str: + # PNG 是评审主物料;SVG 保留几何结构,供后续人工矢量微调。 + if style == "clay_bean": + body = f''' + + + + + + + + ''' + elif style == "mold_baby": + body = f''' + + + + + + + + ''' + elif style == "dot_face": + body = f''' + + + + + + + + ''' + elif style == "soft_totem": + body = f''' + + + + + + + + ''' + elif style == "clay_seed": + body = f''' + + + + + + + ''' + else: + body = f''' + + + + + + + + + + ''' + return f''' + +{body} + +''' + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def build_contact_sheet(previews: list[tuple[str, str, Image.Image]]) -> Image.Image: + cell_size = 320 + label_height = 60 + gap = 28 + columns = 3 + rows = 2 + width = columns * cell_size + (columns + 1) * gap + height = rows * (cell_size + label_height) + (rows + 1) * gap + sheet = Image.new("RGB", (width, height), "#eee9df") + draw = ImageDraw.Draw(sheet) + font = load_font(23) + + for index, (_, title, preview) in enumerate(previews): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_size + label_height + gap) + thumbnail = preview.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=10, + fill="#fffdf8", + ) + label = f"{index + 1:02d} {title}" + text_box = draw.textbbox((0, 0), label, font=font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=font) + return sheet + + +def main() -> None: + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + previews: list[tuple[str, str, Image.Image]] = [] + + for asset_id, title, style in VARIANTS: + preview = render_variant(style) + preview.save(OUTPUT_DIR / f"{asset_id}.png") + (OUTPUT_DIR / f"{asset_id}.svg").write_text(build_svg(style), encoding="utf-8") + previews.append((asset_id, title, preview)) + + contact_sheet = build_contact_sheet(previews) + contact_sheet.save(CONTACT_SHEET_PATH, quality=95) + print( + { + "ok": True, + "output_dir": str(OUTPUT_DIR), + "files": [f"{asset_id}.png" for asset_id, _, _ in VARIANTS] + + [f"{asset_id}.svg" for asset_id, _, _ in VARIANTS] + + [CONTACT_SHEET_PATH.name], + } + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-abstract-mascot-minimal-contact-sheet.py b/scripts/generate-taonier-abstract-mascot-minimal-contact-sheet.py new file mode 100644 index 00000000..f2f58b48 --- /dev/null +++ b/scripts/generate-taonier-abstract-mascot-minimal-contact-sheet.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-abstract-mascot-minimal-concepts" +) +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-abstract-mascot-minimal-contact-sheet.png" + +ITEMS = [ + ("01 泥芯主标", "taonier-minimal-clay-core.png"), + ("02 泥标小偶", "taonier-minimal-clay-token.png"), + ("03 泥种图符", "taonier-minimal-seed-glyph.png"), + ("04 模胚小芽", "taonier-minimal-mold-bud.png"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def main() -> None: + cell_size = 330 + label_height = 58 + gap = 28 + columns = 2 + rows = 2 + width = columns * cell_size + (columns + 1) * gap + height = rows * (cell_size + label_height) + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#eee9df") + draw = ImageDraw.Draw(sheet) + font = load_font(24) + + for index, (label, filename) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_size + label_height + gap) + + source_path = OUTPUT_DIR / filename + if not source_path.exists(): + continue + source = Image.open(source_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=10, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=font) + + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-abstract-mascot-minimal-logo-concepts.mjs b/scripts/generate-taonier-abstract-mascot-minimal-logo-concepts.mjs new file mode 100644 index 00000000..d0912fb9 --- /dev/null +++ b/scripts/generate-taonier-abstract-mascot-minimal-logo-concepts.mjs @@ -0,0 +1,318 @@ +import { Buffer } from 'node:buffer'; +import { + existsSync, + mkdirSync, + readFileSync, + readdirSync, + writeFileSync, +} from 'node:fs'; +import path from 'node:path'; + +const repoRoot = process.cwd(); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-abstract-mascot-minimal-concepts', +); +const defaultTimeoutMs = 420000; + +const concepts = [ + { + id: 'taonier-minimal-clay-core', + title: '泥芯主标', + prompt: + '为中文产品“陶泥儿”设计一个无文字 Logo。方向是抽象陶泥角色 / 吉祥物,但不要人形,不要脸,不要手脚。主体是一枚极简陶泥胚,只有一个主轮廓、一个偏心孔或作品核、一个小星点,像会呼吸的陶泥主标。必须扁平、几何、简洁、亲和、主流 App icon 风格。配色:奶油白、陶土橙、深墨底、少量金色。禁止文字、字母、汉字、水印、复杂五官、聊天气泡、播放三角、儿童玩具、3D、厚阴影、背景场景。', + }, + { + id: 'taonier-minimal-clay-token', + title: '泥标小偶', + prompt: + '为“陶泥儿”设计无文字品牌 Logo。主体不是人物,而是一枚像被捏出来的软陶 token:圆润、稳定、边缘有手捏感,内部只有一个极简星核或孔洞,不要眼睛鼻子嘴巴。风格:flat vector mascot mark, simple, memorable, logo-ready, cute but mature. 配色限制在 3 色到 4 色:奶白、陶土橙、深墨、暖黄。禁止文字、字母、汉字、表情包、聊天气泡、播放按钮、真实陶艺工具、复杂碎片、3D、照片感。', + }, + { + id: 'taonier-minimal-seed-glyph', + title: '泥种图符', + prompt: + '为中文产品“陶泥儿”设计一个无文字 Logo 图标。主题是“抽象陶泥角色”,但造型不使用人体。图形像一颗被轻轻捏过的种子,或者一枚从模具里长出的泥符,轮廓简单,记忆点集中在一个偏心洞和一颗小星核。要求简洁、几何、扁平、可注册、适合 App icon。配色:奶油白主体、暖陶土点缀、深墨背景、少量金黄。禁止文字、字母、汉字、水印、五官、手脚、动物、聊天气泡、播放三角、厚阴影、3D、背景道具。', + }, + { + id: 'taonier-minimal-mold-bud', + title: '模胚小芽', + prompt: + '为“陶泥儿”设计无文字 Logo。主体像一枚从模具里鼓起来的陶泥小芽,只有一个主形、一个缺口、一个闪光点,不要人形,不要头像,不要复杂装饰。整体要像能代表 AI 创作、UGC 造物、轻休闲平台的品牌主标。风格:minimal flat mascot logo, clean, playful, premium, scalable. 配色:深墨、奶白、陶土橙、薄荷青或暖黄。禁止文字、字母、汉字、真实脸、聊天气泡、播放键、儿童卡通、3D、金属质感、摄影背景。', + }, +]; + +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); + } +} + +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', + quality: String(args.get('--quality') || 'low'), + 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(outputDir, { recursive: true }); + const extension = inferExtensionFromBytes(bytes); + const outputPath = path.join(outputDir, `${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', + outputDir, + count: selected.length, + requests: selected.map((concept) => ({ + id: concept.id, + title: concept.title, + body: { + model: 'gpt-image-2-all', + quality: String(args.get('--quality') || 'low'), + 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, + verifiedFiles: readdirSync(outputDir).sort(), + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-abstract-mascot-v2-logo-concepts.py b/scripts/generate-taonier-abstract-mascot-v2-logo-concepts.py new file mode 100644 index 00000000..39584eab --- /dev/null +++ b/scripts/generate-taonier-abstract-mascot-v2-logo-concepts.py @@ -0,0 +1,297 @@ +from __future__ import annotations + +import math +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-abstract-mascot-v2-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-abstract-mascot-v2-contact-sheet.png" + +SIZE = 1024 +SCALE = 4 + +BG = "#101010" +BG_WARM = "#17110e" +BG_BLUE = "#101418" +INK = "#111111" +CREAM = "#fff3d7" +CLAY = "#df7650" +CLAY_DARK = "#bd5b3d" +GOLD = "#ffd35f" +MINT = "#2ec5ad" +CORAL = "#ff6b61" + +VARIANTS = [ + ("taonier-abstract-mascot-v2-clay-sprite", "陶泥小灵", "clay_sprite"), + ("taonier-abstract-mascot-v2-pinch-orbit", "捏孔泥偶", "pinch_orbit"), + ("taonier-abstract-mascot-v2-seed-totem", "星胚图腾", "seed_totem"), + ("taonier-abstract-mascot-v2-soft-mold", "软模团子", "soft_mold"), + ("taonier-abstract-mascot-v2-clay-orb", "泥芯圆偶", "clay_orb"), + ("taonier-abstract-mascot-v2-work-glyph", "作品泥符", "work_glyph"), +] + + +def hex_to_rgb(value: str) -> tuple[int, int, int]: + value = value.removeprefix("#") + return tuple(int(value[index : index + 2], 16) for index in (0, 2, 4)) + + +def s(value: float) -> int: + return round(value * SCALE) + + +def circle(draw: ImageDraw.ImageDraw, cx: float, cy: float, radius: float, fill: str) -> None: + draw.ellipse((s(cx - radius), s(cy - radius), s(cx + radius), s(cy + radius)), fill=hex_to_rgb(fill)) + + +def ellipse( + draw: ImageDraw.ImageDraw, + box: tuple[float, float, float, float], + fill: str, +) -> None: + draw.ellipse(tuple(s(value) for value in box), fill=hex_to_rgb(fill)) + + +def rounded_rect( + draw: ImageDraw.ImageDraw, + box: tuple[float, float, float, float], + radius: float, + fill: str, +) -> None: + draw.rounded_rectangle(tuple(s(value) for value in box), radius=s(radius), fill=hex_to_rgb(fill)) + + +def polygon(cx: float, cy: float, radius: float, sides: int, rotation: float) -> list[tuple[int, int]]: + return [ + (s(cx + math.cos(rotation + math.tau * index / sides) * radius), s(cy + math.sin(rotation + math.tau * index / sides) * radius)) + for index in range(sides) + ] + + +def sparkle(cx: float, cy: float, outer: float, inner: float) -> list[tuple[int, int]]: + points = [] + for index in range(8): + radius = outer if index % 2 == 0 else inner + angle = -math.pi / 2 + index * math.pi / 4 + points.append((s(cx + math.cos(angle) * radius), s(cy + math.sin(angle) * radius))) + return points + + +def draw_clay_sprite(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(BG)) + rounded_rect(draw, (308, 210, 708, 810), 198, CREAM) + circle(draw, 672, 302, 82, CLAY) + circle(draw, 716, 336, 74, BG) + circle(draw, 402, 458, 34, INK) + draw.polygon(sparkle(548, 584, 64, 24), fill=hex_to_rgb(GOLD)) + rounded_rect(draw, (362, 742, 660, 792), 25, CLAY) + + +def draw_pinch_orbit(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(BG_BLUE)) + circle(draw, 512, 512, 276, CREAM) + circle(draw, 724, 394, 94, BG_BLUE) + circle(draw, 692, 412, 38, GOLD) + circle(draw, 314, 512, 76, BG_BLUE) + rounded_rect(draw, (442, 642, 594, 690), 24, CLAY) + circle(draw, 438, 448, 32, INK) + + +def draw_seed_totem(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(BG_WARM)) + draw.polygon(polygon(512, 514, 310, 8, math.pi / 8), fill=hex_to_rgb(CREAM)) + circle(draw, 512, 282, 76, GOLD) + rounded_rect(draw, (376, 356, 648, 726), 136, CLAY) + circle(draw, 430, 528, 28, BG_WARM) + circle(draw, 594, 528, 28, BG_WARM) + draw.polygon(sparkle(512, 634, 58, 22), fill=hex_to_rgb(CREAM)) + rounded_rect(draw, (386, 764, 638, 818), 27, MINT) + + +def draw_soft_mold(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(BG)) + rounded_rect(draw, (270, 300, 754, 740), 148, CREAM) + circle(draw, 270, 520, 84, BG) + circle(draw, 754, 520, 84, BG) + rounded_rect(draw, (390, 404, 634, 650), 110, CLAY) + circle(draw, 512, 526, 62, GOLD) + circle(draw, 512, 526, 28, BG) + rounded_rect(draw, (356, 728, 668, 782), 27, CLAY_DARK) + + +def draw_clay_orb(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(BG_BLUE)) + circle(draw, 512, 512, 274, CREAM) + circle(draw, 512, 512, 142, BG_BLUE) + draw.polygon(sparkle(512, 512, 70, 26), fill=hex_to_rgb(GOLD)) + circle(draw, 648, 340, 64, CLAY) + circle(draw, 666, 360, 40, BG_BLUE) + circle(draw, 374, 650, 46, MINT) + + +def draw_work_glyph(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb(BG_WARM)) + rounded_rect(draw, (340, 184, 684, 832), 172, CREAM) + circle(draw, 512, 348, 124, CLAY) + circle(draw, 512, 348, 56, BG_WARM) + draw.polygon(sparkle(512, 348, 42, 15), fill=hex_to_rgb(GOLD)) + rounded_rect(draw, (412, 492, 612, 690), 98, BG_WARM) + circle(draw, 512, 592, 50, GOLD) + rounded_rect(draw, (404, 764, 620, 824), 30, CREAM) + + +DRAWERS = { + "clay_sprite": draw_clay_sprite, + "pinch_orbit": draw_pinch_orbit, + "seed_totem": draw_seed_totem, + "soft_mold": draw_soft_mold, + "clay_orb": draw_clay_orb, + "work_glyph": draw_work_glyph, +} + + +def render_variant(style: str) -> Image.Image: + image = Image.new("RGB", (SIZE * SCALE, SIZE * SCALE), hex_to_rgb(BG)) + draw = ImageDraw.Draw(image) + DRAWERS[style](draw) + return image.resize((SIZE, SIZE), Image.Resampling.LANCZOS) + + +def build_svg(style: str) -> str: + # PNG 用于快速评审;SVG 保留主几何结构,便于后续进入正式矢量设计。 + if style == "clay_sprite": + body = f''' + + + + + + + ''' + elif style == "pinch_orbit": + body = f''' + + + + + + + ''' + elif style == "seed_totem": + body = f''' + + + + + + + + ''' + elif style == "soft_mold": + body = f''' + + + + + + + + ''' + elif style == "clay_orb": + body = f''' + + + + + + + ''' + else: + body = f''' + + + + + + + + ''' + return f''' + +{body} + +''' + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def build_contact_sheet(previews: list[tuple[str, str, Image.Image]]) -> Image.Image: + cell_size = 320 + label_height = 60 + gap = 28 + columns = 3 + rows = 2 + width = columns * cell_size + (columns + 1) * gap + height = rows * (cell_size + label_height) + (rows + 1) * gap + sheet = Image.new("RGB", (width, height), "#eee9df") + draw = ImageDraw.Draw(sheet) + font = load_font(23) + + for index, (_, title, preview) in enumerate(previews): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_size + label_height + gap) + thumbnail = preview.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=10, + fill="#fffdf8", + ) + label = f"{index + 1:02d} {title}" + text_box = draw.textbbox((0, 0), label, font=font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=font) + return sheet + + +def main() -> None: + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + previews: list[tuple[str, str, Image.Image]] = [] + + for asset_id, title, style in VARIANTS: + preview = render_variant(style) + preview.save(OUTPUT_DIR / f"{asset_id}.png") + (OUTPUT_DIR / f"{asset_id}.svg").write_text(build_svg(style), encoding="utf-8") + previews.append((asset_id, title, preview)) + + contact_sheet = build_contact_sheet(previews) + contact_sheet.save(CONTACT_SHEET_PATH, quality=95) + print( + { + "ok": True, + "output_dir": str(OUTPUT_DIR), + "files": [f"{asset_id}.png" for asset_id, _, _ in VARIANTS] + + [f"{asset_id}.svg" for asset_id, _, _ in VARIANTS] + + [CONTACT_SHEET_PATH.name], + } + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-anchor-logo-concepts.py b/scripts/generate-taonier-anchor-logo-concepts.py new file mode 100644 index 00000000..a744bdfc --- /dev/null +++ b/scripts/generate-taonier-anchor-logo-concepts.py @@ -0,0 +1,323 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Iterable + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-anchor-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-anchor-contact-sheet.png" + +SIZE = 1024 +SCALE = 4 + +VARIANTS = [ + { + "id": "taonier-anchor-core", + "title": "泥点锚标", + "bg": "#151515", + "mark": "#ffffff", + "accent": "#ffffff", + "style": "core", + }, + { + "id": "taonier-anchor-soft-slab", + "title": "软泥层台", + "bg": "#111111", + "mark": "#fffdf4", + "accent": "#fffdf4", + "style": "soft_slab", + }, + { + "id": "taonier-anchor-work-stack", + "title": "作品叠层", + "bg": "#171717", + "mark": "#ffffff", + "accent": "#ffffff", + "style": "work_stack", + }, + { + "id": "taonier-anchor-clay-drop", + "title": "泥点落印", + "bg": "#151515", + "mark": "#ffffff", + "accent": "#f5c95d", + "style": "clay_drop", + }, + { + "id": "taonier-anchor-creation-base", + "title": "创作底座", + "bg": "#121212", + "mark": "#ffffff", + "accent": "#ffffff", + "style": "creation_base", + }, + { + "id": "taonier-anchor-app-token", + "title": "泥点应用标", + "bg": "#101418", + "mark": "#fffaf0", + "accent": "#ffd45d", + "style": "app_token", + }, +] + + +def hex_to_rgb(value: str) -> tuple[int, int, int]: + value = value.removeprefix("#") + return tuple(int(value[index : index + 2], 16) for index in (0, 2, 4)) + + +def s(value: float) -> int: + return round(value * SCALE) + + +def scaled_points(points: Iterable[tuple[float, float]]) -> list[tuple[int, int]]: + return [(s(x), s(y)) for x, y in points] + + +def quad( + start: tuple[float, float], + control: tuple[float, float], + end: tuple[float, float], + steps: int = 24, +) -> list[tuple[float, float]]: + points: list[tuple[float, float]] = [] + for index in range(steps + 1): + t = index / steps + x = (1 - t) * (1 - t) * start[0] + 2 * (1 - t) * t * control[0] + t * t * end[0] + y = (1 - t) * (1 - t) * start[1] + 2 * (1 - t) * t * control[1] + t * t * end[1] + points.append((x, y)) + return points + + +def round_line( + draw: ImageDraw.ImageDraw, + points: list[tuple[float, float]], + fill: str, + width: int, + closed: bool = False, +) -> None: + scaled = scaled_points(points) + if closed: + scaled = [*scaled, scaled[0]] + draw.line(scaled, fill=hex_to_rgb(fill), width=s(width), joint="curve") + radius = s(width) // 2 + if not closed: + for x, y in (scaled[0], scaled[-1]): + draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=hex_to_rgb(fill)) + + +def round_circle(draw: ImageDraw.ImageDraw, center: tuple[float, float], radius: float, fill: str) -> None: + x, y = center + draw.ellipse((s(x - radius), s(y - radius), s(x + radius), s(y + radius)), fill=hex_to_rgb(fill)) + + +def draw_core(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None: + round_line(draw, [(512, 326), (512, 514)], mark, 68) + round_circle(draw, (512, 214), 62, accent) + round_line(draw, [(244, 548), (512, 430), (780, 548), (512, 674)], mark, 66, closed=True) + round_line(draw, [(292, 656), (468, 734), (512, 752), (556, 734), (732, 656)], mark, 52) + round_circle(draw, (337, 548), 17, mark) + + +def draw_soft_slab(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None: + round_line(draw, [(512, 316), (512, 518)], mark, 72) + round_circle(draw, (512, 205), 58, accent) + top = ( + quad((232, 560), (512, 410), (792, 560), 32) + + quad((792, 560), (816, 576), (792, 592), 8)[1:] + + quad((792, 592), (610, 648), (552, 692), 20)[1:] + + quad((552, 692), (512, 716), (472, 692), 12)[1:] + + quad((472, 692), (414, 648), (232, 592), 20)[1:] + + quad((232, 592), (208, 576), (232, 560), 8)[1:] + ) + round_line(draw, top, mark, 56, closed=True) + round_line(draw, [(278, 642), (470, 728), (512, 748), (554, 728), (746, 642)], mark, 46) + round_circle(draw, (342, 554), 15, mark) + + +def draw_work_stack(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None: + round_line(draw, [(512, 310), (512, 504)], mark, 64) + round_circle(draw, (512, 205), 56, accent) + round_line(draw, [(246, 532), (512, 420), (778, 532), (512, 650)], mark, 58, closed=True) + round_line(draw, [(286, 628), (472, 710), (512, 728), (552, 710), (738, 628)], mark, 48) + round_line(draw, [(330, 708), (478, 774), (512, 790), (546, 774), (694, 708)], mark, 38) + round_circle(draw, (348, 535), 14, mark) + + +def draw_clay_drop(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None: + round_line(draw, [(512, 334), (512, 504)], mark, 64) + round_circle(draw, (512, 204), 62, accent) + round_line(draw, [(252, 548), (512, 436), (772, 548), (512, 666)], mark, 62, closed=True) + round_line(draw, [(304, 642), (474, 718), (512, 736), (550, 718), (720, 642)], mark, 50) + round_circle(draw, (346, 548), 16, mark) + round_circle(draw, (512, 556), 12, accent) + + +def draw_creation_base(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None: + round_line(draw, [(512, 304), (512, 494)], mark, 58) + round_circle(draw, (512, 202), 55, accent) + round_line(draw, [(276, 522), (512, 420), (748, 522)], mark, 54) + round_line(draw, [(236, 586), (512, 708), (788, 586)], mark, 60) + round_line(draw, [(292, 676), (478, 756), (512, 770), (546, 756), (732, 676)], mark, 42) + round_circle(draw, (355, 544), 13, mark) + + +def draw_app_token(draw: ImageDraw.ImageDraw, mark: str, accent: str) -> None: + round_line(draw, [(512, 326), (512, 508)], mark, 70) + round_circle(draw, (512, 208), 62, accent) + round_line(draw, [(252, 548), (512, 432), (772, 548), (512, 674)], mark, 66, closed=True) + round_line(draw, [(296, 656), (470, 732), (512, 752), (554, 732), (728, 656)], mark, 52) + round_circle(draw, (338, 548), 15, accent) + + +DRAWERS = { + "core": draw_core, + "soft_slab": draw_soft_slab, + "work_stack": draw_work_stack, + "clay_drop": draw_clay_drop, + "creation_base": draw_creation_base, + "app_token": draw_app_token, +} + + +def build_svg(variant: dict[str, str]) -> str: + bg = variant["bg"] + mark = variant["mark"] + accent = variant["accent"] + style = variant["style"] + shared = 'fill="none" stroke-linecap="round" stroke-linejoin="round"' + dot_fill = accent if style in {"clay_drop", "app_token"} else mark + left_dot_fill = accent if style == "app_token" else mark + + if style == "soft_slab": + base = f''' + + + ''' + stem = f'' + dot = f'' + elif style == "work_stack": + base = f''' + + + + ''' + stem = f'' + dot = f'' + elif style == "clay_drop": + base = f''' + + + + ''' + stem = f'' + dot = f'' + elif style == "creation_base": + base = f''' + + + + ''' + stem = f'' + dot = f'' + else: + stroke = 70 if style == "app_token" else 68 + base_width = 66 + layer_width = 52 + base = f''' + + + ''' + stem = f'' + dot = f'' + + return f''' + + + {base} + {stem} + {dot} + +''' + + +def render_variant(variant: dict[str, str]) -> Image.Image: + image = Image.new("RGB", (SIZE * SCALE, SIZE * SCALE), hex_to_rgb(variant["bg"])) + draw = ImageDraw.Draw(image) + DRAWERS[variant["style"]](draw, variant["mark"], variant["accent"]) + return image.resize((SIZE, SIZE), Image.Resampling.LANCZOS) + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def build_contact_sheet(previews: list[tuple[dict[str, str], Image.Image]]) -> Image.Image: + cell_size = 320 + label_height = 60 + gap = 28 + columns = 3 + rows = 2 + width = columns * cell_size + (columns + 1) * gap + height = rows * (cell_size + label_height) + (rows + 1) * gap + sheet = Image.new("RGB", (width, height), "#eee9df") + draw = ImageDraw.Draw(sheet) + font = load_font(23) + + for index, (variant, preview) in enumerate(previews): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_size + label_height + gap) + thumbnail = preview.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=10, + fill="#fffdf8", + ) + label = f"{index + 1:02d} {variant['title']}" + text_box = draw.textbbox((0, 0), label, font=font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=font) + return sheet + + +def main() -> None: + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + previews: list[tuple[dict[str, str], Image.Image]] = [] + + for variant in VARIANTS: + (OUTPUT_DIR / f"{variant['id']}.svg").write_text(build_svg(variant), encoding="utf-8") + preview = render_variant(variant) + preview.save(OUTPUT_DIR / f"{variant['id']}.png") + previews.append((variant, preview)) + + contact_sheet = build_contact_sheet(previews) + contact_sheet.save(CONTACT_SHEET_PATH, quality=95) + print( + { + "ok": True, + "output_dir": str(OUTPUT_DIR), + "files": [f"{variant['id']}.svg" for variant in VARIANTS] + + [f"{variant['id']}.png" for variant in VARIANTS] + + [CONTACT_SHEET_PATH.name], + } + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-anti-candy-contact-sheet.py b/scripts/generate-taonier-anti-candy-contact-sheet.py new file mode 100644 index 00000000..95c2b889 --- /dev/null +++ b/scripts/generate-taonier-anti-candy-contact-sheet.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-anti-candy-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-anti-candy-contact-sheet.png" + +ITEMS = [ + ("01 哑光陶泥印章", "taonier-anti-candy-01-matte-clay-stamp"), + ("02 窑印星核", "taonier-anti-candy-02-kiln-mark-core"), + ("03 负形星核", "taonier-anti-candy-03-cutout-negative-star"), + ("04 干陶颗粒", "taonier-anti-candy-04-dry-clay-grain"), + ("05 手压泥币", "taonier-anti-candy-05-hand-pressed-token"), + ("06 数字泥符", "taonier-anti-candy-06-digital-clay-glyph"), + ("07 精品扁平标", "taonier-anti-candy-07-premium-flat-mark"), + ("08 单色验证版", "taonier-anti-candy-08-monochrome-proof"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def main() -> None: + cell_size = 300 + label_height = 58 + gap = 24 + columns = 4 + rows = 2 + width = columns * cell_size + (columns + 1) * gap + height = rows * (cell_size + label_height) + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#ebe6dc") + draw = ImageDraw.Draw(sheet) + font = load_font(20) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_size + label_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fbfaf6", + ) + text_box = draw.textbbox((0, 0), label, font=font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-anti-candy-logo-concepts.mjs b/scripts/generate-taonier-anti-candy-logo-concepts.mjs new file mode 100644 index 00000000..d1dc1e40 --- /dev/null +++ b/scripts/generate-taonier-anti-candy-logo-concepts.mjs @@ -0,0 +1,420 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-anti-candy-concepts', +); +const timeoutMsDefault = 180000; + +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 basePrompt = [ + '请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。', + '产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。', + '这次必须反糖果化:第一眼必须像哑光陶泥、泥章、窑印、创作印记,绝对不能像糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力或食品包装。', + '核心隐喻:脑洞泥印。一块被轻轻捏平的不规则软方圆陶泥印章,中间有一个抽象星核凹印或镂空星形泥印,表达灵感被压印成作品。', + '风格:扁平矢量商标为主,低高光、低饱和、哑光粉陶质感;轮廓清楚,可后续矢量化,适合商标、App 图标、社区头像。', + '主色:灰米白、未经上釉的陶土白、陶土褐、深泥灰、少量暗金土黄;颜色必须干燥、克制、低甜度。不使用亮金、糖果黄、奶油黄、粉色、青色、蓝色、紫色、荧光色或彩虹色。', + '形态:保留不规则软方圆,但不要鼓胀、不要胶状、不要可食用的圆润光泽。边缘可以有少量粗糙泥料纹理、压痕、手捏不均匀。', + '数字感:只允许 2 到 3 个很小的深泥灰或暗土黄刻点,像生成节点或 UGC 扩散点;不要闪亮星星,不要糖珠。', + '构图:正方形画布,居中图形标,干净浅灰米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和中间星核凹印。', + '禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、旋涡环形旧稿、三色花瓣旧稿。', + '强禁止食品感:不要 glossy 高光、不要果冻质感、不要奶油夹心、不要糖霜、不要撒糖粒、不要饼干边、不要巧克力流心、不要金色膨胀星糖。', +]; + +const variants = [ + { + id: '01-matte-clay-stamp', + title: '哑光陶泥印章', + prompt: [ + ...basePrompt, + '本张重点:最克制的陶泥印章。灰米白软方圆主体,中间是压进去的暗陶土星核凹印,只有 2 个微小刻点。几乎无高光。', + ], + }, + { + id: '02-kiln-mark-core', + title: '窑印星核', + prompt: [ + ...basePrompt, + '本张重点:窑印感。中间星核像烧陶后的浅浮雕窑印,用深泥灰边缘和陶土褐阴影表现,不要任何金属或糖果光泽。', + ], + }, + { + id: '03-cutout-negative-star', + title: '负形星核', + prompt: [ + ...basePrompt, + '本张重点:负形。星核用干净的镂空负形或深泥灰内孔表达,主体是单块哑光陶泥,整体更像可注册商标图形。', + ], + }, + { + id: '04-dry-clay-grain', + title: '干陶颗粒', + prompt: [ + ...basePrompt, + '本张重点:干陶质感。加入非常细微的陶土颗粒和粉陶纹理,但保持扁平图标,不要照片写实,不要脏乱。', + ], + }, + { + id: '05-hand-pressed-token', + title: '手压泥币', + prompt: [ + ...basePrompt, + '本张重点:手压泥币。像一枚被手工压平的陶泥代币,边缘不完全对称,中间星核为凹刻符号,但不要出现手或工具。', + ], + }, + { + id: '06-digital-clay-glyph', + title: '数字泥符', + prompt: [ + ...basePrompt, + '本张重点:AI 与 UGC 暗示更强。用 3 个极小方形刻点围绕星核,像生成节点,但必须像刻在陶泥上的小孔。', + ], + }, + { + id: '07-premium-flat-mark', + title: '精品扁平标', + prompt: [ + ...basePrompt, + '本张重点:更互联网精品。减少纹理,强化几何平衡和负形,灰米白主体、深泥灰星核、陶土褐小刻痕,适合 App 图标。', + ], + }, + { + id: '08-monochrome-proof', + title: '单色验证版', + prompt: [ + ...basePrompt, + '本张重点:黑白商标验证。尽量用单色深浅关系表达软方圆和星核凹印,减少装饰,确保黑白化后轮廓仍成立。', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join(outputDir, `taonier-anti-candy-${variant.id}.${image.extension}`); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join(outputDir, 'taonier-logo-anti-candy-manifest.json'); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + creativeDirection: { + name: '陶泥儿反糖果化脑洞泥印图形标', + textPolicy: 'no Chinese, no English, no wordmark', + palette: '灰米白、陶土白、陶土褐、深泥灰、少量暗金土黄', + motif: '哑光软方圆陶泥印章 + 星核凹印/负形 + 极少量刻点', + antiCandyRules: + 'no glossy highlight, no cream filling, no jelly, no cookie, no chocolate, no candy star', + }, + variants: variants.map((variant) => { + const file = files.find((item) => path.basename(item).includes(variant.id)); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-braincore-contact-sheet.py b/scripts/generate-taonier-braincore-contact-sheet.py new file mode 100644 index 00000000..fddbc0d4 --- /dev/null +++ b/scripts/generate-taonier-braincore-contact-sheet.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-braincore-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-braincore-contact-sheet.png" + +ITEMS = [ + ("01 极简脑洞星核", "taonier-braincore-01-minimal-braincore"), + ("02 软方圆陶泥印记", "taonier-braincore-02-soft-square-clay-seal"), + ("03 暖棕星核嵌入", "taonier-braincore-03-warm-brown-embedded-core"), + ("04 轻微捏痕版本", "taonier-braincore-04-subtle-pinch-marks"), + ("05 精品几何比例", "taonier-braincore-05-premium-geometric-balance"), + ("06 柔软陶泥质感", "taonier-braincore-06-soft-clay-texture"), + ("07 App 图标优先", "taonier-braincore-07-app-icon-ready"), + ("08 商标黑白提炼", "taonier-braincore-08-trademark-monochrome-ready"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def main() -> None: + cell_size = 300 + label_height = 58 + gap = 24 + columns = 4 + rows = 2 + width = columns * cell_size + (columns + 1) * gap + height = rows * (cell_size + label_height) + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#eee9df") + draw = ImageDraw.Draw(sheet) + font = load_font(20) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_size + label_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-braincore-logo-concepts.mjs b/scripts/generate-taonier-braincore-logo-concepts.mjs new file mode 100644 index 00000000..8e6b7d0b --- /dev/null +++ b/scripts/generate-taonier-braincore-logo-concepts.mjs @@ -0,0 +1,415 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-braincore-concepts', +); +const timeoutMsDefault = 180000; + +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 basePrompt = [ + '请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。', + '产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。', + '核心隐喻:脑洞星核。一团奶白色、暖棕色调的不规则软方圆陶泥包裹一枚暖金色创意星核,表达灵感被捏造成型。', + '风格:扁平矢量商标为主,带非常轻微的软陶质感;结构简洁、边缘柔和、轮廓清晰,适合商标、App 图标、社区头像。', + '主色:奶白、米白、暖棕、陶土棕、少量暖金;整体温暖高级,不使用粉色、青色、蓝色、紫色、荧光色或彩虹色。', + '数字感:只允许在星核周围出现 2 到 4 个极小暖金星点或像素点,暗示 AI 生成、UGC 传播和轻游戏趣味。', + '构图:正方形画布,居中图形标,干净浅米白背景,留足安全边距;缩小到 64px 时仍能看清软方圆轮廓和星核。', + '禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、儿童黏土玩具感、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标、拟物甜品感。', + '必须保持全新构图,不参考任何既有陶泥儿 logo 方案,不做旋涡环形旧稿,不做三色花瓣旧稿。', +]; + +const variants = [ + { + id: '01-minimal-braincore', + title: '极简脑洞星核', + prompt: [ + ...basePrompt, + '本张重点:极简。只保留一个奶白不规则软方圆陶泥主体、一个居中的暖金四角星核、2 个极小暖金星点。不要额外装饰。', + ], + }, + { + id: '02-soft-square-clay-seal', + title: '软方圆陶泥印记', + prompt: [ + ...basePrompt, + '本张重点:陶泥印记。主体像被轻轻按压成型的软方圆印章,边缘有自然手捏起伏,但不能像儿童玩具。星核略微偏心。', + ], + }, + { + id: '03-warm-brown-embedded-core', + title: '暖棕星核嵌入', + prompt: [ + ...basePrompt, + '本张重点:嵌入感。用暖棕内凹形或陶土棕阴影承托暖金星核,像灵感被嵌进陶泥里,仍保持扁平商标质感。', + ], + }, + { + id: '04-subtle-pinch-marks', + title: '轻微捏痕版本', + prompt: [ + ...basePrompt, + '本张重点:捏痕。在陶泥主体边缘加入 2 到 3 个极轻微暖棕捏痕或凹口,表现可塑性;捏痕必须抽象、克制、可矢量化。', + ], + }, + { + id: '05-premium-geometric-balance', + title: '精品几何比例', + prompt: [ + ...basePrompt, + '本张重点:精品比例。整体更接近高级互联网 App 图标,几何平衡、负形干净、软方圆轮廓稳定,陶泥质感只保留一点点。', + ], + }, + { + id: '06-soft-clay-texture', + title: '柔软陶泥质感', + prompt: [ + ...basePrompt, + '本张重点:柔软质感。在不破坏扁平矢量感的前提下,加入细腻奶油陶泥的微妙高光和暖棕渐层,不能变成 3D 玩具。', + ], + }, + { + id: '07-app-icon-ready', + title: 'App 图标优先', + prompt: [ + ...basePrompt, + '本张重点:App 图标。图形占画面约 72%,轮廓饱满有记忆点,星核清晰醒目,适合放入圆角方形 App icon。', + ], + }, + { + id: '08-trademark-monochrome-ready', + title: '商标黑白提炼', + prompt: [ + ...basePrompt, + '本张重点:商标注册。优先保证黑白化后仍清楚,减少渐变和细节,用奶白主体、暖棕负形和暖金星核形成强轮廓。', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join(outputDir, `taonier-braincore-${variant.id}.${image.extension}`); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join(outputDir, 'taonier-logo-braincore-manifest.json'); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + creativeDirection: { + name: '陶泥儿脑洞星核图形标', + textPolicy: 'no Chinese, no English, no wordmark', + palette: '奶白、米白、暖棕、陶土棕、少量暖金', + motif: '不规则软方圆陶泥团 + 脑洞星核 + 极少量星点', + }, + variants: variants.map((variant) => ({ + id: variant.id, + title: variant.title, + file: files.find((file) => path.basename(file).includes(variant.id)) + ? path.basename(files.find((file) => path.basename(file).includes(variant.id))) + : null, + prompt: variant.prompt.join('\n'), + })), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-capybara-jar-ref01-logo-refine-concepts.mjs b/scripts/generate-taonier-capybara-jar-ref01-logo-refine-concepts.mjs new file mode 100644 index 00000000..b17dd4bb --- /dev/null +++ b/scripts/generate-taonier-capybara-jar-ref01-logo-refine-concepts.mjs @@ -0,0 +1,493 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-capybara-jar-ref01-logo-refine-concepts', +); +const referenceImagePath = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-peeking-head-jar-new-animals-concepts', + 'taonier-peeking-head-jar-new-animals-01-capybara.png', +); +const timeoutMsDefault = 180000; + +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 logoBrief = { + brand: '陶泥儿', + source: + '基于 peeking-head-jar-new-animals 批次 01 水豚头参考图继续收敛', + logoType: 'symbol/icon-only mascot mark, no wordmark', + product: + 'AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品', + keep: [ + '陶罐容器为主形体', + '水豚式半圆脑袋只露到眼睛位置', + '两只纯黑点眼,无高光', + '小圆耳朵与平静亲和感', + '中心构图与 32px 可读性', + ], + explore: [ + '不同罐子颜色', + '不同动物头色彩浓度', + '更扁平、更抽象、更商标化', + '更强黑白轮廓', + '减少插画感、渐变感和材质细节', + ], + avoid: [ + '中文或英文字', + '鼻子、嘴巴、腮红、表情高光', + '罐子表情', + '星星、闪光、手、陶艺工具', + '甜点、面包、巧克力、糖果、布丁、餐具感', + '完整动物身体、爪子、复杂场景', + '贴纸感、儿童玩具感、写实陶瓷质感', + ], +}; + +const basePrompt = [ + 'Use the uploaded reference image as the composition and pose reference. Keep the same core idea: a ceramic jar container with a capybara-like small animal peeking out only to eye level.', + 'Create a refined icon-only brand logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', + 'Preserve the reference structure: the jar is the main brand shape; the animal shows only small rounded ears, the upper half-dome head, and two matte black dot eyes above the jar rim.', + 'Do not show nose, mouth, cheek blush, teeth, paws, body, lower face, or a full animal head. The eyes must be pure black dots with no highlights, no white sparkle, and no glossy pupils.', + 'The jar itself must have no face, no expression, no eyes, no smile, and no decorative emoji marks.', + 'Make the result more like a trademark and logo than an illustration: simplified silhouette, clean vector curves, broad flat color fields, fewer gradients, fewer soft shadows, no realistic ceramic rendering.', + 'Keep a warm ceramic-container feeling through color and silhouette only. The jar should not look like a bowl of food, cup, dessert package, pudding cup, bread, bun, chocolate, candy, jelly, or pastry.', + 'Style target: premium friendly mascot-symbol logo, all-age and women-friendly, calm but memorable, suitable for app icon, social avatar, and trademark review.', + 'Composition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.', + 'Avoid star, spark, halo, magic wand, hand, pottery tool, UI, border, sticker outline, background scene, and watermark.', +]; + +const variants = [ + { + id: '01-flat-terracotta', + title: '扁平陶橙', + prompt: [ + ...basePrompt, + 'Variant focus: the most direct logo refinement. Use flat terracotta jar, warm caramel capybara head, minimal rim shadow, almost no gradients. Make the jar silhouette slightly more iconic and compact.', + ], + }, + { + id: '02-cream-cocoa', + title: '奶白可可', + prompt: [ + ...basePrompt, + 'Variant focus: cream ceramic jar with a cocoa-brown capybara head. Keep the palette soft but not edible; use graphic flat fills and a crisp rim shape to avoid dessert feeling.', + ], + }, + { + id: '03-sage-clay', + title: '鼠尾草陶', + prompt: [ + ...basePrompt, + 'Variant focus: muted sage green ceramic jar paired with a warm ochre capybara head. More mature and boutique. Keep the silhouette simple and logo-like, with only two or three main color regions.', + ], + }, + { + id: '04-outline-emblem', + title: '线面徽记', + prompt: [ + ...basePrompt, + 'Variant focus: bolder trademark mark with clean outline plus flat fills. Use a dark warm-brown contour line around the jar and animal, but keep it soft and modern, not sticker-like.', + ], + }, + { + id: '05-abstract-geometric', + title: '抽象几何', + prompt: [ + ...basePrompt, + 'Variant focus: higher abstraction. Reduce the capybara head to a clean half-dome with two round ears and two black dots; reduce the jar to a distinct pot silhouette with a single rim band. Very vector-ready.', + ], + }, + { + id: '06-monochrome-first', + title: '黑白优先', + prompt: [ + ...basePrompt, + 'Variant focus: black-and-white survival first. Design with strong positive and negative shapes; color is secondary. Use warm clay and dark umber, but the mark must remain clear if converted to pure black and white.', + ], + }, + { + id: '07-soft-gradient-logo', + title: '轻渐变商标', + prompt: [ + ...basePrompt, + 'Variant focus: allow only a very subtle premium gradient on broad shapes, like a polished app logo. Keep it much flatter than the reference and remove painterly shadows or texture.', + ], + }, + { + id: '08-bold-avatar', + title: '头像强识别', + prompt: [ + ...basePrompt, + 'Variant focus: compact social-avatar readability. Make the jar a fuller rounded vessel and enlarge the peeking capybara head slightly, while preserving the hidden half-head rhythm and black dot eyes.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesEditUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/edits` + : `${baseUrl}/v1/images/edits`; +} + +function buildDryRunFields(variant) { + return { + model: 'gpt-image-2', + prompt: variant.prompt.join('\n'), + n: '1', + size: '1024x1024', + image: referenceImagePath, + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +function createEditFormData(variant) { + const form = new FormData(); + const imageBytes = readFileSync(referenceImagePath); + form.append('model', 'gpt-image-2'); + form.append('prompt', variant.prompt.join('\n')); + form.append('n', '1'); + form.append('size', '1024x1024'); + form.append( + 'image', + new Blob([imageBytes], { type: 'image/png' }), + path.basename(referenceImagePath), + ); + return form; +} + +async function generateOne(env, variant) { + const payload = await fetchJson( + buildVectorEngineImagesEditUrl(env.baseUrl), + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.apiKey}`, + Accept: 'application/json', + }, + body: createEditFormData(variant), + }, + env.timeoutMs, + ); + + const urls = extractImageUrls(payload); + const b64Images = extractBase64Images(payload); + let image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join( + outputDir, + `taonier-capybara-jar-ref01-logo-refine-${variant.id}.${image.extension}`, + ); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join( + outputDir, + 'taonier-logo-capybara-jar-ref01-logo-refine-manifest.json', + ); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2', + endpoint: '/v1/images/edits', + size: '1024x1024', + referenceImage: path.relative(repoRoot, referenceImagePath), + generatedAt: new Date().toISOString(), + logoSkillSummary: { + requiredReview: + 'visual inspection, 32px readability, black-white viability', + outputStatus: 'AI concept only; final logo needs vector cleanup', + }, + brief: logoBrief, + variants: variants.map((variant) => { + const file = files.find((item) => + path.basename(item).includes(variant.id), + ); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + referenceImagePath, + count: selectedVariants.length, + brief: logoBrief, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + fields: buildDryRunFields(variant), + })), + }, + null, + 2, + ), + ); + process.exit(0); +} + +if (!existsSync(referenceImagePath)) { + console.error( + JSON.stringify({ + ok: false, + error: 'Reference image does not exist', + referenceImagePath, + }), + ); + process.exit(1); +} + +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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-capybara-jar-ref01-logo-refine-contact-sheet.py b/scripts/generate-taonier-capybara-jar-ref01-logo-refine-contact-sheet.py new file mode 100644 index 00000000..850d3936 --- /dev/null +++ b/scripts/generate-taonier-capybara-jar-ref01-logo-refine-contact-sheet.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-capybara-jar-ref01-logo-refine-concepts" +) +CONTACT_SHEET_PATH = ( + OUTPUT_DIR / "taonier-logo-capybara-jar-ref01-logo-refine-contact-sheet.png" +) +REFERENCE_IMAGE = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-peeking-head-jar-new-animals-concepts" + / "taonier-peeking-head-jar-new-animals-01-capybara.png" +) + +ITEMS = [ + ("REF 原01", REFERENCE_IMAGE), + ("01 扁平陶橙", "taonier-capybara-jar-ref01-logo-refine-01-flat-terracotta"), + ("02 奶白可可", "taonier-capybara-jar-ref01-logo-refine-02-cream-cocoa"), + ("03 鼠尾草陶", "taonier-capybara-jar-ref01-logo-refine-03-sage-clay"), + ("04 线面徽记", "taonier-capybara-jar-ref01-logo-refine-04-outline-emblem"), + ("05 抽象几何", "taonier-capybara-jar-ref01-logo-refine-05-abstract-geometric"), + ("06 黑白优先", "taonier-capybara-jar-ref01-logo-refine-06-monochrome-first"), + ("07 轻渐变商标", "taonier-capybara-jar-ref01-logo-refine-07-soft-gradient-logo"), + ("08 头像强识别", "taonier-capybara-jar-ref01-logo-refine-08-bold-avatar"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem_or_path: str | Path) -> Path | None: + if isinstance(stem_or_path, Path): + return stem_or_path if stem_or_path.exists() else None + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem_or_path}.{extension}" + if candidate.exists(): + return candidate + return None + + +def normalize_square(image_path: Path) -> Image.Image: + image = Image.open(image_path).convert("RGB") + if image.size == (1024, 1024): + return image + + if image.width == image.height: + normalized = image.resize((1024, 1024), Image.Resampling.LANCZOS) + else: + normalized = ImageOps.contain(image, (1024, 1024), Image.Resampling.LANCZOS) + canvas = Image.new("RGB", (1024, 1024), "#fffdf8") + x = (1024 - normalized.width) // 2 + y = (1024 - normalized.height) // 2 + canvas.paste(normalized, (x, y)) + normalized = canvas + + if image_path.is_relative_to(OUTPUT_DIR): + normalized.save(image_path, quality=95) + return normalized + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 268 + label_height = 54 + test_height = 44 + gap = 22 + columns = 3 + rows = 3 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(18) + test_font = load_font(13) + + for index, (label, stem_or_path) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem_or_path) + if image_path is None: + continue + + source = normalize_square(image_path) + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 52, test_y + 6)) + sheet.paste(mono, (x + 104, test_y + 6)) + draw.text((x + 150, test_y + 13), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-clay-mascot-contact-sheet.py b/scripts/generate-taonier-clay-mascot-contact-sheet.py new file mode 100644 index 00000000..392e67c3 --- /dev/null +++ b/scripts/generate-taonier-clay-mascot-contact-sheet.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-clay-mascot-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-clay-mascot-contact-sheet.png" + +ITEMS = [ + ("01 陶泥小人", "taonier-clay-mascot-little-maker.png"), + ("02 陶泥手办", "taonier-clay-mascot-figurine-token.png"), + ("03 软陶团子", "taonier-clay-mascot-soft-doll.png"), + ("04 造物泥偶", "taonier-clay-mascot-creator-totem.png"), + ("05 陶泥面偶", "taonier-clay-mascot-idol-mask.png"), + ("06 口袋泥人", "taonier-clay-mascot-pocket-figure.png"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def main() -> None: + cell_size = 330 + label_height = 58 + gap = 28 + columns = 3 + rows = 2 + width = columns * cell_size + (columns + 1) * gap + height = rows * (cell_size + label_height) + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#eee9df") + draw = ImageDraw.Draw(sheet) + font = load_font(24) + + for index, (label, filename) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_size + label_height + gap) + + source = Image.open(OUTPUT_DIR / filename).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=10, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=font) + + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-clay-mascot-logo-concepts.mjs b/scripts/generate-taonier-clay-mascot-logo-concepts.mjs new file mode 100644 index 00000000..97476b66 --- /dev/null +++ b/scripts/generate-taonier-clay-mascot-logo-concepts.mjs @@ -0,0 +1,330 @@ +import { Buffer } from 'node:buffer'; +import { + existsSync, + mkdirSync, + readFileSync, + readdirSync, + writeFileSync, +} from 'node:fs'; +import path from 'node:path'; + +const repoRoot = process.cwd(); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-clay-mascot-concepts', +); +const defaultTimeoutMs = 420000; + +const concepts = [ + { + id: 'taonier-clay-mascot-little-maker', + title: '陶泥小人', + prompt: + '为中文产品“陶泥儿”重新设计一个无文字 Logo 图标。停止此前软泥合拍、旋涡、锚点底座方向,以“陶泥人 / 陶泥手办 / 抽象角色吉祥物”为主线。图形主体是一个被手捏出来的极简陶泥小人:圆头、短身体、短短小手,轮廓像柔软陶泥,但必须压缩成成熟 App 主标,不是完整角色插画。角色胸口或掌心有一颗极简小星点,表达 AI 把脑洞捏成作品。风格:logo-friendly mascot mark, simple silhouette, flat vector feel, friendly, memorable, premium cute, clear at small size。配色使用奶油白、暖陶土、深墨底,可少量暖黄色星点。禁止文字、字母、水印、复杂五官、真实人脸、儿童黏土课、3D 厚重拟物、聊天气泡、播放按钮、手办包装、背景场景。', + }, + { + id: 'taonier-clay-mascot-figurine-token', + title: '陶泥手办', + prompt: + '为“陶泥儿”设计无文字 Logo 图标,方向是陶泥手办 / 抽象吉祥物。主体像一枚小型软陶手办的正面主标:圆润头部、简化身体、两只短臂自然张开,底部像一个小底座但不要做雕塑台。它要有手办收藏感和精品感,但仍是极简品牌图标,不是 3D 玩具照片。角色表情只能用非常简洁的点或负形,不要复杂可爱脸。风格:modern mascot logo, flat vector, bold simple shapes, warm, collectible, app icon ready。配色:象牙白主体、深墨背景、暖陶土阴影或小点缀。禁止文字、字母、真实玩具、塑料质感、过多高光、复杂衣服、帽子、聊天气泡、播放键。', + }, + { + id: 'taonier-clay-mascot-soft-doll', + title: '软陶团子', + prompt: + '为“陶泥儿”设计无文字 Logo 图标,方向是抽象陶泥角色。主体是一只圆滚滚的软陶团子小人,像一团泥被轻轻捏出头、身体和两只小手,整体剪影非常简单,能一眼记住。它需要有 Q 感和亲和力,但不要像表情包或儿童玩具。中央保留一枚小作品星核或泥点,表达创作生成。风格:minimal clay mascot logo, flat vector style, rounded, cute but mature, clean, scalable。配色:奶白 / 米白主体,暖陶土小阴影,深色或奶油色纯背景,最多 3 色。禁止中文、英文、水印、复杂五官、头发、衣服、真实手指、3D、毛绒、聊天气泡、笑脸贴纸。', + }, + { + id: 'taonier-clay-mascot-creator-totem', + title: '造物泥偶', + prompt: + '为“陶泥儿”设计无文字 Logo 图标,方向是陶泥人和品牌图腾之间的抽象角色。主体不是普通人物,而是一个被捏出来的“造物泥偶”:头部圆润,身体像软陶印章,双臂像两处短短捏痕,中间有小星或小孔代表作品核。图形要比吉祥物更符号化,更适合长期主 Logo。风格:abstract mascot brand mark, simple, iconic, flat vector feel, premium, friendly, clear at 32px。配色:深墨背景、奶油白主体、少量暖黄或陶土点缀。禁止真实人、复杂脸、动物、怪物、儿童玩具、厚阴影、3D、文字、字母、水印、UI 元素。', + }, + { + id: 'taonier-clay-mascot-idol-mask', + title: '陶泥面偶', + prompt: + '为“陶泥儿”设计无文字 Logo 图标,方向是抽象角色 / 吉祥物主标。主体是一枚圆润陶泥面偶:像小陶泥人的头脸和上半身融合成一个单一徽标,五官极简,只允许两个小点或一条负形捏痕,整体更像品牌符号而不是头像。要有陶泥手工、AI 创意、轻休闲平台的亲和感。风格:flat vector mascot icon, simple face mark, warm, modern, memorable, not childish。配色:暖奶白、陶土橙、深墨,少量金色作品点。禁止文字、字母、水印、复杂表情、emoji、聊天头像、真实陶艺照片、3D、背景场景、动物形象。', + }, + { + id: 'taonier-clay-mascot-pocket-figure', + title: '口袋泥人', + prompt: + '为“陶泥儿”设计无文字 Logo 图标,方向是小陶泥人 / 口袋手办 / 抽象吉祥物。主体是一个能放进 App icon 的口袋泥人:小小头、软软身体、两侧短手,整体像被捏出的一枚符号,底部可轻微压扁形成稳定站姿。它应表达“人人都能把脑洞捏成作品”,亲和但不幼稚,适合品牌主标。风格:mascot logo, flat vector, bold silhouette, minimal, cute, premium, high contrast。配色:黑底或深墨底,米白陶泥主体,暖黄色小泥点。禁止文字、字母、水印、复杂五官、衣服配饰、真实手办摄影、玩偶包装、聊天气泡、播放三角、3D 厚阴影。', + }, +]; + +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); + } +} + +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', + quality: String(args.get('--quality') || 'low'), + 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(outputDir, { recursive: true }); + const extension = inferExtensionFromBytes(bytes); + const outputPath = path.join(outputDir, `${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', + outputDir, + count: selected.length, + requests: selected.map((concept) => ({ + id: concept.id, + title: concept.title, + body: { + model: 'gpt-image-2-all', + quality: String(args.get('--quality') || 'low'), + 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, + verifiedFiles: readdirSync(outputDir).sort(), + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-closed-geo-contact-sheet.py b/scripts/generate-taonier-closed-geo-contact-sheet.py new file mode 100644 index 00000000..b84958fa --- /dev/null +++ b/scripts/generate-taonier-closed-geo-contact-sheet.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-closed-geo-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-closed-geo-contact-sheet.png" + +ITEMS = [ + ("01 有机闭合徽形", "taonier-closed-geo-01-organic-closed-badge"), + ("02 柔曲陶盾", "taonier-closed-geo-02-smooth-clay-shield"), + ("03 非对称陶符", "taonier-closed-geo-03-asymmetric-pebble-glyph"), + ("04 嵌曲陶牌", "taonier-closed-geo-04-inlaid-curve-plate"), + ("05 扁平矢量符", "taonier-closed-geo-05-flat-vector-symbol"), + ("06 亲和实体形", "taonier-closed-geo-06-friendly-solid-form"), + ("07 数字陶泥面", "taonier-closed-geo-07-digital-clay-panel"), + ("08 商标轮廓款", "taonier-closed-geo-08-trademark-ready-contour"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def main() -> None: + cell_size = 300 + label_height = 58 + gap = 24 + columns = 4 + rows = 2 + width = columns * cell_size + (columns + 1) * gap + height = rows * (cell_size + label_height) + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#ede8de") + draw = ImageDraw.Draw(sheet) + font = load_font(20) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_size + label_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fbfaf6", + ) + text_box = draw.textbbox((0, 0), label, font=font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-closed-geo-logo-concepts.mjs b/scripts/generate-taonier-closed-geo-logo-concepts.mjs new file mode 100644 index 00000000..b4e1f55f --- /dev/null +++ b/scripts/generate-taonier-closed-geo-logo-concepts.mjs @@ -0,0 +1,419 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-closed-geo-concepts', +); +const timeoutMsDefault = 180000; + +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 basePrompt = [ + '请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。', + '产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。', + '这次的核心要求:底盘必须是由流畅曲线组成的「闭合不规则几何体图案」。它要像一个完整的品牌徽形轮廓,而不是自由飘带、旋涡、S 形、G 形、开放环或散开的泥条。', + '外轮廓:闭合、连续、平滑、非方形、非圆形、非椭圆、非圆角方块。可以有 5 到 7 个柔和转折点,像自然捏出来的抽象几何石/软陶徽形,但必须干净、可矢量化。', + '内部结构:可以用 1 到 2 条平滑曲线切分色块或形成负形,但内部结构必须服务整体闭合底盘,不能出现中心星星、中心洞、脸、眼睛、嘴巴或角色感。', + '识别方向:通过闭合外轮廓和内部曲线分区形成记忆点,不靠星形、不靠文字、不靠食物质感。远看要像互联网产品 logo。', + '材质与风格:现代扁平矢量商标,极轻微陶泥温度;低高光、低拟物、干净边缘。不要 3D 面包感,不要巧克力、面团、糕点、糖果、饼干或烘焙质感。', + '配色:暖陶白、浅陶土、陶土橙、暖棕为主体;允许少量低饱和孔雀青、靛蓝灰或深泥灰作为内部曲线色块,占比 6% 到 15%。整体要温暖、有吸引力,但不甜、不像食品。', + '构图:正方形画布,居中图形标,图形占画面约 68% 到 76%,干净浅色背景,留足安全边距。缩小到 64px 时仍能看出闭合外轮廓和内部曲线分区。', + '强禁止:自由飘带、开口环、旋涡、S 形、G 形、云朵、面包、巧克力面包、可颂、甜甜圈、糖果、饼干、糕点、奶油、夹心、亮油高光、烘焙纹理、食物摄影感。', + '强禁止:整体方形底、圆角方块、中心星星、任何星形、闪光、徽章文字、印章文字、脸、表情、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。', +]; + +const variants = [ + { + id: '01-organic-closed-badge', + title: '有机闭合徽形', + prompt: [ + ...basePrompt, + '本张重点:最基础的闭合不规则几何体。暖陶白主体,陶土橙内部曲线切分,外轮廓像柔和抽象鹅卵石但不是圆形。', + ], + }, + { + id: '02-smooth-clay-shield', + title: '柔曲陶盾', + prompt: [ + ...basePrompt, + '本张重点:更稳定的品牌底盘。外轮廓略像柔化盾形或种子形,但没有尖角;内部一条孔雀青曲线增加识别度。', + ], + }, + { + id: '03-asymmetric-pebble-glyph', + title: '非对称陶符', + prompt: [ + ...basePrompt, + '本张重点:非对称但平衡。闭合外轮廓左右不一样,有 6 个柔和转折点,内部用深泥灰负形曲线形成品牌记忆。', + ], + }, + { + id: '04-inlaid-curve-plate', + title: '嵌曲陶牌', + prompt: [ + ...basePrompt, + '本张重点:内部嵌色曲线。像一块闭合陶牌上嵌入一条平滑色带,色带不能像馅料、巧克力或奶油夹心。', + ], + }, + { + id: '05-flat-vector-symbol', + title: '扁平矢量符', + prompt: [ + ...basePrompt, + '本张重点:最扁平、最商标化。减少材质,只用 2 到 3 个大色块形成闭合不规则几何符号,线条极简。', + ], + }, + { + id: '06-friendly-solid-form', + title: '亲和实体形', + prompt: [ + ...basePrompt, + '本张重点:亲和力。闭合底盘像一个柔软、温和、完整的小世界,但不是角色、不是脸、不是食物。', + ], + }, + { + id: '07-digital-clay-panel', + title: '数字陶泥面', + prompt: [ + ...basePrompt, + '本张重点:AI/UGC 暗示。闭合底盘内有 2 到 3 个很小的几何刻点或短曲线节点,但不能像电路板,也不能像撒糖。', + ], + }, + { + id: '08-trademark-ready-contour', + title: '商标轮廓款', + prompt: [ + ...basePrompt, + '本张重点:可注册轮廓。优先保证黑白化后的闭合外轮廓和内部曲线仍有辨识度,避免渐变和复杂纹理。', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join(outputDir, `taonier-closed-geo-${variant.id}.${image.extension}`); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join(outputDir, 'taonier-logo-closed-geo-manifest.json'); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + creativeDirection: { + name: '陶泥儿闭合不规则几何底盘图形标', + textPolicy: 'no Chinese, no English, no wordmark', + correction: + 'closed irregular smooth geometry, not free ribbon, not food, not square base', + motif: '闭合曲线几何底盘 + 内部曲线分区 + 轻陶泥温度', + }, + variants: variants.map((variant) => { + const file = files.find((item) => path.basename(item).includes(variant.id)); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-distinctive-contact-sheet.py b/scripts/generate-taonier-distinctive-contact-sheet.py new file mode 100644 index 00000000..fab620df --- /dev/null +++ b/scripts/generate-taonier-distinctive-contact-sheet.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-distinctive-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-distinctive-contact-sheet.png" + +ITEMS = [ + ("01 孔雀青星核", "taonier-distinctive-01-teal-core-pop"), + ("02 靛蓝切口", "taonier-distinctive-02-indigo-cut-mark"), + ("03 朱砂陶火", "taonier-distinctive-03-cinnabar-clay-spark"), + ("04 强轮廓泥符", "taonier-distinctive-04-bold-outline-token"), + ("05 像素创作种", "taonier-distinctive-05-clay-pixel-seed"), + ("06 动态软方圆", "taonier-distinctive-06-dynamic-squircle"), + ("07 应用图标款", "taonier-distinctive-07-app-store-icon"), + ("08 商标扁平符", "taonier-distinctive-08-trademark-flat-glyph"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def main() -> None: + cell_size = 300 + label_height = 58 + gap = 24 + columns = 4 + rows = 2 + width = columns * cell_size + (columns + 1) * gap + height = rows * (cell_size + label_height) + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#ede8de") + draw = ImageDraw.Draw(sheet) + font = load_font(20) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_size + label_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fbfaf6", + ) + text_box = draw.textbbox((0, 0), label, font=font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-distinctive-logo-concepts.mjs b/scripts/generate-taonier-distinctive-logo-concepts.mjs new file mode 100644 index 00000000..4f2e66ab --- /dev/null +++ b/scripts/generate-taonier-distinctive-logo-concepts.mjs @@ -0,0 +1,422 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-distinctive-concepts', +); +const timeoutMsDefault = 180000; + +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 basePrompt = [ + '请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。', + '产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。', + '这次目标是高辨识度和传播吸引力:第一眼要像一个有记忆点的互联网产品 logo,而不是普通泥块、砖块、陶片、印章、饼干或糖果。', + '核心隐喻:可塑星核。一个独特的「软方圆陶泥外轮廓 + 中心可塑星核」符号,星核像脑洞被捏出的一瞬间,造型要有专属轮廓,不能是普通五角星或普通四角闪光。', + '风格:现代扁平矢量商标,带轻微哑光陶泥质感;边缘干净、对比清楚、轮廓强,适合 App 图标、社区头像、启动页和商标注册前期筛选。', + '材质:要能看出陶泥温度,但不要过度粗糙、不要砖、不要土块、不要考古泥片。质感只做轻量表面颗粒和柔和压痕。', + '配色:以暖陶白或浅陶土色为主体,加入一个高识别点缀色。允许使用低饱和孔雀青、靛蓝灰、朱砂橙、暗金土黄中的一种;点缀色只占 8% 到 18%。整体要更醒目、更年轻,但不要糖果色。', + '图案:中心星核必须是品牌记忆点,可用负形、嵌色、切口、泥痕、像素刻点形成独特轮廓;周围最多 2 到 3 个小刻点暗示 AI/UGC 扩散。', + '构图:正方形画布,居中图形标,图形占画面约 70% 到 78%,干净浅色背景,留足安全边距。缩小到 64px 时仍然能认出外轮廓和中心符号。', + '禁止:脸、眼睛、嘴巴、表情、角色身体、吉祥物立绘、陶艺工具、手、笔刷、复杂场景、按钮、UI、边框、水印、文字、商标字标。', + '强禁止低辨识:不要普通圆泥饼、不要普通砖块、不要石头、不要考古印章、不要单纯凹星孔、不要灰扑扑单色泥片。', + '强禁止食品感:不要糖果、软糖、奶油、饼干、夹心甜点、月饼、巧克力、果冻高光、亮金膨胀星糖。', +]; + +const variants = [ + { + id: '01-teal-core-pop', + title: '孔雀青星核', + prompt: [ + ...basePrompt, + '本张重点:孔雀青识别点。暖陶白软方圆主体,中间是孔雀青负形可塑星核,少量陶土褐压痕,整体清爽年轻。', + ], + }, + { + id: '02-indigo-cut-mark', + title: '靛蓝切口', + prompt: [ + ...basePrompt, + '本张重点:靛蓝灰切口。用一条干净的靛蓝灰泥痕切出中心星核,让图形远看有强剪影,不能像旋涡旧稿。', + ], + }, + { + id: '03-cinnabar-clay-spark', + title: '朱砂陶火', + prompt: [ + ...basePrompt, + '本张重点:朱砂橙活力。中心星核或侧边小泥片使用低饱和朱砂橙,像创作火花,但整体保持陶泥材质和精品克制。', + ], + }, + { + id: '04-bold-outline-token', + title: '强轮廓泥符', + prompt: [ + ...basePrompt, + '本张重点:强轮廓。用深泥灰细描边或深色负形强化外轮廓和中心符号,确保黑白化后仍有高辨识度。', + ], + }, + { + id: '05-clay-pixel-seed', + title: '像素创作种', + prompt: [ + ...basePrompt, + '本张重点:AI/UGC 暗示。中心星核周围有 3 个小方形刻点,像生成像素从陶泥里浮现,但不要复杂电路线。', + ], + }, + { + id: '06-dynamic-squircle', + title: '动态软方圆', + prompt: [ + ...basePrompt, + '本张重点:动态轮廓。外形不是静态泥块,而像正在被捏动的软方圆,有一个明显但简洁的非对称记忆点。', + ], + }, + { + id: '07-app-store-icon', + title: '应用图标款', + prompt: [ + ...basePrompt, + '本张重点:App Store 图标。构图饱满、中心符号强、背景干净,视觉冲击比泥章更强,但不出现文字和脸。', + ], + }, + { + id: '08-trademark-flat-glyph', + title: '商标扁平符', + prompt: [ + ...basePrompt, + '本张重点:最终商标潜力。减少材质和阴影,以 2 到 3 个大色块形成独特符号,保留陶泥可塑感和中心可塑星核。', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join(outputDir, `taonier-distinctive-${variant.id}.${image.extension}`); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join(outputDir, 'taonier-logo-distinctive-manifest.json'); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + creativeDirection: { + name: '陶泥儿高辨识可塑星核图形标', + textPolicy: 'no Chinese, no English, no wordmark', + palette: + '暖陶白/浅陶土主体 + 8%-18% 孔雀青、靛蓝灰、朱砂橙或暗金土黄点缀', + motif: '强轮廓软方圆 + 独特可塑星核 + 少量 AI/UGC 刻点', + correction: + 'avoid candy, avoid brick, avoid plain mud stamp, increase brand recognition', + }, + variants: variants.map((variant) => { + const file = files.find((item) => path.basename(item).includes(variant.id)); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-flow-contact-sheet.py b/scripts/generate-taonier-flow-contact-sheet.py new file mode 100644 index 00000000..6e4b4215 --- /dev/null +++ b/scripts/generate-taonier-flow-contact-sheet.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-flow-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-flow-contact-sheet.png" + +ITEMS = [ + ("01 柔泥回环", "taonier-flow-01-soft-ribbon-loop"), + ("02 陶泥波结", "taonier-flow-02-clay-wave-knot"), + ("03 脑洞涟漪", "taonier-flow-03-imagination-ripple"), + ("04 亲和泥流", "taonier-flow-04-friendly-clay-comet"), + ("05 单笔团块", "taonier-flow-05-single-stroke-blob"), + ("06 双色软流", "taonier-flow-06-two-tone-soft-flow"), + ("07 开放泥环", "taonier-flow-07-open-clay-orbit"), + ("08 品牌曲线符", "taonier-flow-08-brand-flow-glyph"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def main() -> None: + cell_size = 300 + label_height = 58 + gap = 24 + columns = 4 + rows = 2 + width = columns * cell_size + (columns + 1) * gap + height = rows * (cell_size + label_height) + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#ede8de") + draw = ImageDraw.Draw(sheet) + font = load_font(20) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_size + label_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fbfaf6", + ) + text_box = draw.textbbox((0, 0), label, font=font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-flow-logo-concepts.mjs b/scripts/generate-taonier-flow-logo-concepts.mjs new file mode 100644 index 00000000..9de1dbf7 --- /dev/null +++ b/scripts/generate-taonier-flow-logo-concepts.mjs @@ -0,0 +1,418 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-flow-concepts', +); +const timeoutMsDefault = 180000; + +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 basePrompt = [ + '请生成一枚全新的「陶泥儿」产品商标图形标概念稿,但画面中绝对不要出现任何中文字、英文字母、数字、符号字样或品牌字标。', + '产品定位:精品 AI UGC 轻休闲小游戏创作与传播平台,用户可以像捏陶泥一样把脑洞、梗和灵感塑造成小游戏或趣味内容。', + '这次必须彻底放弃方形底和中心星星:不要整体方形、不要圆角方块、不要 App 图标底板、不要中间星形、不要五角星、不要四角星、不要闪光、不要中心挖孔星。', + '核心隐喻:连续的陶泥曲线。用一整块顺滑、亲和、可塑的陶泥大结构来表达「脑洞被揉开、灵感流动、内容成型」,图形要像一个完整的流动符号,而不是底板加中心图案。', + '主结构:由平滑曲线组成的大轮廓,可以像柔软泥流、脑洞涟漪、捏合的回环、温和的旋拧、单线团块或开放结。整体必须连贯、圆润、有亲和力,不能分裂成多个碎片。', + '风格:现代扁平矢量商标,轻微哑光陶泥质感;边缘干净、曲线饱满、负形明确,适合商标、App 图标、社区头像和启动页。', + '配色:暖陶白、浅陶土、陶土橙、暖棕为主,可加入少量低饱和孔雀青或深泥灰作为曲线内侧阴影或小连接点。颜色要温暖、有吸引力,但不要糖果色。', + '识别度:远看必须能记住一个独特的大曲线轮廓;不要靠中心图案识别。缩小到 64px 时仍能看出整体曲线姿态。', + '亲和力:整体像可以被揉捏的柔软小世界,有陪伴感和创作感,但不要做成脸、表情、角色或吉祥物。', + '禁止:方形底、圆角方块、中心星星、任何星形、闪光、徽章、印章、砖块、泥饼、石头、陶片、饼干、糖果、手、陶艺工具、笔刷、复杂场景、按钮、UI、边框、水印、文字。', +]; + +const variants = [ + { + id: '01-soft-ribbon-loop', + title: '柔泥回环', + prompt: [ + ...basePrompt, + '本张重点:一条宽厚柔软的陶泥带形成开放回环,像脑洞被揉开。轮廓连续,内部负形自然,不出现星形或方形底。', + ], + }, + { + id: '02-clay-wave-knot', + title: '陶泥波结', + prompt: [ + ...basePrompt, + '本张重点:像一个温和波浪结,由 2 条互相捏合的平滑曲线组成,但必须视觉上像一个整体符号,不要碎片化。', + ], + }, + { + id: '03-imagination-ripple', + title: '脑洞涟漪', + prompt: [ + ...basePrompt, + '本张重点:用一整块陶泥曲面形成涟漪状大轮廓,中间负形像柔和水滴或脑洞入口,不能像星星。', + ], + }, + { + id: '04-friendly-clay-comet', + title: '亲和泥流', + prompt: [ + ...basePrompt, + '本张重点:亲和泥流。整体像一个流动的陶泥小世界,前端圆润、尾部自然收束,有动势但不尖锐。', + ], + }, + { + id: '05-single-stroke-blob', + title: '单笔团块', + prompt: [ + ...basePrompt, + '本张重点:单笔成型。像用一笔连续曲线捏出的陶泥团,结构极简但有记忆点,适合后续矢量化。', + ], + }, + { + id: '06-two-tone-soft-flow', + title: '双色软流', + prompt: [ + ...basePrompt, + '本张重点:双色曲线。暖陶白主体配少量孔雀青或陶土橙内侧曲线,让图形更吸引人,但不能变成多片拼贴。', + ], + }, + { + id: '07-open-clay-orbit', + title: '开放泥环', + prompt: [ + ...basePrompt, + '本张重点:开放式泥环。不是闭合圆,也不是旋涡旧稿,而是一枚有缺口和呼吸感的平滑陶泥环形符号。', + ], + }, + { + id: '08-brand-flow-glyph', + title: '品牌曲线符', + prompt: [ + ...basePrompt, + '本张重点:最终品牌符号潜力。减少材质和细节,用 1 到 2 个大曲线色块形成独特、亲和、可注册的图形标。', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join(outputDir, `taonier-flow-${variant.id}.${image.extension}`); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join(outputDir, 'taonier-logo-flow-manifest.json'); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + creativeDirection: { + name: '陶泥儿连续曲线亲和图形标', + textPolicy: 'no Chinese, no English, no wordmark', + correction: + 'no square base, no center star, use one continuous friendly clay curve structure', + motif: '柔泥回环、脑洞涟漪、开放泥环、品牌曲线符', + }, + variants: variants.map((variant) => { + const file = files.find((item) => path.basename(item).includes(variant.id)); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-geometric-logo-concepts.py b/scripts/generate-taonier-geometric-logo-concepts.py new file mode 100644 index 00000000..bb8be688 --- /dev/null +++ b/scripts/generate-taonier-geometric-logo-concepts.py @@ -0,0 +1,300 @@ +from __future__ import annotations + +import math +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-geometric-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-geometric-contact-sheet.png" + +SIZE = 1024 +SCALE = 4 + +INK = "#151515" +CREAM = "#fff7e6" +GOLD = "#ffd25d" +CORAL = "#ff6a5f" +MINT = "#29c9ad" +BLUE = "#2f6bff" + +VARIANTS = [ + ("taonier-geometric-offset-core", "偏心泥孔", "offset_core"), + ("taonier-geometric-mold-chip", "模芯切片", "mold_chip"), + ("taonier-geometric-pinched-tile", "捏痕方标", "pinched_tile"), + ("taonier-geometric-dual-plate", "双片合模", "dual_plate"), + ("taonier-geometric-dot-gate", "泥点入口", "dot_gate"), + ("taonier-geometric-work-knot", "作品结点", "work_knot"), +] + + +def hex_to_rgb(value: str) -> tuple[int, int, int]: + value = value.removeprefix("#") + return tuple(int(value[index : index + 2], 16) for index in (0, 2, 4)) + + +def s(value: float) -> int: + return round(value * SCALE) + + +def rgba(value: str) -> tuple[int, int, int, int]: + red, green, blue = hex_to_rgb(value) + return red, green, blue, 255 + + +def regular_polygon(cx: float, cy: float, radius: float, sides: int, rotation: float) -> list[tuple[int, int]]: + points = [] + for index in range(sides): + angle = rotation + math.tau * index / sides + points.append((s(cx + math.cos(angle) * radius), s(cy + math.sin(angle) * radius))) + return points + + +def rounded_rectangle( + draw: ImageDraw.ImageDraw, + box: tuple[float, float, float, float], + radius: float, + fill: str, +) -> None: + draw.rounded_rectangle(tuple(s(value) for value in box), radius=s(radius), fill=hex_to_rgb(fill)) + + +def circle(draw: ImageDraw.ImageDraw, cx: float, cy: float, radius: float, fill: str) -> None: + draw.ellipse((s(cx - radius), s(cy - radius), s(cx + radius), s(cy + radius)), fill=hex_to_rgb(fill)) + + +def draw_offset_core(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#111111")) + rounded_rectangle(draw, (236, 236, 788, 788), 148, CREAM) + circle(draw, 610, 456, 116, "#111111") + circle(draw, 610, 456, 48, GOLD) + rounded_rectangle(draw, (268, 612, 536, 718), 53, "#111111") + rounded_rectangle(draw, (294, 638, 500, 690), 26, CREAM) + circle(draw, 352, 370, 34, "#111111") + circle(draw, 352, 370, 17, GOLD) + + +def draw_mold_chip(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#101418")) + draw.polygon( + [ + (s(278), s(230)), + (s(734), s(230)), + (s(828), s(330)), + (s(828), s(694)), + (s(706), s(794)), + (s(278), s(794)), + (s(196), s(708)), + (s(196), s(322)), + ], + fill=hex_to_rgb(CREAM), + ) + circle(draw, 512, 512, 144, "#101418") + circle(draw, 512, 512, 62, GOLD) + rounded_rectangle(draw, (224, 280, 518, 370), 45, CORAL) + rounded_rectangle(draw, (574, 654, 796, 736), 41, MINT) + + +def draw_pinched_tile(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#14100d")) + rounded_rectangle(draw, (232, 250, 792, 774), 170, CREAM) + circle(draw, 232, 512, 94, "#14100d") + circle(draw, 792, 512, 94, "#14100d") + draw.polygon(regular_polygon(512, 512, 104, 4, math.pi / 4), fill=hex_to_rgb("#14100d")) + circle(draw, 512, 512, 38, GOLD) + rounded_rectangle(draw, (420, 300, 604, 358), 29, CORAL) + rounded_rectangle(draw, (420, 666, 604, 724), 29, MINT) + + +def draw_dual_plate(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#111111")) + rounded_rectangle(draw, (214, 316, 790, 464), 74, CORAL) + rounded_rectangle(draw, (234, 560, 810, 708), 74, MINT) + draw.polygon(regular_polygon(512, 512, 138, 4, math.pi / 4), fill=hex_to_rgb(CREAM)) + draw.polygon(regular_polygon(512, 512, 76, 4, math.pi / 4), fill=hex_to_rgb("#111111")) + circle(draw, 512, 512, 32, GOLD) + circle(draw, 262, 390, 24, CREAM) + circle(draw, 762, 634, 24, CREAM) + + +def draw_dot_gate(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#101010")) + rounded_rectangle(draw, (276, 330, 748, 752), 132, CREAM) + rounded_rectangle(draw, (386, 440, 638, 752), 126, "#101010") + circle(draw, 512, 260, 62, GOLD) + rounded_rectangle(draw, (450, 308, 574, 504), 62, CREAM) + circle(draw, 512, 518, 40, GOLD) + rounded_rectangle(draw, (316, 754, 708, 812), 29, CREAM) + + +def draw_work_knot(draw: ImageDraw.ImageDraw) -> None: + draw.rectangle((0, 0, s(SIZE), s(SIZE)), fill=hex_to_rgb("#121212")) + circle(draw, 396, 402, 132, CREAM) + circle(draw, 628, 402, 132, CREAM) + circle(draw, 396, 622, 132, CREAM) + circle(draw, 628, 622, 132, CREAM) + rounded_rectangle(draw, (386, 386, 638, 638), 72, "#121212") + draw.polygon(regular_polygon(512, 512, 96, 4, math.pi / 4), fill=hex_to_rgb(GOLD)) + circle(draw, 396, 402, 48, CORAL) + circle(draw, 628, 622, 48, MINT) + circle(draw, 628, 402, 28, "#121212") + circle(draw, 396, 622, 28, "#121212") + + +DRAWERS = { + "offset_core": draw_offset_core, + "mold_chip": draw_mold_chip, + "pinched_tile": draw_pinched_tile, + "dual_plate": draw_dual_plate, + "dot_gate": draw_dot_gate, + "work_knot": draw_work_knot, +} + + +def render_variant(style: str) -> Image.Image: + image = Image.new("RGB", (SIZE * SCALE, SIZE * SCALE), hex_to_rgb("#111111")) + draw = ImageDraw.Draw(image) + DRAWERS[style](draw) + return image.resize((SIZE, SIZE), Image.Resampling.LANCZOS) + + +def build_svg(style: str) -> str: + # PNG 是当前评审主物料,SVG 保留为后续设计师重绘的结构草图。 + if style == "offset_core": + body = f''' + + + + + + + + ''' + elif style == "mold_chip": + body = f''' + + + + + + ''' + elif style == "pinched_tile": + body = f''' + + + + + + + + ''' + elif style == "dual_plate": + body = f''' + + + + + + + + ''' + elif style == "dot_gate": + body = f''' + + + + + + + ''' + else: + body = f''' + + + + + + + + + + + ''' + return f''' + +{body} + +''' + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def build_contact_sheet(previews: list[tuple[str, str, Image.Image]]) -> Image.Image: + cell_size = 320 + label_height = 60 + gap = 28 + columns = 3 + rows = 2 + width = columns * cell_size + (columns + 1) * gap + height = rows * (cell_size + label_height) + (rows + 1) * gap + sheet = Image.new("RGB", (width, height), "#eee9df") + draw = ImageDraw.Draw(sheet) + font = load_font(23) + + for index, (_, title, preview) in enumerate(previews): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_size + label_height + gap) + thumbnail = preview.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=10, + fill="#fffdf8", + ) + label = f"{index + 1:02d} {title}" + text_box = draw.textbbox((0, 0), label, font=font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=font) + return sheet + + +def main() -> None: + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + previews: list[tuple[str, str, Image.Image]] = [] + + for asset_id, title, style in VARIANTS: + preview = render_variant(style) + preview.save(OUTPUT_DIR / f"{asset_id}.png") + (OUTPUT_DIR / f"{asset_id}.svg").write_text(build_svg(style), encoding="utf-8") + previews.append((asset_id, title, preview)) + + contact_sheet = build_contact_sheet(previews) + contact_sheet.save(CONTACT_SHEET_PATH, quality=95) + print( + { + "ok": True, + "output_dir": str(OUTPUT_DIR), + "files": [f"{asset_id}.png" for asset_id, _, _ in VARIANTS] + + [f"{asset_id}.svg" for asset_id, _, _ in VARIANTS] + + [CONTACT_SHEET_PATH.name], + } + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-hand-spirit-bold-color-concepts.mjs b/scripts/generate-taonier-hand-spirit-bold-color-concepts.mjs new file mode 100644 index 00000000..cb441ff6 --- /dev/null +++ b/scripts/generate-taonier-hand-spirit-bold-color-concepts.mjs @@ -0,0 +1,405 @@ +import { Blob, Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-hand-spirit-bold-color-concepts', +); +const referenceImagePath = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-hand-spirit-ref01-logo-refine-concepts', + 'taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream.png', +); +const timeoutMsDefault = 420000; + +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 basePrompt = [ + 'Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.', + 'Create a bolder, more attractive color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.', + 'Preserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.', + 'The goal is more memorable social-platform color, more appealing to young users and women, while still premium and trademark-like.', + 'Use bold modern brand colors, not realistic material colors. Saturated colors are allowed, but they must read as clean brand color fields, not candy, dessert, food, jelly, bread, pastry, mochi, bun, sauce, or cosmetic packaging.', + 'Make it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle app-logo polish.', + 'Keep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.', + 'Clean light background, generous safe area. Image-only logo concept.', +]; + +const variants = [ + { + id: '01-berry-aqua-pop', + title: '莓粉青 aqua', + prompt: [ + ...basePrompt, + 'Palette: vivid raspberry pink semi-dome, bright coral side accent, fresh aqua or mint hand support, small cream negative gap. Bold, young, energetic, not sugary.', + ], + }, + { + id: '02-coral-lilac', + title: '珊瑚丁香', + prompt: [ + ...basePrompt, + 'Palette: punchy coral-red semi-dome with warm pink accent, soft lilac-lavender hand support, tiny ivory separator. Feminine, fresh, and premium.', + ], + }, + { + id: '03-mango-turquoise', + title: '芒果松石', + prompt: [ + ...basePrompt, + 'Palette: bright mango-orange semi-dome, hot peach accent, turquoise hand support. High contrast and cheerful, but still flat and logo-like, not food-like.', + ], + }, + { + id: '04-neon-rose-mint', + title: '玫红薄荷', + prompt: [ + ...basePrompt, + 'Palette: neon rose or magenta semi-dome, clean mint green hand support, warm ivory separator. Strong social-avatar memory, modern and playful.', + ], + }, + { + id: '05-poppy-blue', + title: '罂粟蓝调', + prompt: [ + ...basePrompt, + 'Palette: saturated poppy orange-red semi-dome, cobalt or sky-blue support curve, cream separator. More graphic, bold, and youth-culture oriented.', + ], + }, + { + id: '06-violet-peach', + title: '紫桃撞色', + prompt: [ + ...basePrompt, + 'Palette: vivid violet-purple hand support with peach-orange semi-dome and pink accent. Keep the purple limited and crisp so the logo does not become a generic purple tech gradient.', + ], + }, + { + id: '07-flat-duotone', + title: '双色强记忆', + prompt: [ + ...basePrompt, + 'Palette and style: ultra-flat two-color version. Use one bold warm color for the spirit and one bold cool color for the hand. No highlight, no gradient, no shadow. Maximize trademark simplicity.', + ], + }, + { + id: '08-app-icon-bright', + title: '亮彩头像', + prompt: [ + ...basePrompt, + 'Palette and style: brightest app-icon-friendly version. Use coral, hot pink, and aqua with only a very subtle broad gradient. Keep the mark bold and readable at small size.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildEditUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/edits` + : `${baseUrl}/v1/images/edits`; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') return 'png'; + if (normalized === 'image/webp') return 'webp'; + if (normalized === 'image/gif') return 'gif'; + return 'jpg'; +} + +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}`); + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +function createEditFormData(variant) { + const form = new FormData(); + const imageBytes = readFileSync(referenceImagePath); + form.append('model', 'gpt-image-2'); + form.append('prompt', variant.prompt.join('\n')); + form.append('n', '1'); + form.append('size', '1024x1024'); + form.append( + 'image', + new Blob([imageBytes], { type: 'image/png' }), + path.basename(referenceImagePath), + ); + return form; +} + +function buildDryRunFields(variant) { + return { + model: 'gpt-image-2', + prompt: variant.prompt.join('\n'), + n: '1', + size: '1024x1024', + image: referenceImagePath, + }; +} + +async function generateOne(env, variant) { + const payload = await fetchJson( + buildEditUrl(env.baseUrl), + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.apiKey}`, + Accept: 'application/json', + }, + body: createEditFormData(variant), + }, + env.timeoutMs, + ); + + const urls = extractImageUrls(payload); + const b64Images = extractBase64Images(payload); + let image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { bytes, extension: inferExtensionFromBytes(bytes) }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join( + outputDir, + `taonier-hand-spirit-bold-color-${variant.id}.${image.extension}`, + ); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join(outputDir, 'taonier-hand-spirit-bold-color-manifest.json'); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2', + endpoint: '/v1/images/edits', + size: '1024x1024', + referenceImage: path.relative(repoRoot, referenceImagePath), + generatedAt: new Date().toISOString(), + brief: { + brand: '陶泥儿', + goal: '更大胆、更吸引女生和年轻人的手托灵体 logo 配色探索', + keep: '保留托举曲线与半球灵体结构,不加文字、不加脸、不加星星', + }, + variants: variants.map((variant) => { + const file = files.find((item) => path.basename(item).includes(variant.id)); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + referenceImagePath, + count: selectedVariants.length, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + fields: buildDryRunFields(variant), + })), + }, + null, + 2, + ), + ); + process.exit(0); +} + +if (!existsSync(referenceImagePath)) { + console.error(JSON.stringify({ ok: false, error: 'Reference image does not exist' })); + process.exit(1); +} + +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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log(JSON.stringify({ ok: true, count: generated.length, files: generated, manifest: manifestPath }, null, 2)); diff --git a/scripts/generate-taonier-hand-spirit-bold-color-contact-sheet.py b/scripts/generate-taonier-hand-spirit-bold-color-contact-sheet.py new file mode 100644 index 00000000..5ccc8526 --- /dev/null +++ b/scripts/generate-taonier-hand-spirit-bold-color-contact-sheet.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = ( + REPO_ROOT / "public" / "branding" / "taonier-logo-hand-spirit-bold-color-concepts" +) +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-hand-spirit-bold-color-contact-sheet.png" +REFERENCE_IMAGE = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-hand-spirit-ref01-logo-refine-concepts" + / "taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream.png" +) + +ITEMS = [ + ("REF 上轮01", REFERENCE_IMAGE), + ("01 莓粉青 aqua", "taonier-hand-spirit-bold-color-01-berry-aqua-pop"), + ("02 珊瑚丁香", "taonier-hand-spirit-bold-color-02-coral-lilac"), + ("03 芒果松石", "taonier-hand-spirit-bold-color-03-mango-turquoise"), + ("04 玫红薄荷", "taonier-hand-spirit-bold-color-04-neon-rose-mint"), + ("05 罂粟蓝调", "taonier-hand-spirit-bold-color-05-poppy-blue"), + ("06 紫桃撞色", "taonier-hand-spirit-bold-color-06-violet-peach"), + ("07 双色强记忆", "taonier-hand-spirit-bold-color-07-flat-duotone"), + ("08 亮彩头像", "taonier-hand-spirit-bold-color-08-app-icon-bright"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + for candidate in [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ]: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem_or_path: str | Path) -> Path | None: + if isinstance(stem_or_path, Path): + return stem_or_path if stem_or_path.exists() else None + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem_or_path}.{extension}" + if candidate.exists(): + return candidate + return None + + +def normalize_square(image_path: Path) -> Image.Image: + image = Image.open(image_path).convert("RGB") + if image.size == (1024, 1024): + return image + if image.width == image.height: + normalized = image.resize((1024, 1024), Image.Resampling.LANCZOS) + else: + contained = ImageOps.contain(image, (1024, 1024), Image.Resampling.LANCZOS) + normalized = Image.new("RGB", (1024, 1024), "#fffdf8") + x = (1024 - contained.width) // 2 + y = (1024 - contained.height) // 2 + normalized.paste(contained, (x, y)) + if image_path.is_relative_to(OUTPUT_DIR): + normalized.save(image_path, quality=95) + return normalized + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 268 + label_height = 54 + test_height = 44 + gap = 22 + columns = 3 + rows = 3 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(18) + test_font = load_font(13) + + for index, (label, stem_or_path) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem_or_path) + if image_path is None: + continue + + source = normalize_square(image_path) + sheet.paste(source.resize((cell_size, cell_size), Image.Resampling.LANCZOS), (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 52, test_y + 6)) + sheet.paste(mono, (x + 104, test_y + 6)) + draw.text((x + 150, test_y + 13), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-hand-spirit-contact-sheet.py b/scripts/generate-taonier-hand-spirit-contact-sheet.py new file mode 100644 index 00000000..f8effed3 --- /dev/null +++ b/scripts/generate-taonier-hand-spirit-contact-sheet.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-hand-spirit-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-hand-spirit-contact-sheet.png" + +ITEMS = [ + ("01 温柔托举灵体", "taonier-hand-spirit-01-gentle-hand-spirit"), + ("02 分享掌形", "taonier-hand-spirit-02-sharing-palm"), + ("03 青绿托线", "taonier-hand-spirit-03-teal-support"), + ("04 拱形泥灵", "taonier-hand-spirit-04-arched-spirit"), + ("05 轻玩递出", "taonier-hand-spirit-05-playful-offer"), + ("06 黑白优先", "taonier-hand-spirit-06-monochrome-first"), + ("07 头像可读", "taonier-hand-spirit-07-avatar-readable"), + ("08 矢量定稿感", "taonier-hand-spirit-08-vector-ready"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 300 + label_height = 58 + test_height = 46 + gap = 24 + columns = 4 + rows = 2 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(20) + test_font = load_font(14) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 68, test_y + 7)) + sheet.paste(mono, (x + 122, test_y + 7)) + draw.text((x + 166, test_y + 14), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-hand-spirit-logo-concepts.mjs b/scripts/generate-taonier-hand-spirit-logo-concepts.mjs new file mode 100644 index 00000000..b61506e6 --- /dev/null +++ b/scripts/generate-taonier-hand-spirit-logo-concepts.mjs @@ -0,0 +1,442 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-hand-spirit-concepts', +); +const timeoutMsDefault = 180000; + +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 logoBrief = { + brand: '陶泥儿', + coreBelief: '好玩会创造', + logoType: 'symbol/icon-only mark, no wordmark', + product: + 'AI UGC 轻休闲小游戏创作与传播平台,用户像捏陶泥一样把脑洞、梗和灵感塑造成可分享的作品', + metaphor: '抽象化的手托举/递出一个软萌陶泥灵体', + intent: ['托举', '分享', '传递', '创作被捏成一个有生命感的小作品'], + spiritShape: '不规则半球形陶泥灵体,参考黑底白色半圆拱形轮廓,但不照抄', + audience: '女性用户友好、全年龄向、年轻明亮但不低幼', + material: '只保留陶泥温度,不追求泥土质感', + mustHave: [ + '手必须高度抽象,像托举曲线或掌形基座', + '陶泥灵体必须是主角,软萌但不出现脸', + '画面传达分享/传递,而不是供奉/宗教/医疗', + '32px 可识别', + '黑白化仍成立', + ], +}; + +const basePrompt = [ + 'Create an icon-only brand logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', + 'Brand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.', + 'Main metaphor: an abstract hand gently holding up and offering a cute soft clay spirit. The logo should communicate creation, sharing, passing forward, and a small idea becoming a lovable playable object.', + 'Logo type: abstract symbol/icon only. Not a mascot illustration, not an app-icon rounded-square background, not an emblem with text.', + 'Composition: one highly simplified hand-like support curve or palm base below, holding one soft clay spirit above. The hand must be abstract, smooth, and logo-like, not realistic fingers.', + 'Clay spirit shape: a cute irregular semi-dome / half-blob / rounded arch form, inspired by a simple white arched half-circle silhouette on dark background, but transformed into a friendly original brand symbol.', + 'The spirit should be soft and lovable without a face. No eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.', + 'The hand gesture should feel like sharing or gently presenting, not worship, religion, medical care, begging, or holding a food item.', + 'Style: modern minimalist vector logo. Clean broad shapes, clear silhouette, fresh bright colors, very light clay warmth only. It must look good and recognizable at 32px favicon size.', + 'Color direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.', + 'Avoid muted mud colors as the dominant palette. Avoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.', + 'Food avoidance is critical: do not make the spirit look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, cream filling, sauce, dessert, or food packaging.', + 'Shape avoidance: no square base, no rounded-square app background, no free ribbon, no swirl, no S or G letter feeling, no center star.', + 'Composition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.', + 'Validation targets: black-and-white version should still read clearly; the hand support and semi-dome clay spirit should be recognizable at 32px.', +]; + +const variants = [ + { + id: '01-gentle-hand-spirit', + title: '温柔托举灵体', + prompt: [ + ...basePrompt, + 'Variant focus: the clearest version. A simple cream abstract palm curve holds a coral-peach semi-dome clay spirit. Friendly, iconic, and readable.', + ], + }, + { + id: '02-sharing-palm', + title: '分享掌形', + prompt: [ + ...basePrompt, + 'Variant focus: sharing intention. The abstract hand is slightly forward-facing, like offering the clay spirit outward, but still very simplified and not realistic.', + ], + }, + { + id: '03-teal-support', + title: '青绿托线', + prompt: [ + ...basePrompt, + 'Variant focus: use a clear soft teal support curve as the hand and a warm peach clay spirit above. Strong color memory, no food look.', + ], + }, + { + id: '04-arched-spirit', + title: '拱形泥灵', + prompt: [ + ...basePrompt, + 'Variant focus: emphasize the irregular semi-dome clay spirit shape from the reference: simple arched top, flatter base, slightly organic, no face.', + ], + }, + { + id: '05-playful-offer', + title: '轻玩递出', + prompt: [ + ...basePrompt, + 'Variant focus: more playful and lively. The hand support suggests passing the spirit forward, with one broad curve only. Avoid decorative tiny details.', + ], + }, + { + id: '06-monochrome-first', + title: '黑白优先', + prompt: [ + ...basePrompt, + 'Variant focus: design for black-and-white survival first. Use strong positive and negative shapes so the hand and spirit remain readable without color.', + ], + }, + { + id: '07-avatar-readable', + title: '头像可读', + prompt: [ + ...basePrompt, + 'Variant focus: social avatar and favicon readability. Compact, bold silhouette, thicker hand curve, larger semi-dome spirit, no small parts.', + ], + }, + { + id: '08-vector-ready', + title: '矢量定稿感', + prompt: [ + ...basePrompt, + 'Variant focus: designer-ready vector concept. 2-3 flat shapes, crisp boundaries, distinctive hand-support and clay-spirit silhouette, minimal material cue.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join(outputDir, `taonier-hand-spirit-${variant.id}.${image.extension}`); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join(outputDir, 'taonier-logo-hand-spirit-manifest.json'); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + logoSkillSummary: { + requiredReview: 'visual inspection, 32px readability, black-white viability', + outputStatus: 'AI concept only; final logo needs vector cleanup', + }, + brief: logoBrief, + variants: variants.map((variant) => { + const file = files.find((item) => path.basename(item).includes(variant.id)); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + brief: logoBrief, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-hand-spirit-muted-color-concepts.mjs b/scripts/generate-taonier-hand-spirit-muted-color-concepts.mjs new file mode 100644 index 00000000..74a84775 --- /dev/null +++ b/scripts/generate-taonier-hand-spirit-muted-color-concepts.mjs @@ -0,0 +1,404 @@ +import { Blob, Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-hand-spirit-muted-color-concepts', +); +const referenceImagePath = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-hand-spirit-ref01-logo-refine-concepts', + 'taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream.png', +); +const timeoutMsDefault = 420000; + +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 basePrompt = [ + 'Use the uploaded logo as the structural reference. Keep the same icon-only mark structure: a soft semi-dome clay spirit above a smooth abstract hand-like support curve.', + 'Create a low-saturation color exploration for a young women-friendly brand logo. No Chinese, no English, no letters, no numbers, no wordmark, no tagline, no text.', + 'Preserve the existing logo proportions, centered composition, abstract hand support, semi-dome spirit shape, and clean logo silhouette. Do not add a face, eyes, mouth, limbs, star, spark, UI, border, or watermark.', + 'The goal is softer, more tasteful, more wearable brand color. The palette should feel like a modern lifestyle brand, not a toy, candy, dessert, or cosmetics ad.', + 'Use muted but attractive colors: dusty rose, misty lavender, sage green, smoke blue, butter cream, muted coral, terracotta clay, pale apricot. Keep the image warm and youthful without becoming loud.', + 'Make it flatter and clearer than the original reference: broad vector-friendly shapes, crisp edges, minimal gradients, no glossy highlight unless the variant explicitly asks for a subtle polish.', + 'Keep 32px readability and black-white viability. The mark must still work as an app icon, social avatar, and trademark symbol.', + 'Clean light background, generous safe area. Image-only logo concept.', +]; + +const variants = [ + { + id: '01-dusty-rose-sage', + title: '雾玫鼠尾草', + prompt: [ + ...basePrompt, + 'Palette: dusty rose semi-dome, sage green support curve, warm ivory gap. Soft, modern, and feminine without being sweet.', + ], + }, + { + id: '02-smoke-blue-apricot', + title: '烟蓝杏橙', + prompt: [ + ...basePrompt, + 'Palette: smoke blue support curve, pale apricot or muted peach semi-dome, cream separator. Calm, fresh, and suitable for young users.', + ], + }, + { + id: '03-misty-lilac-clay', + title: '雾紫陶土', + prompt: [ + ...basePrompt, + 'Palette: misty lilac support, soft terracotta clay spirit, off-white negative space. More boutique and refined, not purple-tech.', + ], + }, + { + id: '04-butter-rose-tea', + title: '黄油玫瑰茶', + prompt: [ + ...basePrompt, + 'Palette: butter cream spirit, muted rose support curve, faint tea-green accent. Gentle, cozy, and premium with low saturation.', + ], + }, + { + id: '05-clay-blue-mint', + title: '陶蓝薄荷', + prompt: [ + ...basePrompt, + 'Palette: clay orange or muted coral semi-dome, powder blue support, tiny mint accent. Softly playful but not heavy.', + ], + }, + { + id: '06-powder-berry-cloud', + title: '粉雾浆果', + prompt: [ + ...basePrompt, + 'Palette: powder berry semi-dome, cloud pink support curve, warm cream gap. Youthful, gentle, and more like a boutique brand than a toy.', + ], + }, + { + id: '07-sand-violet', + title: '砂紫奶雾', + prompt: [ + ...basePrompt, + 'Palette: sand beige or pale almond spirit, muted violet support curve, soft cream separator. Quiet, tasteful, and logo-ready.', + ], + }, + { + id: '08-muted-duotone', + title: '低饱双色', + prompt: [ + ...basePrompt, + 'Palette and style: two-color muted duotone only. Use one subdued warm hue and one subdued cool hue. No shiny gloss, no intense contrast, no candy feeling.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildEditUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/edits` + : `${baseUrl}/v1/images/edits`; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') return 'png'; + if (normalized === 'image/webp') return 'webp'; + if (normalized === 'image/gif') return 'gif'; + return 'jpg'; +} + +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}`); + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +function createEditFormData(variant) { + const form = new FormData(); + const imageBytes = readFileSync(referenceImagePath); + form.append('model', 'gpt-image-2'); + form.append('prompt', variant.prompt.join('\n')); + form.append('n', '1'); + form.append('size', '1024x1024'); + form.append( + 'image', + new Blob([imageBytes], { type: 'image/png' }), + path.basename(referenceImagePath), + ); + return form; +} + +function buildDryRunFields(variant) { + return { + model: 'gpt-image-2', + prompt: variant.prompt.join('\n'), + n: '1', + size: '1024x1024', + image: referenceImagePath, + }; +} + +async function generateOne(env, variant) { + const payload = await fetchJson( + buildEditUrl(env.baseUrl), + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.apiKey}`, + Accept: 'application/json', + }, + body: createEditFormData(variant), + }, + env.timeoutMs, + ); + + const urls = extractImageUrls(payload); + const b64Images = extractBase64Images(payload); + let image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { bytes, extension: inferExtensionFromBytes(bytes) }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join( + outputDir, + `taonier-hand-spirit-muted-color-${variant.id}.${image.extension}`, + ); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join(outputDir, 'taonier-hand-spirit-muted-color-manifest.json'); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2', + endpoint: '/v1/images/edits', + size: '1024x1024', + referenceImage: path.relative(repoRoot, referenceImagePath), + generatedAt: new Date().toISOString(), + brief: { + brand: '陶泥儿', + goal: '低饱和度但不寡淡的年轻向颜色探索', + keep: '保留托举曲线与半球灵体结构,不加文字、不加脸、不加星星', + }, + variants: variants.map((variant) => { + const file = files.find((item) => path.basename(item).includes(variant.id)); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + referenceImagePath, + count: selectedVariants.length, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + fields: buildDryRunFields(variant), + })), + }, + null, + 2, + ), + ); + process.exit(0); +} + +if (!existsSync(referenceImagePath)) { + console.error(JSON.stringify({ ok: false, error: 'Reference image does not exist' })); + process.exit(1); +} + +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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log(JSON.stringify({ ok: true, count: generated.length, files: generated, manifest: manifestPath }, null, 2)); diff --git a/scripts/generate-taonier-hand-spirit-muted-color-contact-sheet.py b/scripts/generate-taonier-hand-spirit-muted-color-contact-sheet.py new file mode 100644 index 00000000..d9127884 --- /dev/null +++ b/scripts/generate-taonier-hand-spirit-muted-color-contact-sheet.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = ( + REPO_ROOT / "public" / "branding" / "taonier-logo-hand-spirit-muted-color-concepts" +) +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-hand-spirit-muted-color-contact-sheet.png" +REFERENCE_IMAGE = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-hand-spirit-ref01-logo-refine-concepts" + / "taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream.png" +) + +ITEMS = [ + ("REF 上轮01", REFERENCE_IMAGE), + ("01 雾玫鼠尾草", "taonier-hand-spirit-muted-color-01-dusty-rose-sage"), + ("02 烟蓝杏橙", "taonier-hand-spirit-muted-color-02-smoke-blue-apricot"), + ("03 雾紫陶土", "taonier-hand-spirit-muted-color-03-misty-lilac-clay"), + ("04 黄油玫瑰茶", "taonier-hand-spirit-muted-color-04-butter-rose-tea"), + ("05 陶蓝薄荷", "taonier-hand-spirit-muted-color-05-clay-blue-mint"), + ("06 粉雾浆果", "taonier-hand-spirit-muted-color-06-powder-berry-cloud"), + ("07 砂紫奶雾", "taonier-hand-spirit-muted-color-07-sand-violet"), + ("08 低饱双色", "taonier-hand-spirit-muted-color-08-muted-duotone"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + for candidate in [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ]: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem_or_path: str | Path) -> Path | None: + if isinstance(stem_or_path, Path): + return stem_or_path if stem_or_path.exists() else None + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem_or_path}.{extension}" + if candidate.exists(): + return candidate + return None + + +def normalize_square(image_path: Path) -> Image.Image: + image = Image.open(image_path).convert("RGB") + if image.size == (1024, 1024): + return image + if image.width == image.height: + normalized = image.resize((1024, 1024), Image.Resampling.LANCZOS) + else: + contained = ImageOps.contain(image, (1024, 1024), Image.Resampling.LANCZOS) + normalized = Image.new("RGB", (1024, 1024), "#fffdf8") + x = (1024 - contained.width) // 2 + y = (1024 - contained.height) // 2 + normalized.paste(contained, (x, y)) + if image_path.is_relative_to(OUTPUT_DIR): + normalized.save(image_path, quality=95) + return normalized + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 268 + label_height = 54 + test_height = 44 + gap = 22 + columns = 3 + rows = 3 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(18) + test_font = load_font(13) + + for index, (label, stem_or_path) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem_or_path) + if image_path is None: + continue + + source = normalize_square(image_path) + sheet.paste(source.resize((cell_size, cell_size), Image.Resampling.LANCZOS), (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 52, test_y + 6)) + sheet.paste(mono, (x + 104, test_y + 6)) + draw.text((x + 150, test_y + 13), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-hand-spirit-outline-eye-concepts.mjs b/scripts/generate-taonier-hand-spirit-outline-eye-concepts.mjs new file mode 100644 index 00000000..85e8f9aa --- /dev/null +++ b/scripts/generate-taonier-hand-spirit-outline-eye-concepts.mjs @@ -0,0 +1,424 @@ +import { Blob, Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-hand-spirit-outline-eye-concepts', +); +const referenceImagePath = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-hand-spirit-ref01-logo-refine-concepts', + 'taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream.png', +); +const timeoutMsDefault = 420000; + +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 basePrompt = [ + 'Use the uploaded logo as the exact structural reference. Keep the same icon-only composition: a soft semi-dome spirit above a smooth abstract hand-like support curve.', + 'Create a cuter, more logo-like refinement. Add a clean outline around the whole symbol, and add two pure matte black dot eyes to the upper semi-dome part only.', + 'The eyes must belong to the upper half-circle spirit, not the lower support curve. No nose, no mouth, no eyebrows, no blush, no limbs, no character body, no star, no spark, no text.', + 'Do not redesign the overall structure. Keep the same proportions, centered layout, and soft brand silhouette. Only make it more cute, more memorable, and more trademark-like.', + 'The outline should feel clean, friendly, and brand-ready, not sticker-like, not cartoonish, not too thick unless the variant asks for it.', + 'Use the same soft warm palette family as the reference, but allow subtle low-saturation tweaks to improve charm and contrast. Keep it elegant and youthful, not loud.', + 'Keep 32px readability and black-white viability. It must still work as an app icon, social avatar, and trademark symbol.', + 'Clean light background, generous safe area. Image-only logo concept.', +]; + +const variants = [ + { + id: '01-thin-outline-small-eyes', + title: '细描边小眼', + prompt: [ + ...basePrompt, + 'Variant focus: the most restrained cute version. Use a thin warm outline and small matte black dot eyes with calm spacing.', + ], + }, + { + id: '02-medium-outline-round-eyes', + title: '中描边圆眼', + prompt: [ + ...basePrompt, + 'Variant focus: medium outline thickness and slightly rounder dot eyes. Make the face read a touch more openly cute, but still minimal.', + ], + }, + { + id: '03-bold-outline-higher-eyes', + title: '粗描边高眼', + prompt: [ + ...basePrompt, + 'Variant focus: stronger bold outline and eyes placed a little higher on the upper dome, creating a sweeter peeking expression.', + ], + }, + { + id: '04-warm-cocoa-outline', + title: '暖可可描边', + prompt: [ + ...basePrompt, + 'Variant focus: use a warm cocoa or deep beige outline that makes the logo feel softer and more plush, with small centered black eyes.', + ], + }, + { + id: '05-compact-avatar-cute', + title: '头像可爱款', + prompt: [ + ...basePrompt, + 'Variant focus: compact avatar readability. Enlarge the upper dome slightly, keep the hand support bold, and make the eyes more visible without adding any mouth.', + ], + }, + { + id: '06-black-white-first', + title: '黑白优先', + prompt: [ + ...basePrompt, + 'Variant focus: black-and-white survival first. Make the outline and the eyes work clearly even if all color is removed. Very strong logo readability.', + ], + }, + { + id: '07-soft-feminine-cute', + title: '柔和少女感', + prompt: [ + ...basePrompt, + 'Variant focus: a softer feminine-cute version. Keep the outline elegant and the eyes gentle; the whole mark should feel like a friendly brand mascot symbol.', + ], + }, + { + id: '08-vector-ready-cute', + title: '矢量定稿感', + prompt: [ + ...basePrompt, + 'Variant focus: designer-ready vector concept. Clean crisp outline, balanced eye spacing, no decorative detail, very easy to trace into an SVG mark.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildEditUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/edits` + : `${baseUrl}/v1/images/edits`; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') return 'png'; + if (normalized === 'image/webp') return 'webp'; + if (normalized === 'image/gif') return 'gif'; + return 'jpg'; +} + +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}`); + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +function createEditFormData(variant) { + const form = new FormData(); + const imageBytes = readFileSync(referenceImagePath); + form.append('model', 'gpt-image-2'); + form.append('prompt', variant.prompt.join('\n')); + form.append('n', '1'); + form.append('size', '1024x1024'); + form.append( + 'image', + new Blob([imageBytes], { type: 'image/png' }), + path.basename(referenceImagePath), + ); + return form; +} + +function buildDryRunFields(variant) { + return { + model: 'gpt-image-2', + prompt: variant.prompt.join('\n'), + n: '1', + size: '1024x1024', + image: referenceImagePath, + }; +} + +async function generateOne(env, variant) { + const payload = await fetchJson( + buildEditUrl(env.baseUrl), + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.apiKey}`, + Accept: 'application/json', + }, + body: createEditFormData(variant), + }, + env.timeoutMs, + ); + + const urls = extractImageUrls(payload); + const b64Images = extractBase64Images(payload); + let image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { bytes, extension: inferExtensionFromBytes(bytes) }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join( + outputDir, + `taonier-hand-spirit-outline-eye-${variant.id}.${image.extension}`, + ); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join( + outputDir, + 'taonier-hand-spirit-outline-eye-manifest.json', + ); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2', + endpoint: '/v1/images/edits', + size: '1024x1024', + referenceImage: path.relative(repoRoot, referenceImagePath), + generatedAt: new Date().toISOString(), + brief: { + brand: '陶泥儿', + goal: '在上轮 01 的基础上加入描边和黑点眼睛,让标志更可爱', + keep: '保留托举曲线与半球灵体结构,不加文字、不加星星、不改骨架', + }, + variants: variants.map((variant) => { + const file = files.find((item) => path.basename(item).includes(variant.id)); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + referenceImagePath, + count: selectedVariants.length, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + fields: buildDryRunFields(variant), + })), + }, + null, + 2, + ), + ); + process.exit(0); +} + +if (!existsSync(referenceImagePath)) { + console.error( + JSON.stringify({ + ok: false, + error: 'Reference image does not exist', + referenceImagePath, + }), + ); + process.exit(1); +} + +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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-hand-spirit-outline-eye-contact-sheet.py b/scripts/generate-taonier-hand-spirit-outline-eye-contact-sheet.py new file mode 100644 index 00000000..2c26dea8 --- /dev/null +++ b/scripts/generate-taonier-hand-spirit-outline-eye-contact-sheet.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = ( + REPO_ROOT / "public" / "branding" / "taonier-logo-hand-spirit-outline-eye-concepts" +) +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-hand-spirit-outline-eye-contact-sheet.png" +REFERENCE_IMAGE = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-hand-spirit-ref01-logo-refine-concepts" + / "taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream.png" +) + +ITEMS = [ + ("REF 上轮01", REFERENCE_IMAGE), + ("01 细描边小眼", "taonier-hand-spirit-outline-eye-01-thin-outline-small-eyes"), + ("02 中描边圆眼", "taonier-hand-spirit-outline-eye-02-medium-outline-round-eyes"), + ("03 粗描边高眼", "taonier-hand-spirit-outline-eye-03-bold-outline-higher-eyes"), + ("04 暖可可描边", "taonier-hand-spirit-outline-eye-04-warm-cocoa-outline"), + ("05 头像可爱款", "taonier-hand-spirit-outline-eye-05-compact-avatar-cute"), + ("06 黑白优先", "taonier-hand-spirit-outline-eye-06-black-white-first"), + ("07 柔和少女感", "taonier-hand-spirit-outline-eye-07-soft-feminine-cute"), + ("08 矢量定稿感", "taonier-hand-spirit-outline-eye-08-vector-ready-cute"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + for candidate in [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ]: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem_or_path: str | Path) -> Path | None: + if isinstance(stem_or_path, Path): + return stem_or_path if stem_or_path.exists() else None + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem_or_path}.{extension}" + if candidate.exists(): + return candidate + return None + + +def normalize_square(image_path: Path) -> Image.Image: + image = Image.open(image_path).convert("RGB") + if image.size == (1024, 1024): + return image + if image.width == image.height: + normalized = image.resize((1024, 1024), Image.Resampling.LANCZOS) + else: + contained = ImageOps.contain(image, (1024, 1024), Image.Resampling.LANCZOS) + normalized = Image.new("RGB", (1024, 1024), "#fffdf8") + x = (1024 - contained.width) // 2 + y = (1024 - contained.height) // 2 + normalized.paste(contained, (x, y)) + if image_path.is_relative_to(OUTPUT_DIR): + normalized.save(image_path, quality=95) + return normalized + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 268 + label_height = 54 + test_height = 44 + gap = 22 + columns = 3 + rows = 3 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(18) + test_font = load_font(13) + + for index, (label, stem_or_path) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem_or_path) + if image_path is None: + continue + + source = normalize_square(image_path) + sheet.paste(source.resize((cell_size, cell_size), Image.Resampling.LANCZOS), (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 52, test_y + 6)) + sheet.paste(mono, (x + 104, test_y + 6)) + draw.text((x + 150, test_y + 13), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-hand-spirit-ref01-logo-refine-concepts.mjs b/scripts/generate-taonier-hand-spirit-ref01-logo-refine-concepts.mjs new file mode 100644 index 00000000..89d982fb --- /dev/null +++ b/scripts/generate-taonier-hand-spirit-ref01-logo-refine-concepts.mjs @@ -0,0 +1,491 @@ +import { Blob, Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-hand-spirit-ref01-logo-refine-concepts', +); +const referenceImagePath = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-hand-spirit-concepts', + 'taonier-hand-spirit-01-gentle-hand-spirit.png', +); +const timeoutMsDefault = 420000; + +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 logoBrief = { + brand: '陶泥儿', + source: '基于 taonier-hand-spirit-01-gentle-hand-spirit 做商标化探索', + logoType: 'symbol/icon-only mark, no wordmark', + product: + 'AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品', + keep: [ + '上方软萌半球陶泥灵体', + '下方抽象托举手势/掌形曲线', + '托举、传递、分享的动作语义', + '无脸、无文字、无星星', + '亲和、精品、可用于商标和 App 图标', + ], + explore: [ + '更扁平的纯色块版本', + '更精品的低饱和陶器色版本', + '更强线面结构版本', + '更抽象、更少形状的版本', + '更适合 32px 和黑白化的版本', + ], + avoid: [ + '中文或英文字', + '眼睛、嘴巴、表情、角色身体', + '星星、闪光、魔法符号', + '真实手指、宗教托举、医疗护理感', + '面包、甜点、糖果、果冻、奶油、食物包装', + '复杂背景、边框、UI、按钮、水印', + ], +}; + +const basePrompt = [ + 'Use the uploaded reference image as the composition and shape reference. Keep the same core structure: an abstract hand-like support curve below, gently holding a soft semi-dome clay spirit above.', + 'Create a refined icon-only brand logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', + 'Preserve the brand idea: fun creation, sharing, passing a small playable idea forward. The mark should feel like a small creative object being gently offered, not a scene or illustration.', + 'Keep the subject face-free: no eyes, no mouth, no expression, no limbs, no character body, no halo, no star, no spark.', + 'Make it more like a trademark and logo than the reference: fewer shapes, cleaner silhouette, clearer positive/negative structure, more vector-friendly curves, broad flat color regions, less glossy highlight, less painterly shadow.', + 'The hand must remain abstract: a smooth palm-like support curve or bowl-like gesture, not realistic fingers, not a real human hand, not worship, not medical care.', + 'The spirit must remain a soft irregular semi-dome or rounded clay core. It should not become a planet, helmet, bread, bun, mochi, dumpling, pastry, candy, jelly, sauce, dessert, or food package.', + 'Style target: premium friendly logo mark, all-age and women-friendly, warm but not childish, suitable for app icon, social avatar, and trademark review.', + 'Composition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.', + 'Avoid UI, border, sticker outline unless explicitly requested by the variant, background scene, watermark, extra marks, and any text.', +]; + +const variants = [ + { + id: '01-flat-coral-cream', + title: '扁平珊瑚奶白', + prompt: [ + ...basePrompt, + 'Variant focus: the most direct flat-logo refinement. Use coral-orange clay spirit and cream hand support. Reduce the glossy highlight to nearly zero. Use 3-4 crisp flat shapes only.', + ], + }, + { + id: '02-warm-clay-premium', + title: '暖陶精品色', + prompt: [ + ...basePrompt, + 'Variant focus: warmer boutique clay palette. Muted terracotta, soft sand, and warm ivory. More mature and premium, with a compact iconic silhouette and no candy gloss.', + ], + }, + { + id: '03-mint-support', + title: '青绿托举线', + prompt: [ + ...basePrompt, + 'Variant focus: stronger color memory. Use a clear muted teal or mint support curve as the hand and a warm peach clay spirit above. Keep it flat, balanced, and not cosmetic.', + ], + }, + { + id: '04-outline-vector', + title: '线面商标', + prompt: [ + ...basePrompt, + 'Variant focus: bolder trademark construction. Use a clean warm-brown contour line combined with flat fills. The outline should clarify the hand and spirit silhouette, modern rather than sticker-like.', + ], + }, + { + id: '05-abstract-two-shape', + title: '双形抽象', + prompt: [ + ...basePrompt, + 'Variant focus: higher abstraction. Reduce the mark to two dominant shapes: one semi-dome spirit and one sweeping hand support. Remove highlight details. Make the silhouette distinctive and vector-ready.', + ], + }, + { + id: '06-monochrome-first', + title: '黑白优先', + prompt: [ + ...basePrompt, + 'Variant focus: black-and-white survival first. Design with strong positive and negative shapes; color may be warm clay, but the mark must remain clear as a pure monochrome logo.', + ], + }, + { + id: '07-soft-gradient-premium', + title: '轻渐变精品', + prompt: [ + ...basePrompt, + 'Variant focus: a polished but still logo-like version. Allow only very subtle broad gradients for premium softness. Remove small glossy highlights and avoid 3D rendering.', + ], + }, + { + id: '08-compact-avatar', + title: '头像强识别', + prompt: [ + ...basePrompt, + 'Variant focus: compact social-avatar readability. Enlarge the clay spirit slightly, thicken the hand support curve, reduce thin gaps, and keep the total mark bold at 32px.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildEditUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/edits` + : `${baseUrl}/v1/images/edits`; +} + +function buildDryRunFields(variant) { + return { + model: 'gpt-image-2', + prompt: variant.prompt.join('\n'), + n: '1', + size: '1024x1024', + image: referenceImagePath, + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +function createEditFormData(variant) { + const form = new FormData(); + const imageBytes = readFileSync(referenceImagePath); + form.append('model', 'gpt-image-2'); + form.append('prompt', variant.prompt.join('\n')); + form.append('n', '1'); + form.append('size', '1024x1024'); + form.append( + 'image', + new Blob([imageBytes], { type: 'image/png' }), + path.basename(referenceImagePath), + ); + return form; +} + +async function generateOne(env, variant) { + const payload = await fetchJson( + buildEditUrl(env.baseUrl), + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.apiKey}`, + Accept: 'application/json', + }, + body: createEditFormData(variant), + }, + env.timeoutMs, + ); + + const urls = extractImageUrls(payload); + const b64Images = extractBase64Images(payload); + let image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join( + outputDir, + `taonier-hand-spirit-ref01-logo-refine-${variant.id}.${image.extension}`, + ); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join( + outputDir, + 'taonier-logo-hand-spirit-ref01-logo-refine-manifest.json', + ); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2', + endpoint: '/v1/images/edits', + size: '1024x1024', + referenceImage: path.relative(repoRoot, referenceImagePath), + generatedAt: new Date().toISOString(), + logoSkillSummary: { + requiredReview: + 'visual inspection, 32px readability, black-white viability', + outputStatus: 'AI concept only; final logo needs vector cleanup', + }, + brief: logoBrief, + variants: variants.map((variant) => { + const file = files.find((item) => + path.basename(item).includes(variant.id), + ); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + referenceImagePath, + count: selectedVariants.length, + brief: logoBrief, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + fields: buildDryRunFields(variant), + })), + }, + null, + 2, + ), + ); + process.exit(0); +} + +if (!existsSync(referenceImagePath)) { + console.error( + JSON.stringify({ + ok: false, + error: 'Reference image does not exist', + referenceImagePath, + }), + ); + process.exit(1); +} + +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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-hand-spirit-ref01-logo-refine-contact-sheet.py b/scripts/generate-taonier-hand-spirit-ref01-logo-refine-contact-sheet.py new file mode 100644 index 00000000..e4f1a1b0 --- /dev/null +++ b/scripts/generate-taonier-hand-spirit-ref01-logo-refine-contact-sheet.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-hand-spirit-ref01-logo-refine-concepts" +) +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-hand-spirit-ref01-logo-refine-contact-sheet.png" +REFERENCE_IMAGE = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-hand-spirit-concepts" + / "taonier-hand-spirit-01-gentle-hand-spirit.png" +) + +ITEMS = [ + ("REF 原01", REFERENCE_IMAGE), + ("01 扁平珊瑚奶白", "taonier-hand-spirit-ref01-logo-refine-01-flat-coral-cream"), + ("02 暖陶精品色", "taonier-hand-spirit-ref01-logo-refine-02-warm-clay-premium"), + ("03 青绿托举线", "taonier-hand-spirit-ref01-logo-refine-03-mint-support"), + ("04 线面商标", "taonier-hand-spirit-ref01-logo-refine-04-outline-vector"), + ("05 双形抽象", "taonier-hand-spirit-ref01-logo-refine-05-abstract-two-shape"), + ("06 黑白优先", "taonier-hand-spirit-ref01-logo-refine-06-monochrome-first"), + ("07 轻渐变精品", "taonier-hand-spirit-ref01-logo-refine-07-soft-gradient-premium"), + ("08 头像强识别", "taonier-hand-spirit-ref01-logo-refine-08-compact-avatar"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem_or_path: str | Path) -> Path | None: + if isinstance(stem_or_path, Path): + return stem_or_path if stem_or_path.exists() else None + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem_or_path}.{extension}" + if candidate.exists(): + return candidate + return None + + +def composite_on(image: Image.Image, color: str) -> Image.Image: + rgba = image.convert("RGBA") + background = Image.new("RGBA", rgba.size, color) + background.alpha_composite(rgba) + return background.convert("RGB") + + +def normalize_square(image_path: Path) -> Image.Image: + image = Image.open(image_path).convert("RGBA") + if image.size == (1024, 1024): + normalized = image + elif image.width == image.height: + normalized = image.resize((1024, 1024), Image.Resampling.LANCZOS) + else: + contained = ImageOps.contain(image, (1024, 1024), Image.Resampling.LANCZOS) + normalized = Image.new("RGBA", (1024, 1024), (255, 253, 248, 255)) + x = (1024 - contained.width) // 2 + y = (1024 - contained.height) // 2 + normalized.alpha_composite(contained, (x, y)) + + if image_path.is_relative_to(OUTPUT_DIR) and image.size != (1024, 1024): + normalized.convert("RGB").save(image_path, quality=95) + return composite_on(normalized, "#fffdf8") + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 268 + label_height = 54 + test_height = 44 + gap = 22 + columns = 3 + rows = 3 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(18) + test_font = load_font(13) + + for index, (label, stem_or_path) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem_or_path) + if image_path is None: + continue + + source = normalize_square(image_path) + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 52, test_y + 6)) + sheet.paste(mono, (x + 104, test_y + 6)) + draw.text((x + 150, test_y + 13), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-hands-logo-concepts.mjs b/scripts/generate-taonier-hands-logo-concepts.mjs new file mode 100644 index 00000000..c5eb7831 --- /dev/null +++ b/scripts/generate-taonier-hands-logo-concepts.mjs @@ -0,0 +1,315 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; + +const repoRoot = process.cwd(); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-hands-concepts', +); +const defaultTimeoutMs = 420000; + +const concepts = [ + { + id: 'taonier-hands-v2-cradle', + title: '掌心星核', + prompt: + '无文字扁平矢量 Logo,产品名“陶泥儿”。图形是上下两片抽象软掌,轻轻护住中央小星核,像把脑洞捏成作品。主流 App icon,简单、亲和、醒目、小尺寸清晰。珊瑚红、青绿、奶油白,最多 3 色。不要真实手指、播放三角、聊天气泡、笑脸、眼睛、花朵、褐色、文字、字母、3D。', + }, + { + id: 'taonier-hands-v2-clap', + title: '合掌成型', + prompt: + '无文字扁平矢量 Logo,产品名“陶泥儿”。用上下两片圆润软形表现合掌捏合,中间一个小圆点正在成型,表达 AI 把灵感变成小游戏作品。图形完整、现代、亲和、可记忆。珊瑚红、薄荷青、奶白,最多 3 色。不要真实手掌、聊天气泡、播放键、笑脸、眼睛、花朵、褐色、碎元素、3D、文字。', + }, + { + id: 'taonier-hands-v2-bowl', + title: '软掌托碗', + prompt: + '无文字扁平矢量 Logo,产品名“陶泥儿”。下方一片软掌像托碗,上方一片小软形轻压,中央浮出小星点,表达轻托脑洞、一捏成型。品牌感、主流、温暖、干净。青绿主形、珊瑚红辅助、奶白中心,最多 3 色。不要眼睛、嘴巴、聊天气泡、播放键、真实手、花朵、褐色、3D、文字。', + }, + { + id: 'taonier-hands-v2-seal', + title: '双掌印记', + prompt: + '无文字扁平矢量 Logo,产品名“陶泥儿”。两片抽象软掌上下扣合,形成一个圆润印记,中间留出小星形负空间,像双手捏出的创意标记。简洁、亲和、高识别、适合 App icon。珊瑚红、奶油白、青绿,最多 3 色。不要真实手指、宗教手势、医疗标识、聊天气泡、播放三角、眼睛、花朵、褐色、3D、文字。', + }, + { + id: 'taonier-hands-v2-pop', + title: '掌心开捏', + prompt: + '无文字扁平矢量 Logo,产品名“陶泥儿”。上下两片软掌像打开的胶囊,中央小星点从掌心弹出,表达脑洞被捏出来。年轻、亲和、醒目、主流娱乐创作 App 风格。亮珊瑚红、薄荷青、奶白,最多 3 色。不要聊天气泡、播放键、笑脸、眼睛、花朵、真实手指、褐色、碎元素、3D、文字。', + }, +]; + +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); + } +} + +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(outputDir, { recursive: true }); + const extension = inferExtensionFromBytes(bytes); + const outputPath = path.join(outputDir, `${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 selected = concepts.filter( + (concept) => !onlyIds.length || onlyIds.includes(concept.id), +); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + 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, + verifiedFiles: readdirSync(outputDir).sort(), + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-logo-brief-concepts.mjs b/scripts/generate-taonier-logo-brief-concepts.mjs new file mode 100644 index 00000000..744eab48 --- /dev/null +++ b/scripts/generate-taonier-logo-brief-concepts.mjs @@ -0,0 +1,444 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-brief-concepts', +); +const timeoutMsDefault = 180000; + +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 logoBrief = { + brand: '陶泥儿', + logoType: 'symbol/icon-only mark, no wordmark', + product: + 'AI UGC 轻休闲小游戏创作与传播平台,用户像捏陶泥一样把脑洞、梗和灵感塑造成作品', + personality: ['亲和', '精品', '创作感', '轻松', '年轻', '有传播记忆点'], + mustHave: [ + '闭合不规则几何底盘', + '外轮廓由流畅曲线组成', + '整体是一个完整符号而不是自由飘带', + '32px 仍能识别', + '黑白化后仍成立', + '无中文、无英文、无字标', + ], + avoid: [ + '整体方形或圆角方块', + '中心星星或任何星形', + '自由飘带、旋涡、S/G 字母感', + '巧克力面包、甜点、饼干、糖果等食物感', + '砖块、土块、泥饼、陶片、考古印章', + '脸、表情、吉祥物、手、工具', + ], +}; + +const basePrompt = [ + 'Create an icon-only brand logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', + 'Brand personality: friendly, premium, creative, lightweight, young, memorable, suitable for an AI UGC casual game creation platform.', + 'Core metaphor: users shape imagination like clay. The logo should communicate soft creative material becoming a refined digital product symbol.', + 'Logo type: abstract symbol/icon only. Not an emblem with text, not a mascot, not an app-icon rounded-square background.', + 'Main element: one closed irregular geometric base shape made from smooth flowing curves. The outer contour must be closed, continuous, recognizable, organic, and vector-friendly.', + 'The shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, or loose strip. It should feel like a refined custom symbol with 5-7 soft curve turns.', + 'Internal design: use only 1-2 broad smooth curve partitions or negative-shape cuts to make the mark memorable. No center icon inserted. No star, no spark, no hole shaped like a star.', + 'Style: modern minimalist vector logo with very subtle matte clay warmth. Clean edges, broad shapes, high contrast, no tiny details. It must look good and recognizable at 32px favicon size.', + 'Color: warm ceramic white, light terracotta, clay orange, warm brown, with optional small low-saturation teal, indigo-gray, or dark mud gray accent. Avoid sweet candy colors. No glossy highlights.', + 'Food avoidance is critical: the mark must not look like bread, chocolate bread, croissant, pastry, cookie, candy, donut, cream filling, sauce, baked dough, dessert, or food packaging.', + 'Material avoidance: do not make it look like brick, dirt clod, mud pie, pottery shard, stone, archaeological stamp, or rough handmade craft class object.', + 'Composition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.', + 'Validation targets: black-and-white version should still read clearly; the large shape should be recognizable at 32px.', +]; + +const variants = [ + { + id: '01-closed-curve-mark', + title: '闭合曲线标', + prompt: [ + ...basePrompt, + 'Variant focus: the cleanest closed irregular curve mark. Use two large color areas separated by one smooth internal curve. Maximize 32px readability.', + ], + }, + { + id: '02-friendly-geo-seed', + title: '亲和几何种', + prompt: [ + ...basePrompt, + 'Variant focus: a friendly seed-like closed geometric base, but not a literal seed, not food. Rounded and approachable with one teal accent curve.', + ], + }, + { + id: '03-premium-soft-contour', + title: '精品软轮廓', + prompt: [ + ...basePrompt, + 'Variant focus: premium, calm, fewer colors. Strong outer contour with a dark mud-gray internal negative curve. Very logo-like, not illustrative.', + ], + }, + { + id: '04-playful-closed-tile', + title: '轻玩闭合片', + prompt: [ + ...basePrompt, + 'Variant focus: a more playful closed irregular tile with warm terracotta and ceramic white. The internal curve should suggest creation flow, not filling.', + ], + }, + { + id: '05-monochrome-first', + title: '黑白优先', + prompt: [ + ...basePrompt, + 'Variant focus: design as if it will be converted to black and white. Use bold positive and negative shapes; color only supports the structure.', + ], + }, + { + id: '06-digital-clay-accent', + title: '数字陶泥点', + prompt: [ + ...basePrompt, + 'Variant focus: include at most two tiny geometric accent dots or notches that imply AI/UGC, but they must not look like candy sprinkles or decorative confetti.', + ], + }, + { + id: '07-compact-avatar-symbol', + title: '头像紧凑标', + prompt: [ + ...basePrompt, + 'Variant focus: compact social-avatar readability. The closed contour should be slightly fuller and more iconic, but not a rounded-square app background.', + ], + }, + { + id: '08-designer-vector-ready', + title: '矢量定稿感', + prompt: [ + ...basePrompt, + 'Variant focus: make it look like a designer-ready vector concept: 2-3 flat shapes, crisp boundaries, distinctive closed outer contour, minimal material texture.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join(outputDir, `taonier-logo-brief-${variant.id}.${image.extension}`); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join(outputDir, 'taonier-logo-brief-manifest.json'); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + logoSkillSummary: { + requiredReview: 'visual inspection, 32px readability, black-white viability', + outputStatus: 'AI concept only; final logo needs vector cleanup', + }, + brief: logoBrief, + variants: variants.map((variant) => { + const file = files.find((item) => path.basename(item).includes(variant.id)); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + brief: logoBrief, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-logo-brief-contact-sheet.py b/scripts/generate-taonier-logo-brief-contact-sheet.py new file mode 100644 index 00000000..99090f7e --- /dev/null +++ b/scripts/generate-taonier-logo-brief-contact-sheet.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-brief-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-brief-contact-sheet.png" + +ITEMS = [ + ("01 闭合曲线标", "taonier-logo-brief-01-closed-curve-mark"), + ("02 亲和几何种", "taonier-logo-brief-02-friendly-geo-seed"), + ("03 精品软轮廓", "taonier-logo-brief-03-premium-soft-contour"), + ("04 轻玩闭合片", "taonier-logo-brief-04-playful-closed-tile"), + ("05 黑白优先", "taonier-logo-brief-05-monochrome-first"), + ("06 数字陶泥点", "taonier-logo-brief-06-digital-clay-accent"), + ("07 头像紧凑标", "taonier-logo-brief-07-compact-avatar-symbol"), + ("08 矢量定稿感", "taonier-logo-brief-08-designer-vector-ready"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 300 + label_height = 58 + test_height = 46 + gap = 24 + columns = 4 + rows = 2 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#ede8de") + draw = ImageDraw.Draw(sheet) + label_font = load_font(20) + test_font = load_font(14) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fbfaf6", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f4f1ea", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 68, test_y + 7)) + sheet.paste(mono, (x + 122, test_y + 7)) + draw.text((x + 166, test_y + 14), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-mascot-symbol-contact-sheet.py b/scripts/generate-taonier-mascot-symbol-contact-sheet.py new file mode 100644 index 00000000..5a295064 --- /dev/null +++ b/scripts/generate-taonier-mascot-symbol-contact-sheet.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-mascot-symbol-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-mascot-symbol-contact-sheet.png" + +ITEMS = [ + ("01 抽象人形", "taonier-mascot-symbol-01-clay-humanoid"), + ("02 陶泥精灵", "taonier-mascot-symbol-02-clay-sprite"), + ("03 软萌怪物", "taonier-mascot-symbol-03-soft-monster"), + ("04 抽象动物", "taonier-mascot-symbol-04-animal-abstract"), + ("05 泥团小灵", "taonier-mascot-symbol-05-clay-orb-being"), + ("06 轻玩小怪", "taonier-mascot-symbol-06-playful-creature"), + ("07 头像可读", "taonier-mascot-symbol-07-avatar-readable"), + ("08 矢量符号感", "taonier-mascot-symbol-08-vector-ready"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 300 + label_height = 58 + test_height = 46 + gap = 24 + columns = 4 + rows = 2 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(20) + test_font = load_font(14) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 68, test_y + 7)) + sheet.paste(mono, (x + 122, test_y + 7)) + draw.text((x + 166, test_y + 14), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-mascot-symbol-logo-concepts.mjs b/scripts/generate-taonier-mascot-symbol-logo-concepts.mjs new file mode 100644 index 00000000..99013634 --- /dev/null +++ b/scripts/generate-taonier-mascot-symbol-logo-concepts.mjs @@ -0,0 +1,448 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-mascot-symbol-concepts', +); +const timeoutMsDefault = 180000; + +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 logoBrief = { + brand: '陶泥儿', + coreBelief: '好玩会创造', + logoType: 'symbol/icon-only mascot mark, no wordmark', + product: + 'AI UGC 轻休闲小游戏创作与传播平台,用户像捏陶泥一样把脑洞、梗和灵感塑造成可分享的作品', + direction: '抽象吉祥物符号,从人形、精灵、怪物、动物等形态提炼', + audience: '女性用户友好、全年龄向、年轻明亮但不低幼', + mascotRules: [ + '必须是 logo 符号级别,不是完整角色立绘', + '轮廓要有记忆点,32px 可读', + '表情最多极简两点或无表情', + '身体结构要高度抽象、可矢量化', + '保留一点陶泥被捏出的柔软感', + ], + avoid: [ + '复杂角色', + '儿童玩具感', + '怪物恐怖感', + '真实动物', + '食品感', + '文字', + '星星', + '写实泥土纹理', + ], +}; + +const basePrompt = [ + 'Create an icon-only mascot logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', + 'Brand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works and share them with others.', + 'Logo type: abstract mascot symbol only. It must be a simple brand mark, not a full character illustration, not a scene, not a sticker sheet, not an app-icon rounded-square background.', + 'Mascot meaning: a soft clay-born little being that represents playful creation, shareable ideas, and a friendly AI/UGC companion.', + 'Style: modern minimalist vector mascot mark, compact silhouette, broad simple shapes, fresh bright palette, very light clay warmth only. It must look good and recognizable at 32px favicon size.', + 'Character abstraction: use a highly simplified head/body silhouette or creature glyph. No detailed anatomy. No clothing. No props. No complex limbs. Optional face should be extremely minimal, at most two small eyes; no mouth unless it is a tiny simple mark.', + 'Shape language: soft, rounded, slightly irregular, as if gently pinched from clay, but clean and designer-ready. Friendly and memorable, not childish.', + 'Color direction: women-friendly and all-age fresh palette: coral orange, peach pink, cream white, clear soft teal or mint green. Use flat brand-color shapes, not candy rendering.', + 'Avoid realistic clay texture, dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.', + 'Food avoidance: do not make it look like bread, mochi, dumpling, cake, cookie, candy, jelly, donut, dessert, or food packaging.', + 'Avoid scary monster, aggressive teeth, claws, realistic animal, pet-shop icon, emoji face, toy mascot, chibi over-detailing, or generic blob.', + 'No star, no spark, no halo, no magic wand, no hand, no pottery tool, no UI, no border, no watermark.', + 'Composition: centered on a clean light background, generous safe area. Use simple readable silhouette first.', + 'Validation targets: black-and-white version should still read clearly; the mascot silhouette should be recognizable at 32px.', +]; + +const variants = [ + { + id: '01-clay-humanoid', + title: '抽象人形', + prompt: [ + ...basePrompt, + 'Variant focus: abstract humanoid mascot. A tiny soft clay person-like glyph with rounded head and merged body, no limbs or very minimal arms, friendly but not childish.', + ], + }, + { + id: '02-clay-sprite', + title: '陶泥精灵', + prompt: [ + ...basePrompt, + 'Variant focus: clay sprite. A small semi-dome spirit with a gentle lifted silhouette, like a friendly creative helper, no wings, no magic stars, no fantasy clutter.', + ], + }, + { + id: '03-soft-monster', + title: '软萌怪物', + prompt: [ + ...basePrompt, + 'Variant focus: soft friendly monster glyph. Cute but not scary, no teeth, no claws, one distinctive head shape, perhaps tiny horn-like soft bumps but not devilish.', + ], + }, + { + id: '04-animal-abstract', + title: '抽象动物', + prompt: [ + ...basePrompt, + 'Variant focus: abstract animal-like mascot. Suggest a small rounded creature through ears or tail-like curves, but not a specific real animal and not pet logo.', + ], + }, + { + id: '05-clay-orb-being', + title: '泥团小灵', + prompt: [ + ...basePrompt, + 'Variant focus: orb-like clay being. A simple irregular rounded body with minimal face or no face, strong silhouette, playful creation companion.', + ], + }, + { + id: '06-playful-creature', + title: '轻玩小怪', + prompt: [ + ...basePrompt, + 'Variant focus: more playful creature mark. Dynamic but compact, one distinctive asymmetric curve, readable at 32px, still premium and not a toy brand.', + ], + }, + { + id: '07-avatar-readable', + title: '头像可读', + prompt: [ + ...basePrompt, + 'Variant focus: social avatar and favicon readability. Bold compact mascot head/body silhouette, minimal inner detail, high black-and-white clarity.', + ], + }, + { + id: '08-vector-ready', + title: '矢量符号感', + prompt: [ + ...basePrompt, + 'Variant focus: designer-ready vector mascot concept. 2-3 flat shapes, crisp boundaries, distinctive silhouette, minimal material cue, no illustration shading.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join(outputDir, `taonier-mascot-symbol-${variant.id}.${image.extension}`); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join(outputDir, 'taonier-logo-mascot-symbol-manifest.json'); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + logoSkillSummary: { + requiredReview: 'visual inspection, 32px readability, black-white viability', + outputStatus: 'AI concept only; final logo needs vector cleanup', + }, + brief: logoBrief, + variants: variants.map((variant) => { + const file = files.find((item) => path.basename(item).includes(variant.id)); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + brief: logoBrief, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-pair-ears-jar-contact-sheet.py b/scripts/generate-taonier-pair-ears-jar-contact-sheet.py new file mode 100644 index 00000000..4e665ce8 --- /dev/null +++ b/scripts/generate-taonier-pair-ears-jar-contact-sheet.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-pair-ears-jar-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-pair-ears-jar-contact-sheet.png" + +ITEMS = [ + ("01 兔耳陶罐", "taonier-pair-ears-jar-01-rabbit-jar"), + ("02 猫耳陶罐", "taonier-pair-ears-jar-02-cat-jar"), + ("03 狐耳陶罐", "taonier-pair-ears-jar-03-fox-jar"), + ("04 熊耳陶罐", "taonier-pair-ears-jar-04-bear-jar"), + ("05 狗耳陶罐", "taonier-pair-ears-jar-05-dog-jar"), + ("06 双耳组合", "taonier-pair-ears-jar-06-dual-ears"), + ("07 高罐长耳", "taonier-pair-ears-jar-07-tall-jar"), + ("08 商标定稿感", "taonier-pair-ears-jar-08-jar-mark"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 300 + label_height = 58 + test_height = 46 + gap = 24 + columns = 4 + rows = 2 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(20) + test_font = load_font(14) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 68, test_y + 7)) + sheet.paste(mono, (x + 122, test_y + 7)) + draw.text((x + 166, test_y + 14), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-pair-ears-jar-logo-concepts.mjs b/scripts/generate-taonier-pair-ears-jar-logo-concepts.mjs new file mode 100644 index 00000000..2b04cec7 --- /dev/null +++ b/scripts/generate-taonier-pair-ears-jar-logo-concepts.mjs @@ -0,0 +1,450 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-pair-ears-jar-concepts', +); +const timeoutMsDefault = 180000; + +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 logoBrief = { + brand: '陶泥儿', + coreBelief: '好玩会创造', + logoType: 'symbol/icon-only mascot mark, no wordmark', + product: + 'AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品', + direction: + '陶罐容器 + 只露出动物耳朵的小灵体,神秘又可爱,罐子无表情', + audience: '女性用户友好、全年龄向、年轻明亮但不低幼', + structureRules: [ + '主形体是一个陶罐或陶罐容器,强调器皿感和包裹感', + '罐中只露出耳朵,不露完整脸部,不露完整身体', + '耳朵可以是兔、猫、狐狸、熊、狗等动物耳朵,但只能露耳朵', + '罐子可以带短手短脚,但不是必须;若有,也要极简抽象', + '整体必须是 logo 符号级别,不是完整插画角色', + ], + avoid: [ + '中文或英文字', + '表情元素', + '星星或闪光', + '手托举元素', + '写实陶瓷高光', + '脏泥土或砖块', + '面团、汤圆、甜点、面包、巧克力、糖果、布丁', + '完整动物脸', + '恐怖怪物、牙齿、爪子', + ], +}; + +const basePrompt = [ + 'Create an icon-only mascot logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', + 'Core idea: a ceramic jar container with only animal ears peeking out from inside. The jar should feel mysterious and cute at the same time. Do not show a full face or full body, only ears.', + 'Brand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.', + 'Logo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.', + 'Main structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.', + 'Hidden animal presence: only the ears emerge from the jar opening or upper body. The ears may be rabbit, cat, fox, bear, or dog ears depending on the variant, but no face, no eyes, no nose, no mouth, no full head. The ears must feel playful and alive, but still simple and logo-friendly.', + 'Optional tiny feet or short hand nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.', + 'Style: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.', + 'Jar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Ear color direction: use natural animal colors that fit the variant, but keep them soft and not neon.', + 'Avoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.', + 'Food avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.', + 'Avoid any jar face or expression. No eyes, no smile, no blush, no emoji marks on the jar.', + 'Avoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.', + 'Composition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.', +]; + +const variants = [ + { + id: '01-rabbit-jar', + title: '兔耳陶罐', + prompt: [ + ...basePrompt, + 'Variant focus: rabbit ears. Long soft rabbit ears rise from the jar opening with a gentle curve, while the jar remains compact and premium.', + ], + }, + { + id: '02-cat-jar', + title: '猫耳陶罐', + prompt: [ + ...basePrompt, + 'Variant focus: cat ears. Small pointed cat ears peek from the jar opening, giving a slightly sly but still very cute feeling.', + ], + }, + { + id: '03-fox-jar', + title: '狐耳陶罐', + prompt: [ + ...basePrompt, + 'Variant focus: fox ears. Slender fox-like ears with a warm orange accent, a little more clever and playful than the rabbit version.', + ], + }, + { + id: '04-bear-jar', + title: '熊耳陶罐', + prompt: [ + ...basePrompt, + 'Variant focus: bear ears. Two small rounded bear ears emerging from the top, very soft and sleepy, with a sturdy jar silhouette.', + ], + }, + { + id: '05-dog-jar', + title: '狗耳陶罐', + prompt: [ + ...basePrompt, + 'Variant focus: dog ears. Slightly floppy dog ears peeking from the vessel, friendly and lively, but still only ears, no face.', + ], + }, + { + id: '06-dual-ears', + title: '双耳组合', + prompt: [ + ...basePrompt, + 'Variant focus: two different ear shapes on one jar, such as one rabbit ear and one cat ear, but still harmonized into a single mascot symbol.', + ], + }, + { + id: '07-tall-jar', + title: '高罐长耳', + prompt: [ + ...basePrompt, + 'Variant focus: taller jar silhouette with more vertical ears, so the ear read is clearer at favicon size and the vessel feels more iconic.', + ], + }, + { + id: '08-jar-mark', + title: '商标定稿感', + prompt: [ + ...basePrompt, + 'Variant focus: strongest trademark readability. Use a compact jar silhouette, very simple ears, minimal details, excellent black-and-white legibility.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join(outputDir, `taonier-pair-ears-jar-${variant.id}.${image.extension}`); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join(outputDir, 'taonier-logo-pair-ears-jar-manifest.json'); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + logoSkillSummary: { + requiredReview: 'visual inspection, 32px readability, black-white viability', + outputStatus: 'AI concept only; final logo needs vector cleanup', + }, + brief: logoBrief, + variants: variants.map((variant) => { + const file = files.find((item) => path.basename(item).includes(variant.id)); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + brief: logoBrief, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-peeking-head-jar-blackdot-eye-contact-sheet.py b/scripts/generate-taonier-peeking-head-jar-blackdot-eye-contact-sheet.py new file mode 100644 index 00000000..1ea44fb7 --- /dev/null +++ b/scripts/generate-taonier-peeking-head-jar-blackdot-eye-contact-sheet.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-peeking-head-jar-blackdot-eye-concepts" +) +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-peeking-head-jar-blackdot-eye-contact-sheet.png" + +ITEMS = [ + ("01 陶粉兔头", "taonier-peeking-head-jar-blackdot-eye-01-clay-rabbit"), + ("02 灰陶猫头", "taonier-peeking-head-jar-blackdot-eye-02-ash-cat"), + ("03 陶红狐头", "taonier-peeking-head-jar-blackdot-eye-03-terracotta-fox"), + ("04 横纹熊头", "taonier-peeking-head-jar-blackdot-eye-04-striped-bear"), + ("05 长颈狗头", "taonier-peeking-head-jar-blackdot-eye-05-long-neck-dog"), + ("06 低矮鼠头", "taonier-peeking-head-jar-blackdot-eye-06-low-mouse"), + ("07 偏心鹿头", "taonier-peeking-head-jar-blackdot-eye-07-asym-deer"), + ("08 紧凑熊猫", "taonier-peeking-head-jar-blackdot-eye-08-compact-panda"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 300 + label_height = 58 + test_height = 46 + gap = 24 + columns = 4 + rows = 2 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(20) + test_font = load_font(14) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 68, test_y + 7)) + sheet.paste(mono, (x + 122, test_y + 7)) + draw.text((x + 166, test_y + 14), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-peeking-head-jar-blackdot-eye-logo-concepts.mjs b/scripts/generate-taonier-peeking-head-jar-blackdot-eye-logo-concepts.mjs new file mode 100644 index 00000000..e23c39ac --- /dev/null +++ b/scripts/generate-taonier-peeking-head-jar-blackdot-eye-logo-concepts.mjs @@ -0,0 +1,464 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-peeking-head-jar-blackdot-eye-concepts', +); +const timeoutMsDefault = 180000; + +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 logoBrief = { + brand: '陶泥儿', + coreBelief: '好玩会创造', + logoType: 'symbol/icon-only mascot mark, no wordmark', + product: + 'AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品', + direction: + '在保持“半头探出”的节奏下继续拓展,但眼睛必须是纯黑点无高光', + audience: '女性用户友好、全年龄向、年轻明亮但不低幼', + structureRules: [ + '主形体仍然是陶罐容器,罐子负责陶器和包裹感', + '动物只露出耳朵、上半个脑袋和两只黑点眼睛', + '眼睛不能有高光、不能有白点反光、不能有玻璃感', + '不露鼻子、嘴巴、身体、爪子或完整动物脸', + '罐子绝对不能有表情元素', + ], + avoid: [ + '中文或英文字', + '罐子表情', + '动物嘴巴或鼻子', + '眼睛高光', + '白眼球高光', + '星星或闪光', + '手托举元素', + '写实陶瓷高光', + '脏泥土或砖块', + '面团、汤圆、甜点、面包、巧克力、糖果、布丁', + '完整动物身体', + '恐怖怪物、牙齿、爪子', + ], +}; + +const basePrompt = [ + 'Create an icon-only mascot logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', + 'Core idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.', + 'The jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.', + 'Brand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.', + 'Logo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.', + 'Main structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.', + 'Animal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.', + 'Eye policy: the eyes must be pure black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.', + 'Jar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.', + 'Style: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.', + 'Jar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.', + 'Animal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.', + 'Avoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.', + 'Food avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.', + 'Avoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.', + 'Composition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.', +]; + +const variants = [ + { + id: '01-clay-rabbit', + title: '陶粉兔头', + prompt: [ + ...basePrompt, + 'Variant focus: tall slim clay jar with rabbit ears and a cream rabbit head. Use pale clay beige jar and soft peach ear interiors, eyes are black dots only.', + ], + }, + { + id: '02-ash-cat', + title: '灰陶猫头', + prompt: [ + ...basePrompt, + 'Variant focus: squat ash-clay jar with cat ears and a gray-white cat head. Use muted ash beige jar and compact triangular ears, eyes are black dots only.', + ], + }, + { + id: '03-terracotta-fox', + title: '陶红狐头', + prompt: [ + ...basePrompt, + 'Variant focus: flared terracotta jar with fox ears and orange fox head. Use muted terracotta jar, cream face area, and warm orange ear tips, eyes are black dots only.', + ], + }, + { + id: '04-striped-bear', + title: '横纹熊头', + prompt: [ + ...basePrompt, + 'Variant focus: jar with subtle ceramic stripe bands, bear ears, and a cocoa-brown bear head. The eyes remain black dots only, no extra facial marks.', + ], + }, + { + id: '05-long-neck-dog', + title: '长颈狗头', + prompt: [ + ...basePrompt, + 'Variant focus: long-neck jar with floppy dog ears and a tan dog head. Use a soft gray-beige jar and warm tan ears, eyes are black dots only.', + ], + }, + { + id: '06-low-mouse', + title: '低矮鼠头', + prompt: [ + ...basePrompt, + 'Variant focus: low wide jar with mouse ears and a pale gray mouse head. Add tiny pink ear interiors, eyes are black dots only, cute and slightly mischievous.', + ], + }, + { + id: '07-asym-deer', + title: '偏心鹿头', + prompt: [ + ...basePrompt, + 'Variant focus: slightly asymmetrical jar with deer ears and a soft brown deer head. Keep the ears upright and slender, with a calm cream-beige jar, eyes are black dots only.', + ], + }, + { + id: '08-compact-panda', + title: '紧凑熊猫', + prompt: [ + ...basePrompt, + 'Variant focus: compact trademark jar with panda ears and a black-and-cream panda head. Keep the silhouette bold and simple for strongest brand recognition, eyes are black dots only.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join( + outputDir, + `taonier-peeking-head-jar-blackdot-eye-${variant.id}.${image.extension}`, + ); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join( + outputDir, + 'taonier-logo-peeking-head-jar-blackdot-eye-manifest.json', + ); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + logoSkillSummary: { + requiredReview: + 'visual inspection, 32px readability, black-white viability', + outputStatus: 'AI concept only; final logo needs vector cleanup', + }, + brief: logoBrief, + variants: variants.map((variant) => { + const file = files.find((item) => + path.basename(item).includes(variant.id), + ); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + brief: logoBrief, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-peeking-head-jar-broad-contact-sheet.py b/scripts/generate-taonier-peeking-head-jar-broad-contact-sheet.py new file mode 100644 index 00000000..0a5aa7cd --- /dev/null +++ b/scripts/generate-taonier-peeking-head-jar-broad-contact-sheet.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-peeking-head-jar-broad-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-peeking-head-jar-broad-contact-sheet.png" + +ITEMS = [ + ("01 橄榄兔头", "taonier-peeking-head-jar-broad-01-olive-rabbit"), + ("02 砂陶猫头", "taonier-peeking-head-jar-broad-02-sand-cat"), + ("03 杏陶狐头", "taonier-peeking-head-jar-broad-03-apricot-fox"), + ("04 双带熊头", "taonier-peeking-head-jar-broad-04-banded-bear"), + ("05 细颈狗头", "taonier-peeking-head-jar-broad-05-necked-dog"), + ("06 扁罐鼠头", "taonier-peeking-head-jar-broad-06-flat-mouse"), + ("07 斜肩鹿头", "taonier-peeking-head-jar-broad-07-tilted-deer"), + ("08 紧凑熊猫", "taonier-peeking-head-jar-broad-08-compact-panda"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 300 + label_height = 58 + test_height = 46 + gap = 24 + columns = 4 + rows = 2 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(20) + test_font = load_font(14) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 68, test_y + 7)) + sheet.paste(mono, (x + 122, test_y + 7)) + draw.text((x + 166, test_y + 14), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-peeking-head-jar-broad-logo-concepts.mjs b/scripts/generate-taonier-peeking-head-jar-broad-logo-concepts.mjs new file mode 100644 index 00000000..55f4f9dd --- /dev/null +++ b/scripts/generate-taonier-peeking-head-jar-broad-logo-concepts.mjs @@ -0,0 +1,464 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-peeking-head-jar-broad-concepts', +); +const timeoutMsDefault = 180000; + +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 logoBrief = { + brand: '陶泥儿', + coreBelief: '好玩会创造', + logoType: 'symbol/icon-only mascot mark, no wordmark', + product: + 'AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品', + direction: + '保持“半头探出”节奏,继续拓展更丰富的罐型、配色和动物类别,眼睛仍是黑点', + audience: '女性用户友好、全年龄向、年轻明亮但不低幼', + structureRules: [ + '主形体仍然是陶罐容器,罐子负责陶器和包裹感', + '动物只露出耳朵、上半个脑袋和两只黑点眼睛', + '眼睛不能有高光、不能有白点反光、不能有玻璃感', + '不露鼻子、嘴巴、身体、爪子或完整动物脸', + '罐子绝对不能有表情元素', + ], + avoid: [ + '中文或英文字', + '罐子表情', + '动物嘴巴或鼻子', + '眼睛高光', + '白眼球高光', + '星星或闪光', + '手托举元素', + '写实陶瓷高光', + '脏泥土或砖块', + '面团、汤圆、甜点、面包、巧克力、糖果、布丁', + '完整动物身体', + '恐怖怪物、牙齿、爪子', + ], +}; + +const basePrompt = [ + 'Create an icon-only mascot logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', + 'Core idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.', + 'The jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.', + 'Brand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.', + 'Logo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.', + 'Main structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.', + 'Animal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.', + 'Eye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.', + 'Jar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.', + 'Style: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.', + 'Jar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.', + 'Animal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.', + 'Avoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.', + 'Food avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.', + 'Avoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.', + 'Composition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.', +]; + +const variants = [ + { + id: '01-olive-rabbit', + title: '橄榄兔头', + prompt: [ + ...basePrompt, + 'Variant focus: tall slim jar with a muted olive-clay body and rabbit ears. The rabbit head is cream colored with soft peach inner ears, eyes are black dots only.', + ], + }, + { + id: '02-sand-cat', + title: '砂陶猫头', + prompt: [ + ...basePrompt, + 'Variant focus: squat sand-colored jar with cat ears and a gray-white cat head. Make the rim compact and the body broad, eyes are black dots only.', + ], + }, + { + id: '03-apricot-fox', + title: '杏陶狐头', + prompt: [ + ...basePrompt, + 'Variant focus: flared apricot-terracotta jar with fox ears and a warm orange fox head. Use a cream face area and strong ear silhouette, eyes are black dots only.', + ], + }, + { + id: '04-banded-bear', + title: '双带熊头', + prompt: [ + ...basePrompt, + 'Variant focus: jar with two subtle ceramic bands, bear ears, and a cocoa-brown bear head. Keep the vessel sturdy and broad, eyes are black dots only.', + ], + }, + { + id: '05-necked-dog', + title: '细颈狗头', + prompt: [ + ...basePrompt, + 'Variant focus: tall narrow-neck jar with floppy dog ears and a tan dog head. Use a warm gray-beige jar and slightly longer ear shapes, eyes are black dots only.', + ], + }, + { + id: '06-flat-mouse', + title: '扁罐鼠头', + prompt: [ + ...basePrompt, + 'Variant focus: low flat jar with mouse ears and a pale gray mouse head. Add tiny pink ear interiors and a wider mouth rim, eyes are black dots only.', + ], + }, + { + id: '07-tilted-deer', + title: '斜肩鹿头', + prompt: [ + ...basePrompt, + 'Variant focus: slightly tilted jar with deer ears and a soft brown deer head. Use a calm cream-beige jar with a subtle shoulder shift, eyes are black dots only.', + ], + }, + { + id: '08-compact-panda', + title: '紧凑熊猫', + prompt: [ + ...basePrompt, + 'Variant focus: compact trademark jar with panda ears and a black-and-cream panda head. Keep the silhouette bold, simple, and easy to read at 32px, eyes are black dots only.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join( + outputDir, + `taonier-peeking-head-jar-broad-${variant.id}.${image.extension}`, + ); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join( + outputDir, + 'taonier-logo-peeking-head-jar-broad-manifest.json', + ); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + logoSkillSummary: { + requiredReview: + 'visual inspection, 32px readability, black-white viability', + outputStatus: 'AI concept only; final logo needs vector cleanup', + }, + brief: logoBrief, + variants: variants.map((variant) => { + const file = files.find((item) => + path.basename(item).includes(variant.id), + ); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + brief: logoBrief, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-peeking-head-jar-contact-sheet.py b/scripts/generate-taonier-peeking-head-jar-contact-sheet.py new file mode 100644 index 00000000..1f8a3427 --- /dev/null +++ b/scripts/generate-taonier-peeking-head-jar-contact-sheet.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-peeking-head-jar-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-peeking-head-jar-contact-sheet.png" + +ITEMS = [ + ("01 兔头探出", "taonier-peeking-head-jar-01-rabbit-peek"), + ("02 猫头探出", "taonier-peeking-head-jar-02-cat-peek"), + ("03 狐头探出", "taonier-peeking-head-jar-03-fox-peek"), + ("04 熊头探出", "taonier-peeking-head-jar-04-bear-peek"), + ("05 狗头探出", "taonier-peeking-head-jar-05-dog-peek"), + ("06 混合小灵", "taonier-peeking-head-jar-06-mixed-peek"), + ("07 高罐探头", "taonier-peeking-head-jar-07-tall-peek"), + ("08 商标探头", "taonier-peeking-head-jar-08-trademark-peek"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 300 + label_height = 58 + test_height = 46 + gap = 24 + columns = 4 + rows = 2 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(20) + test_font = load_font(14) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 68, test_y + 7)) + sheet.paste(mono, (x + 122, test_y + 7)) + draw.text((x + 166, test_y + 14), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-peeking-head-jar-expanded-contact-sheet.py b/scripts/generate-taonier-peeking-head-jar-expanded-contact-sheet.py new file mode 100644 index 00000000..2d043043 --- /dev/null +++ b/scripts/generate-taonier-peeking-head-jar-expanded-contact-sheet.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-peeking-head-jar-expanded-concepts" +) +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-peeking-head-jar-expanded-contact-sheet.png" + +ITEMS = [ + ("01 高罐兔耳", "taonier-peeking-head-jar-expanded-01-tall-rabbit"), + ("02 矮罐猫头", "taonier-peeking-head-jar-expanded-02-squat-cat"), + ("03 阔口狐头", "taonier-peeking-head-jar-expanded-03-flared-fox"), + ("04 双圈熊头", "taonier-peeking-head-jar-expanded-04-double-band-bear"), + ("05 长颈狗头", "taonier-peeking-head-jar-expanded-05-long-neck-dog"), + ("06 低矮鼠头", "taonier-peeking-head-jar-expanded-06-low-mouse"), + ("07 偏心鹿头", "taonier-peeking-head-jar-expanded-07-asym-deer"), + ("08 紧凑熊猫", "taonier-peeking-head-jar-expanded-08-compact-panda"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 300 + label_height = 58 + test_height = 46 + gap = 24 + columns = 4 + rows = 2 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(20) + test_font = load_font(14) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 68, test_y + 7)) + sheet.paste(mono, (x + 122, test_y + 7)) + draw.text((x + 166, test_y + 14), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-peeking-head-jar-expanded-logo-concepts.mjs b/scripts/generate-taonier-peeking-head-jar-expanded-logo-concepts.mjs new file mode 100644 index 00000000..622ab03f --- /dev/null +++ b/scripts/generate-taonier-peeking-head-jar-expanded-logo-concepts.mjs @@ -0,0 +1,461 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-peeking-head-jar-expanded-concepts', +); +const timeoutMsDefault = 180000; + +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 logoBrief = { + brand: '陶泥儿', + coreBelief: '好玩会创造', + logoType: 'symbol/icon-only mascot mark, no wordmark', + product: + 'AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品', + direction: + '在保持“半头探出”的节奏下,拓展罐型、口沿、底座、动物原色和耳型搭配', + audience: '女性用户友好、全年龄向、年轻明亮但不低幼', + structureRules: [ + '主形体仍然是陶罐容器,罐子负责陶器和包裹感', + '动物只露出耳朵、上半个脑袋和两只眼睛', + '不露鼻子、嘴巴、身体、爪子或完整动物脸', + '罐子绝对不能有表情元素', + '整体必须是 logo 符号级别,不是完整插画角色', + ], + avoid: [ + '中文或英文字', + '罐子表情', + '动物嘴巴或鼻子', + '星星或闪光', + '手托举元素', + '写实陶瓷高光', + '脏泥土或砖块', + '面团、汤圆、甜点、面包、巧克力、糖果、布丁', + '完整动物身体', + '恐怖怪物、牙齿、爪子', + ], +}; + +const basePrompt = [ + 'Create an icon-only mascot logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', + 'Core idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.', + 'The jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.', + 'Brand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.', + 'Logo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.', + 'Main structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.', + 'Animal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.', + 'Jar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.', + 'Style: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.', + 'Jar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, or cool ash clay. Each variant should shift the jar silhouette and color slightly.', + 'Animal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.', + 'Avoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.', + 'Food avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.', + 'Avoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.', + 'Composition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.', +]; + +const variants = [ + { + id: '01-tall-rabbit', + title: '高罐兔耳', + prompt: [ + ...basePrompt, + 'Variant focus: tall slim jar with soft rabbit ears and a cream rabbit head peeking out. Use pale beige jar and soft peach inner ears, elegant and light.', + ], + }, + { + id: '02-squat-cat', + title: '矮罐猫头', + prompt: [ + ...basePrompt, + 'Variant focus: squat round jar with cat ears and a gray-white cat head. Use a warmer taupe jar and small triangular ears, compact and cozy.', + ], + }, + { + id: '03-flared-fox', + title: '阔口狐头', + prompt: [ + ...basePrompt, + 'Variant focus: flared-rim jar with fox ears and orange fox head. Use muted terracotta jar, cream face area, and warm orange ear tips.', + ], + }, + { + id: '04-double-band-bear', + title: '双圈熊头', + prompt: [ + ...basePrompt, + 'Variant focus: double-band jar with bear ears and a cocoa-brown bear head. The jar can have two subtle horizontal rings for ceramic rhythm.', + ], + }, + { + id: '05-long-neck-dog', + title: '长颈狗头', + prompt: [ + ...basePrompt, + 'Variant focus: long-neck jar with floppy dog ears and a tan dog head. Use a soft gray-beige jar and warm tan ears, friendly and upright.', + ], + }, + { + id: '06-low-mouse', + title: '低矮鼠头', + prompt: [ + ...basePrompt, + 'Variant focus: low wide jar with mouse ears and a pale gray mouse head. Add tiny pink ear interiors, cute and slightly mischievous.', + ], + }, + { + id: '07-asym-deer', + title: '偏心鹿头', + prompt: [ + ...basePrompt, + 'Variant focus: slightly asymmetrical jar with deer ears and a soft brown deer head. Keep the ears upright and slender, with a calm cream-beige jar.', + ], + }, + { + id: '08-compact-panda', + title: '紧凑熊猫', + prompt: [ + ...basePrompt, + 'Variant focus: compact trademark jar with panda ears and a black-and-cream panda head. Keep the silhouette bold and simple for strongest brand recognition.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join( + outputDir, + `taonier-peeking-head-jar-expanded-${variant.id}.${image.extension}`, + ); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join( + outputDir, + 'taonier-logo-peeking-head-jar-expanded-manifest.json', + ); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + logoSkillSummary: { + requiredReview: + 'visual inspection, 32px readability, black-white viability', + outputStatus: 'AI concept only; final logo needs vector cleanup', + }, + brief: logoBrief, + variants: variants.map((variant) => { + const file = files.find((item) => + path.basename(item).includes(variant.id), + ); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + brief: logoBrief, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-peeking-head-jar-logo-concepts.mjs b/scripts/generate-taonier-peeking-head-jar-logo-concepts.mjs new file mode 100644 index 00000000..d568ff0f --- /dev/null +++ b/scripts/generate-taonier-peeking-head-jar-logo-concepts.mjs @@ -0,0 +1,452 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-peeking-head-jar-concepts', +); +const timeoutMsDefault = 180000; + +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 logoBrief = { + brand: '陶泥儿', + coreBelief: '好玩会创造', + logoType: 'symbol/icon-only mascot mark, no wordmark', + product: + 'AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品', + direction: + '陶罐容器 + 小动物从罐中露出耳朵、半个脑袋和眼睛,神秘又可爱', + audience: '女性用户友好、全年龄向、年轻明亮但不低幼', + structureRules: [ + '主形体仍然是陶罐容器,罐子负责陶器和包裹感', + '动物只露出耳朵、上半个脑袋和两只眼睛', + '不露鼻子、嘴巴、身体、爪子或完整动物脸', + '罐子绝对不能有表情元素', + '整体必须是 logo 符号级别,不是完整插画角色', + ], + avoid: [ + '中文或英文字', + '罐子表情', + '动物嘴巴或鼻子', + '星星或闪光', + '手托举元素', + '写实陶瓷高光', + '脏泥土或砖块', + '面团、汤圆、甜点、面包、巧克力、糖果、布丁', + '完整动物身体', + '恐怖怪物、牙齿、爪子', + ], +}; + +const basePrompt = [ + 'Create an icon-only mascot logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', + 'Core idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple eyes. Do not show nose, mouth, lower face, paws, body, or full head.', + 'The jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.', + 'Brand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.', + 'Logo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.', + 'Main structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.', + 'Animal visibility: show only ears, top half of head, and two eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.', + 'Jar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.', + 'Optional tiny jar feet or short side nubs are allowed only if they help the silhouette; keep them extremely abstract and minimal.', + 'Style: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.', + 'Jar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, or smoky cream. Animal head and ears should use soft natural animal colors that fit the variant.', + 'Avoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.', + 'Food avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.', + 'Avoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.', + 'Composition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.', +]; + +const variants = [ + { + id: '01-rabbit-peek', + title: '兔头探出', + prompt: [ + ...basePrompt, + 'Variant focus: rabbit. Long soft rabbit ears, upper half of rabbit head and two simple eyes peeking above the jar rim, gentle and premium.', + ], + }, + { + id: '02-cat-peek', + title: '猫头探出', + prompt: [ + ...basePrompt, + 'Variant focus: cat. Small triangular cat ears, upper half of cat head and two simple eyes peeking out, clever and cute, no whiskers.', + ], + }, + { + id: '03-fox-peek', + title: '狐头探出', + prompt: [ + ...basePrompt, + 'Variant focus: fox. Slender fox ears, warm orange upper head with two simple eyes, playful but not sharp or aggressive.', + ], + }, + { + id: '04-bear-peek', + title: '熊头探出', + prompt: [ + ...basePrompt, + 'Variant focus: bear. Rounded bear ears and rounded upper head, two simple eyes, cozy and calm, no muzzle.', + ], + }, + { + id: '05-dog-peek', + title: '狗头探出', + prompt: [ + ...basePrompt, + 'Variant focus: dog. Soft floppy dog ears and upper head peeking from the jar, friendly but not a pet logo, no nose or mouth.', + ], + }, + { + id: '06-mixed-peek', + title: '混合小灵', + prompt: [ + ...basePrompt, + 'Variant focus: ambiguous animal spirit. Ear shapes sit between rabbit and cat, upper head and eyes only, more original and less species-specific.', + ], + }, + { + id: '07-tall-peek', + title: '高罐探头', + prompt: [ + ...basePrompt, + 'Variant focus: taller jar silhouette with animal head peeking to eye level. Make the jar and head relationship clear at favicon size.', + ], + }, + { + id: '08-trademark-peek', + title: '商标探头', + prompt: [ + ...basePrompt, + 'Variant focus: strongest trademark readability. Compact jar, simple half-head, two eyes, very few details, excellent black-and-white legibility.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join(outputDir, `taonier-peeking-head-jar-${variant.id}.${image.extension}`); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join(outputDir, 'taonier-logo-peeking-head-jar-manifest.json'); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + logoSkillSummary: { + requiredReview: 'visual inspection, 32px readability, black-white viability', + outputStatus: 'AI concept only; final logo needs vector cleanup', + }, + brief: logoBrief, + variants: variants.map((variant) => { + const file = files.find((item) => path.basename(item).includes(variant.id)); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + brief: logoBrief, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-peeking-head-jar-new-animals-contact-sheet.py b/scripts/generate-taonier-peeking-head-jar-new-animals-contact-sheet.py new file mode 100644 index 00000000..84109ff9 --- /dev/null +++ b/scripts/generate-taonier-peeking-head-jar-new-animals-contact-sheet.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-peeking-head-jar-new-animals-concepts" +) +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-peeking-head-jar-new-animals-contact-sheet.png" + +ITEMS = [ + ("01 水豚头", "taonier-peeking-head-jar-new-animals-01-capybara"), + ("02 仓鼠头", "taonier-peeking-head-jar-new-animals-02-hamster"), + ("03 考拉头", "taonier-peeking-head-jar-new-animals-03-koala"), + ("04 水獭头", "taonier-peeking-head-jar-new-animals-04-otter"), + ("05 松鼠头", "taonier-peeking-head-jar-new-animals-05-squirrel"), + ("06 浣熊头", "taonier-peeking-head-jar-new-animals-06-raccoon"), + ("07 小羊头", "taonier-peeking-head-jar-new-animals-07-lamb"), + ("08 刺猬头", "taonier-peeking-head-jar-new-animals-08-hedgehog"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 300 + label_height = 58 + test_height = 46 + gap = 24 + columns = 4 + rows = 2 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(20) + test_font = load_font(14) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 68, test_y + 7)) + sheet.paste(mono, (x + 122, test_y + 7)) + draw.text((x + 166, test_y + 14), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-peeking-head-jar-new-animals-logo-concepts.mjs b/scripts/generate-taonier-peeking-head-jar-new-animals-logo-concepts.mjs new file mode 100644 index 00000000..7b52b506 --- /dev/null +++ b/scripts/generate-taonier-peeking-head-jar-new-animals-logo-concepts.mjs @@ -0,0 +1,464 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-peeking-head-jar-new-animals-concepts', +); +const timeoutMsDefault = 180000; + +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 logoBrief = { + brand: '陶泥儿', + coreBelief: '好玩会创造', + logoType: 'symbol/icon-only mascot mark, no wordmark', + product: + 'AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品', + direction: + '保持当前“半头探出”的状态,但把动物类型真正拓宽到新物种', + audience: '女性用户友好、全年龄向、年轻明亮但不低幼', + structureRules: [ + '主形体仍然是陶罐容器,罐子负责陶器和包裹感', + '动物只露出耳朵、上半个脑袋和两只黑点眼睛', + '眼睛不能有高光、不能有白点反光、不能有玻璃感', + '不露鼻子、嘴巴、身体、爪子或完整动物脸', + '罐子绝对不能有表情元素', + ], + avoid: [ + '中文或英文字', + '罐子表情', + '动物嘴巴或鼻子', + '眼睛高光', + '白眼球高光', + '星星或闪光', + '手托举元素', + '写实陶瓷高光', + '脏泥土或砖块', + '面团、汤圆、甜点、面包、巧克力、糖果、布丁', + '完整动物身体', + '恐怖怪物、牙齿、爪子', + ], +}; + +const basePrompt = [ + 'Create an icon-only mascot logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', + 'Core idea: a ceramic jar container with a small animal peeking out only to eye level. Show the animal ears, the upper half of the head, and two simple black-dot eyes. Do not show nose, mouth, lower face, paws, body, or full head.', + 'The jar must remain the main brand shape. The animal head should feel hidden inside the jar, as if a playful idea is peeking out.', + 'Brand core: fun creation. The logo should feel like a tiny clay-born creative spirit that can hold playful ideas, memes, and shareable works.', + 'Logo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.', + 'Main structure: a closed or mostly closed ceramic jar, pot, or vessel with a clear rim and body silhouette. The jar should read as a container first, not as a cup, vase, bowl, bottle, or food package.', + 'Animal visibility: show only ears, top half of head, and two solid black eye dots above or behind the jar rim. The rim may partially cover the lower half of the animal head. No animal mouth, no nose, no cheek blush, no paws, no body.', + 'Eye policy: the eyes must be pure matte black dots. No highlights, no glossy sparkles, no white reflective spots, no glassy pupils, no shiny toy eyes.', + 'Jar policy: the jar itself has no face or expression. No eyes, no smile, no blush, no emoji marks on the jar.', + 'Style: modern minimalist vector mascot logo, clean curves, flat brand-color shapes, premium but warm. Slight clay warmth through color and form only; no realistic texture.', + 'Jar color direction: low-saturation ceramic tones such as beige, sand, warm gray, muted terracotta, pale taupe, clay pink, smoky cream, cool ash clay, or muted olive clay. Each variant should shift the jar silhouette and color slightly.', + 'Animal color direction: use soft natural animal colors that fit the species, but keep them gentle and logo-friendly. The animal head should be more alive and colorful than the jar.', + 'Avoid realistic ceramic glaze, muddy pottery, rough handmade class object, 3D render, ceramic shine, brick, food-like container, toy feeling, chibi over-detailing, or generic blob.', + 'Food avoidance is critical: do not make it look like bread, mochi, dumpling, bun, cake, cookie, candy, pudding, chocolate, jelly, or dessert packaging.', + 'Avoid star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.', + 'Composition: centered on a clean light background with generous safe area. Strong readable silhouette first. Must still read at 32px and in black and white.', +]; + +const variants = [ + { + id: '01-capybara', + title: '水豚头', + prompt: [ + ...basePrompt, + 'Variant focus: capybara. Use a broad calm jar with a warm beige body and a capybara head peeking out. The capybara has simple rounded ears and a very gentle expression made only from black-dot eyes.', + ], + }, + { + id: '02-hamster', + title: '仓鼠头', + prompt: [ + ...basePrompt, + 'Variant focus: hamster. Use a squat round jar with a pale sand body and a hamster head. Slightly fuller cheeks are allowed only as shape, but no mouth or nose; eyes are black dots only.', + ], + }, + { + id: '03-koala', + title: '考拉头', + prompt: [ + ...basePrompt, + 'Variant focus: koala. Use a muted eucalyptus-gray jar and a gray-white koala head with round fuzzy ears. Keep the head soft and calm, eyes are black dots only.', + ], + }, + { + id: '04-otter', + title: '水獭头', + prompt: [ + ...basePrompt, + 'Variant focus: otter. Use a smooth river-stone jar and a warm brown otter head. The ears can be tiny and round, the head is compact and playful, eyes are black dots only.', + ], + }, + { + id: '05-squirrel', + title: '松鼠头', + prompt: [ + ...basePrompt, + 'Variant focus: squirrel. Use a light clay jar and a reddish-brown squirrel head with small upright ears. The head should feel energetic but still only half exposed, eyes are black dots only.', + ], + }, + { + id: '06-raccoon', + title: '浣熊头', + prompt: [ + ...basePrompt, + 'Variant focus: raccoon. Use a muted taupe jar and a gray raccoon head with a darker mask shape implied by color, but no nose or mouth; eyes are black dots only.', + ], + }, + { + id: '07-lamb', + title: '小羊头', + prompt: [ + ...basePrompt, + 'Variant focus: lamb. Use a soft cream jar and a fluffy off-white lamb head with small curled ears. Keep the silhouette gentle and soft, eyes are black dots only.', + ], + }, + { + id: '08-hedgehog', + title: '刺猬头', + prompt: [ + ...basePrompt, + 'Variant focus: hedgehog. Use a compact jar with a warm sand body and a hedgehog head hinted by a rounded spiky silhouette, but keep the spikes soft and logo-simple, eyes are black dots only.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join( + outputDir, + `taonier-peeking-head-jar-new-animals-${variant.id}.${image.extension}`, + ); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join( + outputDir, + 'taonier-logo-peeking-head-jar-new-animals-manifest.json', + ); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + logoSkillSummary: { + requiredReview: + 'visual inspection, 32px readability, black-white viability', + outputStatus: 'AI concept only; final logo needs vector cleanup', + }, + brief: logoBrief, + variants: variants.map((variant) => { + const file = files.find((item) => + path.basename(item).includes(variant.id), + ); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + brief: logoBrief, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-playful-bean-contact-sheet.py b/scripts/generate-taonier-playful-bean-contact-sheet.py new file mode 100644 index 00000000..c5fcb486 --- /dev/null +++ b/scripts/generate-taonier-playful-bean-contact-sheet.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "public" / "branding" / "taonier-logo-playful-bean-concepts" +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-playful-bean-contact-sheet.png" + +ITEMS = [ + ("01 清新豆形标", "taonier-playful-bean-01-fresh-bean-mark"), + ("02 蜜桃软几何", "taonier-playful-bean-02-peach-soft-geometry"), + ("03 青绿创作胚", "taonier-playful-bean-03-mint-creation-embryo"), + ("04 女性向明亮款", "taonier-playful-bean-04-female-bright-mark"), + ("05 全龄轻玩款", "taonier-playful-bean-05-all-age-play-mark"), + ("06 黑白优先款", "taonier-playful-bean-06-monochrome-first"), + ("07 头像小尺寸款", "taonier-playful-bean-07-avatar-readable"), + ("08 矢量定稿感款", "taonier-playful-bean-08-vector-ready"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 300 + label_height = 58 + test_height = 46 + gap = 24 + columns = 4 + rows = 2 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(20) + test_font = load_font(14) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 68, test_y + 7)) + sheet.paste(mono, (x + 122, test_y + 7)) + draw.text((x + 166, test_y + 14), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-playful-bean-logo-concepts.mjs b/scripts/generate-taonier-playful-bean-logo-concepts.mjs new file mode 100644 index 00000000..0193bc63 --- /dev/null +++ b/scripts/generate-taonier-playful-bean-logo-concepts.mjs @@ -0,0 +1,444 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-playful-bean-concepts', +); +const timeoutMsDefault = 180000; + +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 logoBrief = { + brand: '陶泥儿', + coreBelief: '好玩会创造', + logoType: 'symbol/icon-only mark, no wordmark', + product: + 'AI UGC 轻休闲小游戏创作与传播平台,用户像捏陶泥一样把脑洞、梗和灵感塑造成可玩的作品', + coreMetaphor: '已经成形的可玩作品胚', + audience: '女性用户友好、全年龄向、年轻明亮但不低幼', + visualLanguage: '抽象但有玩性的软几何玩具感', + material: '只保留陶泥温度,不追求泥土质感', + shape: '闭合不规则圆润豆形,外轮廓流畅、亲和、有玩性', + colors: ['珊瑚橙', '蜜桃粉', '奶油白', '清透青绿', '少量暖黄或柔紫可选'], + mustHave: [ + '无中文、无英文、无字标', + '无星星、无脸、无表情', + '无方形底盘', + '无食物感', + '32px 可识别', + '黑白化仍成立', + ], +}; + +const basePrompt = [ + 'Create an icon-only brand logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', + 'Brand core belief: fun creation. Users can turn imagination, memes, and ideas into playable AI/UGC casual game works.', + 'Logo meaning: an already-formed playable creation embryo. It should feel like a small friendly finished creative object, not a process diagram and not raw clay.', + 'Logo type: abstract symbol/icon only. No wordmark, no mascot, no face, no app-icon rounded-square background.', + 'Main element: one closed irregular rounded bean-like soft geometric shape. The outer contour must be smooth, friendly, memorable, complete, and vector-friendly.', + 'The shape must not be a square, rounded square, circle, ellipse, simple blob, ribbon, swirl, S shape, G shape, open ring, loose strip, badge, stamp, or tile.', + 'The symbol should be abstract but playful: it should feel like a soft geometric toy-like creation object, mature enough for a premium brand and not childish.', + 'Internal design: optional 1-2 broad curved color fields, soft cut-ins, or abstract playful inlays. Do not show a transformation process. Do not add a center icon.', + 'No star, no spark, no star-shaped negative space, no eyes, no mouth, no character body, no hand, no pottery tool.', + 'Style: modern minimalist vector logo. Flat, crisp, simple broad shapes, very light clay warmth only. No realistic texture. It must look good and recognizable at 32px favicon size.', + 'Color direction: fresh bright palette for women-friendly and all-age users: coral orange, peach pink, cream white, clear soft teal or mint green. Use brand-color flat shapes, not candy rendering.', + 'Avoid muted mud colors as the dominant palette. Avoid dirty clay, brick, pottery shard, mud pie, rough craft class object, and archaeology stamp feeling.', + 'Food avoidance is critical: do not make it look like bread, chocolate bread, pastry, cookie, candy, jelly, donut, cream filling, sauce, baked dough, dessert, fruit candy, or food packaging.', + 'Composition: centered on a clean light background, generous safe area, no border, no UI, no watermark. Use simple readable silhouette first.', + 'Validation targets: black-and-white version should still read clearly; the closed bean-like silhouette should be recognizable at 32px.', +]; + +const variants = [ + { + id: '01-fresh-bean-mark', + title: '清新豆形标', + prompt: [ + ...basePrompt, + 'Variant focus: the cleanest fresh bean mark. Use coral orange and cream white with a tiny soft teal accent. Strong closed irregular bean silhouette, very readable.', + ], + }, + { + id: '02-peach-soft-geometry', + title: '蜜桃软几何', + prompt: [ + ...basePrompt, + 'Variant focus: peach pink and coral soft geometry. Feminine-friendly but not cosmetic, not candy. One smooth inner color field supports the closed bean shape.', + ], + }, + { + id: '03-mint-creation-embryo', + title: '青绿创作胚', + prompt: [ + ...basePrompt, + 'Variant focus: clear mint or teal as the memory accent, with warm cream and coral. The mark should feel like a playable creation object, not a leaf or seed.', + ], + }, + { + id: '04-female-bright-mark', + title: '女性向明亮款', + prompt: [ + ...basePrompt, + 'Variant focus: brighter women-friendly palette, soft coral, peach, cream, and one clean mint accent. Keep it premium and avoid beauty-brand cliché.', + ], + }, + { + id: '05-all-age-play-mark', + title: '全龄轻玩款', + prompt: [ + ...basePrompt, + 'Variant focus: all-age casual play. More energetic and memorable, but still simple. Use two or three flat color fields, no small decorative details.', + ], + }, + { + id: '06-monochrome-first', + title: '黑白优先款', + prompt: [ + ...basePrompt, + 'Variant focus: design for black-and-white survival first. Bold positive and negative shapes, color only supports the structure. No delicate gradients.', + ], + }, + { + id: '07-avatar-readable', + title: '头像小尺寸款', + prompt: [ + ...basePrompt, + 'Variant focus: social avatar and favicon readability. Full, compact closed bean silhouette with one distinctive broad internal curve; no tiny dots.', + ], + }, + { + id: '08-vector-ready', + title: '矢量定稿感款', + prompt: [ + ...basePrompt, + 'Variant focus: designer-ready vector concept. 2-3 flat shapes, crisp boundaries, distinctive closed rounded-bean contour, minimal material cue.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join(outputDir, `taonier-playful-bean-${variant.id}.${image.extension}`); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join(outputDir, 'taonier-logo-playful-bean-manifest.json'); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + logoSkillSummary: { + requiredReview: 'visual inspection, 32px readability, black-white viability', + outputStatus: 'AI concept only; final logo needs vector cleanup', + }, + brief: logoBrief, + variants: variants.map((variant) => { + const file = files.find((item) => path.basename(item).includes(variant.id)); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + brief: logoBrief, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-ref04-locked-color-variants.py b/scripts/generate-taonier-ref04-locked-color-variants.py new file mode 100644 index 00000000..ef31b5cb --- /dev/null +++ b/scripts/generate-taonier-ref04-locked-color-variants.py @@ -0,0 +1,544 @@ +from collections import deque +import math +from pathlib import Path + +from PIL import Image, ImageChops, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +REFERENCE_PATH = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-punch-hole-concepts" + / "taonier-punch-color-inlay.png" +) +OUTPUT_DIR = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-ref04-locked-color-concepts" +) + + +def is_red(pixel): + r, g, b = pixel + return r > 160 and g < 155 and b < 145 and r - g > 45 + + +def is_cyan(pixel): + r, g, b = pixel + return r < 120 and g > 125 and b > 125 and b - r > 65 + + +def is_open_light(pixel): + r, g, b = pixel + lum = (r + g + b) // 3 + return lum > 174 and max(pixel) - min(pixel) < 105 and not is_red(pixel) and not is_cyan(pixel) + + +def colorize(pixel, target, category): + r, g, b = pixel + lum = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 + if category == "dark": + factor = 0.88 + min(lum, 0.42) * 0.44 + else: + factor = 0.9 + lum * 0.2 + return tuple(max(0, min(255, round(channel * factor))) for channel in target) + + +def build_masks(image): + width, height = image.size + pixels = image.load() + open_mask = bytearray(width * height) + + for y in range(height): + for x in range(width): + if is_open_light(pixels[x, y]): + open_mask[y * width + x] = 1 + + # 从画布边缘连通的浅色区域是外部背景;剩下的浅色闭合区域就是中孔。 + external_mask = bytearray(width * height) + queue = deque() + for x in range(width): + for y in (0, height - 1): + index = y * width + x + if open_mask[index] and not external_mask[index]: + external_mask[index] = 1 + queue.append((x, y)) + for y in range(height): + for x in (0, width - 1): + index = y * width + x + if open_mask[index] and not external_mask[index]: + external_mask[index] = 1 + queue.append((x, y)) + + while queue: + x, y = queue.popleft() + for next_x, next_y in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + if 0 <= next_x < width and 0 <= next_y < height: + index = next_y * width + next_x + if open_mask[index] and not external_mask[index]: + external_mask[index] = 1 + queue.append((next_x, next_y)) + + hole_mask = bytearray(width * height) + red_mask = bytearray(width * height) + cyan_mask = bytearray(width * height) + dark_mask = bytearray(width * height) + for y in range(height): + for x in range(width): + index = y * width + x + pixel = pixels[x, y] + if open_mask[index] and not external_mask[index]: + hole_mask[index] = 1 + elif not external_mask[index]: + if is_red(pixel): + red_mask[index] = 1 + elif is_cyan(pixel): + cyan_mask[index] = 1 + elif not open_mask[index]: + dark_mask[index] = 1 + + return { + "dark": dark_mask, + "red": red_mask, + "cyan": cyan_mask, + "hole": hole_mask, + } + + +def mask_bounds(mask, width, height): + xs = [] + ys = [] + for index, value in enumerate(mask): + if value: + xs.append(index % width) + ys.append(index // width) + return min(xs), min(ys), max(xs), max(ys) + + +def draw_center_content(size, hole_mask, variant): + width, height = size + hole_bounds = mask_bounds(hole_mask, width, height) + left, top, right, bottom = hole_bounds + center_x = (left + right) / 2 + center_y = (top + bottom) / 2 + hole_w = right - left + hole_h = bottom - top + + scale = 4 + layer = Image.new("RGBA", (width * scale, height * scale), (0, 0, 0, 0)) + draw = ImageDraw.Draw(layer) + + def box(cx, cy, w, h): + return [ + int((cx - w / 2) * scale), + int((cy - h / 2) * scale), + int((cx + w / 2) * scale), + int((cy + h / 2) * scale), + ] + + def rounded(cx, cy, w, h, radius, fill): + draw.rounded_rectangle(box(cx, cy, w, h), radius=int(radius * scale), fill=fill) + + def ellipse(cx, cy, w, h, fill): + draw.ellipse(box(cx, cy, w, h), fill=fill) + + def star(cx, cy, outer, inner, fill): + points = [] + for index in range(10): + angle = -90 + index * 36 + radius = outer if index % 2 == 0 else inner + points.append( + ( + int((cx + radius * math.cos(math.radians(angle))) * scale), + int((cy + radius * math.sin(math.radians(angle))) * scale), + ) + ) + draw.polygon(points, fill=fill) + + def sparkle(cx, cy, radius, fill, with_rays=False): + points = [] + point_count = 128 + for index in range(point_count): + theta = -math.pi / 2 + index * math.tau / point_count + pulse = abs(math.cos(2 * theta)) ** 4.2 + current_radius = radius * (0.12 + 0.88 * pulse) + points.append( + ( + int((cx + math.cos(theta) * current_radius * 0.78) * scale), + int((cy + math.sin(theta) * current_radius * 1.12) * scale), + ) + ) + draw.polygon(points, fill=fill) + if not with_rays: + return + + ray_color = fill + line_width = max(4, int(radius * 0.11 * scale)) + cap = line_width // 2 + + def rounded_line(start, end): + draw.line( + ( + int(start[0] * scale), + int(start[1] * scale), + int(end[0] * scale), + int(end[1] * scale), + ), + fill=ray_color, + width=line_width, + ) + for point in (start, end): + draw.ellipse( + ( + int(point[0] * scale) - cap, + int(point[1] * scale) - cap, + int(point[0] * scale) + cap, + int(point[1] * scale) + cap, + ), + fill=ray_color, + ) + + rounded_line((cx - radius * 1.48, cy - radius * 0.15), (cx - radius * 1.18, cy - radius * 0.08)) + rounded_line((cx - radius * 1.35, cy + radius * 0.5), (cx - radius * 1.1, cy + radius * 0.3)) + rounded_line((cx + radius * 1.12, cy - radius * 0.42), (cx + radius * 1.33, cy - radius * 0.64)) + rounded_line((cx + radius * 1.25, cy + radius * 0.08), (cx + radius * 1.52, cy + radius * 0.15)) + + if variant == "cream_seed": + rounded(center_x, center_y, hole_w * 0.42, hole_h * 0.34, 44, (244, 216, 166, 255)) + elif variant == "soft_dot": + rounded(center_x, center_y, hole_w * 0.32, hole_h * 0.28, 36, (250, 219, 157, 255)) + elif variant == "double_piece": + rounded(center_x - hole_w * 0.08, center_y + hole_h * 0.01, hole_w * 0.24, hole_h * 0.22, 30, (249, 202, 174, 255)) + rounded(center_x + hole_w * 0.13, center_y - hole_h * 0.02, hole_w * 0.22, hole_h * 0.21, 28, (143, 207, 205, 255)) + elif variant == "tiny_kernel": + ellipse(center_x, center_y, hole_w * 0.26, hole_h * 0.24, (252, 223, 157, 255)) + elif variant == "filled_core": + rounded(center_x, center_y, hole_w * 0.58, hole_h * 0.5, 58, (248, 231, 196, 255)) + ellipse(center_x - hole_w * 0.09, center_y + hole_h * 0.02, hole_w * 0.13, hole_h * 0.12, (240, 93, 82, 255)) + ellipse(center_x + hole_w * 0.1, center_y - hole_h * 0.02, hole_w * 0.13, hole_h * 0.12, (20, 183, 196, 255)) + elif variant == "clay_pearl": + rounded(center_x, center_y, hole_w * 0.36, hole_h * 0.3, 40, (255, 226, 177, 255)) + ellipse(center_x + hole_w * 0.06, center_y - hole_h * 0.05, hole_w * 0.08, hole_h * 0.07, (255, 246, 220, 180)) + elif variant == "cream_star": + star(center_x, center_y, min(hole_w, hole_h) * 0.17, min(hole_w, hole_h) * 0.075, (255, 223, 154, 255)) + elif variant == "small_star": + star(center_x, center_y, min(hole_w, hole_h) * 0.135, min(hole_w, hole_h) * 0.06, (255, 231, 177, 255)) + elif variant == "soft_star_badge": + rounded(center_x, center_y, hole_w * 0.38, hole_h * 0.34, 42, (255, 239, 207, 255)) + star(center_x, center_y, min(hole_w, hole_h) * 0.115, min(hole_w, hole_h) * 0.052, (238, 129, 80, 255)) + elif variant == "coral_star": + star(center_x, center_y, min(hole_w, hole_h) * 0.15, min(hole_w, hole_h) * 0.067, (241, 108, 82, 255)) + elif variant == "mint_star": + star(center_x, center_y, min(hole_w, hole_h) * 0.15, min(hole_w, hole_h) * 0.067, (78, 198, 183, 255)) + elif variant == "soft_sparkle": + sparkle(center_x, center_y, min(hole_w, hole_h) * 0.18, (255, 205, 61, 255), True) + elif variant == "small_sparkle": + sparkle(center_x, center_y, min(hole_w, hole_h) * 0.145, (255, 214, 91, 255), True) + elif variant == "bright_sparkle": + sparkle(center_x, center_y, min(hole_w, hole_h) * 0.17, (255, 197, 43, 255), True) + elif variant == "quiet_sparkle": + sparkle(center_x, center_y, min(hole_w, hole_h) * 0.155, (255, 224, 139, 255), False) + + layer = layer.resize(size, Image.Resampling.LANCZOS) + alpha = Image.frombytes("L", size, bytes(255 if value else 0 for value in hole_mask)) + layer_alpha = layer.getchannel("A") + layer.putalpha(ImageChops.multiply(layer_alpha, alpha)) + return layer + + +VARIANTS = [ + { + "id": "taonier-ref04-locked-warm-ink", + "label": "01 warm", + "dark": (63, 58, 53), + "red": (243, 82, 69), + "cyan": (14, 183, 198), + "content": "cream_seed", + }, + { + "id": "taonier-ref04-locked-blue-ink", + "label": "02 blue", + "dark": (30, 39, 72), + "red": (255, 89, 84), + "cyan": (28, 181, 207), + "content": "soft_dot", + }, + { + "id": "taonier-ref04-locked-plum-ink", + "label": "03 plum", + "dark": (69, 53, 72), + "red": (255, 98, 86), + "cyan": (34, 188, 198), + "content": "double_piece", + }, + { + "id": "taonier-ref04-locked-green-ink", + "label": "04 green", + "dark": (11, 83, 78), + "red": (255, 107, 88), + "cyan": (68, 209, 192), + "content": "tiny_kernel", + }, + { + "id": "taonier-ref04-locked-shrink-core", + "label": "05 fill", + "dark": (43, 43, 47), + "red": (239, 84, 75), + "cyan": (17, 178, 193), + "content": "filled_core", + }, + { + "id": "taonier-ref04-locked-soft-charcoal", + "label": "06 soft", + "dark": (82, 76, 68), + "red": (242, 105, 90), + "cyan": (37, 188, 195), + "content": "clay_pearl", + }, +] + +STAR_OUTPUT_DIR = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-ref04-warm-star-concepts" +) + +STAR_VARIANTS = [ + { + "id": "taonier-ref04-warm-star-terracotta", + "label": "01 clay", + "dark": (121, 76, 54), + "red": (244, 86, 70), + "cyan": (15, 184, 198), + "content": "cream_star", + }, + { + "id": "taonier-ref04-warm-star-caramel", + "label": "02 caramel", + "dark": (142, 94, 51), + "red": (247, 91, 73), + "cyan": (13, 185, 196), + "content": "small_star", + }, + { + "id": "taonier-ref04-warm-star-cocoa", + "label": "03 cocoa", + "dark": (89, 64, 47), + "red": (240, 88, 72), + "cyan": (17, 181, 194), + "content": "soft_star_badge", + }, + { + "id": "taonier-ref04-warm-star-rust", + "label": "04 rust", + "dark": (111, 62, 54), + "red": (249, 93, 75), + "cyan": (15, 184, 198), + "content": "cream_star", + }, + { + "id": "taonier-ref04-warm-star-olive", + "label": "05 olive", + "dark": (92, 81, 48), + "red": (245, 94, 76), + "cyan": (25, 185, 187), + "content": "coral_star", + }, + { + "id": "taonier-ref04-warm-star-plum", + "label": "06 plum", + "dark": (95, 57, 66), + "red": (250, 94, 81), + "cyan": (26, 185, 197), + "content": "mint_star", + }, +] + +SPARKLE_OUTPUT_DIR = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-ref04-warm-sparkle-concepts" +) + +SPARKLE_V2_OUTPUT_DIR = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-ref04-warm-sparkle-v2-concepts" +) + +PALETTE_TRANSFER_OUTPUT_DIR = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-ref04-palette-transfer" +) + +SPARKLE_VARIANTS = [ + { + "id": "taonier-ref04-warm-sparkle-terracotta", + "label": "01 clay", + "dark": (121, 76, 54), + "red": (244, 86, 70), + "cyan": (15, 184, 198), + "content": "soft_sparkle", + }, + { + "id": "taonier-ref04-warm-sparkle-rust", + "label": "02 rust", + "dark": (111, 62, 54), + "red": (249, 93, 75), + "cyan": (15, 184, 198), + "content": "soft_sparkle", + }, + { + "id": "taonier-ref04-warm-sparkle-caramel", + "label": "03 caramel", + "dark": (142, 94, 51), + "red": (247, 91, 73), + "cyan": (13, 185, 196), + "content": "small_sparkle", + }, + { + "id": "taonier-ref04-warm-sparkle-cocoa", + "label": "04 cocoa", + "dark": (89, 64, 47), + "red": (240, 88, 72), + "cyan": (17, 181, 194), + "content": "bright_sparkle", + }, + { + "id": "taonier-ref04-warm-sparkle-clay-quiet", + "label": "05 quiet", + "dark": (121, 76, 54), + "red": (244, 86, 70), + "cyan": (15, 184, 198), + "content": "quiet_sparkle", + }, + { + "id": "taonier-ref04-warm-sparkle-plum", + "label": "06 plum", + "dark": (95, 57, 66), + "red": (250, 94, 81), + "cyan": (26, 185, 197), + "content": "soft_sparkle", + }, +] + +PALETTE_TRANSFER_VARIANTS = [ + { + "id": "taonier-ref04-palette-transfer-warm-yellow-sparkle", + "label": "transfer", + "dark": (224, 162, 58), + "red": (255, 113, 132), + "cyan": (91, 213, 192), + "content": "soft_sparkle", + }, +] + + +def apply_variant(reference, masks, variant): + image = reference.copy().convert("RGBA") + source = reference.convert("RGB") + width, height = source.size + source_pixels = source.load() + result_pixels = image.load() + + for y in range(height): + for x in range(width): + index = y * width + x + pixel = source_pixels[x, y] + if masks["dark"][index]: + result_pixels[x, y] = (*colorize(pixel, variant["dark"], "dark"), 255) + elif masks["red"][index]: + result_pixels[x, y] = (*colorize(pixel, variant["red"], "accent"), 255) + elif masks["cyan"][index]: + result_pixels[x, y] = (*colorize(pixel, variant["cyan"], "accent"), 255) + + content = draw_center_content(source.size, masks["hole"], variant["content"]) + return Image.alpha_composite(image, content).convert("RGB") + + +def build_contact_sheet(items, output_path): + thumb = 260 + label_h = 34 + pad = 18 + cols = 4 + rows = (len(items) + cols - 1) // cols + sheet_w = cols * thumb + (cols + 1) * pad + sheet_h = rows * (thumb + label_h) + (rows + 1) * pad + sheet = Image.new("RGB", (sheet_w, sheet_h), "#f7f3ea") + draw = ImageDraw.Draw(sheet) + try: + font = ImageFont.truetype("arial.ttf", 18) + except OSError: + font = ImageFont.load_default() + + for index, (label, path) in enumerate(items): + image = Image.open(path).convert("RGB") + image.thumbnail((thumb, thumb), Image.Resampling.LANCZOS) + row, col = divmod(index, cols) + x = pad + col * (thumb + pad) + y = pad + row * (thumb + label_h + pad) + bg = Image.new("RGB", (thumb, thumb), "#fffaf1") + bg.paste(image, ((thumb - image.width) // 2, (thumb - image.height) // 2)) + sheet.paste(bg, (x, y)) + draw.rectangle((x, y, x + thumb - 1, y + thumb - 1), outline="#ded5c6", width=1) + bbox = draw.textbbox((0, 0), label, font=font) + draw.text((x + (thumb - (bbox[2] - bbox[0])) // 2, y + thumb + 8), label, fill="#211f1c", font=font) + + sheet.save(output_path) + + +def generate_set(output_dir, variants, contact_name): + output_dir.mkdir(parents=True, exist_ok=True) + reference = Image.open(REFERENCE_PATH).convert("RGB") + masks = build_masks(reference) + contact_items = [("REF-04", REFERENCE_PATH)] + + for variant in variants: + output_path = output_dir / f"{variant['id']}.png" + apply_variant(reference, masks, variant).save(output_path) + contact_items.append((variant["label"], output_path)) + + build_contact_sheet( + contact_items, + output_dir / contact_name, + ) + + +def main(): + generate_set( + OUTPUT_DIR, + VARIANTS, + "taonier-logo-ref04-locked-color-contact-sheet.png", + ) + generate_set( + STAR_OUTPUT_DIR, + STAR_VARIANTS, + "taonier-logo-ref04-warm-star-contact-sheet.png", + ) + generate_set( + SPARKLE_OUTPUT_DIR, + SPARKLE_VARIANTS, + "taonier-logo-ref04-warm-sparkle-contact-sheet.png", + ) + generate_set( + SPARKLE_V2_OUTPUT_DIR, + SPARKLE_VARIANTS, + "taonier-logo-ref04-warm-sparkle-v2-contact-sheet.png", + ) + generate_set( + PALETTE_TRANSFER_OUTPUT_DIR, + PALETTE_TRANSFER_VARIANTS, + "taonier-logo-ref04-palette-transfer-contact-sheet.png", + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-short-foot-creature-contact-sheet.py b/scripts/generate-taonier-short-foot-creature-contact-sheet.py new file mode 100644 index 00000000..adc8459f --- /dev/null +++ b/scripts/generate-taonier-short-foot-creature-contact-sheet.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageOps + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-short-foot-creature-concepts" +) +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-short-foot-creature-contact-sheet.png" + +ITEMS = [ + ("01 弯角泥团", "taonier-short-foot-creature-01-curled-tip"), + ("02 软芽泥团", "taonier-short-foot-creature-02-soft-sprout"), + ("03 波浪小怪", "taonier-short-foot-creature-03-wave-tuft"), + ("04 圆角小怪", "taonier-short-foot-creature-04-round-horn"), + ("05 低趴泥团", "taonier-short-foot-creature-05-low-squat"), + ("06 偏心灵体", "taonier-short-foot-creature-06-asymmetric-charm"), + ("07 头像强识别", "taonier-short-foot-creature-07-avatar-bold"), + ("08 商标轮廓", "taonier-short-foot-creature-08-vector-outline"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def find_image(stem: str) -> Path | None: + for extension in ("png", "webp", "jpg", "jpeg"): + candidate = OUTPUT_DIR / f"{stem}.{extension}" + if candidate.exists(): + return candidate + return None + + +def bw_preview(image: Image.Image, size: int) -> Image.Image: + thumb = ImageOps.grayscale(image).resize((size, size), Image.Resampling.LANCZOS) + return ImageOps.autocontrast(thumb).convert("RGB") + + +def main() -> None: + cell_size = 300 + label_height = 58 + test_height = 46 + gap = 24 + columns = 4 + rows = 2 + cell_total_height = cell_size + label_height + test_height + width = columns * cell_size + (columns + 1) * gap + height = rows * cell_total_height + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f0ebe5") + draw = ImageDraw.Draw(sheet) + label_font = load_font(20) + test_font = load_font(14) + + for index, (label, stem) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_total_height + gap) + + image_path = find_image(stem) + if image_path is None: + continue + + source = Image.open(image_path).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=8, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=label_font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=label_font) + + test_y = y + cell_size + label_height + draw.rounded_rectangle( + (x, test_y, x + cell_size, test_y + test_height), + radius=8, + fill="#f7f3ed", + ) + tiny = source.resize((32, 32), Image.Resampling.LANCZOS) + mono = bw_preview(source, 32) + sheet.paste(tiny, (x + 68, test_y + 7)) + sheet.paste(mono, (x + 122, test_y + 7)) + draw.text((x + 166, test_y + 14), "32px / BW", fill="#5c5148", font=test_font) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-short-foot-creature-logo-concepts.mjs b/scripts/generate-taonier-short-foot-creature-logo-concepts.mjs new file mode 100644 index 00000000..fb127cec --- /dev/null +++ b/scripts/generate-taonier-short-foot-creature-logo-concepts.mjs @@ -0,0 +1,457 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-short-foot-creature-concepts', +); +const timeoutMsDefault = 180000; + +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 logoBrief = { + brand: '陶泥儿', + coreBelief: '好玩会创造', + logoType: 'symbol/icon-only mascot mark, no wordmark', + product: + 'AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品', + direction: + '低重心短脚泥团小灵体 / 小怪物:参考图只用于造型,不继承写实陶瓷质感', + audience: '女性用户友好、全年龄向、年轻明亮但不低幼', + shapeRules: [ + '主体是坐在地上的闭合泥团生物,像一个稳定的软陶泥胚', + '底部有 3-5 个短短的圆脚或脚趾状支点,但不能变成爪子', + '头顶可以有弯角、小尖、软芽、卷曲或捏起的造型,作为记忆点', + '整体必须是 logo 符号级别,不是完整角色插画', + '32px 下仍能看出低重心泥团、短脚和头顶造型', + ], + avoid: [ + '中文或英文字', + '星星或闪光', + '手托举元素', + '写实陶瓷高光', + '脏泥土或砖块', + '面团、汤圆、甜点、面包、巧克力、糖果、布丁', + '恐怖怪物、牙齿、爪子', + '儿童玩具、表情包贴纸', + ], +}; + +const basePrompt = [ + 'Create an icon-only mascot logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', + 'The reference idea is only shape language: a squat soft clay lump creature sitting on the ground, with several very short rounded feet and a distinctive top tuft or curved horn. Do not copy realistic ceramic rendering.', + 'Brand core: fun creation. The mark should feel like a tiny clay-born creative spirit: soft, friendly, shareable, and able to turn ideas into playful AI/UGC casual game works.', + 'Logo type: simple mascot symbol only. It must be a brand mark, not a full character illustration, not a scene, not a sticker, not a rounded-square app icon background.', + 'Main silhouette: low center of gravity, closed mound body, slightly irregular but clean outline, broad base, 3 to 5 stubby rounded feet at the bottom. Feet must be short and cute, never claws, toes, paws, or realistic animal legs.', + 'Top silhouette: one memorable soft sculpted feature on the head, such as a curled clay tip, small pinched sprout, rounded horn, or soft wave tuft. The top feature should be part of the same clay body, not a separate hat or accessory.', + 'Face policy: face is optional; if used, only two tiny simple eye dots. No mouth, no blush, no emoji expression, no detailed face. The silhouette should work even without the face.', + 'Style: modern minimalist vector mascot logo, flat brand-color shapes, clean curves, premium but warm. Slight clay warmth through color and form only; no realistic texture.', + 'Color direction: earthy but fresh, using warm clay beige, terracotta, cream white, soft coral, and a very small teal or mint accent if helpful. Avoid dirty mud, brick red dominance, candy gradients, glossy dessert rendering.', + 'Food avoidance is critical: do not make it look like mochi, dumpling, bun, bread, cake, cookie, candy, pudding, chocolate, jelly, or any edible mascot.', + 'Avoid pottery class object, ceramic figurine, handmade toy, rough mud texture, archaeology stamp, realistic glaze, 3D clay render, or shiny ceramic highlight.', + 'Avoid scary monster, teeth, claws, spikes, realistic animal, pet logo, chibi over-detailing, generic blob, star, spark, halo, magic wand, hand, pottery tool, UI, border, or watermark.', + 'Composition: centered on clean light background, generous safe area, strong readable silhouette first. It should look recognizable at 32px and still work in black and white.', +]; + +const variants = [ + { + id: '01-curled-tip', + title: '弯角泥团', + prompt: [ + ...basePrompt, + 'Variant focus: a squat clay lump creature with one soft curled tip leaning gently forward, four tiny rounded feet, calm premium silhouette.', + ], + }, + { + id: '02-soft-sprout', + title: '软芽泥团', + prompt: [ + ...basePrompt, + 'Variant focus: a low mound creature with a pinched sprout-like top made from the same clay body, three short feet, fresh and memorable.', + ], + }, + { + id: '03-wave-tuft', + title: '波浪小怪', + prompt: [ + ...basePrompt, + 'Variant focus: a playful clay creature with a single wave-shaped top tuft, broad sitting base, 4 tiny feet, more dynamic but still logo-simple.', + ], + }, + { + id: '04-round-horn', + title: '圆角小怪', + prompt: [ + ...basePrompt, + 'Variant focus: a friendly abstract little monster with one rounded horn-like bump and a second smaller bump, stubby feet, no scary details.', + ], + }, + { + id: '05-low-squat', + title: '低趴泥团', + prompt: [ + ...basePrompt, + 'Variant focus: extra low and stable clay mound, wide base, five tiny rounded feet, top feature is a subtle pinched crest, very favicon-readable.', + ], + }, + { + id: '06-asymmetric-charm', + title: '偏心灵体', + prompt: [ + ...basePrompt, + 'Variant focus: asymmetrical friendly spirit mark, body leans slightly to one side, curled top balances the shape, short feet stay grounded.', + ], + }, + { + id: '07-avatar-bold', + title: '头像强识别', + prompt: [ + ...basePrompt, + 'Variant focus: bold social avatar readability, thick simple silhouette, two tiny eye dots allowed, top tuft and feet readable at 32px.', + ], + }, + { + id: '08-vector-outline', + title: '商标轮廓', + prompt: [ + ...basePrompt, + 'Variant focus: designer-ready vector mark. Use 2-3 flat shapes, crisp boundaries, very strong black-and-white silhouette, minimal inner detail.', + ], + }, +]; + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildRequestBody(variant) { + return { + model: 'gpt-image-2-all', + prompt: variant.prompt.join('\n'), + n: 1, + size: '1024x1024', + }; +} + +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 inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +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}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, variant) { + const requestBody = buildRequestBody(variant); + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(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 image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${variant.id}`); + } + + mkdirSync(outputDir, { recursive: true }); + const outputPath = path.join( + outputDir, + `taonier-short-foot-creature-${variant.id}.${image.extension}`, + ); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +function writeManifest(files) { + const manifestPath = path.join( + outputDir, + 'taonier-logo-short-foot-creature-manifest.json', + ); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2-all', + size: '1024x1024', + generatedAt: new Date().toISOString(), + logoSkillSummary: { + requiredReview: + 'visual inspection, 32px readability, black-white viability', + outputStatus: 'AI concept only; final logo needs vector cleanup', + }, + brief: logoBrief, + variants: variants.map((variant) => { + const file = files.find((item) => + path.basename(item).includes(variant.id), + ); + return { + id: variant.id, + title: variant.title, + file: file ? path.basename(file) : null, + prompt: variant.prompt.join('\n'), + }; + }), + }, + null, + 2, + )}\n`, + 'utf8', + ); + return manifestPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); +const selectedVariants = variants.slice(0, limit); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + count: selectedVariants.length, + brief: logoBrief, + requests: selectedVariants.map((variant) => ({ + id: variant.id, + title: variant.title, + body: buildRequestBody(variant), + })), + }, + 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 variant of selectedVariants) { + console.log(`Generating ${variant.id} ${variant.title}...`); + generated.push(await generateOne(env, variant)); +} + +const manifestPath = writeManifest(generated); +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + manifest: manifestPath, + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-spiral-contact-sheet.py b/scripts/generate-taonier-spiral-contact-sheet.py new file mode 100644 index 00000000..15bd4c9f --- /dev/null +++ b/scripts/generate-taonier-spiral-contact-sheet.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = ( + REPO_ROOT / "public" / "branding" / "taonier-logo-spiral-reference-concepts" +) +CONTACT_SHEET_PATH = OUTPUT_DIR / "taonier-logo-spiral-reference-contact-sheet.png" + +ITEMS = [ + ("01 软泥旋合", "taonier-spiral-soft-squish.png"), + ("02 糖果泥卷", "taonier-spiral-candy-roll.png"), + ("03 星核涡标", "taonier-spiral-star-core.png"), + ("04 Q弹泥涡", "taonier-spiral-bouncy-clay.png"), + ("05 创作星涡", "taonier-spiral-creation-whirl.png"), + ("06 旋合软标", "taonier-spiral-soft-token.png"), +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def main() -> None: + cell_size = 330 + label_height = 58 + gap = 28 + columns = 3 + rows = 2 + width = columns * cell_size + (columns + 1) * gap + height = rows * (cell_size + label_height) + (rows + 1) * gap + + sheet = Image.new("RGB", (width, height), "#f6f2eb") + draw = ImageDraw.Draw(sheet) + font = load_font(24) + + for index, (label, filename) in enumerate(ITEMS): + row = index // columns + column = index % columns + x = gap + column * (cell_size + gap) + y = gap + row * (cell_size + label_height + gap) + + source = Image.open(OUTPUT_DIR / filename).convert("RGB") + thumbnail = source.resize((cell_size, cell_size), Image.Resampling.LANCZOS) + sheet.paste(thumbnail, (x, y)) + + draw.rounded_rectangle( + (x, y + cell_size, x + cell_size, y + cell_size + label_height), + radius=10, + fill="#fffdf8", + ) + text_box = draw.textbbox((0, 0), label, font=font) + text_x = x + (cell_size - (text_box[2] - text_box[0])) / 2 + text_y = y + cell_size + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), label, fill="#302a25", font=font) + + sheet.save(CONTACT_SHEET_PATH, quality=95) + print(CONTACT_SHEET_PATH) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-taonier-spiral-logo-concepts.mjs b/scripts/generate-taonier-spiral-logo-concepts.mjs new file mode 100644 index 00000000..10b44914 --- /dev/null +++ b/scripts/generate-taonier-spiral-logo-concepts.mjs @@ -0,0 +1,373 @@ +import { Buffer } from 'node:buffer'; +import { + existsSync, + mkdirSync, + readFileSync, + readdirSync, + writeFileSync, +} from 'node:fs'; +import path from 'node:path'; + +const repoRoot = process.cwd(); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-spiral-reference-concepts', +); +const defaultTimeoutMs = 420000; +const defaultReferenceImagePath = path.join( + outputDir, + 'taonier-spiral-reference.jpg', +); + +const concepts = [ + { + id: 'taonier-spiral-soft-squish', + title: '软泥旋合', + prompt: + '参考输入图的粗圆头螺旋动势,但不要照抄黑白图,也不要使用黑底白线。为中文产品“陶泥儿”设计一个无文字扁平矢量 Logo。结合此前认可的“软泥合拍”造型:上下两团抽象软泥被旋转吸入中心,像把脑洞轻轻揉成作品。上方使用糖果莓粉 / 珊瑚粉,下方使用薄荷青 / 青绿,中央一颗暖黄色小星点。整体主流、亲和、Q 弹、可爱但不幼稚,适合作为 App icon。禁止真实手、手指、播放键、聊天气泡、笑脸、眼睛、花朵、褐色陶土、文字、字母、水印、3D、厚阴影、复杂碎元素。', + }, + { + id: 'taonier-spiral-candy-roll', + title: '糖果泥卷', + prompt: + '以参考输入图的单笔圆头旋涡为结构灵感,为“陶泥儿”设计一个无文字扁平矢量 Logo。图形像一条柔软陶泥带被卷成可爱的糖果泥卷,但需要保留上下双色软泥合拍的感觉:外侧莓粉,内侧薄荷青,中心有小小暖黄星核。造型要圆润、干净、强记忆点,小尺寸清晰,像年轻创作娱乐 App 的主标。不要直接做黑白旋涡,不要做催眠、棒棒糖、浏览器加载、循环箭头或太极图。禁止文字、字母、水印、3D、真实陶艺、聊天气泡、播放三角、笑脸、眼睛。', + }, + { + id: 'taonier-spiral-star-core', + title: '星核涡标', + prompt: + '使用参考输入图的向心旋转和包裹感,设计“陶泥儿”无文字扁平矢量 Logo。两块软泥形沿螺旋方向轻轻包住中央作品星核,像 AI 把灵感旋成小游戏。整体比普通旋涡更像品牌符号:线条粗、端点圆、负形干净,不能像手或眼睛。配色沿用陶泥儿前序方向:莓粉 / 珊瑚粉、薄荷青 / 青绿、奶油白、暖黄色星点。风格主流、亲和、可爱、现代、清晰。禁止黑白原图复刻、聊天气泡、播放键、笑脸、花朵、真实手指、褐色主色、文字、水印。', + }, + { + id: 'taonier-spiral-bouncy-clay', + title: 'Q弹泥涡', + prompt: + '参考输入图的圆润螺旋,但把它转化成“陶泥儿”的 Q 弹软泥 Logo:两条短而厚的软泥弧线从上下错位旋入中心,中间不是黑洞,而是一颗小星 / 小作品核。颜色更可爱:粉桃、薄荷、奶油白、暖黄。图形要比参考更轻、更甜、更品牌化,保留一点“软泥合拍”的上下关系。适合 App icon 和启动页。不要像棒棒糖、蚊香、加载图标、太极、旋风、眼睛、聊天气泡、播放按钮;禁止文字、字母、水印、3D。', + }, + { + id: 'taonier-spiral-creation-whirl', + title: '创作星涡', + prompt: + '结合参考输入图的螺旋势能和陶泥儿此前“软泥合拍”的粉绿配色,设计无文字扁平矢量 Logo。主形是一个开放式旋涡,像软泥被轻轻揉动,中心生成暖黄色四角星。整体要活泼、生动、主流、容易记住,不要太抽象成通用旋涡。形体应保留圆头粗线和柔软手感,但不能出现具体手。颜色:亮莓粉、清爽薄荷青、奶白、暖黄,最多四色。禁止黑白照搬、褐色陶土、播放三角、聊天气泡、笑脸、眼睛、花朵、文字、水印、复杂碎点。', + }, + { + id: 'taonier-spiral-soft-token', + title: '旋合软标', + prompt: + '以参考输入图的粗线螺旋为参考,为“陶泥儿”做更成熟的 App icon 主标。把螺旋收敛成一个完整圆润的软泥符号:上半莓粉,下半青绿,中间用奶白负形形成自然旋转缝隙,中心保留一枚小暖黄星点。它需要兼顾精品 AI 创作、UGC、小游戏和传播感,不能太儿童、不能太像加载图。风格:flat vector logo, clean, friendly, cute, memorable, scalable, solid colors。禁止文字、字母、水印、3D、厚阴影、真实手、播放键、聊天气泡、笑脸、眼睛、太极、棒棒糖。', + }, +]; + +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); + } +} + +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 getMimeType(filePath) { + const extension = path.extname(filePath).toLowerCase(); + if (extension === '.jpg' || extension === '.jpeg') { + return 'image/jpeg'; + } + if (extension === '.webp') { + return 'image/webp'; + } + return 'image/png'; +} + +function readReferenceDataUrl(filePath) { + const bytes = readFileSync(filePath); + return `data:${getMimeType(filePath)};base64,${bytes.toString('base64')}`; +} + +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, referenceDataUrl) { + const requestBody = { + model: 'gpt-image-2-all', + quality: String(args.get('--quality') || 'low'), + prompt: concept.prompt, + image: [referenceDataUrl], + 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(outputDir, { recursive: true }); + const extension = inferExtensionFromBytes(bytes); + const outputPath = path.join(outputDir, `${concept.id}.${extension}`); + writeFileSync(outputPath, bytes); + return outputPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const referenceImagePath = String( + args.get('--reference') || defaultReferenceImagePath, +); +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 (!existsSync(referenceImagePath)) { + console.error( + JSON.stringify({ + ok: false, + error: 'Reference image not found', + referenceImagePath, + }), + ); + process.exit(1); +} + +const referenceDataUrl = readReferenceDataUrl(referenceImagePath); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + referenceImagePath, + referenceImage: { + mimeType: getMimeType(referenceImagePath), + dataUrlLength: referenceDataUrl.length, + }, + count: selected.length, + requests: selected.map((concept) => ({ + id: concept.id, + title: concept.title, + body: { + model: 'gpt-image-2-all', + quality: String(args.get('--quality') || 'low'), + prompt: concept.prompt, + image: [''], + 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, referenceDataUrl)); +} + +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + verifiedFiles: readdirSync(outputDir).sort(), + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-squish-logo-concepts.mjs b/scripts/generate-taonier-squish-logo-concepts.mjs new file mode 100644 index 00000000..198712bc --- /dev/null +++ b/scripts/generate-taonier-squish-logo-concepts.mjs @@ -0,0 +1,315 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; + +const repoRoot = process.cwd(); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-squish-concepts', +); +const defaultTimeoutMs = 420000; + +const concepts = [ + { + id: 'taonier-squish-v2-pulse', + title: '软泥合拍', + prompt: + '无文字扁平矢量 Logo,产品名“陶泥儿”。参考方向:上下两团抽象软泥轻快合拍,中间一颗星点被捏出来。不要画手、手指、掌纹,不要聊天气泡、笑脸、眼睛、花朵、播放键。整体年轻、主流、抽象、生动、像娱乐创作 App icon。上方珊瑚红软形,下方青绿软形,中央奶油白或金色星点,最多 3 色。形状简洁,小尺寸清晰。', + }, + { + id: 'taonier-squish-v2-bounce', + title: '弹力成型', + prompt: + '无文字扁平矢量 Logo,产品名“陶泥儿”。图形由上下两块弹性的软泥豆形组成,中间留出弯曲的白色空间和一颗小星点,表达脑洞被轻轻一压就成型。要抽象、有动感、亲和,不像手、不像眼睛、不像聊天气泡。主流 App icon 风格,简洁、高识别。配色:亮珊瑚、薄荷青、奶油白,最多 3 色。禁止文字、字母、3D、褐色、碎元素。', + }, + { + id: 'taonier-squish-v2-spark-gap', + title: '星隙合拍', + prompt: + '无文字扁平矢量 Logo,产品名“陶泥儿”。上下两团圆润软泥彼此靠近,中间形成一个自然的星形负空间,像灵感在缝隙中被捏出来。图形必须抽象、现代、活泼,不出现手、眼睛、嘴巴、聊天气泡、播放符号或花朵。适合 App icon,小尺寸一眼识别。配色:玫红 / 珊瑚红主上形,青绿色下形,奶白负形,最多 3 色。', + }, + { + id: 'taonier-squish-v2-comet', + title: '合拍星流', + prompt: + '无文字扁平矢量 Logo,产品名“陶泥儿”。两个抽象软泥形上下错位合拍,中央小星点带出一条短短的流线,表达 AI 把脑洞捏成会传播的作品。风格轻快、年轻、抽象、生动,但不要像表情包或特效贴纸。禁止手、眼睛、聊天气泡、笑脸、花朵、播放键、褐色、3D、文字。配色:珊瑚红、青绿、奶白,最多 3 色,元素要少。', + }, + { + id: 'taonier-squish-v2-token', + title: '成型软标', + prompt: + '无文字扁平矢量 Logo,产品名“陶泥儿”。把“软泥合拍”做得更像长期品牌主标:上下两块抽象软泥围成一个完整圆润符号,中间只有一颗小星或圆点,表达创作成型。不要手、眼睛、嘴巴、聊天气泡、播放键、花朵、褐色陶土、复杂碎片。主流、亲和、醒目、可记忆,App icon 风格。配色:珊瑚红、青绿、奶白,最多 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); + } +} + +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(outputDir, { recursive: true }); + const extension = inferExtensionFromBytes(bytes); + const outputPath = path.join(outputDir, `${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 selected = concepts.filter( + (concept) => !onlyIds.length || onlyIds.includes(concept.id), +); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + outputDir, + 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, + verifiedFiles: readdirSync(outputDir).sort(), + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-squish-logo-variants.mjs b/scripts/generate-taonier-squish-logo-variants.mjs new file mode 100644 index 00000000..4f6dcc84 --- /dev/null +++ b/scripts/generate-taonier-squish-logo-variants.mjs @@ -0,0 +1,183 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; + +const repoRoot = process.cwd(); +const outputDir = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-squish-variants', +); + +const variants = [ + { + id: 'taonier-squish-berry-mint', + title: '莓果薄荷', + topStart: '#ff4778', + topEnd: '#ff6b5f', + bottomStart: '#12c9b7', + bottomEnd: '#16b899', + starStart: '#ffd54c', + starEnd: '#ffb82e', + accent: '#ffc545', + background: '#fffaf2', + }, + { + id: 'taonier-squish-candy-pop', + title: '糖果桃青', + topStart: '#ff5fa2', + topEnd: '#ff8670', + bottomStart: '#2ed7c5', + bottomEnd: '#65d8f4', + starStart: '#ffe06f', + starEnd: '#ffbf4d', + accent: '#ffce5e', + background: '#fff8fb', + }, + { + id: 'taonier-squish-jelly-cream', + title: '奶油果冻', + topStart: '#ff758d', + topEnd: '#ff9a70', + bottomStart: '#42d6b5', + bottomEnd: '#7ce3c5', + starStart: '#fff07a', + starEnd: '#ffc955', + accent: '#ffd76a', + background: '#fffdf4', + }, + { + id: 'taonier-squish-bubble-bright', + title: '亮彩泡泡', + topStart: '#ff3f8f', + topEnd: '#ff6d6d', + bottomStart: '#00c2b8', + bottomEnd: '#00d69a', + starStart: '#fff15c', + starEnd: '#ffbe35', + accent: '#ffbd3c', + background: '#fdfcff', + }, + { + id: 'taonier-squish-sunny-coral', + title: '暖日珊瑚', + topStart: '#ff684f', + topEnd: '#ff8d67', + bottomStart: '#22c4a8', + bottomEnd: '#4dd9b5', + starStart: '#ffe36d', + starEnd: '#ffb948', + accent: '#ffc04a', + background: '#fff8ed', + }, + { + id: 'taonier-squish-neon-cute', + title: '霓虹可爱', + topStart: '#ff3d7f', + topEnd: '#ff4fb8', + bottomStart: '#00bfae', + bottomEnd: '#00d2ff', + starStart: '#fff16a', + starEnd: '#ffd13d', + accent: '#ffcf45', + background: '#fbfbff', + }, +]; + +function buildLogoSvg(variant, includeLabel = false) { + const labelHeight = includeLabel ? 72 : 0; + const height = 1024 + labelHeight; + const labelMarkup = includeLabel + ? ` + + ${variant.title}` + : ''; + + return ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${labelMarkup} +`; +} + +function buildContactSheetSvg() { + const cell = 320; + const label = 64; + const gap = 28; + const width = cell * 3 + gap * 4; + const height = (cell + label) * 2 + gap * 3; + const items = variants + .map((variant, index) => { + const row = Math.floor(index / 3); + const col = index % 3; + const x = gap + col * (cell + gap); + const y = gap + row * (cell + label + gap); + const logo = buildLogoSvg(variant) + .replace(/<\?xml[^>]+>\n/u, '') + .replace('', ``); + return `${logo} + + ${String(index + 1).padStart(2, '0')} ${variant.title}`; + }) + .join('\n'); + + return ` + + + ${items} +`; +} + +mkdirSync(outputDir, { recursive: true }); + +for (const variant of variants) { + writeFileSync( + path.join(outputDir, `${variant.id}.svg`), + buildLogoSvg(variant), + 'utf8', + ); +} + +writeFileSync( + path.join(outputDir, 'taonier-squish-variants-contact-sheet.svg'), + buildContactSheetSvg(), + 'utf8', +); + +console.log( + JSON.stringify( + { + ok: true, + outputDir, + files: [...variants.map((variant) => `${variant.id}.svg`), 'taonier-squish-variants-contact-sheet.svg'], + }, + null, + 2, + ), +); diff --git a/scripts/generate-taonier-squish-recolor-variants.py b/scripts/generate-taonier-squish-recolor-variants.py new file mode 100644 index 00000000..7dede059 --- /dev/null +++ b/scripts/generate-taonier-squish-recolor-variants.py @@ -0,0 +1,272 @@ +from __future__ import annotations + +import colorsys +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SOURCE_PATH = ( + REPO_ROOT + / "public" + / "branding" + / "taonier-logo-magic-dot-concepts" + / "taonier-magic-dot-squish.png" +) +OUTPUT_DIR = ( + REPO_ROOT / "public" / "branding" / "taonier-logo-squish-recolor-variants" +) + + +VARIANTS = [ + { + "id": "taonier-squish-recolor-original-plus", + "title": "原版提亮", + "top": ("#ff3f74", "#ff5a8c"), + "bottom": ("#10c6b1", "#19d5b8"), + "star": ("#ffd249", "#ffc13c"), + "background": "#fffdf8", + "saturation": 1.04, + "value": 1.02, + }, + { + "id": "taonier-squish-recolor-candy-mint", + "title": "糖果薄荷", + "top": ("#ff5aa0", "#ff7786"), + "bottom": ("#1fd3c2", "#46e0cc"), + "star": ("#ffe071", "#ffc64b"), + "background": "#fffafd", + "saturation": 1.02, + "value": 1.05, + }, + { + "id": "taonier-squish-recolor-peach-jelly", + "title": "桃桃果冻", + "top": ("#ff6b8d", "#ff8b72"), + "bottom": ("#30cfb7", "#72dec5"), + "star": ("#ffe586", "#ffc75c"), + "background": "#fffaf2", + "saturation": 0.96, + "value": 1.07, + }, + { + "id": "taonier-squish-recolor-pop-bright", + "title": "亮彩出圈", + "top": ("#ff2f82", "#ff4faf"), + "bottom": ("#00c5b9", "#00d8e8"), + "star": ("#fff15a", "#ffc735"), + "background": "#fbfbff", + "saturation": 1.10, + "value": 1.03, + }, + { + "id": "taonier-squish-recolor-coral-soda", + "title": "珊瑚苏打", + "top": ("#ff674d", "#ff8372"), + "bottom": ("#17c5a9", "#55dabc"), + "star": ("#ffe26f", "#ffbe43"), + "background": "#fff9ee", + "saturation": 0.98, + "value": 1.05, + }, + { + "id": "taonier-squish-recolor-bubble-q", + "title": "泡泡Q感", + "top": ("#ff68ba", "#ff77a0"), + "bottom": ("#35d8c9", "#73e7d8"), + "star": ("#fff08c", "#ffd35c"), + "background": "#fffaff", + "saturation": 0.92, + "value": 1.09, + }, +] + + +def hex_to_rgb(value: str) -> tuple[int, int, int]: + value = value.removeprefix("#") + return tuple(int(value[index : index + 2], 16) for index in (0, 2, 4)) + + +def blend_rgb( + first: tuple[int, int, int], second: tuple[int, int, int], amount: float +) -> tuple[int, int, int]: + amount = max(0.0, min(1.0, amount)) + return tuple(round(first[index] * (1 - amount) + second[index] * amount) for index in range(3)) + + +def classify_pixel(red: int, green: int, blue: int) -> str | None: + hue, saturation, value = colorsys.rgb_to_hsv(red / 255, green / 255, blue / 255) + + if value < 0.42 or saturation < 0.09: + return None + if red > 145 and green > 105 and blue < 170 and red > blue + 20 and green > blue + 16: + return "star" + if red > 140 and red > green + 18 and red > blue + 12 and (hue < 0.08 or hue > 0.9): + return "top" + if green > 105 and blue > 88 and green > red + 15 and blue > red + 6 and 0.36 <= hue <= 0.58: + return "bottom" + return None + + +def remap_color( + red: int, + green: int, + blue: int, + x: int, + y: int, + width: int, + height: int, + group: str, + variant: dict[str, object], +) -> tuple[int, int, int]: + hue, saturation, value = colorsys.rgb_to_hsv(red / 255, green / 255, blue / 255) + palette = variant[group] + assert isinstance(palette, tuple) + start = hex_to_rgb(palette[0]) + end = hex_to_rgb(palette[1]) + + if group == "top": + gradient_position = 0.68 * (x / width) + 0.32 * (y / height) + elif group == "bottom": + gradient_position = 0.42 * (x / width) + 0.58 * (y / height) + else: + gradient_position = 0.18 * (x / width) + 0.82 * (y / height) + + target = blend_rgb(start, end, gradient_position) + target_hue, target_saturation, target_value = colorsys.rgb_to_hsv( + target[0] / 255, target[1] / 255, target[2] / 255 + ) + + saturation_boost = float(variant["saturation"]) + value_boost = float(variant["value"]) + new_saturation = max(0.0, min(1.0, target_saturation * saturation_boost)) + # 原图本身有轻微明暗变化,这里保留它,让换色后仍然像同一个软泥形体。 + new_value = max(0.0, min(1.0, target_value * (0.78 + value * 0.22) * value_boost)) + recolored = colorsys.hsv_to_rgb(target_hue, new_saturation, new_value) + + background = hex_to_rgb(str(variant["background"])) + edge_coverage = max(0.0, min(1.0, (saturation - 0.08) / 0.55)) + if group == "star": + edge_coverage = max(0.0, min(1.0, (saturation - 0.06) / 0.5)) + + foreground = tuple(round(channel * 255) for channel in recolored) + return blend_rgb(background, foreground, edge_coverage) + + +def recolor_variant(source: Image.Image, variant: dict[str, object]) -> Image.Image: + image = source.convert("RGBA") + width, height = image.size + background = hex_to_rgb(str(variant["background"])) + result = Image.new("RGBA", image.size, (*background, 255)) + + source_pixels = image.load() + result_pixels = result.load() + for y in range(height): + for x in range(width): + red, green, blue, alpha = source_pixels[x, y] + if alpha == 0: + result_pixels[x, y] = (*background, 0) + continue + + group = classify_pixel(red, green, blue) + if group is None: + if red > 238 and green > 238 and blue > 238: + result_pixels[x, y] = (*background, alpha) + else: + result_pixels[x, y] = (red, green, blue, alpha) + continue + + new_red, new_green, new_blue = remap_color( + red, green, blue, x, y, width, height, group, variant + ) + result_pixels[x, y] = (new_red, new_green, new_blue, alpha) + + return result + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + Path("C:/Windows/Fonts/simsun.ttc"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() + + +def paste_cell( + sheet: Image.Image, + image: Image.Image, + label: str, + index: int, + font: ImageFont.FreeTypeFont | ImageFont.ImageFont, +) -> None: + cell = 310 + label_height = 54 + gap = 28 + columns = 4 + row = index // columns + column = index % columns + x = gap + column * (cell + gap) + y = gap + row * (cell + label_height + gap) + + draw = ImageDraw.Draw(sheet) + thumb = image.resize((cell, cell), Image.Resampling.LANCZOS) + sheet.alpha_composite(thumb, (x, y)) + draw.rounded_rectangle( + (x, y + cell, x + cell, y + cell + label_height), + radius=10, + fill=(255, 253, 248, 255), + ) + text = f"{index:02d} {label}" + text_box = draw.textbbox((0, 0), text, font=font) + text_x = x + (cell - (text_box[2] - text_box[0])) / 2 + text_y = y + cell + (label_height - (text_box[3] - text_box[1])) / 2 - 2 + draw.text((text_x, text_y), text, fill=(50, 42, 36, 255), font=font) + + +def build_contact_sheet(original: Image.Image, outputs: list[tuple[dict[str, object], Image.Image]]) -> Image.Image: + cell = 310 + label_height = 54 + gap = 28 + columns = 4 + rows = 2 + width = columns * cell + (columns + 1) * gap + height = rows * (cell + label_height) + (rows + 1) * gap + sheet = Image.new("RGBA", (width, height), (246, 242, 235, 255)) + font = load_font(22) + + paste_cell(sheet, original.convert("RGBA"), "原参考", 0, font) + for index, (variant, image) in enumerate(outputs, start=1): + paste_cell(sheet, image, str(variant["title"]), index, font) + return sheet + + +def main() -> None: + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + source = Image.open(SOURCE_PATH) + outputs: list[tuple[dict[str, object], Image.Image]] = [] + + for variant in VARIANTS: + image = recolor_variant(source, variant) + image.save(OUTPUT_DIR / f"{variant['id']}.png") + outputs.append((variant, image)) + + contact_sheet = build_contact_sheet(source, outputs) + contact_sheet.convert("RGB").save(OUTPUT_DIR / "taonier-squish-recolor-contact-sheet.png") + + print( + { + "ok": True, + "output_dir": str(OUTPUT_DIR), + "files": [f"{variant['id']}.png" for variant in VARIANTS] + + ["taonier-squish-recolor-contact-sheet.png"], + } + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/make-taonier-hand-spirit-transparent.mjs b/scripts/make-taonier-hand-spirit-transparent.mjs new file mode 100644 index 00000000..29741367 --- /dev/null +++ b/scripts/make-taonier-hand-spirit-transparent.mjs @@ -0,0 +1,319 @@ +import { Blob, Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const timeoutMsDefault = 180000; + +const sourcePath = path.join( + repoRoot, + 'public', + 'branding', + 'taonier-logo-hand-spirit-concepts', + 'taonier-hand-spirit-01-gentle-hand-spirit.png', +); +const outputDir = path.dirname(sourcePath); +const chromaPath = path.join( + outputDir, + 'taonier-hand-spirit-01-gentle-hand-spirit-transparent-source.png', +); +const manifestPath = path.join( + outputDir, + 'taonier-hand-spirit-01-gentle-hand-spirit-transparent-manifest.json', +); + +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 prompt = [ + 'Use the uploaded image as the exact edit target.', + 'Preserve the logo subject exactly: same abstract hand shape, same clay spirit shape, same proportions, same placement, same scale, same colors, same soft vector style.', + 'Do not redesign, simplify, recolor, crop, rotate, add details, remove highlights, change the hand, or change the clay spirit.', + 'Replace only the white/off-white background with a perfectly flat solid #00ff00 chroma-key background.', + 'The background must be one uniform #00ff00 color with no shadows, gradients, texture, reflections, floor plane, border, or lighting variation.', + 'Do not use #00ff00 anywhere inside the logo subject.', + 'No text, no watermark, no UI, no extra marks, no border.', + 'Keep crisp clean edges and generous safe area so the result can be converted into a transparent PNG.', +].join('\n'); + +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 || timeoutMsDefault), + 10, + ), + }; +} + +function buildEditUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/edits` + : `${baseUrl}/v1/images/edits`; +} + +function buildDryRunFields() { + return { + model: 'gpt-image-2', + prompt, + n: '1', + size: '1024x1024', + image: sourcePath, + }; +} + +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); + } +} + +function createEditFormData() { + const form = new FormData(); + const imageBytes = readFileSync(sourcePath); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('n', '1'); + form.append('size', '1024x1024'); + form.append( + 'image', + new Blob([imageBytes], { type: 'image/png' }), + path.basename(sourcePath), + ); + return form; +} + +async function generateChromaSource() { + const env = resolveEnv(); + if (!env.baseUrl || !env.apiKey) { + throw new Error( + JSON.stringify({ + ok: false, + error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY', + hasBaseUrl: Boolean(env.baseUrl), + hasApiKey: Boolean(env.apiKey), + }), + ); + } + + const payload = await fetchJson( + buildEditUrl(env.baseUrl), + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.apiKey}`, + Accept: 'application/json', + }, + body: createEditFormData(), + }, + 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'); + } + + mkdirSync(outputDir, { recursive: true }); + writeFileSync(chromaPath, bytes); + const extension = inferExtensionFromBytes(bytes); + writeFileSync( + manifestPath, + `${JSON.stringify( + { + model: 'gpt-image-2', + endpoint: '/v1/images/edits', + size: '1024x1024', + source: path.relative(repoRoot, sourcePath), + chromaSource: path.relative(repoRoot, chromaPath), + finalOutput: + 'public/branding/taonier-logo-hand-spirit-concepts/taonier-hand-spirit-01-gentle-hand-spirit-transparent.png', + generatedAt: new Date().toISOString(), + prompt, + }, + null, + 2, + )}\n`, + 'utf8', + ); + + return { chromaPath, manifestPath, extension, bytes: bytes.length }; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); + +if (dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + sourcePath, + chromaPath, + manifestPath, + fields: buildDryRunFields(), + }, + null, + 2, + ), + ); + process.exit(0); +} + +if (!existsSync(sourcePath)) { + console.error( + JSON.stringify({ ok: false, error: 'Source image does not exist', sourcePath }), + ); + process.exit(1); +} + +console.log(JSON.stringify({ ok: true, ...(await generateChromaSource()) }, null, 2)); diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index 6cb28534..ff327c57 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -1947,6 +1947,14 @@ dependencies = [ "spacetimedb", ] +[[package]] +name = "module-wooden-fish" +version = "0.1.0" +dependencies = [ + "serde", + "spacetimedb", +] + [[package]] name = "moxcms" version = "0.8.1" @@ -3278,6 +3286,7 @@ dependencies = [ "module-square-hole", "module-story", "module-visual-novel", + "module-wooden-fish", "opentelemetry", "serde", "serde_json", @@ -3311,6 +3320,7 @@ dependencies = [ "module-runtime-item", "module-square-hole", "module-story", + "module-wooden-fish", "serde", "serde_json", "sha2", diff --git a/server-rs/Cargo.toml b/server-rs/Cargo.toml index 1531dfc3..577c61bd 100644 --- a/server-rs/Cargo.toml +++ b/server-rs/Cargo.toml @@ -18,6 +18,7 @@ members = [ "crates/module-inventory", "crates/module-custom-world", "crates/module-jump-hop", + "crates/module-wooden-fish", "crates/module-match3d", "crates/module-npc", "crates/module-puzzle", @@ -59,6 +60,7 @@ module-creative-agent = { path = "crates/module-creative-agent", default-feature module-custom-world = { path = "crates/module-custom-world", default-features = false } module-inventory = { path = "crates/module-inventory", default-features = false } module-jump-hop = { path = "crates/module-jump-hop", default-features = false } +module-wooden-fish = { path = "crates/module-wooden-fish", default-features = false } module-match3d = { path = "crates/module-match3d", default-features = false } module-npc = { path = "crates/module-npc", default-features = false } module-progression = { path = "crates/module-progression", default-features = false } diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index ce7e6c8e..0a4afeaf 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -60,6 +60,7 @@ pub fn build_router(state: AppState) -> Router { .merge(modules::match3d::router(state.clone())) .merge(modules::square_hole::router(state.clone())) .merge(modules::jump_hop::router(state.clone())) + .merge(modules::wooden_fish::router(state.clone())) .merge(modules::puzzle::router(state.clone())) .merge(visual_novel_router(state.clone())) .route( diff --git a/server-rs/crates/api-server/src/creation_entry_config.rs b/server-rs/crates/api-server/src/creation_entry_config.rs index fe6ea73c..fda55d57 100644 --- a/server-rs/crates/api-server/src/creation_entry_config.rs +++ b/server-rs/crates/api-server/src/creation_entry_config.rs @@ -8,8 +8,8 @@ use axum::{ }; use serde_json::{Value, json}; -#[cfg(test)] use module_runtime::build_creation_entry_config_response; +use shared_contracts::creation_entry_config::CreationEntryConfigResponse; use crate::{ api_response::json_success_body, http_error::AppError, request_context::RequestContext, @@ -84,6 +84,12 @@ pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> { if normalized.starts_with("/api/creation/bark-battle") { return Some("bark-battle"); } + if normalized.starts_with("/api/runtime/wooden-fish") { + return Some("wooden-fish"); + } + if normalized.starts_with("/api/creation/wooden-fish") { + return Some("wooden-fish"); + } if normalized.starts_with("/api/runtime/square-hole") { return Some("square-hole"); } @@ -123,9 +129,8 @@ fn creation_entry_error_response(request_context: &RequestContext, error: AppErr error.into_response_with_context(Some(request_context)) } -#[cfg(test)] -pub(crate) fn test_creation_entry_config_response() --> shared_contracts::creation_entry_config::CreationEntryConfigResponse { +/// 中文注释:本地 debug 兜底也来自后端领域默认种子,避免前端恢复硬编码入口配置。 +pub(crate) fn default_creation_entry_config_response() -> CreationEntryConfigResponse { build_creation_entry_config_response(module_runtime::CreationEntryConfigSnapshot { config_id: module_runtime::CREATION_ENTRY_CONFIG_GLOBAL_ID.to_string(), start_card: module_runtime::CreationEntryStartCardSnapshot { @@ -143,6 +148,11 @@ pub(crate) fn test_creation_entry_config_response() }) } +#[cfg(test)] +pub(crate) fn test_creation_entry_config_response() -> CreationEntryConfigResponse { + default_creation_entry_config_response() +} + #[cfg(test)] mod tests { use super::*; @@ -197,6 +207,14 @@ mod tests { resolve_creation_entry_route_id("/api/creation/bark-battle/drafts"), Some("bark-battle"), ); + assert_eq!( + resolve_creation_entry_route_id("/api/runtime/wooden-fish/runs/run-1"), + Some("wooden-fish"), + ); + assert_eq!( + resolve_creation_entry_route_id("/api/creation/wooden-fish/sessions"), + Some("wooden-fish"), + ); assert_eq!( resolve_creation_entry_route_id("/api/creation/edutainment/baby-object-match/assets"), Some("baby-object-match"), @@ -217,10 +235,10 @@ mod tests { .find(|item| item.id == "bark-battle") .expect("test creation entry config should include bark-battle"); - assert_eq!(bark_battle.title, "汪汪声浪"); + assert_eq!(bark_battle.title, "\u{6c6a}\u{6c6a}\u{58f0}\u{6d6a}"); assert!(bark_battle.visible); assert!(bark_battle.open); - assert_eq!(bark_battle.badge, "可创建"); + assert_eq!(bark_battle.badge, "\u{53ef}\u{521b}\u{5efa}"); assert_eq!( bark_battle.image_src, "/creation-type-references/bark-battle.webp" @@ -228,7 +246,7 @@ mod tests { } #[test] - fn test_creation_entry_config_response_keeps_baby_object_match_coming_soon() { + fn test_creation_entry_config_response_keeps_baby_object_match_visible() { let config = test_creation_entry_config_response(); let baby_object_match = config .creation_types @@ -236,10 +254,10 @@ mod tests { .find(|item| item.id == "baby-object-match") .expect("test creation entry config should include baby-object-match"); - assert_eq!(baby_object_match.title, "宝贝识物"); + assert_eq!(baby_object_match.title, "\u{5b9d}\u{8d1d}\u{8bc6}\u{7269}"); assert!(baby_object_match.visible); - assert!(!baby_object_match.open); - assert_eq!(baby_object_match.badge, "敬请期待"); + assert!(baby_object_match.open); + assert_eq!(baby_object_match.badge, "\u{53ef}\u{521b}\u{5efa}"); assert_eq!(baby_object_match.sort_order, 90); } } diff --git a/server-rs/crates/api-server/src/main.rs b/server-rs/crates/api-server/src/main.rs index 0a58d126..a422c80c 100644 --- a/server-rs/crates/api-server/src/main.rs +++ b/server-rs/crates/api-server/src/main.rs @@ -90,6 +90,7 @@ mod volcengine_speech; mod wechat_auth; mod wechat_pay; mod wechat_provider; +mod wooden_fish; mod work_author; mod work_play_tracking; diff --git a/server-rs/crates/api-server/src/match3d/item_assets.rs b/server-rs/crates/api-server/src/match3d/item_assets.rs index 1ecf326d..1365faee 100644 --- a/server-rs/crates/api-server/src/match3d/item_assets.rs +++ b/server-rs/crates/api-server/src/match3d/item_assets.rs @@ -1539,111 +1539,11 @@ pub(super) fn slice_match3d_material_sheet( image: &DownloadedOpenAiImage, item_names: &[String], ) -> Result>, AppError> { - // 中文注释:素材图提示词固定要求 5*5 均匀排布;切图也固定按 5 行 5 列定位格子。 - // 每个格子内再基于前景像素二次校准,避免固定内缩裁断物品边缘。 - let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| { - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": "match3d-assets", - "message": format!("抓大鹅素材图解码失败:{error}"), - })) - })?; - // 中文注释:素材图按绿幕背景生成;先把整张 sheet 的绿幕转成 alpha,再进入格子裁切。 - let source = apply_match3d_material_green_screen_alpha(source); - let (width, height) = source.dimensions(); - let row_count = MATCH3D_MATERIAL_GRID_SIZE; - let cell_width = width / MATCH3D_MATERIAL_GRID_SIZE; - let cell_height = height / row_count; - if cell_width == 0 || cell_height == 0 { - return Err( - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": "match3d-assets", - "message": "抓大鹅素材图尺寸过小,无法切割", - })), - ); - } - - let mut slices = Vec::with_capacity(item_names.len()); - for item_index in 0..item_names.len().min(MATCH3D_MATERIAL_ITEM_BATCH_SIZE) { - let row = item_index as u32; - let mut views = Vec::with_capacity(MATCH3D_ITEM_VIEW_COUNT); - for view_index in 0..MATCH3D_ITEM_VIEW_COUNT { - let col = view_index as u32; - let (crop_x, crop_y, crop_width, crop_height) = - resolve_match3d_material_cell_crop(&source, row_count, row, col); - let cropped = source.crop_imm(crop_x, crop_y, crop_width, crop_height); - let cleaned = crop_match3d_material_view_edge_matte(cropped); - let mut cursor = std::io::Cursor::new(Vec::new()); - cleaned - .write_to(&mut cursor, ImageFormat::Png) - .map_err(|error| { - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": "match3d-assets", - "message": format!("抓大鹅素材图切割失败:{error}"), - })) - })?; - views.push(Match3DSlicedItemImage { - bytes: cursor.into_inner(), - }); - } - slices.push(views); - } - - Ok(slices) -} - -fn resolve_match3d_material_cell_crop( - source: &image::DynamicImage, - row_count: u32, - row: u32, - col: u32, -) -> (u32, u32, u32, u32) { - let (image_width, image_height) = source.dimensions(); - let cell = resolve_match3d_material_cell_bounds(image_width, image_height, row_count, row, col); - let Some(foreground) = detect_match3d_material_foreground_bounds(source, cell) else { - return cell.to_crop_tuple(); - }; - - let cell_width = cell.width(); - let cell_height = cell.height(); - let pad_x = (cell_width / 16).clamp(4, 16); - let pad_y = (cell_height / 16).clamp(4, 16); - let crop = Match3DMaterialCellBounds { - x0: foreground.x0.saturating_sub(pad_x).max(cell.x0), - y0: foreground.y0.saturating_sub(pad_y).max(cell.y0), - x1: foreground.x1.saturating_add(pad_x).min(cell.x1), - y1: foreground.y1.saturating_add(pad_y).min(cell.y1), - }; - - crop.to_crop_tuple() -} - -pub(super) fn crop_match3d_material_view_edge_matte( - image: image::DynamicImage, -) -> image::DynamicImage { - let mut image = image.to_rgba8(); - let (width, height) = image.dimensions(); - remove_match3d_material_view_edge_matte(image.as_mut(), width as usize, height as usize); - let bounds = detect_match3d_material_visible_bounds(&image).unwrap_or_else(|| { - Match3DMaterialCellBounds { - x0: 0, - y0: 0, - x1: width, - y1: height, - } - }); - if bounds.x0 == 0 && bounds.y0 == 0 && bounds.x1 == width && bounds.y1 == height { - return image::DynamicImage::ImageRgba8(image); - } - - image::DynamicImage::ImageRgba8( - image::imageops::crop_imm( - &image, - bounds.x0, - bounds.y0, - bounds.width(), - bounds.height(), - ) - .to_image(), + slice_generated_asset_sheet_two_items_per_row( + image, + item_names, + MATCH3D_MATERIAL_GRID_SIZE as usize, + MATCH3D_ITEM_VIEW_COUNT, ) .map(|rows| { rows.into_iter() diff --git a/server-rs/crates/api-server/src/match3d/works.rs b/server-rs/crates/api-server/src/match3d/works.rs index 2a710519..b8f8bc2a 100644 --- a/server-rs/crates/api-server/src/match3d/works.rs +++ b/server-rs/crates/api-server/src/match3d/works.rs @@ -716,6 +716,40 @@ pub(super) fn load_match3d_container_reference_image() -> Result String { + let theme = config.theme_text.trim(); + let theme = if theme.is_empty() { + MATCH3D_DEFAULT_THEME + } else { + theme + }; + let style_clause = resolve_match3d_asset_style_prompt(config) + .map(|style| format!("\n整体美术风格要求:{style}")) + .unwrap_or_default(); + + format!( + concat!( + "生成抓大鹅游戏关卡画面,要求画面中所有元素精致且风格高度一致,画面中所有UI细节饱满精致、完成度高、顶级游戏品质\n\n", + "抓大鹅主题描述:\n", + "{theme}{style_clause}\n\n", + "画面元素:\n", + "返回按钮位于顶部左上角,顶部中间显示关卡标题“第1关 重庆火锅”和倒计时时间,右上角显示设置按钮\n", + "画面中间是一个和主题匹配的容器,宽度与画面宽度同宽,紧贴画面横向边缘\n", + "底部还有三个道具按钮分别为“移出”、“凑齐”、“打乱”" + ), + theme = theme, + style_clause = style_clause, + ) +} + +pub(super) fn build_match3d_ui_spritesheet_prompt() -> String { + "提取画面中的UI元素,将返回按钮、设置按钮、方格素材(不含边框,仅保留一个)、移出按钮、凑齐按钮、打乱按钮的顺序从上到下从左到右整理成纯绿色绿幕背景spritesheet。背景必须是统一纯绿色绿幕(高饱和亮绿色,接近 #00FF00),背景平整无纹理、无渐变、无阴影、无场景内容,后端会在生图后将绿幕扣成透明并把透明背景 PNG 存到 OSS。UI 素材自身不得使用接近 #00FF00 的高饱和纯绿;绿色题材只能使用深绿、橄榄绿、金绿或蓝绿,并用清晰描边与绿幕区分。".to_string() +} + +pub(super) fn build_match3d_background_from_scene_prompt() -> String { + "移除画面中的所有UI组件和容器中的内含物,完整保留容器和背景,补全被UI覆盖的背景内容".to_string() +} + pub(super) fn build_match3d_background_generation_prompt( config: &Match3DConfigJson, prompt: &str, diff --git a/server-rs/crates/api-server/src/modules.rs b/server-rs/crates/api-server/src/modules.rs index a86abae9..d890a36f 100644 --- a/server-rs/crates/api-server/src/modules.rs +++ b/server-rs/crates/api-server/src/modules.rs @@ -14,3 +14,4 @@ pub mod profile; pub mod puzzle; pub mod square_hole; pub mod story; +pub mod wooden_fish; diff --git a/server-rs/crates/api-server/src/modules/wooden_fish.rs b/server-rs/crates/api-server/src/modules/wooden_fish.rs new file mode 100644 index 00000000..f9ad51a3 --- /dev/null +++ b/server-rs/crates/api-server/src/modules/wooden_fish.rs @@ -0,0 +1,80 @@ +use axum::{ + Router, middleware, + routing::{get, post}, +}; + +use crate::{ + auth::require_bearer_auth, + state::AppState, + wooden_fish::{ + checkpoint_wooden_fish_run, create_wooden_fish_session, execute_wooden_fish_action, + finish_wooden_fish_run, get_wooden_fish_gallery_detail, get_wooden_fish_runtime_work, + get_wooden_fish_session, list_wooden_fish_gallery, publish_wooden_fish_work, + start_wooden_fish_run, + }, +}; + +pub fn router(state: AppState) -> Router { + Router::new() + .route( + "/api/creation/wooden-fish/sessions", + post(create_wooden_fish_session).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/creation/wooden-fish/sessions/{session_id}", + get(get_wooden_fish_session).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/creation/wooden-fish/sessions/{session_id}/actions", + post(execute_wooden_fish_action).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/creation/wooden-fish/works/{profile_id}/publish", + post(publish_wooden_fish_work).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/wooden-fish/works/{profile_id}", + get(get_wooden_fish_runtime_work), + ) + .route( + "/api/runtime/wooden-fish/runs", + post(start_wooden_fish_run).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/wooden-fish/runs/{run_id}/checkpoint", + post(checkpoint_wooden_fish_run).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/wooden-fish/runs/{run_id}/finish", + post(finish_wooden_fish_run).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/wooden-fish/gallery", + get(list_wooden_fish_gallery), + ) + .route( + "/api/runtime/wooden-fish/gallery/{public_work_code}", + get(get_wooden_fish_gallery_detail), + ) +} diff --git a/server-rs/crates/api-server/src/openai_image_generation.rs b/server-rs/crates/api-server/src/openai_image_generation.rs index 3516948e..f9422db4 100644 --- a/server-rs/crates/api-server/src/openai_image_generation.rs +++ b/server-rs/crates/api-server/src/openai_image_generation.rs @@ -423,13 +423,14 @@ pub(crate) async fn create_openai_image_edit_with_references( return Err( AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": VECTOR_ENGINE_PROVIDER, - "message": format!("{failure_context}:图片编辑需要至少一张参考图。"), + "message": format!("{failure_context}:缺少参考图,图片编辑需要至少一张参考图。"), })), ); } let request_url = vector_engine_images_edit_url(settings); let normalized_size = normalize_image_size(size); + let mut form = reqwest::multipart::Form::new() .text("model", GPT_IMAGE_2_MODEL.to_string()) .text( @@ -1277,6 +1278,33 @@ mod tests { ); } + #[tokio::test] + async fn vector_engine_multi_reference_edit_rejects_empty_references() { + let settings = OpenAiImageSettings { + base_url: "https://vector.example".to_string(), + api_key: "test-key".to_string(), + request_timeout_ms: 1_000_000, + external_api_audit_state: None, + }; + let http_client = reqwest::Client::new(); + + let result = create_openai_image_edit_with_references( + &http_client, + &settings, + "提示词", + None, + "1:1", + 1, + &[], + "测试图片编辑失败", + ) + .await; + + let error = result.expect_err("empty references should be rejected locally"); + assert_eq!(error.status_code(), StatusCode::BAD_REQUEST); + assert!(error.body_text().contains("缺少参考图")); + } + #[test] fn reference_data_url_resolves_to_edit_image_part() { let source = format!( diff --git a/server-rs/crates/api-server/src/state.rs b/server-rs/crates/api-server/src/state.rs index 5458e693..6c6d1c60 100644 --- a/server-rs/crates/api-server/src/state.rs +++ b/server-rs/crates/api-server/src/state.rs @@ -463,6 +463,14 @@ impl AppState { self.cache_test_creation_entry_config(config.clone()); Ok(config) } + #[cfg(debug_assertions)] + Err(error) if is_missing_creation_entry_config_procedure(&error) => { + warn!( + error = %error, + "本地 SpacetimeDB 缺少创作入口配置 procedure,使用后端默认入口配置兜底" + ); + Ok(crate::creation_entry_config::default_creation_entry_config_response()) + } #[cfg(test)] Err(_) => Ok(self.read_test_creation_entry_config()), #[cfg(not(test))] @@ -1327,12 +1335,35 @@ fn build_admin_runtime( })) } +#[cfg(debug_assertions)] +fn is_missing_creation_entry_config_procedure(error: &SpacetimeClientError) -> bool { + match error { + SpacetimeClientError::Procedure(message) => message.contains("No such procedure"), + _ => false, + } +} + #[cfg(test)] mod tests { use module_ai::{AiTaskKind, generate_ai_task_id}; use super::*; + #[test] + fn detects_missing_creation_entry_config_procedure_for_debug_fallback() { + assert!(is_missing_creation_entry_config_procedure( + &SpacetimeClientError::Procedure( + "No such procedure: get_creation_entry_config".to_string(), + ), + )); + assert!(is_missing_creation_entry_config_procedure( + &SpacetimeClientError::Procedure("No such procedure".to_string()), + )); + assert!(!is_missing_creation_entry_config_procedure( + &SpacetimeClientError::Timeout, + )); + } + #[test] fn app_state_exposes_usable_ai_task_service() { let state = AppState::new(AppConfig::default()).expect("state should build"); diff --git a/server-rs/crates/api-server/src/vector_engine_audio_generation.rs b/server-rs/crates/api-server/src/vector_engine_audio_generation.rs index d5708bf3..04e51f6e 100644 --- a/server-rs/crates/api-server/src/vector_engine_audio_generation.rs +++ b/server-rs/crates/api-server/src/vector_engine_audio_generation.rs @@ -233,13 +233,15 @@ pub async fn create_visual_novel_sound_effect_task( } pub async fn create_sound_effect_task( - State(_state): State, + State(state): State, axum::extract::Extension(request_context): axum::extract::Extension, payload: Result, JsonRejection>, ) -> Result, Response> { - let _ = parse_json_payload(&request_context, payload)?; - Err(creation_audio_generation_disabled_error() - .into_response_with_context(Some(&request_context))) + let Json(payload) = parse_json_payload(&request_context, payload)?; + create_sound_effect_task_response(&state, payload.prompt, payload.duration, payload.seed) + .await + .map(|task| json_success_body(Some(&request_context), task)) + .map_err(|error| error.into_response_with_context(Some(&request_context))) } pub(crate) async fn generate_sound_effect_asset_for_creation( @@ -518,15 +520,25 @@ pub async fn publish_background_music_asset( } pub async fn publish_sound_effect_asset( - State(_state): State, - Path(_task_id): Path, + State(state): State, + Path(task_id): Path, axum::extract::Extension(request_context): axum::extract::Extension, - axum::extract::Extension(_authenticated): axum::extract::Extension, + axum::extract::Extension(authenticated): axum::extract::Extension, payload: Result, JsonRejection>, ) -> Result, Response> { let payload = parse_json_payload(&request_context, payload)?.0; - Err(creation_audio_generation_disabled_error_for_target(payload) - .into_response_with_context(Some(&request_context))) + let target = build_creation_audio_target(payload, AudioAssetSlot::SoundEffect) + .map_err(|error| error.into_response_with_context(Some(&request_context)))?; + publish_generated_audio_asset( + &state, + authenticated.claims().user_id(), + task_id, + AudioAssetSlot::SoundEffect, + target, + ) + .await + .map(|payload| json_success_body(Some(&request_context), payload)) + .map_err(|error| error.into_response_with_context(Some(&request_context))) } async fn publish_generated_audio_asset( @@ -860,10 +872,36 @@ fn build_visual_novel_audio_target( }) } +fn build_creation_audio_target( + payload: creation_audio::PublishGeneratedAudioAssetRequest, + slot: AudioAssetSlot, +) -> Result { + if matches!(slot, AudioAssetSlot::SoundEffect) + && payload.entity_kind.trim() == "wooden_fish_work" + && payload.slot.trim() == "hit_sound" + && payload.asset_kind.trim() == "wooden_fish_hit_sound" + && payload.storage_prefix + == Some(creation_audio::CreationAudioStoragePrefix::WoodenFishAssets) + { + let entity_id = normalize_limited_text(&payload.entity_id, "entityId", 160)?; + return Ok(AudioAssetBindingTarget { + storage_scope: payload.entity_kind.trim().to_string(), + entity_kind: payload.entity_kind.trim().to_string(), + entity_id, + slot: payload.slot.trim().to_string(), + asset_kind: payload.asset_kind.trim().to_string(), + profile_id: normalize_optional_text(payload.profile_id.as_deref()), + storage_prefix: LegacyAssetPrefix::WoodenFishAssets, + }); + } + + Err(creation_audio_generation_disabled_error_for_target(payload)) +} + fn creation_audio_generation_disabled_error() -> AppError { AppError::from_status(StatusCode::GONE).with_details(json!({ "provider": VECTOR_ENGINE_PROVIDER, - "message": "拼图与抓大鹅音频生成入口已临时关闭", + "message": "当前创作音频目标未开放", })) } @@ -872,8 +910,9 @@ fn creation_audio_generation_disabled_error_for_target( ) -> AppError { creation_audio_generation_disabled_error().with_details(json!({ "provider": VECTOR_ENGINE_PROVIDER, - "message": "拼图与抓大鹅音频生成入口已临时关闭", + "message": "当前创作音频目标未开放", "entityKind": payload.entity_kind.trim(), + "slot": payload.slot.trim(), })) } @@ -1434,7 +1473,7 @@ mod tests { } #[test] - fn disabled_creation_audio_targets_return_gone() { + fn disabled_creation_audio_targets_return_gone_except_wooden_fish_sound_effects() { let payload = creation_audio::PublishGeneratedAudioAssetRequest { entity_kind: "puzzle_work".to_string(), entity_id: "puzzle-profile-1".to_string(), @@ -1467,6 +1506,22 @@ mod tests { }; let error = creation_audio_generation_disabled_error_for_target(payload); assert_eq!(error.status_code(), StatusCode::GONE); + + let payload = creation_audio::PublishGeneratedAudioAssetRequest { + entity_kind: "wooden_fish_work".to_string(), + entity_id: "wooden-fish-profile-1".to_string(), + slot: "hit_sound".to_string(), + asset_kind: "wooden_fish_hit_sound".to_string(), + profile_id: Some("wooden-fish-profile-1".to_string()), + storage_prefix: Some(creation_audio::CreationAudioStoragePrefix::WoodenFishAssets), + }; + let target = build_creation_audio_target(payload, AudioAssetSlot::SoundEffect) + .expect("wooden fish hit sound target should be enabled"); + + assert_eq!(target.entity_kind, "wooden_fish_work"); + assert_eq!(target.slot, "hit_sound"); + assert_eq!(target.storage_prefix, LegacyAssetPrefix::WoodenFishAssets); + assert_eq!(target.storage_scope, "wooden_fish_work"); } #[test] diff --git a/server-rs/crates/api-server/src/wooden_fish.rs b/server-rs/crates/api-server/src/wooden_fish.rs new file mode 100644 index 00000000..016cfa3a --- /dev/null +++ b/server-rs/crates/api-server/src/wooden_fish.rs @@ -0,0 +1,1309 @@ +use std::{ + collections::BTreeMap, + time::{SystemTime, UNIX_EPOCH}, +}; + +use axum::{ + Json, + extract::{Extension, Path, State, rejection::JsonRejection}, + http::{HeaderName, StatusCode, header}, + response::Response, +}; +use module_assets::{ + AssetObjectAccessPolicy, AssetObjectFieldError, build_asset_entity_binding_input, + build_asset_object_upsert_input, generate_asset_binding_id, generate_asset_object_id, +}; +use platform_oss::{LegacyAssetPrefix, OssHeadObjectRequest, OssObjectAccess}; +use serde_json::{Value, json}; +use shared_contracts::wooden_fish::{ + WoodenFishActionRequest, WoodenFishAudioAsset, WoodenFishCheckpointRunRequest, + WoodenFishDraftResponse, WoodenFishFinishRunRequest, WoodenFishGalleryDetailResponse, + WoodenFishGenerationStatus, WoodenFishImageAsset, WoodenFishRunResponse, + WoodenFishSessionResponse, WoodenFishSessionSnapshotResponse, WoodenFishStartRunRequest, + WoodenFishWorkDetailResponse, WoodenFishWorkMutationResponse, WoodenFishWorkspaceCreateRequest, +}; +use shared_kernel::{build_prefixed_uuid_id, format_timestamp_micros}; +use spacetime_client::SpacetimeClientError; + +use crate::generated_image_assets::{ + GeneratedImageAssetAdapter, GeneratedImageAssetDataUrl, + adapter::GeneratedImageAssetAdapterMetadata, adapter::GeneratedImageAssetPersistInput, + decode_generated_image_asset_data_url, normalize_generated_image_asset_mime, +}; +use crate::{ + api_response::json_success_body, + auth::AuthenticatedAccessToken, + http_error::AppError, + openai_image_generation::{ + DownloadedOpenAiImage, OpenAiReferenceImage, build_openai_image_http_client, + create_openai_image_edit, create_openai_image_edit_with_references, + require_openai_image_settings, + }, + platform_errors::map_oss_error, + request_context::RequestContext, + state::AppState, + vector_engine_audio_generation::{ + GeneratedCreationAudioTarget, generate_sound_effect_asset_for_creation, + }, +}; + +const WOODEN_FISH_PROVIDER: &str = "wooden-fish"; +const WOODEN_FISH_CREATION_PROVIDER: &str = "wooden-fish-creation"; +const WOODEN_FISH_RUNTIME_PROVIDER: &str = "wooden-fish-runtime"; +const WOODEN_FISH_TEMPLATE_ID: &str = "wooden-fish"; +const WOODEN_FISH_TEMPLATE_NAME: &str = "敲木鱼"; +const DEFAULT_HIT_OBJECT_PROMPT: &str = "默认敲击物图案,圆润木质质感,透明背景"; +const DEFAULT_HIT_SOUND_PROMPT: &str = "清脆短促的木鱼敲击声"; +const DEFAULT_HIT_OBJECT_ASSET_ID: &str = "wooden-fish-default-hit-object"; +const DEFAULT_HIT_OBJECT_IMAGE_SRC: &str = "/wooden-fish/default-hit-object.png"; +const WOODEN_FISH_ENTITY_KIND: &str = "wooden_fish_work"; +const WOODEN_FISH_HIT_OBJECT_SLOT: &str = "hit_object"; +const WOODEN_FISH_HIT_OBJECT_ASSET_KIND: &str = "wooden_fish_hit_object"; +const WOODEN_FISH_BACKGROUND_SLOT: &str = "background"; +const WOODEN_FISH_BACKGROUND_ASSET_KIND: &str = "wooden_fish_background"; +const WOODEN_FISH_HIT_SOUND_SLOT: &str = "hit_sound"; +const WOODEN_FISH_HIT_SOUND_ASSET_KIND: &str = "wooden_fish_hit_sound"; +const WOODEN_FISH_HIT_SOUND_DURATION_SECONDS: u8 = 3; +const DEFAULT_HIT_OBJECT_REFERENCE_BYTES: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../../public/wooden-fish/default-hit-object.png" +)); + +pub async fn create_wooden_fish_session( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = wooden_fish_json(payload, &request_context, WOODEN_FISH_CREATION_PROVIDER)?; + validate_workspace_request(&request_context, &payload)?; + + let owner_user_id = authenticated.claims().user_id().to_string(); + let session_id = build_prefixed_uuid_id("wooden-fish-session-"); + let now = current_utc_micros(); + let draft = build_wooden_fish_draft(&payload); + let session = WoodenFishSessionSnapshotResponse { + session_id, + owner_user_id, + status: WoodenFishGenerationStatus::Draft, + draft: Some(draft), + created_at: format_timestamp_micros(now), + updated_at: format_timestamp_micros(now), + }; + + Ok(json_success_body( + Some(&request_context), + WoodenFishSessionResponse { + session: state + .spacetime_client() + .create_wooden_fish_session(session) + .await + .map_err(|error| { + wooden_fish_error_response( + &request_context, + WOODEN_FISH_CREATION_PROVIDER, + map_wooden_fish_client_error(error), + ) + })?, + }, + )) +} + +pub async fn get_wooden_fish_session( + State(state): State, + Path(session_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, &session_id, "sessionId")?; + let owner_user_id = authenticated.claims().user_id().to_string(); + let session = state + .spacetime_client() + .get_wooden_fish_session(session_id, owner_user_id) + .await + .map_err(|error| { + wooden_fish_error_response( + &request_context, + WOODEN_FISH_CREATION_PROVIDER, + map_wooden_fish_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + WoodenFishSessionResponse { session }, + )) +} + +pub async fn execute_wooden_fish_action( + State(state): State, + Path(session_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + ensure_non_empty(&request_context, &session_id, "sessionId")?; + let Json(mut payload) = + wooden_fish_json(payload, &request_context, WOODEN_FISH_CREATION_PROVIDER)?; + let owner_user_id = authenticated.claims().user_id().to_string(); + maybe_generate_hit_object_asset( + &state, + &request_context, + &session_id, + owner_user_id.as_str(), + &mut payload, + ) + .await?; + maybe_generate_hit_sound_asset( + &state, + &request_context, + &session_id, + owner_user_id.as_str(), + &mut payload, + ) + .await?; + let response = state + .spacetime_client() + .execute_wooden_fish_action(session_id, owner_user_id, payload) + .await + .map_err(|error| { + wooden_fish_error_response( + &request_context, + WOODEN_FISH_CREATION_PROVIDER, + map_wooden_fish_client_error(error), + ) + })?; + + Ok(json_success_body(Some(&request_context), response)) +} + +pub async fn publish_wooden_fish_work( + State(state): State, + Path(profile_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, &profile_id, "profileId")?; + let work = state + .spacetime_client() + .publish_wooden_fish_work(profile_id, authenticated.claims().user_id().to_string()) + .await + .map_err(|error| { + wooden_fish_error_response( + &request_context, + WOODEN_FISH_CREATION_PROVIDER, + map_wooden_fish_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + WoodenFishWorkMutationResponse { item: work }, + )) +} + +pub async fn get_wooden_fish_runtime_work( + State(state): State, + Path(profile_id): Path, + Extension(request_context): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, &profile_id, "profileId")?; + let work = state + .spacetime_client() + .get_wooden_fish_runtime_work(profile_id) + .await + .map_err(|error| { + wooden_fish_error_response( + &request_context, + WOODEN_FISH_RUNTIME_PROVIDER, + map_wooden_fish_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + WoodenFishWorkDetailResponse { item: work }, + )) +} + +pub async fn start_wooden_fish_run( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = wooden_fish_json(payload, &request_context, WOODEN_FISH_RUNTIME_PROVIDER)?; + ensure_non_empty(&request_context, &payload.profile_id, "profileId")?; + let run = state + .spacetime_client() + .start_wooden_fish_run(payload, authenticated.claims().user_id().to_string()) + .await + .map_err(|error| { + wooden_fish_error_response( + &request_context, + WOODEN_FISH_RUNTIME_PROVIDER, + map_wooden_fish_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + WoodenFishRunResponse { run }, + )) +} + +pub async fn checkpoint_wooden_fish_run( + State(state): State, + Path(run_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + ensure_non_empty(&request_context, &run_id, "runId")?; + let Json(payload) = wooden_fish_json(payload, &request_context, WOODEN_FISH_RUNTIME_PROVIDER)?; + let run = state + .spacetime_client() + .checkpoint_wooden_fish_run( + run_id, + authenticated.claims().user_id().to_string(), + payload, + ) + .await + .map_err(|error| { + wooden_fish_error_response( + &request_context, + WOODEN_FISH_RUNTIME_PROVIDER, + map_wooden_fish_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + WoodenFishRunResponse { run }, + )) +} + +pub async fn finish_wooden_fish_run( + State(state): State, + Path(run_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + ensure_non_empty(&request_context, &run_id, "runId")?; + let Json(payload) = wooden_fish_json(payload, &request_context, WOODEN_FISH_RUNTIME_PROVIDER)?; + let run = state + .spacetime_client() + .finish_wooden_fish_run( + run_id, + authenticated.claims().user_id().to_string(), + payload, + ) + .await + .map_err(|error| { + wooden_fish_error_response( + &request_context, + WOODEN_FISH_RUNTIME_PROVIDER, + map_wooden_fish_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + WoodenFishRunResponse { run }, + )) +} + +pub async fn list_wooden_fish_gallery( + State(state): State, + Extension(request_context): Extension, +) -> Result, Response> { + let gallery = state + .spacetime_client() + .list_wooden_fish_gallery() + .await + .map_err(|error| { + wooden_fish_error_response( + &request_context, + WOODEN_FISH_RUNTIME_PROVIDER, + map_wooden_fish_client_error(error), + ) + })?; + + Ok(json_success_body(Some(&request_context), gallery)) +} + +pub async fn get_wooden_fish_gallery_detail( + State(state): State, + Path(public_work_code): Path, + Extension(request_context): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, &public_work_code, "publicWorkCode")?; + let work = state + .spacetime_client() + .get_wooden_fish_gallery_detail(public_work_code) + .await + .map_err(|error| { + wooden_fish_error_response( + &request_context, + WOODEN_FISH_RUNTIME_PROVIDER, + map_wooden_fish_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + WoodenFishGalleryDetailResponse { item: work }, + )) +} + +fn build_wooden_fish_draft(payload: &WoodenFishWorkspaceCreateRequest) -> WoodenFishDraftResponse { + WoodenFishDraftResponse { + template_id: WOODEN_FISH_TEMPLATE_ID.to_string(), + template_name: WOODEN_FISH_TEMPLATE_NAME.to_string(), + profile_id: None, + work_title: payload.work_title.trim().to_string(), + work_description: payload.work_description.trim().to_string(), + theme_tags: normalize_tags(payload.theme_tags.clone()), + hit_object_prompt: clean_string(&payload.hit_object_prompt, DEFAULT_HIT_OBJECT_PROMPT), + hit_object_reference_image_src: payload + .hit_object_reference_image_src + .as_ref() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()), + hit_sound_prompt: payload + .hit_sound_prompt + .as_ref() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .or_else(|| Some(DEFAULT_HIT_SOUND_PROMPT.to_string())), + floating_words: normalize_floating_words(payload.floating_words.clone()), + hit_object_asset: None, + background_asset: None, + hit_sound_asset: payload.hit_sound_asset.clone(), + cover_image_src: None, + generation_status: WoodenFishGenerationStatus::Draft, + } +} + +fn validate_workspace_request( + request_context: &RequestContext, + payload: &WoodenFishWorkspaceCreateRequest, +) -> Result<(), Response> { + ensure_non_empty(request_context, &payload.work_title, "workTitle")?; + if payload.template_id.trim() != WOODEN_FISH_TEMPLATE_ID { + return Err(wooden_fish_error_response( + request_context, + WOODEN_FISH_CREATION_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": WOODEN_FISH_PROVIDER, + "message": "templateId 必须为 wooden-fish", + })), + )); + } + Ok(()) +} + +async fn maybe_generate_hit_object_asset( + state: &AppState, + request_context: &RequestContext, + session_id: &str, + owner_user_id: &str, + payload: &mut WoodenFishActionRequest, +) -> Result<(), Response> { + if !matches!( + payload.action_type, + shared_contracts::wooden_fish::WoodenFishActionType::CompileDraft + | shared_contracts::wooden_fish::WoodenFishActionType::RegenerateHitObject + ) { + return Ok(()); + } + if payload.hit_object_asset.is_some() && payload.background_asset.is_some() { + return Ok(()); + } + + let profile_id = + resolve_hit_object_profile_id(state, request_context, session_id, owner_user_id, payload) + .await?; + payload.profile_id = Some(profile_id.clone()); + let prompt = payload + .hit_object_prompt + .as_deref() + .map(|value| clean_string(value, DEFAULT_HIT_OBJECT_PROMPT)) + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| DEFAULT_HIT_OBJECT_PROMPT.to_string()); + + let generated = generate_wooden_fish_image_assets( + state, + owner_user_id, + session_id, + profile_id.as_str(), + prompt.as_str(), + payload.hit_object_reference_image_src.as_deref(), + ) + .await + .map_err(|error| { + wooden_fish_error_response(request_context, WOODEN_FISH_CREATION_PROVIDER, error) + })?; + payload.hit_object_asset = Some(generated.hit_object_asset); + payload.background_asset = Some(generated.background_asset); + Ok(()) +} + +fn default_wooden_fish_hit_object_asset() -> WoodenFishImageAsset { + WoodenFishImageAsset { + asset_id: DEFAULT_HIT_OBJECT_ASSET_ID.to_string(), + image_src: DEFAULT_HIT_OBJECT_IMAGE_SRC.to_string(), + image_object_key: "public/wooden-fish/default-hit-object.png".to_string(), + asset_object_id: DEFAULT_HIT_OBJECT_ASSET_ID.to_string(), + generation_provider: "bundled-default".to_string(), + prompt: DEFAULT_HIT_OBJECT_PROMPT.to_string(), + width: 1024, + height: 1024, + } +} + +fn is_default_hit_object_prompt(prompt: &str) -> bool { + let normalized = normalize_hit_object_prompt_for_default_match(prompt); + normalized.is_empty() + || normalized == normalize_hit_object_prompt_for_default_match(DEFAULT_HIT_OBJECT_PROMPT) + || normalized + == normalize_hit_object_prompt_for_default_match("卡通木鱼,圆润可爱,透明背景") + || normalized + == normalize_hit_object_prompt_for_default_match("卡通木鱼,透明背景,居中,圆润可爱") + || normalized == normalize_hit_object_prompt_for_default_match("卡通木鱼") +} + +fn normalize_hit_object_prompt_for_default_match(prompt: &str) -> String { + prompt + .chars() + .filter(|ch| !ch.is_whitespace() && !matches!(ch, ',' | ',' | '。' | '.')) + .collect::() +} + +async fn resolve_hit_object_profile_id( + state: &AppState, + request_context: &RequestContext, + session_id: &str, + owner_user_id: &str, + payload: &WoodenFishActionRequest, +) -> Result { + if let Some(profile_id) = payload + .profile_id + .as_ref() + .map(|value| value.trim()) + .filter(|value| !value.is_empty()) + { + return Ok(profile_id.to_string()); + } + if matches!( + payload.action_type, + shared_contracts::wooden_fish::WoodenFishActionType::CompileDraft + ) { + return Ok(build_prefixed_uuid_id("wooden-fish-profile-")); + } + + let session = state + .spacetime_client() + .get_wooden_fish_session(session_id.to_string(), owner_user_id.to_string()) + .await + .map_err(|error| { + wooden_fish_error_response( + request_context, + WOODEN_FISH_CREATION_PROVIDER, + map_wooden_fish_client_error(error), + ) + })?; + session + .draft + .and_then(|draft| draft.profile_id) + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| { + wooden_fish_error_response( + request_context, + WOODEN_FISH_CREATION_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": WOODEN_FISH_PROVIDER, + "message": "wooden-fish action 需要先完成 compile-draft", + })), + ) + }) +} + +async fn maybe_generate_hit_sound_asset( + state: &AppState, + request_context: &RequestContext, + session_id: &str, + owner_user_id: &str, + payload: &mut WoodenFishActionRequest, +) -> Result<(), Response> { + if !matches!( + payload.action_type, + shared_contracts::wooden_fish::WoodenFishActionType::CompileDraft + | shared_contracts::wooden_fish::WoodenFishActionType::GenerateHitSound + ) { + return Ok(()); + } + if matches!( + payload.action_type, + shared_contracts::wooden_fish::WoodenFishActionType::CompileDraft + ) && payload.hit_sound_asset.is_some() + { + return Ok(()); + } + + let profile_id = + resolve_hit_object_profile_id(state, request_context, session_id, owner_user_id, payload) + .await?; + payload.profile_id = Some(profile_id.clone()); + let prompt = payload + .hit_sound_prompt + .as_deref() + .map(|value| clean_string(value, DEFAULT_HIT_SOUND_PROMPT)) + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| DEFAULT_HIT_SOUND_PROMPT.to_string()); + + let asset = generate_wooden_fish_hit_sound_asset( + state, + owner_user_id, + profile_id.as_str(), + prompt.as_str(), + ) + .await + .map_err(|error| { + wooden_fish_error_response(request_context, WOODEN_FISH_CREATION_PROVIDER, error) + })?; + payload.hit_sound_asset = Some(asset); + Ok(()) +} + +async fn generate_wooden_fish_hit_sound_asset( + state: &AppState, + owner_user_id: &str, + profile_id: &str, + prompt: &str, +) -> Result { + let final_prompt = build_wooden_fish_hit_sound_prompt(prompt); + let generated = generate_sound_effect_asset_for_creation( + state, + owner_user_id, + final_prompt.clone(), + Some(WOODEN_FISH_HIT_SOUND_DURATION_SECONDS), + None, + GeneratedCreationAudioTarget { + entity_kind: WOODEN_FISH_ENTITY_KIND.to_string(), + entity_id: profile_id.to_string(), + slot: WOODEN_FISH_HIT_SOUND_SLOT.to_string(), + asset_kind: WOODEN_FISH_HIT_SOUND_ASSET_KIND.to_string(), + profile_id: Some(profile_id.to_string()), + storage_prefix: LegacyAssetPrefix::WoodenFishAssets, + }, + ) + .await?; + map_generated_creation_audio_to_wooden_fish_asset( + profile_id, + final_prompt.as_str(), + generated, + WOODEN_FISH_HIT_SOUND_DURATION_SECONDS, + ) +} + +fn build_wooden_fish_hit_sound_prompt(prompt: &str) -> String { + format!( + "为敲木鱼玩法生成一次点击触发的短促敲击音效:{}。要求:干净、清脆、无旋律、无环境噪声、无语音、无文字提示音,适合高频点击时叠加播放。", + clean_string(prompt, DEFAULT_HIT_SOUND_PROMPT) + ) +} + +fn map_generated_creation_audio_to_wooden_fish_asset( + profile_id: &str, + prompt: &str, + asset: shared_contracts::creation_audio::CreationAudioAsset, + duration_seconds: u8, +) -> Result { + let asset_object_id = asset + .asset_object_id + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "vector-engine", + "message": "敲木鱼音效生成完成但缺少资产对象 ID", + })) + })?; + let audio_object_key = asset.audio_src.trim().trim_start_matches('/').to_string(); + if audio_object_key.is_empty() { + return Err( + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "vector-engine", + "message": "敲木鱼音效生成完成但缺少音频地址", + })), + ); + } + + Ok(WoodenFishAudioAsset { + asset_id: format!("{profile_id}-hit-sound-{}", asset.task_id), + audio_src: asset.audio_src, + audio_object_key, + asset_object_id, + source: "generated".to_string(), + prompt: asset.prompt.or_else(|| Some(prompt.to_string())), + duration_ms: Some(u32::from(duration_seconds) * 1_000), + }) +} + +struct WoodenFishGeneratedImageAssets { + hit_object_asset: WoodenFishImageAsset, + background_asset: WoodenFishImageAsset, +} + +async fn generate_wooden_fish_image_assets( + state: &AppState, + owner_user_id: &str, + session_id: &str, + profile_id: &str, + prompt: &str, + hit_object_reference_image_src: Option<&str>, +) -> Result { + let settings = require_openai_image_settings(state)?; + let http_client = build_openai_image_http_client(&settings)?; + let clean_reference_image_src = hit_object_reference_image_src + .map(str::trim) + .filter(|value| !value.is_empty()); + let theme = resolve_wooden_fish_generation_theme(prompt, clean_reference_image_src); + let default_reference_image = default_wooden_fish_reference_image()?; + let theme_reference_image = + resolve_wooden_fish_theme_reference_image(clean_reference_image_src)?; + + let (hit_object_asset, background_reference_image) = + if should_generate_wooden_fish_hit_object(prompt, clean_reference_image_src) { + let hit_object_prompt = build_wooden_fish_hit_object_prompt(theme.as_str()); + let mut reference_images = vec![default_reference_image.clone()]; + if let Some(reference_image) = theme_reference_image { + reference_images.push(reference_image); + } + let generated = create_openai_image_edit_with_references( + &http_client, + &settings, + hit_object_prompt.as_str(), + None, + "1:1", + 1, + reference_images.as_slice(), + "生成敲木鱼敲击物图案失败", + ) + .await?; + let task_id = generated.task_id.clone(); + let image = generated.images.into_iter().next().ok_or_else(|| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "vector-engine", + "message": "生成敲木鱼敲击物图案失败:上游未返回图片", + })) + })?; + let background_reference_image = + downloaded_wooden_fish_reference_image(&image, "wooden-fish-generated-hit-object"); + let hit_object_asset = persist_wooden_fish_image_asset( + state, + owner_user_id, + session_id, + profile_id, + task_id.as_str(), + hit_object_prompt.as_str(), + image, + current_utc_micros(), + WoodenFishImageSlotPersistSpec { + slot: WOODEN_FISH_HIT_OBJECT_SLOT, + asset_kind: WOODEN_FISH_HIT_OBJECT_ASSET_KIND, + asset_id_part: "hit-object", + width: 1024, + height: 1024, + }, + ) + .await?; + (hit_object_asset, background_reference_image) + } else { + ( + default_wooden_fish_hit_object_asset(), + default_reference_image, + ) + }; + + let background_prompt = build_wooden_fish_background_prompt(theme.as_str()); + let background_generated = create_openai_image_edit( + &http_client, + &settings, + background_prompt.as_str(), + None, + "9:16", + &background_reference_image, + "生成敲木鱼背景环境图失败", + ) + .await?; + let background_task_id = background_generated.task_id.clone(); + let background_image = background_generated + .images + .into_iter() + .next() + .ok_or_else(|| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "vector-engine", + "message": "生成敲木鱼背景环境图失败:上游未返回图片", + })) + })?; + let background_asset = persist_wooden_fish_image_asset( + state, + owner_user_id, + session_id, + profile_id, + background_task_id.as_str(), + background_prompt.as_str(), + background_image, + current_utc_micros(), + WoodenFishImageSlotPersistSpec { + slot: WOODEN_FISH_BACKGROUND_SLOT, + asset_kind: WOODEN_FISH_BACKGROUND_ASSET_KIND, + asset_id_part: "background", + width: 1024, + height: 1536, + }, + ) + .await?; + + Ok(WoodenFishGeneratedImageAssets { + hit_object_asset, + background_asset, + }) +} + +fn build_wooden_fish_hit_object_prompt(prompt: &str) -> String { + format!( + "生成敲木鱼新样式,要求结构,画风与参考图保持高度一致,新样式颜色搭配使用新主题对应的颜色。\n新主题为:{}", + clean_string(prompt, DEFAULT_HIT_OBJECT_PROMPT) + ) +} + +fn build_wooden_fish_background_prompt(prompt: &str) -> String { + format!( + "生成敲木鱼背景,要求主题,画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,木鱼预设在屏幕中央位置,木鱼主体周围元素保持干净,背景氛围围绕外围设计,背景环境图中不包含新木鱼物品,背景氛围中不增加木槌互动物品。\n主题为:{}", + clean_string(prompt, DEFAULT_HIT_OBJECT_PROMPT) + ) +} + +fn should_generate_wooden_fish_hit_object( + prompt: &str, + hit_object_reference_image_src: Option<&str>, +) -> bool { + hit_object_reference_image_src.is_some() || !is_default_hit_object_prompt(prompt) +} + +fn resolve_wooden_fish_generation_theme( + prompt: &str, + hit_object_reference_image_src: Option<&str>, +) -> String { + let prompt = clean_string(prompt, DEFAULT_HIT_OBJECT_PROMPT); + if !is_default_hit_object_prompt(prompt.as_str()) { + return prompt; + } + if hit_object_reference_image_src.is_some() { + return "用户提供参考图".to_string(); + } + prompt +} + +fn default_wooden_fish_reference_image() -> Result { + let bytes = DEFAULT_HIT_OBJECT_REFERENCE_BYTES.to_vec(); + if bytes.is_empty() { + return Err( + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ + "provider": WOODEN_FISH_CREATION_PROVIDER, + "message": "敲木鱼默认参考图为空", + })), + ); + } + Ok(OpenAiReferenceImage { + bytes, + mime_type: "image/png".to_string(), + file_name: "wooden-fish-default-hit-object-reference.png".to_string(), + }) +} + +fn resolve_wooden_fish_theme_reference_image( + source: Option<&str>, +) -> Result, AppError> { + let Some(source) = source.map(str::trim).filter(|value| !value.is_empty()) else { + return Ok(None); + }; + if !source.to_ascii_lowercase().starts_with("data:image/") { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": WOODEN_FISH_CREATION_PROVIDER, + "field": "hitObjectReferenceImageSrc", + "message": "敲木鱼参考图必须是 base64 图片 Data URL。", + })), + ); + } + let decoded = decode_generated_image_asset_data_url(source).map_err(|_| { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": WOODEN_FISH_CREATION_PROVIDER, + "field": "hitObjectReferenceImageSrc", + "message": "敲木鱼参考图必须是 base64 图片 Data URL。", + })) + })?; + Ok(Some(OpenAiReferenceImage { + file_name: format!("wooden-fish-theme-reference.{}", decoded.format.extension), + mime_type: decoded.format.mime_type, + bytes: decoded.bytes, + })) +} + +fn downloaded_wooden_fish_reference_image( + image: &DownloadedOpenAiImage, + file_name_stem: &str, +) -> OpenAiReferenceImage { + OpenAiReferenceImage { + bytes: image.bytes.clone(), + mime_type: image.mime_type.clone(), + file_name: format!("{file_name_stem}.{}", image.extension), + } +} + +struct WoodenFishImageSlotPersistSpec { + slot: &'static str, + asset_kind: &'static str, + asset_id_part: &'static str, + width: u32, + height: u32, +} + +async fn persist_wooden_fish_image_asset( + state: &AppState, + owner_user_id: &str, + session_id: &str, + profile_id: &str, + task_id: &str, + prompt: &str, + image: DownloadedOpenAiImage, + generated_at_micros: i64, + spec: WoodenFishImageSlotPersistSpec, +) -> Result { + let oss_client = state.oss_client().ok_or_else(|| { + AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({ + "provider": "aliyun-oss", + "reason": "OSS 未完成环境变量配置", + })) + })?; + let http_client = reqwest::Client::new(); + let prepared = + GeneratedImageAssetAdapter::prepare_put_object(GeneratedImageAssetPersistInput { + prefix: LegacyAssetPrefix::WoodenFishAssets, + path_segments: vec![ + sanitize_wooden_fish_asset_segment(session_id, "session"), + sanitize_wooden_fish_asset_segment(profile_id, "profile"), + spec.slot.to_string(), + format!("asset-{generated_at_micros}"), + ], + file_stem: "image".to_string(), + image: GeneratedImageAssetDataUrl { + format: normalize_generated_image_asset_mime(image.mime_type.as_str()), + bytes: image.bytes, + }, + access: OssObjectAccess::Private, + metadata: GeneratedImageAssetAdapterMetadata { + asset_kind: Some(spec.asset_kind.to_string()), + owner_user_id: Some(owner_user_id.to_string()), + entity_kind: Some(WOODEN_FISH_ENTITY_KIND.to_string()), + entity_id: Some(profile_id.to_string()), + slot: Some(spec.slot.to_string()), + provider: Some("image2".to_string()), + task_id: Some(task_id.to_string()), + }, + extra_metadata: BTreeMap::from([ + ("profile_id".to_string(), profile_id.to_string()), + ("session_id".to_string(), session_id.to_string()), + ]), + }) + .map_err(map_wooden_fish_generated_image_asset_error)?; + let persisted_mime_type = prepared.format.mime_type.clone(); + let put_result = oss_client + .put_object(&http_client, prepared.request) + .await + .map_err(map_wooden_fish_asset_oss_error)?; + let head = oss_client + .head_object( + &http_client, + OssHeadObjectRequest { + object_key: put_result.object_key.clone(), + }, + ) + .await + .map_err(map_wooden_fish_asset_oss_error)?; + let asset_object = state + .spacetime_client() + .confirm_asset_object( + build_asset_object_upsert_input( + generate_asset_object_id(generated_at_micros), + head.bucket, + head.object_key.clone(), + AssetObjectAccessPolicy::Private, + head.content_type.or(Some(persisted_mime_type)), + head.content_length, + head.etag, + spec.asset_kind.to_string(), + Some(task_id.to_string()), + Some(owner_user_id.to_string()), + Some(profile_id.to_string()), + Some(profile_id.to_string()), + generated_at_micros, + ) + .map_err(map_wooden_fish_asset_field_error)?, + ) + .await + .map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "spacetimedb", + "message": error.to_string(), + })) + })?; + if let Err(error) = state + .spacetime_client() + .bind_asset_object_to_entity( + build_asset_entity_binding_input( + generate_asset_binding_id(generated_at_micros), + asset_object.asset_object_id.clone(), + WOODEN_FISH_ENTITY_KIND.to_string(), + profile_id.to_string(), + spec.slot.to_string(), + spec.asset_kind.to_string(), + Some(owner_user_id.to_string()), + Some(profile_id.to_string()), + generated_at_micros, + ) + .map_err(map_wooden_fish_asset_field_error)?, + ) + .await + { + tracing::warn!( + provider = "spacetimedb", + owner_user_id, + session_id, + profile_id, + slot = spec.slot, + error = %error, + "敲木鱼图片资产绑定失败,历史素材索引可能缺少绑定记录" + ); + } + + Ok(WoodenFishImageAsset { + asset_id: format!("{profile_id}-{}-{generated_at_micros}", spec.asset_id_part), + image_src: put_result.legacy_public_path, + image_object_key: head.object_key, + asset_object_id: asset_object.asset_object_id, + generation_provider: "image2".to_string(), + prompt: prompt.to_string(), + width: spec.width, + height: spec.height, + }) +} + +fn map_wooden_fish_generated_image_asset_error( + error: crate::generated_image_assets::helpers::GeneratedImageAssetHelperError, +) -> AppError { + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ + "provider": "generated-image-assets", + "message": format!("准备敲木鱼图片资产上传请求失败:{error:?}"), + })) +} + +fn map_wooden_fish_asset_oss_error(error: platform_oss::OssError) -> AppError { + map_oss_error(error, "aliyun-oss") +} + +fn map_wooden_fish_asset_field_error(error: AssetObjectFieldError) -> AppError { + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ + "provider": "wooden-fish-assets", + "message": error.to_string(), + })) +} + +fn sanitize_wooden_fish_asset_segment(value: &str, fallback: &str) -> String { + let sanitized = value + .trim() + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || ('\u{4e00}'..='\u{9fff}').contains(&ch) { + ch + } else { + '-' + } + }) + .collect::() + .trim_matches('-') + .to_string(); + if sanitized.is_empty() { + fallback.to_string() + } else { + sanitized + } +} + +fn ensure_non_empty( + request_context: &RequestContext, + value: &str, + field: &str, +) -> Result<(), Response> { + if value.trim().is_empty() { + return Err(wooden_fish_error_response( + request_context, + WOODEN_FISH_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": WOODEN_FISH_PROVIDER, + "field": field, + "message": format!("{field} 不能为空"), + })), + )); + } + Ok(()) +} + +fn clean_string(value: &str, fallback: &str) -> String { + let value = value.trim(); + if value.is_empty() { + fallback.to_string() + } else { + value.to_string() + } +} + +fn normalize_tags(tags: Vec) -> Vec { + let mut normalized = Vec::new(); + for tag in tags { + let tag = tag.trim(); + if tag.is_empty() || normalized.iter().any(|item| item == tag) { + continue; + } + normalized.push(tag.to_string()); + if normalized.len() >= 6 { + break; + } + } + normalized +} + +fn normalize_floating_words(words: Vec) -> Vec { + let mut normalized = Vec::new(); + for word in words { + let word = normalize_floating_word(&word); + if word.is_empty() || normalized.iter().any(|item| item == &word) { + continue; + } + normalized.push(word); + if normalized.len() >= 8 { + break; + } + } + if normalized.is_empty() { + vec![ + "幸运".to_string(), + "健康".to_string(), + "财富".to_string(), + "姻缘".to_string(), + "幸福".to_string(), + "事业".to_string(), + "成功".to_string(), + "功德".to_string(), + ] + } else { + normalized + } +} + +fn normalize_floating_word(word: &str) -> String { + word.trim() + .trim_end_matches(|ch: char| ch == '1' || ch.is_whitespace()) + .trim_end_matches(['+', '+']) + .trim() + .to_string() +} + +fn wooden_fish_json( + payload: Result, JsonRejection>, + request_context: &RequestContext, + provider: &str, +) -> Result, Response> { + payload.map_err(|error| { + wooden_fish_error_response( + request_context, + provider, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": provider, + "message": error.to_string(), + })), + ) + }) +} + +fn map_wooden_fish_client_error(error: SpacetimeClientError) -> AppError { + let status = match &error { + SpacetimeClientError::Runtime(_) => StatusCode::BAD_REQUEST, + SpacetimeClientError::Procedure(message) + if message.contains("不存在") + || message.contains("not found") + || message.contains("does not exist") => + { + StatusCode::NOT_FOUND + } + SpacetimeClientError::Procedure(message) + if message.contains("发布需要") + || message.contains("不能为空") + || message.contains("必须") => + { + StatusCode::BAD_REQUEST + } + _ => StatusCode::BAD_GATEWAY, + }; + + AppError::from_status(status).with_details(json!({ + "provider": "spacetimedb", + "message": error.to_string(), + })) +} + +fn wooden_fish_error_response( + request_context: &RequestContext, + provider: &str, + error: AppError, +) -> Response { + let mut response = error.into_response_with_context(Some(request_context)); + response.headers_mut().insert( + HeaderName::from_static("x-genarrative-provider"), + header::HeaderValue::from_str(provider) + .unwrap_or_else(|_| header::HeaderValue::from_static("wooden-fish")), + ); + response +} + +fn current_utc_micros() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_micros().min(i64::MAX as u128) as i64) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; + + #[test] + fn wooden_fish_hit_object_prompt_uses_hidden_image2_flow() { + let prompt = build_wooden_fish_hit_object_prompt("赛博莲花木鱼"); + + assert_eq!( + prompt, + "生成敲木鱼新样式,要求结构,画风与参考图保持高度一致,新样式颜色搭配使用新主题对应的颜色。\n新主题为:赛博莲花木鱼" + ); + } + + #[test] + fn wooden_fish_background_prompt_uses_hidden_image2_flow() { + let prompt = build_wooden_fish_background_prompt("赛博莲花木鱼"); + + assert_eq!( + prompt, + "生成敲木鱼背景,要求主题,画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,木鱼预设在屏幕中央位置,木鱼主体周围元素保持干净,背景氛围围绕外围设计,背景环境图中不包含新木鱼物品,背景氛围中不增加木槌互动物品。\n主题为:赛博莲花木鱼" + ); + } + + #[test] + fn wooden_fish_theme_reference_image_decodes_data_url_for_image2() { + let source = format!( + "data:image/png;base64,{}", + BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nreference") + ); + + let image = resolve_wooden_fish_theme_reference_image(Some(source.as_str())) + .expect("data url should parse") + .expect("reference image should exist"); + + assert_eq!(image.mime_type, "image/png"); + assert_eq!(image.file_name, "wooden-fish-theme-reference.png"); + assert!(image.bytes.starts_with(b"\x89PNG\r\n\x1A\n")); + } + + #[test] + fn wooden_fish_theme_reference_image_rejects_non_data_url() { + let error = resolve_wooden_fish_theme_reference_image(Some("/generated/example.png")) + .expect_err("legacy path should not be accepted as direct image2 reference"); + + assert_eq!(error.status_code(), StatusCode::BAD_REQUEST); + assert!(error.body_text().contains("Data URL")); + } + + #[test] + fn wooden_fish_default_hit_object_uses_bundled_asset() { + let asset = default_wooden_fish_hit_object_asset(); + + assert_eq!(asset.asset_id, DEFAULT_HIT_OBJECT_ASSET_ID); + assert_eq!(asset.image_src, DEFAULT_HIT_OBJECT_IMAGE_SRC); + assert_eq!(asset.generation_provider, "bundled-default"); + assert_eq!(asset.width, 1024); + assert_eq!(asset.height, 1024); + } + + #[test] + fn wooden_fish_default_prompt_matches_legacy_defaults() { + assert!(is_default_hit_object_prompt(DEFAULT_HIT_OBJECT_PROMPT)); + assert!(is_default_hit_object_prompt("卡通木鱼,圆润可爱,透明背景")); + assert!(is_default_hit_object_prompt( + "卡通木鱼,透明背景,居中,圆润可爱" + )); + assert!(is_default_hit_object_prompt("卡通木鱼")); + assert!(!is_default_hit_object_prompt("赛博莲花木鱼")); + } + + #[test] + fn wooden_fish_asset_segment_sanitizes_for_oss_object_key() { + assert_eq!( + sanitize_wooden_fish_asset_segment("wooden-fish/profile:1", "fallback"), + "wooden-fish-profile-1" + ); + assert_eq!( + sanitize_wooden_fish_asset_segment(" ", "fallback"), + "fallback" + ); + } + + #[test] + fn wooden_fish_audio_asset_maps_from_generated_sound_effect() { + let asset = shared_contracts::creation_audio::CreationAudioAsset { + task_id: "task-hit-sound-1".to_string(), + provider: "vector-engine-vidu".to_string(), + asset_object_id: Some("assetobj-hit-sound-1".to_string()), + asset_kind: Some(WOODEN_FISH_HIT_SOUND_ASSET_KIND.to_string()), + audio_src: "/generated-wooden-fish-assets/wooden-fish-profile-1/hit-sound.mp3" + .to_string(), + prompt: Some("清脆木鱼声".to_string()), + title: None, + updated_at: None, + }; + + let mapped = map_generated_creation_audio_to_wooden_fish_asset( + "wooden-fish-profile-1", + "清脆木鱼声", + asset, + WOODEN_FISH_HIT_SOUND_DURATION_SECONDS, + ) + .expect("generated sound effect should map to wooden fish audio asset"); + + assert_eq!( + mapped.asset_id, + "wooden-fish-profile-1-hit-sound-task-hit-sound-1" + ); + assert_eq!( + mapped.audio_object_key, + "generated-wooden-fish-assets/wooden-fish-profile-1/hit-sound.mp3" + ); + assert_eq!(mapped.asset_object_id, "assetobj-hit-sound-1"); + assert_eq!(mapped.source, "generated"); + assert_eq!(mapped.duration_ms, Some(3_000)); + } +} diff --git a/server-rs/crates/module-runtime/src/application.rs b/server-rs/crates/module-runtime/src/application.rs index 5c86bbd6..dec7f729 100644 --- a/server-rs/crates/module-runtime/src/application.rs +++ b/server-rs/crates/module-runtime/src/application.rs @@ -105,6 +105,17 @@ pub fn default_creation_entry_type_snapshots( 45, updated_at_micros, ), + build_default_creation_entry_type_snapshot( + "wooden-fish", + "敲木鱼", + "点击祈福轻玩法", + "可创建", + "/wooden-fish/default-hit-object.png", + true, + true, + 47, + updated_at_micros, + ), build_default_creation_entry_type_snapshot( "square-hole", "方洞", @@ -164,10 +175,10 @@ pub fn default_creation_entry_type_snapshots( "baby-object-match", "宝贝识物", "亲子识物分类", - "敬请期待", + "可创建", "/child-motion-demo/picture-book-grass-stage.png", true, - false, + true, 90, updated_at_micros, ), diff --git a/server-rs/crates/module-runtime/src/lib.rs b/server-rs/crates/module-runtime/src/lib.rs index 9c2fd42f..5bbcd1b8 100644 --- a/server-rs/crates/module-runtime/src/lib.rs +++ b/server-rs/crates/module-runtime/src/lib.rs @@ -227,8 +227,8 @@ mod tests { assert_eq!(baby_object_match.title, "宝贝识物"); assert_eq!(baby_object_match.subtitle, "亲子识物分类"); assert!(baby_object_match.visible); - assert!(!baby_object_match.open); - assert_eq!(baby_object_match.badge, "敬请期待"); + assert!(baby_object_match.open); + assert_eq!(baby_object_match.badge, "可创建"); assert_eq!(baby_object_match.sort_order, 90); assert_eq!( baby_object_match.image_src, @@ -272,6 +272,25 @@ mod tests { ); } + #[test] + fn default_creation_entry_types_include_wooden_fish() { + let configs = default_creation_entry_type_snapshots(1); + let wooden_fish = configs + .iter() + .find(|item| item.id == "wooden-fish") + .expect("wooden-fish creation entry should be seeded"); + + assert_eq!(wooden_fish.title, "敲木鱼"); + assert!(wooden_fish.visible); + assert!(wooden_fish.open); + assert_eq!(wooden_fish.badge, "可创建"); + assert_eq!(wooden_fish.sort_order, 47); + assert_eq!( + wooden_fish.image_src, + "/wooden-fish/default-hit-object.png" + ); + } + #[test] fn normalized_clamps_music_volume_into_valid_range() { let low = RuntimeSettings::normalized(-1.0, RuntimePlatformTheme::Light); diff --git a/server-rs/crates/module-wooden-fish/Cargo.toml b/server-rs/crates/module-wooden-fish/Cargo.toml new file mode 100644 index 00000000..30e0fd30 --- /dev/null +++ b/server-rs/crates/module-wooden-fish/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "module-wooden-fish" +edition.workspace = true +version.workspace = true +license.workspace = true + +[features] +default = [] +spacetime-types = ["dep:spacetimedb"] + +[dependencies] +serde = { workspace = true } +spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-wooden-fish/src/application.rs b/server-rs/crates/module-wooden-fish/src/application.rs new file mode 100644 index 00000000..419f58b5 --- /dev/null +++ b/server-rs/crates/module-wooden-fish/src/application.rs @@ -0,0 +1,4 @@ +pub use crate::domain::{ + apply_run_checkpoint, default_floating_words, finish_run, normalize_floating_words, + normalize_word_counters, +}; diff --git a/server-rs/crates/module-wooden-fish/src/commands.rs b/server-rs/crates/module-wooden-fish/src/commands.rs new file mode 100644 index 00000000..f361ad29 --- /dev/null +++ b/server-rs/crates/module-wooden-fish/src/commands.rs @@ -0,0 +1,8 @@ +pub fn normalize_wooden_fish_prompt(value: &str, fallback: &str) -> String { + let value = value.trim(); + if value.is_empty() { + fallback.to_string() + } else { + value.to_string() + } +} diff --git a/server-rs/crates/module-wooden-fish/src/domain.rs b/server-rs/crates/module-wooden-fish/src/domain.rs new file mode 100644 index 00000000..1dc4e5fe --- /dev/null +++ b/server-rs/crates/module-wooden-fish/src/domain.rs @@ -0,0 +1,281 @@ +use serde::{Deserialize, Serialize}; +#[cfg(feature = "spacetime-types")] +use spacetimedb::SpacetimeType; + +pub const WOODEN_FISH_SESSION_ID_PREFIX: &str = "wooden-fish-session-"; +pub const WOODEN_FISH_PROFILE_ID_PREFIX: &str = "wooden-fish-profile-"; +pub const WOODEN_FISH_WORK_ID_PREFIX: &str = "wooden-fish-work-"; +pub const WOODEN_FISH_RUN_ID_PREFIX: &str = "wooden-fish-run-"; + +pub const DEFAULT_FLOATING_WORDS: [&str; 8] = [ + "幸运", "健康", "财富", "姻缘", "幸福", "事业", "成功", "功德", +]; + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum WoodenFishRunStatus { + Playing, + Finished, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct WoodenFishWordCounter { + pub text: String, + pub count: u32, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct WoodenFishRunSnapshot { + pub run_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub status: WoodenFishRunStatus, + pub total_tap_count: u32, + pub word_counters: Vec, + pub started_at_ms: u64, + pub updated_at_ms: u64, + pub finished_at_ms: Option, +} + +pub fn default_floating_words() -> Vec { + DEFAULT_FLOATING_WORDS + .iter() + .map(|value| (*value).to_string()) + .collect() +} + +pub fn normalize_floating_words(words: &[String]) -> Vec { + let mut normalized: Vec = Vec::new(); + for word in words { + let value = normalize_floating_word(word); + if !value.is_empty() && !normalized.iter().any(|item| item == &value) { + normalized.push(value); + } + if normalized.len() == 8 { + break; + } + } + if normalized.is_empty() { + default_floating_words() + } else { + normalized + } +} + +fn normalize_floating_word(word: &str) -> String { + word.trim() + .trim_end_matches(|ch: char| ch == '1' || ch.is_whitespace()) + .trim_end_matches(['+', '+']) + .trim() + .to_string() +} + +pub fn apply_run_checkpoint( + current: &WoodenFishRunSnapshot, + total_tap_count: u32, + word_counters: Vec, + updated_at_ms: u64, +) -> WoodenFishRunSnapshot { + WoodenFishRunSnapshot { + total_tap_count, + word_counters: normalize_word_counters(word_counters), + updated_at_ms, + ..current.clone() + } +} + +pub fn finish_run( + current: &WoodenFishRunSnapshot, + total_tap_count: u32, + word_counters: Vec, + finished_at_ms: u64, +) -> WoodenFishRunSnapshot { + WoodenFishRunSnapshot { + status: WoodenFishRunStatus::Finished, + total_tap_count, + word_counters: normalize_word_counters(word_counters), + updated_at_ms: finished_at_ms, + finished_at_ms: Some(finished_at_ms), + ..current.clone() + } +} + +pub fn normalize_word_counters(counters: Vec) -> Vec { + let mut normalized: Vec = Vec::new(); + for counter in counters { + let text = normalize_floating_word(&counter.text); + if text.is_empty() || counter.count == 0 { + continue; + } + if let Some(existing) = normalized.iter_mut().find(|item| item.text == text) { + existing.count = existing.count.saturating_add(counter.count); + } else { + normalized.push(WoodenFishWordCounter { + text, + count: counter.count, + }); + } + if normalized.len() == 8 { + break; + } + } + normalized +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn floating_words_default_to_eight_blessing_entries() { + assert_eq!( + default_floating_words(), + vec![ + "幸运".to_string(), + "健康".to_string(), + "财富".to_string(), + "姻缘".to_string(), + "幸福".to_string(), + "事业".to_string(), + "成功".to_string(), + "功德".to_string(), + ] + ); + } + + #[test] + fn floating_words_are_trimmed_deduped_and_capped_at_eight() { + let words = vec![ + " 幸运+1 ".to_string(), + "幸运".to_string(), + "健康".to_string(), + "财富".to_string(), + "姻缘".to_string(), + "幸福".to_string(), + "事业".to_string(), + "成功".to_string(), + "功德".to_string(), + "快乐".to_string(), + ]; + + assert_eq!(normalize_floating_words(&words).len(), 8); + assert_eq!(normalize_floating_words(&words)[0], "幸运"); + assert_eq!(normalize_floating_words(&words)[7], "功德"); + } + + #[test] + fn checkpoint_replaces_single_run_counter_snapshot() { + let current = WoodenFishRunSnapshot { + run_id: "wooden-fish-run-1".to_string(), + profile_id: "wooden-fish-profile-1".to_string(), + owner_user_id: "user-1".to_string(), + status: WoodenFishRunStatus::Playing, + total_tap_count: 0, + word_counters: Vec::new(), + started_at_ms: 100, + updated_at_ms: 100, + finished_at_ms: None, + }; + + let next = apply_run_checkpoint( + ¤t, + 2, + vec![WoodenFishWordCounter { + text: "功德".to_string(), + count: 2, + }], + 160, + ); + + assert_eq!(next.total_tap_count, 2); + assert_eq!(next.word_counters[0].text, "功德"); + assert_eq!(next.updated_at_ms, 160); + assert_eq!(next.started_at_ms, 100); + } + + #[test] + fn word_counters_are_trimmed_deduped_and_capped_at_eight() { + let counters = vec![ + WoodenFishWordCounter { + text: " 幸运+1 ".to_string(), + count: 1, + }, + WoodenFishWordCounter { + text: "幸运+1".to_string(), + count: 2, + }, + WoodenFishWordCounter { + text: "健康+1".to_string(), + count: 1, + }, + WoodenFishWordCounter { + text: "财富+1".to_string(), + count: 1, + }, + WoodenFishWordCounter { + text: "姻缘+1".to_string(), + count: 1, + }, + WoodenFishWordCounter { + text: "幸福+1".to_string(), + count: 1, + }, + WoodenFishWordCounter { + text: "事业+1".to_string(), + count: 1, + }, + WoodenFishWordCounter { + text: "成功+1".to_string(), + count: 1, + }, + WoodenFishWordCounter { + text: "功德+1".to_string(), + count: 1, + }, + WoodenFishWordCounter { + text: "快乐+1".to_string(), + count: 1, + }, + ]; + + let normalized = normalize_word_counters(counters); + + assert_eq!(normalized.len(), 8); + assert_eq!(normalized[0].text, "幸运"); + assert_eq!(normalized[0].count, 3); + assert_eq!(normalized[7].text, "功德"); + } + + #[test] + fn finish_run_sets_status_and_finished_timestamp() { + let current = WoodenFishRunSnapshot { + run_id: "wooden-fish-run-1".to_string(), + profile_id: "wooden-fish-profile-1".to_string(), + owner_user_id: "user-1".to_string(), + status: WoodenFishRunStatus::Playing, + total_tap_count: 3, + word_counters: Vec::new(), + started_at_ms: 100, + updated_at_ms: 130, + finished_at_ms: None, + }; + + let next = finish_run( + ¤t, + 4, + vec![WoodenFishWordCounter { + text: "健康".to_string(), + count: 4, + }], + 220, + ); + + assert_eq!(next.status, WoodenFishRunStatus::Finished); + assert_eq!(next.total_tap_count, 4); + assert_eq!(next.updated_at_ms, 220); + assert_eq!(next.finished_at_ms, Some(220)); + assert_eq!(next.word_counters[0].text, "健康"); + } +} diff --git a/server-rs/crates/module-wooden-fish/src/errors.rs b/server-rs/crates/module-wooden-fish/src/errors.rs new file mode 100644 index 00000000..e7d4ff72 --- /dev/null +++ b/server-rs/crates/module-wooden-fish/src/errors.rs @@ -0,0 +1,23 @@ +use std::fmt::{self, Display}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WoodenFishError { + MissingRunId, + MissingProfileId, + MissingOwnerUserId, + RunNotPlaying, +} + +impl Display for WoodenFishError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::MissingRunId => "缺少 runId", + Self::MissingProfileId => "缺少 profileId", + Self::MissingOwnerUserId => "owner_user_id 缺失", + Self::RunNotPlaying => "当前运行态不是 playing", + }; + write!(f, "{message}") + } +} + +impl std::error::Error for WoodenFishError {} diff --git a/server-rs/crates/module-wooden-fish/src/events.rs b/server-rs/crates/module-wooden-fish/src/events.rs new file mode 100644 index 00000000..ae4f1a49 --- /dev/null +++ b/server-rs/crates/module-wooden-fish/src/events.rs @@ -0,0 +1,25 @@ +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum WoodenFishDomainEvent { + DraftCompiled { + profile_id: String, + owner_user_id: String, + occurred_at_micros: i64, + }, + WorkPublished { + profile_id: String, + owner_user_id: String, + occurred_at_micros: i64, + }, + RunCheckpointed { + run_id: String, + owner_user_id: String, + total_tap_count: u32, + occurred_at_micros: i64, + }, + RunFinished { + run_id: String, + owner_user_id: String, + total_tap_count: u32, + occurred_at_micros: i64, + }, +} diff --git a/server-rs/crates/module-wooden-fish/src/lib.rs b/server-rs/crates/module-wooden-fish/src/lib.rs new file mode 100644 index 00000000..6acdc7c7 --- /dev/null +++ b/server-rs/crates/module-wooden-fish/src/lib.rs @@ -0,0 +1,11 @@ +mod application; +mod commands; +mod domain; +mod errors; +mod events; + +pub use application::*; +pub use commands::*; +pub use domain::*; +pub use errors::*; +pub use events::*; diff --git a/server-rs/crates/platform-oss/src/lib.rs b/server-rs/crates/platform-oss/src/lib.rs index 59042f3c..830656b5 100644 --- a/server-rs/crates/platform-oss/src/lib.rs +++ b/server-rs/crates/platform-oss/src/lib.rs @@ -20,12 +20,13 @@ const OSS_V4_REQUEST: &str = "aliyun_v4_request"; const OSS_V4_SERVICE: &str = "oss"; const OSS_UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD"; -pub const LEGACY_PUBLIC_PREFIXES: [&str; 11] = [ +pub const LEGACY_PUBLIC_PREFIXES: [&str; 12] = [ "generated-character-drafts", "generated-characters", "generated-animations", "generated-big-fish-assets", "generated-square-hole-assets", + "generated-wooden-fish-assets", "generated-match3d-assets", "generated-puzzle-assets", "generated-custom-world-scenes", @@ -48,6 +49,7 @@ pub enum LegacyAssetPrefix { Animations, BigFishAssets, SquareHoleAssets, + WoodenFishAssets, Match3DAssets, PuzzleAssets, CustomWorldScenes, @@ -236,6 +238,7 @@ impl LegacyAssetPrefix { "generated-animations" => Some(Self::Animations), "generated-big-fish-assets" => Some(Self::BigFishAssets), "generated-square-hole-assets" => Some(Self::SquareHoleAssets), + "generated-wooden-fish-assets" => Some(Self::WoodenFishAssets), "generated-match3d-assets" => Some(Self::Match3DAssets), "generated-puzzle-assets" => Some(Self::PuzzleAssets), "generated-custom-world-scenes" => Some(Self::CustomWorldScenes), @@ -253,6 +256,7 @@ impl LegacyAssetPrefix { Self::Animations => "generated-animations", Self::BigFishAssets => "generated-big-fish-assets", Self::SquareHoleAssets => "generated-square-hole-assets", + Self::WoodenFishAssets => "generated-wooden-fish-assets", Self::Match3DAssets => "generated-match3d-assets", Self::PuzzleAssets => "generated-puzzle-assets", Self::CustomWorldScenes => "generated-custom-world-scenes", @@ -1317,9 +1321,14 @@ mod tests { LegacyAssetPrefix::parse("/generated-match3d-assets/*"), Some(LegacyAssetPrefix::Match3DAssets) ); + assert_eq!( + LegacyAssetPrefix::parse("/generated-wooden-fish-assets/*"), + Some(LegacyAssetPrefix::WoodenFishAssets) + ); assert!(LEGACY_PUBLIC_PREFIXES.contains(&"generated-puzzle-assets")); assert!(LEGACY_PUBLIC_PREFIXES.contains(&"generated-match3d-assets")); assert!(LEGACY_PUBLIC_PREFIXES.contains(&"generated-bark-battle-assets")); + assert!(LEGACY_PUBLIC_PREFIXES.contains(&"generated-wooden-fish-assets")); assert_eq!(LegacyAssetPrefix::parse("unknown"), None); } @@ -1559,6 +1568,12 @@ mod tests { ), Some(LegacyAssetPrefix::CustomWorldScenes) ); + assert_eq!( + LegacyAssetPrefix::from_object_key( + "generated-wooden-fish-assets/session/profile/hit_object/asset/image.png" + ), + Some(LegacyAssetPrefix::WoodenFishAssets) + ); assert_eq!( LegacyAssetPrefix::from_object_key("workflow-cache/demo.json"), None diff --git a/server-rs/crates/shared-contracts/src/creation_audio.rs b/server-rs/crates/shared-contracts/src/creation_audio.rs index 21d37478..f6a3ca41 100644 --- a/server-rs/crates/shared-contracts/src/creation_audio.rs +++ b/server-rs/crates/shared-contracts/src/creation_audio.rs @@ -61,6 +61,7 @@ pub enum CreationAudioStoragePrefix { PuzzleAssets, #[serde(rename = "match3d_assets")] Match3DAssets, + WoodenFishAssets, CustomWorldScenes, } @@ -125,4 +126,20 @@ mod tests { assert_eq!(payload["taskId"], json!("task-1")); assert_eq!(payload["audioSrc"], json!("/generated-puzzle-assets/a.mp3")); } + + #[test] + fn creation_audio_contracts_support_wooden_fish_storage_prefix() { + let request = PublishGeneratedAudioAssetRequest { + entity_kind: "wooden_fish_work".to_string(), + entity_id: "wooden-fish-profile-1".to_string(), + slot: "hit_sound".to_string(), + asset_kind: "wooden_fish_hit_sound".to_string(), + profile_id: Some("wooden-fish-profile-1".to_string()), + storage_prefix: Some(CreationAudioStoragePrefix::WoodenFishAssets), + }; + + let payload = serde_json::to_value(request).expect("request should serialize"); + + assert_eq!(payload["storagePrefix"], json!("wooden_fish_assets")); + } } diff --git a/server-rs/crates/shared-contracts/src/lib.rs b/server-rs/crates/shared-contracts/src/lib.rs index 5324480d..ea60bfe9 100644 --- a/server-rs/crates/shared-contracts/src/lib.rs +++ b/server-rs/crates/shared-contracts/src/lib.rs @@ -29,3 +29,4 @@ pub mod square_hole_runtime; pub mod square_hole_works; pub mod story; pub mod visual_novel; +pub mod wooden_fish; diff --git a/server-rs/crates/shared-contracts/src/wooden_fish.rs b/server-rs/crates/shared-contracts/src/wooden_fish.rs new file mode 100644 index 00000000..18cc81d5 --- /dev/null +++ b/server-rs/crates/shared-contracts/src/wooden_fish.rs @@ -0,0 +1,507 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum WoodenFishGenerationStatus { + Draft, + Generating, + Ready, + Failed, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum WoodenFishActionType { + CompileDraft, + RegenerateHitObject, + GenerateHitSound, + ReplaceHitSound, + UpdateWorkMeta, + UpdateFloatingWords, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum WoodenFishRunStatus { + Playing, + Finished, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishImageAsset { + pub asset_id: String, + pub image_src: String, + pub image_object_key: String, + pub asset_object_id: String, + pub generation_provider: String, + pub prompt: String, + pub width: u32, + pub height: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishAudioAsset { + pub asset_id: String, + pub audio_src: String, + pub audio_object_key: String, + pub asset_object_id: String, + pub source: String, + #[serde(default)] + pub prompt: Option, + #[serde(default)] + pub duration_ms: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishWorkspaceCreateRequest { + pub template_id: String, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub hit_object_prompt: String, + #[serde(default)] + pub hit_object_reference_image_src: Option, + #[serde(default)] + pub hit_sound_prompt: Option, + #[serde(default)] + pub hit_sound_asset: Option, + pub floating_words: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishActionRequest { + pub action_type: WoodenFishActionType, + #[serde(default, skip_deserializing)] + pub profile_id: Option, + #[serde(default)] + pub work_title: Option, + #[serde(default)] + pub work_description: Option, + #[serde(default)] + pub theme_tags: Option>, + #[serde(default)] + pub hit_object_prompt: Option, + #[serde(default)] + pub hit_object_reference_image_src: Option, + #[serde(default, skip_deserializing)] + pub hit_object_asset: Option, + #[serde(default)] + #[serde(skip_deserializing)] + pub background_asset: Option, + #[serde(default)] + pub hit_sound_prompt: Option, + #[serde(default)] + pub hit_sound_asset: Option, + #[serde(default)] + pub floating_words: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishWordCounter { + pub text: String, + pub count: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishDraftResponse { + pub template_id: String, + pub template_name: String, + #[serde(default)] + pub profile_id: Option, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub hit_object_prompt: String, + #[serde(default)] + pub hit_object_reference_image_src: Option, + #[serde(default)] + pub hit_sound_prompt: Option, + pub floating_words: Vec, + #[serde(default)] + pub hit_object_asset: Option, + #[serde(default)] + pub background_asset: Option, + #[serde(default)] + pub hit_sound_asset: Option, + #[serde(default)] + pub cover_image_src: Option, + pub generation_status: WoodenFishGenerationStatus, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishSessionSnapshotResponse { + pub session_id: String, + pub owner_user_id: String, + pub status: WoodenFishGenerationStatus, + #[serde(default)] + pub draft: Option, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishSessionResponse { + pub session: WoodenFishSessionSnapshotResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishActionResponse { + pub action_type: WoodenFishActionType, + pub session: WoodenFishSessionSnapshotResponse, + #[serde(default)] + pub work: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishWorkSummaryResponse { + pub runtime_kind: String, + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + #[serde(default)] + pub source_session_id: Option, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + #[serde(default)] + pub cover_image_src: Option, + pub publication_status: String, + pub play_count: u32, + pub updated_at: String, + #[serde(default)] + pub published_at: Option, + pub publish_ready: bool, + pub generation_status: WoodenFishGenerationStatus, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishWorkProfileResponse { + pub summary: WoodenFishWorkSummaryResponse, + pub draft: WoodenFishDraftResponse, + pub hit_object_asset: WoodenFishImageAsset, + #[serde(default)] + pub background_asset: Option, + pub hit_sound_asset: WoodenFishAudioAsset, + pub floating_words: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishWorkDetailResponse { + pub item: WoodenFishWorkProfileResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishWorkMutationResponse { + pub item: WoodenFishWorkProfileResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishGalleryCardResponse { + pub public_work_code: String, + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + #[serde(default)] + pub cover_image_src: Option, + pub theme_tags: Vec, + pub publication_status: String, + pub play_count: u32, + pub updated_at: String, + #[serde(default)] + pub published_at: Option, + pub generation_status: WoodenFishGenerationStatus, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishGalleryResponse { + pub items: Vec, + pub has_more: bool, + #[serde(default)] + pub next_cursor: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishGalleryDetailResponse { + pub item: WoodenFishWorkProfileResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishRuntimeRunSnapshotResponse { + pub run_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub status: WoodenFishRunStatus, + pub total_tap_count: u32, + pub word_counters: Vec, + pub started_at_ms: u64, + pub updated_at_ms: u64, + #[serde(default)] + pub finished_at_ms: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishRunResponse { + pub run: WoodenFishRuntimeRunSnapshotResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishStartRunRequest { + pub profile_id: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishCheckpointRunRequest { + pub total_tap_count: u32, + pub word_counters: Vec, + pub client_event_id: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishFinishRunRequest { + pub total_tap_count: u32, + pub word_counters: Vec, + pub client_event_id: String, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn wooden_fish_workspace_request_uses_camel_case_and_default_words() { + let payload = serde_json::to_value(WoodenFishWorkspaceCreateRequest { + template_id: "wooden-fish".to_string(), + work_title: "今日敲木鱼".to_string(), + work_description: "轻松敲击".to_string(), + theme_tags: vec!["休闲".to_string()], + hit_object_prompt: "卡通木鱼".to_string(), + hit_object_reference_image_src: None, + hit_sound_prompt: Some("清脆木鱼声".to_string()), + hit_sound_asset: None, + floating_words: vec![ + "幸运".to_string(), + "健康".to_string(), + "财富".to_string(), + "姻缘".to_string(), + "幸福".to_string(), + "事业".to_string(), + "成功".to_string(), + "功德".to_string(), + ], + }) + .expect("payload should serialize"); + + assert_eq!(payload["templateId"], json!("wooden-fish")); + assert_eq!(payload["hitObjectPrompt"], json!("卡通木鱼")); + assert_eq!(payload["hitSoundPrompt"], json!("清脆木鱼声")); + assert_eq!(payload["floatingWords"][7], json!("功德")); + } + + #[test] + fn wooden_fish_runtime_snapshot_counts_words_inside_one_run() { + let payload = serde_json::to_value(WoodenFishRuntimeRunSnapshotResponse { + run_id: "wooden-fish-run-1".to_string(), + profile_id: "wooden-fish-profile-1".to_string(), + owner_user_id: "user-1".to_string(), + status: WoodenFishRunStatus::Playing, + total_tap_count: 3, + word_counters: vec![ + WoodenFishWordCounter { + text: "幸运".to_string(), + count: 2, + }, + WoodenFishWordCounter { + text: "功德".to_string(), + count: 1, + }, + ], + started_at_ms: 100, + updated_at_ms: 130, + finished_at_ms: None, + }) + .expect("payload should serialize"); + + assert_eq!(payload["status"], json!("playing")); + assert_eq!(payload["totalTapCount"], json!(3)); + assert_eq!(payload["wordCounters"][0]["text"], json!("幸运")); + } + + #[test] + fn wooden_fish_action_request_serializes_audio_and_image_fields() { + let payload = serde_json::to_value(WoodenFishActionRequest { + action_type: WoodenFishActionType::ReplaceHitSound, + profile_id: Some("wooden-fish-profile-1".to_string()), + work_title: None, + work_description: None, + theme_tags: None, + hit_object_prompt: Some("卡通铜钹".to_string()), + hit_object_reference_image_src: Some("/uploads/reference.png".to_string()), + hit_object_asset: Some(WoodenFishImageAsset { + asset_id: "image-1".to_string(), + image_src: "/generated-wooden-fish-assets/profile/hit-object/image.png".to_string(), + image_object_key: "generated-wooden-fish-assets/profile/hit-object/image.png" + .to_string(), + asset_object_id: "image-object-1".to_string(), + generation_provider: "image2".to_string(), + prompt: "卡通铜钹".to_string(), + width: 1024, + height: 1024, + }), + background_asset: Some(WoodenFishImageAsset { + asset_id: "background-1".to_string(), + image_src: "/generated-wooden-fish-assets/profile/background/image.png" + .to_string(), + image_object_key: "generated-wooden-fish-assets/profile/background/image.png" + .to_string(), + asset_object_id: "background-object-1".to_string(), + generation_provider: "image2".to_string(), + prompt: "赛博莲花背景".to_string(), + width: 1024, + height: 1536, + }), + hit_sound_prompt: Some("短促木鱼声".to_string()), + hit_sound_asset: Some(WoodenFishAudioAsset { + asset_id: "sound-1".to_string(), + audio_src: "/generated/wooden-fish.mp3".to_string(), + audio_object_key: "generated/wooden-fish.mp3".to_string(), + asset_object_id: "asset-object-1".to_string(), + source: "upload".to_string(), + prompt: None, + duration_ms: Some(800), + }), + floating_words: Some(vec!["功德".to_string()]), + }) + .expect("payload should serialize"); + + assert_eq!(payload["actionType"], json!("replace-hit-sound")); + assert_eq!(payload["profileId"], json!("wooden-fish-profile-1")); + assert_eq!(payload["hitObjectPrompt"], json!("卡通铜钹")); + assert_eq!( + payload["hitObjectAsset"]["imageObjectKey"], + json!("generated-wooden-fish-assets/profile/hit-object/image.png") + ); + assert_eq!(payload["backgroundAsset"]["height"], json!(1536)); + assert_eq!(payload["hitSoundAsset"]["source"], json!("upload")); + assert_eq!(payload["hitSoundAsset"]["durationMs"], json!(800)); + } + + #[test] + fn wooden_fish_action_request_ignores_client_hit_object_asset() { + let payload = serde_json::from_value::(json!({ + "actionType": "compile-draft", + "hitObjectPrompt": "卡通铜钹", + "hitObjectAsset": { + "assetId": "client-image", + "imageSrc": "/generated-wooden-fish-assets/client/image.png", + "imageObjectKey": "generated-wooden-fish-assets/client/image.png", + "assetObjectId": "client-asset-object", + "generationProvider": "client", + "prompt": "跳过生成", + "width": 1024, + "height": 1024 + } + })) + .expect("payload should deserialize"); + + assert_eq!(payload.action_type, WoodenFishActionType::CompileDraft); + assert_eq!(payload.hit_object_prompt.as_deref(), Some("卡通铜钹")); + assert_eq!(payload.hit_object_asset, None); + } + + #[test] + fn wooden_fish_work_profile_keeps_summary_and_runtime_assets() { + let image = WoodenFishImageAsset { + asset_id: "image-1".to_string(), + image_src: "/generated/wooden-fish.png".to_string(), + image_object_key: "generated/wooden-fish.png".to_string(), + asset_object_id: "image-object-1".to_string(), + generation_provider: "image2".to_string(), + prompt: "卡通木鱼".to_string(), + width: 1024, + height: 1024, + }; + let audio = WoodenFishAudioAsset { + asset_id: "sound-1".to_string(), + audio_src: "/generated/wooden-fish.mp3".to_string(), + audio_object_key: "generated/wooden-fish.mp3".to_string(), + asset_object_id: "sound-object-1".to_string(), + source: "generated".to_string(), + prompt: Some("清脆木鱼".to_string()), + duration_ms: Some(600), + }; + let profile = WoodenFishWorkProfileResponse { + summary: WoodenFishWorkSummaryResponse { + runtime_kind: "wooden-fish".to_string(), + work_id: "wooden-fish-profile-1".to_string(), + profile_id: "wooden-fish-profile-1".to_string(), + owner_user_id: "user-1".to_string(), + source_session_id: Some("wooden-fish-session-1".to_string()), + work_title: "敲木鱼".to_string(), + work_description: String::new(), + theme_tags: vec!["休闲".to_string()], + cover_image_src: Some(image.image_src.clone()), + publication_status: "draft".to_string(), + play_count: 0, + updated_at: "2026-05-20T00:00:00Z".to_string(), + published_at: None, + publish_ready: true, + generation_status: WoodenFishGenerationStatus::Ready, + }, + draft: WoodenFishDraftResponse { + template_id: "wooden-fish".to_string(), + template_name: "敲木鱼".to_string(), + profile_id: Some("wooden-fish-profile-1".to_string()), + work_title: "敲木鱼".to_string(), + work_description: String::new(), + theme_tags: vec!["休闲".to_string()], + hit_object_prompt: "卡通木鱼".to_string(), + hit_object_reference_image_src: None, + hit_sound_prompt: Some("清脆木鱼".to_string()), + floating_words: vec!["功德".to_string()], + hit_object_asset: Some(image.clone()), + background_asset: None, + hit_sound_asset: Some(audio.clone()), + cover_image_src: Some(image.image_src.clone()), + generation_status: WoodenFishGenerationStatus::Ready, + }, + hit_object_asset: image, + background_asset: None, + hit_sound_asset: audio, + floating_words: vec!["功德".to_string()], + }; + + let payload = serde_json::to_value(profile).expect("profile should serialize"); + + assert_eq!(payload["summary"]["runtimeKind"], json!("wooden-fish")); + assert_eq!( + payload["hitObjectAsset"]["generationProvider"], + json!("image2") + ); + assert_eq!(payload["hitSoundAsset"]["source"], json!("generated")); + } +} diff --git a/server-rs/crates/spacetime-client/Cargo.toml b/server-rs/crates/spacetime-client/Cargo.toml index 95c172ef..fcb0c9ce 100644 --- a/server-rs/crates/spacetime-client/Cargo.toml +++ b/server-rs/crates/spacetime-client/Cargo.toml @@ -12,6 +12,7 @@ module-combat = { workspace = true } module-custom-world = { workspace = true } module-inventory = { workspace = true } module-jump-hop = { workspace = true } +module-wooden-fish = { workspace = true } module-match3d = { workspace = true } module-npc = { workspace = true } module-puzzle = { workspace = true } diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index fb492293..5bd54ff4 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -79,7 +79,15 @@ pub use mapper::{ VisualNovelHistoryEntryRecordInput, VisualNovelRunRecord, VisualNovelRunSnapshotRecordInput, VisualNovelRunStartRecordInput, VisualNovelRuntimeEventRecord, VisualNovelRuntimeEventRecordInput, VisualNovelWorkCompileRecordInput, - VisualNovelWorkProfileRecord, VisualNovelWorkUpdateRecordInput, + VisualNovelWorkProfileRecord, VisualNovelWorkUpdateRecordInput, WoodenFishActionRequest, + WoodenFishActionResponse, WoodenFishActionType, WoodenFishAudioAsset, + WoodenFishCheckpointRunRequest, WoodenFishDraftResponse, WoodenFishFinishRunRequest, + WoodenFishGalleryCardResponse, WoodenFishGalleryDetailResponse, WoodenFishGalleryResponse, + WoodenFishGenerationStatus, WoodenFishImageAsset, WoodenFishRunResponse, WoodenFishRunStatus, + WoodenFishRuntimeRunSnapshotResponse, WoodenFishSessionResponse, + WoodenFishSessionSnapshotResponse, WoodenFishStartRunRequest, WoodenFishWordCounter, + WoodenFishWorkDetailResponse, WoodenFishWorkMutationResponse, WoodenFishWorkProfileResponse, + WoodenFishWorkSummaryResponse, WoodenFishWorkspaceCreateRequest, }; pub mod ai; @@ -104,6 +112,7 @@ pub mod square_hole; pub mod story; pub mod story_runtime; pub mod visual_novel; +pub mod wooden_fish; use std::{ collections::HashMap, @@ -563,6 +572,7 @@ impl SpacetimeClient { "SELECT * FROM bark_battle_gallery_view", "SELECT * FROM puzzle_gallery_card_view", "SELECT * FROM jump_hop_gallery_card_view", + "SELECT * FROM wooden_fish_gallery_card_view", "SELECT * FROM custom_world_gallery_entry", "SELECT * FROM match_3_d_gallery_view", "SELECT * FROM square_hole_gallery_view", @@ -578,6 +588,7 @@ impl SpacetimeClient { for query in [ "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'", "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'jump-hop'", + "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'wooden-fish'", "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'custom-world'", "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'match3d'", "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'square-hole'", diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index f2bc9530..df2d43b4 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -18,6 +18,7 @@ mod runtime_profile; mod square_hole; mod story; mod visual_novel; +mod wooden_fish; pub use self::ai::{ AiResultReferenceRecord, AiTaskMutationRecord, AiTaskRecord, AiTaskStageRecord, @@ -118,6 +119,16 @@ pub use self::runtime_profile::{ SquareHoleDropConfirmationRecord, SquareHoleDropFeedbackRecord, SquareHoleRunRecord, }; pub use self::story::{VisualNovelRuntimeEventRecord, VisualNovelRuntimeEventRecordInput}; +pub use self::wooden_fish::{ + WoodenFishActionRequest, WoodenFishActionResponse, WoodenFishActionType, WoodenFishAudioAsset, + WoodenFishCheckpointRunRequest, WoodenFishDraftResponse, WoodenFishFinishRunRequest, + WoodenFishGalleryCardResponse, WoodenFishGalleryDetailResponse, WoodenFishGalleryResponse, + WoodenFishGenerationStatus, WoodenFishImageAsset, WoodenFishRunResponse, WoodenFishRunStatus, + WoodenFishRuntimeRunSnapshotResponse, WoodenFishSessionResponse, + WoodenFishSessionSnapshotResponse, WoodenFishStartRunRequest, WoodenFishWordCounter, + WoodenFishWorkDetailResponse, WoodenFishWorkMutationResponse, WoodenFishWorkProfileResponse, + WoodenFishWorkSummaryResponse, WoodenFishWorkspaceCreateRequest, +}; pub(crate) use self::ai::map_ai_task_procedure_result; pub(crate) use self::assets::{ @@ -223,3 +234,8 @@ pub(crate) use self::visual_novel::{ map_visual_novel_runtime_event_procedure_result, map_visual_novel_work_procedure_result, map_visual_novel_works_procedure_result, }; +pub(crate) use self::wooden_fish::{ + map_wooden_fish_agent_session_procedure_result, map_wooden_fish_gallery_card_view_row, + map_wooden_fish_run_procedure_result, map_wooden_fish_work_procedure_result, + map_wooden_fish_works_procedure_result, +}; diff --git a/server-rs/crates/spacetime-client/src/mapper/wooden_fish.rs b/server-rs/crates/spacetime-client/src/mapper/wooden_fish.rs new file mode 100644 index 00000000..b4edf32f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/mapper/wooden_fish.rs @@ -0,0 +1,240 @@ +use super::*; +pub use shared_contracts::wooden_fish::{ + WoodenFishActionRequest, WoodenFishActionResponse, WoodenFishActionType, WoodenFishAudioAsset, + WoodenFishCheckpointRunRequest, WoodenFishDraftResponse, WoodenFishFinishRunRequest, + WoodenFishGalleryCardResponse, WoodenFishGalleryDetailResponse, WoodenFishGalleryResponse, + WoodenFishGenerationStatus, WoodenFishImageAsset, WoodenFishRunResponse, WoodenFishRunStatus, + WoodenFishRuntimeRunSnapshotResponse, WoodenFishSessionResponse, + WoodenFishSessionSnapshotResponse, WoodenFishStartRunRequest, WoodenFishWordCounter, + WoodenFishWorkDetailResponse, WoodenFishWorkMutationResponse, WoodenFishWorkProfileResponse, + WoodenFishWorkSummaryResponse, WoodenFishWorkspaceCreateRequest, +}; + +pub(crate) fn map_wooden_fish_agent_session_procedure_result( + result: WoodenFishAgentSessionProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + let session = result + .session + .ok_or_else(|| SpacetimeClientError::missing_snapshot("wooden fish agent session 快照"))?; + Ok(map_wooden_fish_session_snapshot(session)) +} + +pub(crate) fn map_wooden_fish_work_procedure_result( + result: WoodenFishWorkProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + let work = result + .work + .ok_or_else(|| SpacetimeClientError::missing_snapshot("wooden fish work 快照"))?; + map_wooden_fish_work_snapshot(work) +} + +pub(crate) fn map_wooden_fish_works_procedure_result( + result: WoodenFishWorksProcedureResult, +) -> Result, SpacetimeClientError> { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + result + .items + .into_iter() + .map(map_wooden_fish_work_snapshot) + .collect() +} + +pub(crate) fn map_wooden_fish_run_procedure_result( + result: WoodenFishRunProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + let run = result + .run + .ok_or_else(|| SpacetimeClientError::missing_snapshot("wooden fish run 快照"))?; + Ok(map_wooden_fish_run_snapshot(run)) +} + +pub(crate) fn map_wooden_fish_gallery_card_view_row( + row: WoodenFishGalleryCardViewRow, +) -> WoodenFishGalleryCardResponse { + WoodenFishGalleryCardResponse { + public_work_code: row.public_work_code, + work_id: row.work_id, + profile_id: row.profile_id, + owner_user_id: row.owner_user_id, + author_display_name: row.author_display_name, + work_title: row.work_title, + work_description: row.work_description, + cover_image_src: empty_string_to_none(row.cover_image_src), + theme_tags: row.theme_tags, + publication_status: normalize_publication_status(&row.publication_status).to_string(), + play_count: row.play_count, + updated_at: format_timestamp_micros(row.updated_at_micros), + published_at: row.published_at_micros.map(format_timestamp_micros), + generation_status: parse_generation_status(&row.generation_status), + } +} + +fn map_wooden_fish_session_snapshot( + snapshot: WoodenFishAgentSessionSnapshot, +) -> WoodenFishSessionSnapshotResponse { + WoodenFishSessionSnapshotResponse { + session_id: snapshot.session_id, + owner_user_id: snapshot.owner_user_id, + status: snapshot + .draft + .as_ref() + .map(|draft| parse_generation_status(&draft.generation_status)) + .unwrap_or(WoodenFishGenerationStatus::Draft), + draft: snapshot.draft.map(map_wooden_fish_draft_snapshot), + created_at: format_timestamp_micros(snapshot.created_at_micros), + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + } +} + +fn map_wooden_fish_work_snapshot( + snapshot: WoodenFishWorkSnapshot, +) -> Result { + let draft = WoodenFishDraftResponse { + template_id: "wooden-fish".to_string(), + template_name: "敲木鱼".to_string(), + profile_id: Some(snapshot.profile_id.clone()), + work_title: snapshot.work_title.clone(), + work_description: snapshot.work_description.clone(), + theme_tags: snapshot.theme_tags.clone(), + hit_object_prompt: snapshot.hit_object_prompt.clone(), + hit_object_reference_image_src: snapshot.hit_object_reference_image_src.clone(), + hit_sound_prompt: snapshot.hit_sound_prompt.clone(), + floating_words: snapshot.floating_words.clone(), + hit_object_asset: snapshot.hit_object_asset.clone().map(map_image_asset), + background_asset: snapshot.background_asset.clone().map(map_image_asset), + hit_sound_asset: snapshot.hit_sound_asset.clone().map(map_audio_asset), + cover_image_src: empty_string_to_none(snapshot.cover_image_src.clone()), + generation_status: parse_generation_status(&snapshot.generation_status), + }; + let hit_object_asset = draft + .hit_object_asset + .clone() + .ok_or_else(|| SpacetimeClientError::missing_snapshot("wooden fish hit object asset"))?; + let hit_sound_asset = draft + .hit_sound_asset + .clone() + .ok_or_else(|| SpacetimeClientError::missing_snapshot("wooden fish hit sound asset"))?; + Ok(WoodenFishWorkProfileResponse { + summary: WoodenFishWorkSummaryResponse { + runtime_kind: "wooden-fish".to_string(), + work_id: snapshot.work_id, + profile_id: snapshot.profile_id, + owner_user_id: snapshot.owner_user_id, + source_session_id: empty_string_to_none(snapshot.source_session_id), + work_title: snapshot.work_title, + work_description: snapshot.work_description, + theme_tags: snapshot.theme_tags, + cover_image_src: empty_string_to_none(snapshot.cover_image_src), + publication_status: normalize_publication_status(&snapshot.publication_status) + .to_string(), + play_count: snapshot.play_count, + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + published_at: snapshot.published_at_micros.map(format_timestamp_micros), + publish_ready: snapshot.publish_ready, + generation_status: parse_generation_status(&snapshot.generation_status), + }, + draft, + hit_object_asset, + background_asset: snapshot.background_asset.map(map_image_asset), + hit_sound_asset, + floating_words: snapshot.floating_words, + }) +} + +fn map_wooden_fish_draft_snapshot(snapshot: WoodenFishDraftSnapshot) -> WoodenFishDraftResponse { + WoodenFishDraftResponse { + template_id: snapshot.template_id, + template_name: snapshot.template_name, + profile_id: snapshot.profile_id, + work_title: snapshot.work_title, + work_description: snapshot.work_description, + theme_tags: snapshot.theme_tags, + hit_object_prompt: snapshot.hit_object_prompt, + hit_object_reference_image_src: snapshot.hit_object_reference_image_src, + hit_sound_prompt: snapshot.hit_sound_prompt, + floating_words: snapshot.floating_words, + hit_object_asset: snapshot.hit_object_asset.map(map_image_asset), + background_asset: snapshot.background_asset.map(map_image_asset), + hit_sound_asset: snapshot.hit_sound_asset.map(map_audio_asset), + cover_image_src: snapshot.cover_image_src, + generation_status: parse_generation_status(&snapshot.generation_status), + } +} + +fn map_image_asset(snapshot: WoodenFishImageAssetSnapshot) -> WoodenFishImageAsset { + WoodenFishImageAsset { + asset_id: snapshot.asset_id, + image_src: snapshot.image_src, + image_object_key: snapshot.image_object_key, + asset_object_id: snapshot.asset_object_id, + generation_provider: snapshot.generation_provider, + prompt: snapshot.prompt, + width: snapshot.width, + height: snapshot.height, + } +} + +fn map_audio_asset(snapshot: WoodenFishAudioAssetSnapshot) -> WoodenFishAudioAsset { + WoodenFishAudioAsset { + asset_id: snapshot.asset_id, + audio_src: snapshot.audio_src, + audio_object_key: snapshot.audio_object_key, + asset_object_id: snapshot.asset_object_id, + source: snapshot.source, + prompt: snapshot.prompt, + duration_ms: snapshot.duration_ms, + } +} + +fn map_wooden_fish_run_snapshot( + snapshot: crate::module_bindings::WoodenFishRunSnapshot, +) -> WoodenFishRuntimeRunSnapshotResponse { + WoodenFishRuntimeRunSnapshotResponse { + run_id: snapshot.run_id, + profile_id: snapshot.profile_id, + owner_user_id: snapshot.owner_user_id, + status: match snapshot.status { + crate::module_bindings::WoodenFishRunStatus::Playing => WoodenFishRunStatus::Playing, + crate::module_bindings::WoodenFishRunStatus::Finished => WoodenFishRunStatus::Finished, + }, + total_tap_count: snapshot.total_tap_count, + word_counters: snapshot + .word_counters + .into_iter() + .map(|counter| WoodenFishWordCounter { + text: counter.text, + count: counter.count, + }) + .collect(), + started_at_ms: snapshot.started_at_ms, + updated_at_ms: snapshot.updated_at_ms, + finished_at_ms: snapshot.finished_at_ms, + } +} + +fn parse_generation_status(value: &str) -> WoodenFishGenerationStatus { + match value { + "generating" => WoodenFishGenerationStatus::Generating, + "ready" => WoodenFishGenerationStatus::Ready, + "failed" => WoodenFishGenerationStatus::Failed, + _ => WoodenFishGenerationStatus::Draft, + } +} + +fn normalize_publication_status(value: &str) -> &str { + match value { + "Published" | "published" => "published", + _ => "draft", + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index c9a71a7f..5f9f3392 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.rs @@ -193,6 +193,7 @@ pub mod chapter_progression_procedure_result_type; pub mod chapter_progression_snapshot_type; pub mod chapter_progression_table; pub mod chapter_progression_type; +pub mod checkpoint_wooden_fish_run_procedure; pub mod claim_profile_task_reward_and_return_procedure; pub mod claim_puzzle_work_point_incentive_procedure; pub mod clear_database_migration_import_chunks_procedure; @@ -206,6 +207,7 @@ pub mod compile_match_3_d_draft_procedure; pub mod compile_puzzle_agent_draft_procedure; pub mod compile_square_hole_draft_procedure; pub mod compile_visual_novel_work_profile_procedure; +pub mod compile_wooden_fish_draft_procedure; pub mod complete_ai_stage_and_return_procedure; pub mod complete_ai_task_and_return_procedure; pub mod confirm_asset_object_and_return_procedure; @@ -227,6 +229,7 @@ pub mod create_profile_recharge_order_and_return_procedure; pub mod create_puzzle_agent_session_procedure; pub mod create_square_hole_agent_session_procedure; pub mod create_visual_novel_agent_session_procedure; +pub mod create_wooden_fish_agent_session_procedure; pub mod creation_entry_config_procedure_result_type; pub mod creation_entry_config_snapshot_type; pub mod creation_entry_config_table; @@ -338,6 +341,7 @@ pub mod finalize_visual_novel_agent_message_turn_procedure; pub mod finish_bark_battle_run_procedure; pub mod finish_match_3_d_time_up_procedure; pub mod finish_square_hole_time_up_procedure; +pub mod finish_wooden_fish_run_procedure; pub mod generate_big_fish_asset_procedure; pub mod get_auth_store_snapshot_procedure; pub mod get_bark_battle_run_procedure; @@ -380,6 +384,9 @@ pub mod get_story_session_state_procedure; pub mod get_visual_novel_agent_session_procedure; pub mod get_visual_novel_run_procedure; pub mod get_visual_novel_work_detail_procedure; +pub mod get_wooden_fish_agent_session_procedure; +pub mod get_wooden_fish_run_procedure; +pub mod get_wooden_fish_work_profile_procedure; pub mod grant_inventory_item_input_type; pub mod grant_new_user_registration_wallet_reward_procedure; pub mod grant_player_progression_experience_and_return_procedure; @@ -458,6 +465,7 @@ pub mod list_puzzle_works_procedure; pub mod list_square_hole_works_procedure; pub mod list_visual_novel_runtime_history_procedure; pub mod list_visual_novel_works_procedure; +pub mod list_wooden_fish_works_procedure; pub mod mark_profile_recharge_order_paid_and_return_procedure; pub mod match_3_d_agent_message_finalize_input_type; pub mod match_3_d_agent_message_row_type; @@ -564,6 +572,7 @@ pub mod publish_match_3_d_work_procedure; pub mod publish_puzzle_work_procedure; pub mod publish_square_hole_work_procedure; pub mod publish_visual_novel_work_procedure; +pub mod publish_wooden_fish_work_procedure; pub mod put_database_migration_import_chunk_procedure; pub mod puzzle_agent_message_finalize_input_type; pub mod puzzle_agent_message_kind_type; @@ -878,6 +887,7 @@ pub mod start_match_3_d_run_procedure; pub mod start_puzzle_run_procedure; pub mod start_square_hole_run_procedure; pub mod start_visual_novel_run_procedure; +pub mod start_wooden_fish_run_procedure; pub mod stop_match_3_d_run_procedure; pub mod stop_square_hole_run_procedure; pub mod story_continue_input_type; @@ -924,6 +934,7 @@ pub mod update_puzzle_run_pause_procedure; pub mod update_puzzle_work_procedure; pub mod update_square_hole_work_procedure; pub mod update_visual_novel_work_procedure; +pub mod update_wooden_fish_work_procedure; pub mod upsert_auth_store_snapshot_procedure; pub mod upsert_chapter_progression_and_return_procedure; pub mod upsert_chapter_progression_reducer; @@ -986,6 +997,42 @@ pub mod visual_novel_work_snapshot_type; pub mod visual_novel_work_update_input_type; pub mod visual_novel_works_list_input_type; pub mod visual_novel_works_procedure_result_type; +pub mod wooden_fish_agent_session_create_input_type; +pub mod wooden_fish_agent_session_get_input_type; +pub mod wooden_fish_agent_session_procedure_result_type; +pub mod wooden_fish_agent_session_row_type; +pub mod wooden_fish_agent_session_snapshot_type; +pub mod wooden_fish_agent_session_table; +pub mod wooden_fish_audio_asset_snapshot_type; +pub mod wooden_fish_creator_config_snapshot_type; +pub mod wooden_fish_draft_compile_input_type; +pub mod wooden_fish_draft_snapshot_type; +pub mod wooden_fish_event_row_type; +pub mod wooden_fish_event_table; +pub mod wooden_fish_gallery_card_view_row_type; +pub mod wooden_fish_gallery_card_view_table; +pub mod wooden_fish_gallery_view_row_type; +pub mod wooden_fish_gallery_view_table; +pub mod wooden_fish_image_asset_snapshot_type; +pub mod wooden_fish_run_checkpoint_input_type; +pub mod wooden_fish_run_finish_input_type; +pub mod wooden_fish_run_get_input_type; +pub mod wooden_fish_run_procedure_result_type; +pub mod wooden_fish_run_snapshot_type; +pub mod wooden_fish_run_start_input_type; +pub mod wooden_fish_run_status_type; +pub mod wooden_fish_runtime_run_row_type; +pub mod wooden_fish_runtime_run_table; +pub mod wooden_fish_word_counter_type; +pub mod wooden_fish_work_get_input_type; +pub mod wooden_fish_work_procedure_result_type; +pub mod wooden_fish_work_profile_row_type; +pub mod wooden_fish_work_profile_table; +pub mod wooden_fish_work_publish_input_type; +pub mod wooden_fish_work_snapshot_type; +pub mod wooden_fish_work_update_input_type; +pub mod wooden_fish_works_list_input_type; +pub mod wooden_fish_works_procedure_result_type; pub use accept_quest_reducer::accept_quest; pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion; @@ -1174,6 +1221,7 @@ pub use chapter_progression_procedure_result_type::ChapterProgressionProcedureRe pub use chapter_progression_snapshot_type::ChapterProgressionSnapshot; pub use chapter_progression_table::*; pub use chapter_progression_type::ChapterProgression; +pub use checkpoint_wooden_fish_run_procedure::checkpoint_wooden_fish_run; pub use claim_profile_task_reward_and_return_procedure::claim_profile_task_reward_and_return; pub use claim_puzzle_work_point_incentive_procedure::claim_puzzle_work_point_incentive; pub use clear_database_migration_import_chunks_procedure::clear_database_migration_import_chunks; @@ -1187,6 +1235,7 @@ pub use compile_match_3_d_draft_procedure::compile_match_3_d_draft; pub use compile_puzzle_agent_draft_procedure::compile_puzzle_agent_draft; pub use compile_square_hole_draft_procedure::compile_square_hole_draft; pub use compile_visual_novel_work_profile_procedure::compile_visual_novel_work_profile; +pub use compile_wooden_fish_draft_procedure::compile_wooden_fish_draft; pub use complete_ai_stage_and_return_procedure::complete_ai_stage_and_return; pub use complete_ai_task_and_return_procedure::complete_ai_task_and_return; pub use confirm_asset_object_and_return_procedure::confirm_asset_object_and_return; @@ -1208,6 +1257,7 @@ pub use create_profile_recharge_order_and_return_procedure::create_profile_recha pub use create_puzzle_agent_session_procedure::create_puzzle_agent_session; pub use create_square_hole_agent_session_procedure::create_square_hole_agent_session; pub use create_visual_novel_agent_session_procedure::create_visual_novel_agent_session; +pub use create_wooden_fish_agent_session_procedure::create_wooden_fish_agent_session; pub use creation_entry_config_procedure_result_type::CreationEntryConfigProcedureResult; pub use creation_entry_config_snapshot_type::CreationEntryConfigSnapshot; pub use creation_entry_config_table::*; @@ -1319,6 +1369,7 @@ pub use finalize_visual_novel_agent_message_turn_procedure::finalize_visual_nove pub use finish_bark_battle_run_procedure::finish_bark_battle_run; pub use finish_match_3_d_time_up_procedure::finish_match_3_d_time_up; pub use finish_square_hole_time_up_procedure::finish_square_hole_time_up; +pub use finish_wooden_fish_run_procedure::finish_wooden_fish_run; pub use generate_big_fish_asset_procedure::generate_big_fish_asset; pub use get_auth_store_snapshot_procedure::get_auth_store_snapshot; pub use get_bark_battle_run_procedure::get_bark_battle_run; @@ -1361,6 +1412,9 @@ pub use get_story_session_state_procedure::get_story_session_state; pub use get_visual_novel_agent_session_procedure::get_visual_novel_agent_session; pub use get_visual_novel_run_procedure::get_visual_novel_run; pub use get_visual_novel_work_detail_procedure::get_visual_novel_work_detail; +pub use get_wooden_fish_agent_session_procedure::get_wooden_fish_agent_session; +pub use get_wooden_fish_run_procedure::get_wooden_fish_run; +pub use get_wooden_fish_work_profile_procedure::get_wooden_fish_work_profile; pub use grant_inventory_item_input_type::GrantInventoryItemInput; pub use grant_new_user_registration_wallet_reward_procedure::grant_new_user_registration_wallet_reward; pub use grant_player_progression_experience_and_return_procedure::grant_player_progression_experience_and_return; @@ -1439,6 +1493,7 @@ pub use list_puzzle_works_procedure::list_puzzle_works; pub use list_square_hole_works_procedure::list_square_hole_works; pub use list_visual_novel_runtime_history_procedure::list_visual_novel_runtime_history; pub use list_visual_novel_works_procedure::list_visual_novel_works; +pub use list_wooden_fish_works_procedure::list_wooden_fish_works; pub use mark_profile_recharge_order_paid_and_return_procedure::mark_profile_recharge_order_paid_and_return; pub use match_3_d_agent_message_finalize_input_type::Match3DAgentMessageFinalizeInput; pub use match_3_d_agent_message_row_type::Match3DAgentMessageRow; @@ -1545,6 +1600,7 @@ pub use publish_match_3_d_work_procedure::publish_match_3_d_work; pub use publish_puzzle_work_procedure::publish_puzzle_work; pub use publish_square_hole_work_procedure::publish_square_hole_work; pub use publish_visual_novel_work_procedure::publish_visual_novel_work; +pub use publish_wooden_fish_work_procedure::publish_wooden_fish_work; pub use put_database_migration_import_chunk_procedure::put_database_migration_import_chunk; pub use puzzle_agent_message_finalize_input_type::PuzzleAgentMessageFinalizeInput; pub use puzzle_agent_message_kind_type::PuzzleAgentMessageKind; @@ -1859,6 +1915,7 @@ pub use start_match_3_d_run_procedure::start_match_3_d_run; pub use start_puzzle_run_procedure::start_puzzle_run; pub use start_square_hole_run_procedure::start_square_hole_run; pub use start_visual_novel_run_procedure::start_visual_novel_run; +pub use start_wooden_fish_run_procedure::start_wooden_fish_run; pub use stop_match_3_d_run_procedure::stop_match_3_d_run; pub use stop_square_hole_run_procedure::stop_square_hole_run; pub use story_continue_input_type::StoryContinueInput; @@ -1905,6 +1962,7 @@ pub use update_puzzle_run_pause_procedure::update_puzzle_run_pause; pub use update_puzzle_work_procedure::update_puzzle_work; pub use update_square_hole_work_procedure::update_square_hole_work; pub use update_visual_novel_work_procedure::update_visual_novel_work; +pub use update_wooden_fish_work_procedure::update_wooden_fish_work; pub use upsert_auth_store_snapshot_procedure::upsert_auth_store_snapshot; pub use upsert_chapter_progression_and_return_procedure::upsert_chapter_progression_and_return; pub use upsert_chapter_progression_reducer::upsert_chapter_progression; @@ -1967,6 +2025,42 @@ pub use visual_novel_work_snapshot_type::VisualNovelWorkSnapshot; pub use visual_novel_work_update_input_type::VisualNovelWorkUpdateInput; pub use visual_novel_works_list_input_type::VisualNovelWorksListInput; pub use visual_novel_works_procedure_result_type::VisualNovelWorksProcedureResult; +pub use wooden_fish_agent_session_create_input_type::WoodenFishAgentSessionCreateInput; +pub use wooden_fish_agent_session_get_input_type::WoodenFishAgentSessionGetInput; +pub use wooden_fish_agent_session_procedure_result_type::WoodenFishAgentSessionProcedureResult; +pub use wooden_fish_agent_session_row_type::WoodenFishAgentSessionRow; +pub use wooden_fish_agent_session_snapshot_type::WoodenFishAgentSessionSnapshot; +pub use wooden_fish_agent_session_table::*; +pub use wooden_fish_audio_asset_snapshot_type::WoodenFishAudioAssetSnapshot; +pub use wooden_fish_creator_config_snapshot_type::WoodenFishCreatorConfigSnapshot; +pub use wooden_fish_draft_compile_input_type::WoodenFishDraftCompileInput; +pub use wooden_fish_draft_snapshot_type::WoodenFishDraftSnapshot; +pub use wooden_fish_event_row_type::WoodenFishEventRow; +pub use wooden_fish_event_table::*; +pub use wooden_fish_gallery_card_view_row_type::WoodenFishGalleryCardViewRow; +pub use wooden_fish_gallery_card_view_table::*; +pub use wooden_fish_gallery_view_row_type::WoodenFishGalleryViewRow; +pub use wooden_fish_gallery_view_table::*; +pub use wooden_fish_image_asset_snapshot_type::WoodenFishImageAssetSnapshot; +pub use wooden_fish_run_checkpoint_input_type::WoodenFishRunCheckpointInput; +pub use wooden_fish_run_finish_input_type::WoodenFishRunFinishInput; +pub use wooden_fish_run_get_input_type::WoodenFishRunGetInput; +pub use wooden_fish_run_procedure_result_type::WoodenFishRunProcedureResult; +pub use wooden_fish_run_snapshot_type::WoodenFishRunSnapshot; +pub use wooden_fish_run_start_input_type::WoodenFishRunStartInput; +pub use wooden_fish_run_status_type::WoodenFishRunStatus; +pub use wooden_fish_runtime_run_row_type::WoodenFishRuntimeRunRow; +pub use wooden_fish_runtime_run_table::*; +pub use wooden_fish_word_counter_type::WoodenFishWordCounter; +pub use wooden_fish_work_get_input_type::WoodenFishWorkGetInput; +pub use wooden_fish_work_procedure_result_type::WoodenFishWorkProcedureResult; +pub use wooden_fish_work_profile_row_type::WoodenFishWorkProfileRow; +pub use wooden_fish_work_profile_table::*; +pub use wooden_fish_work_publish_input_type::WoodenFishWorkPublishInput; +pub use wooden_fish_work_snapshot_type::WoodenFishWorkSnapshot; +pub use wooden_fish_work_update_input_type::WoodenFishWorkUpdateInput; +pub use wooden_fish_works_list_input_type::WoodenFishWorksListInput; +pub use wooden_fish_works_procedure_result_type::WoodenFishWorksProcedureResult; #[derive(Clone, PartialEq, Debug)] @@ -2342,6 +2436,12 @@ pub struct DbUpdate { visual_novel_runtime_history_entry: __sdk::TableUpdate, visual_novel_runtime_run: __sdk::TableUpdate, visual_novel_work_profile: __sdk::TableUpdate, + wooden_fish_agent_session: __sdk::TableUpdate, + wooden_fish_event: __sdk::TableUpdate, + wooden_fish_gallery_card_view: __sdk::TableUpdate, + wooden_fish_gallery_view: __sdk::TableUpdate, + wooden_fish_runtime_run: __sdk::TableUpdate, + wooden_fish_work_profile: __sdk::TableUpdate, } impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { @@ -2668,6 +2768,24 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "visual_novel_work_profile" => db_update.visual_novel_work_profile.append( visual_novel_work_profile_table::parse_table_update(table_update)?, ), + "wooden_fish_agent_session" => db_update.wooden_fish_agent_session.append( + wooden_fish_agent_session_table::parse_table_update(table_update)?, + ), + "wooden_fish_event" => db_update + .wooden_fish_event + .append(wooden_fish_event_table::parse_table_update(table_update)?), + "wooden_fish_gallery_card_view" => db_update.wooden_fish_gallery_card_view.append( + wooden_fish_gallery_card_view_table::parse_table_update(table_update)?, + ), + "wooden_fish_gallery_view" => db_update.wooden_fish_gallery_view.append( + wooden_fish_gallery_view_table::parse_table_update(table_update)?, + ), + "wooden_fish_runtime_run" => db_update.wooden_fish_runtime_run.append( + wooden_fish_runtime_run_table::parse_table_update(table_update)?, + ), + "wooden_fish_work_profile" => db_update.wooden_fish_work_profile.append( + wooden_fish_work_profile_table::parse_table_update(table_update)?, + ), unknown => { return Err(__sdk::InternalError::unknown_name( @@ -3171,6 +3289,27 @@ impl __sdk::DbUpdate for DbUpdate { "bark_battle_gallery_view", &self.bark_battle_gallery_view, ); + diff.wooden_fish_agent_session = cache + .apply_diff_to_table::( + "wooden_fish_agent_session", + &self.wooden_fish_agent_session, + ) + .with_updates_by_pk(|row| &row.session_id); + diff.wooden_fish_event = cache + .apply_diff_to_table::("wooden_fish_event", &self.wooden_fish_event) + .with_updates_by_pk(|row| &row.event_id); + diff.wooden_fish_runtime_run = cache + .apply_diff_to_table::( + "wooden_fish_runtime_run", + &self.wooden_fish_runtime_run, + ) + .with_updates_by_pk(|row| &row.run_id); + diff.wooden_fish_work_profile = cache + .apply_diff_to_table::( + "wooden_fish_work_profile", + &self.wooden_fish_work_profile, + ) + .with_updates_by_pk(|row| &row.profile_id); diff.big_fish_gallery_view = cache.apply_diff_to_table::( "big_fish_gallery_view", &self.big_fish_gallery_view, @@ -3203,6 +3342,15 @@ impl __sdk::DbUpdate for DbUpdate { "visual_novel_gallery_view", &self.visual_novel_gallery_view, ); + diff.wooden_fish_gallery_card_view = cache + .apply_diff_to_table::( + "wooden_fish_gallery_card_view", + &self.wooden_fish_gallery_card_view, + ); + diff.wooden_fish_gallery_view = cache.apply_diff_to_table::( + "wooden_fish_gallery_view", + &self.wooden_fish_gallery_view, + ); diff } @@ -3516,6 +3664,24 @@ impl __sdk::DbUpdate for DbUpdate { "visual_novel_work_profile" => db_update .visual_novel_work_profile .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "wooden_fish_agent_session" => db_update + .wooden_fish_agent_session + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "wooden_fish_event" => db_update + .wooden_fish_event + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "wooden_fish_gallery_card_view" => db_update + .wooden_fish_gallery_card_view + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "wooden_fish_gallery_view" => db_update + .wooden_fish_gallery_view + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "wooden_fish_runtime_run" => db_update + .wooden_fish_runtime_run + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "wooden_fish_work_profile" => db_update + .wooden_fish_work_profile + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), unknown => { return Err( __sdk::InternalError::unknown_name("table", unknown, "QueryRows").into(), @@ -3835,6 +4001,24 @@ impl __sdk::DbUpdate for DbUpdate { "visual_novel_work_profile" => db_update .visual_novel_work_profile .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "wooden_fish_agent_session" => db_update + .wooden_fish_agent_session + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "wooden_fish_event" => db_update + .wooden_fish_event + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "wooden_fish_gallery_card_view" => db_update + .wooden_fish_gallery_card_view + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "wooden_fish_gallery_view" => db_update + .wooden_fish_gallery_view + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "wooden_fish_runtime_run" => db_update + .wooden_fish_runtime_run + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "wooden_fish_work_profile" => db_update + .wooden_fish_work_profile + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), unknown => { return Err( __sdk::InternalError::unknown_name("table", unknown, "QueryRows").into(), @@ -3955,6 +4139,12 @@ pub struct AppliedDiff<'r> { __sdk::TableAppliedDiff<'r, VisualNovelRuntimeHistoryEntryRow>, visual_novel_runtime_run: __sdk::TableAppliedDiff<'r, VisualNovelRuntimeRunRow>, visual_novel_work_profile: __sdk::TableAppliedDiff<'r, VisualNovelWorkProfileRow>, + wooden_fish_agent_session: __sdk::TableAppliedDiff<'r, WoodenFishAgentSessionRow>, + wooden_fish_event: __sdk::TableAppliedDiff<'r, WoodenFishEventRow>, + wooden_fish_gallery_card_view: __sdk::TableAppliedDiff<'r, WoodenFishGalleryCardViewRow>, + wooden_fish_gallery_view: __sdk::TableAppliedDiff<'r, WoodenFishGalleryViewRow>, + wooden_fish_runtime_run: __sdk::TableAppliedDiff<'r, WoodenFishRuntimeRunRow>, + wooden_fish_work_profile: __sdk::TableAppliedDiff<'r, WoodenFishWorkProfileRow>, __unused: std::marker::PhantomData<&'r ()>, } @@ -4458,6 +4648,36 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.visual_novel_work_profile, event, ); + callbacks.invoke_table_row_callbacks::( + "wooden_fish_agent_session", + &self.wooden_fish_agent_session, + event, + ); + callbacks.invoke_table_row_callbacks::( + "wooden_fish_event", + &self.wooden_fish_event, + event, + ); + callbacks.invoke_table_row_callbacks::( + "wooden_fish_gallery_card_view", + &self.wooden_fish_gallery_card_view, + event, + ); + callbacks.invoke_table_row_callbacks::( + "wooden_fish_gallery_view", + &self.wooden_fish_gallery_view, + event, + ); + callbacks.invoke_table_row_callbacks::( + "wooden_fish_runtime_run", + &self.wooden_fish_runtime_run, + event, + ); + callbacks.invoke_table_row_callbacks::( + "wooden_fish_work_profile", + &self.wooden_fish_work_profile, + event, + ); } } @@ -5220,6 +5440,12 @@ impl __sdk::SpacetimeModule for RemoteModule { visual_novel_runtime_history_entry_table::register_table(client_cache); visual_novel_runtime_run_table::register_table(client_cache); visual_novel_work_profile_table::register_table(client_cache); + wooden_fish_agent_session_table::register_table(client_cache); + wooden_fish_event_table::register_table(client_cache); + wooden_fish_gallery_card_view_table::register_table(client_cache); + wooden_fish_gallery_view_table::register_table(client_cache); + wooden_fish_runtime_run_table::register_table(client_cache); + wooden_fish_work_profile_table::register_table(client_cache); } const ALL_TABLE_NAMES: &'static [&'static str] = &[ "ai_result_reference", @@ -5324,5 +5550,11 @@ impl __sdk::SpacetimeModule for RemoteModule { "visual_novel_runtime_history_entry", "visual_novel_runtime_run", "visual_novel_work_profile", + "wooden_fish_agent_session", + "wooden_fish_event", + "wooden_fish_gallery_card_view", + "wooden_fish_gallery_view", + "wooden_fish_runtime_run", + "wooden_fish_work_profile", ]; } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/checkpoint_wooden_fish_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/checkpoint_wooden_fish_run_procedure.rs new file mode 100644 index 00000000..5bd5a45c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/checkpoint_wooden_fish_run_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_run_checkpoint_input_type::WoodenFishRunCheckpointInput; +use super::wooden_fish_run_procedure_result_type::WoodenFishRunProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct CheckpointWoodenFishRunArgs { + pub input: WoodenFishRunCheckpointInput, +} + +impl __sdk::InModule for CheckpointWoodenFishRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `checkpoint_wooden_fish_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait checkpoint_wooden_fish_run { + fn checkpoint_wooden_fish_run(&self, input: WoodenFishRunCheckpointInput) { + self.checkpoint_wooden_fish_run_then(input, |_, _| {}); + } + + fn checkpoint_wooden_fish_run_then( + &self, + input: WoodenFishRunCheckpointInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl checkpoint_wooden_fish_run for super::RemoteProcedures { + fn checkpoint_wooden_fish_run_then( + &self, + input: WoodenFishRunCheckpointInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, WoodenFishRunProcedureResult>( + "checkpoint_wooden_fish_run", + CheckpointWoodenFishRunArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/compile_wooden_fish_draft_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/compile_wooden_fish_draft_procedure.rs new file mode 100644 index 00000000..61c7b013 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/compile_wooden_fish_draft_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_agent_session_procedure_result_type::WoodenFishAgentSessionProcedureResult; +use super::wooden_fish_draft_compile_input_type::WoodenFishDraftCompileInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct CompileWoodenFishDraftArgs { + pub input: WoodenFishDraftCompileInput, +} + +impl __sdk::InModule for CompileWoodenFishDraftArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `compile_wooden_fish_draft`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait compile_wooden_fish_draft { + fn compile_wooden_fish_draft(&self, input: WoodenFishDraftCompileInput) { + self.compile_wooden_fish_draft_then(input, |_, _| {}); + } + + fn compile_wooden_fish_draft_then( + &self, + input: WoodenFishDraftCompileInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl compile_wooden_fish_draft for super::RemoteProcedures { + fn compile_wooden_fish_draft_then( + &self, + input: WoodenFishDraftCompileInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, WoodenFishAgentSessionProcedureResult>( + "compile_wooden_fish_draft", + CompileWoodenFishDraftArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/create_wooden_fish_agent_session_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/create_wooden_fish_agent_session_procedure.rs new file mode 100644 index 00000000..ec6bec0d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/create_wooden_fish_agent_session_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_agent_session_create_input_type::WoodenFishAgentSessionCreateInput; +use super::wooden_fish_agent_session_procedure_result_type::WoodenFishAgentSessionProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct CreateWoodenFishAgentSessionArgs { + pub input: WoodenFishAgentSessionCreateInput, +} + +impl __sdk::InModule for CreateWoodenFishAgentSessionArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `create_wooden_fish_agent_session`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait create_wooden_fish_agent_session { + fn create_wooden_fish_agent_session(&self, input: WoodenFishAgentSessionCreateInput) { + self.create_wooden_fish_agent_session_then(input, |_, _| {}); + } + + fn create_wooden_fish_agent_session_then( + &self, + input: WoodenFishAgentSessionCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl create_wooden_fish_agent_session for super::RemoteProcedures { + fn create_wooden_fish_agent_session_then( + &self, + input: WoodenFishAgentSessionCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, WoodenFishAgentSessionProcedureResult>( + "create_wooden_fish_agent_session", + CreateWoodenFishAgentSessionArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/finish_wooden_fish_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/finish_wooden_fish_run_procedure.rs new file mode 100644 index 00000000..30e452e1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/finish_wooden_fish_run_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_run_finish_input_type::WoodenFishRunFinishInput; +use super::wooden_fish_run_procedure_result_type::WoodenFishRunProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct FinishWoodenFishRunArgs { + pub input: WoodenFishRunFinishInput, +} + +impl __sdk::InModule for FinishWoodenFishRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `finish_wooden_fish_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait finish_wooden_fish_run { + fn finish_wooden_fish_run(&self, input: WoodenFishRunFinishInput) { + self.finish_wooden_fish_run_then(input, |_, _| {}); + } + + fn finish_wooden_fish_run_then( + &self, + input: WoodenFishRunFinishInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl finish_wooden_fish_run for super::RemoteProcedures { + fn finish_wooden_fish_run_then( + &self, + input: WoodenFishRunFinishInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, WoodenFishRunProcedureResult>( + "finish_wooden_fish_run", + FinishWoodenFishRunArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_wooden_fish_agent_session_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_wooden_fish_agent_session_procedure.rs new file mode 100644 index 00000000..e0b80d0e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_wooden_fish_agent_session_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_agent_session_get_input_type::WoodenFishAgentSessionGetInput; +use super::wooden_fish_agent_session_procedure_result_type::WoodenFishAgentSessionProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetWoodenFishAgentSessionArgs { + pub input: WoodenFishAgentSessionGetInput, +} + +impl __sdk::InModule for GetWoodenFishAgentSessionArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_wooden_fish_agent_session`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_wooden_fish_agent_session { + fn get_wooden_fish_agent_session(&self, input: WoodenFishAgentSessionGetInput) { + self.get_wooden_fish_agent_session_then(input, |_, _| {}); + } + + fn get_wooden_fish_agent_session_then( + &self, + input: WoodenFishAgentSessionGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_wooden_fish_agent_session for super::RemoteProcedures { + fn get_wooden_fish_agent_session_then( + &self, + input: WoodenFishAgentSessionGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, WoodenFishAgentSessionProcedureResult>( + "get_wooden_fish_agent_session", + GetWoodenFishAgentSessionArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_wooden_fish_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_wooden_fish_run_procedure.rs new file mode 100644 index 00000000..8a0e145a --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_wooden_fish_run_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_run_get_input_type::WoodenFishRunGetInput; +use super::wooden_fish_run_procedure_result_type::WoodenFishRunProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetWoodenFishRunArgs { + pub input: WoodenFishRunGetInput, +} + +impl __sdk::InModule for GetWoodenFishRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_wooden_fish_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_wooden_fish_run { + fn get_wooden_fish_run(&self, input: WoodenFishRunGetInput) { + self.get_wooden_fish_run_then(input, |_, _| {}); + } + + fn get_wooden_fish_run_then( + &self, + input: WoodenFishRunGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_wooden_fish_run for super::RemoteProcedures { + fn get_wooden_fish_run_then( + &self, + input: WoodenFishRunGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, WoodenFishRunProcedureResult>( + "get_wooden_fish_run", + GetWoodenFishRunArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_wooden_fish_work_profile_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_wooden_fish_work_profile_procedure.rs new file mode 100644 index 00000000..44f67362 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_wooden_fish_work_profile_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_work_get_input_type::WoodenFishWorkGetInput; +use super::wooden_fish_work_procedure_result_type::WoodenFishWorkProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetWoodenFishWorkProfileArgs { + pub input: WoodenFishWorkGetInput, +} + +impl __sdk::InModule for GetWoodenFishWorkProfileArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_wooden_fish_work_profile`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_wooden_fish_work_profile { + fn get_wooden_fish_work_profile(&self, input: WoodenFishWorkGetInput) { + self.get_wooden_fish_work_profile_then(input, |_, _| {}); + } + + fn get_wooden_fish_work_profile_then( + &self, + input: WoodenFishWorkGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_wooden_fish_work_profile for super::RemoteProcedures { + fn get_wooden_fish_work_profile_then( + &self, + input: WoodenFishWorkGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, WoodenFishWorkProcedureResult>( + "get_wooden_fish_work_profile", + GetWoodenFishWorkProfileArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/list_wooden_fish_works_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/list_wooden_fish_works_procedure.rs new file mode 100644 index 00000000..d449ff80 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/list_wooden_fish_works_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_works_list_input_type::WoodenFishWorksListInput; +use super::wooden_fish_works_procedure_result_type::WoodenFishWorksProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct ListWoodenFishWorksArgs { + pub input: WoodenFishWorksListInput, +} + +impl __sdk::InModule for ListWoodenFishWorksArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `list_wooden_fish_works`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait list_wooden_fish_works { + fn list_wooden_fish_works(&self, input: WoodenFishWorksListInput) { + self.list_wooden_fish_works_then(input, |_, _| {}); + } + + fn list_wooden_fish_works_then( + &self, + input: WoodenFishWorksListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl list_wooden_fish_works for super::RemoteProcedures { + fn list_wooden_fish_works_then( + &self, + input: WoodenFishWorksListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, WoodenFishWorksProcedureResult>( + "list_wooden_fish_works", + ListWoodenFishWorksArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/publish_wooden_fish_work_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/publish_wooden_fish_work_procedure.rs new file mode 100644 index 00000000..f370ace6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/publish_wooden_fish_work_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_work_procedure_result_type::WoodenFishWorkProcedureResult; +use super::wooden_fish_work_publish_input_type::WoodenFishWorkPublishInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct PublishWoodenFishWorkArgs { + pub input: WoodenFishWorkPublishInput, +} + +impl __sdk::InModule for PublishWoodenFishWorkArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `publish_wooden_fish_work`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait publish_wooden_fish_work { + fn publish_wooden_fish_work(&self, input: WoodenFishWorkPublishInput) { + self.publish_wooden_fish_work_then(input, |_, _| {}); + } + + fn publish_wooden_fish_work_then( + &self, + input: WoodenFishWorkPublishInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl publish_wooden_fish_work for super::RemoteProcedures { + fn publish_wooden_fish_work_then( + &self, + input: WoodenFishWorkPublishInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, WoodenFishWorkProcedureResult>( + "publish_wooden_fish_work", + PublishWoodenFishWorkArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/start_wooden_fish_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/start_wooden_fish_run_procedure.rs new file mode 100644 index 00000000..daf6358e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/start_wooden_fish_run_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_run_procedure_result_type::WoodenFishRunProcedureResult; +use super::wooden_fish_run_start_input_type::WoodenFishRunStartInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct StartWoodenFishRunArgs { + pub input: WoodenFishRunStartInput, +} + +impl __sdk::InModule for StartWoodenFishRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `start_wooden_fish_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait start_wooden_fish_run { + fn start_wooden_fish_run(&self, input: WoodenFishRunStartInput) { + self.start_wooden_fish_run_then(input, |_, _| {}); + } + + fn start_wooden_fish_run_then( + &self, + input: WoodenFishRunStartInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl start_wooden_fish_run for super::RemoteProcedures { + fn start_wooden_fish_run_then( + &self, + input: WoodenFishRunStartInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, WoodenFishRunProcedureResult>( + "start_wooden_fish_run", + StartWoodenFishRunArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/update_wooden_fish_work_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/update_wooden_fish_work_procedure.rs new file mode 100644 index 00000000..31785275 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/update_wooden_fish_work_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_work_procedure_result_type::WoodenFishWorkProcedureResult; +use super::wooden_fish_work_update_input_type::WoodenFishWorkUpdateInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct UpdateWoodenFishWorkArgs { + pub input: WoodenFishWorkUpdateInput, +} + +impl __sdk::InModule for UpdateWoodenFishWorkArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `update_wooden_fish_work`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait update_wooden_fish_work { + fn update_wooden_fish_work(&self, input: WoodenFishWorkUpdateInput) { + self.update_wooden_fish_work_then(input, |_, _| {}); + } + + fn update_wooden_fish_work_then( + &self, + input: WoodenFishWorkUpdateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl update_wooden_fish_work for super::RemoteProcedures { + fn update_wooden_fish_work_then( + &self, + input: WoodenFishWorkUpdateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, WoodenFishWorkProcedureResult>( + "update_wooden_fish_work", + UpdateWoodenFishWorkArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_create_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_create_input_type.rs new file mode 100644 index 00000000..1ad698a2 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_create_input_type.rs @@ -0,0 +1,22 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishAgentSessionCreateInput { + pub session_id: String, + pub owner_user_id: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: Option, + pub config_json: Option, + pub draft_json: Option, + pub created_at_micros: i64, +} + +impl __sdk::InModule for WoodenFishAgentSessionCreateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_get_input_type.rs new file mode 100644 index 00000000..62f4469b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_get_input_type.rs @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishAgentSessionGetInput { + pub session_id: String, + pub owner_user_id: String, +} + +impl __sdk::InModule for WoodenFishAgentSessionGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_procedure_result_type.rs new file mode 100644 index 00000000..e89fc3a6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_procedure_result_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_agent_session_snapshot_type::WoodenFishAgentSessionSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishAgentSessionProcedureResult { + pub ok: bool, + pub session: Option, + pub error_message: Option, +} + +impl __sdk::InModule for WoodenFishAgentSessionProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_row_type.rs new file mode 100644 index 00000000..8f918cdf --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_row_type.rs @@ -0,0 +1,81 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishAgentSessionRow { + pub session_id: String, + pub owner_user_id: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: String, + pub config_json: String, + pub draft_json: String, + pub published_profile_id: String, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for WoodenFishAgentSessionRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `WoodenFishAgentSessionRow`. +/// +/// Provides typed access to columns for query building. +pub struct WoodenFishAgentSessionRowCols { + pub session_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub current_turn: __sdk::__query_builder::Col, + pub progress_percent: __sdk::__query_builder::Col, + pub stage: __sdk::__query_builder::Col, + pub config_json: __sdk::__query_builder::Col, + pub draft_json: __sdk::__query_builder::Col, + pub published_profile_id: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for WoodenFishAgentSessionRow { + type Cols = WoodenFishAgentSessionRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + WoodenFishAgentSessionRowCols { + session_id: __sdk::__query_builder::Col::new(table_name, "session_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + current_turn: __sdk::__query_builder::Col::new(table_name, "current_turn"), + progress_percent: __sdk::__query_builder::Col::new(table_name, "progress_percent"), + stage: __sdk::__query_builder::Col::new(table_name, "stage"), + config_json: __sdk::__query_builder::Col::new(table_name, "config_json"), + draft_json: __sdk::__query_builder::Col::new(table_name, "draft_json"), + published_profile_id: __sdk::__query_builder::Col::new( + table_name, + "published_profile_id", + ), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `WoodenFishAgentSessionRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct WoodenFishAgentSessionRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub session_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for WoodenFishAgentSessionRow { + type IxCols = WoodenFishAgentSessionRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + WoodenFishAgentSessionRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + session_id: __sdk::__query_builder::IxCol::new(table_name, "session_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for WoodenFishAgentSessionRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_snapshot_type.rs new file mode 100644 index 00000000..f4526483 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_snapshot_type.rs @@ -0,0 +1,27 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_creator_config_snapshot_type::WoodenFishCreatorConfigSnapshot; +use super::wooden_fish_draft_snapshot_type::WoodenFishDraftSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishAgentSessionSnapshot { + pub session_id: String, + pub owner_user_id: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: String, + pub config: WoodenFishCreatorConfigSnapshot, + pub draft: Option, + pub published_profile_id: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for WoodenFishAgentSessionSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_table.rs new file mode 100644 index 00000000..68f2e484 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_agent_session_table.rs @@ -0,0 +1,165 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::wooden_fish_agent_session_row_type::WoodenFishAgentSessionRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `wooden_fish_agent_session`. +/// +/// Obtain a handle from the [`WoodenFishAgentSessionTableAccess::wooden_fish_agent_session`] method on [`super::RemoteTables`], +/// like `ctx.db.wooden_fish_agent_session()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.wooden_fish_agent_session().on_insert(...)`. +pub struct WoodenFishAgentSessionTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `wooden_fish_agent_session`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait WoodenFishAgentSessionTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`WoodenFishAgentSessionTableHandle`], which mediates access to the table `wooden_fish_agent_session`. + fn wooden_fish_agent_session(&self) -> WoodenFishAgentSessionTableHandle<'_>; +} + +impl WoodenFishAgentSessionTableAccess for super::RemoteTables { + fn wooden_fish_agent_session(&self) -> WoodenFishAgentSessionTableHandle<'_> { + WoodenFishAgentSessionTableHandle { + imp: self + .imp + .get_table::("wooden_fish_agent_session"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct WoodenFishAgentSessionInsertCallbackId(__sdk::CallbackId); +pub struct WoodenFishAgentSessionDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for WoodenFishAgentSessionTableHandle<'ctx> { + type Row = WoodenFishAgentSessionRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = WoodenFishAgentSessionInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> WoodenFishAgentSessionInsertCallbackId { + WoodenFishAgentSessionInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: WoodenFishAgentSessionInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = WoodenFishAgentSessionDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> WoodenFishAgentSessionDeleteCallbackId { + WoodenFishAgentSessionDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: WoodenFishAgentSessionDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct WoodenFishAgentSessionUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for WoodenFishAgentSessionTableHandle<'ctx> { + type UpdateCallbackId = WoodenFishAgentSessionUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> WoodenFishAgentSessionUpdateCallbackId { + WoodenFishAgentSessionUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: WoodenFishAgentSessionUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `session_id` unique index on the table `wooden_fish_agent_session`, +/// which allows point queries on the field of the same name +/// via the [`WoodenFishAgentSessionSessionIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.wooden_fish_agent_session().session_id().find(...)`. +pub struct WoodenFishAgentSessionSessionIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> WoodenFishAgentSessionTableHandle<'ctx> { + /// Get a handle on the `session_id` unique index on the table `wooden_fish_agent_session`. + pub fn session_id(&self) -> WoodenFishAgentSessionSessionIdUnique<'ctx> { + WoodenFishAgentSessionSessionIdUnique { + imp: self.imp.get_unique_constraint::("session_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> WoodenFishAgentSessionSessionIdUnique<'ctx> { + /// Find the subscribed row whose `session_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("wooden_fish_agent_session"); + _table.add_unique_constraint::("session_id", |row| &row.session_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `WoodenFishAgentSessionRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait wooden_fish_agent_sessionQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `WoodenFishAgentSessionRow`. + fn wooden_fish_agent_session(&self) + -> __sdk::__query_builder::Table; +} + +impl wooden_fish_agent_sessionQueryTableAccess for __sdk::QueryTableAccessor { + fn wooden_fish_agent_session( + &self, + ) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("wooden_fish_agent_session") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_audio_asset_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_audio_asset_snapshot_type.rs new file mode 100644 index 00000000..9fb39b2d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_audio_asset_snapshot_type.rs @@ -0,0 +1,21 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishAudioAssetSnapshot { + pub asset_id: String, + pub audio_src: String, + pub audio_object_key: String, + pub asset_object_id: String, + pub source: String, + pub prompt: Option, + pub duration_ms: Option, +} + +impl __sdk::InModule for WoodenFishAudioAssetSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_creator_config_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_creator_config_snapshot_type.rs new file mode 100644 index 00000000..e54b4b3a --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_creator_config_snapshot_type.rs @@ -0,0 +1,21 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishCreatorConfigSnapshot { + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub hit_object_prompt: String, + pub hit_object_reference_image_src: Option, + pub hit_sound_prompt: Option, + pub floating_words: Vec, +} + +impl __sdk::InModule for WoodenFishCreatorConfigSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_draft_compile_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_draft_compile_input_type.rs new file mode 100644 index 00000000..402f40ab --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_draft_compile_input_type.rs @@ -0,0 +1,31 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishDraftCompileInput { + pub session_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: Option, + pub hit_object_prompt: String, + pub hit_object_reference_image_src: Option, + pub hit_sound_prompt: Option, + pub hit_object_asset_json: Option, + pub background_asset_json: Option, + pub hit_sound_asset_json: Option, + pub floating_words_json: Option, + pub cover_image_src: Option, + pub generation_status: Option, + pub compiled_at_micros: i64, +} + +impl __sdk::InModule for WoodenFishDraftCompileInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_draft_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_draft_snapshot_type.rs new file mode 100644 index 00000000..17c4e2b9 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_draft_snapshot_type.rs @@ -0,0 +1,32 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_audio_asset_snapshot_type::WoodenFishAudioAssetSnapshot; +use super::wooden_fish_image_asset_snapshot_type::WoodenFishImageAssetSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishDraftSnapshot { + pub template_id: String, + pub template_name: String, + pub profile_id: Option, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub hit_object_prompt: String, + pub hit_object_reference_image_src: Option, + pub hit_sound_prompt: Option, + pub floating_words: Vec, + pub hit_object_asset: Option, + pub background_asset: Option, + pub hit_sound_asset: Option, + pub cover_image_src: Option, + pub generation_status: String, +} + +impl __sdk::InModule for WoodenFishDraftSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_event_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_event_row_type.rs new file mode 100644 index 00000000..9b000003 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_event_row_type.rs @@ -0,0 +1,71 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishEventRow { + pub event_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub run_id: String, + pub event_type: String, + pub result: String, + pub occurred_at: __sdk::Timestamp, +} + +impl __sdk::InModule for WoodenFishEventRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `WoodenFishEventRow`. +/// +/// Provides typed access to columns for query building. +pub struct WoodenFishEventRowCols { + pub event_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub run_id: __sdk::__query_builder::Col, + pub event_type: __sdk::__query_builder::Col, + pub result: __sdk::__query_builder::Col, + pub occurred_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for WoodenFishEventRow { + type Cols = WoodenFishEventRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + WoodenFishEventRowCols { + event_id: __sdk::__query_builder::Col::new(table_name, "event_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + run_id: __sdk::__query_builder::Col::new(table_name, "run_id"), + event_type: __sdk::__query_builder::Col::new(table_name, "event_type"), + result: __sdk::__query_builder::Col::new(table_name, "result"), + occurred_at: __sdk::__query_builder::Col::new(table_name, "occurred_at"), + } + } +} + +/// Indexed column accessor struct for the table `WoodenFishEventRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct WoodenFishEventRowIxCols { + pub event_id: __sdk::__query_builder::IxCol, + pub profile_id: __sdk::__query_builder::IxCol, + pub run_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for WoodenFishEventRow { + type IxCols = WoodenFishEventRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + WoodenFishEventRowIxCols { + event_id: __sdk::__query_builder::IxCol::new(table_name, "event_id"), + profile_id: __sdk::__query_builder::IxCol::new(table_name, "profile_id"), + run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for WoodenFishEventRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_event_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_event_table.rs new file mode 100644 index 00000000..ba0e979d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_event_table.rs @@ -0,0 +1,161 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::wooden_fish_event_row_type::WoodenFishEventRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `wooden_fish_event`. +/// +/// Obtain a handle from the [`WoodenFishEventTableAccess::wooden_fish_event`] method on [`super::RemoteTables`], +/// like `ctx.db.wooden_fish_event()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.wooden_fish_event().on_insert(...)`. +pub struct WoodenFishEventTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `wooden_fish_event`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait WoodenFishEventTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`WoodenFishEventTableHandle`], which mediates access to the table `wooden_fish_event`. + fn wooden_fish_event(&self) -> WoodenFishEventTableHandle<'_>; +} + +impl WoodenFishEventTableAccess for super::RemoteTables { + fn wooden_fish_event(&self) -> WoodenFishEventTableHandle<'_> { + WoodenFishEventTableHandle { + imp: self + .imp + .get_table::("wooden_fish_event"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct WoodenFishEventInsertCallbackId(__sdk::CallbackId); +pub struct WoodenFishEventDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for WoodenFishEventTableHandle<'ctx> { + type Row = WoodenFishEventRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = WoodenFishEventInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> WoodenFishEventInsertCallbackId { + WoodenFishEventInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: WoodenFishEventInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = WoodenFishEventDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> WoodenFishEventDeleteCallbackId { + WoodenFishEventDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: WoodenFishEventDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct WoodenFishEventUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for WoodenFishEventTableHandle<'ctx> { + type UpdateCallbackId = WoodenFishEventUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> WoodenFishEventUpdateCallbackId { + WoodenFishEventUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: WoodenFishEventUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `event_id` unique index on the table `wooden_fish_event`, +/// which allows point queries on the field of the same name +/// via the [`WoodenFishEventEventIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.wooden_fish_event().event_id().find(...)`. +pub struct WoodenFishEventEventIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> WoodenFishEventTableHandle<'ctx> { + /// Get a handle on the `event_id` unique index on the table `wooden_fish_event`. + pub fn event_id(&self) -> WoodenFishEventEventIdUnique<'ctx> { + WoodenFishEventEventIdUnique { + imp: self.imp.get_unique_constraint::("event_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> WoodenFishEventEventIdUnique<'ctx> { + /// Find the subscribed row whose `event_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("wooden_fish_event"); + _table.add_unique_constraint::("event_id", |row| &row.event_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `WoodenFishEventRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait wooden_fish_eventQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `WoodenFishEventRow`. + fn wooden_fish_event(&self) -> __sdk::__query_builder::Table; +} + +impl wooden_fish_eventQueryTableAccess for __sdk::QueryTableAccessor { + fn wooden_fish_event(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("wooden_fish_event") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_gallery_card_view_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_gallery_card_view_row_type.rs new file mode 100644 index 00000000..a11abdbb --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_gallery_card_view_row_type.rs @@ -0,0 +1,76 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishGalleryCardViewRow { + pub public_work_code: String, + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub cover_image_src: String, + pub theme_tags: Vec, + pub publication_status: String, + pub play_count: u32, + pub updated_at_micros: i64, + pub published_at_micros: Option, + pub generation_status: String, +} + +impl __sdk::InModule for WoodenFishGalleryCardViewRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `WoodenFishGalleryCardViewRow`. +/// +/// Provides typed access to columns for query building. +pub struct WoodenFishGalleryCardViewRowCols { + pub public_work_code: __sdk::__query_builder::Col, + pub work_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub author_display_name: __sdk::__query_builder::Col, + pub work_title: __sdk::__query_builder::Col, + pub work_description: __sdk::__query_builder::Col, + pub cover_image_src: __sdk::__query_builder::Col, + pub theme_tags: __sdk::__query_builder::Col>, + pub publication_status: __sdk::__query_builder::Col, + pub play_count: __sdk::__query_builder::Col, + pub updated_at_micros: __sdk::__query_builder::Col, + pub published_at_micros: __sdk::__query_builder::Col>, + pub generation_status: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for WoodenFishGalleryCardViewRow { + type Cols = WoodenFishGalleryCardViewRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + WoodenFishGalleryCardViewRowCols { + public_work_code: __sdk::__query_builder::Col::new(table_name, "public_work_code"), + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + author_display_name: __sdk::__query_builder::Col::new( + table_name, + "author_display_name", + ), + work_title: __sdk::__query_builder::Col::new(table_name, "work_title"), + work_description: __sdk::__query_builder::Col::new(table_name, "work_description"), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + theme_tags: __sdk::__query_builder::Col::new(table_name, "theme_tags"), + publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + updated_at_micros: __sdk::__query_builder::Col::new(table_name, "updated_at_micros"), + published_at_micros: __sdk::__query_builder::Col::new( + table_name, + "published_at_micros", + ), + generation_status: __sdk::__query_builder::Col::new(table_name, "generation_status"), + } + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_gallery_card_view_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_gallery_card_view_table.rs new file mode 100644 index 00000000..d64eab80 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_gallery_card_view_table.rs @@ -0,0 +1,121 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::wooden_fish_gallery_card_view_row_type::WoodenFishGalleryCardViewRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `wooden_fish_gallery_card_view`. +/// +/// Obtain a handle from the [`WoodenFishGalleryCardViewTableAccess::wooden_fish_gallery_card_view`] method on [`super::RemoteTables`], +/// like `ctx.db.wooden_fish_gallery_card_view()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.wooden_fish_gallery_card_view().on_insert(...)`. +pub struct WoodenFishGalleryCardViewTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `wooden_fish_gallery_card_view`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait WoodenFishGalleryCardViewTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`WoodenFishGalleryCardViewTableHandle`], which mediates access to the table `wooden_fish_gallery_card_view`. + fn wooden_fish_gallery_card_view(&self) -> WoodenFishGalleryCardViewTableHandle<'_>; +} + +impl WoodenFishGalleryCardViewTableAccess for super::RemoteTables { + fn wooden_fish_gallery_card_view(&self) -> WoodenFishGalleryCardViewTableHandle<'_> { + WoodenFishGalleryCardViewTableHandle { + imp: self + .imp + .get_table::("wooden_fish_gallery_card_view"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct WoodenFishGalleryCardViewInsertCallbackId(__sdk::CallbackId); +pub struct WoodenFishGalleryCardViewDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for WoodenFishGalleryCardViewTableHandle<'ctx> { + type Row = WoodenFishGalleryCardViewRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = WoodenFishGalleryCardViewInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> WoodenFishGalleryCardViewInsertCallbackId { + WoodenFishGalleryCardViewInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: WoodenFishGalleryCardViewInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = WoodenFishGalleryCardViewDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> WoodenFishGalleryCardViewDeleteCallbackId { + WoodenFishGalleryCardViewDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: WoodenFishGalleryCardViewDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache + .get_or_make_table::("wooden_fish_gallery_card_view"); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse( + "TableUpdate", + "TableUpdate", + ) + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `WoodenFishGalleryCardViewRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait wooden_fish_gallery_card_viewQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `WoodenFishGalleryCardViewRow`. + fn wooden_fish_gallery_card_view( + &self, + ) -> __sdk::__query_builder::Table; +} + +impl wooden_fish_gallery_card_viewQueryTableAccess for __sdk::QueryTableAccessor { + fn wooden_fish_gallery_card_view( + &self, + ) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("wooden_fish_gallery_card_view") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_gallery_view_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_gallery_view_row_type.rs new file mode 100644 index 00000000..ac17e2de --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_gallery_view_row_type.rs @@ -0,0 +1,113 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_audio_asset_snapshot_type::WoodenFishAudioAssetSnapshot; +use super::wooden_fish_image_asset_snapshot_type::WoodenFishImageAssetSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishGalleryViewRow { + pub public_work_code: String, + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub hit_object_prompt: String, + pub hit_object_reference_image_src: Option, + pub hit_sound_prompt: Option, + pub hit_object_asset: Option, + pub background_asset: Option, + pub hit_sound_asset: Option, + pub floating_words: Vec, + pub cover_image_src: String, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub generation_status: String, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} + +impl __sdk::InModule for WoodenFishGalleryViewRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `WoodenFishGalleryViewRow`. +/// +/// Provides typed access to columns for query building. +pub struct WoodenFishGalleryViewRowCols { + pub public_work_code: __sdk::__query_builder::Col, + pub work_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub source_session_id: __sdk::__query_builder::Col, + pub author_display_name: __sdk::__query_builder::Col, + pub work_title: __sdk::__query_builder::Col, + pub work_description: __sdk::__query_builder::Col, + pub theme_tags: __sdk::__query_builder::Col>, + pub hit_object_prompt: __sdk::__query_builder::Col, + pub hit_object_reference_image_src: + __sdk::__query_builder::Col>, + pub hit_sound_prompt: __sdk::__query_builder::Col>, + pub hit_object_asset: + __sdk::__query_builder::Col>, + pub background_asset: + __sdk::__query_builder::Col>, + pub hit_sound_asset: + __sdk::__query_builder::Col>, + pub floating_words: __sdk::__query_builder::Col>, + pub cover_image_src: __sdk::__query_builder::Col, + pub publication_status: __sdk::__query_builder::Col, + pub publish_ready: __sdk::__query_builder::Col, + pub play_count: __sdk::__query_builder::Col, + pub generation_status: __sdk::__query_builder::Col, + pub updated_at_micros: __sdk::__query_builder::Col, + pub published_at_micros: __sdk::__query_builder::Col>, +} + +impl __sdk::__query_builder::HasCols for WoodenFishGalleryViewRow { + type Cols = WoodenFishGalleryViewRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + WoodenFishGalleryViewRowCols { + public_work_code: __sdk::__query_builder::Col::new(table_name, "public_work_code"), + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + source_session_id: __sdk::__query_builder::Col::new(table_name, "source_session_id"), + author_display_name: __sdk::__query_builder::Col::new( + table_name, + "author_display_name", + ), + work_title: __sdk::__query_builder::Col::new(table_name, "work_title"), + work_description: __sdk::__query_builder::Col::new(table_name, "work_description"), + theme_tags: __sdk::__query_builder::Col::new(table_name, "theme_tags"), + hit_object_prompt: __sdk::__query_builder::Col::new(table_name, "hit_object_prompt"), + hit_object_reference_image_src: __sdk::__query_builder::Col::new( + table_name, + "hit_object_reference_image_src", + ), + hit_sound_prompt: __sdk::__query_builder::Col::new(table_name, "hit_sound_prompt"), + hit_object_asset: __sdk::__query_builder::Col::new(table_name, "hit_object_asset"), + background_asset: __sdk::__query_builder::Col::new(table_name, "background_asset"), + hit_sound_asset: __sdk::__query_builder::Col::new(table_name, "hit_sound_asset"), + floating_words: __sdk::__query_builder::Col::new(table_name, "floating_words"), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"), + publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + generation_status: __sdk::__query_builder::Col::new(table_name, "generation_status"), + updated_at_micros: __sdk::__query_builder::Col::new(table_name, "updated_at_micros"), + published_at_micros: __sdk::__query_builder::Col::new( + table_name, + "published_at_micros", + ), + } + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_gallery_view_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_gallery_view_table.rs new file mode 100644 index 00000000..e4a91629 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_gallery_view_table.rs @@ -0,0 +1,116 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::wooden_fish_audio_asset_snapshot_type::WoodenFishAudioAssetSnapshot; +use super::wooden_fish_gallery_view_row_type::WoodenFishGalleryViewRow; +use super::wooden_fish_image_asset_snapshot_type::WoodenFishImageAssetSnapshot; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `wooden_fish_gallery_view`. +/// +/// Obtain a handle from the [`WoodenFishGalleryViewTableAccess::wooden_fish_gallery_view`] method on [`super::RemoteTables`], +/// like `ctx.db.wooden_fish_gallery_view()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.wooden_fish_gallery_view().on_insert(...)`. +pub struct WoodenFishGalleryViewTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `wooden_fish_gallery_view`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait WoodenFishGalleryViewTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`WoodenFishGalleryViewTableHandle`], which mediates access to the table `wooden_fish_gallery_view`. + fn wooden_fish_gallery_view(&self) -> WoodenFishGalleryViewTableHandle<'_>; +} + +impl WoodenFishGalleryViewTableAccess for super::RemoteTables { + fn wooden_fish_gallery_view(&self) -> WoodenFishGalleryViewTableHandle<'_> { + WoodenFishGalleryViewTableHandle { + imp: self + .imp + .get_table::("wooden_fish_gallery_view"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct WoodenFishGalleryViewInsertCallbackId(__sdk::CallbackId); +pub struct WoodenFishGalleryViewDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for WoodenFishGalleryViewTableHandle<'ctx> { + type Row = WoodenFishGalleryViewRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = WoodenFishGalleryViewInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> WoodenFishGalleryViewInsertCallbackId { + WoodenFishGalleryViewInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: WoodenFishGalleryViewInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = WoodenFishGalleryViewDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> WoodenFishGalleryViewDeleteCallbackId { + WoodenFishGalleryViewDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: WoodenFishGalleryViewDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("wooden_fish_gallery_view"); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `WoodenFishGalleryViewRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait wooden_fish_gallery_viewQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `WoodenFishGalleryViewRow`. + fn wooden_fish_gallery_view(&self) -> __sdk::__query_builder::Table; +} + +impl wooden_fish_gallery_viewQueryTableAccess for __sdk::QueryTableAccessor { + fn wooden_fish_gallery_view(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("wooden_fish_gallery_view") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_image_asset_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_image_asset_snapshot_type.rs new file mode 100644 index 00000000..52f28559 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_image_asset_snapshot_type.rs @@ -0,0 +1,22 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishImageAssetSnapshot { + pub asset_id: String, + pub image_src: String, + pub image_object_key: String, + pub asset_object_id: String, + pub generation_provider: String, + pub prompt: String, + pub width: u32, + pub height: u32, +} + +impl __sdk::InModule for WoodenFishImageAssetSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_checkpoint_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_checkpoint_input_type.rs new file mode 100644 index 00000000..0a3f0a16 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_checkpoint_input_type.rs @@ -0,0 +1,20 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishRunCheckpointInput { + pub run_id: String, + pub owner_user_id: String, + pub total_tap_count: u32, + pub word_counters_json: String, + pub client_event_id: String, + pub checkpoint_at_ms: i64, +} + +impl __sdk::InModule for WoodenFishRunCheckpointInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_finish_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_finish_input_type.rs new file mode 100644 index 00000000..a968b5ce --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_finish_input_type.rs @@ -0,0 +1,20 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishRunFinishInput { + pub run_id: String, + pub owner_user_id: String, + pub total_tap_count: u32, + pub word_counters_json: String, + pub client_event_id: String, + pub finished_at_ms: i64, +} + +impl __sdk::InModule for WoodenFishRunFinishInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_get_input_type.rs new file mode 100644 index 00000000..0b20556e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_get_input_type.rs @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishRunGetInput { + pub run_id: String, + pub owner_user_id: String, +} + +impl __sdk::InModule for WoodenFishRunGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_procedure_result_type.rs new file mode 100644 index 00000000..21212b17 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_procedure_result_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_run_snapshot_type::WoodenFishRunSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishRunProcedureResult { + pub ok: bool, + pub run: Option, + pub error_message: Option, +} + +impl __sdk::InModule for WoodenFishRunProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_snapshot_type.rs new file mode 100644 index 00000000..dfd085ac --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_snapshot_type.rs @@ -0,0 +1,26 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_run_status_type::WoodenFishRunStatus; +use super::wooden_fish_word_counter_type::WoodenFishWordCounter; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishRunSnapshot { + pub run_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub status: WoodenFishRunStatus, + pub total_tap_count: u32, + pub word_counters: Vec, + pub started_at_ms: u64, + pub updated_at_ms: u64, + pub finished_at_ms: Option, +} + +impl __sdk::InModule for WoodenFishRunSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_start_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_start_input_type.rs new file mode 100644 index 00000000..c9f55321 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_start_input_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishRunStartInput { + pub run_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub client_event_id: String, + pub started_at_ms: i64, +} + +impl __sdk::InModule for WoodenFishRunStartInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_status_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_status_type.rs new file mode 100644 index 00000000..1b48ba39 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_run_status_type.rs @@ -0,0 +1,18 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +#[derive(Copy, Eq, Hash)] +pub enum WoodenFishRunStatus { + Playing, + + Finished, +} + +impl __sdk::InModule for WoodenFishRunStatus { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_runtime_run_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_runtime_run_row_type.rs new file mode 100644 index 00000000..8c12c1e5 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_runtime_run_row_type.rs @@ -0,0 +1,86 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishRuntimeRunRow { + pub run_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub status: String, + pub total_tap_count: u32, + pub word_counters_json: String, + pub started_at_ms: i64, + pub updated_at_ms: i64, + pub finished_at_ms: i64, + pub snapshot_json: String, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for WoodenFishRuntimeRunRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `WoodenFishRuntimeRunRow`. +/// +/// Provides typed access to columns for query building. +pub struct WoodenFishRuntimeRunRowCols { + pub run_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub status: __sdk::__query_builder::Col, + pub total_tap_count: __sdk::__query_builder::Col, + pub word_counters_json: __sdk::__query_builder::Col, + pub started_at_ms: __sdk::__query_builder::Col, + pub updated_at_ms: __sdk::__query_builder::Col, + pub finished_at_ms: __sdk::__query_builder::Col, + pub snapshot_json: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for WoodenFishRuntimeRunRow { + type Cols = WoodenFishRuntimeRunRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + WoodenFishRuntimeRunRowCols { + run_id: __sdk::__query_builder::Col::new(table_name, "run_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + status: __sdk::__query_builder::Col::new(table_name, "status"), + total_tap_count: __sdk::__query_builder::Col::new(table_name, "total_tap_count"), + word_counters_json: __sdk::__query_builder::Col::new(table_name, "word_counters_json"), + started_at_ms: __sdk::__query_builder::Col::new(table_name, "started_at_ms"), + updated_at_ms: __sdk::__query_builder::Col::new(table_name, "updated_at_ms"), + finished_at_ms: __sdk::__query_builder::Col::new(table_name, "finished_at_ms"), + snapshot_json: __sdk::__query_builder::Col::new(table_name, "snapshot_json"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `WoodenFishRuntimeRunRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct WoodenFishRuntimeRunRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub profile_id: __sdk::__query_builder::IxCol, + pub run_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for WoodenFishRuntimeRunRow { + type IxCols = WoodenFishRuntimeRunRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + WoodenFishRuntimeRunRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + profile_id: __sdk::__query_builder::IxCol::new(table_name, "profile_id"), + run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for WoodenFishRuntimeRunRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_runtime_run_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_runtime_run_table.rs new file mode 100644 index 00000000..f09d3b6e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_runtime_run_table.rs @@ -0,0 +1,162 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::wooden_fish_runtime_run_row_type::WoodenFishRuntimeRunRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `wooden_fish_runtime_run`. +/// +/// Obtain a handle from the [`WoodenFishRuntimeRunTableAccess::wooden_fish_runtime_run`] method on [`super::RemoteTables`], +/// like `ctx.db.wooden_fish_runtime_run()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.wooden_fish_runtime_run().on_insert(...)`. +pub struct WoodenFishRuntimeRunTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `wooden_fish_runtime_run`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait WoodenFishRuntimeRunTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`WoodenFishRuntimeRunTableHandle`], which mediates access to the table `wooden_fish_runtime_run`. + fn wooden_fish_runtime_run(&self) -> WoodenFishRuntimeRunTableHandle<'_>; +} + +impl WoodenFishRuntimeRunTableAccess for super::RemoteTables { + fn wooden_fish_runtime_run(&self) -> WoodenFishRuntimeRunTableHandle<'_> { + WoodenFishRuntimeRunTableHandle { + imp: self + .imp + .get_table::("wooden_fish_runtime_run"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct WoodenFishRuntimeRunInsertCallbackId(__sdk::CallbackId); +pub struct WoodenFishRuntimeRunDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for WoodenFishRuntimeRunTableHandle<'ctx> { + type Row = WoodenFishRuntimeRunRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = WoodenFishRuntimeRunInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> WoodenFishRuntimeRunInsertCallbackId { + WoodenFishRuntimeRunInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: WoodenFishRuntimeRunInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = WoodenFishRuntimeRunDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> WoodenFishRuntimeRunDeleteCallbackId { + WoodenFishRuntimeRunDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: WoodenFishRuntimeRunDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct WoodenFishRuntimeRunUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for WoodenFishRuntimeRunTableHandle<'ctx> { + type UpdateCallbackId = WoodenFishRuntimeRunUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> WoodenFishRuntimeRunUpdateCallbackId { + WoodenFishRuntimeRunUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: WoodenFishRuntimeRunUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `run_id` unique index on the table `wooden_fish_runtime_run`, +/// which allows point queries on the field of the same name +/// via the [`WoodenFishRuntimeRunRunIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.wooden_fish_runtime_run().run_id().find(...)`. +pub struct WoodenFishRuntimeRunRunIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> WoodenFishRuntimeRunTableHandle<'ctx> { + /// Get a handle on the `run_id` unique index on the table `wooden_fish_runtime_run`. + pub fn run_id(&self) -> WoodenFishRuntimeRunRunIdUnique<'ctx> { + WoodenFishRuntimeRunRunIdUnique { + imp: self.imp.get_unique_constraint::("run_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> WoodenFishRuntimeRunRunIdUnique<'ctx> { + /// Find the subscribed row whose `run_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("wooden_fish_runtime_run"); + _table.add_unique_constraint::("run_id", |row| &row.run_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `WoodenFishRuntimeRunRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait wooden_fish_runtime_runQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `WoodenFishRuntimeRunRow`. + fn wooden_fish_runtime_run(&self) -> __sdk::__query_builder::Table; +} + +impl wooden_fish_runtime_runQueryTableAccess for __sdk::QueryTableAccessor { + fn wooden_fish_runtime_run(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("wooden_fish_runtime_run") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_word_counter_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_word_counter_type.rs new file mode 100644 index 00000000..217899f1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_word_counter_type.rs @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishWordCounter { + pub text: String, + pub count: u32, +} + +impl __sdk::InModule for WoodenFishWordCounter { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_get_input_type.rs new file mode 100644 index 00000000..9867cb79 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_get_input_type.rs @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishWorkGetInput { + pub profile_id: String, + pub owner_user_id: String, +} + +impl __sdk::InModule for WoodenFishWorkGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_procedure_result_type.rs new file mode 100644 index 00000000..bebf287c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_procedure_result_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_work_snapshot_type::WoodenFishWorkSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishWorkProcedureResult { + pub ok: bool, + pub work: Option, + pub error_message: Option, +} + +impl __sdk::InModule for WoodenFishWorkProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_profile_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_profile_row_type.rs new file mode 100644 index 00000000..c82a9c6c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_profile_row_type.rs @@ -0,0 +1,137 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishWorkProfileRow { + pub profile_id: String, + pub work_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: String, + pub hit_object_prompt: String, + pub hit_object_reference_image_src: String, + pub hit_sound_prompt: String, + pub hit_object_asset_json: String, + pub hit_sound_asset_json: String, + pub floating_words_json: String, + pub cover_image_src: String, + pub generation_status: String, + pub publication_status: String, + pub play_count: u32, + pub updated_at: __sdk::Timestamp, + pub published_at: Option<__sdk::Timestamp>, + pub background_asset_json: Option, +} + +impl __sdk::InModule for WoodenFishWorkProfileRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `WoodenFishWorkProfileRow`. +/// +/// Provides typed access to columns for query building. +pub struct WoodenFishWorkProfileRowCols { + pub profile_id: __sdk::__query_builder::Col, + pub work_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub source_session_id: __sdk::__query_builder::Col, + pub author_display_name: __sdk::__query_builder::Col, + pub work_title: __sdk::__query_builder::Col, + pub work_description: __sdk::__query_builder::Col, + pub theme_tags_json: __sdk::__query_builder::Col, + pub hit_object_prompt: __sdk::__query_builder::Col, + pub hit_object_reference_image_src: + __sdk::__query_builder::Col, + pub hit_sound_prompt: __sdk::__query_builder::Col, + pub hit_object_asset_json: __sdk::__query_builder::Col, + pub hit_sound_asset_json: __sdk::__query_builder::Col, + pub floating_words_json: __sdk::__query_builder::Col, + pub cover_image_src: __sdk::__query_builder::Col, + pub generation_status: __sdk::__query_builder::Col, + pub publication_status: __sdk::__query_builder::Col, + pub play_count: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, + pub published_at: + __sdk::__query_builder::Col>, + pub background_asset_json: + __sdk::__query_builder::Col>, +} + +impl __sdk::__query_builder::HasCols for WoodenFishWorkProfileRow { + type Cols = WoodenFishWorkProfileRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + WoodenFishWorkProfileRowCols { + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + source_session_id: __sdk::__query_builder::Col::new(table_name, "source_session_id"), + author_display_name: __sdk::__query_builder::Col::new( + table_name, + "author_display_name", + ), + work_title: __sdk::__query_builder::Col::new(table_name, "work_title"), + work_description: __sdk::__query_builder::Col::new(table_name, "work_description"), + theme_tags_json: __sdk::__query_builder::Col::new(table_name, "theme_tags_json"), + hit_object_prompt: __sdk::__query_builder::Col::new(table_name, "hit_object_prompt"), + hit_object_reference_image_src: __sdk::__query_builder::Col::new( + table_name, + "hit_object_reference_image_src", + ), + hit_sound_prompt: __sdk::__query_builder::Col::new(table_name, "hit_sound_prompt"), + hit_object_asset_json: __sdk::__query_builder::Col::new( + table_name, + "hit_object_asset_json", + ), + hit_sound_asset_json: __sdk::__query_builder::Col::new( + table_name, + "hit_sound_asset_json", + ), + floating_words_json: __sdk::__query_builder::Col::new( + table_name, + "floating_words_json", + ), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + generation_status: __sdk::__query_builder::Col::new(table_name, "generation_status"), + publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + published_at: __sdk::__query_builder::Col::new(table_name, "published_at"), + background_asset_json: __sdk::__query_builder::Col::new( + table_name, + "background_asset_json", + ), + } + } +} + +/// Indexed column accessor struct for the table `WoodenFishWorkProfileRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct WoodenFishWorkProfileRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub profile_id: __sdk::__query_builder::IxCol, + pub publication_status: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for WoodenFishWorkProfileRow { + type IxCols = WoodenFishWorkProfileRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + WoodenFishWorkProfileRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + profile_id: __sdk::__query_builder::IxCol::new(table_name, "profile_id"), + publication_status: __sdk::__query_builder::IxCol::new( + table_name, + "publication_status", + ), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for WoodenFishWorkProfileRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_profile_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_profile_table.rs new file mode 100644 index 00000000..6815f169 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_profile_table.rs @@ -0,0 +1,162 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::wooden_fish_work_profile_row_type::WoodenFishWorkProfileRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `wooden_fish_work_profile`. +/// +/// Obtain a handle from the [`WoodenFishWorkProfileTableAccess::wooden_fish_work_profile`] method on [`super::RemoteTables`], +/// like `ctx.db.wooden_fish_work_profile()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.wooden_fish_work_profile().on_insert(...)`. +pub struct WoodenFishWorkProfileTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `wooden_fish_work_profile`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait WoodenFishWorkProfileTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`WoodenFishWorkProfileTableHandle`], which mediates access to the table `wooden_fish_work_profile`. + fn wooden_fish_work_profile(&self) -> WoodenFishWorkProfileTableHandle<'_>; +} + +impl WoodenFishWorkProfileTableAccess for super::RemoteTables { + fn wooden_fish_work_profile(&self) -> WoodenFishWorkProfileTableHandle<'_> { + WoodenFishWorkProfileTableHandle { + imp: self + .imp + .get_table::("wooden_fish_work_profile"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct WoodenFishWorkProfileInsertCallbackId(__sdk::CallbackId); +pub struct WoodenFishWorkProfileDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for WoodenFishWorkProfileTableHandle<'ctx> { + type Row = WoodenFishWorkProfileRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = WoodenFishWorkProfileInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> WoodenFishWorkProfileInsertCallbackId { + WoodenFishWorkProfileInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: WoodenFishWorkProfileInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = WoodenFishWorkProfileDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> WoodenFishWorkProfileDeleteCallbackId { + WoodenFishWorkProfileDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: WoodenFishWorkProfileDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct WoodenFishWorkProfileUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for WoodenFishWorkProfileTableHandle<'ctx> { + type UpdateCallbackId = WoodenFishWorkProfileUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> WoodenFishWorkProfileUpdateCallbackId { + WoodenFishWorkProfileUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: WoodenFishWorkProfileUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `profile_id` unique index on the table `wooden_fish_work_profile`, +/// which allows point queries on the field of the same name +/// via the [`WoodenFishWorkProfileProfileIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.wooden_fish_work_profile().profile_id().find(...)`. +pub struct WoodenFishWorkProfileProfileIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> WoodenFishWorkProfileTableHandle<'ctx> { + /// Get a handle on the `profile_id` unique index on the table `wooden_fish_work_profile`. + pub fn profile_id(&self) -> WoodenFishWorkProfileProfileIdUnique<'ctx> { + WoodenFishWorkProfileProfileIdUnique { + imp: self.imp.get_unique_constraint::("profile_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> WoodenFishWorkProfileProfileIdUnique<'ctx> { + /// Find the subscribed row whose `profile_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("wooden_fish_work_profile"); + _table.add_unique_constraint::("profile_id", |row| &row.profile_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `WoodenFishWorkProfileRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait wooden_fish_work_profileQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `WoodenFishWorkProfileRow`. + fn wooden_fish_work_profile(&self) -> __sdk::__query_builder::Table; +} + +impl wooden_fish_work_profileQueryTableAccess for __sdk::QueryTableAccessor { + fn wooden_fish_work_profile(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("wooden_fish_work_profile") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_publish_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_publish_input_type.rs new file mode 100644 index 00000000..f9920f38 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_publish_input_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishWorkPublishInput { + pub profile_id: String, + pub owner_user_id: String, + pub published_at_micros: i64, +} + +impl __sdk::InModule for WoodenFishWorkPublishInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_snapshot_type.rs new file mode 100644 index 00000000..fdaf3116 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_snapshot_type.rs @@ -0,0 +1,39 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_audio_asset_snapshot_type::WoodenFishAudioAssetSnapshot; +use super::wooden_fish_image_asset_snapshot_type::WoodenFishImageAssetSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishWorkSnapshot { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub hit_object_prompt: String, + pub hit_object_reference_image_src: Option, + pub hit_sound_prompt: Option, + pub hit_object_asset: Option, + pub background_asset: Option, + pub hit_sound_asset: Option, + pub floating_words: Vec, + pub cover_image_src: String, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub generation_status: String, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} + +impl __sdk::InModule for WoodenFishWorkSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_update_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_update_input_type.rs new file mode 100644 index 00000000..cd7c3547 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_work_update_input_type.rs @@ -0,0 +1,29 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishWorkUpdateInput { + pub profile_id: String, + pub owner_user_id: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: String, + pub hit_object_prompt: Option, + pub hit_object_reference_image_src: Option, + pub hit_sound_prompt: Option, + pub hit_object_asset_json: Option, + pub background_asset_json: Option, + pub hit_sound_asset_json: Option, + pub floating_words_json: Option, + pub cover_image_src: Option, + pub generation_status: Option, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for WoodenFishWorkUpdateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_works_list_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_works_list_input_type.rs new file mode 100644 index 00000000..0571854e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_works_list_input_type.rs @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishWorksListInput { + pub owner_user_id: String, + pub published_only: bool, +} + +impl __sdk::InModule for WoodenFishWorksListInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_works_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_works_procedure_result_type.rs new file mode 100644 index 00000000..bb2a8498 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/wooden_fish_works_procedure_result_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::wooden_fish_work_snapshot_type::WoodenFishWorkSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct WoodenFishWorksProcedureResult { + pub ok: bool, + pub items: Vec, + pub error_message: Option, +} + +impl __sdk::InModule for WoodenFishWorksProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/wooden_fish.rs b/server-rs/crates/spacetime-client/src/wooden_fish.rs new file mode 100644 index 00000000..edcfd312 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/wooden_fish.rs @@ -0,0 +1,1105 @@ +use super::*; +use crate::mapper::{ + map_wooden_fish_agent_session_procedure_result, map_wooden_fish_gallery_card_view_row, + map_wooden_fish_run_procedure_result, map_wooden_fish_work_procedure_result, + map_wooden_fish_works_procedure_result, +}; +use shared_contracts::wooden_fish::{ + WoodenFishActionRequest, WoodenFishActionResponse, WoodenFishActionType, + WoodenFishCheckpointRunRequest, WoodenFishDraftResponse, WoodenFishFinishRunRequest, + WoodenFishGalleryResponse, WoodenFishGenerationStatus, WoodenFishRuntimeRunSnapshotResponse, + WoodenFishSessionSnapshotResponse, WoodenFishStartRunRequest, WoodenFishWorkProfileResponse, +}; +use shared_kernel::build_prefixed_uuid_id; + +const WOODEN_FISH_TEMPLATE_ID: &str = "wooden-fish"; +const WOODEN_FISH_TEMPLATE_NAME: &str = "敲木鱼"; +const DEFAULT_HIT_OBJECT_PROMPT: &str = "默认敲击物图案,圆润木质质感,透明背景"; +const DEFAULT_HIT_SOUND_PROMPT: &str = "清脆短促的木鱼敲击声"; + +impl SpacetimeClient { + pub async fn create_wooden_fish_session( + &self, + session: WoodenFishSessionSnapshotResponse, + ) -> Result { + let draft = session.draft.clone().ok_or_else(|| { + SpacetimeClientError::validation_failed("wooden-fish session 缺少 draft") + })?; + let theme_tags_json = Some(json_string(&draft.theme_tags)?); + let config_json = Some(build_config_json(&draft)?); + let draft_json = Some(json_string(&draft)?); + let procedure_input = WoodenFishAgentSessionCreateInput { + session_id: session.session_id, + owner_user_id: session.owner_user_id, + work_title: draft.work_title, + work_description: draft.work_description, + theme_tags_json, + config_json, + draft_json, + created_at_micros: current_unix_micros(), + }; + + self.call_after_connect( + "create_wooden_fish_agent_session", + move |connection, sender| { + connection + .procedures() + .create_wooden_fish_agent_session_then(procedure_input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_wooden_fish_agent_session_procedure_result); + send_once(&sender, mapped); + }); + }, + ) + .await + } + + pub async fn get_wooden_fish_session( + &self, + session_id: String, + owner_user_id: String, + ) -> Result { + let procedure_input = WoodenFishAgentSessionGetInput { + session_id, + owner_user_id, + }; + + self.call_after_connect( + "get_wooden_fish_agent_session", + move |connection, sender| { + connection.procedures().get_wooden_fish_agent_session_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_wooden_fish_agent_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn execute_wooden_fish_action( + &self, + session_id: String, + owner_user_id: String, + payload: WoodenFishActionRequest, + ) -> Result { + let current = self + .get_wooden_fish_session(session_id.clone(), owner_user_id.clone()) + .await?; + let (procedure, _) = build_wooden_fish_action_plan( + ¤t, + &owner_user_id, + &payload, + current_unix_micros(), + )?; + let (session, work) = match procedure { + WoodenFishActionProcedure::Compile(input) => { + let profile_id = input.profile_id.clone(); + let session = self.compile_wooden_fish_draft(input).await?; + let work = self + .get_wooden_fish_work_profile(profile_id, owner_user_id) + .await + .ok(); + (session, work) + } + WoodenFishActionProcedure::Update(input) => { + let work = self.update_wooden_fish_work(input).await?; + let session = apply_wooden_fish_work_to_session(current, &work); + (session, Some(work)) + } + }; + + Ok(WoodenFishActionResponse { + action_type: payload.action_type, + session, + work, + }) + } + + pub async fn compile_wooden_fish_draft( + &self, + procedure_input: WoodenFishDraftCompileInput, + ) -> Result { + self.call_after_connect("compile_wooden_fish_draft", move |connection, sender| { + connection.procedures().compile_wooden_fish_draft_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_wooden_fish_agent_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn get_wooden_fish_work_profile( + &self, + profile_id: String, + owner_user_id: String, + ) -> Result { + let procedure_input = WoodenFishWorkGetInput { + profile_id, + owner_user_id, + }; + + self.call_after_connect("get_wooden_fish_work_profile", move |connection, sender| { + connection.procedures().get_wooden_fish_work_profile_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_wooden_fish_work_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn update_wooden_fish_work( + &self, + procedure_input: WoodenFishWorkUpdateInput, + ) -> Result { + self.call_after_connect("update_wooden_fish_work", move |connection, sender| { + connection.procedures().update_wooden_fish_work_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_wooden_fish_work_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn publish_wooden_fish_work( + &self, + profile_id: String, + owner_user_id: String, + ) -> Result { + let procedure_input = WoodenFishWorkPublishInput { + profile_id, + owner_user_id, + published_at_micros: current_unix_micros(), + }; + + self.call_after_connect("publish_wooden_fish_work", move |connection, sender| { + connection.procedures().publish_wooden_fish_work_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_wooden_fish_work_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn list_wooden_fish_works( + &self, + owner_user_id: String, + ) -> Result, SpacetimeClientError> { + let procedure_input = WoodenFishWorksListInput { + owner_user_id, + published_only: false, + }; + + self.call_after_connect("list_wooden_fish_works", move |connection, sender| { + connection.procedures().list_wooden_fish_works_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_wooden_fish_works_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn get_wooden_fish_runtime_work( + &self, + profile_id: String, + ) -> Result { + self.get_wooden_fish_work_profile(profile_id, String::new()) + .await + } + + pub async fn start_wooden_fish_run( + &self, + payload: WoodenFishStartRunRequest, + owner_user_id: String, + ) -> Result { + let run_id = build_prefixed_uuid_id("wooden-fish-run-"); + let procedure_input = WoodenFishRunStartInput { + client_event_id: format!("{run_id}:start"), + run_id, + owner_user_id, + profile_id: payload.profile_id, + started_at_ms: current_unix_micros().div_euclid(1000), + }; + self.start_wooden_fish_run_with_input(procedure_input).await + } + + pub async fn start_wooden_fish_run_with_input( + &self, + procedure_input: WoodenFishRunStartInput, + ) -> Result { + self.call_after_connect("start_wooden_fish_run", move |connection, sender| { + connection.procedures().start_wooden_fish_run_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_wooden_fish_run_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn checkpoint_wooden_fish_run( + &self, + run_id: String, + owner_user_id: String, + payload: WoodenFishCheckpointRunRequest, + ) -> Result { + let procedure_input = WoodenFishRunCheckpointInput { + run_id, + owner_user_id, + total_tap_count: payload.total_tap_count, + word_counters_json: json_string(&payload.word_counters)?, + client_event_id: payload.client_event_id, + checkpoint_at_ms: current_unix_micros().div_euclid(1000), + }; + + self.call_after_connect("checkpoint_wooden_fish_run", move |connection, sender| { + connection.procedures().checkpoint_wooden_fish_run_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_wooden_fish_run_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn finish_wooden_fish_run( + &self, + run_id: String, + owner_user_id: String, + payload: WoodenFishFinishRunRequest, + ) -> Result { + let procedure_input = WoodenFishRunFinishInput { + run_id, + owner_user_id, + total_tap_count: payload.total_tap_count, + word_counters_json: json_string(&payload.word_counters)?, + client_event_id: payload.client_event_id, + finished_at_ms: current_unix_micros().div_euclid(1000), + }; + + self.call_after_connect("finish_wooden_fish_run", move |connection, sender| { + connection.procedures().finish_wooden_fish_run_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_wooden_fish_run_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn list_wooden_fish_gallery( + &self, + ) -> Result { + self.read_after_connect("list_wooden_fish_gallery", move |connection| { + let mut items = connection + .db() + .wooden_fish_gallery_card_view() + .iter() + .collect::>(); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); + + Ok(WoodenFishGalleryResponse { + items: items + .into_iter() + .map(map_wooden_fish_gallery_card_view_row) + .collect(), + has_more: false, + next_cursor: None, + }) + }) + .await + } + + pub async fn get_wooden_fish_gallery_detail( + &self, + public_work_code: String, + ) -> Result { + let normalized_code = public_work_code.trim().to_string(); + if normalized_code.is_empty() { + return Err(SpacetimeClientError::validation_failed( + "wooden-fish public_work_code 不能为空", + )); + } + + let profile_id = self + .read_after_connect("resolve_wooden_fish_gallery_detail", move |connection| { + connection + .db() + .wooden_fish_gallery_card_view() + .iter() + .find(|row| { + row.public_work_code.eq_ignore_ascii_case(&normalized_code) + || row.profile_id == normalized_code + }) + .map(|row| row.profile_id) + .ok_or_else(|| { + SpacetimeClientError::procedure_failed(Some( + "敲木鱼公开作品不存在".to_string(), + )) + }) + }) + .await?; + + self.get_wooden_fish_work_profile(profile_id, String::new()) + .await + } +} + +enum WoodenFishActionProcedure { + Compile(WoodenFishDraftCompileInput), + Update(WoodenFishWorkUpdateInput), +} + +#[derive(Clone, Copy)] +enum WoodenFishDraftMergeScope { + CompileDraft, + RegenerateHitObject, + GenerateHitSound, + ReplaceHitSound, + UpdateWorkMeta, + UpdateFloatingWords, +} + +#[derive(Clone, Copy)] +enum WoodenFishAssetRefresh { + Preserve, + HitObject, + HitSound, +} + +fn build_wooden_fish_action_plan( + current: &WoodenFishSessionSnapshotResponse, + owner_user_id: &str, + payload: &WoodenFishActionRequest, + now_micros: i64, +) -> Result<(WoodenFishActionProcedure, WoodenFishDraftResponse), SpacetimeClientError> { + let scope = match payload.action_type { + WoodenFishActionType::CompileDraft => WoodenFishDraftMergeScope::CompileDraft, + WoodenFishActionType::RegenerateHitObject => WoodenFishDraftMergeScope::RegenerateHitObject, + WoodenFishActionType::GenerateHitSound => WoodenFishDraftMergeScope::GenerateHitSound, + WoodenFishActionType::ReplaceHitSound => WoodenFishDraftMergeScope::ReplaceHitSound, + WoodenFishActionType::UpdateWorkMeta => WoodenFishDraftMergeScope::UpdateWorkMeta, + WoodenFishActionType::UpdateFloatingWords => WoodenFishDraftMergeScope::UpdateFloatingWords, + }; + let mut draft = merge_action_into_draft(current.draft.clone(), payload, scope)?; + let profile_id = resolve_wooden_fish_profile_id( + &draft, + &payload.action_type, + payload.profile_id.as_deref(), + )?; + draft.profile_id = Some(profile_id.clone()); + + let procedure = match payload.action_type { + WoodenFishActionType::CompileDraft => { + WoodenFishActionProcedure::Compile(build_compile_input( + current, + owner_user_id, + &profile_id, + &mut draft, + WoodenFishAssetRefresh::Preserve, + now_micros, + )?) + } + WoodenFishActionType::RegenerateHitObject => { + WoodenFishActionProcedure::Compile(build_compile_input( + current, + owner_user_id, + &profile_id, + &mut draft, + WoodenFishAssetRefresh::HitObject, + now_micros, + )?) + } + WoodenFishActionType::GenerateHitSound => { + WoodenFishActionProcedure::Compile(build_compile_input( + current, + owner_user_id, + &profile_id, + &mut draft, + WoodenFishAssetRefresh::HitSound, + now_micros, + )?) + } + WoodenFishActionType::ReplaceHitSound => WoodenFishActionProcedure::Update( + build_update_input(owner_user_id, &profile_id, &draft, true, now_micros)?, + ), + WoodenFishActionType::UpdateWorkMeta | WoodenFishActionType::UpdateFloatingWords => { + WoodenFishActionProcedure::Update(build_update_input( + owner_user_id, + &profile_id, + &draft, + false, + now_micros, + )?) + } + }; + + Ok((procedure, draft)) +} + +fn merge_action_into_draft( + draft: Option, + payload: &WoodenFishActionRequest, + scope: WoodenFishDraftMergeScope, +) -> Result { + let mut draft = draft.unwrap_or_else(default_draft); + if matches!( + scope, + WoodenFishDraftMergeScope::CompileDraft | WoodenFishDraftMergeScope::UpdateWorkMeta + ) { + if let Some(value) = payload + .work_title + .as_ref() + .filter(|value| !value.trim().is_empty()) + { + draft.work_title = value.trim().to_string(); + } + if let Some(value) = payload.work_description.as_ref() { + draft.work_description = value.trim().to_string(); + } + if let Some(value) = payload.theme_tags.clone() { + draft.theme_tags = normalize_tags(value); + } + } + if matches!( + scope, + WoodenFishDraftMergeScope::CompileDraft | WoodenFishDraftMergeScope::RegenerateHitObject + ) { + if let Some(value) = payload + .hit_object_prompt + .as_ref() + .filter(|value| !value.trim().is_empty()) + { + draft.hit_object_prompt = value.trim().to_string(); + } + if payload.hit_object_reference_image_src.is_some() { + draft.hit_object_reference_image_src = payload + .hit_object_reference_image_src + .as_ref() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()); + } + if let Some(asset) = payload.hit_object_asset.clone() { + draft.hit_object_asset = Some(asset); + } + if let Some(asset) = payload.background_asset.clone() { + draft.background_asset = Some(asset); + } + } + if matches!( + scope, + WoodenFishDraftMergeScope::CompileDraft + | WoodenFishDraftMergeScope::GenerateHitSound + | WoodenFishDraftMergeScope::ReplaceHitSound + ) { + if let Some(value) = payload + .hit_sound_prompt + .as_ref() + .filter(|value| !value.trim().is_empty()) + { + draft.hit_sound_prompt = Some(value.trim().to_string()); + } + } + if matches!(scope, WoodenFishDraftMergeScope::GenerateHitSound) { + draft.hit_sound_asset = payload.hit_sound_asset.clone(); + } else if matches!( + scope, + WoodenFishDraftMergeScope::CompileDraft | WoodenFishDraftMergeScope::ReplaceHitSound + ) && let Some(asset) = payload.hit_sound_asset.clone() + { + draft.hit_sound_asset = Some(asset); + } + if matches!( + scope, + WoodenFishDraftMergeScope::CompileDraft | WoodenFishDraftMergeScope::UpdateFloatingWords + ) && let Some(words) = payload.floating_words.clone() + { + draft.floating_words = normalize_floating_words(words); + } + if draft.work_title.trim().is_empty() { + return Err(SpacetimeClientError::validation_failed( + "wooden-fish work_title 不能为空", + )); + } + if draft.hit_object_prompt.trim().is_empty() { + draft.hit_object_prompt = DEFAULT_HIT_OBJECT_PROMPT.to_string(); + } + if draft.hit_object_asset.is_some() + && matches!(scope, WoodenFishDraftMergeScope::RegenerateHitObject) + && payload.hit_object_asset.is_none() + { + draft.hit_object_asset = None; + draft.background_asset = None; + } + if draft.floating_words.is_empty() { + draft.floating_words = default_floating_words(); + } + Ok(draft) +} + +fn build_compile_input( + current: &WoodenFishSessionSnapshotResponse, + owner_user_id: &str, + profile_id: &str, + draft: &mut WoodenFishDraftResponse, + refresh: WoodenFishAssetRefresh, + now_micros: i64, +) -> Result { + let _refresh = refresh; + let hit_object_asset = draft.hit_object_asset.clone().ok_or_else(|| { + SpacetimeClientError::validation_failed("wooden fish hit object asset 缺少真实生成资产") + })?; + draft.hit_object_asset = Some(hit_object_asset); + draft.cover_image_src = draft + .hit_object_asset + .as_ref() + .map(|asset| asset.image_src.clone()); + draft.generation_status = WoodenFishGenerationStatus::Ready; + + let hit_object_asset = draft + .hit_object_asset + .clone() + .ok_or_else(|| SpacetimeClientError::missing_snapshot("wooden fish hit object asset"))?; + let hit_sound_asset = draft.hit_sound_asset.clone().ok_or_else(|| { + SpacetimeClientError::validation_failed("wooden fish hit sound asset 缺少真实生成资产") + })?; + let background_asset = draft.background_asset.clone().ok_or_else(|| { + SpacetimeClientError::validation_failed("wooden fish background asset 缺少真实生成资产") + })?; + + Ok(WoodenFishDraftCompileInput { + session_id: current.session_id.clone(), + owner_user_id: owner_user_id.to_string(), + profile_id: profile_id.to_string(), + author_display_name: "敲木鱼玩家".to_string(), + work_title: draft.work_title.clone(), + work_description: draft.work_description.clone(), + theme_tags_json: Some(json_string(&draft.theme_tags)?), + hit_object_prompt: draft.hit_object_prompt.clone(), + hit_object_reference_image_src: draft.hit_object_reference_image_src.clone(), + hit_sound_prompt: draft.hit_sound_prompt.clone(), + hit_object_asset_json: Some(json_string(&hit_object_asset)?), + background_asset_json: Some(json_string(&background_asset)?), + hit_sound_asset_json: Some(json_string(&hit_sound_asset)?), + floating_words_json: Some(json_string(&draft.floating_words)?), + cover_image_src: draft.cover_image_src.clone(), + generation_status: Some("ready".to_string()), + compiled_at_micros: now_micros, + }) +} + +fn build_update_input( + owner_user_id: &str, + profile_id: &str, + draft: &WoodenFishDraftResponse, + include_hit_sound_asset: bool, + now_micros: i64, +) -> Result { + Ok(WoodenFishWorkUpdateInput { + profile_id: profile_id.to_string(), + owner_user_id: owner_user_id.to_string(), + work_title: draft.work_title.clone(), + work_description: draft.work_description.clone(), + theme_tags_json: json_string(&draft.theme_tags)?, + hit_object_prompt: Some(draft.hit_object_prompt.clone()), + hit_object_reference_image_src: draft.hit_object_reference_image_src.clone(), + hit_sound_prompt: draft.hit_sound_prompt.clone(), + hit_object_asset_json: None, + background_asset_json: None, + hit_sound_asset_json: if include_hit_sound_asset { + draft + .hit_sound_asset + .as_ref() + .map(json_string) + .transpose()? + } else { + None + }, + floating_words_json: Some(json_string(&draft.floating_words)?), + cover_image_src: draft.cover_image_src.clone(), + generation_status: None, + updated_at_micros: now_micros, + }) +} + +fn resolve_wooden_fish_profile_id( + draft: &WoodenFishDraftResponse, + action_type: &WoodenFishActionType, + requested_profile_id: Option<&str>, +) -> Result { + if let Some(profile_id) = requested_profile_id + .map(str::trim) + .filter(|value| !value.is_empty()) + { + return Ok(profile_id.to_string()); + } + if let Some(profile_id) = draft + .profile_id + .as_ref() + .map(|value| value.trim()) + .filter(|value| !value.is_empty()) + { + return Ok(profile_id.to_string()); + } + if matches!(action_type, WoodenFishActionType::CompileDraft) { + return Ok(build_prefixed_uuid_id("wooden-fish-profile-")); + } + Err(SpacetimeClientError::validation_failed( + "wooden-fish action 需要先完成 compile-draft", + )) +} + +fn apply_wooden_fish_work_to_session( + mut session: WoodenFishSessionSnapshotResponse, + work: &WoodenFishWorkProfileResponse, +) -> WoodenFishSessionSnapshotResponse { + session.status = work.draft.generation_status.clone(); + session.draft = Some(work.draft.clone()); + session.updated_at = work.summary.updated_at.clone(); + session +} + +fn default_draft() -> WoodenFishDraftResponse { + WoodenFishDraftResponse { + template_id: WOODEN_FISH_TEMPLATE_ID.to_string(), + template_name: WOODEN_FISH_TEMPLATE_NAME.to_string(), + profile_id: None, + work_title: WOODEN_FISH_TEMPLATE_NAME.to_string(), + work_description: String::new(), + theme_tags: vec!["休闲".to_string()], + hit_object_prompt: DEFAULT_HIT_OBJECT_PROMPT.to_string(), + hit_object_reference_image_src: None, + hit_sound_prompt: Some(DEFAULT_HIT_SOUND_PROMPT.to_string()), + floating_words: default_floating_words(), + hit_object_asset: None, + background_asset: None, + hit_sound_asset: None, + cover_image_src: None, + generation_status: WoodenFishGenerationStatus::Draft, + } +} + +fn build_config_json(draft: &WoodenFishDraftResponse) -> Result { + serde_json::to_string(&serde_json::json!({ + "workTitle": draft.work_title, + "workDescription": draft.work_description, + "themeTags": draft.theme_tags, + "hitObjectPrompt": draft.hit_object_prompt, + "hitObjectReferenceImageSrc": draft.hit_object_reference_image_src, + "hitSoundPrompt": draft.hit_sound_prompt, + "floatingWords": draft.floating_words, + })) + .map_err(SpacetimeClientError::validation_failed) +} + +fn normalize_tags(tags: Vec) -> Vec { + tags.into_iter() + .map(|tag| tag.trim().to_string()) + .filter(|tag| !tag.is_empty()) + .take(8) + .collect() +} + +fn default_floating_words() -> Vec { + vec![ + "幸运".to_string(), + "健康".to_string(), + "财富".to_string(), + "姻缘".to_string(), + "幸福".to_string(), + "事业".to_string(), + "成功".to_string(), + "功德".to_string(), + ] +} + +fn normalize_floating_words(words: Vec) -> Vec { + let mut normalized = Vec::new(); + for word in words { + let word = normalize_floating_word(&word); + if word.is_empty() || normalized.iter().any(|item| item == &word) { + continue; + } + normalized.push(word); + if normalized.len() >= 8 { + break; + } + } + if normalized.is_empty() { + default_floating_words() + } else { + normalized + } +} + +fn normalize_floating_word(word: &str) -> String { + word.trim() + .trim_end_matches(|ch: char| ch == '1' || ch.is_whitespace()) + .trim_end_matches(['+', '+']) + .trim() + .to_string() +} + +fn json_string(value: &T) -> Result { + serde_json::to_string(value).map_err(SpacetimeClientError::validation_failed) +} + +#[cfg(test)] +mod tests { + use super::*; + use shared_contracts::wooden_fish::WoodenFishAudioAsset; + + const SESSION_ID: &str = "wooden-fish-session-test"; + const OWNER_USER_ID: &str = "user-test"; + const PROFILE_ID: &str = "wooden-fish-profile-test"; + const NOW_MICROS: i64 = 1_763_456_789_000_000; + + #[test] + fn wooden_fish_action_compile_draft_builds_compile_input_with_assets() { + let session = session_with_draft(draft_without_assets()); + let mut payload = action(WoodenFishActionType::CompileDraft); + payload.hit_object_asset = Some(generated_hit_object_asset("generated-compile-object")); + payload.background_asset = Some(generated_background_asset("generated-compile-background")); + payload.hit_sound_asset = Some(generated_hit_sound_asset("generated-compile-sound")); + + let (plan, draft) = + build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) + .expect("compile-draft should build plan"); + + let WoodenFishActionProcedure::Compile(input) = plan else { + panic!("compile-draft should call compile_wooden_fish_draft"); + }; + assert_eq!(input.session_id, SESSION_ID); + assert_eq!(input.owner_user_id, OWNER_USER_ID); + assert_eq!(input.generation_status.as_deref(), Some("ready")); + assert!( + input + .hit_object_asset_json + .as_deref() + .unwrap_or("") + .contains("generated-compile-object") + ); + assert!( + input + .hit_sound_asset_json + .as_deref() + .unwrap_or("") + .contains("generated-compile-sound") + ); + assert!( + input + .background_asset_json + .as_deref() + .unwrap_or("") + .contains("generated-compile-background") + ); + assert_eq!(draft.generation_status, WoodenFishGenerationStatus::Ready); + } + + #[test] + fn wooden_fish_compile_requires_real_hit_sound_asset_from_api_server() { + let session = session_with_draft(draft_without_assets()); + let mut payload = action(WoodenFishActionType::CompileDraft); + payload.hit_object_asset = Some(generated_hit_object_asset("generated-compile-object")); + payload.background_asset = Some(generated_background_asset("generated-compile-background")); + + let error = + match build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) { + Ok(_) => panic!("compile-draft should not synthesize fake hit sound assets"), + Err(error) => error, + }; + + assert!( + error + .to_string() + .contains("hit sound asset 缺少真实生成资产") + ); + } + + #[test] + fn wooden_fish_compile_requires_real_background_asset_from_api_server() { + let session = session_with_draft(draft_without_assets()); + let mut payload = action(WoodenFishActionType::CompileDraft); + payload.hit_object_asset = Some(generated_hit_object_asset("generated-compile-object")); + payload.hit_sound_asset = Some(generated_hit_sound_asset("generated-compile-sound")); + + let error = + match build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) { + Ok(_) => panic!("compile-draft should not publish without background asset"), + Err(error) => error, + }; + + assert!( + error + .to_string() + .contains("background asset 缺少真实生成资产") + ); + } + + #[test] + fn wooden_fish_action_regenerate_hit_object_replaces_only_object_asset() { + let session = session_with_draft(draft_with_assets()); + let mut payload = action(WoodenFishActionType::RegenerateHitObject); + payload.hit_object_prompt = Some("新的敲击物".to_string()); + payload.hit_object_asset = Some(generated_hit_object_asset("generated-object")); + payload.background_asset = Some(generated_background_asset("generated-background")); + + let (plan, _draft) = + build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) + .expect("regenerate-hit-object should build plan"); + + let WoodenFishActionProcedure::Compile(input) = plan else { + panic!("regenerate-hit-object should call compile_wooden_fish_draft"); + }; + assert!( + !input + .hit_object_asset_json + .as_deref() + .unwrap_or("") + .contains("old-object") + ); + assert!( + input + .hit_object_asset_json + .as_deref() + .unwrap_or("") + .contains("real-profile") + ); + assert!( + !input + .hit_object_asset_json + .as_deref() + .unwrap_or("") + .contains(&NOW_MICROS.to_string()) + ); + assert!( + input + .hit_sound_asset_json + .as_deref() + .unwrap_or("") + .contains("old-sound") + ); + assert!( + input + .background_asset_json + .as_deref() + .unwrap_or("") + .contains("generated-background") + ); + } + + #[test] + fn wooden_fish_action_update_floating_words_builds_update_input() { + let session = session_with_draft(draft_with_assets()); + let mut payload = action(WoodenFishActionType::UpdateFloatingWords); + payload.floating_words = Some(vec![ + " 功德+1 ".to_string(), + "功德+1".to_string(), + "健康+1".to_string(), + ]); + + let (plan, draft) = + build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) + .expect("update-floating-words should build plan"); + + let WoodenFishActionProcedure::Update(input) = plan else { + panic!("update-floating-words should call update_wooden_fish_work"); + }; + assert_eq!(input.profile_id, PROFILE_ID); + assert!(input.hit_sound_asset_json.is_none()); + assert_eq!( + draft.floating_words, + vec!["功德".to_string(), "健康".to_string()] + ); + assert!( + input + .floating_words_json + .as_deref() + .unwrap_or("") + .contains("健康") + ); + } + + fn action(action_type: WoodenFishActionType) -> WoodenFishActionRequest { + WoodenFishActionRequest { + action_type, + profile_id: None, + work_title: None, + work_description: None, + theme_tags: None, + hit_object_prompt: None, + hit_object_reference_image_src: None, + hit_object_asset: None, + background_asset: None, + hit_sound_prompt: None, + hit_sound_asset: None, + floating_words: None, + } + } + + fn session_with_draft(draft: WoodenFishDraftResponse) -> WoodenFishSessionSnapshotResponse { + WoodenFishSessionSnapshotResponse { + session_id: SESSION_ID.to_string(), + owner_user_id: OWNER_USER_ID.to_string(), + status: draft.generation_status.clone(), + draft: Some(draft), + created_at: "2026-05-20T00:00:00Z".to_string(), + updated_at: "2026-05-20T00:00:00Z".to_string(), + } + } + + fn draft_without_assets() -> WoodenFishDraftResponse { + WoodenFishDraftResponse { + profile_id: None, + ..base_draft() + } + } + + fn generated_hit_object_asset(asset_id: &str) -> WoodenFishImageAsset { + WoodenFishImageAsset { + asset_id: asset_id.to_string(), + image_src: "/generated-wooden-fish-assets/real-profile/hit-object/image.png" + .to_string(), + image_object_key: "generated-wooden-fish-assets/real-profile/hit-object/image.png" + .to_string(), + asset_object_id: format!("{asset_id}-asset"), + generation_provider: "image2".to_string(), + prompt: "新的敲击物".to_string(), + width: 1024, + height: 1024, + } + } + + fn generated_background_asset(asset_id: &str) -> WoodenFishImageAsset { + WoodenFishImageAsset { + asset_id: asset_id.to_string(), + image_src: "/generated-wooden-fish-assets/real-profile/background/image.png" + .to_string(), + image_object_key: "generated-wooden-fish-assets/real-profile/background/image.png" + .to_string(), + asset_object_id: format!("{asset_id}-asset"), + generation_provider: "image2".to_string(), + prompt: "新的敲击背景".to_string(), + width: 1024, + height: 1536, + } + } + + fn generated_hit_sound_asset(asset_id: &str) -> WoodenFishAudioAsset { + WoodenFishAudioAsset { + asset_id: asset_id.to_string(), + audio_src: "/generated-wooden-fish-assets/real-profile/hit-sound/sound.mp3".to_string(), + audio_object_key: "generated-wooden-fish-assets/real-profile/hit-sound/sound.mp3" + .to_string(), + asset_object_id: format!("{asset_id}-asset"), + source: "generated".to_string(), + prompt: Some("新的木鱼音效".to_string()), + duration_ms: Some(3000), + } + } + + fn draft_with_assets() -> WoodenFishDraftResponse { + WoodenFishDraftResponse { + profile_id: Some(PROFILE_ID.to_string()), + hit_object_asset: Some(WoodenFishImageAsset { + asset_id: "old-object".to_string(), + image_src: "/generated-wooden-fish-assets/old-object.png".to_string(), + image_object_key: "generated-wooden-fish-assets/old-object.png".to_string(), + asset_object_id: "old-object-asset".to_string(), + generation_provider: "image2".to_string(), + prompt: "旧敲击物".to_string(), + width: 1024, + height: 1024, + }), + background_asset: Some(WoodenFishImageAsset { + asset_id: "old-background".to_string(), + image_src: "/generated-wooden-fish-assets/old-background.png".to_string(), + image_object_key: "generated-wooden-fish-assets/old-background.png".to_string(), + asset_object_id: "old-background-asset".to_string(), + generation_provider: "image2".to_string(), + prompt: "旧背景".to_string(), + width: 1024, + height: 1536, + }), + hit_sound_asset: Some(WoodenFishAudioAsset { + asset_id: "old-sound".to_string(), + audio_src: "/generated-wooden-fish-assets/old-sound.mp3".to_string(), + audio_object_key: "generated-wooden-fish-assets/old-sound.mp3".to_string(), + asset_object_id: "old-sound-asset".to_string(), + source: "generated".to_string(), + prompt: Some("旧音效".to_string()), + duration_ms: Some(700), + }), + cover_image_src: Some("/generated-wooden-fish-assets/old-object.png".to_string()), + generation_status: WoodenFishGenerationStatus::Ready, + ..base_draft() + } + } + + fn base_draft() -> WoodenFishDraftResponse { + WoodenFishDraftResponse { + template_id: WOODEN_FISH_TEMPLATE_ID.to_string(), + template_name: WOODEN_FISH_TEMPLATE_NAME.to_string(), + profile_id: None, + work_title: "旧标题".to_string(), + work_description: "旧描述".to_string(), + theme_tags: vec!["旧标签".to_string()], + hit_object_prompt: "旧敲击物".to_string(), + hit_object_reference_image_src: None, + hit_sound_prompt: Some("旧音效".to_string()), + floating_words: default_floating_words(), + hit_object_asset: None, + background_asset: None, + hit_sound_asset: None, + cover_image_src: None, + generation_status: WoodenFishGenerationStatus::Draft, + } + } +} diff --git a/server-rs/crates/spacetime-module/Cargo.toml b/server-rs/crates/spacetime-module/Cargo.toml index 16ff92e2..a418c2ad 100644 --- a/server-rs/crates/spacetime-module/Cargo.toml +++ b/server-rs/crates/spacetime-module/Cargo.toml @@ -19,6 +19,7 @@ module-combat = { workspace = true, features = ["spacetime-types"] } module-inventory = { workspace = true, features = ["spacetime-types"] } module-custom-world = { workspace = true, features = ["spacetime-types"] } module-jump-hop = { workspace = true, features = ["spacetime-types"] } +module-wooden-fish = { workspace = true, features = ["spacetime-types"] } module-match3d = { workspace = true } module-npc = { workspace = true, features = ["spacetime-types"] } module-puzzle = { workspace = true, features = ["spacetime-types"] } diff --git a/server-rs/crates/spacetime-module/src/lib.rs b/server-rs/crates/spacetime-module/src/lib.rs index b89390a4..adaeb114 100644 --- a/server-rs/crates/spacetime-module/src/lib.rs +++ b/server-rs/crates/spacetime-module/src/lib.rs @@ -1,4 +1,4 @@ -// 中文注释:SpacetimeDB 绑定生成依赖根模块继续公开 re-export 各领域类型; +// 中文注释:SpacetimeDB 绑定生成依赖根模块继续公开 re-export 各领域类型; // 少数领域 helper 同名只影响 value namespace 导出,不影响 table / reducer 类型。 #![allow(ambiguous_glob_reexports)] @@ -15,6 +15,7 @@ pub use module_quest::*; pub use module_runtime::*; pub use module_runtime_item::*; pub use module_story::*; +pub use module_wooden_fish::*; pub(crate) use serde_json::{Map as JsonMap, Value as JsonValue, json}; pub(crate) use shared_kernel::format_timestamp_micros; @@ -37,6 +38,7 @@ mod puzzle; mod runtime; mod square_hole; mod visual_novel; +mod wooden_fish; pub use ai::*; pub use asset_metadata::*; @@ -53,3 +55,4 @@ pub use migration::*; pub use runtime::*; pub use square_hole::*; pub use visual_novel::*; +pub use wooden_fish::*; diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index d12566c6..c2b2bc4b 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -26,6 +26,9 @@ use crate::square_hole::tables::{ square_hole_agent_message, square_hole_agent_session, square_hole_runtime_run, square_hole_work_profile, }; +use crate::wooden_fish::tables::{ + wooden_fish_agent_session, wooden_fish_event, wooden_fish_runtime_run, wooden_fish_work_profile, +}; use crate::{ visual_novel_agent_message, visual_novel_agent_session, visual_novel_runtime_event, visual_novel_runtime_history_entry, visual_novel_runtime_run, visual_novel_work_profile, @@ -241,6 +244,10 @@ macro_rules! migration_tables { jump_hop_work_profile, jump_hop_runtime_run, jump_hop_event, + wooden_fish_agent_session, + wooden_fish_work_profile, + wooden_fish_runtime_run, + wooden_fish_event, square_hole_agent_session, square_hole_agent_message, square_hole_work_profile, @@ -1258,6 +1265,14 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde .or_insert(serde_json::Value::Null); } } + if table_name == "wooden_fish_work_profile" { + if let Some(object) = next_value.as_object_mut() { + // 中文注释:敲木鱼背景环境图晚于首版作品表加入,旧迁移包按未生成背景兼容。 + object + .entry("background_asset_json".to_string()) + .or_insert(serde_json::Value::Null); + } + } next_value } diff --git a/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs b/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs index d063dd1b..0ce27567 100644 --- a/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs +++ b/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs @@ -182,17 +182,8 @@ fn seed_creation_entry_config_if_missing(ctx: &ReducerContext) { migrate_rpg_entry_from_old_hidden_default(ctx, now); migrate_visual_novel_entry_from_old_visible_default(ctx, now); migrate_bark_battle_entry_to_open_default(ctx, now); - migrate_coming_soon_entry_from_old_open_default( - ctx, - now, - ComingSoonEntryDefault { - id: "baby-object-match", - title: "宝贝识物", - subtitle: "亲子识物分类", - image_src: "/child-motion-demo/picture-book-grass-stage.png", - sort_order: 90, - }, - ); + migrate_baby_object_match_entry_from_old_coming_soon_default(ctx, now); + migrate_wooden_fish_entry_from_old_puzzle_image_default(ctx, now); } fn migrate_rpg_entry_from_old_hidden_default(ctx: &ReducerContext, now: Timestamp) { @@ -284,33 +275,24 @@ fn migrate_visual_novel_entry_from_old_visible_default(ctx: &ReducerContext, now }); } -struct ComingSoonEntryDefault { - id: &'static str, - title: &'static str, - subtitle: &'static str, - image_src: &'static str, - sort_order: i32, -} - -fn migrate_coming_soon_entry_from_old_open_default( +fn migrate_baby_object_match_entry_from_old_coming_soon_default( ctx: &ReducerContext, now: Timestamp, - target: ComingSoonEntryDefault, ) { - let id = target.id.to_string(); + let id = "baby-object-match".to_string(); let Some(row) = ctx.db.creation_entry_type_config().id().find(&id) else { return; }; - // 中文注释:只把旧默认开放种子纠偏为敬请期待,不覆盖后台手动维护过的入口配置。 - let still_old_open_default = row.title == target.title - && row.subtitle == target.subtitle - && row.badge == "可创建" - && row.image_src == target.image_src + // 中文注释:宝贝识物已接入完整创作发布链路,只纠偏历史默认敬请期待种子。 + let still_old_coming_soon_default = row.title == "宝贝识物" + && row.subtitle == "亲子识物分类" + && row.badge == "敬请期待" + && row.image_src == "/child-motion-demo/picture-book-grass-stage.png" && row.visible - && row.open - && row.sort_order == target.sort_order; - if !still_old_open_default { + && !row.open + && row.sort_order == 90; + if !still_old_coming_soon_default { return; } @@ -318,8 +300,36 @@ fn migrate_coming_soon_entry_from_old_open_default( .creation_entry_type_config() .id() .update(CreationEntryTypeConfig { - badge: "敬请期待".to_string(), - open: false, + badge: "可创建".to_string(), + open: true, + updated_at: now, + ..row + }); +} + +fn migrate_wooden_fish_entry_from_old_puzzle_image_default(ctx: &ReducerContext, now: Timestamp) { + let id = "wooden-fish".to_string(); + let Some(row) = ctx.db.creation_entry_type_config().id().find(&id) else { + return; + }; + + // 中文注释:只替换敲木鱼旧默认入口图,不覆盖后台手动设置过的其它展示信息。 + let still_old_puzzle_image_default = row.title == "敲木鱼" + && row.subtitle == "点击祈福轻玩法" + && row.badge == "可创建" + && row.image_src == "/creation-type-references/puzzle.webp" + && row.visible + && row.open + && row.sort_order == 47; + if !still_old_puzzle_image_default { + return; + } + + ctx.db + .creation_entry_type_config() + .id() + .update(CreationEntryTypeConfig { + image_src: "/wooden-fish/default-hit-object.png".to_string(), updated_at: now, ..row }); diff --git a/server-rs/crates/spacetime-module/src/wooden_fish.rs b/server-rs/crates/spacetime-module/src/wooden_fish.rs new file mode 100644 index 00000000..3b3982b2 --- /dev/null +++ b/server-rs/crates/spacetime-module/src/wooden_fish.rs @@ -0,0 +1,1259 @@ +pub(crate) mod tables; +mod types; + +pub use tables::*; +pub use types::*; + +use crate::{ProcedureContext, ReducerContext, SpacetimeType, Table, Timestamp}; +use module_wooden_fish::{ + WoodenFishRunSnapshot, WoodenFishRunStatus, WoodenFishWordCounter, apply_run_checkpoint, + default_floating_words, finish_run, normalize_floating_words, normalize_word_counters, +}; +use serde::Serialize; +use serde::de::DeserializeOwned; +use spacetimedb::AnonymousViewContext; + +#[spacetimedb::view(accessor = wooden_fish_gallery_view, public)] +pub fn wooden_fish_gallery_view(ctx: &AnonymousViewContext) -> Vec { + let mut items = ctx + .db + .wooden_fish_work_profile() + .by_wooden_fish_work_publication_status() + .filter(WOODEN_FISH_PUBLICATION_PUBLISHED) + .filter_map(|row| match build_gallery_view_row(&row) { + Ok(item) => Some(item), + Err(error) => { + log::warn!( + "敲木鱼公开广场 view 跳过损坏的作品投影 profile_id={}: {}", + row.profile_id, + error + ); + None + } + }) + .collect::>(); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); + items +} + +#[spacetimedb::view(accessor = wooden_fish_gallery_card_view, public)] +pub fn wooden_fish_gallery_card_view( + ctx: &AnonymousViewContext, +) -> Vec { + wooden_fish_gallery_view(ctx) + .into_iter() + .map(|row| WoodenFishGalleryCardViewRow { + public_work_code: row.public_work_code, + work_id: row.work_id, + profile_id: row.profile_id, + owner_user_id: row.owner_user_id, + author_display_name: row.author_display_name, + work_title: row.work_title, + work_description: row.work_description, + cover_image_src: row.cover_image_src, + theme_tags: row.theme_tags, + publication_status: row.publication_status, + play_count: row.play_count, + updated_at_micros: row.updated_at_micros, + published_at_micros: row.published_at_micros, + generation_status: row.generation_status, + }) + .collect() +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct WoodenFishGalleryViewRow { + pub public_work_code: String, + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub hit_object_prompt: String, + pub hit_object_reference_image_src: Option, + pub hit_sound_prompt: Option, + pub hit_object_asset: Option, + pub background_asset: Option, + pub hit_sound_asset: Option, + pub floating_words: Vec, + pub cover_image_src: String, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub generation_status: String, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct WoodenFishGalleryCardViewRow { + pub public_work_code: String, + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub cover_image_src: String, + pub theme_tags: Vec, + pub publication_status: String, + pub play_count: u32, + pub updated_at_micros: i64, + pub published_at_micros: Option, + pub generation_status: String, +} + +#[spacetimedb::procedure] +pub fn create_wooden_fish_agent_session( + ctx: &mut ProcedureContext, + input: WoodenFishAgentSessionCreateInput, +) -> WoodenFishAgentSessionProcedureResult { + match ctx.try_with_tx(|tx| create_wooden_fish_agent_session_tx(tx, input.clone())) { + Ok(session) => session_result(session), + Err(message) => session_error(message), + } +} + +#[spacetimedb::procedure] +pub fn get_wooden_fish_agent_session( + ctx: &mut ProcedureContext, + input: WoodenFishAgentSessionGetInput, +) -> WoodenFishAgentSessionProcedureResult { + match ctx.try_with_tx(|tx| get_wooden_fish_agent_session_tx(tx, input.clone())) { + Ok(session) => session_result(session), + Err(message) => session_error(message), + } +} + +#[spacetimedb::procedure] +pub fn compile_wooden_fish_draft( + ctx: &mut ProcedureContext, + input: WoodenFishDraftCompileInput, +) -> WoodenFishAgentSessionProcedureResult { + match ctx.try_with_tx(|tx| compile_wooden_fish_draft_tx(tx, input.clone())) { + Ok(session) => session_result(session), + Err(message) => session_error(message), + } +} + +#[spacetimedb::procedure] +pub fn get_wooden_fish_work_profile( + ctx: &mut ProcedureContext, + input: WoodenFishWorkGetInput, +) -> WoodenFishWorkProcedureResult { + match ctx.try_with_tx(|tx| get_wooden_fish_work_profile_tx(tx, input.clone())) { + Ok(work) => work_result(work), + Err(message) => work_error(message), + } +} + +#[spacetimedb::procedure] +pub fn update_wooden_fish_work( + ctx: &mut ProcedureContext, + input: WoodenFishWorkUpdateInput, +) -> WoodenFishWorkProcedureResult { + match ctx.try_with_tx(|tx| update_wooden_fish_work_tx(tx, input.clone())) { + Ok(work) => work_result(work), + Err(message) => work_error(message), + } +} + +#[spacetimedb::procedure] +pub fn publish_wooden_fish_work( + ctx: &mut ProcedureContext, + input: WoodenFishWorkPublishInput, +) -> WoodenFishWorkProcedureResult { + match ctx.try_with_tx(|tx| publish_wooden_fish_work_tx(tx, input.clone())) { + Ok(work) => work_result(work), + Err(message) => work_error(message), + } +} + +#[spacetimedb::procedure] +pub fn list_wooden_fish_works( + ctx: &mut ProcedureContext, + input: WoodenFishWorksListInput, +) -> WoodenFishWorksProcedureResult { + match ctx.try_with_tx(|tx| list_wooden_fish_works_tx(tx, input.clone())) { + Ok(items) => WoodenFishWorksProcedureResult { + ok: true, + items, + error_message: None, + }, + Err(message) => WoodenFishWorksProcedureResult { + ok: false, + items: Vec::new(), + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn start_wooden_fish_run( + ctx: &mut ProcedureContext, + input: WoodenFishRunStartInput, +) -> WoodenFishRunProcedureResult { + match ctx.try_with_tx(|tx| start_wooden_fish_run_tx(tx, input.clone())) { + Ok(run) => run_result(run), + Err(message) => run_error(message), + } +} + +#[spacetimedb::procedure] +pub fn get_wooden_fish_run( + ctx: &mut ProcedureContext, + input: WoodenFishRunGetInput, +) -> WoodenFishRunProcedureResult { + match ctx.try_with_tx(|tx| get_wooden_fish_run_tx(tx, input.clone())) { + Ok(run) => run_result(run), + Err(message) => run_error(message), + } +} + +#[spacetimedb::procedure] +pub fn checkpoint_wooden_fish_run( + ctx: &mut ProcedureContext, + input: WoodenFishRunCheckpointInput, +) -> WoodenFishRunProcedureResult { + match ctx.try_with_tx(|tx| checkpoint_wooden_fish_run_tx(tx, input.clone())) { + Ok(run) => run_result(run), + Err(message) => run_error(message), + } +} + +#[spacetimedb::procedure] +pub fn finish_wooden_fish_run( + ctx: &mut ProcedureContext, + input: WoodenFishRunFinishInput, +) -> WoodenFishRunProcedureResult { + match ctx.try_with_tx(|tx| finish_wooden_fish_run_tx(tx, input.clone())) { + Ok(run) => run_result(run), + Err(message) => run_error(message), + } +} + +fn create_wooden_fish_agent_session_tx( + ctx: &ReducerContext, + input: WoodenFishAgentSessionCreateInput, +) -> Result { + require_non_empty(&input.session_id, "wooden_fish session_id")?; + require_non_empty(&input.owner_user_id, "wooden_fish owner_user_id")?; + if ctx + .db + .wooden_fish_agent_session() + .session_id() + .find(&input.session_id) + .is_some() + { + return Err("wooden_fish_agent_session.session_id 已存在".to_string()); + } + + let created_at = Timestamp::from_micros_since_unix_epoch(input.created_at_micros); + let config = input + .config_json + .as_deref() + .map(parse_config) + .transpose()? + .unwrap_or_else(|| default_config_from_input(&input)); + let draft = input + .draft_json + .as_deref() + .map(parse_json) + .transpose()? + .unwrap_or_else(|| draft_from_config(&config, None, WOODEN_FISH_GENERATION_DRAFT)); + + ctx.db + .wooden_fish_agent_session() + .insert(WoodenFishAgentSessionRow { + session_id: input.session_id.clone(), + owner_user_id: input.owner_user_id.clone(), + current_turn: 0, + progress_percent: 0, + stage: WOODEN_FISH_STAGE_COLLECTING.to_string(), + config_json: to_json_string(&config), + draft_json: to_json_string(&draft), + published_profile_id: String::new(), + created_at, + updated_at: created_at, + }); + + get_wooden_fish_agent_session_tx( + ctx, + WoodenFishAgentSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn get_wooden_fish_agent_session_tx( + ctx: &ReducerContext, + input: WoodenFishAgentSessionGetInput, +) -> Result { + let row = find_owned_session(ctx, &input.session_id, &input.owner_user_id)?; + build_session_snapshot(&row) +} + +fn compile_wooden_fish_draft_tx( + ctx: &ReducerContext, + input: WoodenFishDraftCompileInput, +) -> Result { + require_non_empty(&input.profile_id, "wooden_fish profile_id")?; + let session = find_owned_session(ctx, &input.session_id, &input.owner_user_id)?; + let tags = parse_tags(input.theme_tags_json.as_deref().unwrap_or("[]"))?; + let floating_words = match input + .floating_words_json + .as_deref() + .map(parse_json::>) + .transpose()? + { + Some(words) => normalize_floating_words(&words), + None => default_floating_words(), + }; + let hit_object_asset = input + .hit_object_asset_json + .as_deref() + .map(parse_json) + .transpose()?; + let hit_sound_asset = input + .hit_sound_asset_json + .as_deref() + .map(parse_json) + .transpose()?; + let background_asset = input + .background_asset_json + .as_deref() + .map(parse_json) + .transpose()?; + let cover_image_src = input + .cover_image_src + .as_deref() + .and_then(clean_optional) + .or_else(|| { + hit_object_asset + .as_ref() + .map(|asset: &WoodenFishImageAssetSnapshot| asset.image_src.clone()) + }); + let draft = WoodenFishDraftSnapshot { + template_id: WOODEN_FISH_TEMPLATE_ID.to_string(), + template_name: WOODEN_FISH_TEMPLATE_NAME.to_string(), + profile_id: Some(input.profile_id.clone()), + work_title: clean_string(&input.work_title, WOODEN_FISH_TEMPLATE_NAME), + work_description: input.work_description.trim().to_string(), + theme_tags: tags.clone(), + hit_object_prompt: clean_string( + &input.hit_object_prompt, + "默认敲击物图案,圆润木质质感,透明背景", + ), + hit_object_reference_image_src: input + .hit_object_reference_image_src + .as_deref() + .and_then(clean_optional), + hit_sound_prompt: input.hit_sound_prompt.as_deref().and_then(clean_optional), + floating_words: floating_words.clone(), + hit_object_asset: hit_object_asset.clone(), + background_asset: background_asset.clone(), + hit_sound_asset: hit_sound_asset.clone(), + cover_image_src: cover_image_src.clone(), + generation_status: input + .generation_status + .clone() + .unwrap_or_else(|| WOODEN_FISH_GENERATION_READY.to_string()), + }; + let compiled_at = Timestamp::from_micros_since_unix_epoch(input.compiled_at_micros); + let row = WoodenFishWorkProfileRow { + profile_id: input.profile_id.clone(), + work_id: input.profile_id.clone(), + owner_user_id: input.owner_user_id.clone(), + source_session_id: input.session_id.clone(), + author_display_name: clean_string(&input.author_display_name, "敲木鱼玩家"), + work_title: draft.work_title.clone(), + work_description: draft.work_description.clone(), + theme_tags_json: to_json_string(&tags), + hit_object_prompt: draft.hit_object_prompt.clone(), + hit_object_reference_image_src: draft + .hit_object_reference_image_src + .clone() + .unwrap_or_default(), + hit_sound_prompt: draft.hit_sound_prompt.clone().unwrap_or_default(), + hit_object_asset_json: hit_object_asset + .as_ref() + .map(to_json_string) + .unwrap_or_default(), + hit_sound_asset_json: hit_sound_asset + .as_ref() + .map(to_json_string) + .unwrap_or_default(), + floating_words_json: to_json_string(&floating_words), + cover_image_src: cover_image_src.unwrap_or_default(), + generation_status: draft.generation_status.clone(), + publication_status: WOODEN_FISH_PUBLICATION_DRAFT.to_string(), + play_count: 0, + updated_at: compiled_at, + published_at: None, + background_asset_json: background_asset.as_ref().map(to_json_string), + }; + upsert_work(ctx, row); + let config = config_from_draft(&draft); + replace_session( + ctx, + &session, + WoodenFishAgentSessionRow { + progress_percent: 100, + stage: WOODEN_FISH_STAGE_DRAFT_COMPILED.to_string(), + config_json: to_json_string(&config), + draft_json: to_json_string(&draft), + published_profile_id: input.profile_id, + updated_at: compiled_at, + ..clone_session(&session) + }, + ); + + get_wooden_fish_agent_session_tx( + ctx, + WoodenFishAgentSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn get_wooden_fish_work_profile_tx( + ctx: &ReducerContext, + input: WoodenFishWorkGetInput, +) -> Result { + let row = find_work(ctx, &input.profile_id)?; + if !input.owner_user_id.trim().is_empty() && row.owner_user_id != input.owner_user_id { + return Err("无权访问该 wooden_fish work".to_string()); + } + build_work_snapshot(&row) +} + +fn update_wooden_fish_work_tx( + ctx: &ReducerContext, + input: WoodenFishWorkUpdateInput, +) -> Result { + let row = find_owned_work(ctx, &input.profile_id, &input.owner_user_id)?; + let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros); + let mut next = clone_work(&row); + next.work_title = clean_string(&input.work_title, &row.work_title); + next.work_description = input.work_description.trim().to_string(); + next.theme_tags_json = input.theme_tags_json.clone(); + if let Some(value) = input.hit_object_prompt.as_deref().and_then(clean_optional) { + next.hit_object_prompt = value; + } + if let Some(value) = input + .hit_object_reference_image_src + .as_deref() + .and_then(clean_optional) + { + next.hit_object_reference_image_src = value; + } + if let Some(value) = input.hit_sound_prompt.as_deref().and_then(clean_optional) { + next.hit_sound_prompt = value; + } + if let Some(value) = input + .hit_object_asset_json + .as_deref() + .and_then(clean_optional) + { + let asset = parse_json::(&value)?; + next.hit_object_asset_json = to_json_string(&asset); + next.cover_image_src = asset.image_src; + } + if let Some(value) = input + .hit_sound_asset_json + .as_deref() + .and_then(clean_optional) + { + let asset = parse_json::(&value)?; + next.hit_sound_asset_json = to_json_string(&asset); + } + if let Some(value) = input + .background_asset_json + .as_deref() + .and_then(clean_optional) + { + let asset = parse_json::(&value)?; + next.background_asset_json = Some(to_json_string(&asset)); + } + if let Some(value) = input + .floating_words_json + .as_deref() + .and_then(clean_optional) + { + let words = parse_json::>(&value)?; + next.floating_words_json = to_json_string(&normalize_floating_words(&words)); + } + if let Some(value) = input.cover_image_src.as_deref().and_then(clean_optional) { + next.cover_image_src = value; + } + if let Some(value) = input.generation_status.as_deref().and_then(clean_optional) { + next.generation_status = value; + } + next.updated_at = updated_at; + replace_work(ctx, &row, next); + let updated = find_work(ctx, &row.profile_id)?; + sync_session_from_work_update(ctx, &updated, updated_at)?; + build_work_snapshot(&updated) +} + +fn publish_wooden_fish_work_tx( + ctx: &ReducerContext, + input: WoodenFishWorkPublishInput, +) -> Result { + let row = find_owned_work(ctx, &input.profile_id, &input.owner_user_id)?; + if !is_publish_ready(&row) { + return Err("发布需要完整的敲击物图案、敲击音效和飘字配置".to_string()); + } + let published_at = Timestamp::from_micros_since_unix_epoch(input.published_at_micros); + replace_work( + ctx, + &row, + WoodenFishWorkProfileRow { + publication_status: WOODEN_FISH_PUBLICATION_PUBLISHED.to_string(), + updated_at: published_at, + published_at: Some(published_at), + ..clone_work(&row) + }, + ); + if let Some(session) = ctx + .db + .wooden_fish_agent_session() + .session_id() + .find(&row.source_session_id) + { + replace_session( + ctx, + &session, + WoodenFishAgentSessionRow { + stage: WOODEN_FISH_STAGE_PUBLISHED.to_string(), + updated_at: published_at, + ..clone_session(&session) + }, + ); + } + let updated = find_work(ctx, &row.profile_id)?; + build_work_snapshot(&updated) +} + +fn list_wooden_fish_works_tx( + ctx: &ReducerContext, + input: WoodenFishWorksListInput, +) -> Result, String> { + let mut rows = if input.owner_user_id.trim().is_empty() { + ctx.db.wooden_fish_work_profile().iter().collect::>() + } else { + ctx.db + .wooden_fish_work_profile() + .by_wooden_fish_work_owner_user_id() + .filter(input.owner_user_id.as_str()) + .collect::>() + }; + if input.published_only { + rows.retain(|row| row.publication_status == WOODEN_FISH_PUBLICATION_PUBLISHED); + } + rows.sort_by(|left, right| { + right + .updated_at + .cmp(&left.updated_at) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); + rows.into_iter() + .map(|row| build_work_snapshot(&row)) + .collect() +} + +fn start_wooden_fish_run_tx( + ctx: &ReducerContext, + input: WoodenFishRunStartInput, +) -> Result { + require_non_empty(&input.run_id, "wooden_fish run_id")?; + let work = find_work(ctx, &input.profile_id)?; + if !is_publish_ready(&work) { + return Err("敲木鱼运行态需要完整作品配置".to_string()); + } + let snapshot = WoodenFishRunSnapshot { + run_id: input.run_id.clone(), + profile_id: input.profile_id.clone(), + owner_user_id: input.owner_user_id.clone(), + status: WoodenFishRunStatus::Playing, + total_tap_count: 0, + word_counters: Vec::new(), + started_at_ms: input.started_at_ms.max(0) as u64, + updated_at_ms: input.started_at_ms.max(0) as u64, + finished_at_ms: None, + }; + upsert_run(ctx, &snapshot, input.started_at_ms); + increment_work_play_count(ctx, &work, input.started_at_ms); + insert_event( + ctx, + input.client_event_id, + input.owner_user_id, + input.profile_id, + input.run_id, + WOODEN_FISH_EVENT_RUN_STARTED, + None, + input.started_at_ms, + ); + Ok(snapshot) +} + +fn get_wooden_fish_run_tx( + ctx: &ReducerContext, + input: WoodenFishRunGetInput, +) -> Result { + let row = find_owned_run(ctx, &input.run_id, &input.owner_user_id)?; + parse_json(&row.snapshot_json) +} + +fn checkpoint_wooden_fish_run_tx( + ctx: &ReducerContext, + input: WoodenFishRunCheckpointInput, +) -> Result { + let row = find_owned_run(ctx, &input.run_id, &input.owner_user_id)?; + let current = parse_json::(&row.snapshot_json)?; + if current.status == WoodenFishRunStatus::Finished { + return Err("wooden_fish run 已结束,不能 checkpoint".to_string()); + } + let counters = parse_json::>(&input.word_counters_json)?; + let next = apply_run_checkpoint( + ¤t, + input.total_tap_count, + normalize_word_counters(counters), + input.checkpoint_at_ms.max(0) as u64, + ); + replace_run(ctx, &row, &next, input.checkpoint_at_ms); + insert_event( + ctx, + input.client_event_id, + input.owner_user_id, + next.profile_id.clone(), + input.run_id, + WOODEN_FISH_EVENT_RUN_CHECKPOINT, + Some(next.total_tap_count.to_string()), + input.checkpoint_at_ms, + ); + Ok(next) +} + +fn finish_wooden_fish_run_tx( + ctx: &ReducerContext, + input: WoodenFishRunFinishInput, +) -> Result { + let row = find_owned_run(ctx, &input.run_id, &input.owner_user_id)?; + let current = parse_json::(&row.snapshot_json)?; + let counters = parse_json::>(&input.word_counters_json)?; + let next = finish_run( + ¤t, + input.total_tap_count, + normalize_word_counters(counters), + input.finished_at_ms.max(0) as u64, + ); + replace_run(ctx, &row, &next, input.finished_at_ms); + insert_event( + ctx, + input.client_event_id, + input.owner_user_id, + next.profile_id.clone(), + input.run_id, + WOODEN_FISH_EVENT_RUN_FINISHED, + Some(next.total_tap_count.to_string()), + input.finished_at_ms, + ); + Ok(next) +} + +fn build_gallery_view_row( + row: &WoodenFishWorkProfileRow, +) -> Result { + let work = build_work_snapshot(row)?; + Ok(WoodenFishGalleryViewRow { + public_work_code: build_wooden_fish_public_work_code(&work.profile_id), + work_id: work.work_id, + profile_id: work.profile_id, + owner_user_id: work.owner_user_id, + source_session_id: work.source_session_id, + author_display_name: work.author_display_name, + work_title: work.work_title, + work_description: work.work_description, + theme_tags: work.theme_tags, + hit_object_prompt: work.hit_object_prompt, + hit_object_reference_image_src: work.hit_object_reference_image_src, + hit_sound_prompt: work.hit_sound_prompt, + hit_object_asset: work.hit_object_asset, + background_asset: work.background_asset, + hit_sound_asset: work.hit_sound_asset, + floating_words: work.floating_words, + cover_image_src: work.cover_image_src, + publication_status: work.publication_status, + publish_ready: work.publish_ready, + play_count: work.play_count, + generation_status: work.generation_status, + updated_at_micros: work.updated_at_micros, + published_at_micros: work.published_at_micros, + }) +} + +fn build_session_snapshot( + row: &WoodenFishAgentSessionRow, +) -> Result { + Ok(WoodenFishAgentSessionSnapshot { + session_id: row.session_id.clone(), + owner_user_id: row.owner_user_id.clone(), + current_turn: row.current_turn, + progress_percent: row.progress_percent, + stage: row.stage.clone(), + config: parse_config(&row.config_json)?, + draft: clean_optional(&row.draft_json) + .map(|value| parse_json(&value)) + .transpose()?, + published_profile_id: clean_optional(&row.published_profile_id), + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + }) +} + +fn build_work_snapshot(row: &WoodenFishWorkProfileRow) -> Result { + Ok(WoodenFishWorkSnapshot { + work_id: row.work_id.clone(), + profile_id: row.profile_id.clone(), + owner_user_id: row.owner_user_id.clone(), + source_session_id: row.source_session_id.clone(), + author_display_name: row.author_display_name.clone(), + work_title: row.work_title.clone(), + work_description: row.work_description.clone(), + theme_tags: parse_tags(&row.theme_tags_json)?, + hit_object_prompt: row.hit_object_prompt.clone(), + hit_object_reference_image_src: clean_optional(&row.hit_object_reference_image_src), + hit_sound_prompt: clean_optional(&row.hit_sound_prompt), + hit_object_asset: clean_optional(&row.hit_object_asset_json) + .map(|value| parse_json(&value)) + .transpose()?, + background_asset: row + .background_asset_json + .as_deref() + .and_then(clean_optional) + .map(|value| parse_json(&value)) + .transpose()?, + hit_sound_asset: clean_optional(&row.hit_sound_asset_json) + .map(|value| parse_json(&value)) + .transpose()?, + floating_words: normalize_floating_words(&parse_json_or_default::>( + &row.floating_words_json, + )), + cover_image_src: row.cover_image_src.clone(), + publication_status: row.publication_status.clone(), + publish_ready: is_publish_ready(row), + play_count: row.play_count, + generation_status: row.generation_status.clone(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + published_at_micros: row + .published_at + .map(|value| value.to_micros_since_unix_epoch()), + }) +} + +fn sync_session_from_work_update( + ctx: &ReducerContext, + work: &WoodenFishWorkProfileRow, + updated_at: Timestamp, +) -> Result<(), String> { + let Some(session) = ctx + .db + .wooden_fish_agent_session() + .session_id() + .find(&work.source_session_id) + else { + return Ok(()); + }; + + let snapshot = build_work_snapshot(work)?; + let draft = draft_from_work_snapshot(&snapshot); + let config = config_from_draft(&draft); + replace_session( + ctx, + &session, + WoodenFishAgentSessionRow { + config_json: to_json_string(&config), + draft_json: to_json_string(&draft), + updated_at, + ..clone_session(&session) + }, + ); + Ok(()) +} + +fn find_owned_session( + ctx: &ReducerContext, + session_id: &str, + owner_user_id: &str, +) -> Result { + let row = ctx + .db + .wooden_fish_agent_session() + .session_id() + .find(&session_id.to_string()) + .ok_or_else(|| "wooden_fish_agent_session 不存在".to_string())?; + if row.owner_user_id != owner_user_id { + return Err("无权访问该 wooden_fish session".to_string()); + } + Ok(row) +} + +fn find_work(ctx: &ReducerContext, profile_id: &str) -> Result { + ctx.db + .wooden_fish_work_profile() + .profile_id() + .find(&profile_id.to_string()) + .ok_or_else(|| "wooden_fish_work_profile 不存在".to_string()) +} + +fn find_owned_work( + ctx: &ReducerContext, + profile_id: &str, + owner_user_id: &str, +) -> Result { + let row = find_work(ctx, profile_id)?; + if row.owner_user_id != owner_user_id { + return Err("无权访问该 wooden_fish work".to_string()); + } + Ok(row) +} + +fn find_owned_run( + ctx: &ReducerContext, + run_id: &str, + owner_user_id: &str, +) -> Result { + let row = ctx + .db + .wooden_fish_runtime_run() + .run_id() + .find(&run_id.to_string()) + .ok_or_else(|| "wooden_fish_runtime_run 不存在".to_string())?; + if row.owner_user_id != owner_user_id { + return Err("无权访问该 wooden_fish run".to_string()); + } + Ok(row) +} + +fn upsert_work(ctx: &ReducerContext, row: WoodenFishWorkProfileRow) { + if let Some(old) = ctx + .db + .wooden_fish_work_profile() + .profile_id() + .find(&row.profile_id) + { + ctx.db.wooden_fish_work_profile().delete(old); + } + ctx.db.wooden_fish_work_profile().insert(row); +} + +fn replace_work( + ctx: &ReducerContext, + old: &WoodenFishWorkProfileRow, + next: WoodenFishWorkProfileRow, +) { + ctx.db.wooden_fish_work_profile().delete(clone_work(old)); + ctx.db.wooden_fish_work_profile().insert(next); +} + +fn replace_session( + ctx: &ReducerContext, + old: &WoodenFishAgentSessionRow, + next: WoodenFishAgentSessionRow, +) { + ctx.db + .wooden_fish_agent_session() + .delete(clone_session(old)); + ctx.db.wooden_fish_agent_session().insert(next); +} + +fn upsert_run(ctx: &ReducerContext, snapshot: &WoodenFishRunSnapshot, updated_at_ms: i64) { + if let Some(old) = ctx + .db + .wooden_fish_runtime_run() + .run_id() + .find(&snapshot.run_id) + { + ctx.db.wooden_fish_runtime_run().delete(old); + } + let created_at = Timestamp::from_micros_since_unix_epoch(updated_at_ms.saturating_mul(1000)); + ctx.db + .wooden_fish_runtime_run() + .insert(run_row_from_snapshot(snapshot, created_at, created_at)); +} + +fn replace_run( + ctx: &ReducerContext, + old: &WoodenFishRuntimeRunRow, + snapshot: &WoodenFishRunSnapshot, + updated_at_ms: i64, +) { + ctx.db.wooden_fish_runtime_run().delete(clone_run(old)); + ctx.db + .wooden_fish_runtime_run() + .insert(run_row_from_snapshot( + snapshot, + old.created_at, + Timestamp::from_micros_since_unix_epoch(updated_at_ms.saturating_mul(1000)), + )); +} + +fn run_row_from_snapshot( + snapshot: &WoodenFishRunSnapshot, + created_at: Timestamp, + updated_at: Timestamp, +) -> WoodenFishRuntimeRunRow { + WoodenFishRuntimeRunRow { + run_id: snapshot.run_id.clone(), + owner_user_id: snapshot.owner_user_id.clone(), + profile_id: snapshot.profile_id.clone(), + status: run_status_as_str(snapshot.status).to_string(), + total_tap_count: snapshot.total_tap_count, + word_counters_json: to_json_string(&snapshot.word_counters), + started_at_ms: snapshot.started_at_ms as i64, + updated_at_ms: snapshot.updated_at_ms as i64, + finished_at_ms: snapshot + .finished_at_ms + .map(|value| value as i64) + .unwrap_or(0), + snapshot_json: to_json_string(snapshot), + created_at, + updated_at, + } +} + +fn increment_work_play_count( + ctx: &ReducerContext, + row: &WoodenFishWorkProfileRow, + played_at_ms: i64, +) { + replace_work( + ctx, + row, + WoodenFishWorkProfileRow { + play_count: row.play_count.saturating_add(1), + updated_at: Timestamp::from_micros_since_unix_epoch(played_at_ms.saturating_mul(1000)), + ..clone_work(row) + }, + ); +} + +fn insert_event( + ctx: &ReducerContext, + event_id: String, + owner_user_id: String, + profile_id: String, + run_id: String, + event_type: &str, + result: Option, + occurred_at_ms: i64, +) { + let event_id = clean_optional(&event_id).unwrap_or_else(|| { + format!( + "wooden-fish-event-{}-{}-{}", + run_id, event_type, occurred_at_ms + ) + }); + if ctx + .db + .wooden_fish_event() + .event_id() + .find(&event_id) + .is_some() + { + return; + } + ctx.db.wooden_fish_event().insert(WoodenFishEventRow { + event_id, + owner_user_id, + profile_id, + run_id, + event_type: event_type.to_string(), + result: result.unwrap_or_default(), + occurred_at: Timestamp::from_micros_since_unix_epoch(occurred_at_ms.saturating_mul(1000)), + }); +} + +fn is_publish_ready(row: &WoodenFishWorkProfileRow) -> bool { + !row.work_title.trim().is_empty() + && !row.hit_object_asset_json.trim().is_empty() + && row + .background_asset_json + .as_deref() + .and_then(clean_optional) + .is_some() + && !row.hit_sound_asset_json.trim().is_empty() + && !row.floating_words_json.trim().is_empty() + && row.generation_status == WOODEN_FISH_GENERATION_READY +} + +fn default_config_from_input( + input: &WoodenFishAgentSessionCreateInput, +) -> WoodenFishCreatorConfigSnapshot { + WoodenFishCreatorConfigSnapshot { + work_title: clean_string(&input.work_title, WOODEN_FISH_TEMPLATE_NAME), + work_description: input.work_description.trim().to_string(), + theme_tags: parse_tags(input.theme_tags_json.as_deref().unwrap_or("[]")) + .unwrap_or_default(), + hit_object_prompt: "默认敲击物图案,圆润木质质感,透明背景".to_string(), + hit_object_reference_image_src: None, + hit_sound_prompt: Some("清脆短促的木鱼敲击声".to_string()), + floating_words: default_floating_words(), + } +} + +fn draft_from_config( + config: &WoodenFishCreatorConfigSnapshot, + profile_id: Option, + generation_status: &str, +) -> WoodenFishDraftSnapshot { + WoodenFishDraftSnapshot { + template_id: WOODEN_FISH_TEMPLATE_ID.to_string(), + template_name: WOODEN_FISH_TEMPLATE_NAME.to_string(), + profile_id, + work_title: config.work_title.clone(), + work_description: config.work_description.clone(), + theme_tags: config.theme_tags.clone(), + hit_object_prompt: config.hit_object_prompt.clone(), + hit_object_reference_image_src: config.hit_object_reference_image_src.clone(), + hit_sound_prompt: config.hit_sound_prompt.clone(), + floating_words: normalize_floating_words(&config.floating_words), + hit_object_asset: None, + background_asset: None, + hit_sound_asset: None, + cover_image_src: None, + generation_status: generation_status.to_string(), + } +} + +fn draft_from_work_snapshot(work: &WoodenFishWorkSnapshot) -> WoodenFishDraftSnapshot { + WoodenFishDraftSnapshot { + template_id: WOODEN_FISH_TEMPLATE_ID.to_string(), + template_name: WOODEN_FISH_TEMPLATE_NAME.to_string(), + profile_id: Some(work.profile_id.clone()), + work_title: work.work_title.clone(), + work_description: work.work_description.clone(), + theme_tags: work.theme_tags.clone(), + hit_object_prompt: work.hit_object_prompt.clone(), + hit_object_reference_image_src: work.hit_object_reference_image_src.clone(), + hit_sound_prompt: work.hit_sound_prompt.clone(), + floating_words: work.floating_words.clone(), + hit_object_asset: work.hit_object_asset.clone(), + background_asset: work.background_asset.clone(), + hit_sound_asset: work.hit_sound_asset.clone(), + cover_image_src: clean_optional(&work.cover_image_src), + generation_status: work.generation_status.clone(), + } +} + +fn config_from_draft(draft: &WoodenFishDraftSnapshot) -> WoodenFishCreatorConfigSnapshot { + WoodenFishCreatorConfigSnapshot { + work_title: draft.work_title.clone(), + work_description: draft.work_description.clone(), + theme_tags: draft.theme_tags.clone(), + hit_object_prompt: draft.hit_object_prompt.clone(), + hit_object_reference_image_src: draft.hit_object_reference_image_src.clone(), + hit_sound_prompt: draft.hit_sound_prompt.clone(), + floating_words: normalize_floating_words(&draft.floating_words), + } +} + +fn build_wooden_fish_public_work_code(profile_id: &str) -> String { + let normalized = profile_id + .chars() + .filter(|character| character.is_ascii_alphanumeric()) + .flat_map(|character| character.to_uppercase()) + .collect::(); + let fallback = if normalized.is_empty() { + "00000000".to_string() + } else { + normalized + }; + let suffix = if fallback.len() > 8 { + fallback[fallback.len() - 8..].to_string() + } else { + format!("{fallback:0>8}") + }; + format!("WF-{suffix}") +} + +fn run_status_as_str(status: WoodenFishRunStatus) -> &'static str { + match status { + WoodenFishRunStatus::Playing => "playing", + WoodenFishRunStatus::Finished => "finished", + } +} + +fn require_non_empty(value: &str, label: &str) -> Result<(), String> { + if value.trim().is_empty() { + Err(format!("{label} 不能为空")) + } else { + Ok(()) + } +} + +fn clean_optional(value: &str) -> Option { + let value = value.trim(); + if value.is_empty() { + None + } else { + Some(value.to_string()) + } +} + +fn clean_string(value: &str, fallback: &str) -> String { + clean_optional(value).unwrap_or_else(|| fallback.to_string()) +} + +fn parse_config(value: &str) -> Result { + parse_json(value) +} + +fn parse_tags(value: &str) -> Result, String> { + Ok(parse_json_or_default::>(value) + .into_iter() + .map(|tag| tag.trim().to_string()) + .filter(|tag| !tag.is_empty()) + .take(8) + .collect()) +} + +fn parse_json(value: &str) -> Result +where + T: DeserializeOwned, +{ + serde_json::from_str(value).map_err(|error| error.to_string()) +} + +fn parse_json_or_default(value: &str) -> T +where + T: DeserializeOwned + Default, +{ + serde_json::from_str(value).unwrap_or_default() +} + +fn to_json_string(value: &T) -> String +where + T: Serialize, +{ + serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()) +} + +fn session_result( + session: WoodenFishAgentSessionSnapshot, +) -> WoodenFishAgentSessionProcedureResult { + WoodenFishAgentSessionProcedureResult { + ok: true, + session: Some(session), + error_message: None, + } +} + +fn session_error(message: String) -> WoodenFishAgentSessionProcedureResult { + WoodenFishAgentSessionProcedureResult { + ok: false, + session: None, + error_message: Some(message), + } +} + +fn work_result(work: WoodenFishWorkSnapshot) -> WoodenFishWorkProcedureResult { + WoodenFishWorkProcedureResult { + ok: true, + work: Some(work), + error_message: None, + } +} + +fn work_error(message: String) -> WoodenFishWorkProcedureResult { + WoodenFishWorkProcedureResult { + ok: false, + work: None, + error_message: Some(message), + } +} + +fn run_result(run: WoodenFishRunSnapshot) -> WoodenFishRunProcedureResult { + WoodenFishRunProcedureResult { + ok: true, + run: Some(run), + error_message: None, + } +} + +fn run_error(message: String) -> WoodenFishRunProcedureResult { + WoodenFishRunProcedureResult { + ok: false, + run: None, + error_message: Some(message), + } +} + +fn clone_session(row: &WoodenFishAgentSessionRow) -> WoodenFishAgentSessionRow { + WoodenFishAgentSessionRow { + session_id: row.session_id.clone(), + owner_user_id: row.owner_user_id.clone(), + current_turn: row.current_turn, + progress_percent: row.progress_percent, + stage: row.stage.clone(), + config_json: row.config_json.clone(), + draft_json: row.draft_json.clone(), + published_profile_id: row.published_profile_id.clone(), + created_at: row.created_at, + updated_at: row.updated_at, + } +} + +fn clone_work(row: &WoodenFishWorkProfileRow) -> WoodenFishWorkProfileRow { + WoodenFishWorkProfileRow { + profile_id: row.profile_id.clone(), + work_id: row.work_id.clone(), + owner_user_id: row.owner_user_id.clone(), + source_session_id: row.source_session_id.clone(), + author_display_name: row.author_display_name.clone(), + work_title: row.work_title.clone(), + work_description: row.work_description.clone(), + theme_tags_json: row.theme_tags_json.clone(), + hit_object_prompt: row.hit_object_prompt.clone(), + hit_object_reference_image_src: row.hit_object_reference_image_src.clone(), + hit_sound_prompt: row.hit_sound_prompt.clone(), + hit_object_asset_json: row.hit_object_asset_json.clone(), + background_asset_json: row.background_asset_json.clone(), + hit_sound_asset_json: row.hit_sound_asset_json.clone(), + floating_words_json: row.floating_words_json.clone(), + cover_image_src: row.cover_image_src.clone(), + generation_status: row.generation_status.clone(), + publication_status: row.publication_status.clone(), + play_count: row.play_count, + updated_at: row.updated_at, + published_at: row.published_at, + } +} + +fn clone_run(row: &WoodenFishRuntimeRunRow) -> WoodenFishRuntimeRunRow { + WoodenFishRuntimeRunRow { + run_id: row.run_id.clone(), + owner_user_id: row.owner_user_id.clone(), + profile_id: row.profile_id.clone(), + status: row.status.clone(), + total_tap_count: row.total_tap_count, + word_counters_json: row.word_counters_json.clone(), + started_at_ms: row.started_at_ms, + updated_at_ms: row.updated_at_ms, + finished_at_ms: row.finished_at_ms, + snapshot_json: row.snapshot_json.clone(), + created_at: row.created_at, + updated_at: row.updated_at, + } +} diff --git a/server-rs/crates/spacetime-module/src/wooden_fish/tables.rs b/server-rs/crates/spacetime-module/src/wooden_fish/tables.rs new file mode 100644 index 00000000..27899c15 --- /dev/null +++ b/server-rs/crates/spacetime-module/src/wooden_fish/tables.rs @@ -0,0 +1,87 @@ +use crate::*; + +#[spacetimedb::table( + accessor = wooden_fish_agent_session, + index(accessor = by_wooden_fish_agent_session_owner_user_id, btree(columns = [owner_user_id])) +)] +pub struct WoodenFishAgentSessionRow { + #[primary_key] + pub(crate) session_id: String, + pub(crate) owner_user_id: String, + pub(crate) current_turn: u32, + pub(crate) progress_percent: u32, + pub(crate) stage: String, + pub(crate) config_json: String, + pub(crate) draft_json: String, + pub(crate) published_profile_id: String, + pub(crate) created_at: Timestamp, + pub(crate) updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = wooden_fish_work_profile, + index(accessor = by_wooden_fish_work_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_wooden_fish_work_publication_status, btree(columns = [publication_status])) +)] +pub struct WoodenFishWorkProfileRow { + #[primary_key] + pub(crate) profile_id: String, + pub(crate) work_id: String, + pub(crate) owner_user_id: String, + pub(crate) source_session_id: String, + pub(crate) author_display_name: String, + pub(crate) work_title: String, + pub(crate) work_description: String, + pub(crate) theme_tags_json: String, + pub(crate) hit_object_prompt: String, + pub(crate) hit_object_reference_image_src: String, + pub(crate) hit_sound_prompt: String, + pub(crate) hit_object_asset_json: String, + pub(crate) hit_sound_asset_json: String, + pub(crate) floating_words_json: String, + pub(crate) cover_image_src: String, + pub(crate) generation_status: String, + pub(crate) publication_status: String, + pub(crate) play_count: u32, + pub(crate) updated_at: Timestamp, + pub(crate) published_at: Option, + #[default(None::)] + pub(crate) background_asset_json: Option, +} + +#[spacetimedb::table( + accessor = wooden_fish_runtime_run, + index(accessor = by_wooden_fish_run_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_wooden_fish_run_profile_id, btree(columns = [profile_id])) +)] +pub struct WoodenFishRuntimeRunRow { + #[primary_key] + pub(crate) run_id: String, + pub(crate) owner_user_id: String, + pub(crate) profile_id: String, + pub(crate) status: String, + pub(crate) total_tap_count: u32, + pub(crate) word_counters_json: String, + pub(crate) started_at_ms: i64, + pub(crate) updated_at_ms: i64, + pub(crate) finished_at_ms: i64, + pub(crate) snapshot_json: String, + pub(crate) created_at: Timestamp, + pub(crate) updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = wooden_fish_event, + index(accessor = by_wooden_fish_event_profile_id, btree(columns = [profile_id])), + index(accessor = by_wooden_fish_event_run_id, btree(columns = [run_id])) +)] +pub struct WoodenFishEventRow { + #[primary_key] + pub(crate) event_id: String, + pub(crate) owner_user_id: String, + pub(crate) profile_id: String, + pub(crate) run_id: String, + pub(crate) event_type: String, + pub(crate) result: String, + pub(crate) occurred_at: Timestamp, +} diff --git a/server-rs/crates/spacetime-module/src/wooden_fish/types.rs b/server-rs/crates/spacetime-module/src/wooden_fish/types.rs new file mode 100644 index 00000000..0bbeef03 --- /dev/null +++ b/server-rs/crates/spacetime-module/src/wooden_fish/types.rs @@ -0,0 +1,258 @@ +use crate::*; +use serde::{Deserialize, Serialize}; + +pub const WOODEN_FISH_TEMPLATE_ID: &str = "wooden-fish"; +pub const WOODEN_FISH_TEMPLATE_NAME: &str = "敲木鱼"; +pub const WOODEN_FISH_STAGE_COLLECTING: &str = "Collecting"; +pub const WOODEN_FISH_STAGE_DRAFT_COMPILED: &str = "DraftCompiled"; +pub const WOODEN_FISH_STAGE_PUBLISHED: &str = "Published"; +pub const WOODEN_FISH_PUBLICATION_DRAFT: &str = "Draft"; +pub const WOODEN_FISH_PUBLICATION_PUBLISHED: &str = "Published"; +pub const WOODEN_FISH_GENERATION_DRAFT: &str = "draft"; +pub const WOODEN_FISH_GENERATION_READY: &str = "ready"; +pub const WOODEN_FISH_EVENT_RUN_STARTED: &str = "run-started"; +pub const WOODEN_FISH_EVENT_RUN_CHECKPOINT: &str = "checkpoint"; +pub const WOODEN_FISH_EVENT_RUN_FINISHED: &str = "finish"; + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct WoodenFishAgentSessionCreateInput { + pub session_id: String, + pub owner_user_id: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: Option, + pub config_json: Option, + pub draft_json: Option, + pub created_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct WoodenFishAgentSessionGetInput { + pub session_id: String, + pub owner_user_id: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct WoodenFishDraftCompileInput { + pub session_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: Option, + pub hit_object_prompt: String, + pub hit_object_reference_image_src: Option, + pub hit_sound_prompt: Option, + pub hit_object_asset_json: Option, + pub background_asset_json: Option, + pub hit_sound_asset_json: Option, + pub floating_words_json: Option, + pub cover_image_src: Option, + pub generation_status: Option, + pub compiled_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct WoodenFishWorkUpdateInput { + pub profile_id: String, + pub owner_user_id: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: String, + pub hit_object_prompt: Option, + pub hit_object_reference_image_src: Option, + pub hit_sound_prompt: Option, + pub hit_object_asset_json: Option, + pub background_asset_json: Option, + pub hit_sound_asset_json: Option, + pub floating_words_json: Option, + pub cover_image_src: Option, + pub generation_status: Option, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct WoodenFishWorkPublishInput { + pub profile_id: String, + pub owner_user_id: String, + pub published_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct WoodenFishWorksListInput { + pub owner_user_id: String, + pub published_only: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct WoodenFishWorkGetInput { + pub profile_id: String, + pub owner_user_id: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct WoodenFishRunStartInput { + pub run_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub client_event_id: String, + pub started_at_ms: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct WoodenFishRunGetInput { + pub run_id: String, + pub owner_user_id: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct WoodenFishRunCheckpointInput { + pub run_id: String, + pub owner_user_id: String, + pub total_tap_count: u32, + pub word_counters_json: String, + pub client_event_id: String, + pub checkpoint_at_ms: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct WoodenFishRunFinishInput { + pub run_id: String, + pub owner_user_id: String, + pub total_tap_count: u32, + pub word_counters_json: String, + pub client_event_id: String, + pub finished_at_ms: i64, +} + +#[derive(Clone, Debug, PartialEq, SpacetimeType)] +pub struct WoodenFishAgentSessionProcedureResult { + pub ok: bool, + pub session: Option, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, SpacetimeType)] +pub struct WoodenFishWorkProcedureResult { + pub ok: bool, + pub work: Option, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, SpacetimeType)] +pub struct WoodenFishWorksProcedureResult { + pub ok: bool, + pub items: Vec, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, SpacetimeType)] +pub struct WoodenFishRunProcedureResult { + pub ok: bool, + pub run: Option, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishCreatorConfigSnapshot { + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub hit_object_prompt: String, + #[serde(default)] + pub hit_object_reference_image_src: Option, + #[serde(default)] + pub hit_sound_prompt: Option, + pub floating_words: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishImageAssetSnapshot { + pub asset_id: String, + pub image_src: String, + pub image_object_key: String, + pub asset_object_id: String, + pub generation_provider: String, + pub prompt: String, + pub width: u32, + pub height: u32, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishAudioAssetSnapshot { + pub asset_id: String, + pub audio_src: String, + pub audio_object_key: String, + pub asset_object_id: String, + pub source: String, + #[serde(default)] + pub prompt: Option, + #[serde(default)] + pub duration_ms: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishDraftSnapshot { + pub template_id: String, + pub template_name: String, + pub profile_id: Option, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub hit_object_prompt: String, + pub hit_object_reference_image_src: Option, + pub hit_sound_prompt: Option, + pub floating_words: Vec, + pub hit_object_asset: Option, + pub background_asset: Option, + pub hit_sound_asset: Option, + pub cover_image_src: Option, + pub generation_status: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishAgentSessionSnapshot { + pub session_id: String, + pub owner_user_id: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: String, + pub config: WoodenFishCreatorConfigSnapshot, + pub draft: Option, + pub published_profile_id: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] +#[serde(rename_all = "camelCase")] +pub struct WoodenFishWorkSnapshot { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub hit_object_prompt: String, + pub hit_object_reference_image_src: Option, + pub hit_sound_prompt: Option, + pub hit_object_asset: Option, + pub background_asset: Option, + pub hit_sound_asset: Option, + pub floating_words: Vec, + pub cover_image_src: String, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub generation_status: String, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} diff --git a/src/components/platform-entry/PlatformEntryCreationTypeModal.test.tsx b/src/components/platform-entry/PlatformEntryCreationTypeModal.test.tsx new file mode 100644 index 00000000..16e1b08e --- /dev/null +++ b/src/components/platform-entry/PlatformEntryCreationTypeModal.test.tsx @@ -0,0 +1,64 @@ +/* @vitest-environment jsdom */ + +import { fireEvent, render, screen } from '@testing-library/react'; +import { expect, test, vi } from 'vitest'; + +import type { CreationEntryConfig } from '../../services/creationEntryConfigService'; +import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal'; +import { derivePlatformCreationTypes } from './platformEntryCreationTypes'; + +const entryConfig = { + startCard: { + title: '新建作品', + description: '', + idleBadge: '模板', + busyBadge: '开启中', + }, + typeModal: { + title: '选择创作类型', + description: '', + }, + creationTypes: [ + { + id: 'wooden-fish', + title: '敲木鱼', + subtitle: '轻点积累功德', + badge: '可创建', + imageSrc: '/creation-type-references/puzzle.webp', + visible: true, + open: true, + sortOrder: 10, + updatedAtMicros: 1, + }, + ], +} satisfies CreationEntryConfig; + +test('dispatches wooden fish creation type selection', () => { + const onSelectWoodenFish = vi.fn(); + + render( + {}} + onSelectRpg={() => {}} + onSelectBigFish={() => {}} + onSelectMatch3D={() => {}} + onSelectSquareHole={() => {}} + onSelectJumpHop={() => {}} + onSelectWoodenFish={onSelectWoodenFish} + onSelectPuzzle={() => {}} + onSelectCreativeAgent={() => {}} + onSelectBarkBattle={() => {}} + onSelectVisualNovel={() => {}} + onSelectBabyObjectMatch={() => {}} + />, + ); + + fireEvent.click(screen.getByRole('button', { name: /敲木鱼/u })); + + expect(onSelectWoodenFish).toHaveBeenCalledTimes(1); +}); diff --git a/src/components/platform-entry/PlatformEntryCreationTypeModal.tsx b/src/components/platform-entry/PlatformEntryCreationTypeModal.tsx index aa08bce6..8d6698aa 100644 --- a/src/components/platform-entry/PlatformEntryCreationTypeModal.tsx +++ b/src/components/platform-entry/PlatformEntryCreationTypeModal.tsx @@ -19,6 +19,7 @@ export interface PlatformEntryCreationTypeModalProps { onSelectMatch3D: () => void; onSelectSquareHole: () => void; onSelectJumpHop: () => void; + onSelectWoodenFish: () => void; onSelectPuzzle: () => void; onSelectCreativeAgent: () => void; onSelectBarkBattle: () => void; @@ -102,6 +103,7 @@ export function PlatformEntryCreationTypeModal({ onSelectMatch3D, onSelectSquareHole, onSelectJumpHop, + onSelectWoodenFish, onSelectPuzzle, onSelectCreativeAgent, onSelectBarkBattle, @@ -147,6 +149,9 @@ export function PlatformEntryCreationTypeModal({ if (item.id === 'jump-hop') { onSelectJumpHop(); } + if (item.id === 'wooden-fish') { + onSelectWoodenFish(); + } if (item.id === 'puzzle') { onSelectPuzzle(); } diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index ad7e4011..cb7e7088 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -173,10 +173,10 @@ import { } from '../../services/edutainment-baby-object'; import { jumpHopClient, + type JumpHopGalleryCardResponse, type JumpHopRunResponse, type JumpHopSessionResponse, type JumpHopSessionSnapshotResponse, - type JumpHopGalleryCardResponse, type JumpHopWorkProfileResponse, type JumpHopWorkspaceCreateRequest, } from '../../services/jump-hop/jumpHopClient'; @@ -202,6 +202,7 @@ import { buildMiniGameDraftGenerationProgress, buildPuzzleGenerationAnchorEntries, buildSquareHoleGenerationAnchorEntries, + buildWoodenFishGenerationAnchorEntries, createMiniGameDraftGenerationState, type MiniGameDraftGenerationKind, type MiniGameDraftGenerationState, @@ -216,6 +217,7 @@ import { buildPuzzlePublicWorkCode, buildSquareHolePublicWorkCode, buildVisualNovelPublicWorkCode, + buildWoodenFishPublicWorkCode, isSameBabyObjectMatchPublicWorkCode, isSameBarkBattlePublicWorkCode, isSameBigFishPublicWorkCode, @@ -224,6 +226,7 @@ import { isSamePuzzlePublicWorkCode, isSameSquareHolePublicWorkCode, isSameVisualNovelPublicWorkCode, + isSameWoodenFishPublicWorkCode, } from '../../services/publicWorkCode'; import { createPuzzleAgentSession, @@ -313,6 +316,15 @@ import { publishVisualNovelWork, updateVisualNovelWork, } from '../../services/visual-novel-works'; +import { + woodenFishClient, + type WoodenFishGalleryCardResponse, + type WoodenFishRunResponse, + type WoodenFishSessionResponse, + type WoodenFishSessionSnapshotResponse, + type WoodenFishWorkProfileResponse, + type WoodenFishWorkspaceCreateRequest, +} from '../../services/wooden-fish/woodenFishClient'; import type { CustomWorldProfile } from '../../types'; import { useAuthUi } from '../auth/AuthUiContext'; import { PublishShareModal } from '../common/PublishShareModal'; @@ -334,6 +346,7 @@ import { isPuzzleGalleryEntry, isSquareHoleGalleryEntry, isVisualNovelGalleryEntry, + isWoodenFishGalleryEntry, mapBabyObjectMatchDraftToPlatformGalleryCard, mapBarkBattleWorkToPlatformGalleryCard, mapBigFishWorkToPlatformGalleryCard, @@ -342,6 +355,7 @@ import { mapPuzzleWorkToPlatformGalleryCard, mapSquareHoleWorkToPlatformGalleryCard, mapVisualNovelWorkToPlatformGalleryCard, + mapWoodenFishWorkToPlatformGalleryCard, type PlatformPublicGalleryCard, } from '../rpg-entry/rpgEntryWorldPresentation'; import { useRpgCreationAgentOperationPolling } from '../rpg-entry/useRpgCreationAgentOperationPolling'; @@ -461,6 +475,7 @@ type RecommendRuntimeKind = | 'match3d' | 'puzzle' | 'square-hole' + | 'wooden-fish' | 'visual-novel' | 'rpg'; type SquareHoleRuntimeReturnStage = @@ -477,6 +492,10 @@ type BabyObjectMatchRuntimeReturnStage = | 'work-detail' | 'platform'; type JumpHopRuntimeReturnStage = 'jump-hop-result' | 'work-detail' | 'platform'; +type WoodenFishRuntimeReturnStage = + | 'wooden-fish-result' + | 'work-detail' + | 'platform'; type VisualNovelEntryGenerationPhase = 'generating' | 'ready' | 'failed'; type BabyObjectMatchGenerationPhase = 'generating' | 'ready' | 'failed'; @@ -489,6 +508,7 @@ type RecommendRuntimeState = { puzzleRun: PuzzleRunSnapshot | null; squareHoleRun: SquareHoleRunSnapshot | null; visualNovelRun: VisualNovelRunSnapshot | null; + woodenFishRun: WoodenFishRunResponse['run'] | null; }; type PuzzleSaveArchiveState = { @@ -546,17 +566,21 @@ function getPlatformPublicGalleryEntryKey(entry: PlatformPublicGalleryCard) { ? 'big-fish' : isPuzzleGalleryEntry(entry) ? 'puzzle' - : isMatch3DGalleryEntry(entry) - ? 'match3d' - : isSquareHoleGalleryEntry(entry) - ? 'square-hole' - : isVisualNovelGalleryEntry(entry) - ? 'visual-novel' - : isBarkBattleGalleryEntry(entry) - ? 'bark-battle' - : isEdutainmentGalleryEntry(entry) - ? `edutainment:${entry.templateId}` - : 'rpg'; + : isJumpHopGalleryEntry(entry) + ? 'jump-hop' + : isWoodenFishGalleryEntry(entry) + ? 'wooden-fish' + : isMatch3DGalleryEntry(entry) + ? 'match3d' + : isSquareHoleGalleryEntry(entry) + ? 'square-hole' + : isVisualNovelGalleryEntry(entry) + ? 'visual-novel' + : isBarkBattleGalleryEntry(entry) + ? 'bark-battle' + : isEdutainmentGalleryEntry(entry) + ? `edutainment:${entry.templateId}` + : 'rpg'; return `${kind}:${entry.ownerUserId}:${entry.profileId}`; } @@ -575,6 +599,10 @@ function getPlatformRecommendRuntimeKind( return 'jump-hop'; } + if (isWoodenFishGalleryEntry(entry)) { + return 'wooden-fish'; + } + if (isMatch3DGalleryEntry(entry)) { return 'match3d'; } @@ -613,6 +641,9 @@ function isRecommendRuntimeReadyForEntry( if (expectedKind === 'jump-hop') { return Boolean(state.jumpHopRun); } + if (expectedKind === 'wooden-fish') { + return Boolean(state.woodenFishRun); + } if (expectedKind === 'match3d') { return Boolean(state.match3dRun); } @@ -784,6 +815,12 @@ function mapBarkBattlePublicDetailToWorkSummary( }; } +function mapWoodenFishWorkToPublicWorkDetail( + item: WoodenFishGalleryCardResponse | WoodenFishWorkProfileResponse, +): PlatformPublicGalleryCard { + return mapWoodenFishWorkToPlatformGalleryCard(item); +} + function mapVisualNovelWorkDetailToSession( work: VisualNovelWorkDetail, ): VisualNovelAgentSessionSnapshot { @@ -2321,6 +2358,27 @@ const JumpHopRuntimeShell = lazy(async () => { }; }); +const WoodenFishWorkspace = lazy(async () => { + const module = await import('../wooden-fish-creation/WoodenFishWorkspace'); + return { + default: module.WoodenFishWorkspace, + }; +}); + +const WoodenFishResultView = lazy(async () => { + const module = await import('../wooden-fish-result/WoodenFishResultView'); + return { + default: module.WoodenFishResultView, + }; +}); + +const WoodenFishRuntimeShell = lazy(async () => { + const module = await import('../wooden-fish-runtime/WoodenFishRuntimeShell'); + return { + default: module.WoodenFishRuntimeShell, + }; +}); + const BarkBattleConfigEditor = lazy(async () => { const module = await import('../bark-battle-creation/BarkBattleConfigEditor'); return { @@ -2591,6 +2649,22 @@ export function PlatformEntryFlowShellImpl({ const [barkBattleGalleryEntries, setBarkBattleGalleryEntries] = useState< BarkBattleWorkSummary[] >([]); + const [woodenFishSession, setWoodenFishSession] = + useState(null); + const [woodenFishRun, setWoodenFishRun] = useState< + WoodenFishRunResponse['run'] | null + >(null); + const [woodenFishWork, setWoodenFishWork] = + useState(null); + const [woodenFishGalleryEntries, setWoodenFishGalleryEntries] = useState< + WoodenFishGalleryCardResponse[] + >([]); + const [woodenFishRuntimeReturnStage, setWoodenFishRuntimeReturnStage] = + useState('wooden-fish-result'); + const [woodenFishGenerationState, setWoodenFishGenerationState] = + useState(null); + const [woodenFishError, setWoodenFishError] = useState(null); + const [isWoodenFishBusy, setIsWoodenFishBusy] = useState(false); const [barkBattlePublishedConfig, setBarkBattlePublishedConfig] = useState(null); const [barkBattleDraftConfig, setBarkBattleDraftConfig] = @@ -3230,6 +3304,17 @@ export function PlatformEntryFlowShellImpl({ } }, []); + const refreshWoodenFishGallery = useCallback(async () => { + try { + const galleryResponse = await woodenFishClient.listGallery(); + setWoodenFishGalleryEntries(galleryResponse.items); + return galleryResponse.items; + } catch { + setWoodenFishGalleryEntries([]); + return []; + } + }, []); + const refreshPuzzleShelf = useCallback(async () => { setIsPuzzleLoadingLibrary(true); @@ -3654,6 +3739,9 @@ export function PlatformEntryFlowShellImpl({ const jumpHopPublicEntries = jumpHopGalleryEntries.map( mapJumpHopWorkToPlatformGalleryCard, ); + const woodenFishPublicEntries = woodenFishGalleryEntries.map( + mapWoodenFishWorkToPlatformGalleryCard, + ); const visualNovelPublicEntries = visualNovelGalleryEntries.map( mapVisualNovelWorkToPlatformGalleryCard, ); @@ -3666,6 +3754,7 @@ export function PlatformEntryFlowShellImpl({ ...barkBattlePublicEntries, ...squareHolePublicEntries, ...jumpHopPublicEntries, + ...woodenFishPublicEntries, ...(isVisualNovelCreationOpen ? visualNovelPublicEntries : []), ...babyObjectMatchPublicEntries, ], @@ -3684,6 +3773,7 @@ export function PlatformEntryFlowShellImpl({ barkBattleWorks, squareHoleGalleryEntries, visualNovelGalleryEntries, + woodenFishGalleryEntries, ]); const latestGalleryEntries = useMemo( () => @@ -3702,6 +3792,9 @@ export function PlatformEntryFlowShellImpl({ .filter((work) => work.status === 'published') .map(mapBarkBattleWorkToPlatformGalleryCard) : []), + ...woodenFishGalleryEntries.map( + mapWoodenFishWorkToPlatformGalleryCard, + ), ...squareHoleGalleryEntries.map( mapSquareHoleWorkToPlatformGalleryCard, ), @@ -3731,6 +3824,7 @@ export function PlatformEntryFlowShellImpl({ visualNovelGalleryEntries, barkBattleGalleryEntries, barkBattleWorks, + woodenFishGalleryEntries, ], ); const recommendRuntimeEntries = useMemo(() => { @@ -6087,6 +6181,18 @@ export function PlatformEntryFlowShellImpl({ return; } + if (type === 'wooden-fish') { + enterCreateTab(); + setShowCreationTypeModal(false); + setActiveCreationFormType('wooden-fish'); + setWoodenFishError(null); + setWoodenFishSession(null); + setWoodenFishWork(null); + setWoodenFishRun(null); + setWoodenFishGenerationState(null); + return; + } + if (type === 'puzzle') { enterCreateTab(); setShowCreationTypeModal(false); @@ -6143,6 +6249,11 @@ export function PlatformEntryFlowShellImpl({ setJumpHopRun, setJumpHopSession, setJumpHopWork, + setWoodenFishError, + setWoodenFishGenerationState, + setWoodenFishRun, + setWoodenFishSession, + setWoodenFishWork, setVisualNovelError, ], ); @@ -6182,6 +6293,16 @@ export function PlatformEntryFlowShellImpl({ setSelectionStage('platform'); }, [setSelectionStage]); + const leaveWoodenFishFlow = useCallback(() => { + setWoodenFishRun(null); + setWoodenFishWork(null); + setWoodenFishRuntimeReturnStage('wooden-fish-result'); + setWoodenFishGenerationState(null); + setWoodenFishSession(null); + setWoodenFishError(null); + setSelectionStage('platform'); + }, [setSelectionStage]); + const createReadyJumpHopGenerationState = useCallback( (state: MiniGameDraftGenerationState) => resolveFinishedMiniGameDraftGenerationState(state, 'ready', { @@ -6191,6 +6312,15 @@ export function PlatformEntryFlowShellImpl({ [], ); + const createReadyWoodenFishGenerationState = useCallback( + (state: MiniGameDraftGenerationState) => + resolveFinishedMiniGameDraftGenerationState(state, 'ready', { + completedAssetCount: 2, + totalAssetCount: 2, + }), + [], + ); + const leaveBarkBattleFlow = useCallback(() => { setBarkBattleDraftConfig(null); setBarkBattlePublishedConfig(null); @@ -6654,6 +6784,10 @@ export function PlatformEntryFlowShellImpl({ pushAppHistoryPath('/runtime/baby-love-drawing'); }, [setSelectionStage]); + const startChildMotionDemo = useCallback(() => { + window.location.assign('/child-motion-demo'); + }, []); + const resolveBabyObjectMatchRuntimeDraft = useCallback( async (entry: PlatformPublicGalleryCard) => { if (!isEdutainmentGalleryEntry(entry)) { @@ -7425,6 +7559,325 @@ export function PlatformEntryFlowShellImpl({ [jumpHopRun?.runId], ); + const compileWoodenFishSession = useCallback( + async ( + created: WoodenFishSessionResponse, + payload?: WoodenFishWorkspaceCreateRequest, + ) => { + const generationState = createMiniGameDraftGenerationState('wooden-fish'); + setWoodenFishError(null); + setWoodenFishSession(created.session); + setWoodenFishWork(null); + setWoodenFishRun(null); + setWoodenFishGenerationState(generationState); + setIsWoodenFishBusy(true); + setSelectionStage('wooden-fish-generating'); + + try { + const response = await woodenFishClient.executeAction( + created.session.sessionId, + { + actionType: 'compile-draft', + workTitle: payload?.workTitle ?? created.session.draft?.workTitle, + workDescription: + payload?.workDescription ?? + created.session.draft?.workDescription, + themeTags: payload?.themeTags ?? created.session.draft?.themeTags, + hitObjectPrompt: + payload?.hitObjectPrompt ?? + created.session.draft?.hitObjectPrompt, + hitObjectReferenceImageSrc: + payload?.hitObjectReferenceImageSrc ?? + created.session.draft?.hitObjectReferenceImageSrc, + hitSoundPrompt: + payload?.hitSoundPrompt ?? created.session.draft?.hitSoundPrompt, + hitSoundAsset: + payload?.hitSoundAsset ?? created.session.draft?.hitSoundAsset, + floatingWords: + payload?.floatingWords ?? created.session.draft?.floatingWords, + }, + ); + setWoodenFishSession(response.session); + setWoodenFishWork(response.work ?? null); + setWoodenFishGenerationState( + createReadyWoodenFishGenerationState(generationState), + ); + setSelectionStage('wooden-fish-result'); + } catch (error) { + const errorMessage = resolveRpgCreationErrorMessage( + error, + '生成敲木鱼草稿失败。', + ); + setWoodenFishError(errorMessage); + setWoodenFishGenerationState( + resolveFinishedMiniGameDraftGenerationState( + generationState, + 'failed', + { error: errorMessage }, + ), + ); + try { + const latest = await woodenFishClient.getSession( + created.session.sessionId, + ); + setWoodenFishSession(latest.session); + setWoodenFishWork(null); + } catch { + setWoodenFishSession(created.session); + setWoodenFishWork(null); + } + } finally { + setIsWoodenFishBusy(false); + } + }, + [createReadyWoodenFishGenerationState, setSelectionStage], + ); + + const retryWoodenFishDraftGeneration = useCallback(() => { + if (!woodenFishSession) { + setSelectionStage('wooden-fish-workspace'); + return; + } + + void compileWoodenFishSession({ session: woodenFishSession }); + }, [compileWoodenFishSession, setSelectionStage, woodenFishSession]); + + const regenerateWoodenFishAsset = useCallback( + async (actionType: 'regenerate-hit-object' | 'generate-hit-sound') => { + if (!woodenFishSession?.sessionId) { + setSelectionStage('wooden-fish-workspace'); + return; + } + + const generationState = createMiniGameDraftGenerationState('wooden-fish'); + setWoodenFishError(null); + setWoodenFishGenerationState(generationState); + setIsWoodenFishBusy(true); + setSelectionStage('wooden-fish-generating'); + try { + const response = await woodenFishClient.executeAction( + woodenFishSession.sessionId, + { + actionType, + workTitle: woodenFishSession.draft?.workTitle, + workDescription: woodenFishSession.draft?.workDescription, + themeTags: woodenFishSession.draft?.themeTags, + hitObjectPrompt: woodenFishSession.draft?.hitObjectPrompt, + hitObjectReferenceImageSrc: + woodenFishSession.draft?.hitObjectReferenceImageSrc, + hitSoundPrompt: woodenFishSession.draft?.hitSoundPrompt, + hitSoundAsset: woodenFishSession.draft?.hitSoundAsset, + floatingWords: woodenFishSession.draft?.floatingWords, + }, + ); + setWoodenFishSession(response.session); + setWoodenFishWork(response.work ?? woodenFishWork); + setWoodenFishGenerationState( + createReadyWoodenFishGenerationState(generationState), + ); + setSelectionStage('wooden-fish-result'); + } catch (error) { + const errorMessage = resolveRpgCreationErrorMessage( + error, + actionType === 'regenerate-hit-object' + ? '重新生成敲击物图案失败。' + : '生成敲击音效失败。', + ); + setWoodenFishError(errorMessage); + setWoodenFishGenerationState( + resolveFinishedMiniGameDraftGenerationState( + generationState, + 'failed', + { error: errorMessage }, + ), + ); + } finally { + setIsWoodenFishBusy(false); + } + }, + [ + createReadyWoodenFishGenerationState, + setSelectionStage, + woodenFishSession?.draft, + woodenFishSession?.sessionId, + woodenFishWork, + ], + ); + + const publishWoodenFishDraft = useCallback(async () => { + const profileId = woodenFishWork?.summary.profileId?.trim(); + if (!profileId) { + setWoodenFishError('敲木鱼草稿尚未生成可发布作品。'); + setSelectionStage('wooden-fish-result'); + return; + } + + setIsWoodenFishBusy(true); + setWoodenFishError(null); + try { + const response = await woodenFishClient.publishWork(profileId); + setWoodenFishWork(response.item); + openPublishShareModal({ + title: response.item.summary.workTitle || '敲木鱼', + publicWorkCode: buildWoodenFishPublicWorkCode( + response.item.summary.profileId, + ), + stage: 'work-detail', + }); + } catch (error) { + setWoodenFishError( + resolveRpgCreationErrorMessage(error, '发布敲木鱼作品失败。'), + ); + setSelectionStage('wooden-fish-result'); + } finally { + setIsWoodenFishBusy(false); + } + }, [ + openPublishShareModal, + setSelectionStage, + woodenFishWork?.summary.profileId, + ]); + + const startWoodenFishTestRunFromProfile = useCallback(async () => { + const profileId = woodenFishWork?.summary.profileId?.trim(); + if (!profileId) { + setWoodenFishError('敲木鱼草稿尚未生成可试玩作品。'); + setSelectionStage('wooden-fish-result'); + return; + } + + setIsWoodenFishBusy(true); + setWoodenFishError(null); + setWoodenFishRuntimeReturnStage('wooden-fish-result'); + try { + const response = await woodenFishClient.startRun(profileId); + setWoodenFishRun(response.run); + setSelectionStage('wooden-fish-runtime'); + } catch (error) { + setWoodenFishError( + error instanceof Error ? error.message : '启动敲木鱼试玩失败。', + ); + setSelectionStage('wooden-fish-result'); + } finally { + setIsWoodenFishBusy(false); + } + }, [setSelectionStage, woodenFishWork?.summary.profileId]); + + const startWoodenFishRunFromProfile = useCallback( + async ( + profileId: string, + options: { + embedded?: boolean; + returnStage?: 'work-detail' | 'platform'; + } = {}, + ) => { + const normalizedProfileId = profileId.trim(); + if (!normalizedProfileId) { + setWoodenFishError('当前敲木鱼作品信息不完整,暂时无法进入玩法。'); + return false; + } + + setIsWoodenFishBusy(true); + setWoodenFishError(null); + setWoodenFishRuntimeReturnStage(options.returnStage ?? 'work-detail'); + try { + const [detail, runResponse] = await Promise.all([ + woodenFishClient.getWorkDetail(normalizedProfileId).catch(() => null), + woodenFishClient.startRun(normalizedProfileId), + ]); + if (detail?.item) { + setWoodenFishWork(detail.item); + } + setWoodenFishRun(runResponse.run); + if (!options.embedded) { + setSelectionStage('wooden-fish-runtime'); + pushAppHistoryPath( + buildPublicWorkStagePath( + 'wooden-fish-runtime', + buildWoodenFishPublicWorkCode(normalizedProfileId), + ), + ); + } + return true; + } catch (error) { + setWoodenFishError( + resolveRpgCreationErrorMessage(error, '启动敲木鱼玩法失败。'), + ); + return false; + } finally { + setIsWoodenFishBusy(false); + } + }, + [setSelectionStage], + ); + + const restartWoodenFishRuntimeRun = useCallback(async () => { + const profileId = + woodenFishRun?.profileId?.trim() ?? + woodenFishWork?.summary.profileId?.trim(); + if (!profileId) { + await startWoodenFishTestRunFromProfile(); + return; + } + + setIsWoodenFishBusy(true); + setWoodenFishError(null); + try { + const response = await woodenFishClient.startRun(profileId); + setWoodenFishRun(response.run); + } catch (error) { + setWoodenFishError( + resolveRpgCreationErrorMessage(error, '重新开始敲木鱼失败。'), + ); + } finally { + setIsWoodenFishBusy(false); + } + }, [ + startWoodenFishTestRunFromProfile, + woodenFishRun?.profileId, + woodenFishWork?.summary.profileId, + ]); + + const checkpointWoodenFishRuntimeRun = useCallback( + async (payload: { + totalTapCount: number; + wordCounters: WoodenFishRunResponse['run']['wordCounters']; + }) => { + const runId = woodenFishRun?.runId; + if (!runId) { + return; + } + const response = await woodenFishClient.checkpointRun(runId, payload); + setWoodenFishRun(response.run); + }, + [woodenFishRun?.runId], + ); + + const finishWoodenFishRuntimeRun = useCallback( + async (payload: { + totalTapCount: number; + wordCounters: WoodenFishRunResponse['run']['wordCounters']; + }) => { + const runId = woodenFishRun?.runId; + if (!runId) { + return; + } + setIsWoodenFishBusy(true); + setWoodenFishError(null); + try { + const response = await woodenFishClient.finishRun(runId, payload); + setWoodenFishRun(response.run); + } catch (error) { + setWoodenFishError( + resolveRpgCreationErrorMessage(error, '结束敲木鱼失败。'), + ); + } finally { + setIsWoodenFishBusy(false); + } + }, + [woodenFishRun?.runId], + ); + const executePuzzleAction = puzzleFlow.executeAction; const executePuzzleBackgroundAction = useCallback( @@ -9668,6 +10121,28 @@ export function PlatformEntryFlowShellImpl({ [openPublicWorkDetail, setJumpHopError, setSelectionStage], ); + const openWoodenFishPublicWorkDetail = useCallback( + async (profileId: string) => { + setIsPublicWorkDetailBusy(true); + setWoodenFishError(null); + setPublicWorkDetailError(null); + setSelectionStage('work-detail'); + + try { + const detail = await woodenFishClient.getWorkDetail(profileId); + setWoodenFishWork(detail.item); + openPublicWorkDetail(mapWoodenFishWorkToPublicWorkDetail(detail.item)); + } catch (error) { + setPublicWorkDetailError( + resolveRpgCreationErrorMessage(error, '读取敲木鱼详情失败。'), + ); + } finally { + setIsPublicWorkDetailBusy(false); + } + }, + [openPublicWorkDetail, setSelectionStage], + ); + const openPublicGalleryDetail = useCallback( (entry: PlatformPublicGalleryCard) => { if (isBigFishGalleryEntry(entry)) { @@ -9697,6 +10172,11 @@ export function PlatformEntryFlowShellImpl({ return; } + if (isWoodenFishGalleryEntry(entry)) { + void openWoodenFishPublicWorkDetail(entry.profileId); + return; + } + if (isVisualNovelGalleryEntry(entry)) { void openVisualNovelPublicWorkDetail(entry.profileId); return; @@ -9718,6 +10198,7 @@ export function PlatformEntryFlowShellImpl({ openPuzzlePublicWorkDetail, openPublicWorkDetail, openJumpHopPublicWorkDetail, + openWoodenFishPublicWorkDetail, openRpgPublicWorkDetail, openVisualNovelPublicWorkDetail, platformBootstrap.platformTab, @@ -10466,6 +10947,14 @@ export function PlatformEntryFlowShellImpl({ return; } + if (isWoodenFishGalleryEntry(selectedPublicWorkDetail)) { + setPublicWorkDetailError(null); + void startWoodenFishRunFromProfile(selectedPublicWorkDetail.profileId, { + returnStage: 'work-detail', + }); + return; + } + if (isMatch3DGalleryEntry(selectedPublicWorkDetail)) { const work = mapPublicWorkDetailToMatch3DWork(selectedPublicWorkDetail); if (!work) { @@ -10574,6 +11063,7 @@ export function PlatformEntryFlowShellImpl({ startBarkBattleRunFromWork, startBigFishRunFromWork, startJumpHopRunFromProfile, + startWoodenFishRunFromProfile, startPuzzleRunFromProfile, startMatch3DRunFromProfile, startSquareHoleRunFromProfile, @@ -10633,6 +11123,11 @@ export function PlatformEntryFlowShellImpl({ embedded: true, returnStage: 'platform', }); + } else if (isWoodenFishGalleryEntry(entry)) { + started = await startWoodenFishRunFromProfile(entry.profileId, { + embedded: true, + returnStage: 'platform', + }); } else if (isMatch3DGalleryEntry(entry)) { const work = mapPublicWorkDetailToMatch3DWork(entry); if (!work) { @@ -10725,6 +11220,7 @@ export function PlatformEntryFlowShellImpl({ startBarkBattleRunFromWork, startBigFishRunFromWork, startJumpHopRunFromProfile, + startWoodenFishRunFromProfile, startMatch3DRunFromProfile, startPuzzleRunFromProfile, startSquareHoleRunFromProfile, @@ -10973,6 +11469,25 @@ export function PlatformEntryFlowShellImpl({ ); } + if (activeRecommendRuntimeKind === 'wooden-fish') { + return ( + { + setActiveRecommendRuntimeKind(null); + }} + onRestart={() => { + void restartWoodenFishRuntimeRun(); + }} + onCheckpoint={checkpointWoodenFishRuntimeRun} + onFinish={finishWoodenFishRuntimeRun} + /> + ); + } + if (activeRecommendRuntimeKind === 'square-hole') { return ( { @@ -11190,6 +11712,7 @@ export function PlatformEntryFlowShellImpl({ puzzleRun, squareHoleRun, visualNovelRun, + woodenFishRun, }); if ( (activeRecommendEntry !== null && isActiveRecommendRuntimeReady) || @@ -11221,6 +11744,7 @@ export function PlatformEntryFlowShellImpl({ selectionStage, squareHoleRun, visualNovelRun, + woodenFishRun, ]); const remixPublicWork = useCallback( @@ -11289,6 +11813,12 @@ export function PlatformEntryFlowShellImpl({ return; } + if (isWoodenFishGalleryEntry(entry)) { + setPublicWorkDetailError('敲木鱼作品改造将在后续版本开放。'); + setIsPublicWorkDetailBusy(false); + return; + } + if (isVisualNovelGalleryEntry(entry)) { setPublicWorkDetailError('视觉小说作品改造将在后续版本开放。'); setIsPublicWorkDetailBusy(false); @@ -11410,6 +11940,11 @@ export function PlatformEntryFlowShellImpl({ return; } + if (isWoodenFishGalleryEntry(entry)) { + setPublicWorkDetailError('这份敲木鱼作品暂时请从作品架编辑。'); + return; + } + if (isVisualNovelGalleryEntry(entry)) { const matchedWork = visualNovelWorks.find( (work) => work.profileId === entry.profileId, @@ -11513,6 +12048,7 @@ export function PlatformEntryFlowShellImpl({ const shouldSearchBigFishFirst = upperKeyword.startsWith('BF'); const shouldSearchBabyObjectFirst = upperKeyword.startsWith('BO'); const shouldSearchJumpHopFirst = upperKeyword.startsWith('JH'); + const shouldSearchWoodenFishFirst = upperKeyword.startsWith('WF'); const shouldSearchMatch3DFirst = upperKeyword.startsWith('M3'); const shouldSearchPuzzleFirst = upperKeyword.startsWith('PZ'); const shouldSearchSquareHoleFirst = upperKeyword.startsWith('SH'); @@ -11523,6 +12059,7 @@ export function PlatformEntryFlowShellImpl({ !shouldSearchBabyObjectFirst && !shouldSearchBigFishFirst && !shouldSearchJumpHopFirst && + !shouldSearchWoodenFishFirst && !shouldSearchMatch3DFirst && !shouldSearchPuzzleFirst && !shouldSearchSquareHoleFirst && @@ -11536,6 +12073,7 @@ export function PlatformEntryFlowShellImpl({ !shouldSearchBigFishFirst && !shouldSearchBabyObjectFirst && !shouldSearchJumpHopFirst && + !shouldSearchWoodenFishFirst && !shouldSearchMatch3DFirst && !shouldSearchPuzzleFirst && !shouldSearchSquareHoleFirst && @@ -11632,6 +12170,25 @@ export function PlatformEntryFlowShellImpl({ openPublicWorkDetail(mapJumpHopWorkToPublicWorkDetail(matchedEntry)); }; + const tryOpenWoodenFishGalleryEntry = async () => { + const entries = + woodenFishGalleryEntries.length > 0 + ? woodenFishGalleryEntries + : await refreshWoodenFishGallery(); + const matchedEntry = entries.find((entry) => { + const detailEntry = mapWoodenFishWorkToPublicWorkDetail(entry); + return ( + canExposePublicWork(detailEntry) && + isSameWoodenFishPublicWorkCode(normalizedKeyword, entry.profileId) + ); + }); + + if (!matchedEntry) { + throw new Error('未找到敲木鱼作品。'); + } + + openPublicWorkDetail(mapWoodenFishWorkToPublicWorkDetail(matchedEntry)); + }; const tryOpenMatch3DGalleryEntry = async () => { const entries = match3dGalleryEntries.length > 0 @@ -11765,6 +12322,11 @@ export function PlatformEntryFlowShellImpl({ return; } + if (shouldSearchWoodenFishFirst) { + await tryOpenWoodenFishGalleryEntry(); + return; + } + if (shouldSearchBabyObjectFirst) { await tryOpenBabyObjectMatchGalleryEntry(); return; @@ -11865,6 +12427,7 @@ export function PlatformEntryFlowShellImpl({ refreshBarkBattleGallery, refreshBigFishGallery, refreshJumpHopGallery, + refreshWoodenFishGallery, refreshPuzzleGallery, refreshSquareHoleGallery, refreshVisualNovelGallery, @@ -11875,6 +12438,7 @@ export function PlatformEntryFlowShellImpl({ setSelectionStage, startBarkBattleRunFromWork, visualNovelGalleryEntries, + woodenFishGalleryEntries, ], ); @@ -11949,6 +12513,19 @@ export function PlatformEntryFlowShellImpl({ return; } + if ( + worldType === 'wooden-fish' || + worldType === 'wooden_fish' || + work.worldKey.startsWith('wooden-fish:') + ) { + const profileId = + work.profileId ?? work.worldKey.replace(/^wooden-fish:/u, ''); + if (profileId) { + void openWoodenFishPublicWorkDetail(profileId); + } + return; + } + if ( worldType === 'big_fish' || worldType === 'big-fish' || @@ -12030,6 +12607,7 @@ export function PlatformEntryFlowShellImpl({ openPuzzlePublicWorkDetail, openPublicWorkDetail, openJumpHopPublicWorkDetail, + openWoodenFishPublicWorkDetail, openRpgPublicWorkDetail, openSquareHolePublicWorkDetail, refreshBigFishGallery, @@ -12057,6 +12635,7 @@ export function PlatformEntryFlowShellImpl({ void refreshBigFishGallery(); } void refreshJumpHopGallery(); + void refreshWoodenFishGallery(); void refreshMatch3DGallery(); void refreshPuzzleGallery(); void refreshBarkBattleGallery(); @@ -12073,6 +12652,7 @@ export function PlatformEntryFlowShellImpl({ isVisualNovelCreationOpen, refreshBigFishGallery, refreshJumpHopGallery, + refreshWoodenFishGallery, refreshMatch3DGallery, refreshPuzzleGallery, refreshBarkBattleGallery, @@ -12186,6 +12766,7 @@ export function PlatformEntryFlowShellImpl({ setPuzzleError(null); setVisualNovelError(null); setBabyObjectMatchError(null); + setWoodenFishError(null); void platformBootstrap.refreshCustomWorldWorks().catch((error) => { platformBootstrap.setPlatformError( resolveRpgCreationErrorMessage(error, '读取创作作品列表失败。'), @@ -12211,6 +12792,7 @@ export function PlatformEntryFlowShellImpl({ bigFishError ?? match3dError ?? (isSquareHoleCreationVisible ? squareHoleError : null) ?? + woodenFishError ?? puzzleCreationError ?? puzzleError ?? (isVisualNovelCreationOpen ? visualNovelError : null) ?? @@ -12224,6 +12806,7 @@ export function PlatformEntryFlowShellImpl({ isBigFishBusy || isMatch3DBusy || (isSquareHoleCreationVisible && isSquareHoleBusy) || + isWoodenFishBusy || isPuzzleBusy || (isVisualNovelCreationOpen && isVisualNovelBusy) || (isVisualNovelCreationOpen && isVisualNovelStreamingReply) || @@ -12503,6 +13086,19 @@ export function PlatformEntryFlowShellImpl({ }} /> + ) : activeCreationFormType === 'wooden-fish' ? ( + } + > + { + void compileWoodenFishSession(result, payload); + }} + /> + ) : ( } @@ -12595,6 +13191,7 @@ export function PlatformEntryFlowShellImpl({ onOpenCreateWorld={openCreationTypePicker} onOpenCreateTypePicker={openCreationTypePicker} onOpenGalleryDetail={openPublicGalleryDetail} + onOpenChildMotionDemo={startChildMotionDemo} onOpenBabyLoveDrawing={startBabyLoveDrawingRuntime} onOpenRecommendGalleryDetail={openRecommendGalleryDetail} recommendRuntimeContent={recommendRuntimeContent} @@ -12605,7 +13202,8 @@ export function PlatformEntryFlowShellImpl({ isPuzzleBusy || isMatch3DBusy || isSquareHoleBusy || - isVisualNovelBusy + isVisualNovelBusy || + isWoodenFishBusy } recommendRuntimeError={activeRecommendRuntimeError} onSelectNextRecommendEntry={() => @@ -13757,6 +14355,137 @@ export function PlatformEntryFlowShellImpl({ )} + {selectionStage === 'wooden-fish-workspace' && ( + + } + > + { + void compileWoodenFishSession(result, payload); + }} + /> + + + )} + + {selectionStage === 'wooden-fish-generating' && ( + + } + > + { + setSelectionStage('wooden-fish-workspace'); + }} + onRetry={retryWoodenFishDraftGeneration} + onInterrupt={undefined} + backLabel="返回创作中心" + settingActionLabel={null} + retryLabel="重新生成草稿" + settingTitle="当前敲木鱼信息" + settingDescription={null} + progressTitle="敲木鱼草稿生成进度" + activeBadgeLabel="素材生成中" + pausedBadgeLabel="素材生成已暂停" + idleBadgeLabel="等待返回工作区" + /> + + + )} + + {selectionStage === 'wooden-fish-result' && + woodenFishSession?.draft && ( + + } + > + { + setSelectionStage('wooden-fish-workspace'); + }} + onStartTestRun={startWoodenFishTestRunFromProfile} + onPublish={publishWoodenFishDraft} + onRegenerateHitObject={() => { + void regenerateWoodenFishAsset('regenerate-hit-object'); + }} + onGenerateHitSound={() => { + void regenerateWoodenFishAsset('generate-hit-sound'); + }} + /> + + + )} + + {selectionStage === 'wooden-fish-runtime' && ( + + } + > + { + setSelectionStage(woodenFishRuntimeReturnStage); + }} + onRestart={() => { + void restartWoodenFishRuntimeRun(); + }} + onCheckpoint={checkpointWoodenFishRuntimeRun} + onFinish={finishWoodenFishRuntimeRun} + /> + + + )} + {selectionStage === 'creative-agent-workspace' && ( { handleCreationHubCreateType('jump-hop'); }} + onSelectWoodenFish={() => { + handleCreationHubCreateType('wooden-fish'); + }} onSelectCreativeAgent={() => { runProtectedAction(() => { void openCreativeAgentWorkspace(); diff --git a/src/components/platform-entry/platformEntryCreationTypes.test.ts b/src/components/platform-entry/platformEntryCreationTypes.test.ts index e4a7ed94..00edf621 100644 --- a/src/components/platform-entry/platformEntryCreationTypes.test.ts +++ b/src/components/platform-entry/platformEntryCreationTypes.test.ts @@ -182,3 +182,29 @@ test('edutainment switch hides baby object match creation entry from database co getVisiblePlatformCreationTypes(hiddenCards).map((item) => item.id), ).toEqual(['puzzle']); }); + +test('baby object match entry is visible and open when database marks it creatable', () => { + const cards = derivePlatformCreationTypes([ + { + id: 'baby-object-match', + title: '宝贝识物', + subtitle: '亲子识物分类', + badge: '可创建', + imageSrc: '/child-motion-demo/picture-book-grass-stage.png', + visible: true, + open: true, + sortOrder: 90, + updatedAtMicros: 1, + }, + ]); + + expect(getVisiblePlatformCreationTypes(cards)).toEqual([ + expect.objectContaining({ + id: 'baby-object-match', + hidden: false, + locked: false, + }), + ]); + expect(isPlatformCreationTypeVisible(cards, 'baby-object-match')).toBe(true); + expect(isPlatformCreationTypeOpen(cards, 'baby-object-match')).toBe(true); +}); diff --git a/src/components/platform-entry/platformEntryTypes.ts b/src/components/platform-entry/platformEntryTypes.ts index a3ca2f11..7820728f 100644 --- a/src/components/platform-entry/platformEntryTypes.ts +++ b/src/components/platform-entry/platformEntryTypes.ts @@ -39,6 +39,10 @@ export type SelectionStage = | 'bark-battle-generating' | 'bark-battle-result' | 'bark-battle-runtime' + | 'wooden-fish-workspace' + | 'wooden-fish-generating' + | 'wooden-fish-result' + | 'wooden-fish-runtime' | 'creative-agent-workspace' | 'visual-novel-agent-workspace' | 'visual-novel-generating' diff --git a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx index 371306b4..ab7a6f1d 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx @@ -761,6 +761,7 @@ function renderLoggedOutHomeView( | 'latestEntries' | 'onOpenGalleryDetail' | 'onOpenRecommendGalleryDetail' + | 'onOpenChildMotionDemo' | 'onSearchPublicCode' | 'recommendRuntimeContent' | 'activeRecommendEntryKey' @@ -814,6 +815,7 @@ function renderLoggedOutHomeView( onOpenCreateWorld={vi.fn()} onOpenCreateTypePicker={vi.fn()} onOpenGalleryDetail={overrides.onOpenGalleryDetail ?? vi.fn()} + onOpenChildMotionDemo={overrides.onOpenChildMotionDemo} onOpenRecommendGalleryDetail={overrides.onOpenRecommendGalleryDetail} recommendRuntimeContent={ overrides.recommendRuntimeContent ?? ( @@ -912,6 +914,7 @@ function renderStatefulLoggedOutHomeView( | 'latestEntries' | 'onOpenGalleryDetail' | 'onOpenRecommendGalleryDetail' + | 'onOpenChildMotionDemo' | 'onSearchPublicCode' | 'recommendRuntimeContent' | 'activeRecommendEntryKey' @@ -970,6 +973,7 @@ function renderStatefulLoggedOutHomeView( onOpenCreateWorld={vi.fn()} onOpenCreateTypePicker={vi.fn()} onOpenGalleryDetail={overrides.onOpenGalleryDetail ?? vi.fn()} + onOpenChildMotionDemo={overrides.onOpenChildMotionDemo} onOpenRecommendGalleryDetail={overrides.onOpenRecommendGalleryDetail} recommendRuntimeContent={ overrides.recommendRuntimeContent ?? ( @@ -2214,6 +2218,7 @@ test('discover search fuzzy matches public work id, name, author and description test('mobile discover keeps edutainment works in the last dedicated channel only', async () => { const user = userEvent.setup(); const onSearchPublicCode = vi.fn(); + const onOpenChildMotionDemo = vi.fn(); const generalEntry = buildTaggedPuzzleEntry('normal01', '普通拼图作品', [ '儿童教育', ]); @@ -2234,6 +2239,7 @@ test('mobile discover keeps edutainment works in the last dedicated channel only renderStatefulLoggedOutHomeView({ latestEntries: [edutainmentEntry, generalEntry], + onOpenChildMotionDemo, onSearchPublicCode, }); await user.click(screen.getByRole('button', { name: '发现' })); @@ -2266,6 +2272,12 @@ test('mobile discover keeps edutainment works in the last dedicated channel only name: /儿童动作热身 Demo/u, }), ).toBeTruthy(); + const warmupButton = within(discoverPanel).getByRole('button', { + name: /热身关卡/u, + }); + expect(warmupButton).toBeTruthy(); + await user.click(warmupButton); + expect(onOpenChildMotionDemo).toHaveBeenCalledTimes(1); expect(within(discoverPanel).queryByText('普通拼图作品')).toBeNull(); const searchInput = @@ -2276,6 +2288,23 @@ test('mobile discover keeps edutainment works in the last dedicated channel only expect(onSearchPublicCode).not.toHaveBeenCalled(); }); +test('desktop discover shows child motion demo in edutainment channel', async () => { + mockDesktopLayout(); + const user = userEvent.setup(); + const onOpenChildMotionDemo = vi.fn(); + + renderStatefulLoggedOutHomeView({ + onOpenChildMotionDemo, + }); + await user.click(screen.getByRole('button', { name: '发现' })); + await user.click(screen.getByRole('button', { name: '寓教于乐' })); + + const warmupButton = screen.getByRole('button', { name: /热身关卡/u }); + expect(warmupButton).toBeTruthy(); + await user.click(warmupButton); + expect(onOpenChildMotionDemo).toHaveBeenCalledTimes(1); +}); + test('mobile discover hides edutainment channel and work when switch is disabled', async () => { vi.stubEnv('VITE_ENABLE_EDUTAINMENT_ENTRY', 'false'); const user = userEvent.setup(); diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 88fd8eba..881fb5d7 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -136,6 +136,7 @@ import { isPuzzleGalleryEntry, isSquareHoleGalleryEntry, isVisualNovelGalleryEntry, + isWoodenFishGalleryEntry, type PlatformPublicGalleryCard, type PlatformWorldCardLike, resolvePlatformPublicWorkCode, @@ -173,6 +174,7 @@ export interface RpgEntryHomeViewProps { onOpenCreateWorld: () => void; onOpenCreateTypePicker: () => void; onOpenGalleryDetail: (entry: PlatformPublicGalleryCard) => void; + onOpenChildMotionDemo?: () => void; onOpenBabyLoveDrawing?: () => void; onOpenRecommendGalleryDetail?: (entry: PlatformPublicGalleryCard) => void; recommendRuntimeContent?: ReactNode; @@ -326,6 +328,11 @@ const BABY_LOVE_DRAWING_DEFAULT_CARD = { subtitle: '空白画板', summary: '挥动小手画一张画。', }; +const CHILD_MOTION_DEMO_DEFAULT_CARD = { + title: '热身关卡', + subtitle: '动作识别热身', + summary: '站位、招手和左右手活动。', +}; const PLATFORM_RANKING_TABS: Array<{ id: PlatformRankingTab; @@ -1896,26 +1903,35 @@ async function getPublicWorkAuthorSummary( } function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) { - const kind = isBigFishGalleryEntry(entry) - ? '大鱼' - : isPuzzleGalleryEntry(entry) - ? '拼图' - : isMatch3DGalleryEntry(entry) - ? '抓鹅' - : isSquareHoleGalleryEntry(entry) - ? '方洞' - : isVisualNovelGalleryEntry(entry) - ? '视觉' - : isBarkBattleGalleryEntry(entry) - ? '汪汪' - : isEdutainmentGalleryEntry(entry) - ? entry.templateName - : isJumpHopGalleryEntry(entry) - ? '跳一跳' - : describePlatformThemeLabel(entry.themeMode); - return formatPlatformWorkDisplayTag(kind); + if (isBigFishGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('??'); + } + if (isPuzzleGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('??'); + } + if (isMatch3DGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('??'); + } + if (isSquareHoleGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('??'); + } + if (isJumpHopGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('???'); + } + if (isWoodenFishGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('???'); + } + if (isVisualNovelGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('??'); + } + if (isBarkBattleGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('??'); + } + if (isEdutainmentGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag(entry.templateName); + } + return formatPlatformWorkDisplayTag(describePlatformThemeLabel(entry.themeMode)); } - function getPublicAuthorAvatarLabel(authorDisplayName: string) { return Array.from(authorDisplayName.trim() || '玩')[0] ?? '玩'; } @@ -3767,6 +3783,7 @@ export function RpgEntryHomeView({ onResumeSave, onOpenCreateTypePicker, onOpenGalleryDetail, + onOpenChildMotionDemo, onOpenBabyLoveDrawing, onOpenRecommendGalleryDetail, recommendRuntimeContent, @@ -5477,7 +5494,9 @@ export function RpgEntryHomeView({
{isLoadingPlatform ? ( - ) : edutainmentFeedEntries.length > 0 || onOpenBabyLoveDrawing ? ( + ) : edutainmentFeedEntries.length > 0 || + onOpenChildMotionDemo || + onOpenBabyLoveDrawing ? (
{edutainmentFeedEntries.map((entry) => { const cardKey = buildPublicGalleryCardKey(entry); @@ -5493,6 +5512,24 @@ export function RpgEntryHomeView({ /> ); })} + {onOpenChildMotionDemo ? ( + + ) : null} {onOpenBabyLoveDrawing ? ( + ) : null} {onOpenBabyLoveDrawing ? ( + ) : null} +
+