支持规范参考图输入
为角色形象规范、UI素材规范、自定义规范面板新增参考图上传入口。 生成规范时携带参考图并自动追加参考图生成规范语义。 补充生成流程和上传流程回归测试。 更新画板角色形象生成入口设计文档。
This commit is contained in:
@@ -50,6 +50,13 @@
|
||||
|
||||
- 角色图生成完成后,后端必须先对返回图片执行绿幕 / 近白背景去背,并统一输出透明背景 PNG;随后写入 OSS 私有对象,并确认 `asset_object`。接口回包仍返回透明 PNG Data URL 供画板立即显示,同时返回 `objectKey` / `assetObjectId`,前端创建图层和画板资源记录时必须保存这两个字段。
|
||||
|
||||
## 生成规范参考图
|
||||
|
||||
- `生成规范 -> 角色形象规范`、`UI素材规范`、`自定义规范` 的设定面板支持上传 1 张参考图;`图标素材规范` 继续使用后续图标素材生成面板里的专用规范图链路,不在这里重复新增入口。
|
||||
- 参考图入口只展示字段标题、缩略图或上传图标、文件名,不把参考规则说明铺在 UI 上。
|
||||
- 提交生成规范时,若存在参考图,前端必须把参考图作为 `referenceImageSrcs[0]` 提交到 `/api/editor/images/generations`,并在生图提示词开头自动追加“参考图生成规范”语义:要求模型参考图 1 的构图、风格、材质、色彩、形状语言和视觉层级生成规范图,但不要复制参考图中的文字、水印或无关背景。
|
||||
- 生成结果的信息快照必须记录该参考图,标题为 `参考图`,便于后续在图片信息面板回看生成输入。
|
||||
|
||||
## 可访问性与状态
|
||||
|
||||
- 点选状态下画布显示状态提示 `请选择画布中的图片作为角色形象规范,按 Esc 退出`。
|
||||
@@ -121,4 +128,3 @@
|
||||
- 每帧必须执行绿幕去背,输出透明背景 PNG。
|
||||
- 抽帧结果写入 OSS,并返回帧路径、帧尺寸、帧数、fps、预览视频路径、模型、价格和实际 prompt。
|
||||
- 画板前端首版只展示生成完成结果摘要,不把帧序列自动铺到画布上;后续若要展示逐帧图层,必须继续复用画布图层与素材库资源模型。
|
||||
|
||||
|
||||
@@ -621,6 +621,37 @@ export function ImageCanvasGenerationComposerView({
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{generateDialog.specType !== 'icon' ? (
|
||||
<div className="image-canvas-editor__field-block">
|
||||
<PlatformFieldLabel
|
||||
variant="form"
|
||||
className="image-canvas-editor__field-title"
|
||||
>
|
||||
参考图
|
||||
</PlatformFieldLabel>
|
||||
<button
|
||||
type="button"
|
||||
className="image-canvas-editor__character-spec-ref image-canvas-editor__reference-tile image-canvas-editor__reference-tile--spec"
|
||||
disabled={generateDialog.status === 'generating'}
|
||||
onClick={() => onRequestUpload('spec-reference')}
|
||||
>
|
||||
<span className="image-canvas-editor__reference-tile-visual">
|
||||
{generateDialog.specReference ? (
|
||||
<img
|
||||
src={generateDialog.specReference.src}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<ImagePlus className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
</span>
|
||||
<span className="image-canvas-editor__reference-tile-copy">
|
||||
{generateDialog.specReference?.label ?? '添加参考图'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{generateDialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
|
||||
@@ -155,6 +155,9 @@ function GenerationWorkflowHarness({
|
||||
<button type="button" onClick={workflow.openGenerateDialog}>
|
||||
打开生成
|
||||
</button>
|
||||
<button type="button" onClick={() => workflow.openSpecDialog('ui')}>
|
||||
打开UI规范
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
@@ -214,6 +217,25 @@ function GenerationWorkflowHarness({
|
||||
>
|
||||
提交生成
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
dialogs.setGenerateDialog((currentDialog) =>
|
||||
currentDialog?.mode === 'spec'
|
||||
? {
|
||||
...currentDialog,
|
||||
specReference: {
|
||||
id: 'spec-ref',
|
||||
label: '参考.png',
|
||||
src: 'data:image/png;base64,ref',
|
||||
},
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
>
|
||||
添加规范参考图
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
@@ -307,6 +329,33 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('submits spec generation with reference image and reference prompt semantics', async () => {
|
||||
generateEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: 'UI规范图' }),
|
||||
);
|
||||
render(<GenerationWorkflowHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开UI规范' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加规范参考图' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
size: '2048x1152',
|
||||
kind: 'spec',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
prompt: expect.stringContaining('参考图生成规范'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-generated-1:UI素材规范 1',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('submits quick edits beside the source and fits source plus result', async () => {
|
||||
generateEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: '快速修图' }),
|
||||
@@ -332,7 +381,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
prompt: '快速修图',
|
||||
size: '1024x768',
|
||||
kind: 'quick-edit',
|
||||
model: 'gpt-image-2',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
referenceImageSrcs: ['data:image/png;base64,source'],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -827,19 +827,30 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const specType = dialog.specType ?? 'custom';
|
||||
const specValues =
|
||||
dialog.specValues ?? DEFAULT_SPEC_FORM_VALUES[specType];
|
||||
const specPrompt = buildSpecPrompt(specType, specValues);
|
||||
const specPrompt = buildSpecPrompt(
|
||||
specType,
|
||||
specValues,
|
||||
Boolean(dialog.specReference?.src),
|
||||
);
|
||||
const generated = await generateEditorImage({
|
||||
prompt: specPrompt,
|
||||
size: SPEC_GENERATION_SIZE,
|
||||
model: DEFAULT_IMAGE_MODEL,
|
||||
kind: 'spec',
|
||||
...(dialog.specReference?.src
|
||||
? { referenceImageSrcs: [dialog.specReference.src] }
|
||||
: {}),
|
||||
});
|
||||
addGeneratedResultLayer(generated, {
|
||||
frame: getGeneratingDialogPlaceholder(dialog),
|
||||
assetKind: specType === 'icon' ? 'icon-spec' : 'spec',
|
||||
title: `${SPEC_TYPE_LABEL[specType]} ${layerCounterRef.current + 1}`,
|
||||
dialogId: canvasDialog?.id,
|
||||
generationInputs: buildSpecGenerationInputs(specType, specValues),
|
||||
generationInputs: buildSpecGenerationInputs(
|
||||
specType,
|
||||
specValues,
|
||||
dialog.specReference,
|
||||
),
|
||||
});
|
||||
} else if (dialog.mode === 'character') {
|
||||
const referenceImageSrcs = [
|
||||
|
||||
@@ -132,7 +132,7 @@ function UploadWorkflowHarness({
|
||||
<span data-testid="selected-layer">{selectedLayerId ?? '-'}</span>
|
||||
<span data-testid="dialog">
|
||||
{generateDialog
|
||||
? `${generateDialog.mode}:${generateDialog.status}:${generateDialog.characterSpecReference?.label ?? '-'}:${generateDialog.characterReferences?.length ?? 0}:${generateDialog.iconSpecReference?.label ?? '-'}`
|
||||
? `${generateDialog.mode}:${generateDialog.status}:${generateDialog.characterSpecReference?.label ?? '-'}:${generateDialog.characterReferences?.length ?? 0}:${generateDialog.iconSpecReference?.label ?? '-'}:${generateDialog.specReference?.label ?? '-'}`
|
||||
: '-'}
|
||||
</span>
|
||||
<button
|
||||
@@ -168,6 +168,26 @@ function UploadWorkflowHarness({
|
||||
>
|
||||
准备角色生成
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setGenerateDialog({
|
||||
mode: 'spec',
|
||||
specType: 'ui',
|
||||
prompt: '',
|
||||
status: 'failed',
|
||||
errorMessage: '旧错误',
|
||||
})
|
||||
}
|
||||
>
|
||||
准备规范生成
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => workflow.setUploadTarget('spec-reference')}
|
||||
>
|
||||
选择规范参考图
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => workflow.setUploadTarget('character-spec')}
|
||||
@@ -352,7 +372,7 @@ describe('useImageCanvasUploadWorkflow', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择角色规范' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'character:failed:-:0:-',
|
||||
'character:failed:-:0:-:-',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -364,7 +384,26 @@ describe('useImageCanvasUploadWorkflow', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('dialog').textContent).toContain(
|
||||
'character:idle:角色规范.png:0:-',
|
||||
'character:idle:角色规范.png:0:-:-',
|
||||
);
|
||||
});
|
||||
expect(createEditorAssetMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('dispatches file input uploads to spec reference images', async () => {
|
||||
render(<UploadWorkflowHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '准备规范生成' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择规范参考图' }));
|
||||
fireEvent.change(screen.getByLabelText('上传图片文件'), {
|
||||
target: {
|
||||
files: [createTestFile('UI参考.png')],
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('dialog').textContent).toContain(
|
||||
'spec:idle:-:0:-:UI参考.png',
|
||||
);
|
||||
});
|
||||
expect(createEditorAssetMock).not.toHaveBeenCalled();
|
||||
|
||||
@@ -126,6 +126,31 @@ export function useImageCanvasUploadWorkflow({
|
||||
[setGenerateDialog],
|
||||
);
|
||||
|
||||
const addSpecReferenceFiles = useCallback(
|
||||
async (files: FileList | File[]) => {
|
||||
const imageFile = Array.from(files).find(isImageFile);
|
||||
if (!imageFile) {
|
||||
window.alert('请选择图片文件');
|
||||
return;
|
||||
}
|
||||
|
||||
const imageSrc = await readImageFileAsDataUrl(imageFile);
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog?.mode === 'spec'
|
||||
? {
|
||||
...setFailedGenerationIdle(currentDialog),
|
||||
specReference: {
|
||||
id: `upload-spec-reference-${Date.now()}`,
|
||||
label: imageFile.name || '参考图',
|
||||
src: imageSrc,
|
||||
},
|
||||
}
|
||||
: currentDialog,
|
||||
);
|
||||
},
|
||||
[setGenerateDialog],
|
||||
);
|
||||
|
||||
const addCharacterReferenceFiles = useCallback(
|
||||
async (files: FileList | File[]) => {
|
||||
const imageFiles = Array.from(files).filter(isImageFile);
|
||||
@@ -485,7 +510,9 @@ export function useImageCanvasUploadWorkflow({
|
||||
const files = event.currentTarget.files;
|
||||
const currentUploadTarget = uploadTargetRef.current;
|
||||
if (files?.length) {
|
||||
if (currentUploadTarget === 'character-spec') {
|
||||
if (currentUploadTarget === 'spec-reference') {
|
||||
void addSpecReferenceFiles(files);
|
||||
} else if (currentUploadTarget === 'character-spec') {
|
||||
void addCharacterSpecReferenceFiles(files);
|
||||
} else if (currentUploadTarget === 'character-reference') {
|
||||
void addCharacterReferenceFiles(files);
|
||||
@@ -502,6 +529,7 @@ export function useImageCanvasUploadWorkflow({
|
||||
activeTool,
|
||||
addCharacterReferenceFiles,
|
||||
addCharacterSpecReferenceFiles,
|
||||
addSpecReferenceFiles,
|
||||
addIconSpecReferenceFiles,
|
||||
addUploadedFiles,
|
||||
setUploadTarget,
|
||||
@@ -515,6 +543,7 @@ export function useImageCanvasUploadWorkflow({
|
||||
requestUpload,
|
||||
handleUploadInputChange,
|
||||
addUploadedFiles,
|
||||
addSpecReferenceFiles,
|
||||
addCharacterSpecReferenceFiles,
|
||||
addCharacterReferenceFiles,
|
||||
addIconSpecReferenceFiles,
|
||||
|
||||
Reference in New Issue
Block a user