合并远端主分支更新
同步主分支音效生成与External v1契约更新 保留图标规范与图集生成链路 适配VectorEngine LLM客户端命名并解决前端提交语义冲突
This commit is contained in:
@@ -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,
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user