修复BGM规范化契约与基础校验

补全T0一键简化两轮输入、结构化结果和互斥状态规则
新增TypeScript Unicode White_Space canonicalizer与生成、补全、简化状态
新增Rust零分配canonicalizer并让计数和校验返回规范化Prompt
补齐跨语言Unicode及200/201字符边界测试
This commit is contained in:
2026-08-04 08:53:55 +00:00
parent dafcfbe6b7
commit 8d269d785d
8 changed files with 397 additions and 102 deletions
@@ -6,25 +6,86 @@ import {
BACKGROUND_MUSIC_PROMPT_MAX_CODE_POINTS,
canCompleteBackgroundMusicPrompt,
canGenerateBackgroundMusicFromPrompt,
canSimplifyBackgroundMusicPrompt,
canonicalizeBackgroundMusicPrompt,
countEffectivePromptCodePoints,
countPromptCodePoints,
} from './ImageCanvasBackgroundMusicPromptModel';
describe('ImageCanvasBackgroundMusicPromptModel', () => {
it('counts raw Unicode code points without normalizing the prompt', () => {
const combiningText = 'e\u0301';
const zwjEmoji = '👨‍👩‍👧‍👦';
const prompt = ` \r\n${combiningText}${zwjEmoji}\u200B\uFEFF `;
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(countPromptCodePoints('')).toBe(0);
expect(countPromptCodePoints('\r\n')).toBe(2);
expect(countPromptCodePoints(combiningText)).toBe(2);
expect(countPromptCodePoints(zwjEmoji)).toBe(7);
expect(countPromptCodePoints(prompt)).toBe(15);
expect(prompt).toBe(` \r\n${combiningText}${zwjEmoji}\u200B\uFEFF `);
expect(
canonicalizeBackgroundMusicPrompt(
`${unicodeWhiteSpace}${internalWhiteSpace}${unicodeWhiteSpace}`,
),
).toBe(internalWhiteSpace);
expect(
canonicalizeBackgroundMusicPrompt('\u0085\u200B内容\uFEFF\u0085'),
).toBe('\u200B内容\uFEFF');
expect(canonicalizeBackgroundMusicPrompt('\u200B\uFEFF')).toBe(
'\u200B\uFEFF',
);
});
it('counts only non-Unicode-White_Space code points as effective', () => {
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);
@@ -57,4 +118,91 @@ describe('ImageCanvasBackgroundMusicPromptModel', () => {
expect(canCompleteBackgroundMusicPrompt('😀'.repeat(200))).toBe(true);
expect(canCompleteBackgroundMusicPrompt('😀'.repeat(201))).toBe(false);
});
it('allows simplification only for a non-empty canonical prompt over 200 code points', () => {
expect(canSimplifyBackgroundMusicPrompt(' \r\n\u0085')).toBe(false);
expect(canSimplifyBackgroundMusicPrompt('A'.repeat(200))).toBe(false);
expect(canSimplifyBackgroundMusicPrompt('A'.repeat(201))).toBe(true);
});
it.each([200, 201])(
'canonicalizes %i Unicode White_Space code points to an empty disabled prompt',
(length) => {
const prompt = '\u0085'.repeat(length);
expect(canonicalizeBackgroundMusicPrompt(prompt)).toBe('');
expect(countPromptCodePoints(prompt)).toBe(0);
expect(countEffectivePromptCodePoints(prompt)).toBe(0);
expect(canGenerateBackgroundMusicFromPrompt(prompt)).toBe(false);
expect(canCompleteBackgroundMusicPrompt(prompt)).toBe(false);
expect(canSimplifyBackgroundMusicPrompt(prompt)).toBe(false);
},
);
it('uses the canonical prompt for generation, completion, and simplification states', () => {
const boundaryWhiteSpaceAroundA = `${'\u0085'.repeat(201)}A${'\u3000'.repeat(201)}`;
const overLimitWithInternalWhiteSpace = `A${' '.repeat(199)}B`;
const boundaryWhiteSpaceAround200Characters = `${'\u0085'.repeat(201)}${'A'.repeat(200)}${'\u3000'.repeat(201)}`;
expect(canonicalizeBackgroundMusicPrompt(boundaryWhiteSpaceAroundA)).toBe(
'A',
);
expect(countPromptCodePoints(boundaryWhiteSpaceAroundA)).toBe(1);
expect(countEffectivePromptCodePoints(boundaryWhiteSpaceAroundA)).toBe(1);
expect(
canGenerateBackgroundMusicFromPrompt(boundaryWhiteSpaceAroundA),
).toBe(true);
expect(canCompleteBackgroundMusicPrompt(boundaryWhiteSpaceAroundA)).toBe(
false,
);
expect(canSimplifyBackgroundMusicPrompt(boundaryWhiteSpaceAroundA)).toBe(
false,
);
expect(countPromptCodePoints(overLimitWithInternalWhiteSpace)).toBe(201);
expect(
countEffectivePromptCodePoints(overLimitWithInternalWhiteSpace),
).toBe(2);
expect(
canGenerateBackgroundMusicFromPrompt(overLimitWithInternalWhiteSpace),
).toBe(false);
expect(
canCompleteBackgroundMusicPrompt(overLimitWithInternalWhiteSpace),
).toBe(false);
expect(
canSimplifyBackgroundMusicPrompt(overLimitWithInternalWhiteSpace),
).toBe(true);
expect(countPromptCodePoints(boundaryWhiteSpaceAround200Characters)).toBe(
200,
);
expect(
countEffectivePromptCodePoints(boundaryWhiteSpaceAround200Characters),
).toBe(200);
expect(
canGenerateBackgroundMusicFromPrompt(
boundaryWhiteSpaceAround200Characters,
),
).toBe(true);
expect(
canCompleteBackgroundMusicPrompt(boundaryWhiteSpaceAround200Characters),
).toBe(true);
expect(
canSimplifyBackgroundMusicPrompt(boundaryWhiteSpaceAround200Characters),
).toBe(false);
});
it.each(['\u200B', '\uFEFF'])(
'keeps 201 %s code points and enables only simplification',
(codePoint) => {
const prompt = codePoint.repeat(201);
expect(canonicalizeBackgroundMusicPrompt(prompt)).toBe(prompt);
expect(countPromptCodePoints(prompt)).toBe(201);
expect(countEffectivePromptCodePoints(prompt)).toBe(201);
expect(canGenerateBackgroundMusicFromPrompt(prompt)).toBe(false);
expect(canCompleteBackgroundMusicPrompt(prompt)).toBe(false);
expect(canSimplifyBackgroundMusicPrompt(prompt)).toBe(true);
},
);
});
@@ -3,14 +3,22 @@ export const BACKGROUND_MUSIC_PROMPT_GENERATION_MIN_EFFECTIVE_CODE_POINTS = 1;
export const BACKGROUND_MUSIC_PROMPT_COMPLETION_MIN_EFFECTIVE_CODE_POINTS = 2;
const UNICODE_WHITE_SPACE_CODE_POINT = /^\p{White_Space}$/u;
const LEADING_UNICODE_WHITE_SPACE = /^\p{White_Space}+/u;
const TRAILING_UNICODE_WHITE_SPACE = /\p{White_Space}+$/u;
export function canonicalizeBackgroundMusicPrompt(prompt: string) {
return prompt
.replace(LEADING_UNICODE_WHITE_SPACE, '')
.replace(TRAILING_UNICODE_WHITE_SPACE, '');
}
export function countPromptCodePoints(prompt: string) {
return Array.from(prompt).length;
return Array.from(canonicalizeBackgroundMusicPrompt(prompt)).length;
}
export function countEffectivePromptCodePoints(prompt: string) {
let effectiveCodePoints = 0;
for (const codePoint of prompt) {
for (const codePoint of canonicalizeBackgroundMusicPrompt(prompt)) {
if (!UNICODE_WHITE_SPACE_CODE_POINT.test(codePoint)) {
effectiveCodePoints += 1;
}
@@ -33,3 +41,11 @@ export function canCompleteBackgroundMusicPrompt(prompt: string) {
BACKGROUND_MUSIC_PROMPT_COMPLETION_MIN_EFFECTIVE_CODE_POINTS
);
}
export function canSimplifyBackgroundMusicPrompt(prompt: string) {
return (
countPromptCodePoints(prompt) > BACKGROUND_MUSIC_PROMPT_MAX_CODE_POINTS &&
countEffectivePromptCodePoints(prompt) >=
BACKGROUND_MUSIC_PROMPT_GENERATION_MIN_EFFECTIVE_CODE_POINTS
);
}