This commit is contained in:
2026-05-14 13:40:50 +08:00
parent 5a55180b78
commit 2dc9d752e4
24 changed files with 1873 additions and 98 deletions

View File

@@ -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',
);
});
});

View File

@@ -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() {