diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index ceb2620ef..2b602af5d 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2282,7 +2282,7 @@ ## 2026-06-15 图片画布角色图层新增动画生成入口 - 背景:图片画布已有角色形象图层标记 `assetKind="character"`,需要只对角色图片开放动画生成,不让普通素材误触发角色动画链路。 -- 决策:角色动画入口只由画布图层 `assetKind="character"` 控制,在图片上方浮动工具条和右键菜单显示 `生成动画`;非角色图层不展示入口。点击后打开独立 `角色动画生成面板`,桌面端锚定到图片右侧,移动端按底部面板承接。前端固定提交 `seedance2.0`、分辨率 / 比例 / 帧数 / 时长 / 价格字段;后端经 `/api/editor/character-animations/generations` 使用角色图作为首帧和尾帧生成视频,并立即抽取 32 / 40 / 48 帧、绿幕去背后写入 OSS。 +- 决策:角色动画入口只由画布图层 `assetKind="character"` 控制,在图片上方浮动工具条和右键菜单显示 `生成动画`;非角色图层不展示入口。点击后打开独立 `角色动画生成面板`,桌面端锚定到图片右侧,移动端按底部面板承接。前端固定提交 `seedance2.0-fast`、分辨率 / 比例 / 帧数 / 时长 / 价格字段;后端经 `/api/editor/character-animations/generations` 使用角色图作为首帧和尾帧生成视频,并立即抽取 32 / 40 / 48 帧、绿幕去背后写入 OSS。 - 影响范围:`src/components/image-editor/ImageCanvasEditorView.tsx`、`src/services/image-editor/editorProjectClient.ts`、`server-rs/crates/api-server/src/character_animation_assets.rs`、`server-rs/crates/shared-contracts/src/assets.rs`、图片画布技术方案。 - 验证方式:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/services/image-editor/editorProjectClient.test.ts`、`cargo test -p api-server editor_character_animation --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 - 关联文档:`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。 @@ -2330,7 +2330,7 @@ ## 2026-06-17 图片画布底部生成视频接入 Lovart 面板 - 背景:编辑器画板底部工具栏需要新增 `生成视频`,并和现有 Lovart 式生成类面板、泥点展示、占位图和画布结果图层保持一致。 -- 决策:`生成视频` 点击后创建独立视频生成占位和极简面板,提交 `POST /api/editor/videos/generations`;首期前端仅开放 `seedance2.0`、`seedance2.0-fast`、`kling3.0`、`kling3.0-omni`,不展示 Veo 模型入口;固定文字转视频、`16:9`、标准模式和静音。后端复用 Ark / VectorEngine content generation task 轮询链路,下载视频后持久化到 OSS。生成结果在画布中写入 `mediaType="video"` 与 `assetKind="video"`,图片信息弹窗按视频显示为 `视频信息` / `视频类型`。生成类泥点价格统一走 `editor_generation_config`,视频和角色动画均为 480p 每秒 10 泥点、720p 每秒 20 泥点。 +- 决策:`生成视频` 点击后创建独立视频生成占位和极简面板,提交 `POST /api/editor/videos/generations`;首期前端仅开放 `seedance2.0-fast`、`seedance2.0`、`kling3.0`、`kling3.0-omni`,不展示 Veo 模型入口,默认 `seedance2.0-fast`。后端必须严格区分 Seedance 2.0 Fast 与标准版:`seedance2.0-fast` 映射 `doubao-seedance-2-0-fast-260128`,`seedance2.0` 映射 `doubao-seedance-2-0-260128`,不得混用;固定文字转视频、`16:9`、标准模式和静音。后端复用 Ark / VectorEngine content generation task 轮询链路,下载视频后持久化到 OSS。生成结果在画布中写入 `mediaType="video"` 与 `assetKind="video"`,图片信息弹窗按视频显示为 `视频信息` / `视频类型`。生成类泥点价格统一走 `editor_generation_config`,视频和角色动画均为 480p 每秒 10 泥点、720p 每秒 20 泥点。 - 影响范围:图片画布生成工作流、前端 editorProjectClient、`shared-contracts`、`api-server` 视频生成 BFF、编辑器技术方案和生成类面板方案。 - 验证方式:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx -t "opens the bottom generate video panel"`、`npm run test -- src/components/image-editor/ImageCanvasMetadataModalView.test.tsx`、`npm run test -- src/services/image-editor/editorProjectClient.test.ts`、`cargo test -p shared-contracts editor_video --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server editor_video --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`、`git diff --check`。 - 关联文档:`docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。 diff --git a/docs/superpowers/plans/【编辑器】画板角色动画生成实施计划-2026-06-15.md b/docs/superpowers/plans/【编辑器】画板角色动画生成实施计划-2026-06-15.md index 1b5513d8f..1416c67c7 100644 --- a/docs/superpowers/plans/【编辑器】画板角色动画生成实施计划-2026-06-15.md +++ b/docs/superpowers/plans/【编辑器】画板角色动画生成实施计划-2026-06-15.md @@ -2,11 +2,11 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** 在图片画布编辑器中,仅对角色图片提供角色动画生成入口,并通过后端 seedance2.0 链路生成视频、抽帧、去绿幕并持久化到 OSS。 +**Goal:** 在图片画布编辑器中,仅对角色图片提供角色动画生成入口,并通过后端 seedance2.0-fast 链路生成视频、抽帧、去绿幕并持久化到 OSS。 **Architecture:** 前端在 `ImageCanvasEditorView` 中基于图层 `assetKind === "character"` 控制悬浮按钮和右键菜单,打开锚定到图片右侧的独立动画生成面板。前端 service 调用新增编辑器角色动画 API,后端复用 `character_animation_assets.rs` 中现有视频生成、抽帧、绿幕去背、OSS 写入能力,避免新建平行资产系统。 -**Tech Stack:** React + TypeScript + Vitest;Rust Axum `api-server`;现有 `shared-contracts` 资产 DTO;Aliyun OSS 资产持久化;VectorEngine/Ark seedance2.0 角色动画链路。 +**Tech Stack:** React + TypeScript + Vitest;Rust Axum `api-server`;现有 `shared-contracts` 资产 DTO;Aliyun OSS 资产持久化;VectorEngine/Ark seedance2.0-fast 角色动画链路。 --- @@ -48,7 +48,7 @@ - [ ] **Step 1: 新增 service 类型和请求函数** - `generateEditorCharacterAnimation(input)` 调用 `/api/editor/character-animations/generations`。 - - 限定 model 固定为 `seedance2.0` 的回包展示字段。 + - 限定 model 固定为 `seedance2.0-fast` 的回包展示字段。 - [ ] **Step 2: 扩展图层 assetKind** - `CanvasLayer.assetKind` 支持 `'character' | 'spec' | null`。 @@ -71,7 +71,7 @@ - Modify: `C:/Genarrative/server-rs/crates/api-server/src/modules/play_flow.rs` 或现有 editor router 文件(按现有路由事实选择) - [ ] **Step 1: 写 DTO / prompt / plan 单测** - - 验证请求 480p/720p、32/40/48 帧、比例枚举、模型固定 seedance2.0。 + - 验证请求 480p/720p、32/40/48 帧、比例枚举、模型固定 seedance2.0-fast。 - 验证构造 prompt 包含用户给定固定骨架与动作描述。 - 验证价格计算:480p 每秒 10,720p 每秒 20。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index 9f0a67a7e..828ef80bc 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -18,7 +18,7 @@ - 图片拖拽时显示水平 / 垂直吸附参考线,吸附到其它图层或画板的边缘与中心线。 - 生成资源右上角显示元数据按钮,点击打开独立元数据窗口。图片信息页不展示后端组装后的生图 Prompt,也不提供复制 Prompt;只展示该图片生成时用户在面板里提交的输入快照,包括普通生成提示词、规范表单字段、角色设定、图标素材描述、修改要求,以及角色形象规范 / 常规参考图 / 图标素材规范 / 修改参考图等参考图卡片。旧数据或上传图片没有输入快照时显示 `-`,禁止回退展示内部 Prompt。 - 对生成资源执行修改时,在右侧创建新的生成结果图层,并自动调整视图显示原图和新图。 -- 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和快速编辑保留既有 `gpt-image-2` 路径;`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。纯文本生成走 `/api/editor/images/generations`,基于当前生成图的修改走 `/api/editor/images/edits`。`生成视频` 走 `/api/editor/videos/generations`,前端模型入口仅展示 Seedance 2.0 / Seedance 2.0 Fast / Kling 3.0 / Kling 3.0 Omni,不展示 Veo 入口;首期固定文字转视频、`16:9`、标准模式、静音,生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成工具后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。快速编辑和修改图片在调用后端前必须把当前图层图片源读取为图片 Data URL,来源可以是本地上传 Data URL、站内 public 图片、历史 `/generated-*` 路径或可读取的 OSS generated URL;后端仍只接收图片 Data URL,不把普通 URL 直接透传到 VectorEngine edits。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。 +- 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和快速编辑保留既有 `gpt-image-2` 路径;`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。纯文本生成走 `/api/editor/images/generations`,基于当前生成图的修改走 `/api/editor/images/edits`。`生成视频` 走 `/api/editor/videos/generations`,前端模型入口仅展示 Seedance 2.0 Fast / Seedance 2.0 / Kling 3.0 / Kling 3.0 Omni,不展示 Veo 入口,默认 Seedance 2.0 Fast;首期固定文字转视频、`16:9`、标准模式、静音,生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成工具后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。快速编辑和修改图片在调用后端前必须把当前图层图片源读取为图片 Data URL,来源可以是本地上传 Data URL、站内 public 图片、历史 `/generated-*` 路径或可读取的 OSS generated URL;后端仍只接收图片 Data URL,不把普通 URL 直接透传到 VectorEngine edits。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。 - 底部生成类按钮每次点击都必须创建独立的画布生成对象;新建规范、角色形象或图标素材时,只切换当前编辑面板,不得销毁此前尚未生成或已生成后的其它生成对象状态。归档为非当前编辑对象的生成占位仍可拖动、删除和等待异步完成,完成 / 失败回写必须按生成对象 ID 读取最新占位状态,不能使用提交瞬间的旧快照。 - 所有会新建画布生成占位的入口必须先创建 draft,再统一经过 `ImageCanvasGenerationPlacementModel` 计算落点,禁止各入口自行使用当前视口中心裸坐标。当前覆盖入口包括 `生成图片`、`生成规范`、`生成角色形象`、`生成图标素材`、`生成视频` 和 `生成UI设计图`。placement 模型的避让对象为所有未隐藏画布图层,以及当前 active / inactive generation dialogs 中仍存在的 placeholder;每个避让矩形按 32px 画布世界坐标间距外扩。候选落点以当前视口世界中心为距离目标,优先选择离视口中心最近且不重叠的占位位置;若中心被占用,会按上下左右和环形候选继续寻找。打开生成面板时必须把避让后的 placeholder 写入 `openCanvasGenerationDialog(...)`,并立即调用 `centerViewportOnPlacement(...)` 居中到新占位中心,保持原 viewport scale 不变。 @@ -67,7 +67,7 @@ - `POST /api/editor/images/generations`:按提示词调用 VectorEngine 生成图片;角色生成可携带 `model`、`aspectRatio`、`imageSize` 和 `referenceImageSrcs`。`nanobanana2` 参考图作为 `inline_data` 进入 `generateContent`,`gpt-image-2` 参考图进入 edits。携带参考图的快速编辑也走该接口,前端必须把参考图源预读成图片 Data URL 后放入 `referenceImageSrcs`;接口返回 data URL、尺寸、prompt、model、provider 和 taskId。 - `POST /api/editor/icon-spritesheets/generations`:按图标素材规范图和素材描述数组生成 spritesheet,再由后端切分为独立透明图标。请求支持 `model`、`aspectRatio`、`imageSize`;`nanobanana2` 走原生 `generateContent` 并写入 `generationConfig.imageConfig.aspectRatio/imageSize`,`0.5K` 传 `"512"`;`gpt-image-2` 走 `/v1/images/edits`,后端把 UI 尺寸归一为文档支持的 `1024x1024`、`1024x1536`、`1536x1024`、`2048x2048`、`2048x1152` 等 `size` 字符串。 - `POST /api/editor/images/edits`:按提示词和当前图片 Data URL 调用 VectorEngine edits,返回新的生成图片元数据。 -- `POST /api/editor/videos/generations`:按视频描述、模型、比例、时长、分辨率、模式、声音和泥点价格生成视频。前端可选模型为 `seedance2.0`、`seedance2.0-fast`、`kling3.0`、`kling3.0-omni`;后端复用 Ark / VectorEngine content generation task 轮询链路,下载最终视频并持久化到 OSS,返回 `videoSrc`、尺寸、prompt、model、provider、taskId、durationSeconds、resolution 和 `priceMudPoints`。 +- `POST /api/editor/videos/generations`:按视频描述、模型、比例、时长、分辨率、模式、声音和泥点价格生成视频。前端可选模型为 `seedance2.0-fast`、`seedance2.0`、`kling3.0`、`kling3.0-omni`,默认 `seedance2.0-fast`;后端必须将 `seedance2.0-fast` 映射到 `doubao-seedance-2-0-fast-260128`,将 `seedance2.0` 映射到 `doubao-seedance-2-0-260128`,两者不得混用。后端复用 Ark / VectorEngine content generation task 轮询链路,下载最终视频并持久化到 OSS,返回 `videoSrc`、尺寸、prompt、model、provider、taskId、durationSeconds、resolution 和 `priceMudPoints`。 所有写接口都必须校验 Bearer 登录态和 owner;接口只返回当前用户有权读取的工程与资源。 diff --git a/docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md b/docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md index 431f22793..be2342683 100644 --- a/docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md +++ b/docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md @@ -101,13 +101,15 @@ ## 生成视频模型与接口 - 底部 `生成视频` 面板提交到 `POST /api/editor/videos/generations`。 -- 首期只开放文字生成视频,固定 `16:9`、静音、标准模式;结果作为 `mediaType="video"`、`assetKind="video"` 的视频图层加入画布。 +- 固定 `16:9`、静音、标准模式;结果作为 `mediaType="video"`、`assetKind="video"` 的视频图层加入画布。 - 支持模型: - - `seedance2.0` - `seedance2.0-fast` + - `seedance2.0` - `kling3.0` - `kling3.0-omni` - 前端不展示 Veo 模型入口。 +- `seedance2.0-fast` 是生成视频默认项,并继续映射到 `doubao-seedance-2-0-fast-260128`;`seedance2.0` 是标准版可选项,必须独立映射到 `doubao-seedance-2-0-260128`,不得与 Fast 混用。 +- 角色动画生成固定使用 `seedance2.0-fast`,不继承生成视频标准版选择。 - 后端复用现有 Ark / VectorEngine content generation task 轮询链路,并把生成视频持久化到 OSS;缺少 `ARK_CHARACTER_VIDEO_BASE_URL` 或 `ARK_CHARACTER_VIDEO_API_KEY` 时 fail-closed 返回配置错误。 ## 验收 diff --git a/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md b/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md index 9ac63ce6c..38252a61c 100644 --- a/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md +++ b/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md @@ -136,7 +136,7 @@ - 分辨率:`480p`、`720p`。 - 画面比例:默认 `与角色图片保持同尺寸`,可选 `1:1`、`4:3`、`16:9`、`9:16`、`3:4`。 - 时长:`32帧·4秒`、`40帧·5秒`、`48帧·6秒`。 -4. 模型固定使用 `seedance2.0`,前端不提供模型切换。 +4. 模型固定使用 `seedance2.0-fast`,前端不提供模型切换。 5. 生成按钮上方显示本次生成文本摘要和生成价格: - `480p` 每秒 `10` 泥点。 - `720p` 每秒 `20` 泥点。 @@ -145,7 +145,7 @@ - 前端提交到 `POST /api/editor/character-animations/generations`。 - 请求必须带上角色图片来源、原始尺寸、动画描述、分辨率、画面比例、帧数和时长。角色图片已经持久化到 OSS 时,`sourceImageSrc` 必须优先传 `objectKey`;只有未持久化的本地临时图片才允许传 Data URL。 -- 后端使用角色图片作为首帧和尾帧参考,模型固定映射到 seedance2.0 对应后端模型。 +- 后端使用角色图片作为首帧和尾帧参考,模型固定映射到 `doubao-seedance-2-0-fast-260128`。 - 后端路由兼容旧 Data URL 请求并单独放宽 JSON body limit 到 `12MB`,但该限额只作为兼容兜底,不作为新链路默认传大图的方式。 - 后端 prompt 使用以下固定骨架,并把面板输入追加到 `动作描述:` 后: diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index 573747d06..082e95566 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -1484,7 +1484,7 @@ mod tests { "frameCount": 32, "durationSeconds": 4, "priceMudPoints": 40, - "model": "seedance2.0" + "model": "seedance2.0-fast" }) .to_string(); assert!(request_body.len() > 2 * 1024 * 1024); diff --git a/server-rs/crates/api-server/src/character_animation_assets.rs b/server-rs/crates/api-server/src/character_animation_assets.rs index 26ee14cc0..d2b9b6fae 100644 --- a/server-rs/crates/api-server/src/character_animation_assets.rs +++ b/server-rs/crates/api-server/src/character_animation_assets.rs @@ -63,6 +63,7 @@ use crate::{ use tokio::time::sleep; const CHARACTER_ANIMATION_MODEL: &str = "doubao-seedance-2-0-fast-260128"; +const SEEDANCE_2_STANDARD_MODEL: &str = "doubao-seedance-2-0-260128"; const CHARACTER_ANIMATION_ASSET_KIND: &str = "character_animation"; const CHARACTER_ANIMATION_REFERENCE_ASSET_KIND: &str = "character_animation_reference_video"; const CHARACTER_WORKFLOW_CACHE_ASSET_KIND: &str = "character_workflow_cache"; @@ -85,7 +86,7 @@ const FIXED_ARK_CHARACTER_VIDEO_RESOLUTION: &str = "480p"; const FIXED_ARK_CHARACTER_VIDEO_RATIO: &str = "1:1"; const FIXED_ARK_CHARACTER_VIDEO_DURATION_SECONDS: u32 = 4; const ARK_VIDEO_TASK_POLL_INTERVAL_MS: u64 = 5_000; -const EDITOR_CHARACTER_ANIMATION_MODEL: &str = "seedance2.0"; +const EDITOR_CHARACTER_ANIMATION_MODEL: &str = "seedance2.0-fast"; const EDITOR_CHARACTER_ANIMATION_ASSET_KIND: &str = "editor_character_animation"; const EDITOR_CHARACTER_ANIMATION_PROMPT_PREFIX: &str = "生成游戏角色动画,参考图作为首帧和尾帧,画面中心构图,角色主体完整置于画面中央,禁止镜头透视,禁止特写。背景固定为纯绿色绿幕,只作为抠像底色,禁止出现建筑、室内布景、风景、地面道具、漂浮物、烟雾叙事元素、文字或其他角色以外的场景内容。\n动作描述:"; const EDITOR_VIDEO_ASSET_KIND: &str = "editor_video"; @@ -2317,7 +2318,7 @@ fn normalize_editor_character_animation_request( } if payload.model.trim() != EDITOR_CHARACTER_ANIMATION_MODEL { return Err(editor_character_animation_bad_request( - "model 必须固定为 seedance2.0。", + "model 必须固定为 seedance2.0-fast。", )); } let (frame_width, frame_height) = resolve_editor_character_animation_frame_size( @@ -2376,13 +2377,13 @@ fn require_editor_character_animation_settings( "reason": "ARK_CHARACTER_VIDEO_API_KEY 未配置", })) })?; - // 中文注释:画板角色动画入口产品侧固定为 seedance2.0,不继承通用角色视频环境模型覆盖。 + // 中文注释:画板角色动画入口产品侧固定为 Seedance 2.0 Fast,不继承通用角色视频环境模型覆盖。 Ok(EditorCharacterAnimationSettings { ark: ArkVideoSettings { base_url: base_url.to_string(), api_key: api_key.to_string(), request_timeout_ms: state.config.ark_character_video_request_timeout_ms.max(1), - model: EDITOR_CHARACTER_ANIMATION_MODEL.to_string(), + model: CHARACTER_ANIMATION_MODEL.to_string(), }, resolution: request.resolution.clone(), ratio: request.ratio.clone(), @@ -2447,7 +2448,9 @@ fn normalize_editor_video_model(value: &str) -> Result<&'static str, AppError> { fn resolve_editor_video_provider_model(model: &str) -> &str { match model { - // 中文注释:Seedance 2.0 Fast 复用平台已有 fast seedance 上游模型;标准版按产品 ID 透传。 + // 中文注释:Seedance 2.0 与 Seedance 2.0 Fast 必须映射到不同上游模型,避免生成视频误把标准版发到 fast 模型。 + EDITOR_VIDEO_MODEL_SEEDANCE_2 => SEEDANCE_2_STANDARD_MODEL, + // 中文注释:Seedance 2.0 Fast 继续复用平台已有 fast seedance 上游模型。 EDITOR_VIDEO_MODEL_SEEDANCE_2_FAST => CHARACTER_ANIMATION_MODEL, _ => model, } @@ -4237,6 +4240,7 @@ struct PublishedAnimationAction { #[cfg(test)] mod tests { use super::*; + use crate::AppConfig; #[test] fn parse_video_data_url_accepts_mp4_payload() { @@ -4408,6 +4412,37 @@ mod tests { assert_eq!(normalized.ratio, "3:4"); } + #[test] + fn editor_character_animation_uses_fast_seedance_provider_model() { + let config = AppConfig { + ark_character_video_base_url: "https://ark.example.com/api/v3".to_string(), + ark_character_video_api_key: Some("ark-key".to_string()), + ark_character_video_request_timeout_ms: 420_000, + ..AppConfig::default() + }; + let state = AppState::new(config).expect("state should build"); + let request = + normalize_editor_character_animation_request(EditorCharacterAnimationGenerateRequest { + source_layer_id: "layer-hero".to_string(), + source_image_src: "/generated-characters/hero/master.png".to_string(), + source_width: 1024, + source_height: 1024, + prompt_text: "待机呼吸".to_string(), + resolution: "480p".to_string(), + ratio: "same".to_string(), + frame_count: 32, + duration_seconds: 4, + price_mud_points: 40, + model: EDITOR_CHARACTER_ANIMATION_MODEL.to_string(), + }) + .expect("editor request should normalize"); + + let settings = require_editor_character_animation_settings(&state, &request) + .expect("settings should normalize"); + + assert_eq!(settings.ark.model, CHARACTER_ANIMATION_MODEL); + } + #[test] fn editor_character_animation_rejects_invalid_frame_duration_pair() { let error = @@ -4496,6 +4531,25 @@ mod tests { assert_eq!(normalized.height, 720); } + #[test] + fn editor_video_seedance_standard_uses_non_fast_seedance_model() { + let normalized = normalize_editor_video_request(EditorVideoGenerateRequest { + prompt: "生成更稳定的电影感镜头。".to_string(), + model: EDITOR_VIDEO_MODEL_SEEDANCE_2.to_string(), + aspect_ratio: "16:9".to_string(), + duration_seconds: 4, + resolution: "720p".to_string(), + mode: "std".to_string(), + sound: "off".to_string(), + price_mud_points: 80, + }) + .expect("seedance standard request should normalize"); + + assert_eq!(normalized.model, EDITOR_VIDEO_MODEL_SEEDANCE_2); + assert_eq!(normalized.provider_model, "doubao-seedance-2-0-260128"); + assert_ne!(normalized.provider_model, CHARACTER_ANIMATION_MODEL); + } + #[test] fn editor_video_rejects_price_mismatch() { let error = normalize_editor_video_request(EditorVideoGenerateRequest { diff --git a/server-rs/crates/shared-contracts/src/assets.rs b/server-rs/crates/shared-contracts/src/assets.rs index a07cc8f85..8438d64a2 100644 --- a/server-rs/crates/shared-contracts/src/assets.rs +++ b/server-rs/crates/shared-contracts/src/assets.rs @@ -902,14 +902,14 @@ mod tests { frame_count: 48, duration_seconds: 6, price_mud_points: 120, - model: "seedance2.0".to_string(), + model: "seedance2.0-fast".to_string(), }) .expect("request should serialize"); assert_eq!(payload["sourceLayerId"], json!("layer-1")); assert_eq!(payload["sourceImageSrc"], json!("/generated-characters/hero/master.png")); assert_eq!(payload["priceMudPoints"], json!(120)); - assert_eq!(payload["model"], json!("seedance2.0")); + assert_eq!(payload["model"], json!("seedance2.0-fast")); } #[test] @@ -917,7 +917,7 @@ mod tests { let payload = serde_json::to_value(EditorCharacterAnimationGenerateResponse { ok: true, task_id: "task-1".to_string(), - model: "seedance2.0".to_string(), + model: "seedance2.0-fast".to_string(), prompt: "生成游戏角色动画".to_string(), preview_video_path: "/generated-character-drafts/editor/layer/preview.mp4".to_string(), frames: vec![EditorCharacterAnimationFramePayload { diff --git a/src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx b/src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx index a4d9a5adc..4ad3bd7ab 100644 --- a/src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx +++ b/src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx @@ -199,7 +199,7 @@ describe('ImageCanvasCharacterAnimationPanelView', () => { it('renders completed frame count and failed error state', () => { const result = { taskId: 'task-a', - model: 'seedance2.0' as const, + model: 'seedance2.0-fast' as const, prompt: '行走动作', previewVideoPath: '/preview.mp4', frames: [], diff --git a/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx b/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx index 7e0b4ab2d..1d609540c 100644 --- a/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx @@ -1935,7 +1935,7 @@ describe('ImageCanvasEditorView generation integration', () => { }); generateEditorCharacterAnimationMock.mockResolvedValueOnce({ taskId: 'character-animation-task-1', - model: 'seedance2.0', + model: 'seedance2.0-fast', prompt: '生成游戏角色动画\n动作描述:\n待机', previewVideoPath: '/generated-character-drafts/editor/preview.mp4', frames: Array.from({ length: 48 }, (_, index) => ({ @@ -2049,7 +2049,7 @@ describe('ImageCanvasEditorView generation integration', () => { frameCount: 48, durationSeconds: 6, priceMudPoints: 120, - model: 'seedance2.0', + model: 'seedance2.0-fast', }), ); expect( diff --git a/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx b/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx index 736a7ba28..52930553a 100644 --- a/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx +++ b/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx @@ -261,7 +261,7 @@ describe('ImageCanvasGenerationComposerView', () => { status: 'idle', composerOpen: true, generationReferences: [], - videoModel: 'seedance2.0', + videoModel: 'seedance2.0-fast', videoAspectRatio: '16:9', videoDurationSeconds: 4, videoResolution: '480p', @@ -299,7 +299,7 @@ describe('ImageCanvasGenerationComposerView', () => { status: 'idle', composerOpen: true, generationReferences: [], - videoModel: 'seedance2.0', + videoModel: 'seedance2.0-fast', videoAspectRatio: '16:9', videoDurationSeconds: 4, videoResolution: '480p', @@ -326,7 +326,7 @@ describe('ImageCanvasGenerationComposerView', () => { within(panel).getByRole('button', { name: '视频参数 16:9 · 4秒 · 480p' }), ).toBeTruthy(); expect( - within(panel).getByRole('button', { name: '模型 Seedance 2.0' }), + within(panel).getByRole('button', { name: '模型 Seedance 2.0 Fast' }), ).toBeTruthy(); expect(within(panel).getByRole('button', { name: '生成视频' }).textContent).toBe( '生成40泥点', @@ -351,7 +351,7 @@ describe('ImageCanvasGenerationComposerView', () => { '生成100泥点', ); - fireEvent.click(screen.getByRole('button', { name: '模型 Seedance 2.0' })); + fireEvent.click(screen.getByRole('button', { name: '模型 Seedance 2.0 Fast' })); const modelPanel = screen.getByRole('menu', { name: '视频模型选项' }); expect(within(modelPanel).queryByRole('button', { name: /Veo/i })).toBeNull(); fireEvent.click(within(modelPanel).getByRole('button', { name: 'Kling 3.0' })); diff --git a/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts b/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts index 888e2afee..a9b49e8b0 100644 --- a/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts +++ b/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts @@ -146,7 +146,7 @@ describe('ImageCanvasGenerationDialogModel', () => { status: 'idle', composerOpen: true, generationReferences: [], - videoModel: 'seedance2.0', + videoModel: 'seedance2.0-fast', videoAspectRatio: '16:9', videoResolution: '480p', videoDurationSeconds: 4, diff --git a/src/components/image-editor/ImageCanvasGenerationDialogModel.ts b/src/components/image-editor/ImageCanvasGenerationDialogModel.ts index 8619c627b..b43f6d4e4 100644 --- a/src/components/image-editor/ImageCanvasGenerationDialogModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationDialogModel.ts @@ -17,6 +17,7 @@ import { DEFAULT_ICON_DESCRIPTIONS, DEFAULT_IMAGE_MODEL, DEFAULT_SPEC_FORM_VALUES, + DEFAULT_VIDEO_MODEL, EDITOR_IMAGE_DIMENSION_OPTIONS, EDITOR_VIDEO_MODEL_OPTIONS, ICON_DESCRIPTION_LIMIT, @@ -203,7 +204,7 @@ export function createVideoGenerationDialogDraft({ status: 'idle', composerOpen: true, generationReferences: [], - videoModel: EDITOR_VIDEO_MODEL_OPTIONS[0].value, + videoModel: DEFAULT_VIDEO_MODEL, videoAspectRatio: '16:9', videoResolution: '480p', videoDurationSeconds: 4, diff --git a/src/components/image-editor/ImageCanvasGenerationModel.ts b/src/components/image-editor/ImageCanvasGenerationModel.ts index 34e942e82..38774433e 100644 --- a/src/components/image-editor/ImageCanvasGenerationModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationModel.ts @@ -78,7 +78,10 @@ export const EDITOR_IMAGE_DIMENSION_OPTIONS = { imageSizes: ['1K', '2K'], }, } as const; -export const CHARACTER_ANIMATION_MODEL = 'seedance2.0'; +export const VIDEO_MODEL_SEEDANCE_2 = 'seedance2.0'; +export const VIDEO_MODEL_SEEDANCE_2_FAST = 'seedance2.0-fast'; +export const DEFAULT_VIDEO_MODEL = VIDEO_MODEL_SEEDANCE_2_FAST; +export const CHARACTER_ANIMATION_MODEL = VIDEO_MODEL_SEEDANCE_2_FAST; export const CHARACTER_ANIMATION_ACTION_PROMPTS = [ { label: '待机', text: '待机动作,轻微呼吸起伏。' }, { label: '行走', text: '循环行走动作,步伐稳定。' }, @@ -107,8 +110,8 @@ export const CHARACTER_ANIMATION_DURATION_OPTIONS = [ export const VIDEO_FRAME_ORIGINAL_SIZE = { width: 1280, height: 720 }; export const VIDEO_FRAME_DISPLAY_SIZE = { width: 560, height: 315 }; export const EDITOR_VIDEO_MODEL_OPTIONS = [ - { label: 'Seedance 2.0', value: 'seedance2.0' }, - { label: 'Seedance 2.0 Fast', value: 'seedance2.0-fast' }, + { label: 'Seedance 2.0 Fast', value: VIDEO_MODEL_SEEDANCE_2_FAST }, + { label: 'Seedance 2.0', value: VIDEO_MODEL_SEEDANCE_2 }, { label: 'Kling 3.0', value: 'kling3.0' }, { label: 'Kling 3.0 Omni', value: 'kling3.0-omni' }, ] as const; diff --git a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts index 56b4d1f17..a5000712d 100644 --- a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts +++ b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts @@ -37,11 +37,14 @@ describe('ImageCanvasGenerationSubmissionModel', () => { nextGeneratedIndex: 3, }); - expect(plan).toEqual({ + expect(plan).toMatchObject({ kind: 'image', normalizedPrompt: '一张发光主视觉', input: { prompt: '一张发光主视觉', + model: 'gemini-3.1-flash-image-preview', + aspectRatio: '1:1', + imageSize: '1K', }, result: { generationInputs: { @@ -49,6 +52,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => { references: [], }, }, + rememberImageModel: 'gemini-3.1-flash-image-preview', }); }); @@ -326,7 +330,64 @@ describe('ImageCanvasGenerationSubmissionModel', () => { frameCount: 48, durationSeconds: 6, priceMudPoints: 120, + model: 'seedance2.0-fast', + }, + }); + }); + + it('builds video generation plans with seedance fast as the fallback model', () => { + const plan = buildImageGenerationSubmissionPlan({ + dialog: { + mode: 'video', + prompt: ' 角色绕场一周 ', + status: 'idle', + generationReferences: [], + videoResolution: '480p', + videoDurationSeconds: 4, + }, + layers: [], + nextGeneratedIndex: 3, + }); + + expect(plan).toMatchObject({ + kind: 'video', + normalizedPrompt: '角色绕场一周', + input: { + model: 'seedance2.0-fast', + priceMudPoints: 40, + }, + result: { + title: '生成视频 3', + }, + }); + }); + + it('builds video generation plans with explicit seedance standard model', () => { + const plan = buildImageGenerationSubmissionPlan({ + dialog: { + mode: 'video', + prompt: ' 生成电影感开场动画 ', + status: 'idle', + videoModel: 'seedance2.0', + videoResolution: '720p', + videoDurationSeconds: 5, + }, + layers: [], + nextGeneratedIndex: 7, + }); + + expect(plan).toMatchObject({ + kind: 'video', + normalizedPrompt: '生成电影感开场动画', + input: { + prompt: '生成电影感开场动画', model: 'seedance2.0', + resolution: '720p', + durationSeconds: 5, + priceMudPoints: 100, + }, + result: { + title: '生成视频 7', }, }); }); diff --git a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts index 04611cde8..570db673a 100644 --- a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts @@ -15,6 +15,7 @@ import { DEFAULT_IMAGE_MODEL, DEFAULT_ICON_DESCRIPTIONS, DEFAULT_SPEC_FORM_VALUES, + DEFAULT_VIDEO_MODEL, SPEC_GENERATION_SIZE, SPEC_TYPE_LABEL, buildIconGenerationInputs, @@ -200,7 +201,7 @@ export function buildImageGenerationSubmissionPlan({ if (dialog.mode === 'video') { const resolution = dialog.videoResolution ?? '480p'; const durationSeconds = dialog.videoDurationSeconds ?? 4; - const model = dialog.videoModel ?? 'seedance2.0'; + const model = dialog.videoModel ?? DEFAULT_VIDEO_MODEL; return { kind: 'video', normalizedPrompt, diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx index fc18e7649..de2aaf3f7 100644 --- a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx @@ -589,7 +589,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { sourceLayerId: 'character-layer', sourceImageSrc: 'generated/character.png', promptText: '跑步', - model: 'seedance2.0', + model: 'seedance2.0-fast', }), ); }); diff --git a/src/services/image-editor/editorProjectClient.test.ts b/src/services/image-editor/editorProjectClient.test.ts index de787e19e..01c712ce6 100644 --- a/src/services/image-editor/editorProjectClient.test.ts +++ b/src/services/image-editor/editorProjectClient.test.ts @@ -702,7 +702,7 @@ describe('editorProjectClient', () => { it('generates editor character animations through the backend BFF', async () => { requestJsonMock.mockResolvedValueOnce({ taskId: 'character-animation-1', - model: 'seedance2.0', + model: 'seedance2.0-fast', prompt: '生成游戏角色动画\n动作描述:\n待机', previewVideoPath: '/generated-character-drafts/editor/preview.mp4', frames: [ @@ -730,7 +730,7 @@ describe('editorProjectClient', () => { frameCount: 32, durationSeconds: 4, priceMudPoints: 40, - model: 'seedance2.0', + model: 'seedance2.0-fast', }); expect(result.taskId).toBe('character-animation-1'); @@ -750,7 +750,7 @@ describe('editorProjectClient', () => { frameCount: 32, durationSeconds: 4, priceMudPoints: 40, - model: 'seedance2.0', + model: 'seedance2.0-fast', }), }), '生成角色动画失败', diff --git a/src/services/image-editor/editorProjectClient.ts b/src/services/image-editor/editorProjectClient.ts index 985bfdfb7..716cfc3d9 100644 --- a/src/services/image-editor/editorProjectClient.ts +++ b/src/services/image-editor/editorProjectClient.ts @@ -164,7 +164,7 @@ export type EditorCharacterAnimationGenerationInput = { frameCount: EditorCharacterAnimationFrameCount; durationSeconds: EditorCharacterAnimationDurationSeconds; priceMudPoints: number; - model: 'seedance2.0'; + model: 'seedance2.0-fast'; }; export type EditorCharacterAnimationFrameResult = { @@ -176,7 +176,7 @@ export type EditorCharacterAnimationFrameResult = { export type EditorCharacterAnimationGenerationResult = { taskId: string; - model: 'seedance2.0'; + model: 'seedance2.0-fast'; prompt: string; previewVideoPath: string; frames: EditorCharacterAnimationFrameResult[];