停止向用户泄露抠图模型 (#120)
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/120 Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
This commit was merged in pull request #120.
This commit is contained in:
@@ -171,7 +171,6 @@ function getAssetHaystack(asset: EditorAssetSnapshot) {
|
||||
asset.prompt,
|
||||
asset.actualPrompt,
|
||||
asset.model,
|
||||
asset.provider,
|
||||
asset.sourceType,
|
||||
asset.assetKind,
|
||||
...(asset.generationInputs?.fields ?? []).flatMap((field) => [
|
||||
@@ -246,7 +245,6 @@ function projectResourceToShowcaseAsset(
|
||||
prompt: resource.prompt ?? null,
|
||||
actualPrompt: resource.actualPrompt ?? null,
|
||||
model: resource.model ?? null,
|
||||
provider: resource.provider ?? null,
|
||||
taskId: resource.taskId ?? null,
|
||||
durationSeconds: resource.durationSeconds,
|
||||
sourceResourceId: resource.sourceResourceId,
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
filterGroupedAssetsBySearch,
|
||||
getSelectableAssets,
|
||||
groupAssetsByFolder,
|
||||
layerMatchesSearch,
|
||||
layerStartsWithSearch,
|
||||
moveAssetMarqueeToPointer,
|
||||
moveAssetToFolderLocally,
|
||||
removeAssetById,
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
toggleAssetSelection,
|
||||
} from './ImageCanvasAssetLibraryModel';
|
||||
import type {
|
||||
CanvasLayer,
|
||||
EditorAsset,
|
||||
EditorAssetFolder,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
@@ -60,6 +63,24 @@ function createAsset(overrides: Partial<EditorAsset> = {}): EditorAsset {
|
||||
};
|
||||
}
|
||||
|
||||
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||
return {
|
||||
id: 'layer-a',
|
||||
resourceId: 'resource-a',
|
||||
title: '图层A',
|
||||
src: 'data:image/png;base64,YQ==',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 320,
|
||||
height: 240,
|
||||
originalWidth: 320,
|
||||
originalHeight: 240,
|
||||
zIndex: 1,
|
||||
sourceType: 'generated',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('ImageCanvasAssetLibraryModel', () => {
|
||||
it('groups assets by folder and resolves selectable uploaded assets', () => {
|
||||
const folders = [
|
||||
@@ -184,6 +205,60 @@ describe('ImageCanvasAssetLibraryModel', () => {
|
||||
).toEqual([{ id: 'folder-uploaded', assets: ['asset-b', 'asset-c'] }]);
|
||||
});
|
||||
|
||||
it('excludes internal processing models and providers from asset and layer search without deleting metadata', () => {
|
||||
const internalAsset = createAsset({
|
||||
id: 'asset-internal',
|
||||
label: '透明角色',
|
||||
sourceType: 'generated',
|
||||
model: 'BgFilter complex',
|
||||
provider: 'BgFilter',
|
||||
});
|
||||
const visibleAsset = createAsset({
|
||||
id: 'asset-visible',
|
||||
label: '生成角色',
|
||||
sourceType: 'generated',
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
});
|
||||
const groupedAssets = groupAssetsByFolder(
|
||||
[createFolder()],
|
||||
[internalAsset, visibleAsset],
|
||||
);
|
||||
const internalLayer = createLayer({
|
||||
model: 'segment-common-image',
|
||||
provider: 'Aliyun Matting',
|
||||
});
|
||||
const visibleLayer = createLayer({
|
||||
id: 'layer-visible',
|
||||
model: 'audio1.0',
|
||||
provider: 'VectorEngine',
|
||||
});
|
||||
|
||||
expect(filterGroupedAssetsBySearch(groupedAssets, 'bgfilter')).toEqual([]);
|
||||
expect(filterGroupedAssetsBySearch(groupedAssets, 'vectorengine')).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(
|
||||
filterGroupedAssetsBySearch(groupedAssets, 'gpt-image-2')[0]?.assets.map(
|
||||
(asset) => asset.id,
|
||||
),
|
||||
).toEqual(['asset-visible']);
|
||||
expect(layerMatchesSearch(internalLayer, 'segment-common-image')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(layerStartsWithSearch(internalLayer, 'aliyun')).toBe(false);
|
||||
expect(layerMatchesSearch(visibleLayer, 'audio1.0')).toBe(true);
|
||||
expect(layerMatchesSearch(visibleLayer, 'vectorengine')).toBe(false);
|
||||
expect(internalAsset).toMatchObject({
|
||||
model: 'BgFilter complex',
|
||||
provider: 'BgFilter',
|
||||
});
|
||||
expect(internalLayer).toMatchObject({
|
||||
model: 'segment-common-image',
|
||||
provider: 'Aliyun Matting',
|
||||
});
|
||||
});
|
||||
|
||||
it('renames assets, toggles folders and creates local folders', () => {
|
||||
expect(renameAssetById([createAsset()], 'asset-a', '新名字')[0]).toMatchObject(
|
||||
{ label: '新名字' },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isEditorInternalProcessingModel } from './ImageCanvasEditorModel';
|
||||
import type {
|
||||
AssetMarqueeState,
|
||||
CanvasLayer,
|
||||
@@ -204,13 +205,19 @@ function startsWithSearchText(query: string, values: Array<string | null | undef
|
||||
return values.some((value) => value?.trim().toLowerCase().startsWith(query));
|
||||
}
|
||||
|
||||
function resolveSearchableEditorModel(model: string | null | undefined) {
|
||||
const normalizedModel = model?.trim();
|
||||
return normalizedModel && !isEditorInternalProcessingModel(normalizedModel)
|
||||
? normalizedModel
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function buildAssetSearchValues(asset: EditorAsset) {
|
||||
return [
|
||||
asset.label,
|
||||
asset.prompt,
|
||||
asset.actualPrompt,
|
||||
asset.model,
|
||||
asset.provider,
|
||||
resolveSearchableEditorModel(asset.model),
|
||||
asset.taskId,
|
||||
asset.assetKind,
|
||||
asset.mediaType,
|
||||
@@ -222,8 +229,7 @@ function buildLayerSearchValues(layer: CanvasLayer) {
|
||||
layer.title,
|
||||
layer.prompt,
|
||||
layer.actualPrompt,
|
||||
layer.model,
|
||||
layer.provider,
|
||||
resolveSearchableEditorModel(layer.model),
|
||||
layer.taskId,
|
||||
layer.assetKind,
|
||||
layer.mediaType,
|
||||
|
||||
@@ -282,6 +282,8 @@ describe('ImageCanvasEditorModel', () => {
|
||||
zIndex: 9,
|
||||
sourceType: 'generated',
|
||||
objectKey: 'generated/object.png',
|
||||
model: 'birefnet',
|
||||
sourceResourceId: 'resource-provider-source',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '骑士' }],
|
||||
@@ -307,6 +309,15 @@ describe('ImageCanvasEditorModel', () => {
|
||||
fields: [{ title: '角色设定', value: '骑士' }],
|
||||
references: [],
|
||||
},
|
||||
model: 'birefnet',
|
||||
sourceResourceId: 'resource-provider-source',
|
||||
},
|
||||
],
|
||||
[
|
||||
'resource-provider-source',
|
||||
{
|
||||
imageSrc: '/read/provider-source.png',
|
||||
model: 'gpt-image-2',
|
||||
},
|
||||
],
|
||||
]),
|
||||
@@ -318,11 +329,49 @@ describe('ImageCanvasEditorModel', () => {
|
||||
sourceType: 'generated',
|
||||
assetKind: 'character',
|
||||
objectKey: 'generated/object.png',
|
||||
model: 'gpt-image-2',
|
||||
locked: true,
|
||||
});
|
||||
expect(hydrated?.generationInputs?.fields[0]?.value).toBe('骑士');
|
||||
});
|
||||
|
||||
it('recovers the normal source model after the owner payload removes an internal model', () => {
|
||||
const hydrated = hydrateLayer(
|
||||
{
|
||||
layerId: 'layer-derived',
|
||||
resourceId: 'resource-derived',
|
||||
title: '去背景结果',
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 1024,
|
||||
height: 768,
|
||||
originalWidth: 1024,
|
||||
originalHeight: 768,
|
||||
zIndex: 9,
|
||||
sourceType: 'generated',
|
||||
sourceResourceId: 'resource-source',
|
||||
},
|
||||
new Map([
|
||||
[
|
||||
'resource-derived',
|
||||
{
|
||||
imageSrc: '/read/derived.png',
|
||||
sourceResourceId: 'resource-source',
|
||||
},
|
||||
],
|
||||
[
|
||||
'resource-source',
|
||||
{
|
||||
imageSrc: '/read/source.png',
|
||||
model: 'gpt-image-2',
|
||||
},
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
expect(hydrated?.model).toBe('gpt-image-2');
|
||||
});
|
||||
|
||||
it('hydrates object metadata from project resources when the saved layer is lean', () => {
|
||||
const hydrated = hydrateLayer(
|
||||
{
|
||||
|
||||
@@ -43,6 +43,24 @@ export const MINIMAP_SIZE = { width: 132, height: 84 };
|
||||
export const MINIMAP_PADDING = 8;
|
||||
export const MINIMAP_DRAG_SENSITIVITY = 0.3;
|
||||
export const ASSET_DRAG_MIME_TYPE = 'application/x-genarrative-editor-asset';
|
||||
const INTERNAL_EDITOR_PROCESSING_MODELS = new Set([
|
||||
'anime-seg',
|
||||
'bgfilter complex',
|
||||
'birefnet',
|
||||
'connected-components',
|
||||
'screen-color-keying',
|
||||
'segment-common-image',
|
||||
]);
|
||||
|
||||
export function isEditorInternalProcessingModel(
|
||||
model: string | null | undefined,
|
||||
) {
|
||||
const normalizedModel = model?.trim().toLowerCase();
|
||||
return Boolean(
|
||||
normalizedModel && INTERNAL_EDITOR_PROCESSING_MODELS.has(normalizedModel),
|
||||
);
|
||||
}
|
||||
|
||||
export const MAX_HISTORY_STEPS = 60;
|
||||
export const CONTEXT_MENU_VIEWPORT_MARGIN = 8;
|
||||
export const CONTEXT_MENU_SIZE = {
|
||||
@@ -734,7 +752,7 @@ export function hydrateLayer(
|
||||
prompt: stringOrNull(snapshot.prompt) ?? stringOrNull(resource?.prompt),
|
||||
actualPrompt:
|
||||
stringOrNull(snapshot.actualPrompt) ?? stringOrNull(resource?.actualPrompt),
|
||||
model: stringOrNull(snapshot.model) ?? stringOrNull(resource?.model),
|
||||
model: resolveHydratedLayerModel(snapshot, resource, resourcesById),
|
||||
provider: stringOrNull(snapshot.provider) ?? stringOrNull(resource?.provider),
|
||||
taskId: stringOrNull(snapshot.taskId) ?? stringOrNull(resource?.taskId),
|
||||
objectKey: stringOrNull(snapshot.objectKey) ?? stringOrNull(resource?.objectKey),
|
||||
@@ -761,6 +779,33 @@ export function hydrateLayer(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveHydratedLayerModel(
|
||||
snapshot: EditorProjectLayerSnapshot,
|
||||
resource: CanvasLayerResourceMetadata | undefined,
|
||||
resourcesById: Map<string, CanvasLayerResourceMetadata>,
|
||||
) {
|
||||
const persistedModel =
|
||||
stringOrNull(snapshot.model) ?? stringOrNull(resource?.model);
|
||||
if (persistedModel && !isEditorInternalProcessingModel(persistedModel)) {
|
||||
return persistedModel;
|
||||
}
|
||||
|
||||
let sourceResourceId =
|
||||
stringOrNull(snapshot.sourceResourceId) ??
|
||||
stringOrNull(resource?.sourceResourceId);
|
||||
const visitedResourceIds = new Set<string>();
|
||||
while (sourceResourceId && !visitedResourceIds.has(sourceResourceId)) {
|
||||
visitedResourceIds.add(sourceResourceId);
|
||||
const sourceResource = resourcesById.get(sourceResourceId);
|
||||
const sourceModel = stringOrNull(sourceResource?.model);
|
||||
if (sourceModel && !isEditorInternalProcessingModel(sourceModel)) {
|
||||
return sourceModel;
|
||||
}
|
||||
sourceResourceId = stringOrNull(sourceResource?.sourceResourceId);
|
||||
}
|
||||
return persistedModel;
|
||||
}
|
||||
|
||||
export function mapAssetLibrarySnapshot(
|
||||
library: EditorAssetLibrarySnapshot,
|
||||
): {
|
||||
|
||||
@@ -175,6 +175,7 @@ describe('ImageCanvasExportModel', () => {
|
||||
it('filters built-in prompts from exported visible generation inputs', () => {
|
||||
const metadata = buildLayerExportMetadata(
|
||||
buildLayer({
|
||||
model: 'gpt-image-2',
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{ title: '生成提示词', value: 'AI 生成图片' },
|
||||
@@ -195,6 +196,7 @@ describe('ImageCanvasExportModel', () => {
|
||||
{ title: '用户输入', value: '用户填写的视觉要求' },
|
||||
{ title: '游戏分类', value: '游戏音效' },
|
||||
{ title: 'duration', value: '5秒' },
|
||||
{ title: '处理模型', value: 'birefnet' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
@@ -210,6 +212,7 @@ describe('ImageCanvasExportModel', () => {
|
||||
],
|
||||
references: [],
|
||||
});
|
||||
expect(metadata.visible.model).toBe('gpt-image-2');
|
||||
});
|
||||
|
||||
it('builds a Spine JSON attachment timeline from image sequence frames', () => {
|
||||
|
||||
@@ -8,7 +8,8 @@ import type {
|
||||
import {
|
||||
DEFAULT_ICON_DESCRIPTIONS,
|
||||
formatLayerImageType,
|
||||
getEditorImageModelDisplayName,
|
||||
getEditorLayerModelDisplayName,
|
||||
isEditorUserVisibleGenerationInputField,
|
||||
UI_DESIGN_ASSET_EXTRACTION_PROMPT,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
import { formatCanvasDurationMetric } from './ImageCanvasMediaModel';
|
||||
@@ -543,6 +544,7 @@ function isBuiltInGenerationInputField(field: CanvasGenerationInputField) {
|
||||
const builtInValues =
|
||||
BUILT_IN_GENERATION_INPUT_VALUES_BY_TITLE.get(normalizedTitle);
|
||||
return (
|
||||
!isEditorUserVisibleGenerationInputField(field) ||
|
||||
BUILT_IN_GENERATION_INPUT_TITLES.has(normalizedTitle) ||
|
||||
normalizedValue === UI_DESIGN_ASSET_EXTRACTION_PROMPT ||
|
||||
Boolean(builtInValues?.has(normalizedValue))
|
||||
@@ -572,7 +574,7 @@ export function buildLayerVisibleExportMetadata(layer: CanvasLayer) {
|
||||
const base = {
|
||||
type: formatLayerImageType(layer),
|
||||
generationInputs: buildVisibleGenerationInputs(layer),
|
||||
model: layer.model ? getEditorImageModelDisplayName(layer.model) : '-',
|
||||
model: getEditorLayerModelDisplayName(layer.model),
|
||||
task: formatTaskIdForDisplay(layer.taskId),
|
||||
object: layer.objectKey ?? layer.assetObjectId ?? '-',
|
||||
};
|
||||
|
||||
@@ -341,7 +341,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
|
||||
prompt: '图标 prompt',
|
||||
actualPrompt: '图标 actual prompt',
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'task-icons',
|
||||
priceMudPoints: 20,
|
||||
},
|
||||
@@ -432,7 +431,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
|
||||
prompt: '图标 prompt',
|
||||
actualPrompt: '图标 actual prompt',
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'task-icons',
|
||||
priceMudPoints: 20,
|
||||
},
|
||||
@@ -464,7 +462,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
|
||||
prompt: '金币掉落叮当声',
|
||||
actualPrompt: '金币掉落叮当声',
|
||||
model: 'audio1.0',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'sound-task-1',
|
||||
priceMudPoints: 10,
|
||||
audioKind: 'sound-effect',
|
||||
@@ -525,7 +522,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
|
||||
sourceType: 'generated',
|
||||
prompt: '金币掉落叮当声',
|
||||
model: 'audio1.0',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'sound-task-1',
|
||||
objectKey: 'generated-character-drafts/editor-audios/sfx.mp3',
|
||||
assetObjectId: 'assetobj-audio-1',
|
||||
@@ -535,6 +531,7 @@ describe('ImageCanvasGenerationLayerModel', () => {
|
||||
assetId: 'asset-audio-bff',
|
||||
}),
|
||||
});
|
||||
expect(layer.provider).toBeUndefined();
|
||||
});
|
||||
|
||||
it('creates video result layers with OSS asset identity', () => {
|
||||
@@ -549,7 +546,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
|
||||
prompt: '开场动画',
|
||||
actualPrompt: '开场动画',
|
||||
model: 'seedance2.0-fast',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'video-task-1',
|
||||
priceMudPoints: 40,
|
||||
durationSeconds: 5,
|
||||
@@ -586,6 +582,7 @@ describe('ImageCanvasGenerationLayerModel', () => {
|
||||
durationSeconds: 5,
|
||||
});
|
||||
expect(layer.generatedAssetSnapshot?.assetId).toBe('asset-video-bff');
|
||||
expect(layer.provider).toBeUndefined();
|
||||
});
|
||||
|
||||
it('creates character animation result layers as playable image sequences', () => {
|
||||
@@ -653,11 +650,11 @@ describe('ImageCanvasGenerationLayerModel', () => {
|
||||
prompt: '待机动作',
|
||||
actualPrompt: '待机动作',
|
||||
model: 'seedance2.0-fast',
|
||||
provider: 'ark',
|
||||
taskId: 'character-animation-task-1',
|
||||
assetKind: 'character-animation',
|
||||
durationSeconds: 4,
|
||||
});
|
||||
expect(layer?.generationInputs?.fields[0]?.value).toBe('待机动作');
|
||||
expect(layer?.provider).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,7 +114,6 @@ function applyGeneratedMetadata(
|
||||
prompt: generated.prompt,
|
||||
actualPrompt: generated.actualPrompt ?? generated.prompt,
|
||||
model: generated.model,
|
||||
provider: generated.provider,
|
||||
taskId: generated.taskId,
|
||||
objectKey: resource?.objectKey ?? generated.objectKey,
|
||||
assetObjectId: resource?.assetObjectId ?? generated.assetObjectId,
|
||||
@@ -333,7 +332,6 @@ export function createIconSpritesheetResultLayers({
|
||||
prompt: generated.prompt,
|
||||
actualPrompt: generated.actualPrompt ?? generated.prompt,
|
||||
model: generated.model,
|
||||
provider: generated.provider,
|
||||
taskId: generated.taskId,
|
||||
assetKind: 'icon-spritesheet',
|
||||
sourceAssetId: spritesheetAsset?.assetId,
|
||||
@@ -382,7 +380,6 @@ export function createIconSpritesheetResultLayers({
|
||||
prompt: generated.prompt,
|
||||
actualPrompt: generated.actualPrompt ?? generated.prompt,
|
||||
model: generated.model,
|
||||
provider: generated.provider,
|
||||
taskId: generated.taskId,
|
||||
assetKind: 'icon',
|
||||
sourceAssetId: asset?.assetId,
|
||||
@@ -451,7 +448,6 @@ export function createVideoResultLayer({
|
||||
prompt: generated.prompt,
|
||||
actualPrompt: generated.actualPrompt ?? generated.prompt,
|
||||
model: generated.model,
|
||||
provider: generated.provider,
|
||||
taskId: generated.taskId,
|
||||
objectKey: resource?.objectKey ?? generated.objectKey,
|
||||
assetObjectId: resource?.assetObjectId ?? generated.assetObjectId,
|
||||
@@ -513,7 +509,6 @@ export function createAudioResultLayer({
|
||||
prompt: generated.prompt,
|
||||
actualPrompt: generated.actualPrompt ?? generated.prompt,
|
||||
model: generated.model,
|
||||
provider: generated.provider,
|
||||
taskId: generated.taskId,
|
||||
objectKey: resource?.objectKey ?? generated.objectKey,
|
||||
assetObjectId: resource?.assetObjectId ?? generated.assetObjectId,
|
||||
@@ -575,7 +570,6 @@ export function createCharacterAnimationResultLayer({
|
||||
prompt: generated.prompt,
|
||||
actualPrompt: generated.prompt,
|
||||
model: generated.model,
|
||||
provider: 'ark',
|
||||
taskId: generated.taskId,
|
||||
assetKind: 'character-animation',
|
||||
durationSeconds: generated.durationSeconds,
|
||||
|
||||
@@ -4,7 +4,10 @@ import type {
|
||||
EditorCharacterAnimationRatio,
|
||||
EditorCharacterAnimationResolution,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import { isGeneratedLayer } from './ImageCanvasEditorModel';
|
||||
import {
|
||||
isEditorInternalProcessingModel,
|
||||
isGeneratedLayer,
|
||||
} from './ImageCanvasEditorModel';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasGenerationInputField,
|
||||
@@ -548,6 +551,21 @@ export function getEditorImageModelDisplayName(
|
||||
);
|
||||
}
|
||||
|
||||
export function isEditorUserVisibleGenerationInputField(
|
||||
field: CanvasGenerationInputField,
|
||||
) {
|
||||
return field.title.trim() !== '处理模型';
|
||||
}
|
||||
|
||||
export function getEditorLayerModelDisplayName(
|
||||
model: string | null | undefined,
|
||||
) {
|
||||
if (!model?.trim() || isEditorInternalProcessingModel(model)) {
|
||||
return '-';
|
||||
}
|
||||
return getEditorImageModelDisplayName(model);
|
||||
}
|
||||
|
||||
export function getEditorGenerationModelDisplayName(
|
||||
model: string | null | undefined,
|
||||
) {
|
||||
|
||||
@@ -58,6 +58,7 @@ describe('ImageCanvasMetadataModalView', () => {
|
||||
expect(within(dialog).getByText('参考图')).toBeTruthy();
|
||||
expect(within(dialog).getByText('角色立绘')).toBeTruthy();
|
||||
expect(within(dialog).getByText('项目资源 · resource-reference')).toBeTruthy();
|
||||
expect(within(dialog).getByText('Model')).toBeTruthy();
|
||||
expect(within(dialog).getByText('gpt-image-2')).toBeTruthy();
|
||||
expect(within(dialog).getByText('1024 x 768 px')).toBeTruthy();
|
||||
expect(within(dialog).queryByText('Provider')).toBeNull();
|
||||
@@ -66,6 +67,54 @@ describe('ImageCanvasMetadataModalView', () => {
|
||||
expect(within(dialog).getByText('123')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps the generation model while hiding internal processing models', () => {
|
||||
const generationInputsWithInternalMattingMetadata = {
|
||||
fields: [
|
||||
{ title: '处理阶段', value: '角色抠图' },
|
||||
{ title: '处理模型', value: 'birefnet' },
|
||||
],
|
||||
references: [],
|
||||
mattingProvider: 'Aliyun Matting',
|
||||
mattingModel: 'segment-common-image',
|
||||
};
|
||||
const { rerender } = render(
|
||||
<ImageCanvasMetadataModalView
|
||||
layer={createLayer({
|
||||
model: 'gpt-image-2',
|
||||
generationInputs: generationInputsWithInternalMattingMetadata,
|
||||
})}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '图片信息' });
|
||||
|
||||
expect(within(dialog).getByText('处理阶段')).toBeTruthy();
|
||||
expect(within(dialog).getByText('角色抠图')).toBeTruthy();
|
||||
expect(within(dialog).queryByText('处理模型')).toBeNull();
|
||||
expect(within(dialog).getByText('Model')).toBeTruthy();
|
||||
expect(within(dialog).getByText('gpt-image-2')).toBeTruthy();
|
||||
expect(within(dialog).queryByText('birefnet')).toBeNull();
|
||||
expect(within(dialog).queryByText('Aliyun Matting')).toBeNull();
|
||||
expect(within(dialog).queryByText('segment-common-image')).toBeNull();
|
||||
|
||||
rerender(
|
||||
<ImageCanvasMetadataModalView
|
||||
layer={createLayer({
|
||||
model: 'birefnet',
|
||||
generationInputs: {
|
||||
fields: [{ title: '处理阶段', value: '角色抠图' }],
|
||||
references: [],
|
||||
},
|
||||
})}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(within(dialog).getByText('Model')).toBeTruthy();
|
||||
expect(within(dialog).queryByText('birefnet')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders nanobanana2 instead of the raw Gemini image model id', () => {
|
||||
render(
|
||||
<ImageCanvasMetadataModalView
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
import { formatTaskIdForDisplay } from './ImageCanvasExportModel';
|
||||
import {
|
||||
formatLayerImageType,
|
||||
getEditorImageModelDisplayName,
|
||||
getEditorLayerModelDisplayName,
|
||||
isEditorUserVisibleGenerationInputField,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
import { formatCanvasDurationMetric } from './ImageCanvasMediaModel';
|
||||
|
||||
@@ -27,6 +28,11 @@ export function ImageCanvasMetadataModalView({
|
||||
layer,
|
||||
onClose,
|
||||
}: ImageCanvasMetadataModalViewProps) {
|
||||
const visibleGenerationInputFields =
|
||||
layer?.generationInputs?.fields.filter(
|
||||
isEditorUserVisibleGenerationInputField,
|
||||
) ?? [];
|
||||
const generationInputReferences = layer?.generationInputs?.references ?? [];
|
||||
const mediaLabel =
|
||||
layer?.mediaType === 'audio'
|
||||
? '音频'
|
||||
@@ -52,10 +58,10 @@ export function ImageCanvasMetadataModalView({
|
||||
<dd>{formatLayerImageType(layer)}</dd>
|
||||
<dt>生成输入</dt>
|
||||
<dd className="image-canvas-editor__metadata-inputs">
|
||||
{layer.generationInputs?.fields.length ||
|
||||
layer.generationInputs?.references.length ? (
|
||||
{visibleGenerationInputFields.length ||
|
||||
generationInputReferences.length ? (
|
||||
<>
|
||||
{layer.generationInputs.fields.map((field) => (
|
||||
{visibleGenerationInputFields.map((field) => (
|
||||
<div
|
||||
key={`${field.title}-${field.value}`}
|
||||
className="image-canvas-editor__metadata-input-field"
|
||||
@@ -66,9 +72,9 @@ export function ImageCanvasMetadataModalView({
|
||||
<span>{field.value}</span>
|
||||
</div>
|
||||
))}
|
||||
{layer.generationInputs.references.length ? (
|
||||
{generationInputReferences.length ? (
|
||||
<div className="image-canvas-editor__metadata-reference-list">
|
||||
{layer.generationInputs.references.map((reference) => (
|
||||
{generationInputReferences.map((reference) => (
|
||||
<div
|
||||
key={`${reference.title}-${reference.label}-${reference.refType}-${reference.refId}`}
|
||||
className="image-canvas-editor__metadata-reference-card"
|
||||
@@ -93,7 +99,7 @@ export function ImageCanvasMetadataModalView({
|
||||
)}
|
||||
</dd>
|
||||
<dt>Model</dt>
|
||||
<dd>{layer.model ? getEditorImageModelDisplayName(layer.model) : '-'}</dd>
|
||||
<dd>{getEditorLayerModelDisplayName(layer.model)}</dd>
|
||||
{layer.mediaType === 'audio' ? (
|
||||
<>
|
||||
<dt>时长</dt>
|
||||
|
||||
@@ -269,6 +269,52 @@ describe('ImageCanvasSidebarView', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('does not expose internal models or providers through asset search', () => {
|
||||
const folder = createFolder();
|
||||
const internalAsset = createAsset({
|
||||
id: 'asset-internal',
|
||||
label: '透明角色',
|
||||
sourceType: 'generated',
|
||||
model: 'BgFilter complex',
|
||||
provider: 'BgFilter',
|
||||
});
|
||||
const visibleAsset = createAsset({
|
||||
id: 'asset-visible',
|
||||
label: '生成角色',
|
||||
sourceType: 'generated',
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
});
|
||||
render(
|
||||
<ImageCanvasSidebarView
|
||||
{...createSidebarProps({
|
||||
assets: [internalAsset, visibleAsset],
|
||||
groupedAssets: [
|
||||
{ ...folder, assets: [internalAsset, visibleAsset] },
|
||||
],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
|
||||
const search = within(sidebar).getByLabelText('搜索素材');
|
||||
|
||||
fireEvent.change(search, { target: { value: 'BgFilter' } });
|
||||
expect(
|
||||
within(sidebar).queryByRole('button', { name: '添加透明角色' }),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.change(search, { target: { value: 'VectorEngine' } });
|
||||
expect(
|
||||
within(sidebar).queryByRole('button', { name: '添加生成角色' }),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.change(search, { target: { value: 'gpt-image-2' } });
|
||||
expect(
|
||||
within(sidebar).getByRole('button', { name: '添加生成角色' }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the layer panel with grouped and locked layer details', () => {
|
||||
render(
|
||||
<ImageCanvasSidebarView
|
||||
@@ -346,6 +392,56 @@ describe('ImageCanvasSidebarView', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('does not expose internal models or providers through layer search', () => {
|
||||
render(
|
||||
<ImageCanvasSidebarView
|
||||
{...createSidebarProps({
|
||||
activeSidebarPanel: 'layers',
|
||||
layers: [
|
||||
createLayer({
|
||||
id: 'layer-internal',
|
||||
title: '透明角色图层',
|
||||
model: 'segment-common-image',
|
||||
provider: 'Aliyun Matting',
|
||||
zIndex: 2,
|
||||
}),
|
||||
createLayer({
|
||||
id: 'layer-visible',
|
||||
title: '生成角色图层',
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
zIndex: 1,
|
||||
}),
|
||||
],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
|
||||
const search = within(sidebar).getByLabelText('搜索画布图层');
|
||||
|
||||
fireEvent.change(search, { target: { value: 'segment-common-image' } });
|
||||
expect(
|
||||
within(sidebar).queryByRole('button', {
|
||||
name: '选择图层透明角色图层',
|
||||
}),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.change(search, { target: { value: 'VectorEngine' } });
|
||||
expect(
|
||||
within(sidebar).queryByRole('button', {
|
||||
name: '选择图层生成角色图层',
|
||||
}),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.change(search, { target: { value: 'gpt-image-2' } });
|
||||
expect(
|
||||
within(sidebar).getByRole('button', {
|
||||
name: '选择图层生成角色图层',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps sidebar tab switching and repeated-click closing behavior', () => {
|
||||
render(<SidebarTabsHarness />);
|
||||
|
||||
|
||||
@@ -304,7 +304,6 @@ export type EditorImageGenerationResult = {
|
||||
prompt: string;
|
||||
actualPrompt?: string | null;
|
||||
model: string;
|
||||
provider: string;
|
||||
taskId: string;
|
||||
resource?: EditorProjectResourceSnapshot | null;
|
||||
asset?: EditorAssetSnapshot | null;
|
||||
@@ -342,7 +341,6 @@ export type EditorIconSpritesheetGenerationResult = {
|
||||
prompt: string;
|
||||
actualPrompt?: string | null;
|
||||
model: string;
|
||||
provider: string;
|
||||
taskId: string;
|
||||
priceMudPoints: number;
|
||||
spritesheetResource?: EditorProjectResourceSnapshot | null;
|
||||
@@ -465,7 +463,6 @@ export type EditorVideoGenerationResult = {
|
||||
prompt: string;
|
||||
actualPrompt?: string | null;
|
||||
model: string;
|
||||
provider: string;
|
||||
taskId: string;
|
||||
durationSeconds: number;
|
||||
priceMudPoints?: number;
|
||||
@@ -506,7 +503,6 @@ export type EditorAudioGenerationResult = {
|
||||
prompt: string;
|
||||
actualPrompt?: string | null;
|
||||
model: string;
|
||||
provider: string;
|
||||
taskId: string;
|
||||
priceMudPoints: number;
|
||||
audioKind: 'sound-effect' | 'background-music';
|
||||
|
||||
Reference in New Issue
Block a user