收紧角色外观参考图容量门禁
限制视频模式角色外观图最多九张,并在前端追加、恢复和提交链路统一收口。 在入队归一化和 durable worker 执行前限制单图、累计字节、解码边长与总像素。 补充九/十张、三十兆字节、累计大小和压缩炸弹像素边界回归。 同步更新 Spine 技术方案与团队决策记录。
This commit is contained in:
@@ -1872,6 +1872,26 @@ describe('ImageCanvasEditorModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('drops character animation appearance references beyond the ninth during hydration', () => {
|
||||
const snapshot = {
|
||||
id: 'dialog-character-animation-over-limit',
|
||||
mode: 'character-animation',
|
||||
prompt: '奔跑',
|
||||
status: 'idle',
|
||||
characterAnimationAppearanceReferences: Array.from(
|
||||
{ length: 10 },
|
||||
(_, index) => ({
|
||||
id: `appearance-${index}`,
|
||||
label: `角色外观${index + 1}`,
|
||||
src: `users/user-1/appearance-${index}.png`,
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
const hydrated = hydrateCanvasGenerationDialog(snapshot);
|
||||
expect(hydrated?.characterAnimationAppearanceReferences).toHaveLength(9);
|
||||
});
|
||||
|
||||
it('keeps the operation ledger out of the layout and restores it from the local ledger', () => {
|
||||
const dialogId = 'dialog-perfect-pixel-round-trip';
|
||||
const operation = buildPerfectPixelOperation(dialogId);
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
EditorAssetFolder,
|
||||
PerfectPixelOperationSnapshot,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { CHARACTER_ANIMATION_APPEARANCE_REFERENCE_LIMIT } from './ImageCanvasFileModel';
|
||||
import {
|
||||
hydrateCanvasGenerationInputs,
|
||||
hydrateCanvasGenerationInputsResult,
|
||||
@@ -1282,7 +1283,7 @@ export function hydrateCanvasGenerationDialog(
|
||||
snapshot.characterAnimationAppearanceReferences,
|
||||
resourcesById,
|
||||
currentUserId,
|
||||
),
|
||||
)?.slice(0, CHARACTER_ANIMATION_APPEARANCE_REFERENCE_LIMIT),
|
||||
characterAnimationConversionVideoReference: hydrateCharacterReference(
|
||||
snapshot.characterAnimationConversionVideoReference,
|
||||
resourcesById,
|
||||
|
||||
@@ -25,6 +25,9 @@ export function getEditorUploadAccept(target: string) {
|
||||
return EDITOR_ASSET_UPLOAD_ACCEPT;
|
||||
}
|
||||
|
||||
export const CHARACTER_ANIMATION_APPEARANCE_REFERENCE_LIMIT = 9;
|
||||
export const CHARACTER_ANIMATION_APPEARANCE_TOTAL_MAX_BYTES = 64 * 1024 * 1024;
|
||||
|
||||
export const SEEDANCE_REFERENCE_FILE_LIMITS = {
|
||||
imageBytes: 30 * 1024 * 1024,
|
||||
videoBytes: 50 * 1024 * 1024,
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
SpecFormValues,
|
||||
SpecGenerationType,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { CHARACTER_ANIMATION_APPEARANCE_REFERENCE_LIMIT } from './ImageCanvasFileModel';
|
||||
import {
|
||||
appendLimitedImageReferences,
|
||||
appendLimitedQuickEditReferences,
|
||||
@@ -2404,7 +2405,7 @@ export function appendGenerationReference(
|
||||
characterAnimationAppearanceReferences: [
|
||||
...(dialog.characterAnimationAppearanceReferences ?? []),
|
||||
createCanvasLayerReference(layer),
|
||||
],
|
||||
].slice(0, CHARACTER_ANIMATION_APPEARANCE_REFERENCE_LIMIT),
|
||||
}
|
||||
: {
|
||||
sourceLayerId: layer.id,
|
||||
|
||||
@@ -537,6 +537,39 @@ describe('ImageCanvasUploadModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('caps character animation appearance uploads at nine references', () => {
|
||||
const motionReference = {
|
||||
id: 'motion',
|
||||
label: '动作视频',
|
||||
src: 'motion.mp4',
|
||||
mediaType: 'video' as const,
|
||||
};
|
||||
const existingAppearances = Array.from({ length: 9 }, (_, index) => ({
|
||||
id: `appearance-${index}`,
|
||||
label: `角色外观${index + 1}`,
|
||||
src: `appearance-${index}.png`,
|
||||
mediaType: 'image' as const,
|
||||
}));
|
||||
const nextAppearance = {
|
||||
id: 'appearance-10',
|
||||
label: '第十张角色外观',
|
||||
src: 'appearance-10.png',
|
||||
mediaType: 'image' as const,
|
||||
};
|
||||
|
||||
expect(
|
||||
applyGenerationReferenceUpload({
|
||||
dialog: createDialog({
|
||||
mode: 'character-animation',
|
||||
generationReferences: [motionReference],
|
||||
characterAnimationAppearanceReferences: existingAppearances,
|
||||
}),
|
||||
target: 'video-reference-image',
|
||||
references: [nextAppearance],
|
||||
})?.characterAnimationAppearanceReferences,
|
||||
).toEqual(existingAppearances);
|
||||
});
|
||||
|
||||
it('resizes a same-ratio character animation placeholder for an uploaded reference', () => {
|
||||
const dialog = createDialog({
|
||||
mode: 'character-animation',
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
GenerateDialogState,
|
||||
QuickEditPanelState,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { CHARACTER_ANIMATION_APPEARANCE_REFERENCE_LIMIT } from './ImageCanvasFileModel';
|
||||
import {
|
||||
appendLimitedImageReferences,
|
||||
QUICK_EDIT_REFERENCE_LIMIT,
|
||||
@@ -279,7 +280,7 @@ export function applyGenerationReferenceUpload({
|
||||
characterAnimationAppearanceReferences: [
|
||||
...(dialog.characterAnimationAppearanceReferences ?? []),
|
||||
firstReference,
|
||||
],
|
||||
].slice(0, CHARACTER_ANIMATION_APPEARANCE_REFERENCE_LIMIT),
|
||||
composerOpen: true,
|
||||
}
|
||||
: {
|
||||
|
||||
@@ -51,6 +51,10 @@ import type {
|
||||
QuickEditPanelState,
|
||||
SidebarPanel,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
CHARACTER_ANIMATION_APPEARANCE_REFERENCE_LIMIT,
|
||||
CHARACTER_ANIMATION_APPEARANCE_TOTAL_MAX_BYTES,
|
||||
} from './ImageCanvasFileModel';
|
||||
import {
|
||||
applyImageEditResultToSourceLayer,
|
||||
createAudioResultLayer,
|
||||
@@ -2703,6 +2707,23 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
const selectedReference = characterAnimationPanel.references?.[0];
|
||||
const appearanceReferences =
|
||||
characterAnimationPanel.appearanceReferences ?? [];
|
||||
if (
|
||||
appearanceReferences.length >
|
||||
CHARACTER_ANIMATION_APPEARANCE_REFERENCE_LIMIT
|
||||
) {
|
||||
throw new Error(
|
||||
`角色外观图最多支持${CHARACTER_ANIMATION_APPEARANCE_REFERENCE_LIMIT}张`,
|
||||
);
|
||||
}
|
||||
const knownAppearanceBytes = appearanceReferences.reduce(
|
||||
(total, reference) => total + (reference.sizeBytes ?? 0),
|
||||
0,
|
||||
);
|
||||
if (
|
||||
knownAppearanceBytes > CHARACTER_ANIMATION_APPEARANCE_TOTAL_MAX_BYTES
|
||||
) {
|
||||
throw new Error('角色外观图总大小不能超过64MB');
|
||||
}
|
||||
const referenceMediaType =
|
||||
selectedReference?.mediaType === 'video' ? 'video' : 'image';
|
||||
const stableReferenceSrc = selectedReference
|
||||
|
||||
Reference in New Issue
Block a user