合并远端主分支更新
Project CI / Repository checks (pull_request) Failing after 54s
Project CI / Frontend tests (pull_request) Successful in 3m30s
Project CI / Backend tests (pull_request) Successful in 4m21s
Project CI / Native shell tests (pull_request) Has been cancelled

同步主分支音效生成与External v1契约更新
保留图标规范与图集生成链路
适配VectorEngine LLM客户端命名并解决前端提交语义冲突
This commit is contained in:
2026-08-08 21:54:30 +08:00
103 changed files with 11863 additions and 1097 deletions
@@ -16,6 +16,7 @@ import {
EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_CHARS,
EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_UTF8_BYTES,
EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH,
EDITOR_SOUND_EFFECT_PROMPT_OPTIMIZE_TIMEOUT_MS,
extractEditorUiDesignAssets,
generateEditorBackgroundMusic,
generateEditorCharacterAnimation,
@@ -33,6 +34,7 @@ import {
loadOrCreateRecentEditorProject,
refineEditorIconSpecArtStyle,
refineEditorIconSpecPlaySetting,
optimizeEditorSoundEffectPrompt,
removeEditorImageBackground,
renameEditorProject,
saveEditorProjectLayout,
@@ -112,6 +114,7 @@ describe('editorProjectClient', () => {
prices: { '480p': 13, '720p': 26, '1080p': 52 },
},
'audio1.0': { unit: 'perGeneration', price: 15 },
eleven_text_to_sound_v2: { unit: 'perGeneration', price: 16 },
'chirp-v5': { unit: 'perGeneration', price: 9 },
},
});
@@ -119,6 +122,7 @@ describe('editorProjectClient', () => {
const pricing = await loadEditorGenerationPricing();
expect(pricing.models['gpt-image-2']?.prices?.['1K']).toBe(29);
expect(pricing.models.eleven_text_to_sound_v2?.price).toBe(16);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/generation-pricing',
{ method: 'GET' },
@@ -1988,9 +1992,8 @@ describe('editorProjectClient', () => {
height: 120,
sourceType: 'generated',
prompt: '金币掉落叮当声',
actualPrompt: '金币掉落叮当声',
model: 'audio1.0',
provider: 'VectorEngine',
actualPrompt: 'A bright coin pickup chime',
model: 'eleven_text_to_sound_v2',
taskId: 'sound-task-1',
audioKind: 'sound-effect',
asset: {
@@ -2007,8 +2010,9 @@ describe('editorProjectClient', () => {
const result = await generateEditorSoundEffect({
prompt: '金币掉落叮当声',
model: 'audio1.0',
duration: 7,
model: 'eleven_text_to_sound_v2',
duration: 7.5,
loop: true,
assetFolderId: 'project',
assetLabel: '游戏音效 1',
});
@@ -2022,38 +2026,34 @@ describe('editorProjectClient', () => {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: '金币掉落叮当声',
model: 'audio1.0',
duration: 7,
model: 'eleven_text_to_sound_v2',
duration: 7.5,
loop: true,
assetFolderId: 'project',
assetLabel: '游戏音效 1',
}),
}),
'生成游戏音效失败',
expect.objectContaining({
timeoutMs: 1_200_000,
retry: editorRetryOptionsExpectation,
}),
{ timeoutMs: 1_200_000 },
);
});
it('sends editor sound effect duration to Vidu instead of legacy type and tempo', async () => {
it('canonicalizes omitted duration to null and omitted loop to false', async () => {
requestJsonMock.mockResolvedValueOnce({
audioSrc: '/generated-character-drafts/editor-audios/sfx-null.mp3',
width: 420,
height: 120,
sourceType: 'generated',
prompt: '按钮确认短促音',
actualPrompt: '按钮确认短促音',
model: 'audio1.0',
provider: 'VectorEngine',
actualPrompt: 'A short confirmation click',
model: 'eleven_text_to_sound_v2',
taskId: 'sound-task-null',
audioKind: 'sound-effect',
});
await generateEditorSoundEffect({
prompt: '按钮确认短促音',
model: 'audio1.0',
duration: 5,
model: 'eleven_text_to_sound_v2',
});
expect(requestJsonMock).toHaveBeenCalledWith(
@@ -2061,8 +2061,9 @@ describe('editorProjectClient', () => {
expect.objectContaining({
body: JSON.stringify({
prompt: '按钮确认短促音',
model: 'audio1.0',
duration: 5,
model: 'eleven_text_to_sound_v2',
duration: null,
loop: false,
}),
}),
'生成游戏音效失败',
@@ -2070,6 +2071,66 @@ describe('editorProjectClient', () => {
);
});
it('accepts the 0.5 and 30 second sound effect duration boundaries', async () => {
requestJsonMock.mockResolvedValue({});
for (const duration of [0.5, 30]) {
await generateEditorSoundEffect({
prompt: '按钮确认短促音',
model: 'eleven_text_to_sound_v2',
duration,
});
}
expect(requestJsonMock).toHaveBeenNthCalledWith(
1,
'/api/editor/audios/sound-effects/generations',
expect.objectContaining({
body: JSON.stringify({
prompt: '按钮确认短促音',
model: 'eleven_text_to_sound_v2',
duration: 0.5,
loop: false,
}),
}),
'生成游戏音效失败',
expect.any(Object),
);
expect(requestJsonMock).toHaveBeenNthCalledWith(
2,
'/api/editor/audios/sound-effects/generations',
expect.objectContaining({
body: JSON.stringify({
prompt: '按钮确认短促音',
model: 'eleven_text_to_sound_v2',
duration: 30,
loop: false,
}),
}),
'生成游戏音效失败',
expect.any(Object),
);
});
it('rejects invalid sound effect durations before the request', async () => {
for (const duration of [
0.49,
30.01,
Number.NaN,
Number.POSITIVE_INFINITY,
]) {
await expect(
generateEditorSoundEffect({
prompt: '按钮确认短促音',
model: 'eleven_text_to_sound_v2',
duration,
}),
).rejects.toThrow('游戏音效时长必须在 0.5-30 秒之间');
}
expect(requestJsonMock).not.toHaveBeenCalled();
});
it('generates editor background music through the backend BFF', async () => {
const representativeComplexPromptCase =
backgroundMusicPromptCanonicalizationCases.find(
@@ -2087,7 +2148,6 @@ describe('editorProjectClient', () => {
prompt: canonicalPrompt,
actualPrompt: canonicalPrompt,
model: 'chirp-v4',
provider: 'VectorEngine',
taskId: 'music-task-1',
audioKind: 'background-music',
});
@@ -2168,7 +2228,9 @@ describe('editorProjectClient', () => {
);
expect(EDITOR_BACKGROUND_MUSIC_PROMPT_ASSIST_TIMEOUT_MS).toBe(180_000);
expect(requestJsonMock.mock.calls[0]).toHaveLength(4);
expect(requestJsonMock.mock.calls[0][3]).toEqual(promptAssistRequestOptions);
expect(requestJsonMock.mock.calls[0][3]).toEqual(
promptAssistRequestOptions,
);
});
it('simplifies a background music prompt without forwarding client-controlled limits', async () => {
@@ -2206,7 +2268,47 @@ describe('editorProjectClient', () => {
promptAssistRequestOptions,
);
expect(requestJsonMock.mock.calls[0]).toHaveLength(4);
expect(requestJsonMock.mock.calls[0][3]).toEqual(promptAssistRequestOptions);
expect(requestJsonMock.mock.calls[0][3]).toEqual(
promptAssistRequestOptions,
);
});
it('optimizes an SFX prompt through the authenticated internal BFF without retries or client-controlled limits', async () => {
requestJsonMock.mockResolvedValueOnce({
prompt: '清脆明亮的金币落地声',
charCount: 10,
});
const controller = new AbortController();
const result = await optimizeEditorSoundEffectPrompt(
{
currentPrompt: '金币落地声',
targetChars: 2048,
model: 'client-must-not-control',
} as Parameters<typeof optimizeEditorSoundEffectPrompt>[0] & {
targetChars: number;
model: string;
},
{ signal: controller.signal },
);
expect(result).toEqual({
prompt: '清脆明亮的金币落地声',
charCount: 10,
});
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/audios/sound-effects/prompts/optimizations',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ currentPrompt: '金币落地声' }),
signal: controller.signal,
},
'优化游戏音效提示词失败',
{ timeoutMs: EDITOR_SOUND_EFFECT_PROMPT_OPTIMIZE_TIMEOUT_MS },
);
expect(EDITOR_SOUND_EFFECT_PROMPT_OPTIMIZE_TIMEOUT_MS).toBe(180_000);
expect(requestJsonMock.mock.calls[0]).toHaveLength(4);
});
it.each([
@@ -1,6 +1,12 @@
import type {
BackgroundMusicPromptAssistRequest,
BackgroundMusicPromptAssistResponse,
import {
type BackgroundMusicPromptAssistRequest,
type BackgroundMusicPromptAssistResponse,
type EditorSoundEffectGenerationMetadataV2,
type EditorSoundEffectGenerationRequest,
SOUND_EFFECT_DURATION_MAX_SECONDS,
SOUND_EFFECT_DURATION_MIN_SECONDS,
type SoundEffectPromptOptimizeRequest,
type SoundEffectPromptOptimizeResponse,
} from '../../../packages/shared/src/contracts/editorAudio';
import type { EditorSceneGenerationRequest } from '../../../packages/shared/src/contracts/editorScene';
import type { ExternalGenerationJobStatusRecord } from '../../../packages/shared/src/contracts/externalGeneration';
@@ -44,11 +50,14 @@ const EDITOR_BACKGROUND_MUSIC_PROMPT_COMPLETION_API =
'/api/editor/audios/background-music/prompts/completions';
const EDITOR_BACKGROUND_MUSIC_PROMPT_SIMPLIFICATION_API =
'/api/editor/audios/background-music/prompts/simplifications';
const EDITOR_SOUND_EFFECT_PROMPT_OPTIMIZATION_API =
'/api/editor/audios/sound-effects/prompts/optimizations';
/**
* 覆盖简化两个业务语义轮加单轮 transport retry 的服务端预算,同时为浏览器到 BFF 的
* 悬挂连接提供有界退出;超时后走既有失败路径恢复 idle,不写回候选。
*/
export const EDITOR_BACKGROUND_MUSIC_PROMPT_ASSIST_TIMEOUT_MS = 180_000;
export const EDITOR_SOUND_EFFECT_PROMPT_OPTIMIZE_TIMEOUT_MS = 180_000;
const EDITOR_GENERATION_PRICING_API = '/api/editor/generation-pricing';
const EDITOR_IMAGE_MODEL_NANOBANANA2 = 'gemini-3.1-flash-image-preview';
const EDITOR_IMAGE_REFERENCE_LIMIT = 5;
@@ -184,6 +193,7 @@ export type EditorAssetGenerationInputReference = {
export type EditorAssetGenerationInputs = {
fields: EditorAssetGenerationInputField[];
references: EditorAssetGenerationInputReference[];
soundEffect?: EditorSoundEffectGenerationMetadataV2;
[key: string]: unknown;
};
@@ -602,16 +612,14 @@ export type EditorVideoGenerationResult = {
queueState?: ExternalGenerationJobStatusRecord | null;
};
export type EditorSoundEffectGenerationInput = {
prompt: string;
model: 'audio1.0';
duration: number;
projectId?: string | null;
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
generationInputs?: EditorAssetGenerationInputs | null;
assetFolderId?: string | null;
assetLabel?: string | null;
};
export type EditorSoundEffectGenerationInput =
EditorSoundEffectGenerationRequest & {
projectId?: string | null;
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
generationInputs?: EditorAssetGenerationInputs | null;
assetFolderId?: string | null;
assetLabel?: string | null;
};
export type EditorBackgroundMusicGenerationInput = {
gptDescriptionPrompt: string;
@@ -627,6 +635,10 @@ export type EditorBackgroundMusicPromptAssistOptions = {
signal?: AbortSignal;
};
export type EditorSoundEffectPromptOptimizeOptions = {
signal?: AbortSignal;
};
export type EditorAudioGenerationResult = {
audioSrc: string;
objectKey?: string | null;
@@ -641,6 +653,7 @@ export type EditorAudioGenerationResult = {
priceMudPoints: number;
audioKind: 'sound-effect' | 'background-music';
durationSeconds?: number | null;
loop?: boolean | null;
resource?: EditorProjectResourceSnapshot | null;
asset?: EditorAssetSnapshot | null;
project?: EditorProjectSnapshot | null;
@@ -1509,12 +1522,22 @@ export async function generateEditorVideo(input: EditorVideoGenerationInput) {
export async function generateEditorSoundEffect(
input: EditorSoundEffectGenerationInput,
) {
const duration = input.duration ?? null;
if (
duration !== null &&
(!Number.isFinite(duration) ||
duration < SOUND_EFFECT_DURATION_MIN_SECONDS ||
duration > SOUND_EFFECT_DURATION_MAX_SECONDS)
) {
throw new Error('游戏音效时长必须在 0.5-30 秒之间');
}
return requestJson<EditorAudioGenerationResponse>(
EDITOR_SOUND_EFFECT_GENERATION_API,
jsonRequest('POST', {
prompt: input.prompt,
model: input.model,
duration: input.duration,
duration,
loop: input.loop ?? false,
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.canvasCompletion
? { canvasCompletion: input.canvasCompletion }
@@ -1528,7 +1551,6 @@ export async function generateEditorSoundEffect(
'生成游戏音效失败',
{
timeoutMs: 1_200_000,
retry: EDITOR_GENERATION_REQUEST_RETRY_OPTIONS,
},
);
}
@@ -1578,6 +1600,25 @@ export function simplifyEditorBackgroundMusicPrompt(
);
}
export function optimizeEditorSoundEffectPrompt(
input: SoundEffectPromptOptimizeRequest,
options: EditorSoundEffectPromptOptimizeOptions = {},
) {
return requestJson<SoundEffectPromptOptimizeResponse>(
EDITOR_SOUND_EFFECT_PROMPT_OPTIMIZATION_API,
{
...jsonRequest('POST', {
currentPrompt: input.currentPrompt,
}),
signal: options.signal,
},
'优化游戏音效提示词失败',
{
timeoutMs: EDITOR_SOUND_EFFECT_PROMPT_OPTIMIZE_TIMEOUT_MS,
},
);
}
export async function generateEditorBackgroundMusic(
input: EditorBackgroundMusicGenerationInput,
) {