This commit is contained in:
2026-05-11 20:27:41 +08:00
parent e30b733b17
commit 481a27fc53
60 changed files with 6357 additions and 1100 deletions

View File

@@ -0,0 +1,90 @@
import type {
AudioGenerationTaskResponse,
CreateBackgroundMusicRequest,
CreateSoundEffectRequest,
GeneratedAudioAssetResponse,
PublishGeneratedAudioAssetRequest,
} from '../../../packages/shared/src/contracts/creationAudio';
import { type ApiRetryOptions, requestJson } from '../apiClient';
const CREATION_AUDIO_API_BASE = '/api/creation/audio';
const CREATION_AUDIO_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 500,
maxDelayMs: 1200,
retryUnsafeMethods: true,
};
export function createBackgroundMusicTask(payload: CreateBackgroundMusicRequest) {
return requestJson<AudioGenerationTaskResponse>(
`${CREATION_AUDIO_API_BASE}/background-music`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'提交背景音乐生成失败',
{ retry: CREATION_AUDIO_RETRY, timeoutMs: 20000 },
);
}
export function publishBackgroundMusicAsset(
taskId: string,
payload: PublishGeneratedAudioAssetRequest,
) {
return requestJson<GeneratedAudioAssetResponse>(
`${CREATION_AUDIO_API_BASE}/background-music/${encodeURIComponent(taskId)}/asset`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'生成背景音乐素材失败',
{ retry: CREATION_AUDIO_RETRY, timeoutMs: 30000 },
);
}
export function createSoundEffectTask(payload: CreateSoundEffectRequest) {
return requestJson<AudioGenerationTaskResponse>(
`${CREATION_AUDIO_API_BASE}/sound-effect`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'提交音效生成失败',
{ retry: CREATION_AUDIO_RETRY, timeoutMs: 20000 },
);
}
export function publishSoundEffectAsset(
taskId: string,
payload: PublishGeneratedAudioAssetRequest,
) {
return requestJson<GeneratedAudioAssetResponse>(
`${CREATION_AUDIO_API_BASE}/sound-effect/${encodeURIComponent(taskId)}/asset`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'生成音效素材失败',
{ retry: CREATION_AUDIO_RETRY, timeoutMs: 30000 },
);
}
export async function waitForGeneratedAudioAsset(
taskId: string,
publish: () => Promise<GeneratedAudioAssetResponse>,
) {
let latestAsset: GeneratedAudioAssetResponse | null = null;
for (let attempt = 0; attempt < 40; attempt += 1) {
latestAsset = await publish();
if (latestAsset.audioSrc?.trim()) {
return latestAsset;
}
await new Promise((resolve) => window.setTimeout(resolve, 3000));
}
throw new Error(latestAsset?.status || `音频生成超时:${taskId}`);
}

View File

@@ -0,0 +1 @@
export * from './creationAudioGenerationClient';

View File

@@ -6,5 +6,7 @@ export {
listMatch3DWorks,
match3dWorksClient,
publishMatch3DWork,
updateMatch3DAudioAssets,
updateMatch3DGeneratedItemAssets,
updateMatch3DWork,
} from './match3dWorksClient';

View File

@@ -4,6 +4,7 @@ import type {
Match3DWorkDetailResponse,
Match3DWorkMutationResponse,
Match3DWorksResponse,
PutMatch3DAudioAssetsRequest,
PutMatch3DWorkRequest,
} from '../../../packages/shared/src/contracts/match3dWorks';
import { type ApiRetryOptions, requestJson } from '../apiClient';
@@ -81,6 +82,27 @@ export function updateMatch3DWork(
);
}
/**
* 保存抓大鹅结果页生成的素材快照。
*/
export function updateMatch3DGeneratedItemAssets(
profileId: string,
payload: PutMatch3DAudioAssetsRequest,
) {
return requestJson<Match3DWorkMutationResponse>(
`${MATCH3D_WORKS_API_BASE}/${encodeURIComponent(profileId)}/audio-assets`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'更新抓大鹅生成素材失败',
{ retry: MATCH3D_WORKS_WRITE_RETRY },
);
}
export const updateMatch3DAudioAssets = updateMatch3DGeneratedItemAssets;
/**
* 根据当前作品名称与题材生成发布标签。
*/
@@ -128,5 +150,7 @@ export const match3dWorksClient = {
listGallery: listMatch3DGallery,
list: listMatch3DWorks,
publish: publishMatch3DWork,
updateAudioAssets: updateMatch3DAudioAssets,
updateGeneratedItemAssets: updateMatch3DGeneratedItemAssets,
update: updateMatch3DWork,
};

View File

@@ -186,6 +186,24 @@ describe('miniGameDraftGenerationProgress', () => {
expect(progress?.steps[0]?.detail).toBe('根据题材设定生成作品名称与标签。');
});
test('match3d draft generation keeps backend observed model phase', () => {
const state = {
...createMiniGameDraftGenerationState('match3d'),
phase: 'match3d-generate-models' as const,
completedAssetCount: 1,
totalAssetCount: 3,
};
const progress = buildMiniGameDraftGenerationProgress(
state,
state.startedAtMs + 20_000,
);
expect(progress?.phaseId).toBe('match3d-generate-models');
expect(progress?.steps.at(-1)?.completed).toBe(1);
expect(progress?.steps.at(-1)?.total).toBe(3);
});
test('match3d generation anchors show theme and fixed three items', () => {
const entries = buildMatch3DGenerationAnchorEntries(null, {
themeText: '水果',

View File

@@ -1,12 +1,12 @@
import type { BigFishSessionSnapshotResponse } from '../../packages/shared/src/contracts/bigFish';
import type {
CreatePuzzleAgentSessionRequest,
PuzzleAgentSessionSnapshot,
} from '../../packages/shared/src/contracts/puzzleAgentSession';
import type {
CreateMatch3DSessionRequest,
Match3DAgentSessionSnapshot,
} from '../../packages/shared/src/contracts/match3dAgent';
import type {
CreatePuzzleAgentSessionRequest,
PuzzleAgentSessionSnapshot,
} from '../../packages/shared/src/contracts/puzzleAgentSession';
import type {
CustomWorldGenerationProgress,
CustomWorldGenerationStep,
@@ -180,6 +180,17 @@ const MATCH3D_STEPS = [
},
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
const MATCH3D_PHASE_ORDER: Partial<
Record<MiniGameDraftGenerationPhase, number>
> = {
'match3d-work-title': 0,
'match3d-item-names': 1,
'match3d-material-sheet': 2,
'match3d-slice-images': 3,
'match3d-upload-images': 4,
'match3d-generate-models': 5,
};
function clampProgress(value: number) {
return Math.max(0, Math.min(100, Math.round(value)));
}
@@ -283,23 +294,23 @@ function resolveSquareHolePhaseByElapsedMs(
function resolveMatch3DPhaseByElapsedMs(
elapsedMs: number,
currentPhase: MiniGameDraftGenerationPhase,
): MiniGameDraftGenerationPhase {
if (elapsedMs >= 92_000) {
return 'match3d-generate-models';
}
if (elapsedMs >= 72_000) {
return 'match3d-upload-images';
}
if (elapsedMs >= 58_000) {
return 'match3d-slice-images';
}
if (elapsedMs >= 16_000) {
return 'match3d-material-sheet';
}
if (elapsedMs >= 4_000) {
return 'match3d-item-names';
}
return 'match3d-work-title';
const elapsedPhase =
elapsedMs >= 92_000
? 'match3d-generate-models'
: elapsedMs >= 72_000
? 'match3d-upload-images'
: elapsedMs >= 58_000
? 'match3d-slice-images'
: elapsedMs >= 16_000
? 'match3d-material-sheet'
: elapsedMs >= 4_000
? 'match3d-item-names'
: 'match3d-work-title';
const elapsedOrder = MATCH3D_PHASE_ORDER[elapsedPhase] ?? 0;
const currentOrder = MATCH3D_PHASE_ORDER[currentPhase] ?? -1;
return currentOrder > elapsedOrder ? currentPhase : elapsedPhase;
}
function resolvePuzzleTimelineByElapsedMs(elapsedMs: number) {
@@ -367,7 +378,7 @@ export function buildMiniGameDraftGenerationProgress(
state.phase !== 'ready'
? {
...state,
phase: resolveMatch3DPhaseByElapsedMs(elapsedMs),
phase: resolveMatch3DPhaseByElapsedMs(elapsedMs, state.phase),
}
: state;