Files
Genarrative/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts
T
k88936 609b036b04 前端按业务ID提交图片编辑
优先使用图层 resourceId 并回退正式 sourceAssetId
未登记来源在上传标注图前失败关闭
保持红框标注图为辅助 referenceImageSrcs 而非主来源
移除图片编辑请求中的旧来源和类型字段并补齐工作流测试
2026-08-07 14:04:26 +08:00

1157 lines
32 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 {
IMAGE_MODEL_GPT_IMAGE_2,
IMAGE_MODEL_NANOBANANA2,
} from './ImageCanvasGenerationModel';
import {
buildCharacterAnimationSubmissionPlan,
buildIconSpritesheetGenerationSubmissionPlan,
buildImageGenerationSubmissionPlan,
resolveRegisteredEditorReferenceId,
} 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('resolves registered image-edit sources by resource ID first', () => {
expect(
resolveRegisteredEditorReferenceId({
resourceId: ' resource-source ',
sourceAssetId: 'asset-source',
}),
).toBe('resource-source');
});
it('falls back to a registered asset ID for image edits', () => {
expect(
resolveRegisteredEditorReferenceId({
resourceId: 'local-resource-source',
sourceAssetId: ' asset-source ',
}),
).toBe('asset-source');
});
it('rejects unregistered image-edit sources', () => {
expect(() =>
resolveRegisteredEditorReferenceId({
resourceId: 'generation-dialog:source',
sourceAssetId: 'upload-source',
}),
).toThrow('原图尚未登记为项目资源或素材');
});
it('builds normal image generation submission plans', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'generate',
prompt: ' 一张发光主视觉 ',
status: 'idle',
},
layers: [],
nextGeneratedIndex: 3,
});
expect(plan).toMatchObject({
kind: 'image',
normalizedPrompt: '一张发光主视觉',
input: {
prompt: '一张发光主视觉',
model: 'gemini-3.1-flash-image-preview',
style: 'none',
aspectRatio: '1:1',
imageSize: '1K',
},
result: {
title: '生成图片 3',
generationInputs: {
fields: [{ title: '生成提示词', value: '一张发光主视觉' }],
references: [],
},
},
rememberImageModel: 'gemini-3.1-flash-image-preview',
});
});
it('trims custom asset names and limits them to 80 characters', () => {
const customName = ` ${'名'.repeat(81)} `;
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'generate',
prompt: '一张发光主视觉',
assetLabel: customName,
status: 'idle',
},
layers: [],
nextGeneratedIndex: 3,
});
expect(plan).toMatchObject({
kind: 'image',
result: { title: '名'.repeat(80) },
});
});
it('normalizes legacy nanobanana aliases before submitting image generation', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'generate',
prompt: '一张明亮图片',
status: 'idle',
imageModel: 'nanobanana2',
aspectRatio: '16:9',
imageSize: '2K',
},
layers: [],
nextGeneratedIndex: 5,
});
expect(plan).toMatchObject({
kind: 'image',
input: {
model: IMAGE_MODEL_NANOBANANA2,
aspectRatio: '16:9',
imageSize: '2K',
},
rememberImageModel: IMAGE_MODEL_NANOBANANA2,
});
});
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: '源图',
refType: 'project-resource',
refId: 'resource-source',
},
],
},
});
});
it('builds quick-edit canvas generator plans as direct source edits', () => {
const sourceLayer = createLayer({
objectKey: 'generated-character-drafts/editor/source.png',
assetKind: 'character',
originalWidth: 1537,
originalHeight: 1025,
});
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'quick-edit',
prompt: '把当前图改成雨天',
status: 'idle',
sourceLayerId: sourceLayer.id,
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
aspectRatio: '4:3',
imageSize: '1K',
generationReferences: [
{
id: 'style-reference',
label: '风格参考',
src: 'data:image/png;base64,style',
},
],
},
layers: [sourceLayer],
nextGeneratedIndex: 4,
});
expect(plan).toMatchObject({
kind: 'quick-edit',
normalizedPrompt: '把当前图改成雨天',
sourceLayer,
editInput: {
size: '1537x1025',
model: IMAGE_MODEL_GPT_IMAGE_2,
},
result: {
title: '源图 快速编辑',
assetKind: 'character',
generationInputs: {
fields: [{ title: '快速编辑提示词', value: '把当前图改成雨天' }],
references: [
{
title: '原图',
label: '源图',
refType: 'project-resource',
refId: 'resource-source',
},
],
},
},
rememberImageModel: IMAGE_MODEL_GPT_IMAGE_2,
});
});
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: {
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',
resourceId: 'resource-spec-ref',
},
},
layers: [],
nextGeneratedIndex: 8,
});
expect(plan).toMatchObject({
kind: 'image',
normalizedPrompt: 'AI 生成图片',
input: {
size: '2048x1152',
model: 'gpt-image-2',
aspectRatio: '16:9',
imageSize: '2K',
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',
refType: 'project-resource',
refId: 'resource-spec-ref',
},
],
},
);
});
it('treats the renamed UI spec entry as an icon spec asset', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'spec',
prompt: '',
status: 'idle',
specType: 'ui',
specValues: {
playSetting: '消除类派对玩法',
artStyle: '糖果玻璃拟物',
bodyRatio: '3',
characterView: '',
customPrompt: '',
},
},
layers: [],
nextGeneratedIndex: 2,
});
expect(plan).toMatchObject({
kind: 'image',
result: {
assetKind: 'icon-spec',
title: '图标规范 2',
},
});
expect(plan.kind === 'image' ? plan.input : null).not.toHaveProperty(
'referenceImageSrcs',
);
expect(plan.kind === 'image' ? plan.input.prompt : '').not.toContain(
'参考图生成规范',
);
});
it('builds character plans with references and remembered model', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'character',
prompt: ' 白发骑士 ',
status: 'idle',
imageModel: 'gpt-image-2',
style: 'pixelArt',
aspectRatio: '2:3',
imageSize: '2K',
characterSpecReference: {
id: 'spec',
label: '角色规范',
src: 'data:image/png;base64,spec',
resourceId: 'resource-character-spec',
},
characterReferences: [
{
id: 'ref-1',
label: '盔甲参考',
src: 'data:image/png;base64,armor',
resourceId: 'resource-armor',
},
],
},
layers: [],
nextGeneratedIndex: 9,
});
expect(plan).toMatchObject({
kind: 'image',
normalizedPrompt: '白发骑士',
input: {
prompt: '白发骑士',
kind: 'character',
model: 'gpt-image-2',
screenColor: 'auto',
segModel: 'birefnet',
style: 'pixelArt',
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: '角色规范',
refType: 'project-resource',
refId: 'resource-character-spec',
},
{
title: '常规参考图 1',
label: '盔甲参考',
refType: 'project-resource',
refId: 'resource-armor',
},
],
},
);
});
it('locks UI design generation submissions to gpt-image-2', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'ui-design',
prompt: ' 设计移动端商城首页 ',
status: 'idle',
imageModel: 'nanobanana2',
aspectRatio: '16:9',
imageSize: '0.5K',
uiDesignSpecReference: {
id: 'ui-spec',
label: 'UI图标规范',
src: 'data:image/png;base64,ui-spec',
resourceId: 'resource-ui-spec',
},
generationReferences: [
{
id: 'ui-ref',
label: '界面风格参考',
src: 'data:image/png;base64,ui-ref',
resourceId: 'resource-ui-ref',
},
],
},
layers: [],
nextGeneratedIndex: 6,
});
expect(plan).toMatchObject({
kind: 'image',
normalizedPrompt: '设计移动端商城首页',
input: {
kind: 'ui-design',
model: IMAGE_MODEL_GPT_IMAGE_2,
aspectRatio: '16:9',
imageSize: '1K',
referenceImageSrcs: [
'data:image/png;base64,ui-spec',
'data:image/png;base64,ui-ref',
],
},
result: {
assetKind: 'ui-design',
title: 'UI设计图 6',
generationInputs: {
fields: [{ title: '用户输入', value: '设计移动端商城首页' }],
references: [
{
title: '图标规范',
label: 'UI图标规范',
refType: 'project-resource',
refId: 'resource-ui-spec',
},
{
title: '参考图 1',
label: '界面风格参考',
refType: 'project-resource',
refId: 'resource-ui-ref',
},
],
},
},
rememberImageModel: IMAGE_MODEL_GPT_IMAGE_2,
});
});
it('builds publication material plans with structured game fields', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'publication',
prompt: '',
status: 'idle',
style: 'pixelArt',
publicationGameInfo: {
gameName: ' 重庆洪崖洞火锅 ',
gameCategories: '抓大鹅、休闲、治愈、手绘风',
gameDescription: '在一堆物品里点击,找到三个一样的物品将其消除',
},
publicationReferences: [
{
id: 'ref-1',
label: '参考图',
src: 'data:image/png;base64,ref',
resourceId: 'resource-publication-ref',
},
],
},
layers: [],
nextGeneratedIndex: 10,
});
expect(plan).toEqual({
kind: 'image',
normalizedPrompt: [
'游戏名:重庆洪崖洞火锅',
'游戏分类:抓大鹅、休闲、治愈、手绘风',
'一句话描述游戏:在一堆物品里点击,找到三个一样的物品将其消除',
].join('\n'),
input: {
prompt: expect.stringContaining('【宣发素材类型】游戏首图'),
size: '720x540',
kind: 'publication-material',
model: 'gpt-image-2',
aspectRatio: '4:3',
imageSize: '1K',
referenceImageSrcs: ['data:image/png;base64,ref'],
},
result: {
assetKind: 'publication-material',
title: '10 宣发素材',
generationInputs: {
fields: [
{ title: '游戏名', value: '重庆洪崖洞火锅' },
{ title: '游戏分类', value: '抓大鹅、休闲、治愈、手绘风' },
{
title: '一句话描述游戏',
value: '在一堆物品里点击,找到三个一样的物品将其消除',
},
],
references: [
{
title: '宣发参考图 1',
label: '参考图',
refType: 'project-resource',
refId: 'resource-publication-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 one 720p portrait image', () => {
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',
aspectRatio: '9:16',
imageSize: '1K',
},
result: {
assetKind: 'publication-material',
title: '11 宣发素材',
},
});
expect(plan.kind === 'image' ? plan.input.prompt : '').toContain(
'本次只生成一张 720 x 1280 竖版详情图',
);
expect(plan.kind === 'image' ? plan.input.prompt : '').toContain(
'不是五图合集或拼贴',
);
});
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',
aspectRatio: '16:9',
imageSize: '1K',
},
result: {
title: '12 宣发素材',
},
});
expect(plan.kind === 'image' ? plan.input.prompt : '').toContain(
'16:9 横版 720p,建议 1280 x 720',
);
expect(plan.kind === 'image' ? plan.input.prompt : '').toContain(
'此处换上你的游戏二维码',
);
expect(plan.kind === 'image' ? plan.input.prompt : '').toContain(
'方块内部上方用黑色小字',
);
});
it('locks publication material plans to gpt-image-2 and does not remember the fixed model', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'publication',
prompt: '',
status: 'idle',
publicationWorkflowId: 'publication-promo-poster',
publicationGameInfo: {
gameName: '星际塔防',
gameCategories: '',
gameDescription: '',
},
publicationReferences: [],
imageModel: 'nanobanana2',
},
layers: [],
nextGeneratedIndex: 4,
});
expect(plan.kind).toBe('image');
if (plan.kind !== 'image') {
return;
}
expect(plan.input).toEqual(
expect.objectContaining({
kind: 'publication-material',
model: 'gpt-image-2',
size: '1280x720',
aspectRatio: '16:9',
imageSize: '1K',
}),
);
expect(plan.rememberImageModel).toBeUndefined();
});
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',
resourceId: 'resource-icon-spec',
},
iconDescriptions: [' ', '\n'],
});
expect(plan).toEqual({
ok: false,
errorMessage: '请填写素材描述',
});
});
it('builds icon spritesheet plans with the complete prompt and references', () => {
const plan = buildIconSpritesheetGenerationSubmissionPlan({
mode: 'icon',
prompt: ' 返回按钮\n\n设置按钮 ',
status: 'idle',
imageModel: 'gpt-image-2',
style: 'pixelArt',
aspectRatio: '3:2',
imageSize: '2K',
assetLabel: ' 冒险游戏图标 ',
iconSpecReference: {
id: 'icon-spec',
label: '图标规范',
src: 'data:image/png;base64,spec',
resourceId: 'resource-icon-spec',
},
generationReferences: [
{
id: 'icon-ref',
label: '按钮风格参考',
src: 'data:image/png;base64,ref',
resourceId: 'resource-icon-ref',
},
],
iconDescriptions: ['旧返回', '旧设置'],
});
expect(plan).toEqual({
ok: true,
iconDescriptions: ['返回按钮\n\n设置按钮'],
input: {
referenceImageSrc: 'data:image/png;base64,spec',
referenceImageSrcs: ['data:image/png;base64,ref'],
iconDescriptions: ['返回按钮\n\n设置按钮'],
model: 'gpt-image-2',
screenColor: 'auto',
segModel: 'birefnet',
style: 'pixelArt',
aspectRatio: '3:2',
imageSize: '2K',
},
generationInputs: {
fields: [{ title: '素材描述', value: '返回按钮\n\n设置按钮' }],
references: [
{
title: '图标规范',
label: '图标规范',
refType: 'project-resource',
refId: 'resource-icon-spec',
},
{
title: '参考图 1',
label: '按钮风格参考',
refType: 'project-resource',
refId: 'resource-icon-ref',
},
],
},
rememberImageModel: 'gpt-image-2',
resultTitle: '冒险游戏图标',
});
});
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: ' 循环奔跑动作 ',
assetLabel: ' 勇者奔跑 ',
resolution: '720p',
ratio: 'same',
frameCount: 48,
durationSeconds: 6,
status: 'idle',
},
sourceLayer,
});
expect(plan).toEqual({
promptText: '循环奔跑动作',
resultTitle: '勇者奔跑',
input: {
sourceLayerId: 'character-layer',
sourceImageSrc: 'generated/character.png',
sourceWidth: 960,
sourceHeight: 1280,
promptText: '循环奔跑动作',
screenColor: 'auto',
resolution: '720p',
ratio: 'same',
frameCount: 48,
durationSeconds: 6,
model: 'seedance2.0-fast',
assetLabel: '勇者奔跑',
},
});
});
it('builds seedance video plans with multimodal references', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'video',
prompt: ' 根据参考生成角色宣传片 ',
status: 'idle',
videoModel: 'seedance2.0-fast',
videoResolution: '720p',
videoDurationSeconds: 5,
generationReferences: [
{
id: 'image-ref',
label: '参考图.png',
src: 'data:image/png;base64,image',
mediaType: 'image',
mimeType: 'image/png',
sizeBytes: 1024,
resourceId: 'resource-video-image-ref',
},
{
id: 'video-ref',
label: '参考视频.mp4',
src: 'https://signed.example.com/video.mp4',
mediaType: 'video',
mimeType: 'video/mp4',
sizeBytes: 2048,
durationSeconds: 4,
objectKey:
'generated-character-drafts/editor/seedance-references/video/video.mp4',
resourceId: 'resource-video-ref',
},
{
id: 'audio-ref',
label: '参考音频.mp3',
src: 'https://signed.example.com/audio.mp3',
mediaType: 'audio',
mimeType: 'audio/mpeg',
sizeBytes: 512,
durationSeconds: 3,
objectKey:
'generated-character-drafts/editor/seedance-references/audio/audio.mp3',
resourceId: 'resource-audio-ref',
},
],
},
layers: [],
nextGeneratedIndex: 6,
});
expect(plan).toMatchObject({
kind: 'video',
input: {
model: 'seedance2.0-fast',
referenceImageSrcs: ['data:image/png;base64,image'],
referenceVideoSrcs: [
'generated-character-drafts/editor/seedance-references/video/video.mp4',
],
referenceAudioSrcs: [
'generated-character-drafts/editor/seedance-references/audio/audio.mp3',
],
},
result: {
generationInputs: {
fields: [
{ title: '视频描述', value: '根据参考生成角色宣传片' },
{ title: '时长', value: '5秒' },
],
references: [
{
title: '参考图 1',
label: '参考图.png',
refType: 'project-resource',
refId: 'resource-video-image-ref',
},
{
title: '参考视频 1',
label: '参考视频.mp4',
refType: 'project-resource',
refId: 'resource-video-ref',
},
{
title: '参考音频 1',
label: '参考音频.mp3',
refType: 'project-resource',
refId: 'resource-audio-ref',
},
],
},
},
});
});
it('does not submit references to non seedance video models', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'video',
prompt: '镜头推进',
status: 'idle',
videoModel: 'kling3.0',
generationReferences: [
{
id: 'image-ref',
label: '参考图.png',
src: 'data:image/png;base64,image',
mediaType: 'image',
},
],
},
layers: [],
nextGeneratedIndex: 2,
});
expect(plan.kind).toBe('video');
if (plan.kind === 'video') {
expect(plan.input).not.toHaveProperty('referenceImageSrcs');
expect(plan.input).not.toHaveProperty('referenceVideoSrcs');
expect(plan.input).not.toHaveProperty('referenceAudioSrcs');
}
});
it('rejects seedance video plans that only contain audio references', () => {
expect(() =>
buildImageGenerationSubmissionPlan({
dialog: {
mode: 'video',
prompt: '根据音频生成视频',
status: 'idle',
videoModel: 'seedance2.0',
generationReferences: [
{
id: 'audio-ref',
label: '旁白.wav',
src: 'data:audio/wav;base64,audio',
mediaType: 'audio',
durationSeconds: 4,
},
],
},
layers: [],
nextGeneratedIndex: 2,
}),
).toThrow('参考音频必须搭配参考图片或参考视频');
});
it('rejects seedance video plans that exceed reference count limits', () => {
expect(() =>
buildImageGenerationSubmissionPlan({
dialog: {
mode: 'video',
prompt: '生成视频',
status: 'idle',
videoModel: 'seedance2.0-fast',
generationReferences: Array.from({ length: 10 }, (_, index) => ({
id: `image-${index}`,
label: `参考图${index}.png`,
src: `data:image/png;base64,${index}`,
mediaType: 'image' as const,
})),
},
layers: [],
nextGeneratedIndex: 2,
}),
).toThrow('参考图片最多 9 张');
});
it('builds video generation plans with seedance fast as the fallback model', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'video',
prompt: ' 角色绕场一周 ',
status: 'idle',
generationReferences: [],
videoResolution: '480p',
videoDurationSeconds: 4,
},
layers: [],
nextGeneratedIndex: 3,
});
expect(plan).toMatchObject({
kind: 'video',
normalizedPrompt: '角色绕场一周',
input: {
model: 'seedance2.0-fast',
},
result: {
title: '生成视频 3',
},
});
});
it('builds video generation plans with explicit seedance standard model', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'video',
prompt: ' 生成电影感开场动画 ',
status: 'idle',
videoModel: 'seedance2.0',
videoResolution: '720p',
videoDurationSeconds: 5,
},
layers: [],
nextGeneratedIndex: 7,
});
expect(plan).toMatchObject({
kind: 'video',
normalizedPrompt: '生成电影感开场动画',
input: {
prompt: '生成电影感开场动画',
model: 'seedance2.0',
resolution: '720p',
durationSeconds: 5,
},
result: {
title: '生成视频 7',
},
});
});
it('builds game sound effect audio submission plans', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'audio-sound-effect',
prompt: ' 金币掉落叮当声 ',
status: 'idle',
soundDurationSeconds: 7,
},
layers: [],
nextGeneratedIndex: 4,
});
expect(plan).toEqual({
kind: 'audio',
audioKind: 'sound-effect',
normalizedPrompt: '金币掉落叮当声',
input: {
prompt: '金币掉落叮当声',
model: 'audio1.0',
duration: 7,
},
result: {
title: '游戏音效 4',
generationInputs: {
fields: [
{ title: 'prompt', value: '金币掉落叮当声' },
{ title: 'model', value: 'audio1.0' },
{ title: '时长', value: '7秒' },
],
references: [],
},
},
});
});
it('uses the game sound effect fallback for an all-whitespace prompt', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'audio-sound-effect',
prompt: ' \t\r\n ',
status: 'idle',
},
layers: [],
nextGeneratedIndex: 6,
});
expect(plan).toEqual({
kind: 'audio',
audioKind: 'sound-effect',
normalizedPrompt: '游戏音效',
input: {
prompt: '游戏音效',
model: 'audio1.0',
duration: 5,
},
result: {
title: '游戏音效 6',
generationInputs: {
fields: [
{ title: 'prompt', value: '游戏音效' },
{ title: 'model', value: 'audio1.0' },
{ title: '时长', value: '5秒' },
],
references: [],
},
},
});
});
it('builds game background music audio submission plans with instrumental fixed to true', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'audio-background-music',
prompt: '不会进入正式请求的旧值',
status: 'idle',
},
layers: [],
nextGeneratedIndex: 5,
canonicalBackgroundMusicPrompt: '\uFEFF温暖轻快的森林冒险背景音乐\uFEFF',
});
expect(plan).toEqual({
kind: 'audio',
audioKind: 'background-music',
normalizedPrompt: '\uFEFF温暖轻快的森林冒险背景音乐\uFEFF',
input: {
gptDescriptionPrompt: '\uFEFF温暖轻快的森林冒险背景音乐\uFEFF',
makeInstrumental: true,
},
result: {
title: '游戏背景音乐 5',
generationInputs: {
fields: [
{
title: 'gpt_description_prompt',
value: '\uFEFF温暖轻快的森林冒险背景音乐\uFEFF',
},
],
references: [],
},
},
});
});
it('does not build a background music plan without a claimed canonical prompt', () => {
expect(() =>
buildImageGenerationSubmissionPlan({
dialog: {
mode: 'audio-background-music',
prompt: ' 不应原生 trim 或回退默认值 ',
status: 'idle',
},
layers: [],
nextGeneratedIndex: 1,
}),
).toThrow('背景音乐提交缺少已确认的提示词');
});
});