Spine序列帧动画多模态生成
This commit is contained in:
@@ -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' ||
|
||||
|
||||
Reference in New Issue
Block a user