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
@@ -10,9 +10,9 @@ import type {
SpecGenerationType,
} from './ImageCanvasEditorTypes';
import {
CHARACTER_ANIMATION_DURATION_OPTIONS,
AUDIO_FRAME_DISPLAY_SIZE,
AUDIO_FRAME_ORIGINAL_SIZE,
CHARACTER_ANIMATION_DURATION_OPTIONS,
CHARACTER_FRAME_DISPLAY_SIZE,
CHARACTER_FRAME_ORIGINAL_SIZE,
createCanvasLayerReference,
@@ -21,7 +21,6 @@ import {
DEFAULT_SPEC_FORM_VALUES,
DEFAULT_VIDEO_MODEL,
EDITOR_IMAGE_DIMENSION_OPTIONS,
EDITOR_VIDEO_MODEL_OPTIONS,
ICON_DESCRIPTION_LIMIT,
ICON_FRAME_DISPLAY_SIZE,
ICON_FRAME_ORIGINAL_SIZE,
@@ -34,6 +33,11 @@ import {
} from './ImageCanvasGenerationModel';
type CanvasSize = { width: number; height: number };
const VIDEO_REFERENCE_LIMITS = {
image: 9,
video: 3,
audio: 3,
} as const;
function getViewportWorldCenter({
canvasSize,
@@ -57,11 +61,43 @@ function resetFailedGenerationDialog(dialog: GenerateDialogState) {
};
}
export function parseIconDescriptionsText(text: string): string[] {
return text
.split(/[\r\n,,、;;/|]+/u)
.map((description) => description.trim())
.filter(Boolean)
.slice(0, ICON_DESCRIPTION_LIMIT);
}
export function formatIconDescriptionsText(descriptions: string[]): string {
return descriptions.join('\n');
}
function isSeedanceVideoModel(model: string | undefined) {
const resolvedModel = model ?? DEFAULT_VIDEO_MODEL;
return resolvedModel === 'seedance2.0' || resolvedModel === 'seedance2.0-fast';
}
function getReferenceMediaType(layer: CanvasLayer) {
return layer.mediaType === 'video' || layer.mediaType === 'audio'
? layer.mediaType
: 'image';
}
function appendLimitedSeedanceCanvasReference(
references: NonNullable<GenerateDialogState['generationReferences']>,
layer: CanvasLayer,
) {
const mediaType = getReferenceMediaType(layer);
const currentCount = references.filter(
(reference) => (reference.mediaType ?? 'image') === mediaType,
).length;
if (currentCount >= VIDEO_REFERENCE_LIMITS[mediaType]) {
return references;
}
return [...references, createCanvasLayerReference(layer)];
}
function resolveImageDimensionDefaults(imageModel: string) {
const dimensionOptions =
EDITOR_IMAGE_DIMENSION_OPTIONS[
@@ -178,7 +214,7 @@ export function createIconGenerationDialogDraft({
const dimensionDefaults = resolveImageDimensionDefaults(imageModel);
return {
mode: 'icon',
prompt: '',
prompt: formatIconDescriptionsText(DEFAULT_ICON_DESCRIPTIONS),
status: 'idle',
composerOpen: true,
iconSpecReference: null,
@@ -241,8 +277,8 @@ export function createSoundEffectGenerationDialogDraft({
prompt: '',
status: 'idle',
composerOpen: true,
soundType: 'ui',
soundTempo: 'medium',
soundType: 'one-shot',
soundTempo: null,
placeholder: {
x: worldCenter.x - AUDIO_FRAME_DISPLAY_SIZE.width / 2,
y: worldCenter.y - AUDIO_FRAME_DISPLAY_SIZE.height / 2,
@@ -328,6 +364,7 @@ export function createQuickEditPanelDraft(
sourceLayer: CanvasLayer,
): QuickEditPanelState {
return {
mode: 'quick-edit',
sourceLayerId: sourceLayer.id,
prompt: '',
size: formatImageSizeValue(
@@ -339,6 +376,19 @@ export function createQuickEditPanelDraft(
};
}
export function createRedrawPanelDraft(
sourceLayer: CanvasLayer,
): QuickEditPanelState {
return {
...createQuickEditPanelDraft(sourceLayer),
mode: 'redraw',
prompt:
sourceLayer.prompt?.trim() ||
sourceLayer.generationInputs?.fields[0]?.value?.trim() ||
'',
};
}
export function createCharacterAnimationPanelDraft(
layer: CanvasLayer,
): CharacterAnimationPanelState | null {
@@ -360,7 +410,7 @@ export function assignCharacterSpecReference(
dialog: GenerateDialogState | null,
layer: CanvasLayer,
): GenerateDialogState | null {
return dialog?.mode === 'character'
return dialog?.mode === 'character' && getReferenceMediaType(layer) === 'image'
? {
...resetFailedGenerationDialog(dialog),
characterSpecReference: createCanvasLayerReference(layer),
@@ -373,7 +423,7 @@ export function appendCharacterReference(
dialog: GenerateDialogState | null,
layer: CanvasLayer,
): GenerateDialogState | null {
return dialog?.mode === 'character'
return dialog?.mode === 'character' && getReferenceMediaType(layer) === 'image'
? {
...resetFailedGenerationDialog(dialog),
characterReferences: [
@@ -390,6 +440,9 @@ export function appendGenerationReference(
layer: CanvasLayer,
): GenerateDialogState | null {
if (dialog?.mode === 'spec') {
if (getReferenceMediaType(layer) !== 'image') {
return dialog;
}
return {
...resetFailedGenerationDialog(dialog),
specReference: createCanvasLayerReference(layer),
@@ -399,13 +452,26 @@ export function appendGenerationReference(
if (dialog?.mode === 'video' && !isSeedanceVideoModel(dialog.videoModel)) {
return dialog;
}
return dialog?.mode === 'generate' || dialog?.mode === 'video'
if (dialog?.mode === 'generate') {
if (getReferenceMediaType(layer) !== 'image') {
return dialog;
}
return {
...resetFailedGenerationDialog(dialog),
generationReferences: [
...(dialog.generationReferences ?? []),
createCanvasLayerReference(layer),
],
composerOpen: true,
};
}
return dialog?.mode === 'video'
? {
...resetFailedGenerationDialog(dialog),
generationReferences: [
...(dialog.generationReferences ?? []),
createCanvasLayerReference(layer),
],
generationReferences: appendLimitedSeedanceCanvasReference(
dialog.generationReferences ?? [],
layer,
),
composerOpen: true,
}
: dialog;
@@ -462,39 +528,19 @@ export function updateSpecFormDialogValue(
};
}
export function updateIconDescriptionInDialog(
export function updateIconDescriptionsTextInDialog(
dialog: GenerateDialogState | null,
index: number,
value: string,
): GenerateDialogState | null {
return dialog?.mode === 'icon'
? {
...resetFailedGenerationDialog(dialog),
iconDescriptions: (
dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS
).map((description, descriptionIndex) =>
descriptionIndex === index ? value : description,
),
prompt: value,
iconDescriptions: parseIconDescriptionsText(value),
}
: dialog;
}
export function appendIconDescriptionToDialog(
dialog: GenerateDialogState | null,
): GenerateDialogState | null {
if (dialog?.mode !== 'icon') {
return dialog;
}
const descriptions = dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS;
if (descriptions.length >= ICON_DESCRIPTION_LIMIT) {
return dialog;
}
return {
...resetFailedGenerationDialog(dialog),
iconDescriptions: [...descriptions, ''],
};
}
export function updateCharacterAnimationDurationPanel(
panel: CharacterAnimationPanelState | null,
frameCountValue: string,