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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user