1
This commit is contained in:
@@ -7,6 +7,8 @@ import {
|
||||
getMatch3DGeneratedImageAssetSources,
|
||||
getMatch3DGeneratedModelAssetSources,
|
||||
hasMatch3DGeneratedImageAsset,
|
||||
mergeMatch3DGeneratedItemAssetsForRuntime,
|
||||
normalizeMatch3DGeneratedItemAssetsForRuntime,
|
||||
preloadMatch3DGeneratedImageAssets,
|
||||
preloadMatch3DGeneratedModelAssets,
|
||||
readMatch3DGeneratedModelBytes,
|
||||
@@ -263,4 +265,86 @@ describe('match3dGeneratedModelCache', () => {
|
||||
'views%2Fview-01.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('作品级背景音乐会归一化到首个抓大鹅素材', () => {
|
||||
const assets = normalizeMatch3DGeneratedItemAssetsForRuntime([
|
||||
{
|
||||
itemId: 'match3d-item-1',
|
||||
itemName: '草莓',
|
||||
imageSrc: '/match3d/strawberry.png',
|
||||
imageObjectKey: null,
|
||||
modelSrc: null,
|
||||
modelObjectKey: null,
|
||||
status: 'image_ready',
|
||||
},
|
||||
{
|
||||
itemId: 'match3d-item-2',
|
||||
itemName: '苹果',
|
||||
imageSrc: '/match3d/apple.png',
|
||||
imageObjectKey: null,
|
||||
modelSrc: null,
|
||||
modelObjectKey: null,
|
||||
status: 'image_ready',
|
||||
backgroundMusicTitle: '果园轻舞',
|
||||
backgroundMusic: {
|
||||
taskId: 'music-task-1',
|
||||
provider: 'vector-engine-suno',
|
||||
assetObjectId: 'asset-music-1',
|
||||
assetKind: 'match3d_background_music',
|
||||
audioSrc: '/generated-match3d-assets/audio/music.mp3',
|
||||
prompt: '',
|
||||
title: '果园轻舞',
|
||||
updatedAt: '2026-05-14T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(assets[0]?.backgroundMusic?.audioSrc).toBe(
|
||||
'/generated-match3d-assets/audio/music.mp3',
|
||||
);
|
||||
expect(assets[0]?.backgroundMusicTitle).toBe('果园轻舞');
|
||||
expect(assets[1]?.backgroundMusic).toBeNull();
|
||||
});
|
||||
|
||||
test('合并 action 草稿和作品详情时保留详情里的背景音乐', () => {
|
||||
const assets = mergeMatch3DGeneratedItemAssetsForRuntime(
|
||||
[
|
||||
{
|
||||
itemId: 'match3d-item-1',
|
||||
itemName: '草莓',
|
||||
imageSrc: '/match3d/strawberry.png',
|
||||
imageObjectKey: null,
|
||||
modelSrc: null,
|
||||
modelObjectKey: null,
|
||||
status: 'image_ready',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
itemId: 'match3d-item-1',
|
||||
itemName: '草莓',
|
||||
imageSrc: '/match3d/strawberry.png',
|
||||
imageObjectKey: null,
|
||||
modelSrc: null,
|
||||
modelObjectKey: null,
|
||||
status: 'image_ready',
|
||||
backgroundMusic: {
|
||||
taskId: 'music-task-1',
|
||||
provider: 'vector-engine-suno',
|
||||
assetObjectId: 'asset-music-1',
|
||||
assetKind: 'match3d_background_music',
|
||||
audioSrc: '/generated-match3d-assets/audio/music.mp3',
|
||||
prompt: '',
|
||||
title: '果园轻舞',
|
||||
updatedAt: '2026-05-14T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
expect(assets).toHaveLength(1);
|
||||
expect(assets[0]?.backgroundMusic?.audioSrc).toBe(
|
||||
'/generated-match3d-assets/audio/music.mp3',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -125,6 +125,160 @@ export function hasMatch3DGeneratedImageAsset(
|
||||
);
|
||||
}
|
||||
|
||||
function findMatch3DBackgroundMusicCarrier(
|
||||
assets: readonly Match3DGeneratedItemAsset[],
|
||||
) {
|
||||
return assets.find((asset) => asset.backgroundMusic?.audioSrc?.trim());
|
||||
}
|
||||
|
||||
function findMatch3DBackgroundMusicMetadataCarrier(
|
||||
assets: readonly Match3DGeneratedItemAsset[],
|
||||
) {
|
||||
return assets.find(
|
||||
(asset) =>
|
||||
asset.backgroundMusicTitle?.trim() ||
|
||||
asset.backgroundMusicStyle?.trim() ||
|
||||
asset.backgroundMusicPrompt?.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 抓大鹅背景音乐当前暂存在 generatedItemAssets 里,但它表达的是作品级音乐。
|
||||
* 归一化到首个素材,避免前端只读首项时把已生成音乐显示成“暂无音乐”。
|
||||
*/
|
||||
export function normalizeMatch3DGeneratedItemAssetsForRuntime(
|
||||
assets: readonly Match3DGeneratedItemAsset[] | null | undefined,
|
||||
) {
|
||||
if (!assets?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const musicCarrier = findMatch3DBackgroundMusicCarrier(assets);
|
||||
const metadataCarrier =
|
||||
musicCarrier ?? findMatch3DBackgroundMusicMetadataCarrier(assets);
|
||||
if (!musicCarrier && !metadataCarrier) {
|
||||
return [...assets];
|
||||
}
|
||||
|
||||
return assets.map((asset, index) => {
|
||||
if (index !== 0) {
|
||||
if (
|
||||
!asset.backgroundMusic &&
|
||||
!asset.backgroundMusicTitle &&
|
||||
!asset.backgroundMusicStyle &&
|
||||
!asset.backgroundMusicPrompt
|
||||
) {
|
||||
return asset;
|
||||
}
|
||||
return {
|
||||
...asset,
|
||||
backgroundMusic: null,
|
||||
backgroundMusicTitle: null,
|
||||
backgroundMusicStyle: null,
|
||||
backgroundMusicPrompt: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...asset,
|
||||
backgroundMusic:
|
||||
asset.backgroundMusic ?? musicCarrier?.backgroundMusic ?? null,
|
||||
backgroundMusicTitle:
|
||||
asset.backgroundMusicTitle ??
|
||||
metadataCarrier?.backgroundMusicTitle ??
|
||||
musicCarrier?.backgroundMusic?.title ??
|
||||
null,
|
||||
backgroundMusicStyle:
|
||||
asset.backgroundMusicStyle ??
|
||||
metadataCarrier?.backgroundMusicStyle ??
|
||||
null,
|
||||
backgroundMusicPrompt:
|
||||
asset.backgroundMusicPrompt ??
|
||||
metadataCarrier?.backgroundMusicPrompt ??
|
||||
musicCarrier?.backgroundMusic?.prompt ??
|
||||
null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function mergeMatch3DGeneratedItemAssetsForRuntime(
|
||||
primaryAssets: readonly Match3DGeneratedItemAsset[] | null | undefined,
|
||||
fallbackAssets: readonly Match3DGeneratedItemAsset[] | null | undefined,
|
||||
) {
|
||||
const primary = primaryAssets ?? [];
|
||||
const fallback = fallbackAssets ?? [];
|
||||
if (primary.length <= 0) {
|
||||
return normalizeMatch3DGeneratedItemAssetsForRuntime(fallback);
|
||||
}
|
||||
if (fallback.length <= 0) {
|
||||
return normalizeMatch3DGeneratedItemAssetsForRuntime(primary);
|
||||
}
|
||||
|
||||
const fallbackById = new Map(fallback.map((asset) => [asset.itemId, asset]));
|
||||
const merged = primary.map((asset) => {
|
||||
const fallbackAsset = fallbackById.get(asset.itemId);
|
||||
if (!fallbackAsset) {
|
||||
return asset;
|
||||
}
|
||||
const hasPrimaryImage = getMatch3DGeneratedImageViewSources(asset).length > 0;
|
||||
const hasPrimaryModel = resolveMatch3DGeneratedModelAssetSource(asset).length > 0;
|
||||
return {
|
||||
...asset,
|
||||
itemName: asset.itemName.trim() || fallbackAsset.itemName,
|
||||
imageSrc: asset.imageSrc?.trim()
|
||||
? asset.imageSrc
|
||||
: (fallbackAsset.imageSrc ?? null),
|
||||
imageObjectKey: asset.imageObjectKey?.trim()
|
||||
? asset.imageObjectKey
|
||||
: (fallbackAsset.imageObjectKey ?? null),
|
||||
imageViews:
|
||||
asset.imageViews && asset.imageViews.length > 0
|
||||
? asset.imageViews
|
||||
: (fallbackAsset.imageViews ?? []),
|
||||
modelSrc: asset.modelSrc?.trim()
|
||||
? asset.modelSrc
|
||||
: (fallbackAsset.modelSrc ?? null),
|
||||
modelObjectKey: asset.modelObjectKey?.trim()
|
||||
? asset.modelObjectKey
|
||||
: (fallbackAsset.modelObjectKey ?? null),
|
||||
modelFileName: asset.modelFileName?.trim()
|
||||
? asset.modelFileName
|
||||
: (fallbackAsset.modelFileName ?? null),
|
||||
taskUuid: asset.taskUuid?.trim()
|
||||
? asset.taskUuid
|
||||
: (fallbackAsset.taskUuid ?? null),
|
||||
subscriptionKey: asset.subscriptionKey?.trim()
|
||||
? asset.subscriptionKey
|
||||
: (fallbackAsset.subscriptionKey ?? null),
|
||||
backgroundMusic:
|
||||
asset.backgroundMusic ?? fallbackAsset.backgroundMusic ?? null,
|
||||
backgroundMusicTitle:
|
||||
asset.backgroundMusicTitle ?? fallbackAsset.backgroundMusicTitle ?? null,
|
||||
backgroundMusicStyle:
|
||||
asset.backgroundMusicStyle ?? fallbackAsset.backgroundMusicStyle ?? null,
|
||||
backgroundMusicPrompt:
|
||||
asset.backgroundMusicPrompt ??
|
||||
fallbackAsset.backgroundMusicPrompt ??
|
||||
null,
|
||||
backgroundAsset: asset.backgroundAsset ?? fallbackAsset.backgroundAsset ?? null,
|
||||
clickSound: asset.clickSound ?? fallbackAsset.clickSound ?? null,
|
||||
soundPrompt: asset.soundPrompt ?? fallbackAsset.soundPrompt ?? null,
|
||||
status:
|
||||
!hasPrimaryImage && !hasPrimaryModel && fallbackAsset.status
|
||||
? fallbackAsset.status
|
||||
: asset.status,
|
||||
error: asset.error ?? fallbackAsset.error ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
for (const fallbackAsset of fallback) {
|
||||
if (!merged.some((asset) => asset.itemId === fallbackAsset.itemId)) {
|
||||
merged.push(fallbackAsset);
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeMatch3DGeneratedItemAssetsForRuntime(merged);
|
||||
}
|
||||
|
||||
export function getMatch3DGeneratedModelAssetSources(
|
||||
assets: readonly Match3DGeneratedItemAsset[] = [],
|
||||
) {
|
||||
@@ -225,7 +379,10 @@ export async function preloadMatch3DGeneratedRuntimeAssets(
|
||||
options: Omit<Match3DModelBytesOptions, 'signal'> = {},
|
||||
) {
|
||||
// 中文注释:新抓大鹅运行态以 2D 图片为主;3D 模型只作为历史草稿预览兼容。
|
||||
await preloadMatch3DGeneratedImageAssets(assets, options);
|
||||
await preloadMatch3DGeneratedImageAssets(
|
||||
normalizeMatch3DGeneratedItemAssetsForRuntime(assets),
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export function clearMatch3DGeneratedModelBytesCache() {
|
||||
|
||||
Reference in New Issue
Block a user