合并远端主分支更新
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([