新增图片生成像素艺术后处理
新增确定性像素网格规整模块并保留第三方 MIT 许可证。 为普通图片、角色和图标图集接入像素艺术选项、并发预算和失败降级。 同步前端交互、请求契约、OpenAPI、项目文档与测试。
This commit is contained in:
@@ -2561,6 +2561,21 @@
|
||||
],
|
||||
"description": "纯色抠像背景色。可传画布支持的纯色背景 hex(如 #CFEFFF)指定;传 \"auto\"、null 或省略则由服务端自动决策。"
|
||||
},
|
||||
"style": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"examples": [
|
||||
"none",
|
||||
"pixelArt"
|
||||
],
|
||||
"description": "可选生成后处理风格,当前识别 none 与 pixelArt。省略、null、空字符串或 none 按无风格处理;pixelArt 仅支持普通图片(kind 省略)和 character。未知字符串或不支持该风格的 kind 按 none 继续生成并返回 unsupported-image-style 告警;非字符串值返回 400。"
|
||||
},
|
||||
"size": {
|
||||
"type": "string",
|
||||
"description": "兼容旧 size 入参;未传 aspectRatio/imageSize 时生效。",
|
||||
@@ -2585,7 +2600,7 @@
|
||||
"ui-design",
|
||||
"publication-material"
|
||||
],
|
||||
"default": "spec"
|
||||
"description": "省略时生成普通图片;其它值选择对应的专用生成流程。"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
@@ -2963,6 +2978,21 @@
|
||||
],
|
||||
"description": "纯色抠像背景色。可传画布支持的纯色背景 hex(如 #CFEFFF)指定;传 \"auto\"、null 或省略则由服务端自动决策。"
|
||||
},
|
||||
"style": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"examples": [
|
||||
"none",
|
||||
"pixelArt"
|
||||
],
|
||||
"description": "可选生成后处理风格,当前识别 none 与 pixelArt。省略、null、空字符串或 none 按无风格处理;pixelArt 启用图标图集像素规整。未知字符串按 none 继续生成并返回 unsupported-image-style 告警;非字符串值返回 400。"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"default": "gemini-3.1-flash-image-preview"
|
||||
@@ -3176,8 +3206,13 @@
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"const": "postprocess-failed-source-preserved",
|
||||
"description": "透明背景处理最终失败并保留 provider 原图时的稳定原因码。"
|
||||
"enum": [
|
||||
"postprocess-failed-source-preserved",
|
||||
"dimension-restore-fallback",
|
||||
"unsupported-image-style",
|
||||
"multiple-generation-warnings"
|
||||
],
|
||||
"description": "生成成功但后处理发生非阻断降级时的稳定原因码。"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
@@ -3226,7 +3261,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "图集已成功持久化,但自动拆分未完成时返回;此时 iconImageSrcs 为空,调用方仍应使用整张图集。与通用 warning 互斥。"
|
||||
"description": "图集已成功持久化,但自动拆分未完成时返回;此时 iconImageSrcs 为空,调用方仍应使用整张图集。透明背景最终失败时不会进入拆分;风格归一化或像素规整产生通用 warning 时,两者可以并存。"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
@@ -3290,14 +3325,8 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "透明背景处理最终失败、provider 原图作为主结果时返回的非阻断告警。与 sliceWarning 互斥。"
|
||||
"description": "生成成功但风格归一化、尺寸恢复、透明背景处理或像素规整发生非阻断降级时返回。透明背景最终失败时不会进入拆分;其它通用告警可以与 sliceWarning 并存。"
|
||||
}
|
||||
},
|
||||
"not": {
|
||||
"required": [
|
||||
"warning",
|
||||
"sliceWarning"
|
||||
]
|
||||
}
|
||||
},
|
||||
"EditorCharacterAnimationGenerationRequest": {
|
||||
|
||||
@@ -4510,3 +4510,13 @@
|
||||
- 对账边界:保留管理员显式手动对账。owner 始终可用;member 必须单独持有 `profile-wallet-consumption-reconcile` 独立操作权限,任意 Tab 都不隐式授予。`POST /admin/api/profile/users/reconcile-consumption` 经二次确认后调用 runtime service identity 受限 procedure,扫描该用户全部权威流水、比较并校准投影,记录管理员与对账时间。
|
||||
- 展示边界:现有共享“用户详情”弹窗的钱包区增加“历史花费”,前端只展示 BFF 顶层字段,不自行汇总账单;只有 BFF 返回 `canReconcileConsumption=true` 时展示手动对账按钮。
|
||||
- 验证方式:SpacetimeDB 钱包聚合测试、api-server / admin-web 定向测试、`npm run spacetime:generate`、`npm run check:spacetime-schema`、`npm run check:spacetime-runtime-access`、`npm run admin-web:typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-07-28 图片生成风格使用可扩展字段并以纯内存像素规整首发
|
||||
|
||||
- 契约:普通图片 / 角色共用的图片生成请求和图标图集生成请求增加可选字符串 `style`,当前公开合法值为 `none / pixelArt`。省略、`null`、空字符串和 `none` 归一为内部 `None` 且不告警;未知字符串、或在 `spec / quick-edit / ui-design / publication-material` 等不支持的图片 `kind` 上请求 `pixelArt` 时,按 `None` 继续原管线并返回 `unsupported-image-style` 通用告警;非字符串 JSON 返回 `400`。旧队列 payload 缺少字段时兼容为 `None`。
|
||||
- UI 边界:只有普通 `生成图片`、`生成角色形象` 和 `生成图标素材` 显示 `像素艺术` 勾选项;当前选择可进入已有生成器快照和请求 / 队列 payload,但不写入 `generationInputs`、素材元数据或新表。画布 Agent 和其它生成 / 编辑入口不开放该选项。
|
||||
- 处理边界:`PixelArt` 由 `platform-image` 的纯同步、纯内存 Rust 模块执行,不运行 Python、不访问 OSS / 数据库 / 画布。普通图片直接使用 provider 图;角色和图标必须等 BgFilter 成功并把 Alpha 回贴到 provider 原尺寸后,以 provider 平底原图分析网格、以透明 RGBA 图采样。固定参数为分析色数 16、Alpha 覆盖阈值 0.375、像素尺寸自动、无固定色板、K-means 最大采样 262144;单格 RGB 按 Alpha 加权,输出 Alpha 只为 0 / 255,逻辑低分辨率结果用 nearest 恢复交付尺寸并跳过 Lanczos。
|
||||
- 执行边界:像素规整 CPU 工作使用进程级最大并发 2;取得并发许可的排队时间与实际处理时间共享最多 30 秒预算,同时不得晚于当前请求 deadline,最终取更早者。输入图片任一边上限为 10000 像素、总像素上限为 8294400;超限、排队超时或处理超时均按 best-effort 非致命降级,不持久化部分结果。
|
||||
- 去背边界:不修改 BgFilter `flat` 参数、`cross_check`、fallback、Alpha 回贴和默认关闭 despill 的现有行为。BgFilter 最终失败时不运行像素规整;像素规整失败按 best-effort 非致命降级,保留进入该步骤前的图片并通过既有通用 `warning` 完成任务,不退款。
|
||||
- 持久化边界:逻辑低分辨率图、像素化前后对比图、预览、诊断和报告一律不持久化;像素模式只替换原本即将上传的最终图片字节。普通图片、角色、图标的 OSS PUT、asset / project resource 和画布 item 数量必须与 `None` 模式完全一致;角色 / 图标最多因复用失败增加一次对已有 provider 对象的 OSS GET,不得增加 PUT、资源类型、画布项、队列类型或 schema 字段。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`、`docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`、`docs/openapi/genarrative-external-v1.openapi.json`。
|
||||
|
||||
@@ -31,6 +31,18 @@
|
||||
- 画布底部工具栏 / 面板 Dock 提供“画布 Agent”入口。点击后打开右侧独立 Agent 对话面板;桌面端为右侧窄面板,移动端占满可用宽度。该面板只与右上角任务侧栏互斥;素材 / 图层侧栏允许与 Agent 同时展开,切换左侧栏不得关闭 Agent。Agent 面板不得在当前画布内容下方追加内联内容,也不默认展示大段功能说明文案。
|
||||
- 所有会新建画布生成占位的入口必须先创建 draft,再统一经过 `ImageCanvasGenerationPlacementModel` 计算落点,禁止各入口自行使用当前视口中心裸坐标或原图右侧固定偏移。当前覆盖入口包括 `生成图片`、`生成规范`、`生成角色形象`、`生成图标素材`、`生成视频`、`生成UI设计图` 和 `生成角色动作`。placement 模型的避让对象为所有未隐藏画布图层,以及当前 active / inactive generation dialogs 中仍存在的 placeholder;每个避让矩形按 32px 画布世界坐标间距外扩。候选落点以当前视口世界中心为距离目标,优先选择离视口中心最近且不重叠的占位位置;若中心被占用,会按上下左右和环形候选继续寻找。打开生成面板时必须把避让后的 placeholder 写入 `openCanvasGenerationDialog(...)`,并立即调用 `centerViewportOnPlacement(...)` 居中到新占位中心,保持原 viewport scale 不变;图片快速编辑不属于新建占位入口,提交后覆盖源图。
|
||||
|
||||
### 静态图片风格与像素规整边界
|
||||
|
||||
- 普通 `生成图片`、`生成角色形象` 和 `生成图标素材` 三个面板增加紧凑的 `像素艺术` 勾选项;移动端可独占一行,但不增加功能说明文案。当前生成对象以 `style: "none" | "pixelArt"` 保存选择并随现有请求 / 队列 payload 传递;该字段不写入用户可见 `generationInputs`,也不新增素材元数据字段。其它生成、编辑、UI 素材提取、角色动画及画布 Agent 入口不展示或设置该选项。
|
||||
- `style` 是可选字符串兼容边界。省略、`null`、空字符串和 `"none"` 统一归一为内部 `None`,不返回告警;`"pixelArt"` 仅允许普通图片(`kind` 省略)与 `kind="character"`,图标图集请求单独允许该值。未知字符串或在 `spec / quick-edit / ui-design / publication-material` 等不支持的图片 `kind` 上请求 `"pixelArt"` 时,按 `None` 完成原管线并通过既有通用 `warning` 返回 `unsupported-image-style`;非字符串 JSON 仍是畸形请求并返回 `400`。旧 payload 缺少字段时等价于 `None`。
|
||||
- `None` 必须保持现有生成、尺寸处理、BgFilter、上传、资源和画布链路不变。`PixelArt` 只增加父流程内的纯内存 Rust 后处理,不启动 Python 或独立服务,也不改变 BgFilter 的 `flat` 参数、Alpha 回贴、`cross_check`、fallback 或默认关闭 despill 的现有行为。
|
||||
- 普通图片在 provider 回图后,以同一张图同时作为网格分析源和 RGBA 采样源;角色与图标在 BgFilter 正常成功、现有 Alpha 蒙版回贴到 provider 原尺寸后执行双输入规整,其中网格分析源为带纯色背景的 provider 原图,RGBA 采样源为 Alpha 已回贴的透明图。固定首版参数为:分析色数 `16`、Alpha 覆盖阈值 `0.375`、像素格尺寸自动检测、固定色板关闭、K-means 最大采样 `262144`。
|
||||
- 像素规整 CPU 工作使用进程级最大并发 `2`;取得并发许可的排队时间与实际处理时间共享最多 `30` 秒预算,同时不得晚于当前请求 deadline,最终以两者中更早者为准。输入图片任一边不得超过 `10000` 像素,总像素不得超过 `8294400`;超限、排队超时或处理超时均按像素后处理失败的 best-effort 规则保留进入该步骤前的图片。
|
||||
- 单格颜色按 `Σ(A × RGB) / ΣA` 进行 Alpha 加权;单格覆盖率按 `Σ(A / 255) / N` 计算。覆盖率大于等于 `0.375` 且 `ΣA > 0` 时输出硬 Alpha `255`,否则输出严格的 `[0,0,0,0]`;最终 Alpha 只允许 `0 / 255`。分析用 16 色只负责网格识别,不限制最终输出色数。
|
||||
- 逻辑低分辨率图只存在于内存,随后使用 nearest 恢复到该任务原有交付尺寸,并直接替换原本即将持久化的最终图片字节。像素模式不得再经过 Lanczos 或其它会重新引入软边的插值。角色和图标应复用 Alpha 回贴阶段已经读取的 provider 原图;确需重新读取时,最多增加一次对已有 provider 对象的 OSS GET,不得新增 OSS PUT。
|
||||
- 像素模式的持久化增量必须为零:普通图片仍只上传原有一张最终主图;角色仍只保留原有 provider 原图与透明主图;图标仍只保留原有 provider 原图、透明图集和实际成功的切片。禁止保存逻辑低分辨率图、像素化前后双份主图、预览图、网格诊断图或报告,禁止新增 asset / resource 类型、项目资源、画布 item、队列 job kind 或数据库字段。
|
||||
- 像素后处理属于 best-effort:失败时保留进入该步骤前的图片,继续原有最终上传与画布完成,并通过既有通用 `warning` 返回非阻断原因,不把任务改为失败或退款。BgFilter 自身失败时仍按原 source-only fallback 收口,像素处理不运行;图标后处理成功后再执行原有自动拆分,拆分告警继续使用现有 `sliceWarning` 语义。
|
||||
|
||||
### 角色动作帧抠图像素边界
|
||||
|
||||
- 图片画布角色动作的 FFmpeg 抽帧在上传 OSS 前转为 RGB8,并按最终帧宽高 contain 到内容尺寸;抠图前不创建最终尺寸画布、不引入 Alpha 通道、不增加 padding。同一个无补边 object key 供 `BgFilter → 阿里云通用抠图 → 本地键色` 三段链路使用。抠图完成后才转为最终目标尺寸 RGBA8,并以 `RGBA(0,0,0,0)` 居中补边。`560×752 → 323×480` 的验收样例中,抠图输入为 `323×434 RGB8 PNG`,最终输出为上下各 `23px` 透明补边的 `323×480 RGBA8 PNG`。该规则只作用于图片画布角色动作输入准备,不改变旧动作发布、采样、BgFilter 请求或 OSS 流程。
|
||||
@@ -95,6 +107,7 @@
|
||||
- `POST /api/editor/images/generations`:按提示词调用 VectorEngine 生成图片。带 `model / aspectRatio / imageSize` 的用户生成以统一业务像素矩阵创建前端占位和最终画布资源,例如两种图片模型的 `2K·16:9` 都交付 `2048x1152`;不得先请求固定 1K 再放大为 2K。`gpt-image-2` 在 provider 边界使用其接口支持的对齐请求尺寸,该尺寸不是业务交付尺寸;`nanobanana2` 仍把比例和清晰度档位写入 `generateContent`。provider 回图大于业务目标且比例偏差在允许范围内时,在内存中缩小并轻微裁切到业务尺寸后只上传最终结果。任意一边小于业务目标或比例偏差过大时禁止放大或大幅裁切,只上传 provider 实际回图,以实际尺寸写入结果并通过通用 `warning` 提示用户。主结果只写一次 OSS 且不额外创建“原始输出”。角色生成可携带 `model`、`screenColor`、`segModel`、`aspectRatio`、`imageSize` 和 `referenceImageSrcs`;父流程先按 provider 原始分辨率保存带纯色背景源图,随后只以 object key 向唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC;子 worker 在每次真实 provider attempt 前签发短期 OSS URL,并向 BgFilter 传入 `screen_color=<screenColor>`、`seg_model=<segModel>`。父流程不直连 BgFilter、不签发该 URL,也不重试已被 worker 接收的内部 RPC(连接从未建立时按调度方案 §5.1 有界重连);透明处理成功时只重采样透明图的 alpha 蒙版并应用回 provider 原图 RGB,最后把透明主结果归一到统一业务像素;最终失败时按前述多产物降级规则以原图主结果和通用 `warning` 收口。图标图集和 UI 图集的透明处理正常成功但返回尺寸与 provider 原图不同时,同样只重采样 alpha 蒙版并应用回 provider 原图,不放大低分辨率后处理成品。宣发素材携带 `kind: "publication-material"` 时固定归一为 `gpt-image-2`,不支持 `nanobanana2`,并继续按固定交付像素处理。从既有图层重新打开生成器且没有仍存活的对话框快照时,前端按该图层真实 `originalWidth / originalHeight` 恢复比例和清晰度,不得回落到新建面板的 1K 默认值。普通重绘继续走该接口并把当前图层图片作为参考图;图片快速编辑不走该接口。请求可携带 `projectId`、`assetFolderId`、`assetKind`、`generationInputs` 和 `sourceResourceId`,后端生成完成后在响应中返回实际产物的 project / resource / asset 快照。
|
||||
- `POST /api/editor/images/background-removals`:接收当前图片的 `objectKey`、`resourceId` 或 `assetId` 候选引用,登录态和稳定引用入口校验通过后创建外部生成任务,响应只返回 `queueState`。父 `external-generation-worker` 负责把候选引用解析为已登记、已校验当前账号归属的私有 OSS object key,只向唯一 `bgfilter-worker` 发起一次内部 HTTP RPC,传递 object key、`maxQueueWaitMs`、公式化 `callBudgetMs` 以及固定的 `background_mode=complex + seg_model=birefnet + cross_check=off`;父侧不下载原图、不签发 URL,也不发送 `file` 或 `screen_color`。子 worker 在每次真实 provider attempt 前签发 600 秒 OSS URL,以默认 `Q=2048` admission 保险丝和 provider 并发 `N=16` 限流,取得 provider permit 后才启动 `callBudgetMs`,并对同一次逻辑调用最多执行两次顺序 provider attempt;成功图片以内部 HTTP 二进制 body 返回父流程,父侧不重试已被 worker 接收的内部 RPC(连接从未建立时按调度方案 §5.1 有界重连)。complex 任意最终失败都直接使父任务失败,不进入阿里云或本地键色 fallback。请求可携带 `projectId`、`targetLayerId`、`assetFolderId`、`assetLabel`、`sourceResourceId` 和 `canvasCompletion`;成功后仍由父流程完成最终 OSS / project resource 持久化,有 `canvasCompletion` 时按生成占位写入结果图层,否则沿用旧的目标图层替换路径。provider 令牌只在子 worker 服务端通过 `GENARRATIVE_EDITOR_BGFILTER_TOKEN` 注入,未配置时兼容回退旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN`;父子内部调用另使用独立内部 Token。
|
||||
- `POST /api/editor/icon-spritesheets/generations`:按图标规范图和素材描述数组生成 spritesheet;api-server 先保存带纯色背景 spritesheet 源图,透明处理成功后再保存透明 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`。透明处理最终失败时只保存并返回原图主结果,不生成透明图或切片;透明图成功但拆分失败时保留整张透明图并返回 `sliceWarning`。响应只返回实际产物对应的 project / resource / asset 快照及可选通用 `warning`。
|
||||
- `POST /api/editor/images/generations` 与 `POST /api/editor/icon-spritesheets/generations` 还可携带可选 `style`;公开合法字符串为 `none / pixelArt`,兼容归一化、支持的 `kind`、非阻断告警和零新增持久化规则以“静态图片风格与像素规整边界”为准。`POST /api/editor/ui-designs/assets/extractions` 不接受该字段。
|
||||
- `POST /api/editor/ui-designs/assets/extractions`:前端把红色框选轮廓绘入本地临时图后,先将该图上传 OSS 并确认 asset object,再以返回的 `objectKey` 作为参考图入队;Data URL / Blob URL 只允许停留在上传前的浏览器临时态。接口固定 `gpt-image-2` 和自动决策纯色背景素材提取提示词生成素材 spritesheet;api-server 先保存带纯色背景 spritesheet 源图,透明处理成功后再保存透明 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`。透明处理最终失败时只保存并返回原图主结果,不生成透明图或切片;透明图成功但拆分失败时保留整张透明图并返回 `sliceWarning`。请求可携带 `projectId`、`assetFolderId`、`generationInputs` 和 `spritesheetLabel`,响应只返回实际产物对应的 project / resource / asset 快照及可选通用 `warning`;前端按后端快照落画布,不补造缺失产物。
|
||||
- `POST /api/editor/images/edits`:按提示词和当前图片的已登记 `objectKey` / `resourceId` 修改图片,返回新的生成图片元数据;图片快速编辑当前只提交 `sourceImageSrc`,不提交隐藏的 `referenceImageSrcs`,并随用户当前选择提交 `model / aspectRatio / imageSize / size`。api-server 必须先归一模型再选择 VectorEngine 协议:`nanobanana2` 调用 `/v1beta/models/{model}:generateContent` 并把原图作为 `inline_data`、比例和清晰度写入 `generationConfig.imageConfig`;`gpt-image-2` 调用 `/v1/images/edits` multipart。gpt-image-2 路径在 provider 边界把目标尺寸和所有 multipart 参考图临时补齐到 16 的倍数;nanobanana2 路径保留 provider 的比例 / 清晰度请求,但两条路径回图后都以统一业务目标尺寸尝试归一。只允许缩小和轻微裁切;回图任意一边小于目标或比例偏差过大时保留 provider 实际回图及尺寸,并返回通用 `warning`,不得放大伪造所选档位。无论是否发生尺寸恢复都只创建一个 project resource / 账号素材,不显示重复“原始输出”。provider 对齐尺寸或原生 K 档像素不得泄漏到正常完成的最终响应、资源或图层 Resolution;变换失败降级时以实际 provider 原图尺寸为准。本地红框标记图必须先上传再提交 objectKey;请求携带 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`。
|
||||
@@ -120,6 +133,8 @@
|
||||
- 拖拽图片或生成占位框接近其它图片 / 生成占位框边缘、中心或等距分布位置时显示吸附线,并保存吸附后的最终布局。
|
||||
- 生成图片点击后显示画布内 `Image Generator` 占位框和跟随占位框的生成输入框,生成失败保留占位和输入状态,生成成功后在占位位置创建真实图层,并让输入框继续跟随该生成图。
|
||||
- 选择 `1K / 2K` 或切换比例后,占位框在待生成和生成中阶段都必须立即显示对应目标像素尺寸;从普通图片、角色、图标图集或 UI 设计图再次改造时同样适用,完成落图前后不得从默认 1K 框跳变为 2K 成品。
|
||||
- 普通图片、角色和图标面板显示 `像素艺术` 勾选项并正确提交 / 恢复 `style: "none" | "pixelArt"`;其它生成或编辑面板不显示该选项。旧 payload、未知字符串、不支持 `kind` 和非字符串输入分别按本方案约定的兼容或错误语义处理。
|
||||
- `pixelArt` 输出 Alpha 只包含 `0 / 255`,使用 nearest 恢复交付尺寸且不新增颜色软边;成功和后处理失败两条路径都不得比 `none` 增加 OSS PUT、项目资源、账号素材或画布 item,逻辑低分辨率图不得出现在 OSS 或响应资源快照中。
|
||||
- 生成中的占位图聚焦后支持键盘 `Delete` / `Backspace` 删除,不新增可见删除按钮;删除后对应异步回写必须按生成器 ID 判空并丢弃,不能把已删除素材重新落回画布。音乐 / 音频生成占位和已生成音频图层同样必须支持键盘删除。
|
||||
- 画布常用快捷键必须与右上角快捷键弹窗一致;新增快捷键时应同步更新 `ImageCanvasShortcutModel`、快捷键 hook 单测和本方案。输入框、文本域和 contenteditable 聚焦时不得触发画布编辑快捷键。
|
||||
- 撤销或恢复画布布局时不得覆盖同 ID 生成对象当前的任务生命周期、提示词、参考图和结果;上传持久化延迟回填内部资源 ID 不得把安全移动误判为素材替换。生成结果必须在加入画布前写入生成历史,自动适合视图不得覆盖这条栈顶记录。
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
日期:`2026-06-15`
|
||||
|
||||
更新时间:`2026-07-28`
|
||||
|
||||
## 背景
|
||||
|
||||
图片画布编辑器已有普通图片生成、生成规范、生成角色形象和角色动画入口。本次新增 `生成图标素材`,用于一次输入多条图标素材描述,生成一张纯色背景 spritesheet;后端去背景正常成功后,再尝试自动拆分为可独立编辑的素材。
|
||||
@@ -43,6 +45,7 @@
|
||||
- `model`:支持 `gemini-3.1-flash-image-preview`(UI 显示 `nanobanana2`)和 `gpt-image-2`,默认 `nanobanana2`。
|
||||
- `aspectRatio`:按 `x:y` 展示,选项跟随模型。
|
||||
- `imageSize`:按 `0.5K / 1K / 2K` 展示,选项跟随模型。
|
||||
- `style`:可选生成后处理风格;未勾选像素艺术时传 `"none"`,勾选时传 `"pixelArt"`。
|
||||
- `priceMudPoints`:按当前模型和尺寸从编辑器生成计费配置计算;`nanobanana2 1K` 为 `12`,`gpt-image-2 1K` 为 `3`、`gpt-image-2 2K` 为 `5`。前端只提交配置函数计算值,后端用 `editor_generation_config` 校验,不允许素材生成面板自行写死价格。
|
||||
- 模型与尺寸选项:
|
||||
- `nanobanana2`:比例 `1:1 / 4:3 / 3:2 / 2:3 / 9:16 / 16:9`;大小 `0.5K / 1K / 2K`。后端走 `/v1beta/models/{model}:generateContent`,把图标规范图作为 `inline_data`,并把 `aspectRatio` / `imageSize` 写入 `generationConfig.imageConfig`;`0.5K` 按 VectorEngine 文档传 `"512"`。
|
||||
@@ -57,11 +60,22 @@
|
||||
<素材描述按中文顿号拼接>
|
||||
```
|
||||
|
||||
## 像素风格后处理
|
||||
|
||||
- 图标素材面板增加紧凑的 `像素艺术` 勾选项。选择保存于现有生成器快照,并可随现有请求和队列 payload 传递;不写入用户可见 `generationInputs`、素材元数据或新建的持久化记录。
|
||||
- `style` 省略、为 `null`、空字符串或 `"none"` 时按内部 `None` 处理且不告警;`"pixelArt"` 启用像素规整。未知字符串按 `None` 继续生成,并通过既有通用 `warning` 返回 `unsupported-image-style`;非字符串 JSON 仍返回 `400`。
|
||||
- 像素规整位于 BgFilter 正常成功且现有 Alpha 蒙版已经回贴到 provider 原尺寸之后、透明 spritesheet 最终尺寸处理和上传之前。网格分析源使用已有的带纯色背景 provider 原图,RGBA 采样源使用 Alpha 已回贴的透明图;像素规整成功后才进入原有连通域自动拆分。
|
||||
- 首版固定参数为分析色数 `16`、Alpha 覆盖阈值 `0.375`、像素格尺寸自动检测、固定色板关闭、K-means 最大采样 `262144`。单格颜色按 `Σ(A × RGB) / ΣA` 进行 Alpha 加权;覆盖率 `Σ(A / 255) / N >= 0.375` 且 `ΣA > 0` 时输出硬 Alpha `255`,否则输出严格 `[0,0,0,0]`。分析色数不限制最终输出色数。
|
||||
- 像素规整 CPU 工作使用进程级最大并发 `2`;取得并发许可的排队时间与实际处理时间共享最多 `30` 秒预算,同时不得晚于当前请求 deadline,最终以两者中更早者为准。输入图片任一边不得超过 `10000` 像素,总像素不得超过 `8294400`;超限、排队超时或处理超时均保留 Alpha 已回贴的透明图并走非致命降级,随后仍可进入原有自动拆分。
|
||||
- 逻辑低分辨率图只存在内存,并以 nearest 恢复到图集原有交付尺寸;像素模式不再经过 Lanczos。实现应复用 Alpha 回贴阶段读取的 provider 原图;必要时最多增加一次读取已有 provider 对象的 OSS GET,不得增加 OSS PUT。
|
||||
- 开启或关闭像素风格都保持现有 provider 原图、透明图集和实际成功切片的持久化与画布数量不变。禁止上传逻辑低分辨率图、像素化前后双份图集、预览或诊断图,也不新增 asset kind、项目资源、画布 item、任务类型或数据库字段。
|
||||
- 本功能不修改 BgFilter `flat` 调用、`cross_check=off`、fallback、Alpha 回贴或默认关闭 despill 的现状。BgFilter 最终失败时沿用只保留 provider 原图且不拆分的既有收口,像素规整不运行;像素规整自身失败时保留已成功的透明图并继续上传和拆分,通过通用 `warning` 非致命提示,不退款。`sliceWarning` 继续只表达透明图成功后的自动拆分失败,可与风格归一化或像素规整产生的通用 `warning` 并存。
|
||||
|
||||
## 去背与保存
|
||||
|
||||
- 父流程收到 spritesheet 后先把带解析后纯色背景的源图写入私有 OSS,并在上传完成后释放原图缓冲;随后只持 object key,并仅向同机唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC,请求中的源图只以 object key 传递,并附带 BgFilter 参数、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和有界审计关联,父流程不签发 BgFilter URL、不直连 provider,也不重试已被 worker 接收的内部 RPC(连接从未建立时按调度方案 §5.1 有界重连)。子 worker 在 `Q` admission 和 `Semaphore(N)` 约束下执行这次逻辑调用;排队只消耗 `maxQueueWaitMs`,取得 provider permit 后才启动 `callBudgetMs`。每次 provider attempt 前重新签发 600 秒 GET URL,multipart 固定传 `image_url`、`screen_color=<screenColor>`、`seg_model=<segModel>`、`background_mode=flat` 和 `cross_check=off`,不包含 `file`,并在调用预算内最多执行两次顺序 attempt。前端用户路径固定提交 `screenColor=auto` 与默认 `segModel=birefnet`,后端仍识别内部保留的 `anime-seg`,但这些内部参数不对用户可见。成功时,子 worker 通过内部 HTTP 二进制 body 把经过校验的图片字节直接返回父流程,不持久化中间结果;BgFilter 最终失败且父业务预算仍有效时,由父流程进入“阿里云通用抠图(按签名 URL 单独下载)→ 本地键色(再按 object key 独立下载一次原图并在产出后释放)”降级链。
|
||||
- 透明背景处理正常成功时,父流程把带背景原图和去背后的透明 spritesheet 写入 OSS、项目资源和账号素材库,再按 alpha 连通域和素材描述顺序执行附加拆分;若 BgFilter 返回较小图集,只把 alpha 蒙版重采样到 provider 原图尺寸并应用回原始高分辨率 RGB,不放大低分辨率后处理成品。画布完成快照同时写入透明主图与右侧 provider 原图(二者均已登记为 project resource / 账号素材),`generatedLayerId` 仍锚定透明主图;成功拆出的切片从 provider 原图右侧继续排列。调用方未指定素材文件夹时统一落默认“项目”文件夹。每个成功切片单独写入 OSS、项目资源和账号素材库,`sourceResourceId` 指向透明图集资源。BgFilter 与父侧 fallback 最终均失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建透明图集,也不继续拆分,`iconImageSrcs=[]`。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。最终透明结果及切片的 OSS / 资源 / 画布持久化仍全部由父流程负责。
|
||||
- 自动拆分只在透明图集成功后执行,属于 best-effort 附加动作,不参与图集生成的成功判定。连通域识别或切片持久化失败时,接口仍返回并回填整张透明图集,`iconImageSrcs=[]`,并通过 `sliceWarning.code/reason` 暴露非阻断原因;`sliceWarning` 与透明背景最终失败使用的通用 `warning` 互斥,前者只表示透明图集成功但自动拆分失败,`sliceWarning.reason` 原始契约保持不变。前端在 inline、worker 队列完成和刷新恢复三条路径统一显示对应 warning toast,用户可在图集工具栏手动重试。
|
||||
- 自动拆分只在透明图集成功后执行,属于 best-effort 附加动作,不参与图集生成的成功判定。连通域识别或切片持久化失败时,接口仍返回并回填整张透明图集,`iconImageSrcs=[]`,并通过 `sliceWarning.code/reason` 暴露非阻断原因;`sliceWarning` 与透明背景最终失败使用的通用 `warning` 互斥,因为透明背景失败时不会进入拆分,但可与风格归一化或像素规整产生的通用 `warning` 并存。前者只表示透明图集成功但自动拆分失败,`sliceWarning.reason` 原始契约保持不变。前端在 inline、worker 队列完成和刷新恢复三条路径统一显示对应 warning toast,用户可在图集工具栏手动重试。
|
||||
- 响应通过 `iconImageSrcs` 返回成功切片素材;自动生成使用用户输入的素材描述命名,UI 设计提取和手动拆分按从上到下、从左到右自动命名为 `素材 N`。
|
||||
- 手动拆分调用 `POST /api/editor/icon-spritesheets/slices`,只允许读取当前用户项目中的 `icon-spritesheet` 资源,不调用图片生成 provider,不扣除泥点。输入限制为单边最多 `4096` 像素、总像素最多 `2048×2048`,单次最多持久化 `64` 个切片;超限在任何切片写入前拒绝。
|
||||
|
||||
@@ -78,6 +92,7 @@
|
||||
- 默认 6 个素材描述会进入 prompt;用户在单个文本框中继续输入时最多解析 100 个素材描述。
|
||||
- 默认打开图标素材面板时选中 `nanobanana2 / 1:1 / 1K`;模型切换后,角色和图标素材面板之间沿用上次选择的模型。
|
||||
- 图标素材生成请求必须带 `model`、`aspectRatio` 和 `imageSize`;`nanobanana2` 请求体必须包含 `generationConfig.imageConfig.aspectRatio/imageSize`,`gpt-image-2` 请求必须包含文档映射后的 `size`。
|
||||
- 图标素材面板可选择 `style: "none" | "pixelArt"`;`none` 完整保持原处理路径,`pixelArt` 在 Alpha 回贴后、自动拆分前执行内存像素规整,最终 OSS PUT、项目资源、图集画布项和切片画布项数量不得因此增加。
|
||||
- 图标素材生成可以上传普通参考图;提交时图标规范图仍走 `referenceImageSrc`,普通参考图走 `referenceImageSrcs`,二者都必须是稳定引用(`objectKey` / 项目资源 ID / 素材 ID),禁止 Data URL / Blob URL,并写入 `generationInputs.references`。
|
||||
- 透明背景处理和自动拆分都成功后,画布同时出现透明 spritesheet 主图、其右侧的 provider 原图,以及从原图右侧铺开的按描述命名的独立图标图层;透明图集成功但拆分失败时仍出现透明主图与右侧原图,透明背景处理最终失败时只出现 provider 原图。
|
||||
- 选中透明图集图层时显示 `拆分图集`;点击后源图集显示扫描蒙层与 `拆图中` 状态,工具栏按钮同步切换为旋转图标和 `拆图中` 并禁用重复提交。完成后恢复工具栏,不新增第二张图集,只在 provider 原图右侧追加自动识别的独立素材,并同步写入素材库。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
日期:`2026-06-15`
|
||||
|
||||
更新时间:`2026-07-21`
|
||||
更新时间:`2026-07-28`
|
||||
|
||||
## 背景
|
||||
|
||||
@@ -60,6 +60,18 @@
|
||||
- `nanobanana2`:比例 `1:1 / 4:3 / 3:2 / 2:3 / 9:16 / 16:9`;大小 `0.5K / 1K / 2K`。后端走 `/v1beta/models/{model}:generateContent`,把比例写入 `generationConfig.imageConfig.aspectRatio`,把大小写入 `generationConfig.imageConfig.imageSize`;其中 `0.5K` 按文档传 `"512"`。
|
||||
- `gpt-image-2`:比例 `1:1 / 4:3 / 3:2 / 2:3 / 9:16 / 16:9`;大小 `1K / 2K`。后端走 `/v1/images/generations` 或 `/v1/images/edits`。K 档按最长边计算,并转换为 provider 可直接生成的合法像素:`1K` 的 `1:1 / 4:3 / 3:2 / 2:3 / 9:16 / 16:9` 分别为 `1024x1024 / 1024x768 / 1024x688 / 688x1024 / 608x1088 / 1088x608`;`2K` 分别为 `2048x2048 / 2048x1536 / 2048x1376 / 1376x2048 / 1152x2048 / 2048x1152`。其中 9:16 的 1K 尺寸按 provider 最小总像素和 16 对齐约束修正。禁止把 2K 竖图回落为 1K 请求,也禁止在回图后放大伪造所选 K 档。
|
||||
- 后端如果收到参考图,`nanobanana2` 把参考图作为 `inline_data` 传入原生 `generateContent`;`gpt-image-2` 走带多参考图的图片编辑链路。没有参考图时按所选模型走纯文本生成链路。
|
||||
|
||||
## 风格与像素规整
|
||||
|
||||
- 角色面板增加紧凑的 `像素艺术` 勾选项,请求使用可选字符串字段 `style`:未勾选传 `"none"`,勾选传 `"pixelArt"`。该选择可以随现有生成器快照和队列 payload 保存,但不写入用户可见 `generationInputs`、素材元数据或新建的持久化记录。
|
||||
- `style` 省略、为 `null`、空字符串或 `"none"` 时按内部 `None` 处理且不告警;`"pixelArt"` 在 `kind="character"` 时启用像素规整。未知字符串按 `None` 继续生成,并通过既有通用 `warning` 返回 `unsupported-image-style`;非字符串 JSON 仍返回 `400`。同一图片生成请求 DTO 被其它 `kind` 复用时,只有普通图片和 `character` 支持 `"pixelArt"`,其它 `kind` 收到该值也按不支持风格降级。
|
||||
- 像素规整位于 BgFilter 正常成功且现有 Alpha 蒙版已经回贴到 provider 原尺寸之后、最终尺寸处理和透明主图上传之前。网格分析源使用已有的带纯色背景 provider 原图,RGBA 采样源使用 Alpha 已回贴的透明图;软 Alpha 只参与单格覆盖率和 Alpha 加权 RGB 计算,输出 Alpha 硬化为 `0 / 255`。
|
||||
- 首版参数固定为分析色数 `16`、Alpha 覆盖阈值 `0.375`、像素格尺寸自动检测、固定色板关闭、K-means 最大采样 `262144`。单格覆盖率 `Σ(A / 255) / N >= 0.375` 且 `ΣA > 0` 时输出 `A=255`,颜色按 `Σ(A × RGB) / ΣA` 计算;否则输出 `[0,0,0,0]`。分析色数不限制最终输出色数。
|
||||
- 像素规整 CPU 工作使用进程级最大并发 `2`;取得并发许可的排队时间与实际处理时间共享最多 `30` 秒预算,同时不得晚于当前请求 deadline,最终以两者中更早者为准。输入图片任一边不得超过 `10000` 像素,总像素不得超过 `8294400`;超限、排队超时或处理超时均保留 Alpha 已回贴的透明图并走非致命降级。
|
||||
- 逻辑低分辨率图只存在内存,并以 nearest 恢复到角色任务原有交付尺寸,像素模式不再经过 Lanczos。实现应复用 Alpha 回贴阶段读取的 provider 原图;必要时最多增加一次读取已有 provider 对象的 OSS GET,不得增加 OSS PUT。
|
||||
- 开启或关闭像素风格都保持现有 provider 原图与透明主图两份产物、项目资源和画布图层数量不变。禁止上传逻辑低分辨率图、像素化前后双份主图、预览或诊断图,也不新增 asset kind、画布 item 或任务类型。
|
||||
- 本功能不修改 BgFilter `flat` 调用、`cross_check=on`、fallback、Alpha 回贴或默认关闭 despill 的现状。BgFilter 最终失败时沿用只保留 provider 原图的既有收口且不运行像素规整;像素规整自身失败时保留已成功的透明图并继续原有持久化,通过通用 `warning` 非致命提示,不退款。
|
||||
|
||||
- `kind = "character"` 时,后端不直接把前端文本当完整生图提示词,而是把文本作为 `角色设定` 填入固定提示词骨架:
|
||||
|
||||
```text
|
||||
@@ -106,6 +118,7 @@
|
||||
- `从画布中选择` 后点击已有画布图片可绑定为角色规范,`Esc` 可退出点选状态。
|
||||
- 上传常规参考图后缩略图右下角显示序号。
|
||||
- 输入角色设定并生成时,请求包含 `kind: "character"`、角色设定 prompt、参考图数组、`model`、`screenColor`、`aspectRatio` 和 `imageSize`。
|
||||
- 角色面板可选择 `style: "none" | "pixelArt"`;`none` 的处理路径和产物保持不变,`pixelArt` 在 Alpha 回贴后执行内存像素规整,最终 OSS PUT、项目资源和画布图层数量不得增加。
|
||||
- 默认打开角色生成面板时选中 `nanobanana2 / 1:1 / 1K`;切换到 `gpt-image-2` 后再次打开角色或图标素材面板应沿用该模型。
|
||||
- 生成成功后在占位图位置创建 `assetKind: "character"` 图层,右上角显示 `角色` 标签,布局保存包含该字段。
|
||||
|
||||
|
||||
@@ -1912,6 +1912,83 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn editor_pixel_art_style_wrong_types_return_bad_request() {
|
||||
let state = AppState::new(AppConfig {
|
||||
external_generation_mode: ExternalGenerationMode::Queue,
|
||||
..AppConfig::default()
|
||||
})
|
||||
.expect("state should build");
|
||||
let seed_user = seed_phone_user_with_password(&state, "13800138228", TEST_PASSWORD).await;
|
||||
let token = sign_test_user_token(&state, &seed_user, "sess_editor_pixel_style_body");
|
||||
let app = build_router(state);
|
||||
let requests = [
|
||||
(
|
||||
"/api/editor/images/generations",
|
||||
serde_json::json!({
|
||||
"prompt": "生成像素图片",
|
||||
"style": {"unexpected": true},
|
||||
}),
|
||||
),
|
||||
(
|
||||
"/api/editor/icon-spritesheets/generations",
|
||||
serde_json::json!({
|
||||
"referenceImageSrc": "/generated-images/editor/icon-spec.png",
|
||||
"iconDescriptions": ["宝箱"],
|
||||
"style": ["pixelArt"],
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (path, request_body) in requests {
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri(path)
|
||||
.header("authorization", format!("Bearer {token}"))
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(request_body.to_string()))
|
||||
.expect("request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(
|
||||
response.status(),
|
||||
StatusCode::BAD_REQUEST,
|
||||
"{path} should normalize JSON data errors to 400"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn editor_generation_json_validation_preserves_unsupported_media_type() {
|
||||
let state = AppState::new(AppConfig {
|
||||
external_generation_mode: ExternalGenerationMode::Queue,
|
||||
..AppConfig::default()
|
||||
})
|
||||
.expect("state should build");
|
||||
let seed_user = seed_phone_user_with_password(&state, "13800138229", TEST_PASSWORD).await;
|
||||
let token = sign_test_user_token(&state, &seed_user, "sess_editor_pixel_style_media_type");
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/editor/images/generations")
|
||||
.header("authorization", format!("Bearer {token}"))
|
||||
.body(Body::from(r#"{"prompt":"生成图片","style":"pixelArt"}"#))
|
||||
.expect("request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn editor_image_edit_rejects_inline_data_url_before_queueing() {
|
||||
let state = AppState::new(AppConfig {
|
||||
|
||||
@@ -330,6 +330,7 @@ impl EditorAgentTool for GenerateImageTool {
|
||||
prompt: args.prompt,
|
||||
size: None,
|
||||
kind: None,
|
||||
style: None,
|
||||
model: Some(args.model),
|
||||
screen_color: None,
|
||||
seg_model: None,
|
||||
@@ -437,6 +438,7 @@ impl EditorAgentTool for GenerateCharacterTool {
|
||||
prompt: args.prompt,
|
||||
size: None,
|
||||
kind: Some("character".to_string()),
|
||||
style: None,
|
||||
model: Some(args.model),
|
||||
screen_color: Some("auto".to_string()),
|
||||
seg_model: Some("birefnet".to_string()),
|
||||
@@ -546,6 +548,7 @@ impl EditorAgentTool for GenerateUiDesignTool {
|
||||
prompt: args.prompt,
|
||||
size: None,
|
||||
kind: Some("ui-design".to_string()),
|
||||
style: None,
|
||||
model: Some(args.model),
|
||||
screen_color: None,
|
||||
seg_model: None,
|
||||
@@ -799,6 +802,7 @@ impl EditorAgentTool for GenerateIconSpritesheetTool {
|
||||
reference_image_src,
|
||||
reference_image_srcs: Some(reference_image_srcs),
|
||||
icon_descriptions: args.icon_descriptions,
|
||||
style: None,
|
||||
model: Some(args.model),
|
||||
screen_color: Some("auto".to_string()),
|
||||
seg_model: Some("birefnet".to_string()),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,8 +32,8 @@ use crate::{
|
||||
editor_project_resource_payload_from_record, extract_editor_ui_design_assets_for_owner,
|
||||
generate_editor_icon_spritesheet_for_owner, generate_editor_image_for_owner,
|
||||
map_editor_project_error, normalize_editor_persisted_media_src, normalize_optional_string,
|
||||
save_editor_project_layout_with_revision_and_get, serialize_editor_asset_metadata,
|
||||
serialize_editor_layers,
|
||||
parse_editor_generation_json_payload, save_editor_project_layout_with_revision_and_get,
|
||||
serialize_editor_asset_metadata, serialize_editor_layers,
|
||||
},
|
||||
external_api_auth::ExternalApiPrincipal,
|
||||
http_error::AppError,
|
||||
@@ -623,8 +623,9 @@ pub async fn generate_external_editor_image(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(principal): Extension<ExternalApiPrincipal>,
|
||||
Json(payload): Json<EditorImageGenerationRequest>,
|
||||
payload: Result<Json<EditorImageGenerationRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let Json(payload) = parse_editor_generation_json_payload(payload)?;
|
||||
require_scope(&principal, SCOPE_EDITOR_IMAGE_GENERATE)?;
|
||||
generate_editor_image_for_owner(
|
||||
&state,
|
||||
@@ -655,8 +656,9 @@ pub async fn generate_external_editor_icon_spritesheet(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(principal): Extension<ExternalApiPrincipal>,
|
||||
Json(payload): Json<EditorIconSpritesheetGenerationRequest>,
|
||||
payload: Result<Json<EditorIconSpritesheetGenerationRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let Json(payload) = parse_editor_generation_json_payload(payload)?;
|
||||
require_scope(&principal, SCOPE_EDITOR_IMAGE_GENERATE)?;
|
||||
generate_editor_icon_spritesheet_for_owner(
|
||||
&state,
|
||||
@@ -868,6 +870,16 @@ mod tests {
|
||||
.get("priceMudPoints")
|
||||
.is_none()
|
||||
);
|
||||
let image_style_schema =
|
||||
&parsed["components"]["schemas"]["EditorImageGenerationRequest"]["properties"]["style"];
|
||||
assert_eq!(image_style_schema["anyOf"][0]["type"], "string");
|
||||
assert!(image_style_schema["anyOf"][0].get("enum").is_none());
|
||||
assert_eq!(image_style_schema["examples"], json!(["none", "pixelArt"]));
|
||||
assert!(
|
||||
parsed["components"]["schemas"]["EditorImageGenerationRequest"]["properties"]["kind"]
|
||||
.get("default")
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(
|
||||
parsed["components"]["schemas"]["EditorProject"]["properties"]["layers"]["type"],
|
||||
"array"
|
||||
@@ -901,6 +913,11 @@ mod tests {
|
||||
.get("/api/external/v1/editor/icon-spritesheets/generations")
|
||||
.is_some()
|
||||
);
|
||||
let icon_style_schema = &parsed["components"]["schemas"]["EditorIconSpritesheetGenerationRequest"]
|
||||
["properties"]["style"];
|
||||
assert_eq!(icon_style_schema["anyOf"][0]["type"], "string");
|
||||
assert!(icon_style_schema["anyOf"][0].get("enum").is_none());
|
||||
assert_eq!(icon_style_schema["examples"], json!(["none", "pixelArt"]));
|
||||
assert_eq!(
|
||||
parsed["components"]["schemas"]["EditorIconSpritesheetGenerationResponse"]["properties"]
|
||||
["sliceWarning"]["anyOf"][0]["$ref"],
|
||||
@@ -920,6 +937,15 @@ mod tests {
|
||||
parsed["components"]["schemas"]["EditorGenerationWarning"]["required"],
|
||||
json!(["code", "reason"])
|
||||
);
|
||||
assert_eq!(
|
||||
parsed["components"]["schemas"]["EditorGenerationWarning"]["properties"]["code"]["enum"],
|
||||
json!([
|
||||
"postprocess-failed-source-preserved",
|
||||
"dimension-restore-fallback",
|
||||
"unsupported-image-style",
|
||||
"multiple-generation-warnings"
|
||||
])
|
||||
);
|
||||
assert!(
|
||||
parsed["paths"]
|
||||
.get("/api/external/v1/editor/ui-designs/assets/extractions")
|
||||
@@ -944,6 +970,7 @@ mod tests {
|
||||
.get("priceMudPoints")
|
||||
.is_none()
|
||||
);
|
||||
assert!(ui_extraction_schema["properties"].get("style").is_none());
|
||||
assert!(
|
||||
parsed["paths"]
|
||||
.get("/api/external/v1/editor/videos/generations")
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Hugo Duprez
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,7 +1,12 @@
|
||||
pub mod generated_asset_sheets;
|
||||
pub mod generated_assets;
|
||||
pub mod pixel_art_snapper;
|
||||
pub mod vector_engine;
|
||||
|
||||
pub use pixel_art_snapper::{
|
||||
PIXEL_ART_ALPHA_COVERAGE_THRESHOLD, PIXEL_ART_ANALYSIS_COLORS, PIXEL_ART_KMEANS_SAMPLE_LIMIT,
|
||||
PIXEL_ART_MAX_IMAGE_PIXELS, PixelArtSnapError, snap_pixel_art, snap_pixel_art_with_deadline,
|
||||
};
|
||||
pub use vector_engine::{
|
||||
DownloadedImage, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, GeneratedImages, NANOBANANA_2_MODEL,
|
||||
PlatformImageError, PlatformImageFailureAudit, PlatformImageStatusHint, ReferenceImage,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -836,6 +836,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
name: '生成图片模型 nanobanana2',
|
||||
}).className,
|
||||
).toContain('platform-inline-option-button');
|
||||
const pixelArtToggle = within(generateDialog).getByRole('checkbox', {
|
||||
name: '像素艺术',
|
||||
}) as HTMLInputElement;
|
||||
expect(pixelArtToggle.checked).toBe(false);
|
||||
expect(
|
||||
within(generateDialog).getByRole('button', { name: '生成' }).className,
|
||||
).toContain('platform-button');
|
||||
@@ -847,6 +851,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '一张明亮的拼图主视觉' },
|
||||
});
|
||||
fireEvent.click(pixelArtToggle);
|
||||
fireEvent.click(
|
||||
within(generateDialog).getByRole('button', { name: '生成' }),
|
||||
);
|
||||
@@ -857,6 +862,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect.objectContaining({
|
||||
prompt: '一张明亮的拼图主视觉',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
style: 'pixelArt',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
projectId: 'editor-project-default',
|
||||
@@ -865,6 +871,11 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
const submittedGenerationInputs =
|
||||
generateEditorImageMock.mock.calls[0]?.[0]?.generationInputs;
|
||||
expect(JSON.stringify(submittedGenerationInputs)).not.toContain(
|
||||
'pixelArt',
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByAltText(/画布图片:生成图片/)).toBeTruthy();
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createLayerFromAsset,
|
||||
DEFAULT_CANVAS_BACKGROUND_COLOR,
|
||||
formatCanvasDisplayScalePercent,
|
||||
hydrateCanvasGenerationDialog,
|
||||
hydrateLayer,
|
||||
normalizeAssetLibrary,
|
||||
normalizeCanvasBackgroundHex,
|
||||
@@ -631,6 +632,7 @@ describe('ImageCanvasEditorModel', () => {
|
||||
composerOpen: false,
|
||||
generatedLayerId: 'layer-generated',
|
||||
imageModel: 'gpt-image-2',
|
||||
style: 'pixelArt',
|
||||
generationStartedAt: 1_771_400_000_000,
|
||||
generationFinishedAt: 1_771_400_004_000,
|
||||
placeholder: {
|
||||
@@ -688,6 +690,7 @@ describe('ImageCanvasEditorModel', () => {
|
||||
status: 'generating',
|
||||
generatedLayerId: 'layer-generated',
|
||||
imageModel: 'gpt-image-2',
|
||||
style: 'pixelArt',
|
||||
generationStartedAt: 1_771_400_000_000,
|
||||
generationFinishedAt: 1_771_400_004_000,
|
||||
placeholder: {
|
||||
@@ -704,6 +707,35 @@ describe('ImageCanvasEditorModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults restored supported image styles to none and drops them from other modes', () => {
|
||||
expect(
|
||||
hydrateCanvasGenerationDialog({
|
||||
id: 'generation-dialog-legacy',
|
||||
mode: 'generate',
|
||||
prompt: '旧任务',
|
||||
status: 'idle',
|
||||
})?.style,
|
||||
).toBe('none');
|
||||
expect(
|
||||
hydrateCanvasGenerationDialog({
|
||||
id: 'generation-dialog-unknown-style',
|
||||
mode: 'character',
|
||||
prompt: '未知风格',
|
||||
status: 'idle',
|
||||
style: 'futureStyle',
|
||||
})?.style,
|
||||
).toBe('none');
|
||||
expect(
|
||||
hydrateCanvasGenerationDialog({
|
||||
id: 'generation-dialog-spec',
|
||||
mode: 'spec',
|
||||
prompt: '规范任务',
|
||||
status: 'idle',
|
||||
style: 'pixelArt',
|
||||
})?.style,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('drops restored generator references owned by another user', () => {
|
||||
const dialog: CanvasGenerationDialogState = {
|
||||
id: 'generation-dialog-owner',
|
||||
|
||||
@@ -537,6 +537,14 @@ export function hydrateCanvasGenerationDialog(
|
||||
if (!id || !isCanvasGenerationDialogMode(snapshot.mode)) {
|
||||
return null;
|
||||
}
|
||||
const style =
|
||||
snapshot.mode === 'generate' ||
|
||||
snapshot.mode === 'character' ||
|
||||
snapshot.mode === 'icon'
|
||||
? snapshot.style === 'pixelArt'
|
||||
? 'pixelArt'
|
||||
: 'none'
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -601,6 +609,7 @@ export function hydrateCanvasGenerationDialog(
|
||||
currentUserId,
|
||||
),
|
||||
imageModel: stringOrUndefined(snapshot.imageModel),
|
||||
style,
|
||||
videoModel:
|
||||
typeof snapshot.videoModel === 'string'
|
||||
? snapshot.videoModel
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
EditorCharacterAnimationGenerationResult,
|
||||
EditorCharacterAnimationRatio,
|
||||
EditorCharacterAnimationResolution,
|
||||
EditorImageGenerationStyle,
|
||||
EditorVideoAspectRatio,
|
||||
EditorVideoModel,
|
||||
EditorVideoResolution,
|
||||
@@ -215,6 +216,7 @@ export type GenerateDialogState = {
|
||||
publicationReferences?: CharacterReferenceImage[];
|
||||
uiDesignSpecReference?: CharacterReferenceImage | null;
|
||||
imageModel?: string;
|
||||
style?: EditorImageGenerationStyle;
|
||||
videoModel?: EditorVideoModel;
|
||||
videoAspectRatio?: EditorVideoAspectRatio;
|
||||
videoResolution?: EditorVideoResolution;
|
||||
|
||||
@@ -69,6 +69,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
mode: 'generate',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
style: 'none',
|
||||
placeholder: {
|
||||
x: -312,
|
||||
y: -332,
|
||||
@@ -156,6 +157,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: 'character',
|
||||
style: 'none',
|
||||
imageModel: 'gpt-image-2',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
@@ -176,6 +178,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: 'icon',
|
||||
style: 'none',
|
||||
imageModel: 'unknown-model',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
@@ -609,6 +612,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
imageModel: 'gpt-image-2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
style: 'pixelArt',
|
||||
},
|
||||
mode: 'redraw',
|
||||
}),
|
||||
@@ -619,6 +623,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
imageModel: 'gpt-image-2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
style: 'none',
|
||||
placeholder: {
|
||||
x: 472,
|
||||
y: 140,
|
||||
|
||||
@@ -196,6 +196,7 @@ export function createGenerateDialogDraft({
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
style: 'none',
|
||||
imageModel: DEFAULT_IMAGE_MODEL,
|
||||
aspectRatio: dimensionDefaults.aspectRatio,
|
||||
imageSize: dimensionDefaults.imageSize,
|
||||
@@ -269,6 +270,7 @@ export function createCharacterGenerationDialogDraft({
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
style: 'none',
|
||||
characterSpecReference: null,
|
||||
characterReferences: [],
|
||||
imageModel: normalizedImageModel,
|
||||
@@ -307,6 +309,7 @@ export function createIconGenerationDialogDraft({
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
style: 'none',
|
||||
iconSpecReference: null,
|
||||
generationReferences: [],
|
||||
iconDescriptions: [],
|
||||
|
||||
@@ -47,6 +47,7 @@ function ImageOptionsHarness({
|
||||
<output aria-label="当前模型">{dialog.imageModel}</output>
|
||||
<output aria-label="当前比例">{dialog.aspectRatio}</output>
|
||||
<output aria-label="当前尺寸">{dialog.imageSize}</output>
|
||||
<output aria-label="当前风格">{dialog.style ?? '-'}</output>
|
||||
<output aria-label="当前状态">{dialog.status}</output>
|
||||
<output aria-label="当前错误">{dialog.errorMessage ?? '-'}</output>
|
||||
<output aria-label="当前占位">
|
||||
@@ -66,6 +67,67 @@ function ImageOptionsHarness({
|
||||
}
|
||||
|
||||
describe('ImageCanvasGenerationImageOptionsView', () => {
|
||||
it.each(['generate', 'character', 'icon'] as const)(
|
||||
'shows and updates the pixel art style for %s generation',
|
||||
(mode) => {
|
||||
render(
|
||||
<ImageOptionsHarness
|
||||
initialDialog={{
|
||||
mode,
|
||||
prompt: '',
|
||||
status: 'failed',
|
||||
errorMessage: '旧错误',
|
||||
style: 'none',
|
||||
imageModel: IMAGE_MODEL_NANOBANANA2,
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const toggle = screen.getByRole('checkbox', {
|
||||
name: '像素艺术',
|
||||
}) as HTMLInputElement;
|
||||
const modelButton = screen.getByRole('button', {
|
||||
name: '生成图片模型 nanobanana2',
|
||||
});
|
||||
expect(toggle.checked).toBe(false);
|
||||
expect(
|
||||
toggle.compareDocumentPosition(modelButton) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).not.toBe(0);
|
||||
|
||||
fireEvent.click(toggle);
|
||||
|
||||
expect(toggle.checked).toBe(true);
|
||||
expect(screen.getByLabelText('当前风格').textContent).toBe('pixelArt');
|
||||
expect(screen.getByLabelText('当前状态').textContent).toBe('idle');
|
||||
expect(screen.getByLabelText('当前错误').textContent).toBe('-');
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['spec', 'quick-edit', 'ui-design', 'publication'] as const)(
|
||||
'does not show the pixel art style for %s generation',
|
||||
(mode) => {
|
||||
render(
|
||||
<ImageOptionsHarness
|
||||
initialDialog={{
|
||||
mode,
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
imageModel: IMAGE_MODEL_NANOBANANA2,
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByRole('checkbox', { name: '像素艺术' }),
|
||||
).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it('updates dimensions from a menu, keeps the menu open and marks the selection', () => {
|
||||
render(
|
||||
<ImageOptionsHarness
|
||||
|
||||
@@ -137,6 +137,10 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
const dimensionsButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const modelButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const isQuickEdit = dialog.mode === 'quick-edit';
|
||||
const supportsImageStyle =
|
||||
dialog.mode === 'generate' ||
|
||||
dialog.mode === 'character' ||
|
||||
dialog.mode === 'icon';
|
||||
const normalizedLockedModel = lockedModel
|
||||
? normalizeEditorImageModel(lockedModel)
|
||||
: null;
|
||||
@@ -306,6 +310,21 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
{supportsImageStyle ? (
|
||||
<label className="image-canvas-editor__image-style-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={dialog.style === 'pixelArt'}
|
||||
disabled={isGenerating}
|
||||
onChange={(event) =>
|
||||
updateDialog({
|
||||
style: event.target.checked ? 'pixelArt' : 'none',
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span>像素艺术</span>
|
||||
</label>
|
||||
) : null}
|
||||
{includeModel ? (
|
||||
<div
|
||||
className={[
|
||||
|
||||
@@ -47,6 +47,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
input: {
|
||||
prompt: '一张发光主视觉',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
style: 'none',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
},
|
||||
@@ -193,6 +194,24 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send image style from an incomplete quick-edit snapshot', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
mode: 'quick-edit',
|
||||
prompt: '把当前图改成雨天',
|
||||
status: 'idle',
|
||||
style: 'pixelArt',
|
||||
},
|
||||
layers: [],
|
||||
nextGeneratedIndex: 4,
|
||||
});
|
||||
|
||||
expect(plan.kind).toBe('image');
|
||||
expect(plan.kind === 'image' ? plan.input : null).not.toHaveProperty(
|
||||
'style',
|
||||
);
|
||||
});
|
||||
|
||||
it('builds spec generation plans with reference prompt semantics', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
@@ -294,6 +313,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
prompt: ' 白发骑士 ',
|
||||
status: 'idle',
|
||||
imageModel: 'gpt-image-2',
|
||||
style: 'pixelArt',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
characterSpecReference: {
|
||||
@@ -324,6 +344,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
model: 'gpt-image-2',
|
||||
screenColor: 'auto',
|
||||
segModel: 'birefnet',
|
||||
style: 'pixelArt',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
referenceImageSrcs: [
|
||||
@@ -432,6 +453,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
mode: 'publication',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
style: 'pixelArt',
|
||||
publicationGameInfo: {
|
||||
gameName: ' 重庆洪崖洞火锅 ',
|
||||
gameCategories: '抓大鹅、休闲、治愈、手绘风',
|
||||
@@ -665,6 +687,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
prompt: ' 返回按钮 \n\n设置按钮',
|
||||
status: 'idle',
|
||||
imageModel: 'gpt-image-2',
|
||||
style: 'pixelArt',
|
||||
aspectRatio: '3:2',
|
||||
imageSize: '2K',
|
||||
assetLabel: ' 冒险游戏图标 ',
|
||||
@@ -695,6 +718,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
model: 'gpt-image-2',
|
||||
screenColor: 'auto',
|
||||
segModel: 'birefnet',
|
||||
style: 'pixelArt',
|
||||
aspectRatio: '3:2',
|
||||
imageSize: '2K',
|
||||
},
|
||||
|
||||
@@ -354,6 +354,7 @@ export function buildImageGenerationSubmissionPlan({
|
||||
model: imageModel,
|
||||
screenColor,
|
||||
segModel,
|
||||
style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none',
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||
@@ -564,6 +565,9 @@ export function buildImageGenerationSubmissionPlan({
|
||||
input: {
|
||||
prompt: normalizedPrompt,
|
||||
model: imageModel,
|
||||
...(dialog.mode === 'generate'
|
||||
? { style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none' }
|
||||
: {}),
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
...(dialog.generationReferences?.length
|
||||
@@ -639,6 +643,7 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
model: rememberImageModel,
|
||||
screenColor,
|
||||
segModel,
|
||||
style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none',
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
},
|
||||
|
||||
@@ -7079,6 +7079,33 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
width: 8.5rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__image-style-toggle {
|
||||
grid-column: 3;
|
||||
display: inline-flex;
|
||||
min-height: 2.25rem;
|
||||
align-items: center;
|
||||
justify-self: end;
|
||||
gap: 0.38rem;
|
||||
padding: 0 0.5rem;
|
||||
color: #475569;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 760;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-canvas-editor__image-style-toggle input {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
margin: 0;
|
||||
accent-color: var(--image-canvas-brand-fill);
|
||||
}
|
||||
|
||||
.image-canvas-editor__image-style-toggle:has(input:disabled) {
|
||||
color: #94a3b8;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.image-canvas-editor__model-trigger-label {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
@@ -8904,6 +8931,7 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
|
||||
.image-canvas-editor__option-cluster--dimensions,
|
||||
.image-canvas-editor__option-cluster--model,
|
||||
.image-canvas-editor__image-style-toggle,
|
||||
.image-canvas-editor__readonly-generation-option,
|
||||
.image-canvas-editor__generation-submit,
|
||||
.image-canvas-editor__character-animation-submit {
|
||||
@@ -8913,6 +8941,10 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.image-canvas-editor__image-style-toggle {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-submit {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -838,6 +838,7 @@ describe('editorProjectClient', () => {
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#FFD6C2',
|
||||
segModel: 'anime-seg',
|
||||
style: 'pixelArt',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '1K',
|
||||
projectId: 'editor-project-1',
|
||||
@@ -862,6 +863,7 @@ describe('editorProjectClient', () => {
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#FFD6C2',
|
||||
segModel: 'anime-seg',
|
||||
style: 'pixelArt',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '1K',
|
||||
projectId: 'editor-project-1',
|
||||
@@ -1015,6 +1017,7 @@ describe('editorProjectClient', () => {
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
segModel: 'anime-seg',
|
||||
style: 'none',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '2K',
|
||||
projectId: 'editor-project-1',
|
||||
@@ -1036,6 +1039,7 @@ describe('editorProjectClient', () => {
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
segModel: 'anime-seg',
|
||||
style: 'none',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '2K',
|
||||
projectId: 'editor-project-1',
|
||||
|
||||
@@ -184,6 +184,8 @@ export type EditorAssetLibrarySnapshot = {
|
||||
assets: EditorAssetSnapshot[];
|
||||
};
|
||||
|
||||
export type EditorImageGenerationStyle = 'none' | 'pixelArt';
|
||||
|
||||
export type EditorImageGenerationInput = {
|
||||
prompt: string;
|
||||
size?: string;
|
||||
@@ -196,6 +198,7 @@ export type EditorImageGenerationInput = {
|
||||
model?: string;
|
||||
screenColor?: string;
|
||||
segModel?: string;
|
||||
style?: EditorImageGenerationStyle;
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
referenceImageSrcs?: string[];
|
||||
@@ -228,6 +231,7 @@ export type EditorIconSpritesheetGenerationInput = {
|
||||
model?: string;
|
||||
screenColor?: string;
|
||||
segModel?: string;
|
||||
style?: EditorImageGenerationStyle;
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
projectId?: string | null;
|
||||
@@ -920,6 +924,7 @@ export async function generateEditorImage(input: EditorImageGenerationInput) {
|
||||
...(input.model ? { model: input.model } : {}),
|
||||
...(input.screenColor ? { screenColor: input.screenColor } : {}),
|
||||
...(input.segModel ? { segModel: input.segModel } : {}),
|
||||
...(input.style ? { style: input.style } : {}),
|
||||
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
|
||||
...(input.imageSize ? { imageSize: input.imageSize } : {}),
|
||||
...(input.referenceImageSrcs?.length
|
||||
@@ -966,6 +971,7 @@ export async function generateEditorIconSpritesheet(
|
||||
model: input.model?.trim() || EDITOR_IMAGE_MODEL_NANOBANANA2,
|
||||
...(input.screenColor ? { screenColor: input.screenColor } : {}),
|
||||
...(input.segModel ? { segModel: input.segModel } : {}),
|
||||
...(input.style ? { style: input.style } : {}),
|
||||
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
|
||||
...(input.imageSize ? { imageSize: input.imageSize } : {}),
|
||||
...(input.projectId ? { projectId: input.projectId } : {}),
|
||||
|
||||
Reference in New Issue
Block a user