From 2095d8686a61ad1e70b4dd3d58315a361a96eed7 Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 7 Jul 2026 04:04:37 +0000 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E7=94=BB=E5=B8=83=E7=94=9F?= =?UTF-8?q?=E5=9B=BE=E6=8A=A0=E5=9B=BE=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除前端用户侧背景色和抠图模型选择入口,提交路径固定使用 auto 与 birefnet。 清理生成输入快照和恢复逻辑中的抠图背景色与抠图模型残留。 保留后端 BgFilter 内部兼容能力,并修正空图标描述不再兜底默认描述提交。 同步更新编辑器和项目记忆文档。 --- .../shared-memory/decision-log.md | 6 +- ...架构】图片画布编辑器MVP接入方案-2026-06-11.md | 10 +- ...】server-rs与SpacetimeDB数据契约-2026-05-15.md | 2 +- ...】生成类面板Lovart统一改造方案-2026-06-17.md | 2 +- ...辑器】画板UI设计图生成入口设计-2026-06-17.md | 4 +- ...辑器】画板图标素材生成入口设计-2026-06-15.md | 6 +- ...辑器】画板角色形象生成入口设计-2026-06-15.md | 10 +- scripts/check-bgfilter-live.mjs | 21 +- scripts/generate-gpt-image-2-elf-archer.mjs | 266 ------------------ .../crates/api-server/src/editor_project.rs | 3 +- ...eCanvasCharacterGenerationComposerView.tsx | 2 - ...CanvasEditorGenerationIntegration.test.tsx | 8 - .../image-editor/ImageCanvasEditorModel.ts | 1 - .../ImageCanvasEditorShellView.test.tsx | 4 - .../image-editor/ImageCanvasEditorTypes.ts | 2 - .../image-editor/ImageCanvasEditorView.tsx | 4 - .../ImageCanvasGenerationDialogModel.test.ts | 6 - .../ImageCanvasGenerationDialogModel.ts | 17 -- ...eCanvasGenerationImageOptionsView.test.tsx | 91 +----- .../ImageCanvasGenerationImageOptionsView.tsx | 202 +------------ .../ImageCanvasGenerationModel.test.ts | 41 +-- .../ImageCanvasGenerationModel.ts | 156 +--------- ...ageCanvasGenerationSubmissionModel.test.ts | 31 +- .../ImageCanvasGenerationSubmissionModel.ts | 23 +- ...ImageCanvasIconSpritesheetComposerView.tsx | 2 - .../image-editor/ImageCanvasStageView.tsx | 6 - .../ImageCanvasUiAssetExtractionModel.ts | 6 - ...anvasUiAssetExtractionOverlayView.test.tsx | 100 +------ ...mageCanvasUiAssetExtractionOverlayView.tsx | 160 ----------- ...anvasGenerationSubmissionWorkflow.test.tsx | 12 - ...ImageCanvasGenerationSubmissionWorkflow.ts | 16 +- .../useImageCanvasGenerationWorkflow.test.tsx | 6 +- .../useImageCanvasGenerationWorkflow.ts | 54 ---- src/index.css | 129 +-------- 34 files changed, 74 insertions(+), 1335 deletions(-) delete mode 100644 scripts/generate-gpt-image-2-elf-archer.mjs diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index a2a9e33d0..f09ef1a42 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -19,7 +19,7 @@ ## 2026-07-02 图片画布生成抠图背景色使用 screenColor 传递 - 背景:画布角色、图标和 UI 素材生成过去固定要求 `#00FF00` 绿幕,后续 BGfilter 服务需要按生成时背景色做去背景,不能继续把背景色写死在 prompt 或后处理里。 -- 决策:角色形象、图标 spritesheet 和 UI 设计图素材提取新增用户可选纯色抠图背景。前端下拉展示颜色名、方块色样和 hex,默认 `自动`;手动可选 `浅雾蓝 #CFEFFF`、`浅钢蓝 #B0C2E0`、`暖浅桃色 #FFD6C2`、`淡薰衣草紫 #E6D8FF`、`浅粉灰 #F4D8E8`、`中度天蓝 #7FB3FF`、`浅柠黄 #FFF2A8`、`淡薄荷绿 #CFFFE1`、`浅中性灰 #D8DEE8`、`淡灰紫 #D8D2E8`、`高对比浅青 #A8F7F0`。请求字段统一为前端 / JSON `screenColor`、Rust `screen_color`;生成输入快照写入 `抠图背景色`,同源重绘可从快照恢复。api-server 接受 `auto` 或上述 11 个 hex,缺省或空值按 `auto` 处理;手动未知值返回 `400`;生成后 BgFilter 去背景用解析后的具体 hex 作为 `screen_color`。角色动作抽帧暂不接入该选择,继续使用 legacy `#00FF00` 绿幕。 +- 决策:角色形象、图标 spritesheet 和 UI 设计图素材提取不再向用户提供手动抠图背景色选择;前端用户路径统一提交 `screenColor=auto`,但用户可见生成输入快照不再写入 `抠图背景色` 或 `抠图模型`。api-server 在 11 个候选色中自动决策具体 hex,失败后兜底 `#CFEFFF`;最终 prompt 和 BgFilter 去背景只接收解析后的具体 hex 作为 `screen_color`。后端仍保留手动 hex 解析能力供内部兼容,角色动作抽帧暂不接入该选择,继续使用 legacy `#00FF00` 绿幕。 - 影响范围:`/editor/canvas` 角色形象生成、图标素材生成、UI 设计图素材提取、BgFilter 服务入参、图片画布 MVP 和角色形象生成设计文档。 - 验证方式:运行画布生成模型 / workflow / API client 定向前端测试、`cargo test -p api-server editor_green_screen --manifest-path server-rs/Cargo.toml`、`cargo test -p platform-image generated_asset_sheet_light_blue_key_color_removes_selected_background --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`。 @@ -27,7 +27,7 @@ ## 2026-07-05 图片画布抠图背景色自动决策 - 背景:手动背景色选择对用户负担较高,且不同角色、图标和 UI 素材主题需要避开不同主体色;但 BgFilter 和生成 prompt 仍必须拿到明确的纯色 hex。 -- 决策:前端把 `自动` 作为默认背景色选项并继续通过 `screenColor` 提交;api-server 新增 `editor_screen_background_decision` 模块,在角色形象、图标 spritesheet 和 UI 设计图素材提取组装 prompt 前解析 `screenColor`。手动 hex 直接校验并使用;`auto` 通过服务端 LLM 在 11 个候选色中选择具体 hex,最多重试 3 次,LLM 未配置、请求失败或返回非法颜色时 fallback 到 `浅雾蓝 #CFEFFF`。自动解析结果会写入生成输入快照 `抠图背景色`,格式为 `自动 -> <颜色名> `,兜底时追加 `(兜底)`;最终生图 prompt 和 BgFilter `screen_color` 永远只接收具体 hex,不透传 `auto`。 +- 决策:前端用户路径直接固定通过 `screenColor=auto` 提交,不再展示背景色选项;api-server 新增 `editor_screen_background_decision` 模块,在角色形象、图标 spritesheet 和 UI 设计图素材提取组装 prompt 前解析 `screenColor`。手动 hex 直接校验并使用;`auto` 通过服务端 LLM 在 11 个候选色中选择具体 hex,最多重试 3 次,LLM 未配置、请求失败或返回非法颜色时 fallback 到 `浅雾蓝 #CFEFFF`。自动解析结果不写入用户可见生成输入快照;最终生图 prompt 和 BgFilter `screen_color` 永远只接收具体 hex,不透传 `auto`。 - 影响范围:`/editor/canvas` 角色形象生成、图标素材生成、UI 设计图素材提取、api-server LLM 调用、BgFilter 参数、图片画布文档。 - 验证方式:运行背景决策模块单测、画布生成模型 / workflow / API client 定向前端测试、`cargo test -p api-server editor_screen_background_decision editor_green_screen --manifest-path server-rs/Cargo.toml`、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`、`docs/【编辑器】画板UI设计图生成入口设计-2026-06-17.md`、`docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`。 @@ -43,7 +43,7 @@ ## 2026-07-03 图片画布生成纯色背景资产接入 BgFilter - 背景:独立 BgFilter 服务已部署在 image host,并提供 `POST /bgfilter/remove-background`,支持显式 `screen_color` 和 `seg_model`。手动去背景已有独立 BiRefNet BFF,不能把两个服务的配置或语义混在一起。 -- 决策:角色形象生成、图标 spritesheet 生成和 UI 设计图素材提取在保存带纯色背景源图后,统一调用 BgFilter 生成透明 PNG;请求 multipart 字段为 `file`、`screen_color=` 和 `seg_model=`,当前用户界面只展示并提交默认 `birefnet`,`anime-seg` 路径保留为后端可识别的内部能力但不对用户可见。BgFilter 使用独立配置 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL`、`GENARRATIVE_EDITOR_BGFILTER_TOKEN`、`GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS`,默认 base URL 为 `http://58.87.105.82/bgfilter`,token 未配置时复用 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN`。手动 `POST /api/editor/images/background-removals` 继续使用独立 BiRefNet 配置 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL`,不受 BgFilter 影响。BgFilter 参数里的 `seg_model=birefnet` 只表示 BgFilter 内部分割后端,不等于手动去背景的独立 BiRefNet 服务。若 BgFilter 失败,api-server 对这些标准纯色背景生成图使用本地 `editor_green_screen` 兜底。角色动作抽帧仍保留 legacy `#00FF00` 与本地 `editor_green_screen` 透明化。 +- 决策:角色形象生成、图标 spritesheet 生成和 UI 设计图素材提取在保存带纯色背景源图后,统一调用 BgFilter 生成透明 PNG;请求 multipart 字段为 `file`、`screen_color=` 和 `seg_model=`,前端用户路径固定提交 `segModel=birefnet` 且不展示抠图模型选择;`anime-seg` 路径保留为后端可识别的内部能力但不对用户可见。BgFilter 使用独立配置 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL`、`GENARRATIVE_EDITOR_BGFILTER_TOKEN`、`GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS`,默认 base URL 为 `http://58.87.105.82/bgfilter`,token 未配置时复用 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN`。手动 `POST /api/editor/images/background-removals` 继续使用独立 BiRefNet 配置 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL`,不受 BgFilter 影响。BgFilter 参数里的 `seg_model=birefnet` 只表示 BgFilter 内部分割后端,不等于手动去背景的独立 BiRefNet 服务。若 BgFilter 失败,api-server 对这些标准纯色背景生成图使用本地 `editor_green_screen` 兜底。角色动作抽帧仍保留 legacy `#00FF00` 与本地 `editor_green_screen` 透明化。 - 影响范围:`server-rs/crates/api-server/src/config.rs`、`server-rs/crates/api-server/src/editor_project.rs`、图片画布 MVP 文档和角色形象生成设计文档。 - 验证方式:运行 `cargo test -p api-server config::tests::from_env_reads_editor_bgfilter_settings_and_reuses_background_token editor_project::tests::editor_canvas_screen_background_generation_uses_bgfilter_postprocess --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index 17e27324e..a3d14d34b 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -20,8 +20,8 @@ - 图片拖拽时显示水平 / 垂直吸附参考线,吸附到其它图层、生成占位框或画板的边缘与中心线;当移动元素接近两个同轴元素形成的等距位置时,支持横向或纵向等距吸附。 - 生成资源右上角显示元数据按钮,点击打开独立元数据窗口。图片信息页不展示后端组装后的生图 Prompt,也不提供复制 Prompt;只展示该图片生成时用户在面板里提交的输入快照,包括普通生成提示词、规范表单字段、角色设定、图标素材描述、快速编辑提示词、重绘提示词,以及角色规范 / 常规参考图 / 图标规范 / 编辑参考图等参考图卡片,并提供“复制信息”复制当前可见字段。参考图输入快照只保存 `refType/refId` 行引用,其中 `refType="project-resource"` 指向 `editor_project_resource.resourceId`,`refType="asset"` 指向 `editor_asset.assetId`;不得把图片 Data URL、普通 URL 或 `objectKey` 写入 `generationInputs.references`。旧数据或上传图片没有输入快照时显示 `-`,禁止回退展示内部 Prompt。 - 对生成资源执行重绘时,在右侧创建新的生成结果图层,并自动调整视图显示原图和新图;重绘面板不因提交成功自动关闭,便于连续改提示词。重绘 / 改造输入框只允许从 `generationInputs.fields` 中恢复用户可见输入快照,例如普通生成提示词、视频描述、音效 `prompt`、背景音乐 `gpt_description_prompt`、角色设定、UI 用户输入、图标素材描述、规范表单和宣发素材字段;禁止回退展示资源 `prompt` / `actualPrompt` 中的后端拼接 Prompt、固定生成模板或模型默认提示词。没有用户输入快照的旧图层打开改造时保持空输入,等待用户重新填写。 -- 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和重绘保留既有 `gpt-image-2` 路径;图片快速编辑统一打开框选区域 + 单提示词 + 模型选择面板,默认沿用原图模型,不展示参考图或比例 / 尺寸控件;其中生成规范类图片固定 `16:9`、`2K`、`gpt-image-2`,面板底部用与可编辑面板一致的比例 / 尺寸 / 模型胶囊按钮展示固定参数,但按钮为禁用态,不允许在该面板改比例、尺寸或模型。`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型;两类面板额外提供抠图背景色下拉和抠图模型下拉;背景色展示颜色名称、方块色样和 hex 值,默认 `自动`,手动可选 `浅雾蓝 #CFEFFF`、`浅钢蓝 #B0C2E0`、`暖浅桃色 #FFD6C2`、`淡薰衣草紫 #E6D8FF`、`浅粉灰 #F4D8E8`、`中度天蓝 #7FB3FF`、`浅柠黄 #FFF2A8`、`淡薄荷绿 #CFFFE1`、`浅中性灰 #D8DEE8`、`淡灰紫 #D8D2E8`、`高对比浅青 #A8F7F0`;抠图模型当前仅对用户展示默认 `birefnet`,`anime-seg` 作为内部保留能力不在用户界面暴露。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。宣发素材三个工作流(游戏首图、详情五图、运营海报)固定使用 `gpt-image-2`,面板模型胶囊为禁用态,不提供 `nanobanana2` 入口;前端按 workflow 同时提交 `outputSize`、`aspectRatio` 和 `imageSize`,其中游戏首图为 `720x540 / 4:3`、详情单图为 `720x1280 / 9:16`、运营海报为 `1280x720 / 16:9`;后端收到 `kind: "publication-material"` 时也强制归一为 `gpt-image-2` 生成和计费,生成回填图层优先使用生成占位的 `originalWidth/originalHeight`,即使上游回包尺寸漂移也不得把宣发素材卡片变成随机 `1:1` 或 `4:3`。纯文本生成走 `/api/editor/images/generations`,重绘在前端读入当前图层图片 Data URL 后走同一图片生成 BFF,并在原图右侧生成一张新图;普通图层重绘作为 `quick-edit` 参考图提交,角色图层重绘必须按 `kind: "character"` 提交,继续套用角色生成器提示词限定、透明 PNG 后处理和角色资产持久化。`生成视频` 走 `/api/editor/videos/generations`,前端模型入口仅展示 Seedance 2.0 Fast / Seedance 2.0 / Kling 3.0 / Kling 3.0 Omni,不展示 Veo 入口,默认 Seedance 2.0 Fast;视频参数按当前正式面板支持的比例、时长、清晰度和声音开关提交,且 Seedance Fast 与 Seedance 标准版必须按各自真实模型 ID 独立映射,不得混用。生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成图片后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。图片快速编辑和重绘在调用图片 BFF 前必须把当前图层图片源读取为图片 Data URL;视频素材快速编辑走视频生成 BFF,不允许走图片模型;角色动作的 `生成动画` 仍固定使用 `seedance2.0-fast` 动作 / 视频模型,角色动作素材的 `快速编辑` 按当前帧图片走图片编辑。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。 -- 图片画布抠图分两类:手动去除背景面向用户任意图片,走登录态同源 BFF `POST /api/editor/images/background-removals` 并转发远端 BiRefNet;编辑器自己生成的标准纯色背景抠图资产在保存源图后统一调用独立 BgFilter 服务 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL/remove-background`,默认 `http://58.87.105.82/bgfilter/remove-background`。角色形象生成、图标 spritesheet 生成和 UI 设计图素材提取都必须把所选 `screenColor` 注入请求体和 `generationInputs.fields` 的 `抠图背景色`;`screenColor=auto` 时由 api-server 在组装 prompt 前调用背景决策模块,从上述 11 色中选择具体 hex,最多重试 3 次,失败后兜底 `#CFEFFF`;手动未知 hex 返回 `400`。最终生图 prompt 和 BgFilter `screen_color` multipart 字段只接收解析后的具体 hex,不透传 `auto`。三条 BgFilter 路径还必须把用户可见的默认 `segModel=birefnet` 传为 `seg_model`;后端仍保留识别 `anime-seg` 的内部兼容能力,但前端用户入口不展示也不提交该值。这里的 `birefnet` 只是 BgFilter 管线内部后端,不等同于手动去背景的独立 BiRefNet 服务。后端在调用 BgFilter 前必须先把带纯色背景 / 绿幕源图写入 OSS;若 BgFilter 请求失败、返回非成功状态、空图片或非法图片,api-server 对这些标准纯色背景生成图使用本地 `editor_green_screen` 按同一 `screenColor` 兜底去背。角色动作抽帧后的序列帧暂不接用户背景色选择,仍沿用 legacy `#00FF00` 绿幕和本地 `editor_green_screen` 透明化,不依赖 BiRefNet 或 BgFilter。BiRefNet 手动去背景服务地址为 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL/remove-background`,默认 `http://58.87.105.82/remove-background`;BgFilter 可选访问令牌来自 `GENARRATIVE_EDITOR_BGFILTER_TOKEN`,未配置时复用 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN`,所有令牌都只在服务端注入,前端不持有令牌。api-server 对上游结果做响应字节和图片尺寸上限保护,并先落 OSS / asset object,再返回 `imageSrc/objectKey/assetObjectId/taskId`;queue 模式下手动去背景进入 SpacetimeDB 外部生成队列,画布任务侧栏只展示服务器任务阶段,生成中才显示耗时,不显示百分比;有项目上下文时前端同时创建去背景生成占位并把 `canvasCompletion` 交给后端,完成后由后端写入结果图层和最新项目快照。 +- 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和重绘保留既有 `gpt-image-2` 路径;图片快速编辑统一打开框选区域 + 单提示词 + 模型选择面板,默认沿用原图模型,不展示参考图或比例 / 尺寸控件;其中生成规范类图片固定 `16:9`、`2K`、`gpt-image-2`,面板底部用与可编辑面板一致的比例 / 尺寸 / 模型胶囊按钮展示固定参数,但按钮为禁用态,不允许在该面板改比例、尺寸或模型。`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型;两类面板不展示抠图背景色或抠图模型选择;前端用户路径固定提交 `screenColor=auto` 和 `segModel=birefnet`,由后端自动决策具体抠图背景色,`anime-seg` 作为内部保留能力不在用户界面暴露。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。宣发素材三个工作流(游戏首图、详情五图、运营海报)固定使用 `gpt-image-2`,面板模型胶囊为禁用态,不提供 `nanobanana2` 入口;前端按 workflow 同时提交 `outputSize`、`aspectRatio` 和 `imageSize`,其中游戏首图为 `720x540 / 4:3`、详情单图为 `720x1280 / 9:16`、运营海报为 `1280x720 / 16:9`;后端收到 `kind: "publication-material"` 时也强制归一为 `gpt-image-2` 生成和计费,生成回填图层优先使用生成占位的 `originalWidth/originalHeight`,即使上游回包尺寸漂移也不得把宣发素材卡片变成随机 `1:1` 或 `4:3`。纯文本生成走 `/api/editor/images/generations`,重绘在前端读入当前图层图片 Data URL 后走同一图片生成 BFF,并在原图右侧生成一张新图;普通图层重绘作为 `quick-edit` 参考图提交,角色图层重绘必须按 `kind: "character"` 提交,继续套用角色生成器提示词限定、透明 PNG 后处理和角色资产持久化。`生成视频` 走 `/api/editor/videos/generations`,前端模型入口仅展示 Seedance 2.0 Fast / Seedance 2.0 / Kling 3.0 / Kling 3.0 Omni,不展示 Veo 入口,默认 Seedance 2.0 Fast;视频参数按当前正式面板支持的比例、时长、清晰度和声音开关提交,且 Seedance Fast 与 Seedance 标准版必须按各自真实模型 ID 独立映射,不得混用。生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成图片后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。图片快速编辑和重绘在调用图片 BFF 前必须把当前图层图片源读取为图片 Data URL;视频素材快速编辑走视频生成 BFF,不允许走图片模型;角色动作的 `生成动画` 仍固定使用 `seedance2.0-fast` 动作 / 视频模型,角色动作素材的 `快速编辑` 按当前帧图片走图片编辑。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。 +- 图片画布抠图分两类:手动去除背景面向用户任意图片,走登录态同源 BFF `POST /api/editor/images/background-removals` 并转发远端 BiRefNet;编辑器自己生成的标准纯色背景抠图资产在保存源图后统一调用独立 BgFilter 服务 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL/remove-background`,默认 `http://58.87.105.82/bgfilter/remove-background`。角色形象生成、图标 spritesheet 生成和 UI 设计图素材提取的前端用户路径都固定把 `screenColor=auto` 注入请求体,但用户可见 `generationInputs.fields` 不再记录 `抠图背景色` 或 `抠图模型`;api-server 在组装 prompt 前调用背景决策模块,从 11 个候选色中选择具体 hex,最多重试 3 次,失败后兜底 `#CFEFFF`。后端仍保留手动 hex 解析能力供内部兼容。最终生图 prompt 和 BgFilter `screen_color` multipart 字段只接收解析后的具体 hex,不透传 `auto`。三条 BgFilter 路径还必须固定把默认 `segModel=birefnet` 传为 `seg_model`;后端仍保留识别 `anime-seg` 的内部兼容能力,但前端用户入口不展示也不提交该值。这里的 `birefnet` 只是 BgFilter 管线内部后端,不等同于手动去背景的独立 BiRefNet 服务。后端在调用 BgFilter 前必须先把带纯色背景 / 绿幕源图写入 OSS;若 BgFilter 请求失败、返回非成功状态、空图片或非法图片,api-server 对这些标准纯色背景生成图使用本地 `editor_green_screen` 按同一 `screenColor` 兜底去背。角色动作抽帧后的序列帧暂不接用户背景色选择,仍沿用 legacy `#00FF00` 绿幕和本地 `editor_green_screen` 透明化,不依赖 BiRefNet 或 BgFilter。BiRefNet 手动去背景服务地址为 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL/remove-background`,默认 `http://58.87.105.82/remove-background`;BgFilter 可选访问令牌来自 `GENARRATIVE_EDITOR_BGFILTER_TOKEN`,未配置时复用 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN`,所有令牌都只在服务端注入,前端不持有令牌。api-server 对上游结果做响应字节和图片尺寸上限保护,并先落 OSS / asset object,再返回 `imageSrc/objectKey/assetObjectId/taskId`;queue 模式下手动去背景进入 SpacetimeDB 外部生成队列,画布任务侧栏只展示服务器任务阶段,生成中才显示耗时,不显示百分比;有项目上下文时前端同时创建去背景生成占位并把 `canvasCompletion` 交给后端,完成后由后端写入结果图层和最新项目快照。 - 图片快速编辑面板只保留一个提示词输入框和模型选择,不展示额外参考图或比例 / 尺寸控件;原图 / 原素材作为 `/api/editor/images/edits` 的 `sourceImageSrc` 直接提交,不作为 `referenceImageSrcs`。打开快速编辑时画布必须自动平移缩放,让原素材完整落在可视区上半部分,底部面板固定出现在素材下方且不遮挡内容,竖屏 UI 素材也必须完整展示。快速编辑右侧显示矩形、椭圆、画笔框选工具,但进入时不默认启用;点击工具后显示选中态,再点同一工具取消启用。完成框选后,画布红色细框显示连续序号,提示词可按这些编号填写每个区域怎么改。点击 `修改` 后仍停留在当前快速编辑面板显示修改中,不创建独立 `Quick Edit Generator` 画布占位;生成成功后直接用结果覆盖原图图层,失败时保留当前面板并显示错误。 - 底部生成类按钮每次点击都必须创建独立的画布生成对象;新建规范、角色形象或图标素材时,只切换当前编辑面板,不得销毁此前尚未生成或已生成后的其它生成对象状态。归档为非当前编辑对象的生成占位仍可拖动、删除和等待异步完成,完成 / 失败回写必须按生成对象 ID 读取最新占位状态,不能使用提交瞬间的旧快照。 - 画布右上角提供自动隐藏任务侧栏。列表为空且侧栏关闭时只保留图标开关;生成或去背景任务进入时默认打开;用户可手动切换开关状态。 @@ -53,7 +53,7 @@ - 项目封面图是画布当前视口栅格化后的静态快照资源,不在项目列表页临时重放 `layers + viewport`。前端在项目加载后和防抖保存 layout 时生成 320x240 PNG,走私有 OSS / asset object 上传,再创建 `editor_project_resource`,其中 `assetKind="project-cover-snapshot"`、`sourceType="uploaded"`。`/project` 与 `/creation` 最近项目卡只读取最新封面快照资源渲染;没有封面快照时显示普通项目占位,不回退为实时画布组合。 - 图片、音频、视频和角色动画帧文件本体继续走 OSS / asset object;浏览器读取私有 generated 对象统一经 `/api/assets/read-url` 换签,签名 URL 可在 session 内复用,但不得作为持久化真相。`/api/assets/read-url` 属于页面展示层高频后台请求,前端统一在 `assetReadUrlService` 内做同 key pending 去重、session 缓存和跨组件节流;UI 设计切片、角色动画帧或大量素材恢复时不得绕过该服务并发换签,否则单页可在同一秒内打满发布入口 `genarrative_api_rps` burst。 - 登录态上传和生成结果必须先落 OSS / asset object,再向 `editor_project_resource` / `editor_asset` 写入轻量 `imageSrc: "/"`、`objectKey` 和 `assetObjectId`;未登录演示态可以在内存里使用 Data URL 预览,但项目、素材库、项目资源和 `editor_canvas.layers_json` 不得写入 `data:image/*`、`data:video/*`、`data:audio/*` 或 `blob:`。旧数据读取时如果已有 `objectKey`,`imageSrc` 归一成 `/`;没有 `objectKey` 的旧 Data URL 需要走修复上传并回写轻量引用。上传到生成面板参考图槽位的图片必须先创建 `editor_project_resource` 行;没有当前工程 ID 时才创建账号级 `editor_asset` 行,随后把对应 `resourceId` 或 `assetId` 写入参考图临时状态,生成请求仍使用临时状态中的图片源或 `objectKey`。 -- 资源表保存资源和素材级元数据;图层位置、层级、分组选中所需 ID 和 groupId 保存在 `editor_canvas` 的布局 JSON。布局 JSON 是混合数组:普通图层按 `layerId/resourceId` 保存,生成器占位和生成器对话框按 `itemType: "generation-dialog"` 保存,不新增单独表。普通图层的新保存不再把 `assetKind/generationInputs` 写入布局 JSON;刷新时优先从 `editor_project_resource` 恢复,旧布局中的同名字段只作为兼容兜底。生成器快照必须包含生成器 ID、模式、提示词、参数、参考图、状态、占位框位置和可选 `generatedLayerId`;角色、图标等纯色抠图生成器还必须保存并恢复 `screenColor`,同源重绘可从生成器快照或 `generationInputs.fields` 的 `抠图背景色` 恢复该值;宣发素材生成器还必须保存并恢复 `publicationWorkflowId`、`publicationGameInfo` 和 `publicationReferences`,避免刷新后生成卡片字段或参考图丢失。生成器快照中的参考图同样只保存 `resourceId/sourceAssetId` 行引用和展示所需 label,不保存图片 Data URL、signed URL 或 `objectKey`;刷新时用 `editor_project_resource` / `editor_asset` 行恢复临时生成请求所需图片源。生成成功后仍保存该快照,只是渲染时由 `generatedLayerId` 锚定到成品图层而不重复显示灰色占位框。`generationInputs.references` 是用户可见输入快照中的行级索引,只允许保存 `{ title, label, refType, refId }`;生成接口所需的图片 Data URL、signed URL 或 `objectKey` 只存在于提交前的临时参考图状态和请求体字段,不进入资源 / 素材元数据。图层展示尺寸不再作为独立 `Size` 真相保存,刷新与新建图层均按 `Resolution`(`originalWidth/originalHeight`)原分辨率显示。图层组第一版是画布内布局语义,不单独建表。 +- 资源表保存资源和素材级元数据;图层位置、层级、分组选中所需 ID 和 groupId 保存在 `editor_canvas` 的布局 JSON。布局 JSON 是混合数组:普通图层按 `layerId/resourceId` 保存,生成器占位和生成器对话框按 `itemType: "generation-dialog"` 保存,不新增单独表。普通图层的新保存不再把 `assetKind/generationInputs` 写入布局 JSON;刷新时优先从 `editor_project_resource` 恢复,旧布局中的同名字段只作为兼容兜底。生成器快照必须包含生成器 ID、模式、提示词、参数、参考图、状态、占位框位置和可选 `generatedLayerId`;角色、图标等纯色抠图生成器的前端用户路径不保存或恢复 `screenColor` / `segModel`,同源重绘也不再从 `generationInputs.fields` 恢复 `抠图背景色` 或 `抠图模型`;宣发素材生成器还必须保存并恢复 `publicationWorkflowId`、`publicationGameInfo` 和 `publicationReferences`,避免刷新后生成卡片字段或参考图丢失。生成器快照中的参考图同样只保存 `resourceId/sourceAssetId` 行引用和展示所需 label,不保存图片 Data URL、signed URL 或 `objectKey`;刷新时用 `editor_project_resource` / `editor_asset` 行恢复临时生成请求所需图片源。生成成功后仍保存该快照,只是渲染时由 `generatedLayerId` 锚定到成品图层而不重复显示灰色占位框。`generationInputs.references` 是用户可见输入快照中的行级索引,只允许保存 `{ title, label, refType, refId }`;生成接口所需的图片 Data URL、signed URL 或 `objectKey` 只存在于提交前的临时参考图状态和请求体字段,不进入资源 / 素材元数据。图层展示尺寸不再作为独立 `Size` 真相保存,刷新与新建图层均按 `Resolution`(`originalWidth/originalHeight`)原分辨率显示。图层组第一版是画布内布局语义,不单独建表。 - 图片类、生成视频和音频结果除作为 `editor_project_resource` 和画布图层保存外,还要写入账号级 `editor_asset` 素材库;该写入由生成 BFF 在请求携带 `assetFolderId` 时完成。`GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 下,画布图片、改图、图标素材、UI 素材提取、角色动作、视频、音效和背景音乐生成都先返回 `queueState`,前端轮询 `/api/runtime/external-generation/jobs/{jobId}` 到完成后重新读取项目快照;`inline` 或无项目上下文时才使用响应中的 resource / asset 快照做本地落画布兜底,不再把同一生成结果二次调用素材创建接口。生成请求失败、inline 完成或 queue 任务终态完成 / 失败后,右上角泥点 chip 必须通过 `/profile/dashboard` 回读余额,不做本地乐观扣减。生成视频会单独抽取首帧封面并写入 `thumbnailSrc`,素材栏和拖回画布时沿用该封面作为 poster。 - 画布 Agent 会话按“SpacetimeDB 元数据 + OSS 消息正文”存储:`editor_agent_conversation` 只保存 `conversationId/projectId/ownerUserId/title/messagesObjectKey/deleted/createdAt/updatedAt` 等会话元数据;消息正文整体保存为私有 OSS JSON 文档 `editor-agent/{conversationId}.json`。消息文档单对象上限为 2 MiB,同一会话的消息追加和 SSE 最终写回由 api-server 按 `conversationId` 串行化,避免“读 OSS → 改消息 → 写 OSS”并发覆盖。前端只通过 api-server BFF 读取和发送会话,不直接读写 SpacetimeDB,也不直接读写 OSS。 - Agent 消息附件只允许引用当前工程画布资源或账号素材库图片,来源类型为 `canvas_resource` / `library_asset`,最多 9 张。附件请求可携带展示用 `imageSrc/thumbnailSrc/objectKey/width/height/label`,但持久化真相仍以后端校验后的 resource / asset 行和 OSS 对象为准;不得把 Data URL、signed URL 或 blob URL 当作会话长期事实。 @@ -86,7 +86,7 @@ - `POST /api/editor/images/generations`:按提示词调用 VectorEngine 生成图片;角色生成可携带 `model`、`screenColor`、`segModel`、`aspectRatio`、`imageSize` 和 `referenceImageSrcs`,生成成功后 api-server 先保存带纯色背景源图,再调用 BgFilter 并传入 `screen_color=`、`seg_model=` 生成透明 PNG。宣发素材携带 `kind: "publication-material"` 时固定归一为 `gpt-image-2`,不支持 `nanobanana2`。`nanobanana2` 参考图作为 `inline_data` 进入 `generateContent`,`gpt-image-2` 参考图进入 edits。普通重绘继续走该接口并把当前图层图片作为参考图;图片快速编辑不走该接口。请求可携带 `projectId`、`assetFolderId`、`assetKind`、`generationInputs` 和 `sourceResourceId`,后端生成成功后创建 project resource / 账号素材并在响应中返回 resource / asset 快照。 - `POST /api/editor/images/background-removals`:接收当前图片源,校验登录态后由 api-server 解析为图片文件并转发到 BiRefNet 去背景服务;请求可携带 `projectId`、`targetLayerId`、`assetFolderId`、`assetLabel`、`sourceResourceId` 和 `canvasCompletion`,有 `canvasCompletion` 时完成后按生成占位写入结果图层,否则沿用旧的目标图层替换路径;响应返回 `imageSrc`、`objectKey`、`assetObjectId`、`width`、`height`、`taskId`、`elapsedMs`、`provider` 和可选 `project` 快照。服务地址由 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL` 配置,令牌只在服务端通过 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 注入。 - `POST /api/editor/icon-spritesheets/generations`:按图标规范图和素材描述数组生成 spritesheet,生成成功后 api-server 先保存带纯色背景 spritesheet 源图,再调用 BgFilter 生成透明 spritesheet。请求支持 `model`、`screenColor`、`segModel`、`aspectRatio`、`imageSize`、`priceMudPoints`、`projectId`、`assetFolderId` 和 `generationInputs`;`priceMudPoints` 必须来自编辑器生成计费配置中对应生图模型的尺寸档位(如 `nanobanana2` 的 `0.5K / 1K / 2K` 或 `gpt-image-2` 的 `1K / 2K`),后端用 `editor_generation_config` 校验后才调用上游;`nanobanana2` 走原生 `generateContent` 并写入 `generationConfig.imageConfig.aspectRatio/imageSize`,`0.5K` 传 `"512"`;`gpt-image-2` 走 `/v1/images/edits`。后端保存透明 spritesheet project resource / 账号素材,并随响应返回对应快照。 -- `POST /api/editor/ui-designs/assets/extractions`:以前端已绘入红色框选轮廓的 UI 设计图 Data URL 作为参考图,固定 `gpt-image-2` 和所选纯色背景素材提取提示词生成素材 spritesheet,生成成功后 api-server 先保存带纯色背景 spritesheet 源图,再调用 BgFilter 生成透明 spritesheet,并按连通域自动拆分为 `素材 1..N`,返回结构复用图标 spritesheet 响应。请求必须携带 `screenColor`、`segModel`、`aspectRatio: "1:1"`、`imageSize: "1K" | "2K"` 和 `priceMudPoints`;框选数量不超过 6 个时前端按 `1:1·1K` 与 gpt-image-2 1K 价格提交,超过 6 个时按 `1:1·2K` 与 2K 价格提交。后端必须在调用上游前校验比例、尺寸和泥点价格,只允许 `1:1 / 1K / 2K`。请求可携带 `projectId`、`assetFolderId`、`generationInputs` 和 `spritesheetLabel`,后端保存 spritesheet / 拆分素材并返回对应 resource / asset 快照;前端必须把 spritesheet 原图与拆分素材都加入画布。 +- `POST /api/editor/ui-designs/assets/extractions`:以前端已绘入红色框选轮廓的 UI 设计图 Data URL 作为参考图,固定 `gpt-image-2` 和自动决策纯色背景素材提取提示词生成素材 spritesheet,生成成功后 api-server 先保存带纯色背景 spritesheet 源图,再调用 BgFilter 生成透明 spritesheet,并按连通域自动拆分为 `素材 1..N`,返回结构复用图标 spritesheet 响应。请求必须携带 `screenColor`、`segModel`、`aspectRatio: "1:1"`、`imageSize: "1K" | "2K"` 和 `priceMudPoints`;框选数量不超过 6 个时前端按 `1:1·1K` 与 gpt-image-2 1K 价格提交,超过 6 个时按 `1:1·2K` 与 2K 价格提交。后端必须在调用上游前校验比例、尺寸和泥点价格,只允许 `1:1 / 1K / 2K`。请求可携带 `projectId`、`assetFolderId`、`generationInputs` 和 `spritesheetLabel`,后端保存 spritesheet / 拆分素材并返回对应 resource / asset 快照;前端必须把 spritesheet 原图与拆分素材都加入画布。 - `POST /api/editor/images/edits`:按提示词和当前图片 Data URL 调用 VectorEngine edits,返回新的生成图片元数据;接口能力仍可接收明确参考图,但图片快速编辑当前只提交 `sourceImageSrc`,不提交隐藏的 `referenceImageSrcs`。请求携带 project / asset 上下文时由后端创建新 resource / asset,前端只消费响应快照。 - `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`,两者不得混用。后端允许 6 类比例、4 到 15 秒整数、`480p / 720p / 1080p`,并拒绝 `seedance2.0-fast + 1080p`;`sound=on/off` 映射 Ark `generate_audio=true/false`。后端复用 Ark / VectorEngine content generation task 轮询链路,下载最终视频并持久化到 OSS;请求携带 `projectId` / `assetFolderId` 时同步创建 project resource / 账号素材并返回 `project` / `asset` 快照,基础响应返回 `videoSrc`、尺寸、prompt、model、provider、taskId、durationSeconds、resolution 和 `priceMudPoints`。 - `POST /api/editor/audios/sound-effects/generations` 与 `POST /api/editor/audios/background-music/generations`:按音效 / 背景音乐参数生成音频并持久化到 OSS;请求携带 `projectId` / `assetFolderId` 时同步创建 project resource / 账号素材并返回 `project` / `resource` / `asset` 快照,基础响应返回 `audioSrc`、prompt、model、provider、taskId、duration、歌词和 `priceMudPoints`。 @@ -126,7 +126,7 @@ - 发送消息后,面板展示用户消息、Agent 阶段状态和 SSE 增量回复;`stage/message_delta/tool_started/tool_completed/generation_result/error/done` 都能被正确渲染。流式响应中点击“停止”会中断当前请求,并把仍在 streaming / generating 的消息标记为停止态。 - Agent 返回生成结果缩略图后,点击缩略图应优先聚焦当前画布中已有 `resourceId` 对应图层;如果当前内存布局尚未包含该资源,则重新读取工程快照,应用后再聚焦新图层。对话入口触发生成时不创建“即将生成”画布占位;生成中状态只显示在消息流,生成完成后通过后端 `canvasCompletion` 落新图层。工具失败时消息内必须保留失败 generation record 和错误气泡,不能只弹一次性 toast。 - 画布 Agent 会话刷新后能从后端恢复会话标题、消息、附件和生成记录;前端不得根据本地临时状态伪造会话持久化结果。 -- 图片选中后的浮动工具栏按钮顺序固定为:快速编辑、分割线、裁扩按钮、去除背景按钮、UI设计图专属提取素材、角色图专属生成动画、分割线、重绘、下载按钮。裁扩通过画布边界拖拉完成,不再展示四边数值输入;默认自由比例,选择固定比例后拖拉边界保持对应比例,完成后在原素材旁边新增裁扩结果图层,扩展区域透明填充。去除背景调用同源 BFF `POST /api/editor/images/background-removals`,由 api-server 代理远端 BiRefNet 服务并持久化结果;有项目上下文时先在画布创建关闭面板的去背景生成占位,完成后由后端通过 `canvasCompletion` 把新 project resource 写入该占位并返回快照,无占位上下文时才用新的 project resource 引用替换当前图层。画布任务侧栏按“排队/生成中”和“已完成”分页,生成中排在排队前,生成中耗时从任务开始时间戳实时计算,排队中不计时;进行中任务只显示阶段文本和已用时,不显示百分比;完成态生成任务副标题显示用户提示词并单行截断;点击任务只聚焦对应画布内容,不激活生成面板或改变任务顺序,聚焦时必须预留图片上方工具栏、底部工具栏和可见生成对话框空间。UI设计图的提取素材必须先进入红框素材框选状态,默认启用矩形框选,右侧框选工具与快速编辑统一且可再次点击取消启用态,当前启用工具按钮必须保持高亮。素材提取面板必须在素材下方,使用与生成新素材一致的面板宽度和底部模型 / 按钮样式,提示语显示 `使用框选工具框选你希望从画面中提取的素材`,并展示按原图坐标准确裁剪的框选区域截图预览、固定模型 `gpt-image-2`、左下角计划规格 `1:1·1K/2K`、抠图背景色和 `提取 · N泥点` 按钮,不显示额外取消按钮;点击素材和面板以外的画布区域即退出 UI 素材提取。至少框选一个区域后才可提交,前端把红色轮廓绘入原图后固定走 `gpt-image-2` 和所选纯色背景素材提取提示词;生成的透明 spritesheet 原图和拆分后的独立素材都作为画布图层保留。 +- 图片选中后的浮动工具栏按钮顺序固定为:快速编辑、分割线、裁扩按钮、去除背景按钮、UI设计图专属提取素材、角色图专属生成动画、分割线、重绘、下载按钮。裁扩通过画布边界拖拉完成,不再展示四边数值输入;默认自由比例,选择固定比例后拖拉边界保持对应比例,完成后在原素材旁边新增裁扩结果图层,扩展区域透明填充。去除背景调用同源 BFF `POST /api/editor/images/background-removals`,由 api-server 代理远端 BiRefNet 服务并持久化结果;有项目上下文时先在画布创建关闭面板的去背景生成占位,完成后由后端通过 `canvasCompletion` 把新 project resource 写入该占位并返回快照,无占位上下文时才用新的 project resource 引用替换当前图层。画布任务侧栏按“排队/生成中”和“已完成”分页,生成中排在排队前,生成中耗时从任务开始时间戳实时计算,排队中不计时;进行中任务只显示阶段文本和已用时,不显示百分比;完成态生成任务副标题显示用户提示词并单行截断;点击任务只聚焦对应画布内容,不激活生成面板或改变任务顺序,聚焦时必须预留图片上方工具栏、底部工具栏和可见生成对话框空间。UI设计图的提取素材必须先进入红框素材框选状态,默认启用矩形框选,右侧框选工具与快速编辑统一且可再次点击取消启用态,当前启用工具按钮必须保持高亮。素材提取面板必须在素材下方,使用与生成新素材一致的面板宽度和底部模型 / 按钮样式,提示语显示 `使用框选工具框选你希望从画面中提取的素材`,并展示按原图坐标准确裁剪的框选区域截图预览、固定模型 `gpt-image-2`、左下角计划规格 `1:1·1K/2K` 和 `提取 · N泥点` 按钮,不显示额外取消按钮;点击素材和面板以外的画布区域即退出 UI 素材提取。至少框选一个区域后才可提交,前端把红色轮廓绘入原图后固定走 `gpt-image-2` 和自动决策纯色背景素材提取提示词;生成的透明 spritesheet 原图和拆分后的独立素材都作为画布图层保留。 - 重绘生成资源后,右侧出现新生成结果图层,并自动 fit 原图 + 新图,且重绘面板保持打开。 - 快速编辑 / 重绘站内 public 示例图、历史 generated 图或 OSS generated 图时,前端先读取成 `data:image/*;base64,...` 再提交,后端不得再收到 `/creation-type-references/*`、`/generated-*` 或 OSS URL 作为 `referenceImageSrcs/sourceImageSrc`。 - 快速编辑不保留额外参考图入口;点击修改时只把原图或红框序号标注图作为 `/api/editor/images/edits` 的 `sourceImageSrc` 提交给后端。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 2e6e508da..165fa2dc9 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -218,7 +218,7 @@ npm run check:server-rs-ddd - LLM:通用 LLM 门面继续使用 `GENARRATIVE_LLM_*`;创意 Agent `gpt-5.4-mini` Chat Completions 文本链路已于 2026-06 从 APIMart 迁移到 VectorEngine,使用 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 构造 OpenAI-compatible client,`api-server` 会把未带 `/v1` 的 VectorEngine base URL 规范化到 `/v1` 后请求 `/chat/completions`。通用 `/api/llm/chat/completions` 代理使用 `GENARRATIVE_LLM_PROVIDER=openai-compatible`、`GENARRATIVE_LLM_BASE_URL=https://api.vectorengine.cn/v1`、`GENARRATIVE_LLM_MODEL=gpt-5.4-mini`;未单独配置 `GENARRATIVE_LLM_API_KEY` 时可复用 `VECTOR_ENGINE_API_KEY`。`APIMART_BASE_URL` / `APIMART_API_KEY` 只作为历史残留,不再作为创意 Agent gpt-5.4-mini 客户端来源;后续排障时优先确认 VectorEngine `/v1/models`、`/v1/chat/completions` 和 `/v1/responses` 可用性。 - 图片生成:VectorEngine `gpt-image-2` 图片 provider 归属 `platform-image`,密钥只在后端环境变量中;`api-server` 内的 `openai_image_generation.rs` 只是兼容调用面和外部失败审计桥接,不再承载 provider 协议实现。实际外部生成运行记录统一落 `tracking_event`,`event_key = external_generation_run`,metadata 记录开始 / 结束时间、耗时、状态、成功标记、失败原因、provider task id 和结果摘要,不再写回过时的 `ai_task`。DashScope 只按仍在使用的历史能力单独处理,不作为 GPT-image-2 兜底。VectorEngine `/v1/images/generations` 和 `/v1/images/edits` 上游 POST 使用 `libcurl` 发送;`reqwest` 只保留给参考图 URL 下载和响应中图片 URL 下载。`/v1/images/edits` 的 multipart 参考图必须作为 libcurl 文件上传 part 发送,字段名为 `image`,实现上使用 `Form::buffer(file_name, bytes)` 并设置 `Content-Type`;不能只用 `contents(...).filename(...)`,否则上游会把请求转码为缺少图片并返回 `image is required`。`request_send` 阶段的 curl timeout / connect error 按可重试传输错误处理,最多尝试 5 次,并使用指数退避加短抖动;排障时优先看 `attempt`、`max_attempts`、`retry_delay_ms`、`reference_image_bytes_total` 和 `request_params`,不要把 `SendRequest` 当成上游业务错误。 -- 编辑器抠图服务:手动 `POST /api/editor/images/background-removals` 继续代理独立 BiRefNet 服务,配置为 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL`、`GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 和 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS`。角色形象生成、图标 spritesheet 生成和 UI 设计图素材提取的生成后纯色背景透明化改走独立 BgFilter 服务,配置为 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL`、`GENARRATIVE_EDITOR_BGFILTER_TOKEN` 和 `GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS`,默认 base URL 为 `http://58.87.105.82/bgfilter`,token 未配置时复用 BiRefNet token。BgFilter 请求必须显式传 `screen_color=` 和 `seg_model=`;用户路径只展示并提交默认 `birefnet`,后端仍识别内部保留的 `anime-seg`,其中 `birefnet` 只表示 BgFilter 管线内部后端,不等同于手动去背景的独立 BiRefNet 服务。角色动作抽帧仍沿用 legacy `#00FF00` 和本地 `editor_green_screen` 透明化。 +- 编辑器抠图服务:手动 `POST /api/editor/images/background-removals` 继续代理独立 BiRefNet 服务,配置为 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL`、`GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 和 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS`。角色形象生成、图标 spritesheet 生成和 UI 设计图素材提取的生成后纯色背景透明化改走独立 BgFilter 服务,配置为 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL`、`GENARRATIVE_EDITOR_BGFILTER_TOKEN` 和 `GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS`,默认 base URL 为 `http://58.87.105.82/bgfilter`,token 未配置时复用 BiRefNet token。BgFilter 请求必须显式传 `screen_color=` 和 `seg_model=`;前端用户路径不展示抠图模型选择并固定提交默认 `birefnet`,后端仍识别内部保留的 `anime-seg`,其中 `birefnet` 只表示 BgFilter 管线内部后端,不等同于手动去背景的独立 BiRefNet 服务。角色动作抽帧仍沿用 legacy `#00FF00` 和本地 `editor_green_screen` 透明化。 - Match3D 物品 sheet:关卡整图完成后走 VectorEngine `/v1/images/edits` multipart `image`,模型为 `gpt-image-2`,`2K 1:1` 输出 `10*10` spritesheet;物品 sheet prompt 固定要求单一纯绿色 `#00FF00 / RGB(0,255,0)` 绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG,并把透明整图写入 `itemSpritesheetImageSrc/itemSpritesheetImageObjectKey`。后端优先按透明 alpha 连通域从该 sheet 识别真实素材矩形并持久化 20 个物品、每个 5 个形态;识别数量不足时才回退 `10*10` 固定网格。通用系列素材图集的行列索引按每行 2 个物品计算,必须落在 `1..=10`,难度只决定运行态加载 3 / 9 / 15 / 20 种。 - Match3D UI spritesheet 和背景派生图:关卡整图作为参考图并发生成 `1K 1:1` UI spritesheet 与 `1K 9:16` 背景图,模型均为 `gpt-image-2`。UI spritesheet prompt 固定要求单一纯绿色 `#00FF00 / RGB(0,255,0)` 绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG;背景图必须合成为全画幅不透明 PNG。 - Match3D 1:1 容器 UI:VectorEngine `/v1/images/edits` multipart 参考图。该容器参考图是后端生图协议输入,必须通过 `include_bytes!` 随 `api-server` 编译进二进制,避免 API 单独发布或运行目录缺少 `public/` 时生成失败。 diff --git a/docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md b/docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md index 40c8b22f7..1df4cf1d9 100644 --- a/docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md +++ b/docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md @@ -185,7 +185,7 @@ - 生成视频 / 角色形象 / 角色动作 / 音效 / 背景音乐新建后,画布占位空白样式和右上角标签均与对应生成类型一致,不再统一使用图片占位 icon。 - 新建空白待生成占位的尺寸必须和面板参数一致;图片类修改比例 / 尺寸、视频修改清晰度后,画布空白占位同步变更且保持中心点。 - 点击角色图只选中图层并显示工具栏,不自动弹出重绘、快速编辑或角色动画面板;点击工具栏或右键菜单中的 `生成动画` 才创建角色动作占位和面板。 -- 点击 UI 设计图只选中图层并显示工具栏;工具栏在 `去除背景按钮` 后显示 `提取素材`,点击后画布自动缩放平移到素材完整展示,并在素材下方显示 UI 素材提取面板。UI 素材提取默认启用矩形框选,右侧工具栏与快速编辑统一,当前启用工具按钮保持高亮,点击同一工具可取消启用态;面板提示语显示 `使用框选工具框选你希望从画面中提取的素材`,并展示按原图坐标准确裁剪的框选截图预览、固定模型 `gpt-image-2`、计划规格、抠图背景色、抠图模型和提取按钮泥点,不显示额外取消按钮;点击素材和面板以外的画布区域即退出 UI 素材提取。用户至少框选一个区域后才能点击 `提取`,前端把红色轮廓绘入原图作为参考图,再固定用所选纯色背景素材提取提示词生成 spritesheet。框选数量不超过阈值时提交 `1:1·1K` 参数,超过阈值时提交 `1:1·2K` 参数;后端按 gpt-image-2 对应尺寸计算扣费,保存纯色背景源图后调用 BgFilter 按默认抠图模型 `birefnet` 透明化,并复用图标素材拆分流程,把透明 spritesheet 图集和拆分素材都放到画布。 +- 点击 UI 设计图只选中图层并显示工具栏;工具栏在 `去除背景按钮` 后显示 `提取素材`,点击后画布自动缩放平移到素材完整展示,并在素材下方显示 UI 素材提取面板。UI 素材提取默认启用矩形框选,右侧工具栏与快速编辑统一,当前启用工具按钮保持高亮,点击同一工具可取消启用态;面板提示语显示 `使用框选工具框选你希望从画面中提取的素材`,并展示按原图坐标准确裁剪的框选截图预览、固定模型 `gpt-image-2`、计划规格和提取按钮泥点,不显示额外取消按钮;点击素材和面板以外的画布区域即退出 UI 素材提取。用户至少框选一个区域后才能点击 `提取`,前端把红色轮廓绘入原图作为参考图,再固定用自动决策纯色背景素材提取提示词生成 spritesheet。框选数量不超过阈值时提交 `1:1·1K` 参数,超过阈值时提交 `1:1·2K` 参数;后端按 gpt-image-2 对应尺寸计算扣费,保存纯色背景源图后调用 BgFilter 按默认抠图模型 `birefnet` 透明化,并复用图标素材拆分流程,把透明 spritesheet 图集和拆分素材都放到画布。 - 生成游戏音效面板底部不显示字段标题,左下角只有一个时长参数按钮,选项为 Vidu duration `2-10` 秒;右下角固定模型胶囊显示 `Vidu` 并紧贴生成按钮。 - 生成游戏背景音乐面板右下角固定模型胶囊显示 `Suno` 并紧贴生成按钮;`make_instrumental` 不在 UI 中展示。 - 生成视频结果以视频图层加入画布,画布媒体元素标记为 `画布视频:生成视频 N`。 diff --git a/docs/【编辑器】画板UI设计图生成入口设计-2026-06-17.md b/docs/【编辑器】画板UI设计图生成入口设计-2026-06-17.md index 5385bcd39..84a4f9de7 100644 --- a/docs/【编辑器】画板UI设计图生成入口设计-2026-06-17.md +++ b/docs/【编辑器】画板UI设计图生成入口设计-2026-06-17.md @@ -28,7 +28,7 @@ - 支持自定义画面比例和大小尺寸。 - 模型固定为 `gpt-image-2`,模型展示对齐角色规范面板底部固定模型样式,不响应点击、不弹出模型切换菜单;历史草稿如果残留其他模型,提交时也必须强制改为 `gpt-image-2`。 - 默认画面比例为 `16:9`,默认大小为 `1K`。 -- UI 素材提取面板提供抠图背景色下拉和抠图模型下拉;背景色默认 `自动`,手动项展示颜色名称、方块色样和 hex 值,请求字段为 `screenColor`。`screenColor=auto` 时后端先在 11 个候选色中自动决策具体 hex,最多重试 3 次,失败兜底 `#CFEFFF`;后端调用 BgFilter 时只把解析后的具体 hex 作为 `screen_color` 传入。抠图模型请求字段为 `segModel`,用户界面只展示并提交默认 `birefnet`;后端仍识别内部保留的 `anime-seg`,但该选项不对用户可见。 +- UI 素材提取面板不展示抠图背景色或抠图模型选择;前端用户路径固定提交 `screenColor=auto` 和 `segModel=birefnet`。后端先在 11 个候选色中自动决策具体 hex,最多重试 3 次,失败兜底 `#CFEFFF`;后端调用 BgFilter 时只把解析后的具体 hex 作为 `screen_color` 传入。后端仍识别内部保留的 `anime-seg`,但该选项不对用户可见。 ## 提示词契约 @@ -58,7 +58,7 @@ - 后端固定使用 `gpt-image-2` 图片编辑链路,并固定提示词: ```text -仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选抠图背景色。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。 +仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用后端自动决策出的抠图背景色。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。 ``` - 后端收到 spritesheet 后先把带解析后纯色背景的源图写入 OSS,再调用 BgFilter 按默认 `segModel=birefnet` 透明化,并复用图标素材的连通域拆分能力;未知素材数量时按从上到下、从左到右自动命名为 `素材 1`、`素材 2`。 diff --git a/docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md b/docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md index 5ee227c62..84a19ac5e 100644 --- a/docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md +++ b/docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md @@ -47,18 +47,18 @@ - `nanobanana2`:比例 `1:1 / 2:3 / 3:2 / 9:16 / 16:9`;大小 `0.5K / 1K / 2K`。后端走 `/v1beta/models/{model}:generateContent`,把图标规范图作为 `inline_data`,并把 `aspectRatio` / `imageSize` 写入 `generationConfig.imageConfig`;`0.5K` 按 VectorEngine 文档传 `"512"`。 - `gpt-image-2`:比例 `1:1 / 2:3 / 3:2 / 9:16 / 16:9`;大小 `1K / 2K`。后端走 `/v1/images/edits`,把图标规范图作为 multipart `image`,按 `size` 映射:`1K 1:1 -> 1024x1024`、`1K 2:3/9:16 -> 1024x1536`、`1K 3:2/16:9 -> 1536x1024`、`2K 1:1 -> 2048x2048`、`2K 3:2/16:9 -> 2048x1152`;文档未列出 `2K` 竖版,`2K 2:3/9:16` 后端回落到 `1024x1536`。 - 用户在角色或图标素材面板中切换过模型后,下一次打开这两类面板继续使用上次模型。 -- 抠图背景色默认 `自动`,前端提交 `screenColor=auto`;手动选择时提交 11 个候选色之一。后端在组装 prompt 前把 `auto` 自动决策为具体 hex,最多重试 3 次,失败后兜底 `#CFEFFF`,最终 prompt 和 BgFilter 不透传 `auto`。 +- 不展示抠图背景色或抠图模型选择;前端用户路径固定提交 `screenColor=auto` 和 `segModel=birefnet`。后端在组装 prompt 前把 `auto` 自动决策为具体 hex,最多重试 3 次,失败后兜底 `#CFEFFF`,最终 prompt 和 BgFilter 不透传 `auto`。 - Prompt 固定为: ```text -参考图1的图标规范,背景必须是所选单一纯色抠图背景,且平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光;禁止出现文字,保证每个图标素材的所有内容区域是完全连通的。按照以下的素材的顺序从上到下从左到右依次生成并整理成一张spritesheet: +参考图1的图标规范,背景必须是自动决策出的单一纯色抠图背景,且平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光;禁止出现文字,保证每个图标素材的所有内容区域是完全连通的。按照以下的素材的顺序从上到下从左到右依次生成并整理成一张spritesheet: <素材描述按中文顿号拼接> ``` ## 去背与保存 -- 后端收到 spritesheet 后先把带解析后纯色背景的源图写入 OSS,再调用 BgFilter 透明化;请求字段包含 `screenColor` 和 `segModel`,用户界面只展示并提交默认 `birefnet`,后端仍识别内部保留的 `anime-seg`,但该选项不对用户可见。 +- 后端收到 spritesheet 后先把带解析后纯色背景的源图写入 OSS,再调用 BgFilter 透明化;请求字段包含 `screenColor` 和 `segModel`,前端用户路径固定提交 `screenColor=auto` 与默认 `birefnet`,后端仍识别内部保留的 `anime-seg`,但该选项不对用户可见。 - 去背后的整张 spritesheet 统一编码为透明 PNG,并作为唯一图标素材产物持久化。 - 响应保留 `iconImageSrcs` 字段用于兼容旧客户端,但图标素材生成固定返回空数组;UI 设计图提取素材仍可复用该响应结构返回切片素材。 diff --git a/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md b/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md index a4362e26f..d55d5a2c2 100644 --- a/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md +++ b/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md @@ -29,9 +29,7 @@ - 上传后的每张常规参考图以缩略图展示。 - 每张常规参考图右下角显示大号序号,从 `1` 开始递增。 3. 唯一文本框为 `角色设定`。 -4. 左下角展示画面比例、大小和抠图背景色选择按钮。 - - 抠图背景色下拉必须展示颜色名称、方块色样和 hex 值。 - - 默认 `自动`;手动可选 `浅雾蓝 #CFEFFF`、`浅钢蓝 #B0C2E0`、`暖浅桃色 #FFD6C2`、`淡薰衣草紫 #E6D8FF`、`浅粉灰 #F4D8E8`、`中度天蓝 #7FB3FF`、`浅柠黄 #FFF2A8`、`淡薄荷绿 #CFFFE1`、`浅中性灰 #D8DEE8`、`淡灰紫 #D8D2E8`、`高对比浅青 #A8F7F0`。 +4. 左下角只展示画面比例和大小,不展示抠图背景色或抠图模型选择;前端用户路径固定提交 `screenColor=auto` 和 `segModel=birefnet`。 5. 右下角展示模型选择和生成按钮。 ## 普通生成面板视觉口径 @@ -54,7 +52,7 @@ - 请求同时提交 `model`、`screenColor`、`segModel`、`aspectRatio` 和 `imageSize`: - `model` 支持 `gemini-3.1-flash-image-preview`(UI 显示 `nanobanana2`)和 `gpt-image-2`,默认 `nanobanana2`。 - 用户在角色或图标素材面板中切换过模型后,下一次打开这两类面板继续使用上次模型。 - - `screenColor` 允许 `auto` 或上述 11 个 hex;前端默认提交 `auto`,同源重绘从生成器快照或输入快照 `抠图背景色` 恢复。后端在组装 prompt 前把 `auto` 自动决策为具体 hex,最多重试 3 次,失败后兜底 `#CFEFFF`;调用 BgFilter 时只把解析后的具体 hex 作为 `screen_color` 传入。`segModel` 当前用户界面只展示并提交默认 `birefnet`;后端仍识别内部保留的 `anime-seg`,但该选项不对用户可见。 + - 前端用户路径固定提交 `screenColor=auto` 和 `segModel=birefnet`,不从生成器快照或输入快照恢复旧手动背景色 / 抠图模型。后端在组装 prompt 前把 `auto` 自动决策为具体 hex,最多重试 3 次,失败后兜底 `#CFEFFF`;调用 BgFilter 时只把解析后的具体 hex 作为 `screen_color` 传入。后端仍识别内部保留的 `anime-seg`,但该选项不对用户可见。 - 比例按 `x:y` 展示;大小按 `0.5K / 1K / 2K` 展示。 - 尺寸选项来源以 VectorEngine 接入文档为准: - `nanobanana2`:比例 `1:1 / 2:3 / 3:2 / 9:16 / 16:9`;大小 `0.5K / 1K / 2K`。后端走 `/v1beta/models/{model}:generateContent`,把比例写入 `generationConfig.imageConfig.aspectRatio`,把大小写入 `generationConfig.imageConfig.imageSize`;其中 `0.5K` 按文档传 `"512"`。 @@ -67,7 +65,7 @@ 角色设定:<用户输入的角色设定> ``` -- 角色图生成完成后,编辑器后端必须先把带所选纯色背景的源图写入 OSS,再调用独立 BgFilter 服务透明化:multipart 字段包含 `file`、`screen_color=` 和 `seg_model=`,用户路径默认并只提交 `seg_model=birefnet`。这里的 `seg_model=birefnet` 是 BgFilter 管线内部后端,不等同于手动去背景使用的独立 BiRefNet 服务。角色图 prompt 按 `screenColor` 写入颜色名称、hex 和 RGB。该流程不再调用 RPG / 资产工坊的角色主图专用 `character_visual_assets` 后处理,也不调用手动去背景的独立 BiRefNet;输出仍统一为透明背景 PNG,随后写入 OSS 私有对象并确认 `asset_object`。接口回包仍返回透明 PNG Data URL 供画板立即显示,同时返回 `objectKey` / `assetObjectId`,前端创建图层和画板资源记录时必须保存这些字段。 +- 角色图生成完成后,编辑器后端必须先把带自动决策纯色背景的源图写入 OSS,再调用独立 BgFilter 服务透明化:multipart 字段包含 `file`、`screen_color=` 和 `seg_model=`,用户路径默认并只提交 `seg_model=birefnet`。这里的 `seg_model=birefnet` 是 BgFilter 管线内部后端,不等同于手动去背景使用的独立 BiRefNet 服务。角色图 prompt 按 `screenColor` 写入颜色名称、hex 和 RGB。该流程不再调用 RPG / 资产工坊的角色主图专用 `character_visual_assets` 后处理,也不调用手动去背景的独立 BiRefNet;输出仍统一为透明背景 PNG,随后写入 OSS 私有对象并确认 `asset_object`。接口回包仍返回透明 PNG Data URL 供画板立即显示,同时返回 `objectKey` / `assetObjectId`,前端创建图层和画板资源记录时必须保存这些字段。 - 对 `assetKind: "character"` 的角色图层执行 `重绘` 时,前端仍使用原图作为参考图,但请求 `kind` 必须传 `character`,让后端继续套用上述角色提示词限定、角色图后处理和角色资产持久化;普通图片图层重绘仍保持 `kind: "quick-edit"`。 ## 生成规范参考图 @@ -116,7 +114,7 @@ - 角色生成提交统一走 `/api/editor/images/generations`,按 `角色规范 -> 常规参考图` 顺序传 `referenceImageSrcs`,并写入 `assetKind: "character"`。 - 角色图层重绘同样走 `/api/editor/images/generations` 的 `kind: "character"` 分支,原图作为参考图提交,生成结果继续保留 `assetKind: "character"`。 - 角色和图标素材生成已接入 `nanobanana2` / `gpt-image-2` 模型切换、上次模型记忆,以及按模型归一的比例 / 大小尺寸;`nanobanana2` 使用原生 `generateContent` 的 `imageConfig.aspectRatio/imageSize`,`gpt-image-2` 使用文档列出的 `size` 字符串。 -- 角色生成后端已按固定 prompt 骨架补入 `角色设定` 和所选纯色抠图背景,并在生成成功后先保存纯色背景源图,再通过 BgFilter 按用户路径默认 `segModel=birefnet` 执行透明化、写入 `generated-character-drafts/editor/character-images//image.png` 路径下的 OSS 私有对象,返回的 `objectKey` / `assetObjectId` 会随画板资源记录保存。 +- 角色生成后端已按固定 prompt 骨架补入 `角色设定` 和自动决策纯色抠图背景,并在生成成功后先保存纯色背景源图,再通过 BgFilter 按用户路径默认 `segModel=birefnet` 执行透明化、写入 `generated-character-drafts/editor/character-images//image.png` 路径下的 OSS 私有对象,返回的 `objectKey` / `assetObjectId` 会随画板资源记录保存。 - `Esc` 只退出角色规范画布点选状态,不关闭角色生成面板。 - 已补充回归测试覆盖角色形象生成、点选退出、角色动画入口隔离和快速编辑入口。 - 本次验证命令: diff --git a/scripts/check-bgfilter-live.mjs b/scripts/check-bgfilter-live.mjs index 18348ccd1..56dbd99ca 100644 --- a/scripts/check-bgfilter-live.mjs +++ b/scripts/check-bgfilter-live.mjs @@ -60,9 +60,16 @@ function readPositiveIntArg(name, fallback) { if (!rawValue) { return fallback; } + return parsePositiveInt(`--${name}`, rawValue); +} + +function parsePositiveInt(label, rawValue) { + if (!/^\d+$/.test(rawValue)) { + throw new Error(`${label} 必须是正整数,当前为 ${rawValue}`); + } const value = Number.parseInt(rawValue, 10); - if (!Number.isFinite(value) || value <= 0) { - throw new Error(`--${name} 必须是正整数,当前为 ${rawValue}`); + if (value <= 0) { + throw new Error(`${label} 必须是正整数,当前为 ${rawValue}`); } return value; } @@ -168,11 +175,13 @@ const segModel = const token = readConfig('GENARRATIVE_EDITOR_BGFILTER_TOKEN') || readConfig('GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN'); -const timeoutMs = Number.parseInt( - readArg('timeout-ms') || +const timeoutMs = readPositiveIntArg( + 'timeout-ms', + parsePositiveInt( + 'GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS', readConfig('GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS') || - `${DEFAULT_TIMEOUT_MS}`, - 10, + `${DEFAULT_TIMEOUT_MS}`, + ), ); const width = readPositiveIntArg('width', 96); const height = readPositiveIntArg('height', width); diff --git a/scripts/generate-gpt-image-2-elf-archer.mjs b/scripts/generate-gpt-image-2-elf-archer.mjs deleted file mode 100644 index 41e6ab8b7..000000000 --- a/scripts/generate-gpt-image-2-elf-archer.mjs +++ /dev/null @@ -1,266 +0,0 @@ -import { Buffer } from 'node:buffer'; -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const repoRoot = path.resolve(__dirname, '..'); -const defaultTimeoutMs = 1_000_000; - -function readArg(name) { - const prefix = `--${name}=`; - const value = process.argv.find((arg) => arg.startsWith(prefix)); - return value ? value.slice(prefix.length) : ''; -} - -function resolveOutputSize() { - const value = readArg('size') || '2048x1152'; - if (!/^\d{2,5}x\d{2,5}$/u.test(value)) { - throw new Error(`--size 必须形如 2048x2048,当前为 ${value}`); - } - return value; -} - -const outputSize = resolveOutputSize(); - -function readDotenv(fileName) { - const filePath = path.join(repoRoot, fileName); - if (!existsSync(filePath)) { - return {}; - } - - const values = {}; - for (const line of readFileSync(filePath, 'utf8').split(/\r?\n/u)) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) { - continue; - } - const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u.exec(trimmed); - if (!match) { - continue; - } - let value = match[2].trim(); - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - value = value.slice(1, -1); - } - values[match[1]] = value; - } - return values; -} - -function resolveEnv() { - const loaded = { - ...readDotenv('.env.example'), - ...readDotenv('.env.local'), - ...readDotenv('.env.secrets.local'), - ...process.env, - }; - return { - baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '') - .trim() - .replace(/\/+$/u, ''), - apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(), - timeoutMs: Number.parseInt( - String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs), - 10, - ), - }; -} - -function buildImagesGenerationUrl(baseUrl) { - return baseUrl.endsWith('/v1') - ? `${baseUrl}/images/generations` - : `${baseUrl}/v1/images/generations`; -} - -function collectStringsByKey(value, targetKey, output) { - if (Array.isArray(value)) { - value.forEach((entry) => collectStringsByKey(entry, targetKey, output)); - return; - } - if (!value || typeof value !== 'object') { - return; - } - - for (const [key, nested] of Object.entries(value)) { - if (key === targetKey) { - if (typeof nested === 'string' && nested.trim()) { - output.push(nested.trim()); - } - if (Array.isArray(nested)) { - nested.forEach((entry) => { - if (typeof entry === 'string' && entry.trim()) { - output.push(entry.trim()); - } - }); - } - } - collectStringsByKey(nested, targetKey, output); - } -} - -function extractImageUrls(payload) { - const urls = []; - collectStringsByKey(payload, 'url', urls); - collectStringsByKey(payload, 'image', urls); - collectStringsByKey(payload, 'image_url', urls); - return [...new Set(urls)].filter((url) => /^https?:\/\//u.test(url)); -} - -function extractBase64Images(payload) { - const values = []; - collectStringsByKey(payload, 'b64_json', values); - return values; -} - -function inferExtensionFromContentType(contentType) { - const normalized = contentType.split(';')[0]?.trim().toLowerCase(); - if (normalized === 'image/png') { - return 'png'; - } - if (normalized === 'image/webp') { - return 'webp'; - } - if (normalized === 'image/gif') { - return 'gif'; - } - return 'jpg'; -} - -function inferExtensionFromBytes(bytes) { - if (bytes.subarray(0, 8).equals(Buffer.from('\x89PNG\r\n\x1A\n', 'binary'))) { - return 'png'; - } - if (bytes.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]))) { - return 'jpg'; - } - if ( - bytes.subarray(0, 4).toString('ascii') === 'RIFF' && - bytes.subarray(8, 12).toString('ascii') === 'WEBP' - ) { - return 'webp'; - } - return 'png'; -} - -async function fetchJson(url, options, timeoutMs) { - const abortController = new AbortController(); - const timer = setTimeout(() => abortController.abort(), timeoutMs); - try { - const response = await fetch(url, { - ...options, - signal: abortController.signal, - }); - const text = await response.text(); - if (!response.ok) { - throw new Error(`VectorEngine ${response.status}: ${text.slice(0, 600)}`); - } - return JSON.parse(text); - } catch (error) { - if (error?.name === 'AbortError') { - throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`); - } - throw error; - } finally { - clearTimeout(timer); - } -} - -async function downloadUrl(url, timeoutMs) { - const abortController = new AbortController(); - const timer = setTimeout(() => abortController.abort(), timeoutMs); - try { - const response = await fetch(url, { signal: abortController.signal }); - if (!response.ok) { - throw new Error(`download ${response.status}`); - } - return { - bytes: Buffer.from(await response.arrayBuffer()), - extension: inferExtensionFromContentType( - response.headers.get('content-type') || 'image/jpeg', - ), - }; - } catch (error) { - if (error?.name === 'AbortError') { - throw new Error(`Generated image download timed out after ${timeoutMs}ms`); - } - throw error; - } finally { - clearTimeout(timer); - } -} - -function buildPrompt() { - return [ - '按照角色描述生成游戏角色立绘。', - '角色设定:女精灵弓箭手,修长敏捷,尖耳,银金色长发,翠绿色眼睛,穿轻便森林皮甲与深绿色披风,手持精致长弓,背负箭袋,姿态自信警觉。', - '严格要求:画面中心构图,角色主体完整置于画面中央,完整展示头部、身体、双腿、武器和披风,禁止镜头透视,禁止特写。', - '美术风格:高品质幻想游戏概念设定图,干净轮廓,精致但不复杂,适合作为可抠图角色素材。', - '背景固定为单一纯色背景 浅雾蓝 #CFEFFF / RGB(207,239,255),只作为抠像底色;纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具;角色主体不得带与背景色相同或相近的描边、投影或反光。', - '禁止生成建筑、室内布景、风景、地面道具、漂浮物、烟雾叙事元素、文字、水印、边框、UI 元素或其他角色。', - ].join(''); -} - -const env = resolveEnv(); -if (!env.baseUrl || !env.apiKey) { - console.error( - `缺少配置:VECTOR_ENGINE_BASE_URL=${env.baseUrl ? '已配置' : '未配置'},VECTOR_ENGINE_API_KEY=${ - env.apiKey ? '已配置' : '未配置' - }`, - ); - process.exit(1); -} - -const requestBody = { - model: 'gpt-image-2', - prompt: buildPrompt(), - n: 1, - size: outputSize, -}; - -console.log('gpt-image-2 生成女精灵弓箭手'); -console.log(`目标: ${buildImagesGenerationUrl(env.baseUrl)}`); -console.log(`size: ${outputSize}`); -console.log(`timeout_ms: ${env.timeoutMs}`); -console.log('api_key: 已配置\n'); - -const payload = await fetchJson( - buildImagesGenerationUrl(env.baseUrl), - { - method: 'POST', - headers: { - Authorization: `Bearer ${env.apiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - }, - env.timeoutMs, -); - -let image; -const base64Images = extractBase64Images(payload); -if (base64Images.length > 0) { - const bytes = Buffer.from(base64Images[0], 'base64'); - image = { bytes, extension: inferExtensionFromBytes(bytes) }; -} else { - const urls = extractImageUrls(payload); - if (urls.length === 0) { - throw new Error(`响应中未找到图片 URL 或 b64_json:${JSON.stringify(payload).slice(0, 600)}`); - } - image = await downloadUrl(urls[0], env.timeoutMs); -} - -const downloadsDir = path.join(os.homedir(), 'Downloads'); -mkdirSync(downloadsDir, { recursive: true }); -const outputPath = path.join( - downloadsDir, - `gpt-image-2-elf-archer-${outputSize}.${image.extension}`, -); -writeFileSync(outputPath, image.bytes); - -console.log(`已保存: ${outputPath}`); -console.log(`bytes: ${image.bytes.length}`); diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 163252b33..7bf032961 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -2235,7 +2235,7 @@ async fn request_editor_generated_screen_background_with_bgfilter( let url = editor_bgfilter_endpoint(state)?; let call_id = format!("bgfilter-call-{}", current_utc_micros()); let request_started_at = Instant::now(); - let timeout_ms = state.config.editor_bgfilter_request_timeout_ms.max(120_000); + let timeout_ms = state.config.editor_bgfilter_request_timeout_ms.max(1); let input_bytes = image.bytes.len(); let source_mime_type = image.mime_type.clone(); let source_file_name = format!( @@ -7234,6 +7234,7 @@ mod tests { "async fn request_editor_background_removal_image", &[ "editor_bgfilter_endpoint", + "editor_bgfilter_request_timeout_ms.max(1)", "\"screen_color\"", "\"seg_model\"", "seg_model.to_string()", diff --git a/src/components/image-editor/ImageCanvasCharacterGenerationComposerView.tsx b/src/components/image-editor/ImageCanvasCharacterGenerationComposerView.tsx index 66d2f8a8d..350a40773 100644 --- a/src/components/image-editor/ImageCanvasCharacterGenerationComposerView.tsx +++ b/src/components/image-editor/ImageCanvasCharacterGenerationComposerView.tsx @@ -291,8 +291,6 @@ export function ImageCanvasCharacterGenerationComposerView({ dialog={dialog} setGenerateDialog={setGenerateDialog} includeDimensions - includeBackgroundColor - includeSegModel onRememberImageModel={onRememberImageModel} optionLabelPrefix="生成图片" cost={calculateEditorImageGenerationPrice({ diff --git a/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx b/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx index f4af6ea26..d2f40c3c0 100644 --- a/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx @@ -3215,14 +3215,6 @@ describe('ImageCanvasEditorView generation integration', () => { value: '仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。', }), - expect.objectContaining({ - title: '抠图背景色', - value: '自动', - }), - expect.objectContaining({ - title: '抠图模型', - value: '默认 birefnet', - }), ]), references: expect.arrayContaining([ expect.objectContaining({ diff --git a/src/components/image-editor/ImageCanvasEditorModel.ts b/src/components/image-editor/ImageCanvasEditorModel.ts index b19c6f40f..830c27ec8 100644 --- a/src/components/image-editor/ImageCanvasEditorModel.ts +++ b/src/components/image-editor/ImageCanvasEditorModel.ts @@ -552,7 +552,6 @@ export function hydrateCanvasGenerationDialog( currentUserId, ), imageModel: stringOrUndefined(snapshot.imageModel), - screenColor: stringOrUndefined(snapshot.screenColor), videoModel: typeof snapshot.videoModel === 'string' ? snapshot.videoModel diff --git a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx index e504efeb1..dc5fa045a 100644 --- a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx @@ -194,8 +194,6 @@ function createStageProps(): ImageCanvasStageViewProps { onExtractUiDesignAssets: vi.fn(), onUiAssetExtractionToolChange: vi.fn(), onUiAssetExtractionModelChange: vi.fn(), - onUiAssetExtractionScreenColorChange: vi.fn(), - onUiAssetExtractionSegModelChange: vi.fn(), onUiAssetExtractionPointerStart: vi.fn(), onUiAssetExtractionPointerMove: vi.fn(), onUiAssetExtractionPointerEnd: vi.fn(), @@ -344,8 +342,6 @@ describe('ImageCanvasEditorShellView', () => { sourceLayerId: layer.id, tool: 'rect' as const, model: 'gemini-3.1-flash-image-preview', - screenColor: '#CFEFFF', - segModel: 'birefnet', marks: [], references: [], draftMark: null, diff --git a/src/components/image-editor/ImageCanvasEditorTypes.ts b/src/components/image-editor/ImageCanvasEditorTypes.ts index 72b1db57a..d5f3131ba 100644 --- a/src/components/image-editor/ImageCanvasEditorTypes.ts +++ b/src/components/image-editor/ImageCanvasEditorTypes.ts @@ -213,8 +213,6 @@ export type GenerateDialogState = { publicationReferences?: CharacterReferenceImage[]; uiDesignSpecReference?: CharacterReferenceImage | null; imageModel?: string; - screenColor?: string; - segModel?: string; videoModel?: EditorVideoModel; videoAspectRatio?: EditorVideoAspectRatio; videoResolution?: EditorVideoResolution; diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index 7682c290d..504334bf5 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -1261,8 +1261,6 @@ export function ImageCanvasEditorView({ quickEditSelectionSourceLayer, changeUiAssetExtractionTool, changeUiAssetExtractionModel, - changeUiAssetExtractionScreenColor, - changeUiAssetExtractionSegModel, changeQuickEditSelectionTool, startUiAssetExtractionPointer, startQuickEditSelectionPointer, @@ -2003,8 +2001,6 @@ export function ImageCanvasEditorView({ onExtractUiDesignAssets: extractUiDesignAssets, onUiAssetExtractionToolChange: changeUiAssetExtractionTool, onUiAssetExtractionModelChange: changeUiAssetExtractionModel, - onUiAssetExtractionScreenColorChange: changeUiAssetExtractionScreenColor, - onUiAssetExtractionSegModelChange: changeUiAssetExtractionSegModel, onUiAssetExtractionPointerStart: startUiAssetExtractionPointer, onUiAssetExtractionPointerMove: moveUiAssetExtractionPointer, onUiAssetExtractionPointerEnd: endUiAssetExtractionPointer, diff --git a/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts b/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts index 28c65cbc5..11dfff51d 100644 --- a/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts +++ b/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts @@ -516,8 +516,6 @@ describe('ImageCanvasGenerationDialogModel', () => { mode: 'character', prompt: '红发骑士', sourceLayerId: 'layer-source', - screenColor: '#FFD6C2', - segModel: 'anime-seg', placeholder: { x: 472, y: 140, @@ -585,8 +583,6 @@ describe('ImageCanvasGenerationDialogModel', () => { status: 'idle', generatedLayerId: 'layer-source', imageModel: 'gpt-image-2', - screenColor: '#E6D8FF', - segModel: 'anime-seg', aspectRatio: '2:3', imageSize: '2K', }, @@ -597,8 +593,6 @@ describe('ImageCanvasGenerationDialogModel', () => { prompt: '普通图', sourceLayerId: 'layer-source', imageModel: 'gpt-image-2', - screenColor: '#E6D8FF', - segModel: 'anime-seg', aspectRatio: '2:3', imageSize: '2K', }); diff --git a/src/components/image-editor/ImageCanvasGenerationDialogModel.ts b/src/components/image-editor/ImageCanvasGenerationDialogModel.ts index d4b807a32..d18898685 100644 --- a/src/components/image-editor/ImageCanvasGenerationDialogModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationDialogModel.ts @@ -18,9 +18,7 @@ import { CHARACTER_ANIMATION_FRAME_DISPLAY_SIZE, CHARACTER_ANIMATION_FRAME_ORIGINAL_SIZE, createCanvasLayerReference, - DEFAULT_EDITOR_BGFILTER_SEG_MODEL, DEFAULT_IMAGE_MODEL, - DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR, DEFAULT_PUBLICATION_GAME_INFO, DEFAULT_SOUND_EFFECT_DURATION_SECONDS, DEFAULT_SOUND_EFFECT_MODEL, @@ -37,9 +35,7 @@ import { IMAGE_MODEL_GPT_IMAGE_2, inferEditorImageAspectRatio, inferEditorImageSizeLabel, - normalizeEditorBgFilterSegModel, normalizeEditorImageModel, - normalizeEditorGenerationBackgroundColor, PUBLICATION_FRAME_ORIGINAL_SIZE, resolveEditorImageGenerationPixelSize, resolveEditorVideoGenerationPixelSize, @@ -276,8 +272,6 @@ export function createCharacterGenerationDialogDraft({ characterSpecReference: null, characterReferences: [], imageModel: normalizedImageModel, - screenColor: DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR, - segModel: DEFAULT_EDITOR_BGFILTER_SEG_MODEL, aspectRatio: dimensionDefaults.aspectRatio, imageSize: dimensionDefaults.imageSize, placeholder: { @@ -312,8 +306,6 @@ export function createIconGenerationDialogDraft({ generationReferences: [], iconDescriptions: [], imageModel: normalizedImageModel, - screenColor: DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR, - segModel: DEFAULT_EDITOR_BGFILTER_SEG_MODEL, aspectRatio: dimensionDefaults.aspectRatio, imageSize: dimensionDefaults.imageSize, placeholder: { @@ -747,18 +739,9 @@ function restoreSharedImageOptions( sourceDialog?: CanvasGenerationDialogState | null, ): Omit { const fields = getGenerationInputFieldValues(sourceLayer); - const sourceScreenColor = - sourceDialog?.screenColor ?? fields.get('抠图背景色'); - const sourceSegModel = sourceDialog?.segModel ?? fields.get('抠图模型'); return { ...draft, imageModel: sourceDialog?.imageModel ?? draft.imageModel, - screenColor: normalizeEditorGenerationBackgroundColor( - sourceScreenColor ?? draft.screenColor, - ), - segModel: normalizeEditorBgFilterSegModel( - sourceSegModel ?? draft.segModel, - ), aspectRatio: sourceDialog?.aspectRatio ?? draft.aspectRatio ?? diff --git a/src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx b/src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx index 7770866ea..827601f0e 100644 --- a/src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx +++ b/src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx @@ -15,15 +15,13 @@ import { function ImageOptionsHarness({ initialDialog, includeDimensions = true, - includeBackgroundColor = false, - includeSegModel = false, + onRememberImageModel = vi.fn(), lockedModel, }: { initialDialog: GenerateDialogState; includeDimensions?: boolean; - includeBackgroundColor?: boolean; - includeSegModel?: boolean; + onRememberImageModel?: (model: string) => void; lockedModel?: string; }) { @@ -37,8 +35,6 @@ function ImageOptionsHarness({ dialog={dialog} setGenerateDialog={setDialog} includeDimensions={includeDimensions} - includeBackgroundColor={includeBackgroundColor} - includeSegModel={includeSegModel} onRememberImageModel={onRememberImageModel} lockedModel={lockedModel} cost={calculateEditorImageGenerationPrice({ @@ -51,8 +47,6 @@ function ImageOptionsHarness({ {dialog.imageModel} {dialog.aspectRatio} {dialog.imageSize} - {dialog.screenColor} - {dialog.segModel} {dialog.status} {dialog.errorMessage ?? '-'} @@ -180,87 +174,6 @@ describe('ImageCanvasGenerationImageOptionsView', () => { ).toBe('生成3泥点'); }); - it('updates the keyed background color from the color menu', () => { - render( - , - ); - - fireEvent.click( - screen.getByRole('button', { - name: '生成图片背景色 自动', - }), - ); - const panel = screen.getByRole('menu', { name: '生成图片背景色选项' }); - fireEvent.click( - within(panel).getByRole('button', { name: '高对比浅青 #A8F7F0' }), - ); - - expect(screen.getByRole('menu', { name: '生成图片背景色选项' })).toBeTruthy(); - expect( - within(panel) - .getByRole('button', { name: '高对比浅青 #A8F7F0' }) - .getAttribute('aria-pressed'), - ).toBe('true'); - expect( - screen.getByRole('button', { - name: '生成图片背景色 高对比浅青 #A8F7F0', - }), - ).toBeTruthy(); - expect(screen.getByLabelText('当前背景色').textContent).toBe('#A8F7F0'); - expect(screen.getByLabelText('当前状态').textContent).toBe('idle'); - expect(screen.getByLabelText('当前错误').textContent).toBe('-'); - }); - - it('only exposes the default BgFilter segmentation model', () => { - render( - , - ); - - fireEvent.click( - screen.getByRole('button', { - name: '生成图片抠图模型 默认', - }), - ); - const panel = screen.getByRole('menu', { name: '生成图片抠图模型选项' }); - const options = within(panel).getAllByRole('button'); - - expect(options).toHaveLength(1); - expect(options[0].getAttribute('aria-label')).toBe('默认'); - expect( - within(panel) - .getByRole('button', { name: '默认' }) - .getAttribute('aria-pressed'), - ).toBe('true'); - expect(screen.getByLabelText('当前抠图模型').textContent).toBe( - 'birefnet', - ); - }); - it('locks the model trigger when a fixed model is provided', () => { const rememberImageModel = vi.fn(); render( diff --git a/src/components/image-editor/ImageCanvasGenerationImageOptionsView.tsx b/src/components/image-editor/ImageCanvasGenerationImageOptionsView.tsx index a8e05b0dc..74367c2e6 100644 --- a/src/components/image-editor/ImageCanvasGenerationImageOptionsView.tsx +++ b/src/components/image-editor/ImageCanvasGenerationImageOptionsView.tsx @@ -1,4 +1,4 @@ -import { Check, ChevronDown, Cpu, Scissors } from 'lucide-react'; +import { Check, ChevronDown, Cpu } from 'lucide-react'; import { type CSSProperties, type Dispatch, @@ -15,15 +15,9 @@ import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton import type { GenerateDialogState } from './ImageCanvasEditorTypes'; import { EDITOR_IMAGE_DIMENSION_OPTIONS, - EDITOR_VISIBLE_BGFILTER_SEG_MODEL_OPTIONS, EDITOR_IMAGE_MODEL_OPTIONS, - EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS, - getVisibleEditorBgFilterSegModelOption, - getEditorGenerationBackgroundColorOption, getEditorImageModelDisplayName, IMAGE_MODEL_NANOBANANA2, - normalizeVisibleEditorBgFilterSegModel, - normalizeEditorGenerationBackgroundColor, normalizeEditorImageModel, QUICK_EDIT_MODEL_OPTIONS, resizeGenerationPlaceholderToImageSelection, @@ -36,8 +30,6 @@ type ImageCanvasGenerationImageOptionsViewProps = { setGenerateDialog: Dispatch>; includeDimensions: boolean; includeModel?: boolean; - includeBackgroundColor?: boolean; - includeSegModel?: boolean; onRememberImageModel: (model: string) => void; optionLabelPrefix?: string; dimensionRatioAriaLabelPrefix?: string; @@ -54,7 +46,7 @@ type ImageCanvasGenerationImageOptionsViewProps = { ) => CSSProperties; }; -type OpenPanel = 'dimensions' | 'model' | 'backgroundColor' | 'segModel' | null; +type OpenPanel = 'dimensions' | 'model' | null; function resetFailedDialogStatus(dialog: GenerateDialogState) { return { @@ -129,8 +121,6 @@ export function ImageCanvasGenerationImageOptionsView({ setGenerateDialog, includeDimensions, includeModel = true, - includeBackgroundColor = false, - includeSegModel = false, onRememberImageModel, optionLabelPrefix, dimensionRatioAriaLabelPrefix, @@ -145,8 +135,6 @@ export function ImageCanvasGenerationImageOptionsView({ }: ImageCanvasGenerationImageOptionsViewProps) { const [openPanel, setOpenPanel] = useState(null); const dimensionsButtonRef = useRef(null); - const backgroundColorButtonRef = useRef(null); - const segModelButtonRef = useRef(null); const modelButtonRef = useRef(null); const isQuickEdit = dialog.mode === 'quick-edit'; const normalizedLockedModel = lockedModel @@ -168,12 +156,6 @@ export function ImageCanvasGenerationImageOptionsView({ const selectedModelLabel = modelOptions.find((option) => option.value === selectedModel)?.label ?? getEditorImageModelDisplayName(selectedModel); - const selectedBackgroundColor = getEditorGenerationBackgroundColorOption( - dialog.screenColor, - ); - const selectedSegModel = getVisibleEditorBgFilterSegModelOption( - dialog.segModel, - ); const selectedSizeLabel = resolveEditorImageSizeLabel({ aspectRatio: selection.aspectRatio, imageSize: selection.imageSize, @@ -190,12 +172,7 @@ export function ImageCanvasGenerationImageOptionsView({ useImageCanvasFloatingOptionDismiss({ isOpen: openPanel !== null, - boundaryRefs: [ - dimensionsButtonRef, - backgroundColorButtonRef, - segModelButtonRef, - modelButtonRef, - ], + boundaryRefs: [dimensionsButtonRef, modelButtonRef], onDismiss: dismissOpenPanel, }); @@ -245,49 +222,6 @@ export function ImageCanvasGenerationImageOptionsView({ }); }; - const updateBackgroundColor = (screenColor: string) => { - setGenerateDialog((currentDialog) => - currentDialog && currentDialog.mode === dialog.mode - ? { - ...resetFailedDialogStatus(currentDialog), - screenColor: normalizeEditorGenerationBackgroundColor(screenColor), - } - : currentDialog, - ); - }; - - const renderBackgroundColorSwatch = (value: string) => - value === 'auto' ? ( -