角色重绘复用角色提示词限定
角色图层重绘提交为角色生成类型 补充角色重绘请求契约测试 更新图片画布重绘链路文档
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
- 图片拖拽时显示水平 / 垂直吸附参考线,吸附到其它图层或画板的边缘与中心线。
|
||||
- 生成资源右上角显示元数据按钮,点击打开独立元数据窗口。图片信息页不展示后端组装后的生图 Prompt,也不提供复制 Prompt;只展示该图片生成时用户在面板里提交的输入快照,包括普通生成提示词、规范表单字段、角色设定、图标素材描述、快速编辑提示词、重绘提示词,以及角色规范 / 常规参考图 / 图标规范 / 编辑参考图等参考图卡片。旧数据或上传图片没有输入快照时显示 `-`,禁止回退展示内部 Prompt。
|
||||
- 对生成资源执行重绘时,在右侧创建新的生成结果图层,并自动调整视图显示原图和新图;重绘面板不因提交成功自动关闭,便于连续改提示词。
|
||||
- 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和重绘保留既有 `gpt-image-2` 路径;快速编辑按原图模型提交;其中生成规范类图片固定 `16:9`、`2K`、`gpt-image-2`,面板底部用与可编辑面板一致的比例 / 尺寸 / 模型胶囊按钮展示固定参数,但按钮为禁用态,不允许在该面板改比例、尺寸或模型。`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。纯文本生成走 `/api/editor/images/generations`,重绘在前端读入当前图层图片 Data URL 后作为 `quick-edit` 参考图走同一图片生成 BFF,并在原图右侧生成一张新图。`生成视频` 走 `/api/editor/videos/generations`,前端模型入口仅展示 Seedance 2.0 Fast / Seedance 2.0 / Kling 3.0 / Kling 3.0 Omni,不展示 Veo 入口,默认 Seedance 2.0 Fast;首期固定文字转视频、`16:9`、标准模式、静音,生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成图片后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。快速编辑和重绘在调用后端前必须把当前图层图片源读取为图片 Data URL,来源可以是本地上传 Data URL、站内 public 图片、历史 `/generated-*` 路径或可读取的 OSS generated URL;后端仍只接收图片 Data URL,不把普通 URL 直接透传到 VectorEngine edits。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。
|
||||
- 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和重绘保留既有 `gpt-image-2` 路径;快速编辑按原图模型提交;其中生成规范类图片固定 `16:9`、`2K`、`gpt-image-2`,面板底部用与可编辑面板一致的比例 / 尺寸 / 模型胶囊按钮展示固定参数,但按钮为禁用态,不允许在该面板改比例、尺寸或模型。`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。纯文本生成走 `/api/editor/images/generations`,重绘在前端读入当前图层图片 Data URL 后走同一图片生成 BFF,并在原图右侧生成一张新图;普通图层重绘作为 `quick-edit` 参考图提交,角色图层重绘必须按 `kind: "character"` 提交,继续套用角色生成器提示词限定、透明 PNG 后处理和角色资产持久化。`生成视频` 走 `/api/editor/videos/generations`,前端模型入口仅展示 Seedance 2.0 Fast / Seedance 2.0 / Kling 3.0 / Kling 3.0 Omni,不展示 Veo 入口,默认 Seedance 2.0 Fast;首期固定文字转视频、`16:9`、标准模式、静音,生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成图片后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。快速编辑和重绘在调用后端前必须把当前图层图片源读取为图片 Data URL,来源可以是本地上传 Data URL、站内 public 图片、历史 `/generated-*` 路径或可读取的 OSS generated URL;后端仍只接收图片 Data URL,不把普通 URL 直接透传到 VectorEngine edits。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。
|
||||
- 快速编辑面板对齐其它生成类面板:首行支持额外参考图,最多 8 张;原图自动作为最后一张参考图展示和提交。尺寸、模型沿用原图配置,只以禁用态参数胶囊展示,不可点击修改。提交前将提示词里对原图的 `原图`、`当前图片`、`当前图` 或 `图1` 引用改写为最后一张原图编号,例如额外 1 张参考图时写成 `图2`。点击快速编辑生成后立即创建独立 `Quick Edit Generator` 画布占位播放生成中动画,不再在原图图层上播放生成中遮罩;该生成中占位可通过键盘 `Delete` / `Backspace` 删除。
|
||||
- 底部生成类按钮每次点击都必须创建独立的画布生成对象;新建规范、角色形象或图标素材时,只切换当前编辑面板,不得销毁此前尚未生成或已生成后的其它生成对象状态。归档为非当前编辑对象的生成占位仍可拖动、删除和等待异步完成,完成 / 失败回写必须按生成对象 ID 读取最新占位状态,不能使用提交瞬间的旧快照。
|
||||
- 所有会新建画布生成占位的入口必须先创建 draft,再统一经过 `ImageCanvasGenerationPlacementModel` 计算落点,禁止各入口自行使用当前视口中心裸坐标。当前覆盖入口包括 `生成图片`、`生成规范`、`生成角色形象`、`生成图标素材`、`生成视频`、`生成UI设计图` 和 `生成角色动作`。placement 模型的避让对象为所有未隐藏画布图层,以及当前 active / inactive generation dialogs 中仍存在的 placeholder;每个避让矩形按 32px 画布世界坐标间距外扩。候选落点以当前视口世界中心为距离目标,优先选择离视口中心最近且不重叠的占位位置;若中心被占用,会按上下左右和环形候选继续寻找。打开生成面板时必须把避让后的 placeholder 写入 `openCanvasGenerationDialog(...)`,并立即调用 `centerViewportOnPlacement(...)` 居中到新占位中心,保持原 viewport scale 不变。
|
||||
|
||||
@@ -323,13 +323,15 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
'quick-edit:generating:closed:-:placeholder:-',
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
||||
prompt: '快速修图',
|
||||
size: '1024x768',
|
||||
kind: 'quick-edit',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
referenceImageSrcs: ['data:image/png;base64,source.png'],
|
||||
});
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '快速修图',
|
||||
size: '1024x768',
|
||||
kind: 'quick-edit',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
referenceImageSrcs: ['data:image/png;base64,source.png'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
@@ -368,6 +370,53 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('submits character redraws through the character image prompt contract', async () => {
|
||||
generateEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: '角色换成蓝色披风' }),
|
||||
);
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
assetKind: 'character',
|
||||
prompt: '原始角色设定',
|
||||
}),
|
||||
]}
|
||||
initialQuickEditPanel={{
|
||||
mode: 'redraw',
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: ' 角色换成蓝色披风 ',
|
||||
size: '1024x768',
|
||||
model: 'gpt-image-2',
|
||||
status: 'idle',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '角色换成蓝色披风',
|
||||
size: '1024x768',
|
||||
kind: 'character',
|
||||
model: 'gpt-image-2',
|
||||
referenceImageSrcs: ['data:image/png;base64,source.png'],
|
||||
assetKind: 'character',
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-quick-edit-1:源图 重绘:resource-source:character',
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('quick-edit').textContent).toBe(
|
||||
'layer-source:idle:角色换成蓝色披风:-',
|
||||
);
|
||||
});
|
||||
|
||||
it('submits edit dialogs beside the source and clears the modal state', async () => {
|
||||
editEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: '修改当前图片' }),
|
||||
@@ -390,10 +439,12 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
'edit:generating:open:-:-:-',
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith({
|
||||
prompt: '修改当前图片',
|
||||
sourceImageSrc: 'data:image/png;base64,source.png',
|
||||
});
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '修改当前图片',
|
||||
sourceImageSrc: 'data:image/png;base64,source.png',
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
@@ -492,13 +543,15 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
'publication:generating:closed:-:placeholder:-',
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
||||
prompt: expect.stringContaining('【宣发素材类型】详情页单图'),
|
||||
size: '720x1280',
|
||||
kind: 'publication-material',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
referenceImageSrcs: ['data:image/png;base64,large-ref'],
|
||||
});
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: expect.stringContaining('【宣发素材类型】详情页单图'),
|
||||
size: '720x1280',
|
||||
kind: 'publication-material',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
referenceImageSrcs: ['data:image/png;base64,large-ref'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
const prompt = generateEditorImageMock.mock.calls[0]?.[0]?.prompt ?? '';
|
||||
expect(prompt).toContain('游戏名:云朵消消乐');
|
||||
@@ -707,9 +760,11 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(extractEditorUiDesignAssetsMock).toHaveBeenCalledWith({
|
||||
sourceImageSrc: 'data:image/png;base64,ui-design.png',
|
||||
});
|
||||
expect(extractEditorUiDesignAssetsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceImageSrc: 'data:image/png;base64,ui-design.png',
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
|
||||
@@ -697,10 +697,12 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
quickEditSourceLayer,
|
||||
quickEditReferences,
|
||||
);
|
||||
const isCharacterRedraw =
|
||||
panelMode === 'redraw' && quickEditSourceLayer.assetKind === 'character';
|
||||
const generated = await generateEditorImage({
|
||||
prompt: normalizedPrompt,
|
||||
size: quickEditPanel.size,
|
||||
kind: 'quick-edit',
|
||||
kind: isCharacterRedraw ? 'character' : 'quick-edit',
|
||||
model: normalizeEditorImageModel(quickEditPanel.model),
|
||||
referenceImageSrcs: [
|
||||
...extraReferenceImageSrcs,
|
||||
|
||||
Reference in New Issue
Block a user