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:
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user