feat(edutainment): refresh baby object match flow
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
# 宝贝识物创作发布实现方案 2026-05-11
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本方案对应第 2 线程:创作发布线程。
|
||||
|
||||
本线程落地:
|
||||
|
||||
1. 创作入口配置;
|
||||
2. 模板表单;
|
||||
3. 本地草稿生成 service;
|
||||
4. 结果页;
|
||||
5. 发布 payload 约束;
|
||||
6. 本地 Demo 运行态;
|
||||
7. 后端 image-2 / 作品持久化 / 运行态接口预留形状。
|
||||
|
||||
本阶段运行态先做浏览器本地 Demo,并消费现有本地 mocap 动作数据源;正式硬件接口和摄像头调教在后续接口稳定后继续接入。
|
||||
|
||||
## 2. 前端接入点
|
||||
|
||||
新增玩法 ID:
|
||||
|
||||
```text
|
||||
baby-object-match
|
||||
```
|
||||
|
||||
用户展示名:
|
||||
|
||||
```text
|
||||
宝贝识物
|
||||
```
|
||||
|
||||
工程接入文件:
|
||||
|
||||
1. `server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs`
|
||||
2. `src/components/platform-entry/platformEntryCreationTypes.ts`
|
||||
3. `src/components/platform-entry/PlatformEntryCreationTypeModal.tsx`
|
||||
4. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
|
||||
`src/config/newWorkEntryConfig.ts` 已迁移删除,不再作为入口事实源。`baby-object-match` 必须存在于 SpacetimeDB `creation_entry_type_config` 默认种子中,默认展示名为 `宝贝识物`、`visible=true`、`open=true`、`sortOrder=90`;前端只通过 `GET /api/creation-entry/config` 读取后端配置并在 `platformEntryCreationTypes.ts` 做展示派生。
|
||||
|
||||
`baby-object-match` 必须复用 `VITE_ENABLE_EDUTAINMENT_ENTRY` 开关;开关关闭时,创作类型弹层不展示 `宝贝识物`,创作页作品架不展示本地宝贝识物草稿或已发布作品卡,公开发现、搜索、详情、作品号和浏览历史也继续完全不可见。
|
||||
|
||||
新增阶段:
|
||||
|
||||
```text
|
||||
baby-object-match-workspace
|
||||
baby-object-match-generating
|
||||
baby-object-match-result
|
||||
baby-object-match-runtime
|
||||
```
|
||||
|
||||
## 3. 契约
|
||||
|
||||
前端共享契约放在:
|
||||
|
||||
```text
|
||||
packages/shared/src/contracts/edutainmentBabyObject.ts
|
||||
```
|
||||
|
||||
核心字段:
|
||||
|
||||
1. `BabyObjectMatchDraft.templateId = "baby-object-match"`;
|
||||
2. `BabyObjectMatchDraft.templateName = "宝贝识物"`;
|
||||
3. `BabyObjectMatchDraft.themeTags` 必须包含精确 `寓教于乐`;
|
||||
4. `BabyObjectMatchItemAsset.generationProvider` 首版允许为 `vector-engine-gpt-image-2` 或 `placeholder`;
|
||||
5. `BabyObjectMatchDraft.visualPackage` 可选承载背景环境、礼物盒和篮子三类必需视觉资源;历史草稿中的 `ui-frame`、`smoke-puff`、`left-hand` 与 `right-hand` 仅保留运行态兼容读取或忽略;
|
||||
6. `BabyObjectMatchPublishRequest.draft.themeTags` 发布前必须归一化补齐 `寓教于乐`。
|
||||
|
||||
## 4. Service 边界
|
||||
|
||||
前端 service 放在:
|
||||
|
||||
```text
|
||||
src/services/edutainment-baby-object/babyObjectMatchClient.ts
|
||||
```
|
||||
|
||||
首版提供:
|
||||
|
||||
1. `createBabyObjectMatchDraft(payload)`;
|
||||
2. `saveBabyObjectMatchDraft(draft)`;
|
||||
3. `publishBabyObjectMatchWork(payload)`;
|
||||
4. `deleteLocalBabyObjectMatchDraft(profileId)`;
|
||||
5. `regenerateBabyObjectMatchDraftAssets(draft)`;
|
||||
6. `hasBabyObjectMatchPlaceholderAssets(draft)`。
|
||||
|
||||
当前后端正式作品持久化接口未在本线程扩表落地,因此 service 仍使用本地 Demo 存储草稿和发布状态。由于 image-2 会返回多张 base64 PNG 大图,本地 Demo 草稿必须优先写入 IndexedDB `genarrative-edutainment-baby-object-drafts/drafts`,不得把完整草稿 JSON 写入 `localStorage`;`localStorage` 仅作为旧版小草稿迁移读取来源,读取后迁移到 IndexedDB 并清理旧 key,避免触发浏览器 `Storage` 配额错误。
|
||||
|
||||
物品图片生成已接入后端 image-2 接口:
|
||||
|
||||
```text
|
||||
POST /api/creation/edutainment/baby-object-match/assets
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"itemNames": ["苹果", "香蕉"]
|
||||
}
|
||||
```
|
||||
|
||||
响应体:
|
||||
|
||||
```json
|
||||
{
|
||||
"assets": [
|
||||
{
|
||||
"itemId": "baby-object-item-1",
|
||||
"itemName": "苹果",
|
||||
"imageSrc": "data:image/png;base64,...",
|
||||
"assetObjectId": null,
|
||||
"generationProvider": "vector-engine-gpt-image-2",
|
||||
"prompt": "..."
|
||||
}
|
||||
],
|
||||
"visualPackage": {
|
||||
"themePrompt": "...",
|
||||
"assets": [
|
||||
{
|
||||
"assetId": "baby-object-visual-background",
|
||||
"assetKind": "background",
|
||||
"imageSrc": "data:image/png;base64,...",
|
||||
"assetObjectId": null,
|
||||
"generationProvider": "vector-engine-gpt-image-2",
|
||||
"prompt": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
该接口返回从同一张 `2x2` 素材 sheet 切出的两个物品透明 PNG、礼物盒透明 PNG、篮子透明 PNG,以及单独生成的一张背景环境图。本地 Demo 阶段暂不写入 OSS 或 SpacetimeDB `asset_object`。当前创作链路必须真实拿到 `generationProvider = "vector-engine-gpt-image-2"` 的两个物品图和必需视觉主题包后才允许进入结果页;若本地未配置 VectorEngine、登录态失效、接口返回 401/5xx、上游生成失败或响应缺少任一必需资源,前端 service 必须抛出错误并停留在生成失败状态,不得静默回退到占位图。左右手位置指示器是运行态默认静态素材,不在该接口中生成。
|
||||
|
||||
为了降低 image-2 调用成本,一次创作只发起两次图片生成:一次 `1024x1024` 的 `2x2` 素材 sheet,固定包含左上物品 A、右上物品 B、左下篮子、右下礼物盒;一次 `1536x1024` 的场景背景图。前端 `babyObjectMatchClient` 对该 POST 使用 10 分钟请求超时,且不做自动重试,避免第一次生成仍在后端执行时又发起第二次重复生成。后端并发启动两张图生成,并把该路由的 VectorEngine 单图请求等待预算提升到至少 8 分钟,避免某张图 3 分钟附近仍在生成时被后端提前断开。后端日志记录资源开始、完成和耗时,排查时优先按同一次 HTTP 请求查看 `宝贝识物 image-2 2x2 素材 sheet 生成完成`、`宝贝识物 image-2 场景资源生成完成` 与 `VectorEngine 图片生成上游错误`。
|
||||
|
||||
历史本地草稿中若已保存 `generationProvider = "placeholder"` 的旧占位资源,结果页必须提示“重新生成 image-2 资源”,并禁用试玩和发布。用户点击重新生成、发布或试玩前,前端统一调用 `regenerateBabyObjectMatchDraftAssets(draft)` 补齐资源;补齐失败时保留在结果页并展示错误。
|
||||
|
||||
后续正式作品持久化接入时,应补齐:
|
||||
|
||||
```text
|
||||
POST /api/creation/edutainment/baby-object-match/drafts
|
||||
PUT /api/creation/edutainment/baby-object-match/drafts/{draftId}
|
||||
POST /api/creation/edutainment/baby-object-match/drafts/{draftId}/publish
|
||||
```
|
||||
|
||||
图片生成必须在后端调用 VectorEngine `gpt-image-2-all`,不得从前端直接调用外部图片接口。
|
||||
|
||||
后端 `2x2` 素材 sheet prompt 约束:
|
||||
|
||||
1. 锁定寓教于乐板块统一的明亮卡通绘本插画风;
|
||||
2. 固定四格布局:左上格物品 A,右上格物品 B,左下格篮子,右下格礼物盒;
|
||||
3. 两个物品格只能围绕对应关键词生成单一主体,不生成背景、场景、人物、手、篮子、礼物盒、文字、水印或 UI;
|
||||
4. 篮子格只生成一个主体饱满、开口清晰的大号篮子,不放入待分类物品,手柄和篮口镂空处不得留下白底描边或毛边;
|
||||
5. 礼物盒格只生成一个主体饱满、中心构图的大号礼物盒;
|
||||
6. 每格使用纯白或接近纯白背景,不绘制网格线、标签、按钮或边框;
|
||||
7. 服务端按 `2x2` 固定格切图,并按单格边缘采样背景色转透明 PNG,返回的物品、篮子和礼物盒素材必须已完成透明背景后处理;
|
||||
8. 篮子切图在通用透明背景处理后,还必须额外清理近白、低饱和的白底毛边,优先覆盖手柄镂空、篮口镂空和边缘残留白底;该处理仅应用于篮子格,不应用于两个物品格,避免误伤白色物品主体。
|
||||
|
||||
后端场景背景 prompt 约束:
|
||||
|
||||
1. 背景图单独生成,总风格继续锁定寓教于乐明亮卡通绘本插画风;
|
||||
2. 若关键词偏动漫角色、玩具或公仔,背景环境匹配动漫、玩具主题;若关键词偏水果,匹配果园、自然主题;其它关键词按语义匹配合适主题;
|
||||
3. 背景环境图使用非透明横向图,但必须保证中间、中下方和底部左右篮子区域清爽,给放大后的礼物盒、中央物品和左右篮子预留空间;
|
||||
4. 背景图不画入礼物盒、篮子、物品、人物、文字或操作 UI;
|
||||
5. 左右篮子的固定选项规则不受主题包影响,运行态只把 `basket` 作为篮子造型包装复用。
|
||||
|
||||
运行态左右手位置指示器不随创作生成。默认素材保存在 `public/edutainment-baby-object/image2-picture-book-hands/baby-object-left-hand-v8-transparent.png` 与 `public/edutainment-baby-object/image2-picture-book-hands/baby-object-right-hand-v8-transparent.png`,姿势沿用图1的圆形手与斜向手臂结构,并按寓教于乐明亮绘本插画风完成 image2 填色和风格化处理。后续若要替换默认手型,应更新这两个静态资源和运行态 CSS 默认变量,而不是恢复每次创作的左右手 image-2 生成。
|
||||
|
||||
## 5. UI 边界
|
||||
|
||||
工作台只展示两个必填输入和生成按钮。
|
||||
|
||||
结果页只展示草稿核心信息、两个物品、保存草稿、发布、试玩。不在 UI 内写玩法说明长文案。
|
||||
|
||||
移动端优先:表单和结果页使用单列布局,桌面端自然扩展为双列。
|
||||
|
||||
## 6. 运行态边界
|
||||
|
||||
前端运行态放在:
|
||||
|
||||
```text
|
||||
src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx
|
||||
```
|
||||
|
||||
运行态直接消费 `BabyObjectMatchDraft`,必须使用草稿中的两个物品名称和物品图。
|
||||
每轮只随机当前从礼物盒跳出的物品;左右篮子不随机交换,左侧固定为草稿 `itemAssets[0]`,右侧固定为草稿 `itemAssets[1]`。
|
||||
|
||||
若草稿包含 `visualPackage`,运行态通过背景图片层、CSS 变量和图片节点消费:
|
||||
|
||||
1. `background`:作为舞台最底层 `ResolvedAssetImage` 背景图;存在该资源时必须关闭默认草地兜底层,避免生成场景被 CSS 草地遮住或弱化;
|
||||
2. `gift-box`:替换 CSS 礼物盒主体,按旧视觉约 2 倍尺寸展示,只在礼盒入场和打开阶段存在;
|
||||
3. `basket`:替换篮子主体造型,按旧视觉约 1.5 倍尺寸展示,左右两侧复用同一张主题篮子图;
|
||||
4. 左右手位置指示器:始终使用运行态默认静态素材;历史草稿中若带有 `left-hand` / `right-hand` 资源,不再作为视觉包完整性或运行时皮肤来源。
|
||||
|
||||
左右篮子的选项 UI 必须以篮子中心线为基准居中展示:物品图标位于篮子上方,图标下方展示对应物品名称短标签,左侧固定展示草稿第一个物品,右侧固定展示草稿第二个物品。该名称标签是运行态 UI 的一部分,用于后续只看图案或只看名称的玩法变体预留,但当前不新增额外规则。
|
||||
|
||||
历史草稿若包含 `ui-frame` 或 `smoke-puff`,运行态继续兼容读取;新生成链路不再把这两类资源作为必需 image-2 产物。礼物盒打开烟雾特效优先使用 CSS 动效兜底,避免为了单个特效额外增加生图调用。
|
||||
|
||||
旧草稿或接口失败时 `visualPackage = null`,运行态继续使用现有 CSS 绘本风兜底。
|
||||
|
||||
中央物品 UI 与左右篮子上方物品图标必须使用固定正方形槽位,不允许因为生成物品是手机、长条玩具等窄长形状而拉伸外层 UI 框。素材图片在槽位内使用等比 `contain` 缩放,长条形状只缩小主体,不改变圆形 UI 框尺寸。
|
||||
|
||||
首关状态机:
|
||||
|
||||
1. `intro-left-showing`:物品 A 居中展示 2 秒,名称 UI 和字体约为默认大小的 2 倍,不接受动作判定;
|
||||
2. `intro-left-flying`:物品 A 和名称飞入左侧篮子预设位置,飞行过程中名称 UI 和字体恢复为默认大小,不接受动作判定;
|
||||
3. `intro-left-ready`:左侧目标就绪后等待 1 秒,不接受动作判定;
|
||||
4. `intro-right-showing`:物品 B 居中展示 2 秒,名称 UI 和字体约为默认大小的 2 倍,不接受动作判定;
|
||||
5. `intro-right-flying`:物品 B 和名称飞入右侧篮子预设位置,飞行过程中名称 UI 和字体恢复为默认大小,不接受动作判定;
|
||||
6. `intro-right-ready`:右侧目标就绪后等待 1 秒,不接受动作判定;
|
||||
7. `gift-entering`:礼物盒从上方落下入场动画阶段,不接受动作判定;首次进入该状态必须发生在两个目标展示完成后,后续正确反馈结束后直接进入该状态;
|
||||
8. `gift-opening`:礼物盒打开并播放烟雾特效阶段,不接受动作判定;
|
||||
9. `item-appearing`:礼物盒从舞台移除,当前物品从烟雾中出现并停稳,不接受动作判定;
|
||||
10. `active`:物品彻底出现后才开放选篮判定;
|
||||
11. `correct`:展示“真棒”反馈,对应篮筐播放正确特效并停顿,成功次数加 1;特效完全结束后重新进入 `gift-entering`,下一轮礼物盒从上方落下,不重复目标展示;
|
||||
12. `wrong`:展示“再想一想吧”反馈,物品弹回中央;反馈结束后回到 `active`,不重新随机物品;
|
||||
13. `complete`:成功次数达到 20,展示“恭喜你!小朋友!”和按钮。
|
||||
|
||||
动作输入:
|
||||
|
||||
1. 运行态实时展示左右手位置,手部位置来自 `useMocapInput` 的明确左/右手坐标;
|
||||
2. 任意一只手先接触中央物品 UI 后,当前物品绑定到该手并跟随移动;
|
||||
3. 绑定手带物品进入左侧篮子区域时选择左篮,进入右侧篮子区域时选择右篮;
|
||||
4. 正确时沿用“真棒”反馈和对应篮筐特效,错误时物品弹回中央并回到可再次抓取状态;
|
||||
5. 物品被某只手持有时,手部指示器不再压在物品图标中心;左手吸附到当前物品图标左下角,右手吸附到当前物品图标右下角,保持图案主体可读;
|
||||
6. 不再使用“左手固定选左篮、右手固定选右篮”的规则,也不再使用连续横向轨迹阈值直接选篮。
|
||||
|
||||
运行态直接通过 `useMocapInput` 消费本地 mocap WebSocket `/stream`。宝贝识物优先使用 `general.limb_nodes` / `limb_nodes` 里的骨架手腕节点作为左右手指示器、抓取和选篮坐标;若当前帧没有骨架手腕,再回退到每只手的 `wrist` 挂点,最后才回退到 `hand.x / hand.y`。该策略只让 `useMocapInput` 额外暴露 `bodyJoints.leftWrist/rightWrist`,不修改全局掌心派生点规则,避免影响拼图、热身关和其它运行态。选篮不再通过 `wave_left_hand`、`wave_right_hand`、`wave` 等动作名触发;侧别为 `unknown` 的手部轨迹不参与抓取或选篮。动作判定只在 `active` 阶段开放,礼盒入场、礼盒打开、物品出现、正确反馈和错误反馈阶段收到的动作包必须清空持有状态并忽略,不允许跨阶段补判定。当前本地 mocap 输出的 handedness 按摄像头视角标记,宝贝识物运行态必须先换算为用户身体视角:骨架 `rightWrist` / `rightHand.wrist` / `rightHand` 坐标映射玩家左手,骨架 `leftWrist` / `leftHand.wrist` / `leftHand` 坐标映射玩家右手;换算只用于展示和抓取手身份,不再决定只能选择哪一侧篮子。草稿试玩、发布后正式体验和热身关后的本地 Demo 都复用同一个运行态,因此三条入口都必须具备同一套动作控制能力。
|
||||
|
||||
开发者调试输入:
|
||||
|
||||
1. 鼠标左键按下并拖动:映射左手位置;
|
||||
2. 鼠标右键按下并拖动:映射右手位置;
|
||||
3. 调试输入同样必须先触碰中央物品,物品绑定到目标手后,再拖入左侧或右侧篮子完成选择。
|
||||
|
||||
运行态控制按钮不参与调试输入和选篮判定。左上角返回按钮、完成弹层按钮以及后续新增的运行态控制元素,其 `pointerdown` 不得被舞台拖拽逻辑 `preventDefault` 或指针捕获吞掉,保证游戏进行中仍可直接点击返回。
|
||||
|
||||
当前篮子判定仍只认篮子主体附近区域,但在上一版核心区基础上扩大约 50%;命中阈值为左篮 `x <= 0.36 && y >= 0.62`、右篮 `x >= 0.64 && y >= 0.62`,既避免物品尚未贴近篮子主体就提前判定,也避免贴到篮子边缘后仍难以命中。
|
||||
|
||||
运行态不得新增计时、失败次数、分数、体力或难度递增规则。
|
||||
|
||||
音效和语音播报当前只保留接口预留边界,正式语音接口后续接入。
|
||||
|
||||
## 7. 发布约束
|
||||
|
||||
发布前必须执行:
|
||||
|
||||
1. 两个物品名非空;
|
||||
2. 两个物品名对应的 asset 存在;
|
||||
3. 标签补齐精确 `寓教于乐`;
|
||||
4. `publicationStatus` 从 `draft` 变为 `published`。
|
||||
|
||||
发布后首版本地响应返回 `publicWorkCode`,用于分享弹窗;正式后端接入时 public code 生成规则需要纳入统一作品号服务。
|
||||
|
||||
## 8. 热身关衔接
|
||||
|
||||
`/child-motion-demo` 热身完成后的“开始游戏”按钮进入同一个 `BabyObjectMatchRuntimeShell`。
|
||||
|
||||
热身关独立 Demo 没有创作者草稿上下文,因此使用固定本地 Demo 草稿承载两个物品,仅用于热身关后验证首关体验;正式平台体验仍必须从 `宝贝识物` 模板创作发布后进入寓教于乐板块。
|
||||
|
||||
## 9. 验收命令
|
||||
|
||||
```bash
|
||||
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/edutainment-creation/BabyObjectMatchWorkspace.test.tsx src/components/edutainment-result/BabyObjectMatchResultView.test.tsx src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/components/child-motion-demo/ChildMotionWarmupDemo.test.tsx src/services/edutainment-baby-object/babyObjectMatchClient.test.ts
|
||||
cargo test -p api-server edutainment_baby_object --manifest-path server-rs/Cargo.toml
|
||||
npx vitest run src/components/platform-entry/platformEdutainmentVisibility.test.ts src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/custom-world-home/creationWorkShelf.test.ts src/services/useMocapInput.test.ts src/services/child-motion-demo/childMotionDebugInput.test.ts src/routing/appRoutes.test.ts
|
||||
npx eslint src/components/platform-entry/platformEntryCreationTypes.ts src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --ext .ts,.tsx --max-warnings 0
|
||||
npm run check:encoding
|
||||
npm run typecheck
|
||||
npm run build:raw
|
||||
```
|
||||
|
||||
若后续接入真实 Rust API 和 SpacetimeDB 表,再补充 `npm run api-server`、`/healthz`、Rust contract / api-server / spacetime-client 定向测试和 migration 表目录更新。
|
||||
Reference in New Issue
Block a user