Spine序列帧动画多模态生成

This commit is contained in:
2026-08-10 20:14:09 +08:00
parent 3c457feb4c
commit 1575afc92c
55 changed files with 7398 additions and 795 deletions
@@ -48,6 +48,7 @@ import {
isNormalizedCanvasGenerationInputs,
normalizeEditorImageModel,
PUBLICATION_FRAME_ORIGINAL_SIZE,
resizeGenerationPlaceholderToCharacterAnimationSelection,
resizeGenerationPlaceholderToImageSelection,
resizeGenerationPlaceholderToVideoSelection,
resolveDialogExtraImageReferenceLimit,
@@ -1264,31 +1265,104 @@ function createNormalizedGenerationDialogDraft({
}
if (inputs.action === 'character-animation.generate') {
const animationSource = resolveNormalizedReferences(
const canvasReferences = resolveNormalizedReferences(
sourceLayer,
'source',
availableLayers,
)
.map((reference) =>
(availableLayers ?? []).find(
(layer) => createCanvasLayerReference(layer).id === reference.id,
),
)
.find((layer): layer is CanvasLayer => Boolean(layer));
if (!animationSource) {
);
const inputMode = getNormalizedString(sourceLayer, 'inputMode', 'image');
const storedReferenceSrc = getNormalizedString(
sourceLayer,
'referenceSrc',
).trim();
const storedReferenceMediaType = getNormalizedString(
sourceLayer,
'referenceMediaType',
inputMode === 'video' ? 'video' : 'image',
);
const storedReferenceInput = sourceLayer.generationInputs?.references.find(
(reference) => reference.id === 'source',
);
const storedReferenceWidth = getNormalizedNumber(
sourceLayer,
'referenceWidth',
0,
);
const storedReferenceHeight = getNormalizedNumber(
sourceLayer,
'referenceHeight',
0,
);
const restoredReference =
canvasReferences[0] ??
(storedReferenceSrc
? {
id: `restored-character-animation-reference:${sourceLayer.id}`,
label:
storedReferenceInput?.label ??
(storedReferenceMediaType === 'video' ? '参考视频' : '角色图片'),
src: storedReferenceSrc,
objectKey: storedReferenceSrc,
...(storedReferenceInput?.refType === 'asset'
? { sourceAssetId: storedReferenceInput.refId }
: {}),
...(storedReferenceWidth > 0
? { width: storedReferenceWidth }
: {}),
...(storedReferenceHeight > 0
? { height: storedReferenceHeight }
: {}),
mediaType:
storedReferenceMediaType === 'video'
? ('video' as const)
: ('image' as const),
}
: undefined);
const animationSource = restoredReference
? (availableLayers ?? []).find(
(layer) =>
createCanvasLayerReference(layer).id === restoredReference.id,
)
: undefined;
if (inputMode !== 'text' && !restoredReference) {
return null;
}
const draft = createCharacterAnimationGenerationDialogDraft({
canvasSize,
viewport,
layer: animationSource,
...(animationSource?.assetKind === 'character'
? { layer: animationSource }
: {}),
});
// 帧数/时长必须匹配合法配对(32/4、40/5、48/6),否则 UI 会同时高亮
// 冲突的选项而提交被后端拒绝。这里按合法配对校验,损坏数据回退到默认 32/4。
const restoredFrameCount = getNormalizedNumber(sourceLayer, 'frameCount', 32);
const restoredDurationSeconds = getNormalizedNumber(
sourceLayer,
'durationSeconds',
4,
);
const durationPairValid = CHARACTER_ANIMATION_DURATION_OPTIONS.some(
(option) =>
option.frameCount === restoredFrameCount &&
option.durationSeconds === restoredDurationSeconds,
);
const pairedFrameCount = durationPairValid ? restoredFrameCount : 32;
const pairedDurationSeconds = durationPairValid
? restoredDurationSeconds
: 4;
return draft
? placeDraftBesideSourceLayer(
{
resizeGenerationPlaceholderToCharacterAnimationSelection({
...draft,
prompt,
sourceLayerId: animationSource.id,
sourceLayerId: animationSource?.id,
generationReferences: restoredReference ? [restoredReference] : [],
characterAnimationBackgroundColor: (getNormalizedString(
sourceLayer,
'backgroundColor',
).trim() ||
undefined) as GenerateDialogState['characterAnimationBackgroundColor'],
characterAnimationResolution: getNormalizedString(
sourceLayer,
'resolution',
@@ -1297,19 +1371,11 @@ function createNormalizedGenerationDialogDraft({
characterAnimationRatio: getNormalizedString(
sourceLayer,
'ratio',
'same',
animationSource ? 'same' : '1:1',
) as GenerateDialogState['characterAnimationRatio'],
characterAnimationFrameCount: getNormalizedNumber(
sourceLayer,
'frameCount',
32,
) as GenerateDialogState['characterAnimationFrameCount'],
characterAnimationDurationSeconds: getNormalizedNumber(
sourceLayer,
'durationSeconds',
4,
) as GenerateDialogState['characterAnimationDurationSeconds'],
},
characterAnimationFrameCount: pairedFrameCount as GenerateDialogState['characterAnimationFrameCount'],
characterAnimationDurationSeconds: pairedDurationSeconds as GenerateDialogState['characterAnimationDurationSeconds'],
}),
sourceLayer,
)
: null;
@@ -2019,6 +2085,7 @@ export function createCharacterAnimationPanelDraft(
}
return {
sourceLayerId: layer.id,
references: [createCanvasLayerReference(layer)],
promptText: '',
resolution: '480p',
ratio: 'same',
@@ -2035,20 +2102,22 @@ export function createCharacterAnimationGenerationDialogDraft({
}: {
canvasSize: CanvasSize;
viewport: CanvasViewport;
layer: CanvasLayer;
layer?: CanvasLayer;
}): Omit<CanvasGenerationDialogState, 'id'> | null {
if (layer.assetKind !== 'character') {
if (layer && layer.assetKind !== 'character') {
return null;
}
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
return {
const draft: Omit<CanvasGenerationDialogState, 'id'> = {
mode: 'character-animation',
sourceLayerId: layer.id,
...(layer ? { sourceLayerId: layer.id } : {}),
generationReferences: layer ? [createCanvasLayerReference(layer)] : [],
prompt: '',
status: 'idle',
composerOpen: true,
characterAnimationResolution: '480p',
characterAnimationRatio: 'same',
characterAnimationRatio: layer ? 'same' : '1:1',
...(layer ? {} : { characterAnimationBackgroundColor: 'green' as const }),
characterAnimationFrameCount: 32,
characterAnimationDurationSeconds: 4,
placeholder: {
@@ -2060,6 +2129,7 @@ export function createCharacterAnimationGenerationDialogDraft({
originalHeight: CHARACTER_ANIMATION_FRAME_ORIGINAL_SIZE.height,
},
};
return resizeGenerationPlaceholderToCharacterAnimationSelection(draft);
}
export function assignCharacterSpecReference(
@@ -2111,6 +2181,23 @@ export function appendGenerationReference(
if (dialog?.mode === 'video' && !isSeedanceVideoModel(dialog.videoModel)) {
return dialog;
}
if (dialog?.mode === 'character-animation') {
const mediaType = getReferenceMediaType(layer);
if (
mediaType === 'audio' ||
(mediaType === 'image' &&
layer.assetKind !== 'character' &&
layer.assetKind !== 'character-animation')
) {
return dialog;
}
return resizeGenerationPlaceholderToCharacterAnimationSelection({
...resetFailedGenerationDialog(dialog),
sourceLayerId: layer.id,
generationReferences: [createCanvasLayerReference(layer)],
composerOpen: true,
});
}
if (
dialog?.mode === 'generate' ||
dialog?.mode === 'scene' ||