测试:完成BGM提示词优化收口并切换助手模型
增加 TypeScript/Rust 共用 canonicalization fixture 补齐 BGM 跨层等值、SFX 和后端边界测试 将 BGM 补全与简化请求模型改为 gpt-5.6-luna,保留画布 Agent gpt-5.4-mini 同步权威设计、项目记忆和 T6 发布门禁记录 验证本地定向测试与真实 VectorEngine 补全/简化请求
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import backgroundMusicPromptCanonicalizationCases from '../../../packages/shared/test-fixtures/background-music-prompt-canonicalization.json';
|
||||
import {
|
||||
BACKGROUND_MUSIC_PROMPT_COMPLETION_MIN_EFFECTIVE_CODE_POINTS,
|
||||
BACKGROUND_MUSIC_PROMPT_GENERATION_MIN_EFFECTIVE_CODE_POINTS,
|
||||
@@ -15,86 +16,15 @@ import {
|
||||
} from './ImageCanvasBackgroundMusicPromptModel';
|
||||
|
||||
describe('ImageCanvasBackgroundMusicPromptModel', () => {
|
||||
it('removes only leading and trailing Unicode White_Space code points', () => {
|
||||
const unicodeWhiteSpace = [
|
||||
'\u0009',
|
||||
'\u000A',
|
||||
'\u000B',
|
||||
'\u000C',
|
||||
'\u000D',
|
||||
'\u0020',
|
||||
'\u0085',
|
||||
'\u00A0',
|
||||
'\u1680',
|
||||
'\u2000',
|
||||
'\u2001',
|
||||
'\u2002',
|
||||
'\u2003',
|
||||
'\u2004',
|
||||
'\u2005',
|
||||
'\u2006',
|
||||
'\u2007',
|
||||
'\u2008',
|
||||
'\u2009',
|
||||
'\u200A',
|
||||
'\u2028',
|
||||
'\u2029',
|
||||
'\u202F',
|
||||
'\u205F',
|
||||
'\u3000',
|
||||
].join('');
|
||||
const internalWhiteSpace = 'A \t\r\n\u0085\u00A0\u3000B';
|
||||
|
||||
expect(
|
||||
canonicalizeBackgroundMusicPrompt(
|
||||
`${unicodeWhiteSpace}${internalWhiteSpace}${unicodeWhiteSpace}`,
|
||||
),
|
||||
).toBe(internalWhiteSpace);
|
||||
expect(
|
||||
canonicalizeBackgroundMusicPrompt('\u0085\u200B内容\uFEFF\u0085'),
|
||||
).toBe('\u200B内容\uFEFF');
|
||||
expect(canonicalizeBackgroundMusicPrompt('\u200B\uFEFF')).toBe(
|
||||
'\u200B\uFEFF',
|
||||
);
|
||||
});
|
||||
|
||||
it('is idempotent without normalizing internal Unicode content', () => {
|
||||
const combiningText = 'e\u0301';
|
||||
const zwjEmoji = '👨👩👧👦';
|
||||
const prompt = ` \u0085${combiningText}\r\n${zwjEmoji}\u200B\uFEFF\u3000`;
|
||||
const canonicalPrompt = `${combiningText}\r\n${zwjEmoji}\u200B\uFEFF`;
|
||||
|
||||
expect(canonicalizeBackgroundMusicPrompt(prompt)).toBe(canonicalPrompt);
|
||||
expect(canonicalizeBackgroundMusicPrompt(canonicalPrompt)).toBe(
|
||||
canonicalPrompt,
|
||||
);
|
||||
expect(canonicalPrompt).toContain(combiningText);
|
||||
expect(canonicalPrompt).toContain('\r\n');
|
||||
expect(canonicalPrompt).toContain(zwjEmoji);
|
||||
expect(canonicalPrompt).toContain('\u200B\uFEFF');
|
||||
});
|
||||
|
||||
it('counts code points from the canonical prompt', () => {
|
||||
const combiningText = 'e\u0301';
|
||||
const zwjEmoji = '👨👩👧👦';
|
||||
const prompt = ` \r\n${combiningText}\r\n${zwjEmoji}\u200B\uFEFF `;
|
||||
|
||||
expect(countPromptCodePoints('')).toBe(0);
|
||||
expect(countPromptCodePoints('\r\n')).toBe(0);
|
||||
expect(countPromptCodePoints(combiningText)).toBe(2);
|
||||
expect(countPromptCodePoints(zwjEmoji)).toBe(7);
|
||||
expect(countPromptCodePoints(prompt)).toBe(13);
|
||||
expect(prompt).toBe(` \r\n${combiningText}\r\n${zwjEmoji}\u200B\uFEFF `);
|
||||
});
|
||||
|
||||
it('counts only non-Unicode-White_Space code points in the canonical prompt as effective', () => {
|
||||
expect(countEffectivePromptCodePoints(' \t\r\n\u0085\u00A0\u3000')).toBe(0);
|
||||
expect(countEffectivePromptCodePoints('e\u0301')).toBe(2);
|
||||
expect(countEffectivePromptCodePoints('👨👩👧👦')).toBe(7);
|
||||
expect(countEffectivePromptCodePoints('\uFEFF')).toBe(1);
|
||||
expect(countEffectivePromptCodePoints('\u200B')).toBe(1);
|
||||
expect(countEffectivePromptCodePoints('。1A')).toBe(3);
|
||||
});
|
||||
it.each(backgroundMusicPromptCanonicalizationCases)(
|
||||
'canonicalizes shared fixture case $name',
|
||||
({ input, prompt, charCount, effectiveCharCount }) => {
|
||||
expect(canonicalizeBackgroundMusicPrompt(input)).toBe(prompt);
|
||||
expect(countPromptCodePoints(input)).toBe(charCount);
|
||||
expect(countEffectivePromptCodePoints(input)).toBe(effectiveCharCount);
|
||||
expect(canonicalizeBackgroundMusicPrompt(prompt)).toBe(prompt);
|
||||
},
|
||||
);
|
||||
|
||||
it('allows formal generation for 1-200 code points with effective content', () => {
|
||||
expect(BACKGROUND_MUSIC_PROMPT_MAX_CODE_POINTS).toBe(200);
|
||||
|
||||
@@ -1039,32 +1039,35 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('uses 5 seconds as default game sound effect duration', () => {
|
||||
it('uses the game sound effect fallback for an all-whitespace prompt', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
mode: 'audio-sound-effect',
|
||||
prompt: '按钮确认短促音',
|
||||
prompt: ' \t\r\n ',
|
||||
status: 'idle',
|
||||
},
|
||||
layers: [],
|
||||
nextGeneratedIndex: 6,
|
||||
});
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
expect(plan).toEqual({
|
||||
kind: 'audio',
|
||||
audioKind: 'sound-effect',
|
||||
normalizedPrompt: '游戏音效',
|
||||
input: {
|
||||
prompt: '按钮确认短促音',
|
||||
prompt: '游戏音效',
|
||||
model: 'audio1.0',
|
||||
duration: 5,
|
||||
},
|
||||
result: {
|
||||
title: '游戏音效 6',
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{ title: 'prompt', value: '按钮确认短促音' },
|
||||
{ title: 'prompt', value: '游戏音效' },
|
||||
{ title: 'model', value: 'audio1.0' },
|
||||
{ title: 'duration', value: '5秒' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { useRef, useState } from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import backgroundMusicPromptCanonicalizationCases from '../../../packages/shared/test-fixtures/background-music-prompt-canonicalization.json';
|
||||
import { ApiClientError } from '../../services/apiClient';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
@@ -1814,18 +1815,27 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
});
|
||||
|
||||
it('claims and canonicalizes one background music submission before the first await', async () => {
|
||||
const canonicalPrompt = '\uFEFF森林冒险\n夜晚\uFEFF';
|
||||
const representativeComplexPromptCase =
|
||||
backgroundMusicPromptCanonicalizationCases.find(
|
||||
({ name }) => name === 'representative-complex',
|
||||
);
|
||||
if (!representativeComplexPromptCase) {
|
||||
throw new Error('missing representative-complex prompt fixture');
|
||||
}
|
||||
const canonicalPrompt = representativeComplexPromptCase.prompt;
|
||||
const generation =
|
||||
createDeferred<ReturnType<typeof createAudioGenerated>>();
|
||||
const onAppendCanvasLayers = vi.fn<[CanvasLayer[]], void>();
|
||||
generateEditorBackgroundMusicMock.mockReturnValueOnce(generation.promise);
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
currentUserId="user-a"
|
||||
projectId="editor-project-bgm"
|
||||
onAppendCanvasLayers={onAppendCanvasLayers}
|
||||
initialDialog={{
|
||||
id: 'dialog-bgm',
|
||||
mode: 'audio-background-music',
|
||||
prompt: `\u0085 ${canonicalPrompt} \u0085`,
|
||||
prompt: representativeComplexPromptCase.input,
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
placeholder: {
|
||||
@@ -1848,22 +1858,34 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(generateEditorBackgroundMusicMock).toHaveBeenCalledTimes(1);
|
||||
expect(generateEditorBackgroundMusicMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gptDescriptionPrompt: canonicalPrompt,
|
||||
makeInstrumental: true,
|
||||
projectId: 'editor-project-bgm',
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{
|
||||
title: 'gpt_description_prompt',
|
||||
value: canonicalPrompt,
|
||||
},
|
||||
],
|
||||
references: [],
|
||||
expect(generateEditorBackgroundMusicMock).toHaveBeenCalledWith({
|
||||
gptDescriptionPrompt: canonicalPrompt,
|
||||
makeInstrumental: true,
|
||||
projectId: 'editor-project-bgm',
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{
|
||||
title: 'gpt_description_prompt',
|
||||
value: canonicalPrompt,
|
||||
},
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '游戏背景音乐 1',
|
||||
canvasCompletion: {
|
||||
dialogId: 'dialog-bgm',
|
||||
title: '游戏背景音乐 1',
|
||||
placeholder: {
|
||||
x: 200,
|
||||
y: 160,
|
||||
width: 420,
|
||||
height: 120,
|
||||
originalWidth: 420,
|
||||
originalHeight: 120,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
expect(screen.getByTestId('tracked-dialog').textContent).toContain(
|
||||
`audio-background-music:${canonicalPrompt}:idle:open`,
|
||||
);
|
||||
@@ -1891,6 +1913,19 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'background-music',
|
||||
);
|
||||
expect(onAppendCanvasLayers).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
const generatedLayer = onAppendCanvasLayers.mock.calls[0]?.[0]?.[0];
|
||||
expect(generatedLayer?.prompt).toBe(canonicalPrompt);
|
||||
expect(generatedLayer?.actualPrompt).toBe(canonicalPrompt);
|
||||
expect(generatedLayer?.generationInputs).toEqual({
|
||||
fields: [
|
||||
{
|
||||
title: 'gpt_description_prompt',
|
||||
value: canonicalPrompt,
|
||||
},
|
||||
],
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import backgroundMusicPromptCanonicalizationCases from '../../../packages/shared/test-fixtures/background-music-prompt-canonicalization.json';
|
||||
import {
|
||||
completeEditorBackgroundMusicPrompt,
|
||||
createEditorAsset,
|
||||
@@ -1582,13 +1583,21 @@ describe('editorProjectClient', () => {
|
||||
});
|
||||
|
||||
it('generates editor background music through the backend BFF', async () => {
|
||||
const representativeComplexPromptCase =
|
||||
backgroundMusicPromptCanonicalizationCases.find(
|
||||
({ name }) => name === 'representative-complex',
|
||||
);
|
||||
if (!representativeComplexPromptCase) {
|
||||
throw new Error('missing representative-complex prompt fixture');
|
||||
}
|
||||
const canonicalPrompt = representativeComplexPromptCase.prompt;
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
audioSrc: '/generated-character-drafts/editor-audios/bgm.mp3',
|
||||
width: 420,
|
||||
height: 120,
|
||||
sourceType: 'generated',
|
||||
prompt: '森林冒险背景音乐',
|
||||
actualPrompt: '森林冒险背景音乐',
|
||||
prompt: canonicalPrompt,
|
||||
actualPrompt: canonicalPrompt,
|
||||
model: 'chirp-v4',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'music-task-1',
|
||||
@@ -1596,7 +1605,7 @@ describe('editorProjectClient', () => {
|
||||
});
|
||||
|
||||
const result = await generateEditorBackgroundMusic({
|
||||
gptDescriptionPrompt: '森林冒险背景音乐',
|
||||
gptDescriptionPrompt: canonicalPrompt,
|
||||
makeInstrumental: true,
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '游戏背景音乐 1',
|
||||
@@ -1609,7 +1618,7 @@ describe('editorProjectClient', () => {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
gptDescriptionPrompt: '森林冒险背景音乐',
|
||||
gptDescriptionPrompt: canonicalPrompt,
|
||||
makeInstrumental: true,
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '游戏背景音乐 1',
|
||||
|
||||
Reference in New Issue
Block a user