实现 SFX V2 T1 契约与提示词基础

统一前后端 SFX Prompt Unicode canonicalization、字符计数和 2048 边界
固化 52 个音效预设、追加规则及跨语言测试向量
演进共享音频请求响应、duration、Loop 和 V2 metadata 契约
增加 External model 输入矩阵纯函数并保持正式接线归属 T5
补齐前端提交、读取、深拷贝与请求边界回归测试
更新共享计划并标记 T1 完成且当前不可发布
This commit is contained in:
2026-08-06 13:05:48 +00:00
parent 6fd731f56c
commit cd80c085ff
24 changed files with 1559 additions and 97 deletions
@@ -1752,9 +1752,9 @@ 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',
provider: 'elevenlabs',
taskId: 'sound-task-1',
audioKind: 'sound-effect',
asset: {
@@ -1771,8 +1771,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',
});
@@ -1786,8 +1787,9 @@ 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',
}),
@@ -1800,24 +1802,23 @@ describe('editorProjectClient', () => {
);
});
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',
provider: 'elevenlabs',
taskId: 'sound-task-null',
audioKind: 'sound-effect',
});
await generateEditorSoundEffect({
prompt: '按钮确认短促音',
model: 'audio1.0',
duration: 5,
model: 'eleven_text_to_sound_v2',
});
expect(requestJsonMock).toHaveBeenCalledWith(
@@ -1825,8 +1826,9 @@ describe('editorProjectClient', () => {
expect.objectContaining({
body: JSON.stringify({
prompt: '按钮确认短促音',
model: 'audio1.0',
duration: 5,
model: 'eleven_text_to_sound_v2',
duration: null,
loop: false,
}),
}),
'生成游戏音效失败',
@@ -1834,6 +1836,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(
@@ -1,6 +1,10 @@
import type {
BackgroundMusicPromptAssistRequest,
BackgroundMusicPromptAssistResponse,
import {
type BackgroundMusicPromptAssistRequest,
type BackgroundMusicPromptAssistResponse,
type EditorSoundEffectGenerationMetadataV2,
type EditorSoundEffectGenerationRequest,
SOUND_EFFECT_DURATION_MAX_SECONDS,
SOUND_EFFECT_DURATION_MIN_SECONDS,
} from '../../../packages/shared/src/contracts/editorAudio';
import type { ExternalGenerationJobStatusRecord } from '../../../packages/shared/src/contracts/externalGeneration';
import { requestJson } from '../apiClient';
@@ -125,6 +129,7 @@ export type EditorAssetGenerationInputReference = {
export type EditorAssetGenerationInputs = {
fields: EditorAssetGenerationInputField[];
references: EditorAssetGenerationInputReference[];
soundEffect?: EditorSoundEffectGenerationMetadataV2;
[key: string]: unknown;
};
@@ -547,16 +552,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;
@@ -582,10 +585,12 @@ export type EditorAudioGenerationResult = {
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
priceMudPoints: number;
audioKind: 'sound-effect' | 'background-music';
durationSeconds?: number | null;
loop?: boolean | null;
resource?: EditorProjectResourceSnapshot | null;
asset?: EditorAssetSnapshot | null;
project?: EditorProjectSnapshot | null;
@@ -1353,12 +1358,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 }