diff --git a/docs/technical/ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md b/docs/technical/ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md new file mode 100644 index 00000000..13edd5ea --- /dev/null +++ b/docs/technical/ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md @@ -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) diff --git a/docs/technical/M6_CHARACTER_ANIMATION_ASSET_EXTERNAL_GENERATION_STAGE2_2026-04-23.md b/docs/technical/M6_CHARACTER_ANIMATION_ASSET_EXTERNAL_GENERATION_STAGE2_2026-04-23.md new file mode 100644 index 00000000..0f84e33b --- /dev/null +++ b/docs/technical/M6_CHARACTER_ANIMATION_ASSET_EXTERNAL_GENERATION_STAGE2_2026-04-23.md @@ -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) diff --git a/docs/technical/M6_CHARACTER_ANIMATION_BACKEND_FRAME_EXTRACTION_AND_PUBLISH_STAGE3_2026-04-23.md b/docs/technical/M6_CHARACTER_ANIMATION_BACKEND_FRAME_EXTRACTION_AND_PUBLISH_STAGE3_2026-04-23.md new file mode 100644 index 00000000..411e4f3e --- /dev/null +++ b/docs/technical/M6_CHARACTER_ANIMATION_BACKEND_FRAME_EXTRACTION_AND_PUBLISH_STAGE3_2026-04-23.md @@ -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) diff --git a/docs/technical/M6_CHARACTER_VISUAL_ASSET_EXTERNAL_GENERATION_STAGE2_2026-04-23.md b/docs/technical/M6_CHARACTER_VISUAL_ASSET_EXTERNAL_GENERATION_STAGE2_2026-04-23.md new file mode 100644 index 00000000..13cf4889 --- /dev/null +++ b/docs/technical/M6_CHARACTER_VISUAL_ASSET_EXTERNAL_GENERATION_STAGE2_2026-04-23.md @@ -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) diff --git a/src/components/asset-studio/characterAssetWorkflowPersistence.ts b/src/components/asset-studio/characterAssetWorkflowPersistence.ts index abebb20c..0e6d8806 100644 --- a/src/components/asset-studio/characterAssetWorkflowPersistence.ts +++ b/src/components/asset-studio/characterAssetWorkflowPersistence.ts @@ -111,6 +111,10 @@ export type CharacterAnimationDraftPayload = { loop: boolean; frameWidth: number; frameHeight: number; + frameCount?: number; + applyChromaKey?: boolean; + sampleStartRatio?: number; + sampleEndRatio?: number; previewVideoPath?: string; }; diff --git a/src/components/rpg-creation-asset-studio/useRoleAnimationWorkflow.ts b/src/components/rpg-creation-asset-studio/useRoleAnimationWorkflow.ts index 8450ba90..fa5cd94c 100644 --- a/src/components/rpg-creation-asset-studio/useRoleAnimationWorkflow.ts +++ b/src/components/rpg-creation-asset-studio/useRoleAnimationWorkflow.ts @@ -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 => { + }) => { 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>; 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, }, }, diff --git a/src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx b/src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx index dccc2c0f..972efc66 100644 --- a/src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx +++ b/src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx @@ -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,