Image editor: Seedance2.0 refs and audio changes

Adds Seedance 2.0 reference-media constraints and forbids video data URLs; updates docs/decision-log and pitfalls to record the new rules. Introduces typed audio params (type: one-shot|loop, tempo: number|null), generator-specific placeholder styles/avoidance and UI changes for generation panels. Implements related front-end changes (new crop/ raster edit components, many image-editor models, tests and workflow hooks) and server-side contract/handler updates across api-server, shared-contracts, platform-audio, platform-image and vector_engine_audio_generation to support the new submission/asset flow and validations.
This commit is contained in:
2026-06-18 17:31:35 +08:00
parent 6f1ae6caef
commit 9c7b3ad16d
68 changed files with 3262 additions and 1294 deletions
@@ -8,28 +8,28 @@ import type {
import {
appendCharacterReference,
appendGenerationReference,
appendIconDescriptionToDialog,
assignCharacterSpecReference,
assignIconSpecReference,
assignUiDesignSpecReference,
closeGenerateComposerDialog,
createBackgroundMusicGenerationDialogDraft,
createCharacterAnimationPanelDraft,
createCharacterGenerationDialogDraft,
createEditDialogDraft,
createGenerateDialogDraft,
createIconGenerationDialogDraft,
createSoundEffectGenerationDialogDraft,
createBackgroundMusicGenerationDialogDraft,
createQuickEditPanelDraft,
createSoundEffectGenerationDialogDraft,
createSpecDialogDraft,
createUiDesignGenerationDialogDraft,
createVideoGenerationDialogDraft,
formatIconDescriptionsText,
hideGeneratedLayerComposerAfterBlur,
parseIconDescriptionsText,
updateCharacterAnimationDurationPanel,
updateIconDescriptionInDialog,
updateIconDescriptionsTextInDialog,
updateSpecFormDialogValue,
} from './ImageCanvasGenerationDialogModel';
import { ICON_DESCRIPTION_LIMIT } from './ImageCanvasGenerationModel';
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
return {
@@ -120,6 +120,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
imageModel: 'unknown-model',
aspectRatio: '1:1',
imageSize: '1K',
prompt: '返回按钮\n设置按钮\n下一关按钮\n提示按钮\n原图按钮\n冻结按钮',
iconSpecReference: null,
iconDescriptions: [
'返回按钮',
@@ -193,8 +194,8 @@ describe('ImageCanvasGenerationDialogModel', () => {
mode: 'audio-sound-effect',
status: 'idle',
composerOpen: true,
soundType: 'ui',
soundTempo: 'medium',
soundType: 'one-shot',
soundTempo: null,
placeholder: {
x: 270,
y: 300,
@@ -234,6 +235,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
sourceLayerId: 'layer-source',
});
expect(createQuickEditPanelDraft(sourceLayer)).toEqual({
mode: 'quick-edit',
sourceLayerId: 'layer-source',
prompt: '',
size: '1024x768',
@@ -303,6 +305,14 @@ describe('ImageCanvasGenerationDialogModel', () => {
it('routes picked canvas references to spec, image, video, and UI design fields', () => {
const sourceLayer = createLayer({ title: '参考图' });
const videoLayer = createLayer({
id: 'layer-video',
title: '动作参考',
src: 'https://signed.example.com/reference.mp4',
mediaType: 'video',
objectKey: 'generated-character-drafts/editor/seedance-references/video/reference.mp4',
assetObjectId: 'asset-object-video',
});
expect(
appendGenerationReference(
{
@@ -331,6 +341,41 @@ describe('ImageCanvasGenerationDialogModel', () => {
).toMatchObject({
generationReferences: [{ label: '参考图' }],
});
expect(
appendGenerationReference(
{
mode: 'video',
prompt: '',
status: 'idle',
videoModel: 'seedance2.0-fast',
generationReferences: [],
},
videoLayer,
),
).toMatchObject({
generationReferences: [
{
label: '动作参考',
mediaType: 'video',
objectKey:
'generated-character-drafts/editor/seedance-references/video/reference.mp4',
assetObjectId: 'asset-object-video',
},
],
});
const imageDialog: GenerateDialogState = {
mode: 'generate',
prompt: '',
status: 'idle',
generationReferences: [],
};
expect(appendGenerationReference(imageDialog, videoLayer)).toBe(imageDialog);
const specDialog: GenerateDialogState = {
mode: 'spec',
prompt: '',
status: 'idle',
};
expect(appendGenerationReference(specDialog, videoLayer)).toBe(specDialog);
const uiDialog: GenerateDialogState = {
mode: 'ui-design',
@@ -351,6 +396,57 @@ describe('ImageCanvasGenerationDialogModel', () => {
});
});
it('limits Seedance canvas-picked references by media type', () => {
const videoDialog: GenerateDialogState = {
mode: 'video',
prompt: '',
status: 'idle',
videoModel: 'seedance2.0',
generationReferences: Array.from({ length: 3 }, (_, index) => ({
id: `video-${index}`,
label: `视频${index + 1}`,
src: `generated-character-drafts/video-${index}.mp4`,
mediaType: 'video',
})),
};
expect(
appendGenerationReference(
videoDialog,
createLayer({
id: 'video-extra',
title: '额外视频',
mediaType: 'video',
src: 'generated-character-drafts/video-extra.mp4',
}),
),
).toMatchObject({
generationReferences: [
{ label: '视频1' },
{ label: '视频2' },
{ label: '视频3' },
],
});
expect(
appendGenerationReference(
videoDialog,
createLayer({
id: 'audio-first',
title: '旁白',
mediaType: 'audio',
src: 'generated-character-drafts/audio.mp3',
}),
),
).toMatchObject({
generationReferences: [
{ label: '视频1' },
{ label: '视频2' },
{ label: '视频3' },
{ label: '旁白', mediaType: 'audio' },
],
});
});
it('updates failed spec and icon dialog fields back to idle state', () => {
const specDialog: GenerateDialogState = {
mode: 'spec',
@@ -387,31 +483,22 @@ describe('ImageCanvasGenerationDialogModel', () => {
errorMessage: '描述错误',
iconDescriptions: ['旧描述'],
};
expect(updateIconDescriptionInDialog(iconDialog, 0, '新描述')).toEqual(
expect(updateIconDescriptionsTextInDialog(iconDialog, '剑\n盾、药水')).toEqual(
expect.objectContaining({
status: 'idle',
errorMessage: undefined,
iconDescriptions: ['新描述'],
}),
);
expect(appendIconDescriptionToDialog(iconDialog)).toEqual(
expect.objectContaining({
iconDescriptions: ['旧描述', ''],
}),
);
expect(
appendIconDescriptionToDialog({
...iconDialog,
iconDescriptions: Array.from(
{ length: ICON_DESCRIPTION_LIMIT },
(_, index) => `图标${index}`,
),
}),
).toEqual(
expect.objectContaining({
iconDescriptions: expect.arrayContaining(['图标0']),
prompt: '剑\n盾、药水',
iconDescriptions: ['剑', '盾', '药水'],
}),
);
expect(parseIconDescriptionsText('剑,盾\n药水/宝箱|钥匙')).toEqual([
'剑',
'盾',
'药水',
'宝箱',
'钥匙',
]);
expect(formatIconDescriptionsText(['剑', '盾'])).toBe('剑\n盾');
});
it('updates character animation duration and composer visibility', () => {