Files
Genarrative/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts
T
Codex 0ebf6242bb 完成图片编辑器宣发素材生成
新增宣发素材入口、工作流模型、画布生成卡片和参考图选择能力。

接入宣发素材真实生图请求,隐藏 Prompt 摘要和约束预览,并将约束作为内部 Prompt 使用。

支持游戏首图、详情五图、运营海报尺寸,其中详情五图一次生成五张 720x1280 竖图。

后端仅对 publication-material 生图扣 5 泥点,并沿用现有失败退款逻辑。

补充前后端测试和宣发素材设计文档。
2026-06-18 00:05:25 +08:00

464 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
import {
buildCharacterAnimationSubmissionPlan,
buildIconSpritesheetGenerationSubmissionPlan,
buildImageGenerationSubmissionPlan,
} from './ImageCanvasGenerationSubmissionModel';
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
return {
id: 'layer-source',
resourceId: 'resource-source',
title: '源图',
src: 'data:image/png;base64,source',
x: 120,
y: 140,
width: 320,
height: 240,
originalWidth: 1024,
originalHeight: 768,
zIndex: 2,
sourceType: 'uploaded',
...overrides,
};
}
describe('ImageCanvasGenerationSubmissionModel', () => {
it('builds normal image generation submission plans', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'generate',
prompt: ' 一张发光主视觉 ',
status: 'idle',
},
layers: [],
nextGeneratedIndex: 3,
});
expect(plan).toEqual({
kind: 'image',
normalizedPrompt: '一张发光主视觉',
input: {
prompt: '一张发光主视觉',
},
result: {
generationInputs: {
fields: [{ title: '生成提示词', value: '一张发光主视觉' }],
references: [],
},
},
});
});
it('builds edit submission plans with the source layer snapshot', () => {
const sourceLayer = createLayer();
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'edit',
prompt: '',
status: 'idle',
sourceLayerId: sourceLayer.id,
},
layers: [sourceLayer],
nextGeneratedIndex: 4,
});
expect(plan).toMatchObject({
kind: 'edit',
normalizedPrompt: '修改当前图片',
sourceLayer,
generationInputs: {
fields: [{ title: '修改要求', value: '修改当前图片' }],
references: [
{
title: '参考图',
label: '源图',
src: 'data:image/png;base64,source',
},
],
},
});
});
it('builds spec generation plans with reference prompt semantics', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'spec',
prompt: '',
status: 'idle',
specType: 'icon',
specValues: {
playSetting: '休闲消除',
artStyle: '清爽卡通',
bodyRatio: '3',
characterView: '',
customPrompt: '',
},
specReference: {
id: 'spec-ref',
label: '参考.png',
src: 'data:image/png;base64,ref',
},
},
layers: [],
nextGeneratedIndex: 8,
});
expect(plan).toMatchObject({
kind: 'image',
normalizedPrompt: 'AI 生成图片',
input: {
size: '2048x1152',
kind: 'spec',
referenceImageSrcs: ['data:image/png;base64,ref'],
prompt: expect.stringContaining('参考图生成规范'),
},
result: {
assetKind: 'icon-spec',
title: '图标素材规范 8',
},
});
expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual({
fields: [
{ title: '玩法设定', value: '休闲消除' },
{ title: '美术风格', value: '清爽卡通' },
],
references: [
{
title: '参考图',
label: '参考.png',
src: 'data:image/png;base64,ref',
},
],
});
});
it('builds character plans with references and remembered model', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'character',
prompt: ' 白发骑士 ',
status: 'idle',
imageModel: 'gpt-image-2',
aspectRatio: '2:3',
imageSize: '2K',
characterSpecReference: {
id: 'spec',
label: '角色规范',
src: 'data:image/png;base64,spec',
},
characterReferences: [
{
id: 'ref-1',
label: '盔甲参考',
src: 'data:image/png;base64,armor',
},
],
},
layers: [],
nextGeneratedIndex: 9,
});
expect(plan).toMatchObject({
kind: 'image',
normalizedPrompt: '白发骑士',
input: {
prompt: '白发骑士',
kind: 'character',
model: 'gpt-image-2',
aspectRatio: '2:3',
imageSize: '2K',
referenceImageSrcs: [
'data:image/png;base64,spec',
'data:image/png;base64,armor',
],
},
result: {
assetKind: 'character',
title: '角色形象 9',
},
rememberImageModel: 'gpt-image-2',
});
expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual({
fields: [{ title: '角色设定', value: '白发骑士' }],
references: [
{
title: '角色形象规范',
label: '角色规范',
src: 'data:image/png;base64,spec',
},
{
title: '常规参考图 1',
label: '盔甲参考',
src: 'data:image/png;base64,armor',
},
],
});
});
it('builds publication material plans with structured game fields', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'publication',
prompt: '',
status: 'idle',
publicationGameInfo: {
gameName: ' 重庆洪崖洞火锅 ',
gameCategories: '抓大鹅、休闲、治愈、手绘风',
gameDescription: '在一堆物品里点击,找到三个一样的物品将其消除',
},
publicationReferences: [
{
id: 'ref-1',
label: '参考图',
src: 'data:image/png;base64,ref',
},
],
},
layers: [],
nextGeneratedIndex: 10,
});
expect(plan).toEqual({
kind: 'image',
normalizedPrompt: [
'游戏名:重庆洪崖洞火锅',
'游戏分类:抓大鹅、休闲、治愈、手绘风',
'一句话描述游戏:在一堆物品里点击,找到三个一样的物品将其消除',
].join('\n'),
input: {
prompt: expect.stringContaining('【宣发素材类型】游戏首图'),
size: '720x540',
kind: 'publication-material',
referenceImageSrcs: ['data:image/png;base64,ref'],
},
result: {
title: '10 宣发素材',
generationInputs: {
fields: [
{ title: '游戏名', value: '重庆洪崖洞火锅' },
{ title: '游戏分类', value: '抓大鹅、休闲、治愈、手绘风' },
{
title: '一句话描述游戏',
value: '在一堆物品里点击,找到三个一样的物品将其消除',
},
],
references: [
{
title: '宣发参考图 1',
label: '参考图',
src: 'data:image/png;base64,ref',
},
],
},
},
});
const prompt = plan.kind === 'image' ? plan.input.prompt : '';
expect(prompt).toContain('游戏名:重庆洪崖洞火锅');
expect(prompt).toContain('标题文字必须直接进入画面');
expect(prompt).toContain('输出检查:720 x 5404:3');
});
it('builds detail gallery publication plans as 720p portrait images', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'publication',
prompt: '',
status: 'idle',
publicationWorkflowId: 'publication-detail-gallery',
publicationGameInfo: {
gameName: '云朵消消乐',
gameCategories: '',
gameDescription: '',
},
},
layers: [],
nextGeneratedIndex: 11,
});
expect(plan).toMatchObject({
kind: 'image',
input: {
prompt: expect.stringContaining('【宣发素材类型】详情五图'),
size: '720x1280',
kind: 'publication-material',
candidateCount: 5,
},
result: {
title: '11 宣发素材',
},
});
expect(plan.kind === 'image' ? plan.input.prompt : '').toContain(
'五张均为 720 x 1280 竖版',
);
});
it('builds promo poster publication plans as 720p landscape images', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'publication',
prompt: '',
status: 'idle',
publicationWorkflowId: 'publication-promo-poster',
publicationGameInfo: {
gameName: '火锅节活动',
gameCategories: '',
gameDescription: '',
},
},
layers: [],
nextGeneratedIndex: 12,
});
expect(plan).toMatchObject({
kind: 'image',
input: {
prompt: expect.stringContaining('【宣发素材类型】运营海报'),
size: '1280x720',
kind: 'publication-material',
},
result: {
title: '12 宣发素材',
},
});
expect(plan.kind === 'image' ? plan.input.prompt : '').toContain(
'16:9 横版 720p,建议 1280 x 720',
);
});
it('throws when edit source layer is missing', () => {
expect(() =>
buildImageGenerationSubmissionPlan({
dialog: {
mode: 'edit',
prompt: '修图',
status: 'idle',
sourceLayerId: 'missing-layer',
},
layers: [],
nextGeneratedIndex: 1,
}),
).toThrow('未找到要修改的图片');
});
it('returns an icon spritesheet error when the spec reference is missing', () => {
const plan = buildIconSpritesheetGenerationSubmissionPlan({
mode: 'icon',
prompt: '',
status: 'idle',
iconDescriptions: ['返回按钮'],
});
expect(plan).toEqual({
ok: false,
errorMessage: '请选择图标素材规范',
});
});
it('returns an icon spritesheet error when descriptions are empty', () => {
const plan = buildIconSpritesheetGenerationSubmissionPlan({
mode: 'icon',
prompt: '',
status: 'idle',
iconSpecReference: {
id: 'icon-spec',
label: '图标规范',
src: 'data:image/png;base64,spec',
},
iconDescriptions: [' ', '\n'],
});
expect(plan).toEqual({
ok: false,
errorMessage: '请填写素材描述',
});
});
it('builds icon spritesheet plans with trimmed descriptions and references', () => {
const plan = buildIconSpritesheetGenerationSubmissionPlan({
mode: 'icon',
prompt: '',
status: 'idle',
imageModel: 'gpt-image-2',
aspectRatio: '3:2',
imageSize: '2K',
iconSpecReference: {
id: 'icon-spec',
label: '图标规范',
src: 'data:image/png;base64,spec',
},
iconDescriptions: [' 返回按钮 ', '', '设置按钮'],
});
expect(plan).toEqual({
ok: true,
iconDescriptions: ['返回按钮', '设置按钮'],
input: {
referenceImageSrc: 'data:image/png;base64,spec',
iconDescriptions: ['返回按钮', '设置按钮'],
model: 'gpt-image-2',
aspectRatio: '3:2',
imageSize: '2K',
},
generationInputs: {
fields: [
{ title: '素材描述 1', value: '返回按钮' },
{ title: '素材描述 2', value: '设置按钮' },
],
references: [
{
title: '图标素材规范',
label: '图标规范',
src: 'data:image/png;base64,spec',
},
],
},
rememberImageModel: 'gpt-image-2',
});
});
it('builds character animation plans with trimmed prompt and object key source', () => {
const sourceLayer = createLayer({
id: 'character-layer',
title: '角色图',
objectKey: 'generated/character.png',
assetKind: 'character',
originalWidth: 960,
originalHeight: 1280,
});
const plan = buildCharacterAnimationSubmissionPlan({
panel: {
sourceLayerId: 'character-layer',
promptText: ' 循环奔跑动作 ',
resolution: '720p',
ratio: 'same',
frameCount: 48,
durationSeconds: 6,
status: 'idle',
},
sourceLayer,
});
expect(plan).toEqual({
promptText: '循环奔跑动作',
input: {
sourceLayerId: 'character-layer',
sourceImageSrc: 'generated/character.png',
sourceWidth: 960,
sourceHeight: 1280,
promptText: '循环奔跑动作',
resolution: '720p',
ratio: 'same',
frameCount: 48,
durationSeconds: 6,
priceMudPoints: 120,
model: 'seedance2.0',
},
});
});
});