收紧序列帧去背景来源与绿幕门禁
验证去背景每帧正式资产对象的用户归属、对象路径和来源任务 仅允许编辑器生成的纯绿幕角色动画执行算法去背景 补齐去背景生成 action 白名单但保持不可改造 更新工具栏门禁、回归测试和Spine序列帧技术方案
This commit is contained in:
@@ -26,6 +26,7 @@ export const CANVAS_GENERATION_ACTIONS = [
|
||||
'audio.background-music.generate',
|
||||
'character-animation.generate',
|
||||
'character-animation.convert',
|
||||
'character-animation.remove-background',
|
||||
'image.edit',
|
||||
'ui-design.extract-assets',
|
||||
'image.perfect-pixel',
|
||||
|
||||
@@ -5,6 +5,10 @@ import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasLayer,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
hydrateCanvasGenerationInputsResult,
|
||||
isRemixableCanvasGenerationAction,
|
||||
} from './ImageCanvasGenerationInputsModel';
|
||||
import {
|
||||
applyEditorGenerationPricingConfig,
|
||||
BACKGROUND_MUSIC_MODEL_SUNO,
|
||||
@@ -31,6 +35,7 @@ import {
|
||||
calculateEditorUiDesignPrice,
|
||||
calculateEditorVideoPrice,
|
||||
canOpenRedrawPanel,
|
||||
canRemoveCharacterAnimationBackground,
|
||||
CANVAS_GENERATION_PARAMETER_FALLBACK_WARNING,
|
||||
CHARACTER_ANIMATION_MODEL,
|
||||
createCanvasLayerReference,
|
||||
@@ -65,6 +70,62 @@ import {
|
||||
} from './ImageCanvasGenerationModel';
|
||||
|
||||
describe('ImageCanvasGenerationModel', () => {
|
||||
it('allows sequence background removal only for green generated animations', () => {
|
||||
const layer = {
|
||||
assetKind: 'character-animation',
|
||||
generationInputs: {
|
||||
version: 2 as const,
|
||||
action: 'character-animation.generate' as const,
|
||||
fields: [
|
||||
{ id: 'backgroundColor', title: '背景颜色', value: 'green' as const },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
} as unknown as CanvasLayer;
|
||||
expect(canRemoveCharacterAnimationBackground(layer)).toBe(true);
|
||||
expect(
|
||||
canRemoveCharacterAnimationBackground({
|
||||
...layer,
|
||||
generationInputs: {
|
||||
...layer.generationInputs!,
|
||||
action: 'character-animation.convert',
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
canRemoveCharacterAnimationBackground({
|
||||
...layer,
|
||||
generationInputs: {
|
||||
...layer.generationInputs!,
|
||||
fields: [{ id: 'backgroundColor', title: '背景颜色', value: 'blue' }],
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
canRemoveCharacterAnimationBackground({
|
||||
...layer,
|
||||
generationInputs: undefined,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('hydrates remove-background as a known but non-remixable action', () => {
|
||||
const result = hydrateCanvasGenerationInputsResult({
|
||||
version: 2,
|
||||
action: 'character-animation.remove-background',
|
||||
fields: [],
|
||||
references: [],
|
||||
});
|
||||
|
||||
expect(result.state).toBe('valid');
|
||||
expect(result.inputs?.action).toBe('character-animation.remove-background');
|
||||
expect(
|
||||
isRemixableCanvasGenerationAction(
|
||||
'character-animation.remove-background',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('uses a character animation preview video when a video reference is requested', () => {
|
||||
const reference = createCanvasLayerReference(
|
||||
{
|
||||
|
||||
@@ -1042,6 +1042,19 @@ export function buildCharacterAnimationGenerationInputs(
|
||||
};
|
||||
}
|
||||
|
||||
export function canRemoveCharacterAnimationBackground(layer: CanvasLayer) {
|
||||
if (
|
||||
layer.assetKind !== 'character-animation' ||
|
||||
layer.generationInputs?.version !== 2 ||
|
||||
layer.generationInputs.action !== 'character-animation.generate'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return layer.generationInputs.fields.some(
|
||||
(field) => field.id === 'backgroundColor' && field.value === 'green',
|
||||
);
|
||||
}
|
||||
|
||||
export function canSplitCharacterAnimationFrames(layer: CanvasLayer) {
|
||||
const resourceId = resolveRegisteredProjectResourceId(layer.resourceId);
|
||||
return (
|
||||
|
||||
@@ -460,7 +460,10 @@ describe('ImageCanvasSelectedLayerToolbarView', () => {
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'character-animation.generate',
|
||||
fields: [{ id: 'prompt', title: '动作描述', value: '挥手' }],
|
||||
fields: [
|
||||
{ id: 'prompt', title: '动作描述', value: '挥手' },
|
||||
{ id: 'backgroundColor', title: '背景颜色', value: 'green' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
@@ -489,6 +492,40 @@ describe('ImageCanvasSelectedLayerToolbarView', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('hides sequence background removal unless the authoritative background is green', () => {
|
||||
const whiteLayer = createLayer({
|
||||
mediaType: 'image-sequence',
|
||||
assetKind: 'character-animation',
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'character-animation.generate',
|
||||
fields: [
|
||||
{ id: 'prompt', title: '动作描述', value: '挥手' },
|
||||
{ id: 'backgroundColor', title: '背景颜色', value: 'white' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
renderSelectedToolbar({ selectedLayer: whiteLayer });
|
||||
|
||||
expect(screen.queryByRole('button', { name: '去背景' })).toBeNull();
|
||||
|
||||
cleanup();
|
||||
const convertedLayer = createLayer({
|
||||
mediaType: 'image-sequence',
|
||||
assetKind: 'character-animation',
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'character-animation.convert',
|
||||
fields: [{ id: 'backgroundColor', title: '背景颜色', value: 'green' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
renderSelectedToolbar({ selectedLayer: convertedLayer });
|
||||
|
||||
expect(screen.queryByRole('button', { name: '去背景' })).toBeNull();
|
||||
});
|
||||
|
||||
it('disables frame splitting while a character animation resource is pending registration', () => {
|
||||
const props = renderSelectedToolbar({
|
||||
selectedLayer: createLayer({
|
||||
|
||||
@@ -16,6 +16,7 @@ import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
||||
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
canOpenRedrawPanel,
|
||||
canRemoveCharacterAnimationBackground,
|
||||
canSplitCharacterAnimationFrames,
|
||||
isQuickEditSupportedLayer,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
@@ -67,6 +68,8 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
return null;
|
||||
}
|
||||
const canRedraw = canOpenRedrawPanel(selectedLayer);
|
||||
const canRemoveCharacterAnimation =
|
||||
canRemoveCharacterAnimationBackground(selectedLayer);
|
||||
const isCharacterAnimationResourcePending =
|
||||
selectedLayer.assetKind === 'character-animation' &&
|
||||
!canSplitCharacterAnimationFrames(selectedLayer);
|
||||
@@ -281,35 +284,39 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
) : null}
|
||||
{selectedLayer.assetKind === 'character-animation' ? (
|
||||
<>
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label={
|
||||
isRemovingCharacterAnimationBackground ? '去背景中' : '去背景'
|
||||
}
|
||||
title={
|
||||
isRemovingCharacterAnimationBackground ? '去背景中' : '去背景'
|
||||
}
|
||||
icon={
|
||||
isRemovingCharacterAnimationBackground ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<ImageOff className="h-4 w-4" />
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
isCharacterAnimationResourcePending ||
|
||||
isRemovingCharacterAnimationBackground
|
||||
}
|
||||
aria-busy={
|
||||
isCharacterAnimationResourcePending ||
|
||||
isRemovingCharacterAnimationBackground
|
||||
}
|
||||
onClick={() => onRemoveCharacterAnimationBackground(selectedLayer)}
|
||||
>
|
||||
<span>
|
||||
{isRemovingCharacterAnimationBackground ? '去背景中' : '去背景'}
|
||||
</span>
|
||||
</CanvasChromeButton>
|
||||
{canRemoveCharacterAnimation ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label={
|
||||
isRemovingCharacterAnimationBackground ? '去背景中' : '去背景'
|
||||
}
|
||||
title={
|
||||
isRemovingCharacterAnimationBackground ? '去背景中' : '去背景'
|
||||
}
|
||||
icon={
|
||||
isRemovingCharacterAnimationBackground ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<ImageOff className="h-4 w-4" />
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
isCharacterAnimationResourcePending ||
|
||||
isRemovingCharacterAnimationBackground
|
||||
}
|
||||
aria-busy={
|
||||
isCharacterAnimationResourcePending ||
|
||||
isRemovingCharacterAnimationBackground
|
||||
}
|
||||
onClick={() =>
|
||||
onRemoveCharacterAnimationBackground(selectedLayer)
|
||||
}
|
||||
>
|
||||
<span>
|
||||
{isRemovingCharacterAnimationBackground ? '去背景中' : '去背景'}
|
||||
</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label={characterAnimationSplitLabel}
|
||||
|
||||
Reference in New Issue
Block a user