接入画布生图背景抠图配置
新增画布角色、图标和UI素材提取的背景色与抠图模型参数。 接入独立 BgFilter 服务并保留旧手动去背景服务。 保存纯色背景源图并在生成后调用 BgFilter 去背景。 调整生图面板选项布局和抠图模型显示文案。 同步前后端测试、架构文档和项目记忆。
This commit is contained in:
@@ -19,11 +19,19 @@
|
||||
## 2026-07-02 图片画布生成抠图背景色使用 screenColor 传递
|
||||
|
||||
- 背景:画布角色、图标和 UI 素材生成过去固定要求 `#00FF00` 绿幕,后续 BGfilter 服务需要按生成时背景色做去背景,不能继续把背景色写死在 prompt 或后处理里。
|
||||
- 决策:角色形象、图标 spritesheet 和 UI 设计图素材提取新增用户可选纯色抠图背景。前端下拉展示颜色名、方块色样和 hex,默认 `浅雾蓝 #CFEFFF`,可选 `浅钢蓝 #B0C2E0`、`暖浅桃色 #FFD6C2`、`淡薰衣草紫 #E6D8FF`、`浅粉灰 #F4D8E8`、`中度天蓝 #7FB3FF`。请求字段统一为前端 / JSON `screenColor`、Rust `screen_color`;生成输入快照写入 `抠图背景色`,同源重绘可从快照恢复。api-server 只接受上述六个 hex,缺省或空值使用 `#CFEFFF`,未知值返回 `400`;本地确定性去背用所选颜色作为 key color。角色动作抽帧暂不接入该选择,继续使用 legacy `#00FF00` 绿幕。
|
||||
- 影响范围:`/editor/canvas` 角色形象生成、图标素材生成、UI 设计图素材提取、`editor_green_screen` 后处理、未来 BGfilter 服务入参、图片画布 MVP 和角色形象生成设计文档。
|
||||
- 决策:角色形象、图标 spritesheet 和 UI 设计图素材提取新增用户可选纯色抠图背景。前端下拉展示颜色名、方块色样和 hex,默认 `浅雾蓝 #CFEFFF`,可选 `浅钢蓝 #B0C2E0`、`暖浅桃色 #FFD6C2`、`淡薰衣草紫 #E6D8FF`、`浅粉灰 #F4D8E8`、`中度天蓝 #7FB3FF`。请求字段统一为前端 / JSON `screenColor`、Rust `screen_color`;生成输入快照写入 `抠图背景色`,同源重绘可从快照恢复。api-server 只接受上述六个 hex,缺省或空值使用 `#CFEFFF`,未知值返回 `400`;生成后 BgFilter 去背景用该值作为 `screen_color`。角色动作抽帧暂不接入该选择,继续使用 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`。
|
||||
|
||||
## 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=<screenColor>` 和 `seg_model=<segModel>`,`segModel` 用户可选 `birefnet` 或 `anime-seg`,默认 `birefnet`。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 服务。角色动作抽帧仍保留 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`。
|
||||
|
||||
## 2026-07-01 认证工作集只经 typed projection 同步正式表
|
||||
|
||||
- 背景:同手机号重复账号、兑换码白名单错配和微信资料不回写暴露出 `module-auth` 内存工作集、`auth_store_snapshot` 和正式认证表之间仍有历史互刷路径;旧 JSON 快照会把过期手机号索引或用户资料重新带回运行态。
|
||||
@@ -164,7 +172,7 @@
|
||||
## 2026-06-18 图片画布 UI 设计图提取素材保留图集
|
||||
|
||||
- 背景:UI 设计图需要从成图中继续抽取可复用独立素材;原图标素材生成只把拆分后的图标放入画布,spritesheet 原图没有保留,后续追溯和二次切图不方便。
|
||||
- 决策:`assetKind="ui-design"` 图层浮动工具栏新增 `提取素材`,点击后先进入红框素材框选编辑态,默认矩形框选,并支持椭圆框选和画笔自由框选。至少存在一个框选区域后才能提交;前端把红色轮廓绘入原 UI 设计图并将合成图作为 `/api/editor/ui-designs/assets/extractions` 的参考图。后端固定 `gpt-image-2` 和提示词 `仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。`,返回结构复用图标 spritesheet 响应。UI 提取把 spritesheet 图集作为 `assetKind="icon-spritesheet"` 图层放到画布,再放拆分后的 `assetKind="icon"` 素材;图标素材生成只保留扣绿后的 spritesheet 图集,不再额外铺独立图标。
|
||||
- 决策:`assetKind="ui-design"` 图层浮动工具栏新增 `提取素材`,点击后先进入红框素材框选编辑态,默认矩形框选,并支持椭圆框选和画笔自由框选。至少存在一个框选区域后才能提交;前端把红色轮廓绘入原 UI 设计图并将合成图作为 `/api/editor/ui-designs/assets/extractions` 的参考图。后端固定 `gpt-image-2` 和纯色背景素材提取提示词,返回结构复用图标 spritesheet 响应。UI 提取把 spritesheet 图集作为 `assetKind="icon-spritesheet"` 图层放到画布,再放拆分后的 `assetKind="icon"` 素材;图标素材生成只保留透明 spritesheet 图集,不再额外铺独立图标。2026-07-03 起,UI 提取的纯色背景由 `screenColor` 选择并经 BgFilter 透明化。
|
||||
- 影响范围:图片画布浮动工具栏、编辑器图片生成 BFF、`platform-image` 图集连通域拆分、画布图层类型和编辑器文档。
|
||||
- 验证方式:运行图片画布工具栏 / 图集落层 / 生成提交相关前端测试,`cargo test -p platform-image generated_asset_sheets --manifest-path server-rs/Cargo.toml`,以及 `cargo test -p api-server editor_ui_design_asset_extraction_prompt_is_fixed --manifest-path server-rs/Cargo.toml`。
|
||||
- 关联文档:`docs/【编辑器】画板UI设计图生成入口设计-2026-06-17.md`、`docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -208,6 +208,7 @@ npm run check:server-rs-ddd
|
||||
|
||||
- LLM:通用 LLM 门面继续使用 `GENARRATIVE_LLM_*`;创意 Agent `gpt-5` Responses / Chat Completions 文本链路已于 2026-06 从 APIMart 迁移到 VectorEngine,使用 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 构造 OpenAI-compatible client,`api-server` 会把未带 `/v1` 的 VectorEngine base URL 规范化到 `/v1` 后请求 `/responses`。`APIMART_BASE_URL` / `APIMART_API_KEY` 只作为历史残留,不再作为创意 Agent gpt-5 客户端来源;后续排障时优先确认 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=<screenColor>` 和 `seg_model=<segModel>`;`segModel` 只允许 `birefnet` 或 `anime-seg`,默认 `birefnet`,其中 `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/` 时生成失败。
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
- 生成视频 / 角色形象 / 角色动作 / 音效 / 背景音乐新建后,画布占位空白样式和右上角标签均与对应生成类型一致,不再统一使用图片占位 icon。
|
||||
- 新建空白待生成占位的尺寸必须和面板参数一致;图片类修改比例 / 尺寸、视频修改清晰度后,画布空白占位同步变更且保持中心点。
|
||||
- 点击角色图只选中图层并显示工具栏,不自动弹出重绘、快速编辑或角色动画面板;点击工具栏或右键菜单中的 `生成动画` 才创建角色动作占位和面板。
|
||||
- 点击 UI 设计图只选中图层并显示工具栏;工具栏在 `去除背景按钮` 后显示 `提取素材`,点击后画布自动缩放平移到素材完整展示,并在素材下方显示 UI 素材提取面板。UI 素材提取默认启用矩形框选,右侧工具栏与快速编辑统一,当前启用工具按钮保持高亮,点击同一工具可取消启用态;面板提示语显示 `使用框选工具框选你希望从画面中提取的素材`,并展示按原图坐标准确裁剪的框选截图预览、固定模型 `gpt-image-2`、计划规格和提取按钮泥点,不显示额外取消按钮;点击素材和面板以外的画布区域即退出 UI 素材提取。用户至少框选一个区域后才能点击 `提取`,前端把红色轮廓绘入原图作为参考图,再固定用提示词 `仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板或绿色阴影。` 生成 spritesheet。框选数量不超过阈值时提交 `1:1·1K` 参数,超过阈值时提交 `1:1·2K` 参数;后端按 gpt-image-2 对应尺寸计算扣费,后续复用图标素材拆分流程,把 spritesheet 图集和拆分素材都放到画布。
|
||||
- 点击 UI 设计图只选中图层并显示工具栏;工具栏在 `去除背景按钮` 后显示 `提取素材`,点击后画布自动缩放平移到素材完整展示,并在素材下方显示 UI 素材提取面板。UI 素材提取默认启用矩形框选,右侧工具栏与快速编辑统一,当前启用工具按钮保持高亮,点击同一工具可取消启用态;面板提示语显示 `使用框选工具框选你希望从画面中提取的素材`,并展示按原图坐标准确裁剪的框选截图预览、固定模型 `gpt-image-2`、计划规格、抠图背景色、抠图模型和提取按钮泥点,不显示额外取消按钮;点击素材和面板以外的画布区域即退出 UI 素材提取。用户至少框选一个区域后才能点击 `提取`,前端把红色轮廓绘入原图作为参考图,再固定用所选纯色背景素材提取提示词生成 spritesheet。框选数量不超过阈值时提交 `1:1·1K` 参数,超过阈值时提交 `1:1·2K` 参数;后端按 gpt-image-2 对应尺寸计算扣费,保存纯色背景源图后调用 BgFilter 按所选抠图模型透明化,并复用图标素材拆分流程,把透明 spritesheet 图集和拆分素材都放到画布。
|
||||
- 生成游戏音效面板底部不显示字段标题,左下角只有一个时长参数按钮,选项为 Vidu duration `2-10` 秒;右下角固定模型胶囊显示 `Vidu` 并紧贴生成按钮。
|
||||
- 生成游戏背景音乐面板右下角固定模型胶囊显示 `Suno` 并紧贴生成按钮;`make_instrumental` 不在 UI 中展示。
|
||||
- 生成视频结果以视频图层加入画布,画布媒体元素标记为 `画布视频:生成视频 N`。
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
- 支持自定义画面比例和大小尺寸。
|
||||
- 模型固定为 `gpt-image-2`,模型展示对齐角色规范面板底部固定模型样式,不响应点击、不弹出模型切换菜单;历史草稿如果残留其他模型,提交时也必须强制改为 `gpt-image-2`。
|
||||
- 默认画面比例为 `16:9`,默认大小为 `1K`。
|
||||
- UI 素材提取面板提供抠图背景色下拉和抠图模型下拉;背景色展示颜色名称、方块色样和 hex 值,请求字段为 `screenColor`,后端调用 BgFilter 时作为 `screen_color` 传入;抠图模型请求字段为 `segModel`,可选 `birefnet` 或 `anime-seg`,默认 `birefnet`,后端调用 BgFilter 时作为 `seg_model` 传入。
|
||||
|
||||
## 提示词契约
|
||||
|
||||
@@ -57,10 +58,10 @@
|
||||
- 后端固定使用 `gpt-image-2` 图片编辑链路,并固定提示词:
|
||||
|
||||
```text
|
||||
仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。
|
||||
仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选抠图背景色。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。
|
||||
```
|
||||
|
||||
- 后端收到 spritesheet 后先把带绿幕源图写入 OSS,再走 `server-rs/crates/api-server/src/editor_green_screen.rs` 的统一绿幕透明化方法,并复用图标素材的连通域拆分能力;未知素材数量时按从上到下、从左到右自动命名为 `素材 1`、`素材 2`。
|
||||
- 后端收到 spritesheet 后先把带所选纯色背景的源图写入 OSS,再调用 BgFilter 按所选 `segModel` 透明化,并复用图标素材的连通域拆分能力;未知素材数量时按从上到下、从左到右自动命名为 `素材 1`、`素材 2`。
|
||||
- 前端先把 spritesheet 原图作为 `assetKind: "icon-spritesheet"` 图集图层放在 UI 设计图右侧,再把拆分出的独立素材作为 `assetKind: "icon"` 图标图层继续放到画布。
|
||||
|
||||
## 验收点
|
||||
@@ -70,5 +71,5 @@
|
||||
- 从画布选择时只能绑定图标规范图片。
|
||||
- 请求参数包含 `kind: "ui-design"`、`model: "gpt-image-2"`、比例、大小与可选参考图。
|
||||
- 上传普通参考图后,请求参考图数组同时包含图标规范和普通参考图,生成图层信息面板展示 `用户输入`、`图标规范` 与普通参考图。
|
||||
- 选中 UI 设计图时浮动工具栏显示 `提取素材`;点击后进入红框素材框选状态,至少框选一个区域后才能调用固定 `gpt-image-2` 提取接口,画布同时出现 spritesheet 图集和拆分后的独立素材。
|
||||
- 选中 UI 设计图时浮动工具栏显示 `提取素材`;点击后进入红框素材框选状态,至少框选一个区域后才能调用固定 `gpt-image-2` 提取接口,请求包含 `screenColor`,画布同时出现透明 spritesheet 图集和拆分后的独立素材。
|
||||
- UI 素材提取面板上传普通参考图后,提取请求参考图数组同时包含红框 UI 设计图和普通参考图,生成图层信息面板展示 `UI设计图` 与普通参考图。
|
||||
|
||||
@@ -50,14 +50,14 @@
|
||||
- Prompt 固定为:
|
||||
|
||||
```text
|
||||
参考图1的图标规范,背景必须是单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕,且平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光;禁止出现文字,保证每个图标素材的所有内容区域是完全连通的。按照以下的素材的顺序从上到下从左到右依次生成并整理成一张spritesheet:
|
||||
参考图1的图标规范,背景必须是所选单一纯色抠图背景,且平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光;禁止出现文字,保证每个图标素材的所有内容区域是完全连通的。按照以下的素材的顺序从上到下从左到右依次生成并整理成一张spritesheet:
|
||||
|
||||
<素材描述按中文顿号拼接>
|
||||
```
|
||||
|
||||
## 去背与保存
|
||||
|
||||
- 后端收到 spritesheet 后先把带绿幕源图写入 OSS,再走 `server-rs/crates/api-server/src/editor_green_screen.rs` 的统一绿幕透明化方法,底层复用 `platform-image::generated_asset_sheets`。
|
||||
- 后端收到 spritesheet 后先把带所选纯色背景的源图写入 OSS,再调用 BgFilter 透明化;请求字段包含 `screenColor` 和 `segModel`,`segModel` 可选 `birefnet` 或 `anime-seg`,默认 `birefnet`。
|
||||
- 去背后的整张 spritesheet 统一编码为透明 PNG,并作为唯一图标素材产物持久化。
|
||||
- 响应保留 `iconImageSrcs` 字段用于兼容旧客户端,但图标素材生成固定返回空数组;UI 设计图提取素材仍可复用该响应结构返回切片素材。
|
||||
|
||||
|
||||
@@ -51,10 +51,10 @@
|
||||
- 角色规范与常规参考图作为 `referenceImageSrcs` 传入,顺序固定为:
|
||||
1. 角色规范图。
|
||||
2. 常规参考图列表。
|
||||
- 请求同时提交 `model`、`screenColor`、`aspectRatio` 和 `imageSize`:
|
||||
- 请求同时提交 `model`、`screenColor`、`segModel`、`aspectRatio` 和 `imageSize`:
|
||||
- `model` 支持 `gemini-3.1-flash-image-preview`(UI 显示 `nanobanana2`)和 `gpt-image-2`,默认 `nanobanana2`。
|
||||
- 用户在角色或图标素材面板中切换过模型后,下一次打开这两类面板继续使用上次模型。
|
||||
- `screenColor` 只允许上述六个 hex;前端默认提交 `#CFEFFF`,同源重绘从生成器快照或输入快照 `抠图背景色` 恢复,后续 BGfilter 服务也复用该字段。
|
||||
- `screenColor` 只允许上述六个 hex;前端默认提交 `#CFEFFF`,同源重绘从生成器快照或输入快照 `抠图背景色` 恢复,后端调用 BgFilter 时把该字段作为 `screen_color` 传入。`segModel` 只允许 `birefnet` 或 `anime-seg`,默认 `birefnet`,同源重绘从生成器快照或输入快照 `抠图模型` 恢复,后端调用 BgFilter 时把该字段作为 `seg_model` 传入。
|
||||
- 比例按 `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 +67,7 @@
|
||||
角色设定:<用户输入的角色设定>
|
||||
```
|
||||
|
||||
- 角色图生成完成后,编辑器后端必须先把带所选纯色背景的源图写入 OSS,再走 `server-rs/crates/api-server/src/editor_green_screen.rs` 的统一透明化方法:复用 `platform-image::generated_asset_sheets` 的 key color 去背能力,并开启内部镂空检测。角色图 prompt 按 `screenColor` 写入颜色名称、hex 和 RGB;后端对非绿色 key color 不启用近白背景清理,避免浅色主体被误删。该流程不再调用 RPG / 资产工坊的角色主图专用 `character_visual_assets` 后处理,也不调用远端 BiRefNet;输出仍统一为透明背景 PNG,随后写入 OSS 私有对象并确认 `asset_object`。接口回包仍返回透明 PNG Data URL 供画板立即显示,同时返回 `objectKey` / `assetObjectId`,前端创建图层和画板资源记录时必须保存这些字段。
|
||||
- 角色图生成完成后,编辑器后端必须先把带所选纯色背景的源图写入 OSS,再调用独立 BgFilter 服务透明化:multipart 字段包含 `file`、`screen_color=<screenColor>` 和 `seg_model=<segModel>`,默认 `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 +116,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 骨架补入 `角色设定` 和所选纯色抠图背景,并在生成成功后通过编辑器通用抠图方法执行 key color 去背和内部镂空清理、写入 `generated-character-drafts/editor/character-images/<taskId>/image.png` 路径下的 OSS 私有对象,返回的 `objectKey` / `assetObjectId` 会随画板资源记录保存。
|
||||
- 角色生成后端已按固定 prompt 骨架补入 `角色设定` 和所选纯色抠图背景,并在生成成功后先保存纯色背景源图,再通过 BgFilter 按所选 `segModel` 执行透明化、写入 `generated-character-drafts/editor/character-images/<taskId>/image.png` 路径下的 OSS 私有对象,返回的 `objectKey` / `assetObjectId` 会随画板资源记录保存。
|
||||
- `Esc` 只退出角色规范画布点选状态,不关闭角色生成面板。
|
||||
- 已补充回归测试覆盖角色形象生成、点选退出、角色动画入口隔离和快速编辑入口。
|
||||
- 本次验证命令:
|
||||
|
||||
@@ -16,6 +16,8 @@ const DEFAULT_SPACETIME_HEALTH_CHECK_TIMEOUT_SECONDS: u64 = 2;
|
||||
pub(crate) const DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS: u64 = 1_000_000;
|
||||
const DEFAULT_EDITOR_BACKGROUND_REMOVAL_BASE_URL: &str = "http://58.87.105.82";
|
||||
const DEFAULT_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS: u64 = 120_000;
|
||||
const DEFAULT_EDITOR_BGFILTER_BASE_URL: &str = "http://58.87.105.82/bgfilter";
|
||||
const DEFAULT_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS: u64 = 120_000;
|
||||
|
||||
// 集中管理 api-server 的启动配置,避免入口层直接散落环境变量解析逻辑。
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -56,6 +58,9 @@ pub struct AppConfig {
|
||||
pub editor_background_removal_base_url: String,
|
||||
pub editor_background_removal_token: Option<String>,
|
||||
pub editor_background_removal_request_timeout_ms: u64,
|
||||
pub editor_bgfilter_base_url: String,
|
||||
pub editor_bgfilter_token: Option<String>,
|
||||
pub editor_bgfilter_request_timeout_ms: u64,
|
||||
pub log_filter: String,
|
||||
pub otel_enabled: bool,
|
||||
pub admin_username: Option<String>,
|
||||
@@ -283,6 +288,9 @@ impl Default for AppConfig {
|
||||
editor_background_removal_token: None,
|
||||
editor_background_removal_request_timeout_ms:
|
||||
DEFAULT_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS,
|
||||
editor_bgfilter_base_url: DEFAULT_EDITOR_BGFILTER_BASE_URL.to_string(),
|
||||
editor_bgfilter_token: None,
|
||||
editor_bgfilter_request_timeout_ms: DEFAULT_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS,
|
||||
log_filter: "info,tower_http=info".to_string(),
|
||||
otel_enabled: false,
|
||||
admin_username: None,
|
||||
@@ -473,6 +481,19 @@ impl AppConfig {
|
||||
]) {
|
||||
config.editor_background_removal_request_timeout_ms = timeout_ms;
|
||||
}
|
||||
if let Some(base_url) = read_first_non_empty_env(&["GENARRATIVE_EDITOR_BGFILTER_BASE_URL"])
|
||||
{
|
||||
config.editor_bgfilter_base_url = base_url;
|
||||
}
|
||||
config.editor_bgfilter_token = read_first_non_empty_env(&[
|
||||
"GENARRATIVE_EDITOR_BGFILTER_TOKEN",
|
||||
"GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN",
|
||||
]);
|
||||
if let Some(timeout_ms) =
|
||||
read_first_positive_u64_env(&["GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS"])
|
||||
{
|
||||
config.editor_bgfilter_request_timeout_ms = timeout_ms;
|
||||
}
|
||||
if let Some(listen_backlog) =
|
||||
read_first_positive_i32_env(&["GENARRATIVE_API_LISTEN_BACKLOG"])
|
||||
{
|
||||
@@ -1461,9 +1482,10 @@ fn parse_positive_u16(raw: &str) -> Option<u16> {
|
||||
mod tests {
|
||||
use super::{
|
||||
AppConfig, DEFAULT_EDITOR_BACKGROUND_REMOVAL_BASE_URL,
|
||||
DEFAULT_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS,
|
||||
DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS, ExternalGenerationMode, LlmProvider,
|
||||
ProcessRole, parse_bool, parse_external_generation_mode, parse_process_role,
|
||||
DEFAULT_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS, DEFAULT_EDITOR_BGFILTER_BASE_URL,
|
||||
DEFAULT_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS, DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS,
|
||||
ExternalGenerationMode, LlmProvider, ProcessRole, parse_bool,
|
||||
parse_external_generation_mode, parse_process_role,
|
||||
};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
@@ -1486,6 +1508,15 @@ mod tests {
|
||||
DEFAULT_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS
|
||||
);
|
||||
assert!(config.editor_background_removal_token.is_none());
|
||||
assert_eq!(
|
||||
config.editor_bgfilter_base_url,
|
||||
DEFAULT_EDITOR_BGFILTER_BASE_URL
|
||||
);
|
||||
assert_eq!(
|
||||
config.editor_bgfilter_request_timeout_ms,
|
||||
DEFAULT_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS
|
||||
);
|
||||
assert!(config.editor_bgfilter_token.is_none());
|
||||
assert!(config.ark_character_video_base_url.is_empty());
|
||||
assert_eq!(config.hyper3d_base_url, "https://api.hyper3d.com/api/v2");
|
||||
assert!(config.ark_character_video_model.is_empty());
|
||||
@@ -2152,6 +2183,55 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reads_editor_bgfilter_settings_and_reuses_background_token() {
|
||||
let _guard = ENV_LOCK
|
||||
.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.expect("env lock should not poison");
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_BASE_URL");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_TOKEN");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN");
|
||||
std::env::set_var(
|
||||
"GENARRATIVE_EDITOR_BGFILTER_BASE_URL",
|
||||
"http://10.0.0.12/bgfilter",
|
||||
);
|
||||
std::env::set_var(
|
||||
"GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN",
|
||||
"shared-token",
|
||||
);
|
||||
std::env::set_var("GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS", "180000");
|
||||
}
|
||||
|
||||
let config = AppConfig::from_env();
|
||||
assert_eq!(config.editor_bgfilter_base_url, "http://10.0.0.12/bgfilter");
|
||||
assert_eq!(
|
||||
config.editor_bgfilter_token.as_deref(),
|
||||
Some("shared-token")
|
||||
);
|
||||
assert_eq!(config.editor_bgfilter_request_timeout_ms, 180_000);
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("GENARRATIVE_EDITOR_BGFILTER_TOKEN", "bgfilter-token");
|
||||
}
|
||||
|
||||
let config = AppConfig::from_env();
|
||||
assert_eq!(
|
||||
config.editor_bgfilter_token.as_deref(),
|
||||
Some("bgfilter-token")
|
||||
);
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_BASE_URL");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_TOKEN");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reads_prefixed_character_animation_ffmpeg_paths() {
|
||||
let _guard = ENV_LOCK
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -292,6 +292,7 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
setGenerateDialog={setGenerateDialog}
|
||||
includeDimensions
|
||||
includeBackgroundColor
|
||||
includeSegModel
|
||||
onRememberImageModel={onRememberImageModel}
|
||||
optionLabelPrefix="生成图片"
|
||||
cost={calculateEditorImageGenerationPrice({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -190,6 +190,7 @@ function createStageProps(): ImageCanvasStageViewProps {
|
||||
onUiAssetExtractionToolChange: vi.fn(),
|
||||
onUiAssetExtractionModelChange: vi.fn(),
|
||||
onUiAssetExtractionScreenColorChange: vi.fn(),
|
||||
onUiAssetExtractionSegModelChange: vi.fn(),
|
||||
onUiAssetExtractionPointerStart: vi.fn(),
|
||||
onUiAssetExtractionPointerMove: vi.fn(),
|
||||
onUiAssetExtractionPointerEnd: vi.fn(),
|
||||
@@ -339,6 +340,7 @@ describe('ImageCanvasEditorShellView', () => {
|
||||
tool: 'rect' as const,
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
screenColor: '#CFEFFF',
|
||||
segModel: 'birefnet',
|
||||
marks: [],
|
||||
references: [],
|
||||
draftMark: null,
|
||||
|
||||
@@ -209,6 +209,7 @@ export type GenerateDialogState = {
|
||||
uiDesignSpecReference?: CharacterReferenceImage | null;
|
||||
imageModel?: string;
|
||||
screenColor?: string;
|
||||
segModel?: string;
|
||||
videoModel?: EditorVideoModel;
|
||||
videoAspectRatio?: EditorVideoAspectRatio;
|
||||
videoResolution?: EditorVideoResolution;
|
||||
|
||||
@@ -1181,6 +1181,7 @@ export function ImageCanvasEditorView() {
|
||||
changeUiAssetExtractionTool,
|
||||
changeUiAssetExtractionModel,
|
||||
changeUiAssetExtractionScreenColor,
|
||||
changeUiAssetExtractionSegModel,
|
||||
changeQuickEditSelectionTool,
|
||||
startUiAssetExtractionPointer,
|
||||
startQuickEditSelectionPointer,
|
||||
@@ -1913,6 +1914,7 @@ export function ImageCanvasEditorView() {
|
||||
onUiAssetExtractionToolChange: changeUiAssetExtractionTool,
|
||||
onUiAssetExtractionModelChange: changeUiAssetExtractionModel,
|
||||
onUiAssetExtractionScreenColorChange: changeUiAssetExtractionScreenColor,
|
||||
onUiAssetExtractionSegModelChange: changeUiAssetExtractionSegModel,
|
||||
onUiAssetExtractionPointerStart: startUiAssetExtractionPointer,
|
||||
onUiAssetExtractionPointerMove: moveUiAssetExtractionPointer,
|
||||
onUiAssetExtractionPointerEnd: endUiAssetExtractionPointer,
|
||||
|
||||
@@ -502,6 +502,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
fields: [
|
||||
{ title: '角色设定', value: '红发骑士' },
|
||||
{ title: '抠图背景色', value: '暖浅桃色 #FFD6C2' },
|
||||
{ title: '抠图模型', value: '动漫风格 anime-seg' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
@@ -516,6 +517,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
prompt: '红发骑士',
|
||||
sourceLayerId: 'layer-source',
|
||||
screenColor: '#FFD6C2',
|
||||
segModel: 'anime-seg',
|
||||
placeholder: {
|
||||
x: 472,
|
||||
y: 140,
|
||||
@@ -584,6 +586,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
generatedLayerId: 'layer-source',
|
||||
imageModel: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
segModel: 'anime-seg',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
},
|
||||
@@ -595,6 +598,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
sourceLayerId: 'layer-source',
|
||||
imageModel: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
segModel: 'anime-seg',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
});
|
||||
|
||||
@@ -18,6 +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,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
IMAGE_MODEL_GPT_IMAGE_2,
|
||||
inferEditorImageAspectRatio,
|
||||
inferEditorImageSizeLabel,
|
||||
normalizeEditorBgFilterSegModel,
|
||||
normalizeEditorImageModel,
|
||||
normalizeEditorGenerationBackgroundColor,
|
||||
PUBLICATION_FRAME_ORIGINAL_SIZE,
|
||||
@@ -275,6 +277,7 @@ export function createCharacterGenerationDialogDraft({
|
||||
characterReferences: [],
|
||||
imageModel: normalizedImageModel,
|
||||
screenColor: DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
|
||||
segModel: DEFAULT_EDITOR_BGFILTER_SEG_MODEL,
|
||||
aspectRatio: dimensionDefaults.aspectRatio,
|
||||
imageSize: dimensionDefaults.imageSize,
|
||||
placeholder: {
|
||||
@@ -310,6 +313,7 @@ export function createIconGenerationDialogDraft({
|
||||
iconDescriptions: [],
|
||||
imageModel: normalizedImageModel,
|
||||
screenColor: DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
|
||||
segModel: DEFAULT_EDITOR_BGFILTER_SEG_MODEL,
|
||||
aspectRatio: dimensionDefaults.aspectRatio,
|
||||
imageSize: dimensionDefaults.imageSize,
|
||||
placeholder: {
|
||||
@@ -745,12 +749,16 @@ function restoreSharedImageOptions(
|
||||
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 ??
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* @vitest-environment jsdom */
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { useState } from 'react';
|
||||
@@ -16,12 +16,14 @@ 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;
|
||||
}) {
|
||||
@@ -36,6 +38,7 @@ function ImageOptionsHarness({
|
||||
setGenerateDialog={setDialog}
|
||||
includeDimensions={includeDimensions}
|
||||
includeBackgroundColor={includeBackgroundColor}
|
||||
includeSegModel={includeSegModel}
|
||||
onRememberImageModel={onRememberImageModel}
|
||||
lockedModel={lockedModel}
|
||||
cost={calculateEditorImageGenerationPrice({
|
||||
@@ -49,6 +52,7 @@ function ImageOptionsHarness({
|
||||
<output aria-label="当前比例">{dialog.aspectRatio}</output>
|
||||
<output aria-label="当前尺寸">{dialog.imageSize}</output>
|
||||
<output aria-label="当前背景色">{dialog.screenColor}</output>
|
||||
<output aria-label="当前抠图模型">{dialog.segModel}</output>
|
||||
<output aria-label="当前状态">{dialog.status}</output>
|
||||
<output aria-label="当前错误">{dialog.errorMessage ?? '-'}</output>
|
||||
<output aria-label="当前占位">
|
||||
@@ -219,6 +223,51 @@ describe('ImageCanvasGenerationImageOptionsView', () => {
|
||||
expect(screen.getByLabelText('当前错误').textContent).toBe('-');
|
||||
});
|
||||
|
||||
it('updates the BgFilter segmentation model from the model menu', () => {
|
||||
render(
|
||||
<ImageOptionsHarness
|
||||
includeSegModel
|
||||
initialDialog={{
|
||||
mode: 'character',
|
||||
prompt: '',
|
||||
status: 'failed',
|
||||
errorMessage: '旧错误',
|
||||
imageModel: IMAGE_MODEL_NANOBANANA2,
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
screenColor: '#CFEFFF',
|
||||
segModel: 'birefnet',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: '生成图片抠图模型 默认',
|
||||
}),
|
||||
);
|
||||
const panel = screen.getByRole('menu', { name: '生成图片抠图模型选项' });
|
||||
fireEvent.click(
|
||||
within(panel).getByRole('button', {
|
||||
name: '动漫风格',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole('menu', { name: '生成图片抠图模型选项' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(panel)
|
||||
.getByRole('button', { name: '动漫风格' })
|
||||
.getAttribute('aria-pressed'),
|
||||
).toBe('true');
|
||||
expect(screen.getByLabelText('当前抠图模型').textContent).toBe(
|
||||
'anime-seg',
|
||||
);
|
||||
expect(screen.getByLabelText('当前状态').textContent).toBe('idle');
|
||||
expect(screen.getByLabelText('当前错误').textContent).toBe('-');
|
||||
});
|
||||
|
||||
it('locks the model trigger when a fixed model is provided', () => {
|
||||
const rememberImageModel = vi.fn();
|
||||
render(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Check, ChevronDown, Cpu } from 'lucide-react';
|
||||
import { Check, ChevronDown, Cpu, Scissors } from 'lucide-react';
|
||||
import {
|
||||
type CSSProperties,
|
||||
type Dispatch,
|
||||
@@ -15,11 +15,14 @@ import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton
|
||||
import type { GenerateDialogState } from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
||||
EDITOR_BGFILTER_SEG_MODEL_OPTIONS,
|
||||
EDITOR_IMAGE_MODEL_OPTIONS,
|
||||
EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS,
|
||||
getEditorBgFilterSegModelOption,
|
||||
getEditorGenerationBackgroundColorOption,
|
||||
getEditorImageModelDisplayName,
|
||||
IMAGE_MODEL_NANOBANANA2,
|
||||
normalizeEditorBgFilterSegModel,
|
||||
normalizeEditorGenerationBackgroundColor,
|
||||
normalizeEditorImageModel,
|
||||
QUICK_EDIT_MODEL_OPTIONS,
|
||||
@@ -34,6 +37,7 @@ type ImageCanvasGenerationImageOptionsViewProps = {
|
||||
includeDimensions: boolean;
|
||||
includeModel?: boolean;
|
||||
includeBackgroundColor?: boolean;
|
||||
includeSegModel?: boolean;
|
||||
onRememberImageModel: (model: string) => void;
|
||||
optionLabelPrefix?: string;
|
||||
dimensionRatioAriaLabelPrefix?: string;
|
||||
@@ -50,7 +54,7 @@ type ImageCanvasGenerationImageOptionsViewProps = {
|
||||
) => CSSProperties;
|
||||
};
|
||||
|
||||
type OpenPanel = 'dimensions' | 'model' | 'backgroundColor' | null;
|
||||
type OpenPanel = 'dimensions' | 'model' | 'backgroundColor' | 'segModel' | null;
|
||||
|
||||
function resetFailedDialogStatus(dialog: GenerateDialogState) {
|
||||
return {
|
||||
@@ -126,6 +130,7 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
includeDimensions,
|
||||
includeModel = true,
|
||||
includeBackgroundColor = false,
|
||||
includeSegModel = false,
|
||||
onRememberImageModel,
|
||||
optionLabelPrefix,
|
||||
dimensionRatioAriaLabelPrefix,
|
||||
@@ -141,6 +146,7 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
const [openPanel, setOpenPanel] = useState<OpenPanel>(null);
|
||||
const dimensionsButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const backgroundColorButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const segModelButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const modelButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const isQuickEdit = dialog.mode === 'quick-edit';
|
||||
const normalizedLockedModel = lockedModel
|
||||
@@ -165,6 +171,7 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
const selectedBackgroundColor = getEditorGenerationBackgroundColorOption(
|
||||
dialog.screenColor,
|
||||
);
|
||||
const selectedSegModel = getEditorBgFilterSegModelOption(dialog.segModel);
|
||||
const selectedSizeLabel = resolveEditorImageSizeLabel({
|
||||
aspectRatio: selection.aspectRatio,
|
||||
imageSize: selection.imageSize,
|
||||
@@ -184,6 +191,7 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
boundaryRefs: [
|
||||
dimensionsButtonRef,
|
||||
backgroundColorButtonRef,
|
||||
segModelButtonRef,
|
||||
modelButtonRef,
|
||||
],
|
||||
onDismiss: dismissOpenPanel,
|
||||
@@ -246,6 +254,17 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
);
|
||||
};
|
||||
|
||||
const updateSegModel = (segModel: string) => {
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog && currentDialog.mode === dialog.mode
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
segModel: normalizeEditorBgFilterSegModel(segModel),
|
||||
}
|
||||
: currentDialog,
|
||||
);
|
||||
};
|
||||
|
||||
const togglePanel = (panel: Exclude<OpenPanel, null>) => {
|
||||
setOpenPanel((currentPanel) => (currentPanel === panel ? null : panel));
|
||||
};
|
||||
@@ -400,6 +419,68 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
{includeSegModel ? (
|
||||
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--seg-model">
|
||||
<PlatformInlineOptionButton
|
||||
ref={segModelButtonRef}
|
||||
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--seg-model"
|
||||
aria-label={`${labelPrefix}抠图模型 ${selectedSegModel.label}`}
|
||||
aria-expanded={openPanel === 'segModel'}
|
||||
disabled={isGenerating}
|
||||
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
||||
onClick={() => togglePanel('segModel')}
|
||||
>
|
||||
<span className="image-canvas-editor__model-trigger-label">
|
||||
<span
|
||||
className="image-canvas-editor__model-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Scissors />
|
||||
</span>
|
||||
<span>{selectedSegModel.label}</span>
|
||||
</span>
|
||||
</PlatformInlineOptionButton>
|
||||
{openPanel === 'segModel'
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__option-popover image-canvas-editor__option-popover--seg-model image-canvas-editor__portal-menu"
|
||||
label={`${labelPrefix}抠图模型选项`}
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(segModelButtonRef.current, 'above')}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="image-canvas-editor__option-popover-items image-canvas-editor__option-popover-items--seg-model">
|
||||
{EDITOR_BGFILTER_SEG_MODEL_OPTIONS.map((option) => {
|
||||
const selected = selectedSegModel.value === option.value;
|
||||
return (
|
||||
<OptionChoice
|
||||
key={option.value}
|
||||
selected={selected}
|
||||
className="image-canvas-editor__option-popover-choice--seg-model"
|
||||
ariaLabel={option.label}
|
||||
onClick={() => updateSegModel(option.value)}
|
||||
>
|
||||
<span
|
||||
className="image-canvas-editor__model-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Scissors />
|
||||
</span>
|
||||
<span>{option.label}</span>
|
||||
<Check
|
||||
className="image-canvas-editor__option-selected-check"
|
||||
data-visible={selected}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</OptionChoice>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PlatformFloatingMenu>,
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
{includeModel ? (
|
||||
<div
|
||||
className={[
|
||||
|
||||
@@ -253,6 +253,7 @@ describe('ImageCanvasGenerationModel', () => {
|
||||
fields: [
|
||||
{ title: '角色设定', value: '主角骑士' },
|
||||
{ title: '抠图背景色', value: '浅雾蓝 #CFEFFF' },
|
||||
{ title: '抠图模型', value: '默认 birefnet' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
@@ -281,6 +282,7 @@ describe('ImageCanvasGenerationModel', () => {
|
||||
fields: [
|
||||
{ title: '素材描述', value: '返回按钮\n设置按钮' },
|
||||
{ title: '抠图背景色', value: '浅雾蓝 #CFEFFF' },
|
||||
{ title: '抠图模型', value: '默认 birefnet' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
@@ -322,6 +324,7 @@ describe('ImageCanvasGenerationModel', () => {
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
},
|
||||
{ title: '抠图背景色', value: '浅雾蓝 #CFEFFF' },
|
||||
{ title: '抠图模型', value: '默认 birefnet' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
|
||||
@@ -109,6 +109,14 @@ export type EditorGenerationBackgroundColorValue =
|
||||
(typeof EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS)[number]['value'];
|
||||
export const DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR =
|
||||
EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS[0].value;
|
||||
export const EDITOR_BGFILTER_SEG_MODEL_OPTIONS = [
|
||||
{ label: '默认', value: 'birefnet' },
|
||||
{ label: '动漫风格', value: 'anime-seg' },
|
||||
] as const;
|
||||
export type EditorBgFilterSegModelValue =
|
||||
(typeof EDITOR_BGFILTER_SEG_MODEL_OPTIONS)[number]['value'];
|
||||
export const DEFAULT_EDITOR_BGFILTER_SEG_MODEL =
|
||||
EDITOR_BGFILTER_SEG_MODEL_OPTIONS[0].value;
|
||||
export const UI_DESIGN_ASSET_EXTRACTION_PROMPT =
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。';
|
||||
export const QUICK_EDIT_SIZE_PRESETS = [
|
||||
@@ -591,6 +599,36 @@ export function formatEditorGenerationBackgroundColor(
|
||||
return `${option.label} ${option.value}`;
|
||||
}
|
||||
|
||||
export function normalizeEditorBgFilterSegModel(
|
||||
value: string | null | undefined,
|
||||
): EditorBgFilterSegModelValue {
|
||||
const normalizedValue = value?.trim().toLowerCase();
|
||||
const normalizedToken = normalizedValue?.match(/(?:anime-seg|birefnet)/u)?.[0];
|
||||
const matched = EDITOR_BGFILTER_SEG_MODEL_OPTIONS.find(
|
||||
(option) =>
|
||||
option.value === normalizedValue || option.value === normalizedToken,
|
||||
);
|
||||
return matched?.value ?? DEFAULT_EDITOR_BGFILTER_SEG_MODEL;
|
||||
}
|
||||
|
||||
export function getEditorBgFilterSegModelOption(
|
||||
value: string | null | undefined,
|
||||
) {
|
||||
const normalizedValue = normalizeEditorBgFilterSegModel(value);
|
||||
return (
|
||||
EDITOR_BGFILTER_SEG_MODEL_OPTIONS.find(
|
||||
(option) => option.value === normalizedValue,
|
||||
) ?? EDITOR_BGFILTER_SEG_MODEL_OPTIONS[0]
|
||||
);
|
||||
}
|
||||
|
||||
export function formatEditorBgFilterSegModel(
|
||||
value: string | null | undefined,
|
||||
) {
|
||||
const option = getEditorBgFilterSegModelOption(value);
|
||||
return `${option.label} ${option.value}`;
|
||||
}
|
||||
|
||||
export function buildQuickEditModelOptions(currentModel: string) {
|
||||
void currentModel;
|
||||
const options = [...QUICK_EDIT_MODEL_OPTIONS];
|
||||
@@ -1117,6 +1155,7 @@ export function buildCharacterGenerationInputs(
|
||||
specReference: CharacterReferenceImage | null | undefined,
|
||||
references: CharacterReferenceImage[] | undefined,
|
||||
screenColor?: string,
|
||||
segModel?: string,
|
||||
): CanvasGenerationInputs {
|
||||
return {
|
||||
fields: [
|
||||
@@ -1125,6 +1164,10 @@ export function buildCharacterGenerationInputs(
|
||||
'抠图背景色',
|
||||
formatEditorGenerationBackgroundColor(screenColor),
|
||||
),
|
||||
...createGenerationInputField(
|
||||
'抠图模型',
|
||||
formatEditorBgFilterSegModel(segModel),
|
||||
),
|
||||
],
|
||||
references: [
|
||||
...(specReference
|
||||
@@ -1231,6 +1274,7 @@ export function buildIconGenerationInputs(
|
||||
specReference: CharacterReferenceImage,
|
||||
references?: CharacterReferenceImage[],
|
||||
screenColor?: string,
|
||||
segModel?: string,
|
||||
): CanvasGenerationInputs {
|
||||
return {
|
||||
fields: [
|
||||
@@ -1242,6 +1286,10 @@ export function buildIconGenerationInputs(
|
||||
'抠图背景色',
|
||||
formatEditorGenerationBackgroundColor(screenColor),
|
||||
),
|
||||
...createGenerationInputField(
|
||||
'抠图模型',
|
||||
formatEditorBgFilterSegModel(segModel),
|
||||
),
|
||||
],
|
||||
references: [
|
||||
...createGenerationInputReference('图标规范', specReference),
|
||||
@@ -1256,6 +1304,7 @@ export function buildUiDesignAssetExtractionGenerationInputs(
|
||||
sourceLayer: CanvasLayer,
|
||||
references?: CharacterReferenceImage[],
|
||||
screenColor?: string,
|
||||
segModel?: string,
|
||||
): CanvasGenerationInputs {
|
||||
return {
|
||||
fields: [
|
||||
@@ -1267,6 +1316,10 @@ export function buildUiDesignAssetExtractionGenerationInputs(
|
||||
'抠图背景色',
|
||||
formatEditorGenerationBackgroundColor(screenColor),
|
||||
),
|
||||
...createGenerationInputField(
|
||||
'抠图模型',
|
||||
formatEditorBgFilterSegModel(segModel),
|
||||
),
|
||||
],
|
||||
references: [
|
||||
...createLayerGenerationInputReference('UI设计图', sourceLayer),
|
||||
|
||||
@@ -274,6 +274,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
status: 'idle',
|
||||
imageModel: 'gpt-image-2',
|
||||
screenColor: '#FFD6C2',
|
||||
segModel: 'anime-seg',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
characterSpecReference: {
|
||||
@@ -303,6 +304,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
kind: 'character',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#FFD6C2',
|
||||
segModel: 'anime-seg',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
referenceImageSrcs: [
|
||||
@@ -321,6 +323,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
fields: [
|
||||
{ title: '角色设定', value: '白发骑士' },
|
||||
{ title: '抠图背景色', value: '暖浅桃色 #FFD6C2' },
|
||||
{ title: '抠图模型', value: '动漫风格 anime-seg' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
@@ -617,7 +620,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an icon spritesheet error when descriptions are empty', () => {
|
||||
it('falls back to default icon descriptions when the draft descriptions are empty', () => {
|
||||
const plan = buildIconSpritesheetGenerationSubmissionPlan({
|
||||
mode: 'icon',
|
||||
prompt: '',
|
||||
@@ -631,10 +634,15 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
iconDescriptions: [' ', '\n'],
|
||||
});
|
||||
|
||||
expect(plan).toEqual({
|
||||
ok: false,
|
||||
errorMessage: '请填写素材描述',
|
||||
});
|
||||
expect(plan.ok).toBe(true);
|
||||
expect(plan.ok === true ? plan.iconDescriptions : []).toEqual([
|
||||
'返回按钮',
|
||||
'设置按钮',
|
||||
'下一关按钮',
|
||||
'提示按钮',
|
||||
'原图按钮',
|
||||
'冻结按钮',
|
||||
]);
|
||||
});
|
||||
|
||||
it('builds icon spritesheet plans with trimmed descriptions and references', () => {
|
||||
@@ -644,6 +652,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
status: 'idle',
|
||||
imageModel: 'gpt-image-2',
|
||||
screenColor: '#7FB3FF',
|
||||
segModel: 'anime-seg',
|
||||
aspectRatio: '3:2',
|
||||
imageSize: '2K',
|
||||
iconSpecReference: {
|
||||
@@ -672,6 +681,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#7FB3FF',
|
||||
segModel: 'anime-seg',
|
||||
aspectRatio: '3:2',
|
||||
imageSize: '2K',
|
||||
},
|
||||
@@ -679,6 +689,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
fields: [
|
||||
{ title: '素材描述', value: '返回按钮\n设置按钮' },
|
||||
{ title: '抠图背景色', value: '中度天蓝 #7FB3FF' },
|
||||
{ title: '抠图模型', value: '动漫风格 anime-seg' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
IMAGE_MODEL_GPT_IMAGE_2,
|
||||
inferEditorImageAspectRatio,
|
||||
inferEditorImageSizeLabel,
|
||||
normalizeEditorBgFilterSegModel,
|
||||
normalizeEditorGenerationBackgroundColor,
|
||||
normalizeEditorImageModel,
|
||||
resolveCharacterAnimationSourceImageSrc,
|
||||
@@ -351,6 +352,7 @@ export function buildImageGenerationSubmissionPlan({
|
||||
const screenColor = normalizeEditorGenerationBackgroundColor(
|
||||
dialog.screenColor,
|
||||
);
|
||||
const segModel = normalizeEditorBgFilterSegModel(dialog.segModel);
|
||||
return {
|
||||
kind: 'image',
|
||||
normalizedPrompt,
|
||||
@@ -359,6 +361,7 @@ export function buildImageGenerationSubmissionPlan({
|
||||
kind: 'character',
|
||||
model: imageModel,
|
||||
screenColor,
|
||||
segModel,
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||
@@ -371,6 +374,7 @@ export function buildImageGenerationSubmissionPlan({
|
||||
dialog.characterSpecReference,
|
||||
dialog.characterReferences,
|
||||
screenColor,
|
||||
segModel,
|
||||
),
|
||||
},
|
||||
rememberImageModel: imageModel,
|
||||
@@ -575,11 +579,14 @@ export function buildImageGenerationSubmissionPlan({
|
||||
export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
dialog: GenerateDialogState,
|
||||
): IconSpritesheetGenerationSubmissionPlan {
|
||||
const iconDescriptions = (
|
||||
dialog.prompt.trim()
|
||||
? dialog.prompt.split(/[\r\n,,、;;/|]+/u)
|
||||
: (dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS)
|
||||
)
|
||||
const iconDescriptionSource = dialog.prompt.trim()
|
||||
? dialog.prompt.split(/[\r\n,,、;;/|]+/u)
|
||||
: (dialog.iconDescriptions ?? []).some((description) =>
|
||||
description.trim(),
|
||||
)
|
||||
? (dialog.iconDescriptions ?? [])
|
||||
: DEFAULT_ICON_DESCRIPTIONS;
|
||||
const iconDescriptions = iconDescriptionSource
|
||||
.map((description) => description.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, ICON_DESCRIPTION_LIMIT);
|
||||
@@ -602,6 +609,7 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
const screenColor = normalizeEditorGenerationBackgroundColor(
|
||||
dialog.screenColor,
|
||||
);
|
||||
const segModel = normalizeEditorBgFilterSegModel(dialog.segModel);
|
||||
return {
|
||||
ok: true,
|
||||
iconDescriptions,
|
||||
@@ -619,6 +627,7 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
iconDescriptions,
|
||||
model: rememberImageModel,
|
||||
screenColor,
|
||||
segModel,
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
},
|
||||
@@ -627,6 +636,7 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
dialog.iconSpecReference,
|
||||
dialog.generationReferences,
|
||||
screenColor,
|
||||
segModel,
|
||||
),
|
||||
rememberImageModel,
|
||||
};
|
||||
|
||||
@@ -240,6 +240,7 @@ export function ImageCanvasIconSpritesheetComposerView({
|
||||
setGenerateDialog={setGenerateDialog}
|
||||
includeDimensions
|
||||
includeBackgroundColor
|
||||
includeSegModel
|
||||
onRememberImageModel={onRememberImageModel}
|
||||
optionLabelPrefix="生成图片"
|
||||
cost={calculateEditorIconSpritesheetPrice(
|
||||
|
||||
@@ -129,6 +129,7 @@ export type ImageCanvasStageViewProps = {
|
||||
onUiAssetExtractionToolChange: (tool: UiAssetExtractionTool | null) => void;
|
||||
onUiAssetExtractionModelChange: (model: string) => void;
|
||||
onUiAssetExtractionScreenColorChange: (screenColor: string) => void;
|
||||
onUiAssetExtractionSegModelChange: (segModel: string) => void;
|
||||
onUiAssetExtractionPointerStart: (point: { x: number; y: number }) => void;
|
||||
onUiAssetExtractionPointerMove: (point: { x: number; y: number }) => void;
|
||||
onUiAssetExtractionPointerEnd: () => void;
|
||||
@@ -252,6 +253,7 @@ export function ImageCanvasStageView({
|
||||
onUiAssetExtractionToolChange,
|
||||
onUiAssetExtractionModelChange,
|
||||
onUiAssetExtractionScreenColorChange,
|
||||
onUiAssetExtractionSegModelChange,
|
||||
onUiAssetExtractionPointerStart,
|
||||
onUiAssetExtractionPointerMove,
|
||||
onUiAssetExtractionPointerEnd,
|
||||
@@ -369,6 +371,7 @@ export function ImageCanvasStageView({
|
||||
onPointerEnd={onUiAssetExtractionPointerEnd}
|
||||
onModelChange={onUiAssetExtractionModelChange}
|
||||
onScreenColorChange={onUiAssetExtractionScreenColorChange}
|
||||
onSegModelChange={onUiAssetExtractionSegModelChange}
|
||||
onRequestUpload={onRequestUiAssetExtractionReferenceUpload}
|
||||
onRemoveReference={onRemoveUiAssetExtractionReference}
|
||||
onSubmit={onSubmitUiAssetExtraction}
|
||||
|
||||
@@ -31,6 +31,7 @@ export type UiAssetExtractionState = {
|
||||
tool: UiAssetExtractionTool | null;
|
||||
model: string;
|
||||
screenColor: string;
|
||||
segModel: string;
|
||||
marks: UiAssetExtractionMark[];
|
||||
references: CharacterReferenceImage[];
|
||||
draftMark: UiAssetExtractionMark | null;
|
||||
@@ -53,6 +54,7 @@ export function createUiAssetExtractionState(
|
||||
initialTool?: UiAssetExtractionTool | null;
|
||||
model: string;
|
||||
screenColor: string;
|
||||
segModel: string;
|
||||
},
|
||||
): UiAssetExtractionState {
|
||||
return {
|
||||
@@ -60,6 +62,7 @@ export function createUiAssetExtractionState(
|
||||
tool: options.initialTool === undefined ? 'rect' : options.initialTool,
|
||||
model: options.model,
|
||||
screenColor: options.screenColor,
|
||||
segModel: options.segModel,
|
||||
marks: [],
|
||||
references: [],
|
||||
draftMark: null,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user