feat: 收口角色动作资产发布前端与验证文档

This commit is contained in:
2026-04-23 20:40:03 +08:00
parent 27e84c46a0
commit 349a397888
7 changed files with 1101 additions and 28 deletions

View File

@@ -0,0 +1,315 @@
# 图片、视频、动作外部生成手动验证运行手册
日期:`2026-04-23`
## 1. 文档目的
这份文档用于冻结 `验证清单.md` 第四项“图片、视频、动作的生成要真实走到外部服务的生成服务上,而不是用占位符来敷衍”的验证口径。
本次先解决两个问题:
1. 当前仓库里“真实外部生成链”和“Stage 1 占位兼容链”同时存在,若不先写清楚,很容易把占位产物误记为通过。
2. 现有技术设计文档描述了多条资产链,但没有一份面向人工联调的统一运行手册,导致每次验证都要重新猜入口、猜日志、猜通过标准。
## 2. 当前结论总览
截至 `2026-04-23` 当前代码状态,第 4 项仍不能整体直接判定“已通过”,原因是不同资产链状态不同。
### 2.1 当前已经接入真实外部图片生成的入口
以下入口当前会真实请求外部图片生成服务,而不是只生成本地占位图:
1. `Big Fish` 结果页:
- `生成背景`
- `生成并应用正式图` -> `Lv.x 主图`
- `生成并应用正式图` -> `Lv.x 动作工坊`
2. `custom world / RPG 创作`
- 场景图生成
- 作品封面 AI 生成
这些入口当前统一会走 Rust `api-server`,并向 DashScope 图片生成接口发起请求,再落到 OSS 与兼容读路径。
### 2.2 当前仍未完全闭环的入口
以下入口当前仍不能直接判定为“动作资产全后端闭环”:
1. 角色资产工坊 `image-sequence`
- 当前生成的是服务端 SVG 帧,不是真实外部序列图模型结果。
2. 角色资产工坊 `motion-transfer / reference-to-video`
- 当前仍未接入真实外部模型主链。
3. 角色资产工坊 `image-to-video`
- 当前已真实请求 Ark 生成 OSS 草稿区 `preview.mp4`
- 但正式帧抽取和去绿幕仍在前端浏览器完成,再回传后端发布。
因此:
1. 第 4 项里“图片真实外部生成”目前可以做人工验证。
2. 第 4 项里“视频真实外部生成”已有 `image-to-video` 主链证据,但“动作正式资产全后端闭环”仍需要继续验证与收口,不能把前端抽帧回传链直接记成完全通过。
## 3. 代码级判定依据
### 3.1 已接真实外部图片服务的依据
#### 3.1.1 Big Fish 正式图片链
`server-rs/crates/api-server/src/big_fish.rs`
当前 `generate_big_fish_formal_asset(...)` 会执行:
1. 读取 Big Fish 草稿 prompt
2. 调用 `require_big_fish_dashscope_settings(...)`
3. 调用 `create_big_fish_text_to_image_generation(...)`
4. 向 DashScope `text2image/image-synthesis` 发起异步任务请求
5. 下载远端生成图片
6. 上传 OSS
7. 确认 `asset_object`
8. 绑定到 Big Fish 槽位
这条链已经不是占位图写盘。
#### 3.1.2 Custom World 场景图与封面图
`server-rs/crates/api-server/src/custom_world_ai.rs`
当前 `create_text_to_image_generation(...)``create_reference_image_generation(...)` 会:
1. 真实请求 DashScope 图片生成接口
2. 轮询任务状态或解析生成结果
3. 下载远端图片
4. 上传 OSS
5. 生成 `asset_object` 与实体绑定
因此场景图、AI 封面图当前属于“真实外部图片生成”。
### 3.2 仍未完全闭环的依据
#### 3.2.1 角色动作资产工坊
`server-rs/crates/api-server/src/character_animation_assets.rs`
当前链路现状:
1. `image-to-video` 已真实请求 Ark 生成视频
2. 成功结果会下载并写入 `generated-character-drafts/*/preview.mp4`
3. `publish` 当前仍读取前端传入的 `framesDataUrls`
4. 前端仍通过 `HTMLVideoElement + canvas` 自行抽帧并做去绿幕
因此当前状态应判定为“真实外部视频生成主链已完成,但正式动作资产后端闭环尚未完成”。
## 4. 本次验证范围
本次人工验证分成两部分。
### 4.1 可直接操作并验证通过/失败的范围
1. Big Fish 主图生成是否真实打到 DashScope
2. Big Fish 动作工坊静态关键帧图是否真实打到 DashScope
3. Big Fish 背景图是否真实打到 DashScope
4. Custom World 场景图是否真实打到 DashScope
5. Custom World AI 封面图是否真实打到 DashScope
### 4.2 本次要明确记录为“未通过”的范围
1. 角色资产工坊 `生成角色形象`
2. 角色资产工坊 `生成动作`
3. 任何依赖仓库内占位视频或 SVG 帧的动作生成入口
这些入口本次可以操作,但只能用于确认“当前仍未完全闭环”的具体断点,不能把前端抽帧回传链计入“动作资产全后端闭环”通过证据。
## 5. 前置条件
开始验证前,必须同时满足以下条件:
1. 仓库根目录 `.env.local` 已配置:
- `DASHSCOPE_API_KEY`
- `ALIYUN_OSS_BUCKET`
- `ALIYUN_OSS_ENDPOINT`
- `ALIYUN_OSS_ACCESS_KEY_ID`
- `ALIYUN_OSS_ACCESS_KEY_SECRET`
2. 本机已安装:
- `cargo`
- `node`
- `spacetime`
- `ffmpeg`
- `ffprobe`
3. 本地端口可用或已有可复用 Rust 栈:
- Web`3000`
- Rust API`8082`
- SpacetimeDB`3101`
4. 必须使用 Rust 栈,而不是旧 Node 栈。
说明:
1. 当前 Vite 前端必须指向 Rust `api-server`,否则会把验证结果混入旧链路。
2. 验证时必须能实时查看 Rust `api-server` 日志。
## 6. 启动方式
推荐统一使用:
```powershell
npm run dev:rust
```
该命令会完成以下动作:
1. 启动本地 `SpacetimeDB standalone`
2. 发布 `server-rs/crates/spacetime-module`
3. 启动 Rust `api-server`
4. 启动 Vite Web 开发服务器
若已有栈在运行,至少确认:
1. Web 可访问:`http://127.0.0.1:3000`
2. Rust API 为当前前端的实际代理目标
3. `api-server` 正在输出日志
## 7. 手动验证入口
### 7.1 Big Fish 正式图片链
前端路径:
1. 打开 `http://127.0.0.1:3000`
2. 进入平台创作入口
3. 选择 `Big Fish`
4. 先完成草稿编译
5. 进入结果页
6. 在结果页依次操作:
- `生成背景`
- 打开某个等级的 `主图工坊`,点击 `生成并应用正式图`
- 打开某个等级的 `动作工坊`,点击 `生成并应用正式图`
期望日志特征:
1. Rust `api-server` 中出现 `provider = dashscope`
2. 有 Big Fish 正式图片生成请求
3. 有 DashScope 任务创建或轮询相关日志
4. 生成成功后出现 OSS 写入或正式路径返回
前端期望结果:
1. 资源 URL 不再是 `/generated-big-fish/...`
2. 而是 `/generated-big-fish-assets/...`
3. 结果页状态显示为 `已生成`,而不是 `占位已生成`
### 7.2 Custom World 场景图
前端路径:
1. 进入 RPG / Custom World 创作流程
2. 打开场景或地标编辑入口
3. 点击场景图生成相关操作
期望日志特征:
1. Rust `api-server` 中出现 `provider = dashscope`
2. 有图片生成任务创建与轮询
3. 成功后有 OSS 对象写入和读取兼容路径
前端期望结果:
1. 返回图片不是本地 SVG 占位
2. 保存后场景主图可稳定显示
### 7.3 Custom World AI 封面图
前端路径:
1. 进入作品编辑页
2. 打开 `编辑作品封面`
3. 选择 `AI 生成作品封面`
4. 输入封面氛围提示词
5. 点击生成并保存
期望日志特征:
1. Rust `api-server` 中出现 `provider = dashscope`
2. 有封面图生成任务
3. 成功后有 OSS 上传与对象确认日志
前端期望结果:
1. 生成结果可预览
2. 保存后作品封面更新为正式图
### 7.4 角色资产工坊反向验证
前端路径:
1. 打开任一角色的 AI 资产工坊
2. 点击 `生成角色形象`
3. 再点击 `生成动作`
本入口的验证目标不是“通过”,而是确认它当前仍未接真实外部视频/图片服务。
期望证据:
1. `生成角色形象` 返回的是 SVG 草稿候选
2. `生成动作` 若未导入参考视频,会回退预置占位视频
3. 日志或结果模型字段不应被当作真实外部视频生成通过证据
## 8. 通过标准
第 4 项只有在以下条件全部满足时,才能勾成通过:
1. 至少一条图片生成入口已拿到真实外部服务调用证据。
2. 至少一条视频或动作生成入口已拿到真实外部服务调用证据。
3. 这些证据不能依赖 SVG 占位、仓库内预置视频或本地占位文件。
4. 前端结果能与日志中的正式链路一一对应。
换言之:
1. 仅图片链通过,不代表第 4 项整体通过。
2. 仅 Big Fish 动作工坊生成出一张静态图,也不等于“视频/动作真实生成”通过。
## 9. 当前预判结论
按当前代码基线,本次更可能得到以下结论:
1. 图片真实外部生成:可以拿到通过证据。
2. 视频、动作真实外部生成:`image-to-video` 主链已可拿到真实外部视频生成证据,但正式动作资产后端闭环仍需要继续收口。
因此本次人工验证完成后,建议把第 4 项拆成至少两条独立清单:
1. 图片生成真实外部服务验证
2. 视频生成真实外部服务验证
3. 动作正式资产后端闭环验证
否则会把“已完成的图片链 / 视频生成链”与“仍未完成的正式动作发布后端闭环”混成一个模糊状态。
## 10. 失败判定与排查
### 10.1 图片入口失败
优先看 Rust `api-server` 日志中的错误文本:
1. `dashscope api key 未配置`
- 说明环境变量缺失。
2. `构造 DashScope HTTP 客户端失败`
- 说明本地网络或 TLS 运行环境异常。
3. `读取生成响应失败`
- 说明上游请求已发出,但响应解析失败。
4. `下载远端图片失败`
- 说明上游已生成图片,但下载或签名读链出错。
5. OSS 相关错误
- 说明生成已成功,但落 OSS 或确认对象失败。
### 10.2 角色资产工坊“看起来成功”
若角色工坊前端看起来成功,不应立刻视为通过,需要先核对:
1. 当前策略是否是 `image-sequence / motion-transfer / reference-to-video`
2. 若是 `image-to-video``preview.mp4` 是否来自真实 Ark 生成
3. 正式发布是否仍要求前端回传 `framesDataUrls`
若只是“后端出真实视频、前端再抽帧回传”,则只能记为“视频生成主链通过,正式动作发布后端闭环未完成”,不能直接把整条动作资产链记为完全通过。
## 11. 关联文档
1. [BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md](./BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md)
2. [M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
3. [M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md](./M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md)
4. [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md)
5. [AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md](./AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md)
6. [M6_CHARACTER_ANIMATION_BACKEND_FRAME_EXTRACTION_AND_PUBLISH_STAGE3_2026-04-23.md](./M6_CHARACTER_ANIMATION_BACKEND_FRAME_EXTRACTION_AND_PUBLISH_STAGE3_2026-04-23.md)

View File

@@ -0,0 +1,277 @@
# M6 角色动作外部真实生成 Stage 2 设计
日期:`2026-04-23`
## 1. 文档目的
这份文档用于冻结 `POST /api/assets/character-animation/generate` 从 Stage 1 占位动作链切到真实外部动作生成后的实现口径。
本阶段优先级最高的是当前前端正在使用的 `image-to-video` 主链;如条件允许,再同步补齐 `image-sequence``motion-transfer``reference-to-video` 的真实实现。
## 2. 当前问题
Stage 1 当前存在以下占位行为:
1. `CHARACTER_ANIMATION_MODEL = "rust-placeholder-character-animation"`
2. `image-sequence` 直接产出 SVG 帧
3. 视频类策略会复用参考视频或仓库占位预览视频
4. 即使返回 `previewVideoPath`,也不代表真实调用了外部视频模型
这会导致角色资产工坊的动作生成不能通过“真实外部生成”验证。
## 3. Stage 2 范围
### 3.1 本阶段必须完成
1. `image-to-video` 改为真实 Ark 视频生成
2. 继续兼容前端当前请求字段
3. 继续把预览视频写入 `generated-character-drafts/*`
4. 任务查询和正式发布 contract 继续保持兼容
5. 删除 `image-to-video` 的占位视频回退
### 3.2 争取同步完成
1. `image-sequence` 接真实 DashScope 连续图片生成
2. `motion-transfer` 接真实 DashScope 视频驱动
3. `reference-to-video` 接真实 DashScope 参考生视频
### 3.3 本阶段不做
1. 不新增 SpacetimeDB 动作任务表
2. 不改前端调用字段名
3. 不把动作生成迁回 Node
## 4. 主链优先级
当前前端 `useRoleAnimationWorkflow.ts` 固定使用:
1. `strategy = image-to-video`
2. `videoModel = doubao-seedance-2-0-fast-260128`
3. `motionTransferModel = wan2.2-animate-move`
4. `referenceVideoModel = wan2.7-r2v`
因此 Stage 2 的必须项是先把 `image-to-video` 主链迁成真实实现。
## 5. image-to-video 真实协议
### 5.1 上游与模型
`image-to-video` 统一走 Ark
1. 基础 URL`ARK_CHARACTER_VIDEO_BASE_URL``ARK_BASE_URL`
2. 默认 URL`https://ark.cn-beijing.volces.com/api/v3`
3. API Key`ARK_CHARACTER_VIDEO_API_KEY``ARK_API_KEY`
4. 默认模型:`doubao-seedance-2-0-fast-260128`
### 5.2 创建任务接口
请求:
`POST {ARK_BASE_URL}/contents/generations/tasks`
请求体固定遵循旧 Node 已验证协议:
1. `model`
2. `content[0] = { type: "text", text: prompt }`
3. `content[1] = { type: "image_url", role: "first_frame", image_url: { url } }`
4. `content[2] = { type: "image_url", role: "last_frame", image_url: { url } }`
5. `resolution = "480p"`
6. `ratio = "1:1"`
7. `duration = 4`
8. `watermark = false`
说明:
1. 即使前端传 `720P / 16:9 / 7s`Stage 2 也必须按现网主链固定为 `480p / 1:1 / 4`
2. 这是旧 Node 测试已经冻结的真实协议
### 5.3 轮询接口
请求:
`GET {ARK_BASE_URL}/contents/generations/tasks/{taskId}`
成功判定:
1. 返回状态进入 `completed/succeeded/done` 等完成态
2. 或响应里已经能抽取到 `video_url`
失败判定:
1. `failed`
2. `canceled/cancelled`
3. `error`
4. `rejected`
5. `expired`
6. `unknown`
## 6. 输入媒体解析
### 6.1 首帧与尾帧
`image-to-video` 需要:
1. `visualSource` 作为首帧主参考
2. `lastFrameImageDataUrl` 存在时用作尾帧
3. 若未传尾帧,则回落到 `visualSource`
### 6.2 支持的输入
媒体源继续兼容:
1. Data URL
2. `/generated-*` 旧路径
Rust 需要把它们统一转成可直接给 Ark 的 Data URL。
## 7. Prompt 口径
### 7.1 image-to-video prompt
Stage 2 要求尽量对齐旧 Node `buildArkCharacterAnimationPrompt` 语义,至少包含:
1. 单人 NPC 全身动作视频
2. 动作英文名
3. 角色固定为首尾两张图中的同一人
4. 右向斜侧身
5. 轮廓清晰,武器不可丢失
6. 避免多角色与镜头切换
7. 纯绿色绿幕
8. 首帧严格使用图片 1
9. 尾帧严格使用图片 2
### 7.2 审核降级
`image-to-video` 允许在命中不当内容时重试一次保守 prompt。
重试规则:
1. 第一次请求用正式 prompt
2. 若 Ark 返回错误信息明确命中不当内容
3. 且存在保守 prompt
4. 则用保守 prompt 再请求一次
除此之外不允许再回退到占位视频。
## 8. 其他策略口径
### 8.1 image-sequence
若本轮同步落地:
1. 走 DashScope 图片生成接口
2. 请求体沿用旧 Node 的 `image-generation/generation`
3. `messages[0].content = [{ text }, { image }, ...]`
4. `parameters.n = frameCount`
5. `parameters.size = 768*1024`
6. `parameters.enable_sequential = true`
7. `parameters.prompt_extend = true`
8. `parameters.watermark = false`
### 8.2 motion-transfer
若本轮同步落地:
1. 走 DashScope `image2video/video-synthesis`
2. 先把主图和参考视频上传为 `oss://` 资源
3. 请求头显式带:
1. `X-DashScope-Async: enable`
2. `X-DashScope-OssResourceResolve: enable`
### 8.3 reference-to-video
若本轮同步落地:
1. 走 DashScope `video-generation/video-synthesis`
2. 支持主图、参考图、参考视频混合媒体
3. 同样依赖 DashScope upload policy 上传为 `oss://` 资源
## 9. 预览视频落 OSS
所有真实视频策略成功后都必须:
1. 下载远程 `video_url`
2.`preview.mp4` 落到 `generated-character-drafts/{character}/animation/{animation}/{taskId}/`
3. 返回 `/generated-character-drafts/*/preview.mp4`
Stage 2 明确禁止:
1. 复用仓库内占位视频
2. 仅在 `referenceVideoDataUrls[0]` 不为空时偷传参考视频作为结果
## 10. 配置项
### 10.1 DashScope
继续使用已有:
1. `DASHSCOPE_BASE_URL`
2. `DASHSCOPE_API_KEY`
3. `DASHSCOPE_IMAGE_REQUEST_TIMEOUT_MS`
### 10.2 Ark
Stage 2 新增动作视频专属配置:
1. `ARK_CHARACTER_VIDEO_BASE_URL`
2. `ARK_CHARACTER_VIDEO_API_KEY`
3. `ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS`
4. `ARK_CHARACTER_VIDEO_MODEL`
兼容回退顺序:
1. 先读 `ARK_CHARACTER_VIDEO_*`
2. 再读 `ARK_*`
3. 最后回退到默认值
## 11. 错误与回退策略
### 11.1 直接报错的情况
以下情况不再允许伪成功:
1. 缺少 `ARK_API_KEY` 且策略为 `image-to-video`
2. 上游创建任务失败
3. 上游轮询失败
4. 上游成功但没有视频 URL
5. 下载视频失败
6. OSS 写入失败
### 11.2 禁止继续保留的回退
1. `load_stage1_placeholder_preview_video()`
2. 无模型调用时伪造 `previewVideoPath`
3. `image-sequence` 继续用 SVG 帧充当真实结果
## 12. 任务状态口径
继续复用现有阶段:
1. `prepare_prompt`
2. `request_model`
3. `normalize_result`
4. `persist_result`
新增要求:
1. `request_model` 需记录真实上游任务 id
2. `normalize_result` 需记录 `previewVideoPath``imageSources`
3. `failed` 状态必须返回真实错误消息,不再隐藏为占位成功
## 13. 验收标准
满足以下条件视为 Stage 2 完成:
1. 角色资产工坊点击生成动作时,`image-to-video` 会真实请求 Ark
2. 请求体包含 `first_frame``last_frame`
3. 请求参数固定为 `480p / 1:1 / 4`
4. 返回视频真实来自上游而非占位文件
5. 预览视频成功写入 OSS 草稿区
6. 正式发布链仍能产出 `generated-animations/*` manifest
7. 不再依赖仓库内占位视频完成主流程
## 14. 关联文档
1. [M6_CHARACTER_ANIMATION_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_ANIMATION_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
2. [ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md](./ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md)
3. [AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md](./AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md)

View File

@@ -0,0 +1,258 @@
# M6 角色动作后端抽帧与正式发布 Stage 3 设计
日期:`2026-04-23`
## 1. 文档目的
这份文档用于冻结角色动作资产链路从“后端生成预览视频,前端抽帧后回传发布”继续推进到“后端生成预览视频,后端抽帧、抠绿幕、上传 OSS、落库绑定”的 Stage 3 实现口径。
本阶段的目标不是新增一套全新动作系统,而是在现有 `POST /api/assets/character-animation/generate``POST /api/assets/character-animation/publish` 的 contract 基础上,尽量用最小迁移把动作资产正式发布链完整收口到 Rust `api-server`
## 2. 当前问题
截至 `2026-04-23` Stage 2 完成后,角色动作链路已经具备以下能力:
1. `image-to-video` 会真实请求 Ark 生成 `preview.mp4`
2. 预览视频会真实写入 OSS 草稿区
3. 正式发布时会把动作 manifest、动作集 manifest 上传 OSS并通过 `asset_object + asset_entity_binding` 绑定到角色
但仍存在一个关键断点:
1. `preview.mp4` 的抽帧仍在前端浏览器完成
2. 去绿幕仍在前端 `canvas` 中完成
3. 前端要把整组 `framesDataUrls` 再回传给后端,才会触发正式发布
这与当前工程约束冲突:
1. 前端只负责表现,不负责正式资产加工
2. 外部 I/O、文件处理、OSS 写入应尽量统一收口在后端
## 3. Stage 3 范围
### 3.1 本阶段必须完成
1. `character-animation/publish` 支持仅凭 `previewVideoPath` 在后端完成抽帧
2. 后端抽帧后负责做去绿幕、尺寸归一、PNG 输出、正式帧上传
3. 正式动作 manifest 与动作集 manifest 继续由后端生成并上传 OSS
4. 正式动作集继续由后端确认 `asset_object` 并绑定到角色
5. 前端主链不再要求本地生成 `framesDataUrls`
### 3.2 本阶段必须保留的兼容能力
1. 若前端仍传 `framesDataUrls`,后端继续按旧路径发布
2.`previewVideoPath` 字段名不改
3. 已存在的 `animationMap` 结构不改
### 3.3 本阶段不做
1. 不把外部视频解码逻辑放进 SpacetimeDB reducer / procedure
2. 不新增动作资产专用 SpacetimeDB 表
3. 不改前端 `generate` 接口字段名
4. 不重做 UI 面板结构
## 4. 后端边界
Stage 3 的后端抽帧与上传逻辑统一落在 Rust `api-server`,不进入 `spacetime-module`
原因:
1. 抽帧依赖外部进程与本地临时文件,不满足 reducer 的 deterministic 约束
2. OSS 读写与外部视频工具调用属于平台 I/O不应落入 SpacetimeDB 真相层
3. `spacetime-module` 继续只负责资产对象确认与绑定后的真相落库
因此本阶段职责边界为:
1. `api-server`:下载草稿视频、抽帧、去绿幕、上传 OSS、生成 manifest、调用 `spacetime-client`
2. `spacetime-module`:继续只承接 `asset_object``asset_entity_binding`
## 5. 技术决策
## 5.1 抽帧工具
Stage 3 统一采用系统 `ffmpeg + ffprobe`,不在 Rust 内部新增重型视频解码依赖。
原因:
1. 当前仓库没有可复用的视频解码能力
2. 仅为动作发布链引入一整套 Rust 视频编解码栈,风险高且集成成本大
3. 当前动作帧数很低,采用外部命令行工具能更快稳定落地
## 5.2 运行时依赖
部署与本地联调环境必须满足:
1. `ffmpeg` 可执行
2. `ffprobe` 可执行
配置项:
1. `CHARACTER_ANIMATION_FFMPEG_PATH`
2. `CHARACTER_ANIMATION_FFPROBE_PATH`
3. `CHARACTER_ANIMATION_FRAME_EXTRACT_TIMEOUT_MS`
默认值:
1. `ffmpeg`
2. `ffprobe`
3. `120000`
说明:
1. 若未显式配置路径,则默认从系统 `PATH` 查找
2. 若运行环境缺少 `ffmpeg``ffprobe`,发布动作时必须直接报错,禁止偷偷回退到前端抽帧或占位帧
## 5.3 去绿幕方案
Stage 3 继续保持“后端产出透明背景 PNG 序列”的口径。
实现方式:
1. `ffmpeg` 负责按指定时间点截取原始视频帧
2. Rust 使用 `image` crate 读取单帧 PNG
3. Rust 复用前端现有绿幕去底逻辑的同等阈值与邻域策略,对 RGBA 像素做去背景与边缘去污染
4. Rust 再把去底后的图像按 contain 方式绘制到目标帧尺寸画布中,输出正式 PNG 帧
这样做的原因:
1. 抽帧交给 `ffmpeg`,减少视频解码复杂度
2. 去底逻辑留在 Rust方便与前端当前视觉结果对齐
3. 正式帧统一输出 PNG便于当前 `animationMap` 与运行时消费保持稳定
## 6. Contract 变更
## 6.1 `CharacterAnimationDraftPayload`
在保留旧字段的基础上,新增后端抽帧所需参数:
1. `frameCount?: number`
2. `applyChromaKey?: boolean`
3. `sampleStartRatio?: number`
4. `sampleEndRatio?: number`
兼容规则:
1. `framesDataUrls` 允许为空或省略
2.`framesDataUrls` 非空时,后端继续沿用旧发布路径
3.`framesDataUrls` 为空且存在 `previewVideoPath` 时,后端进入 Stage 3 抽帧路径
## 6.2 默认采样规则
若前端未显式传采样区间,则后端按以下默认值执行:
1. `loop = true` 时:
- `sampleStartRatio = 0.12`
- `sampleEndRatio = 0.94`
2. `loop = false` 时:
- `sampleStartRatio = 0`
- `sampleEndRatio = 1`
`frameCount` 未传,则后端默认使用 `8`
## 7. 正式发布算法
`publish` 收到某个动作草稿时,后端按以下顺序处理:
1.`framesDataUrls` 非空:
- 继续读取图片源
- 继续上传正式帧
- 继续生成动作 manifest
2.`framesDataUrls` 为空但 `previewVideoPath` 存在:
- 从 OSS 草稿区读取视频
- 写入临时目录
- 调用 `ffprobe` 读取时长
- 依据 `frameCount / sampleStartRatio / sampleEndRatio / loop` 计算采样时间点
- 对每个时间点调用 `ffmpeg` 抽一张原始 PNG 帧
- 对每帧做去绿幕与 contain 尺寸归一
-`generated-animations/{character}/{animationSetId}/{action}/framexx.png` 上传正式帧
- 生成动作 manifest
3. 若两者都没有:
- 直接报 `400`
## 8. Manifest 与资产绑定口径
Stage 3 不改变正式 manifest 的结构真相:
1. 动作级 manifest 仍记录:
- `action`
- `frameCount`
- `fps`
- `loop`
- `frameWidth`
- `frameHeight`
- `previewVideoPath`
- `framePaths`
2. 动作集 manifest 仍记录:
- `animationSetId`
- `characterId`
- `visualAssetId`
- `actions`
- `animationMap`
资产落库口径继续保持:
1. 只对动作集根 manifest 执行 `confirm_asset_object`
2. 再把该对象绑定到角色 `animation_set` 槽位
说明:
1. Stage 3 的重点是“正式帧生产链后移”,不是扩展资产对象颗粒度
2. 单帧 PNG 与单动作 manifest 继续只作为 OSS 内部发布物,不额外逐一建表
## 9. 前端迁移口径
Stage 3 后前端职责收口为:
1. 请求后端生成 `preview.mp4`
2. 记录动作元数据:
- `fps`
- `loop`
- `frameWidth`
- `frameHeight`
- `frameCount`
- `applyChromaKey`
- `sampleStartRatio`
- `sampleEndRatio`
- `previewVideoPath`
3. 调用 `publish`,让后端完成正式帧生产
4. 用返回的 `animationMap` 做 UI 预览与角色配置
前端不再承担:
1. 本地视频解码
2. 本地抽帧
3. 本地去绿幕
4. 把整组帧图片 Data URL 回传后端
## 10. 错误策略
以下情况必须直接失败,不允许伪成功:
1. `previewVideoPath` 存在但 OSS 读取失败
2. `ffprobe` 执行失败
3. `ffmpeg` 执行失败
4. 抽帧数量不足
5. PNG 解码失败
6. 去绿幕或 PNG 编码失败
7. 正式帧上传 OSS 失败
错误返回要求:
1. 明确指出失败发生在“视频读取 / 抽帧 / 去底 / 上传 / 落库”哪个阶段
2. 缺少 `ffmpeg``ffprobe` 时,错误消息必须能直接提示环境缺失
## 11. 验收标准
满足以下条件视为 Stage 3 完成:
1. 角色动作 `generate` 会真实生成并返回 OSS 草稿区 `preview.mp4`
2. 角色动作 `publish` 在不传 `framesDataUrls` 的情况下也能成功
3. 正式动作帧由后端抽出并写入 `generated-animations/*`
4. `manifest.json``framePaths` 指向正式 PNG 帧
5. 动作集对象 `object_key` 成功确认并绑定到角色
6. 前端主链不再依赖浏览器本地抽帧
## 12. 关联文档
1. [M6_CHARACTER_ANIMATION_ASSET_EXTERNAL_GENERATION_STAGE2_2026-04-23.md](./M6_CHARACTER_ANIMATION_ASSET_EXTERNAL_GENERATION_STAGE2_2026-04-23.md)
2. [ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md](./ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md)
3. [AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md](./AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md)

View File

@@ -0,0 +1,227 @@
# M6 角色主形象外部真实生成 Stage 2 设计
日期:`2026-04-23`
## 1. 文档目的
这份文档用于冻结 `POST /api/assets/character-visual/generate` 从 Stage 1 SVG 占位草稿切到真实外部图片生成后的实现口径。
本阶段目标不是重做整套资产系统,而是在保留现有 `OSS + asset_object + asset_entity_binding + AiTaskService` 主链不变的前提下,把“候选图生成”改成真实调用图片模型。
## 2. 当前问题
Stage 1 当前存在以下占位实现:
1. `server-rs/crates/api-server/src/character_visual_assets.rs`
2. `CHARACTER_VISUAL_MODEL = "rust-svg-character-visual"`
3. `persist_visual_drafts(...)` 直接生成 `candidate-xx.svg`
4. `publish` 虽然已经能把候选对象发布到 `generated-characters/*`,但候选对象本身不是真实模型产物
这会导致第 4 项验证里“角色资产工坊图片真实外部生成”不能通过。
## 3. Stage 2 范围
### 3.1 本阶段必须完成
1. `character-visual/generate` 改为真实 DashScope 图片生成
2. 支持 `text-to-image`
3. 支持 `image-to-image`
4. 参考图继续兼容 Data URL 与 `/generated-*` 旧路径
5. 候选草稿继续写入 `generated-character-drafts/{character}/visual/{taskId}/candidate-xx.*`
6. `publish` 继续沿用现有 `OSS + asset_object + asset_entity_binding` 正式主链
7. 返回 contract 继续保持前端可直接消费
### 3.2 本阶段不做
1. 不新增 SpacetimeDB 资产任务真相表
2. 不回写本地 `public/generated-*`
3. 不新增前端协议字段
4. 不把角色主图生成迁回 `server-node`
## 4. 真实模型与上游协议
### 4.1 默认模型
默认模型统一使用:
`wan2.7-image-pro`
说明:
1. 这是旧 Node 角色主图正式链的默认模型
2. 当前前端 `useRoleVisualCandidateWorkflow.ts` 也固定传该模型
3. Rust 允许请求显式覆盖 `imageModel`,但默认值必须与旧链一致
### 4.2 DashScope 接口
#### 文生图
`sourceMode = text-to-image` 时:
1. 请求 `POST {DASHSCOPE_BASE_URL}/services/aigc/image-generation/generation`
2. 头部带 `Authorization: Bearer {DASHSCOPE_API_KEY}`
3. 头部带 `X-DashScope-Async: enable`
4. body 使用旧 Node 已验证的 `messages[0].content = [{ text }]` 结构
5. `parameters` 需显式带:
1. `n`
2. `size`
3. `negative_prompt`
4. `prompt_extend = true`
5. `watermark = false`
6. 创建成功后按 `/tasks/{task_id}` 轮询直到成功或失败
#### 图生图
`sourceMode = image-to-image` 时:
1. 请求 `POST {DASHSCOPE_BASE_URL}/services/aigc/image-generation/generation`
2. body 仍使用旧 Node 已验证的 `messages[0].content = [{ text }, { image }, ...]`
3. 参考图顺序保持为“文字在前,图片在后”
4. `parameters` 与文生图一致
5. 同样使用异步任务轮询
## 5. Prompt 口径
### 5.1 正向 prompt
Rust Stage 2 不再使用“LLM 先摘要再拼 SVG”的链路。
新的生成 prompt 口径:
1. 以请求里的 `promptText + characterBriefText` 组装正式主图 prompt
2. 约束必须覆盖:
1. 单人
2. 右向斜侧身
3. 1:1 正方形画布
4. 纯绿色绿幕
5. 3 到 4 头身
6. 像素动作角色
7. 不要扩写复杂背景
3. 主目标是与旧 Node `buildNpcVisualPrompt` 生成出的正式约束保持同方向
### 5.2 负向 prompt
Rust Stage 2 需要显式带负向提示词,至少覆盖以下禁止项:
1. 正面视角
2. 左朝向
3. 纯 90 度侧视
4. 半身像
5. 多角色
6. 复杂背景
7. 文字
8. 水印
9. UI 元素
10. 软萌 Q 版大头贴
## 6. 参考图解析口径
参考图继续兼容两类输入:
1. `data:image/*;base64,...`
2. `/generated-*` 旧路径
不再兼容:
1. 任意仓库磁盘路径
2.`/generated-*` 的普通相对路径
Rust 解析步骤:
1. Data URL 直接转发
2. `/generated-*` 通过 OSS 签名读获取二进制
3. 下载后重新编码为 `data:{mime};base64,...`
4. 再提交给 DashScope
## 7. 候选图下载与存储
DashScope 成功后需要:
1. 下载返回的远程图片 URL
2. 归一化 mime type 和扩展名
3. 对 PNG 做去绿幕/去白底透明化处理
4. 上传到 OSS 草稿区
5. 返回 `/generated-character-drafts/*`
### 7.1 去底规则
为保持与旧 Node 可见效果一致:
1. 当下载结果是 PNG 时,对图像执行背景透明化
2. 透明化逻辑需兼容:
1. 纯绿底
2. 白底边缘
3. 绿边污染
3. 若处理失败,则保留原图,不因后处理失败阻断整个生成链
## 8. 配置项
Stage 2 统一使用以下配置:
1. `DASHSCOPE_BASE_URL`
2. `DASHSCOPE_API_KEY`
3. `DASHSCOPE_IMAGE_REQUEST_TIMEOUT_MS`
补充默认值:
1. `DASHSCOPE_BASE_URL` 默认 `https://dashscope.aliyuncs.com/api/v1`
2. 超时继续沿用现有 `dashscope_image_request_timeout_ms`
当前阶段不新增新的角色主图专属环境变量。
## 9. 错误与回退策略
Stage 2 明确取消 SVG 占位回退。
### 9.1 允许的失败表现
当以下条件不满足时,接口直接返回错误:
1. 缺少 `DASHSCOPE_API_KEY`
2. DashScope 创建任务失败
3. DashScope 轮询失败
4. DashScope 成功但未返回图片 URL
5. 下载图片失败
6. OSS 写入失败
### 9.2 禁止的回退
以下回退在 Stage 2 禁止继续保留:
1. LLM 摘要后生成 SVG 占位图
2. 本地静态预置图
3. 返回“伪成功”但实际未调模型
## 10. 任务状态口径
`AiTaskService` 继续复用现有阶段:
1. `prepare_prompt`
2. `request_model`
3. `normalize_result`
4. `persist_result`
阶段语义调整如下:
1. `prepare_prompt`:冻结最终 prompt、source mode、参考图数量
2. `request_model`记录真实模型名、DashScope task id、实际 prompt
3. `normalize_result`:记录候选对象路径
4. `persist_result`:确认 OSS 草稿已落成
## 11. 验收标准
满足以下条件视为 Stage 2 完成:
1.`character-visual/generate` 时不再生成 SVG 草稿
2. 返回的候选对象是 DashScope 真实产图
3. 草稿对象仍写到 `generated-character-drafts/*`
4. `publish` 仍能发布到 `generated-characters/*`
5. 发布后仍能形成 `asset_object`
6. 发布后仍能形成 `asset_entity_binding`
7. 前端角色资产工坊无须改协议即可继续使用
## 12. 关联文档
1. [M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
2. [ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md](./ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md)
3. [BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md](./BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md)

View File

@@ -111,6 +111,10 @@ export type CharacterAnimationDraftPayload = {
loop: boolean;
frameWidth: number;
frameHeight: number;
frameCount?: number;
applyChromaKey?: boolean;
sampleStartRatio?: number;
sampleEndRatio?: number;
previewVideoPath?: string;
};

View File

@@ -1,7 +1,3 @@
import {
buildAnimationClipFromVideoSource,
type DraftAnimationClip,
} from '../asset-studio/characterAssetWorkflowModel';
import { generateCharacterAnimationDraft } from '../asset-studio/characterAssetWorkflowPersistence';
import type { CharacterAnimationGenerationPayload } from '../asset-studio/characterAssetWorkflowPersistence';
@@ -17,7 +13,7 @@ export function useRoleAnimationWorkflow() {
animationPromptText: string;
characterBriefText: string;
role: EditableCustomWorldRole;
}): Promise<DraftAnimationClip> => {
}) => {
const { actionConfig, animationPromptText, characterBriefText, role } =
params;
@@ -53,20 +49,22 @@ export function useRoleAnimationWorkflow() {
throw new Error('当前自定义世界动作工坊只支持图生视频方案。');
}
return buildAnimationClipFromVideoSource(result.previewVideoPath, {
animation: actionConfig.animation,
return {
fps: actionConfig.fps,
loop: actionConfig.loop,
frameWidth: 192,
frameHeight: 256,
frameCount: actionConfig.frameCount,
applyChromaKey: true,
sampleStartRatio: actionConfig.loop ? 0.12 : 0,
sampleEndRatio: actionConfig.loop ? 0.94 : 1,
});
previewVideoPath: result.previewVideoPath,
};
};
const publishAnimationClipForRole = async (params: {
actionConfig: CustomWorldAiActionConfig;
clip: DraftAnimationClip;
clip: Awaited<ReturnType<typeof generateAnimationClipForRole>>;
role: EditableCustomWorldRole;
}) => {
const { actionConfig, clip, role } = params;
@@ -80,11 +78,15 @@ export function useRoleAnimationWorkflow() {
visualAssetId: role.generatedVisualAssetId,
animations: {
[actionConfig.animation]: {
framesDataUrls: clip.frames,
framesDataUrls: [],
fps: clip.fps,
loop: clip.loop,
frameWidth: clip.frameWidth,
frameHeight: clip.frameHeight,
frameCount: clip.frameCount,
applyChromaKey: clip.applyChromaKey,
sampleStartRatio: clip.sampleStartRatio,
sampleEndRatio: clip.sampleEndRatio,
previewVideoPath: clip.previewVideoPath,
},
},

View File

@@ -62,7 +62,6 @@ import {
type ScenePresetInfo,
WorldType,
} from '../../types';
import { buildAnimationClipFromVideoSource } from '../asset-studio/characterAssetWorkflowModel';
import {
type CharacterAnimationGenerationPayload,
generateCharacterAnimationDraft,
@@ -3832,28 +3831,19 @@ function RoleSkillEditorModal({
throw new Error('当前技能动作预览仅支持图生视频生成。');
}
const clip = await buildAnimationClipFromVideoSource(
generationResult.previewVideoPath,
{
animation: AnimationState.ATTACK,
fps: 10,
loop: false,
frameCount: 8,
applyChromaKey: true,
},
);
const publishResult = await publishCharacterAnimationAssets({
characterId: role.id,
visualAssetId: role.generatedVisualAssetId,
animations: {
[actionKey]: {
framesDataUrls: clip.frames,
fps: clip.fps,
loop: clip.loop,
frameWidth: clip.frameWidth,
frameHeight: clip.frameHeight,
previewVideoPath: clip.previewVideoPath,
framesDataUrls: [],
fps: 10,
loop: false,
frameWidth: 192,
frameHeight: 256,
frameCount: 8,
applyChromaKey: true,
previewVideoPath: generationResult.previewVideoPath,
},
},
updateCharacterOverride: false,