合并最新主分支
合并 origin/master 的 SFX 音效信息页元数据修复。
This commit is contained in:
@@ -1218,6 +1218,35 @@ describe('ImageCanvasEditorModel', () => {
|
||||
generationInputsOrNull({ fields: [], references: [], soundEffect }),
|
||||
).toEqual({ fields: [], references: [], soundEffect });
|
||||
|
||||
// 中文注释:自动时长的 requestedDurationSeconds 是「没有请求时长」,显式 null 与整个键
|
||||
// 缺失同义。服务端读边界的内联媒体脱敏曾把这个 null 连键删掉,只认 null 会让整份元数据
|
||||
// 被判非法,音频信息页连 fields 一起空掉。
|
||||
const autoSoundEffect = {
|
||||
...soundEffect,
|
||||
durationMode: 'auto' as const,
|
||||
requestedDurationSeconds: null,
|
||||
};
|
||||
const autoSoundEffectWithoutRequestedDuration: Record<string, unknown> = {
|
||||
...autoSoundEffect,
|
||||
};
|
||||
delete autoSoundEffectWithoutRequestedDuration.requestedDurationSeconds;
|
||||
for (const validAutoSoundEffect of [
|
||||
autoSoundEffect,
|
||||
autoSoundEffectWithoutRequestedDuration,
|
||||
]) {
|
||||
expect(
|
||||
generationInputsOrNull({
|
||||
fields: [{ title: '用户描述', value: '金币落地' }],
|
||||
references: [],
|
||||
soundEffect: validAutoSoundEffect,
|
||||
}),
|
||||
).toEqual({
|
||||
fields: [{ title: '用户描述', value: '金币落地' }],
|
||||
references: [],
|
||||
soundEffect: autoSoundEffect,
|
||||
});
|
||||
}
|
||||
|
||||
for (const invalidSoundEffect of [
|
||||
{ ...soundEffect, schemaVersion: 1 },
|
||||
{ ...soundEffect, model: 'audio1.0' },
|
||||
|
||||
@@ -135,7 +135,13 @@ function soundEffectGenerationMetadataOrNull(
|
||||
}
|
||||
let requestedDurationSeconds: number | null;
|
||||
if (value.durationMode === 'auto') {
|
||||
if (value.requestedDurationSeconds !== null) {
|
||||
// 中文注释:自动时长的契约值是「没有请求时长」,显式 null 与整个键缺失同义,两种形态都要
|
||||
// 接受。服务端读边界的内联媒体脱敏曾把这个 null 连键删掉,只认 null 会让整份 soundEffect
|
||||
// 元数据被判非法,音频信息页连带 fields 一起退化成空。
|
||||
if (
|
||||
value.requestedDurationSeconds !== null &&
|
||||
value.requestedDurationSeconds !== undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
requestedDurationSeconds = null;
|
||||
|
||||
@@ -352,6 +352,125 @@ describe('ImageCanvasMetadataModalView', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the full sound effect task id when SFX metadata is unavailable', () => {
|
||||
render(
|
||||
<ImageCanvasMetadataModalView
|
||||
layer={createLayer({
|
||||
title: '金币音效',
|
||||
src: '/generated-character-drafts/editor-audios/coin.mp3',
|
||||
mediaType: 'audio',
|
||||
assetKind: 'sound-effect',
|
||||
originalWidth: 420,
|
||||
originalHeight: 120,
|
||||
model: 'eleven_text_to_sound_v2',
|
||||
taskId: 'task-sfx-v2-1234-abcd',
|
||||
generationInputs: {
|
||||
fields: [{ title: '用户描述', value: '金币落地' }],
|
||||
references: [],
|
||||
},
|
||||
})}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// 中文注释:音效 taskId 是 operation id 形态,「取最后一段数字」会截出 1234 这种误导值。
|
||||
// Task 这一栏不能因为 soundEffect 元数据缺失就降级成假值。
|
||||
const dialog = screen.getByRole('dialog', { name: '音频信息' });
|
||||
expect(within(dialog).getByText('task-sfx-v2-1234-abcd')).toBeTruthy();
|
||||
expect(within(dialog).queryByText('1234')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the task id display tied to the generating model across asset tag overrides', () => {
|
||||
// 中文注释:展示口径由「谁生成了这个 taskId」决定,也就是 model。素材标签只改
|
||||
// assetKindOverride,不该影响它:背景音乐被改标成「音效」不能换用完整 id,音效被改标成
|
||||
// 「背景音乐」更不能退回截断假值。
|
||||
const { unmount } = render(
|
||||
<ImageCanvasMetadataModalView
|
||||
layer={createLayer({
|
||||
title: '游戏背景音乐',
|
||||
src: '/generated-character-drafts/editor-audios/music.wav',
|
||||
mediaType: 'audio',
|
||||
resourceAssetKind: 'background-music',
|
||||
assetKindOverride: 'sound-effect',
|
||||
assetKind: 'sound-effect',
|
||||
originalWidth: 420,
|
||||
originalHeight: 120,
|
||||
model: 'suno',
|
||||
taskId: 'audio-task-75',
|
||||
generationInputs: {
|
||||
fields: [{ title: '音乐描述', value: '紧张的地下城循环音乐' }],
|
||||
references: [],
|
||||
},
|
||||
})}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const musicDialog = screen.getByRole('dialog', { name: '音频信息' });
|
||||
expect(within(musicDialog).getByText('75')).toBeTruthy();
|
||||
expect(within(musicDialog).queryByText('audio-task-75')).toBeNull();
|
||||
unmount();
|
||||
|
||||
render(
|
||||
<ImageCanvasMetadataModalView
|
||||
layer={createLayer({
|
||||
title: '金币音效',
|
||||
src: '/generated-character-drafts/editor-audios/coin.mp3',
|
||||
mediaType: 'audio',
|
||||
resourceAssetKind: 'sound-effect',
|
||||
assetKindOverride: 'background-music',
|
||||
assetKind: 'background-music',
|
||||
originalWidth: 420,
|
||||
originalHeight: 120,
|
||||
model: 'eleven_text_to_sound_v2',
|
||||
taskId: 'task-sfx-v2-1234-abcd',
|
||||
generationInputs: {
|
||||
fields: [{ title: '用户描述', value: '金币落地' }],
|
||||
references: [],
|
||||
},
|
||||
})}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const soundEffectDialog = screen.getByRole('dialog', { name: '音频信息' });
|
||||
expect(
|
||||
within(soundEffectDialog).getByText('task-sfx-v2-1234-abcd'),
|
||||
).toBeTruthy();
|
||||
expect(within(soundEffectDialog).queryByText('1234')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the full sound effect task id when the resource kind is unresolvable', () => {
|
||||
// 中文注释:legacy 布局快照没有 assetKindOverride 键,水合时旧的 assetKind 会被当成
|
||||
// override;资源行又缺 assetKind(或整个 resource 查不到)时 resourceAssetKind 是 null。
|
||||
// 这正是元数据缺失最常伴随的形态,判据不能依赖资源分类,否则又退回截断假值。
|
||||
render(
|
||||
<ImageCanvasMetadataModalView
|
||||
layer={createLayer({
|
||||
title: '金币音效',
|
||||
src: '/generated-character-drafts/editor-audios/coin.mp3',
|
||||
mediaType: 'audio',
|
||||
resourceAssetKind: null,
|
||||
assetKindOverride: 'sound-effect',
|
||||
assetKind: 'sound-effect',
|
||||
originalWidth: 420,
|
||||
originalHeight: 120,
|
||||
model: 'eleven_text_to_sound_v2',
|
||||
taskId: 'task-sfx-v2-1234-abcd',
|
||||
generationInputs: {
|
||||
fields: [{ title: '用户描述', value: '金币落地' }],
|
||||
references: [],
|
||||
},
|
||||
})}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '音频信息' });
|
||||
expect(within(dialog).getByText('task-sfx-v2-1234-abcd')).toBeTruthy();
|
||||
expect(within(dialog).queryByText('1234')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not render a dialog when no layer is selected', () => {
|
||||
render(<ImageCanvasMetadataModalView layer={null} onClose={vi.fn()} />);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EDITOR_SOUND_EFFECT_MODEL } from '../../../packages/shared/src/contracts/editorAudio';
|
||||
import { UnifiedModal } from '../common/UnifiedModal';
|
||||
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
import { formatTaskIdForDisplay } from './ImageCanvasExportModel';
|
||||
@@ -105,7 +106,19 @@ export function ImageCanvasMetadataModalView({
|
||||
) : null}
|
||||
<dt>Task</dt>
|
||||
<dd>
|
||||
{layer.generationInputs?.soundEffect?.schemaVersion === 2
|
||||
{/* 中文注释:音效的 taskId 是 operation id 形态,`formatTaskIdForDisplay` 的
|
||||
「取最后一段数字」会把它截成 `0`、`1234` 这类误导值。判据要回答的是「这个
|
||||
taskId 由谁生成」,而不是「这个图层被归成哪一类」,所以落在 model 上:它是
|
||||
资源行权威字段(registered 图层的 serializeLayer 不回写它),改「素材标签」
|
||||
不影响它,水合时 resolveHydratedLayerModel 也优先取资源行的值;后端
|
||||
canonicalize_editor_sound_effect_model 只接受 eleven_text_to_sound_v2 一个取值。
|
||||
用 assetKind / resourceAssetKind 判都踩过坑:前者是 assetKindOverride ??
|
||||
resourceAssetKind,用户改一次标签就翻转;后者在 legacy 布局(快照没有
|
||||
assetKindOverride 键,旧 assetKind 被当成 override)和资源行查不到时都是 null。
|
||||
soundEffect 那一支只是补充——它和生成 action 一样长在 generationInputs 上,
|
||||
元数据整块缺失时会一并消失,而那正是这里要兜住的场景。 */}
|
||||
{layer.model === EDITOR_SOUND_EFFECT_MODEL ||
|
||||
layer.generationInputs?.soundEffect?.schemaVersion === 2
|
||||
? layer.taskId || '-'
|
||||
: formatTaskIdForDisplay(layer.taskId)}
|
||||
</dd>
|
||||
|
||||
Reference in New Issue
Block a user