84 lines
3.2 KiB
TypeScript
84 lines
3.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import {
|
||
splitLongSpeechText,
|
||
splitLongSpeechActions,
|
||
TTS_MAX_TEXT_LENGTH,
|
||
} from '@/lib/audio/tts-utils';
|
||
import type { Action, SpeechAction } from '@/lib/types/action';
|
||
|
||
describe('splitLongSpeechText', () => {
|
||
it('returns the trimmed text unchanged when within the limit', () => {
|
||
expect(splitLongSpeechText(' hello world ', 100)).toEqual(['hello world']);
|
||
});
|
||
|
||
it('splits at sentence boundaries, keeping the punctuation', () => {
|
||
expect(splitLongSpeechText('句子一。句子二!句子三?', 6)).toEqual([
|
||
'句子一。',
|
||
'句子二!',
|
||
'句子三?',
|
||
]);
|
||
});
|
||
|
||
it('packs consecutive short sentences up to the limit', () => {
|
||
const out = splitLongSpeechText('aa。bb。cc。dd。', 6);
|
||
expect(out).toEqual(['aa。bb。', 'cc。dd。']);
|
||
expect(out.join('')).toBe('aa。bb。cc。dd。');
|
||
});
|
||
|
||
it('falls back to clause punctuation for an over-long sentence', () => {
|
||
const out = splitLongSpeechText('一,二,三,四,五', 4);
|
||
expect(out.every((c) => c.length <= 4)).toBe(true);
|
||
expect(out.join('').replace(/,/g, '')).toContain('一');
|
||
});
|
||
|
||
it('hard-splits a long run with no punctuation', () => {
|
||
expect(splitLongSpeechText('x'.repeat(25), 10)).toEqual(['xxxxxxxxxx', 'xxxxxxxxxx', 'xxxxx']);
|
||
});
|
||
|
||
it('never emits a chunk longer than maxLength, nor an empty chunk (invariant)', () => {
|
||
const text = '这是一个很长的句子,包含很多子句、标点。'.repeat(50) + 'z'.repeat(120);
|
||
for (const max of [8, 16, 64, 200]) {
|
||
const out = splitLongSpeechText(text, max);
|
||
expect(
|
||
out.every((c) => c.length <= max),
|
||
`max=${max}`,
|
||
).toBe(true);
|
||
expect(
|
||
out.some((c) => c.length === 0),
|
||
`max=${max}`,
|
||
).toBe(false);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('splitLongSpeechActions', () => {
|
||
const speech = (id: string, text: string): SpeechAction =>
|
||
({ id, type: 'speech', text }) as SpeechAction;
|
||
|
||
it('returns actions untouched for a provider with no length limit', () => {
|
||
const actions: Action[] = [speech('a', 'x'.repeat(5000))];
|
||
expect(splitLongSpeechActions(actions, 'openai-tts')).toBe(actions);
|
||
});
|
||
|
||
it('leaves short speech and non-speech actions unchanged', () => {
|
||
const actions: Action[] = [speech('s', 'short'), { id: 'sp', type: 'spotlight' } as Action];
|
||
expect(splitLongSpeechActions(actions, 'glm-tts')).toBe(actions);
|
||
});
|
||
|
||
it('splits an over-limit speech action into chunked sub-actions', () => {
|
||
const max = TTS_MAX_TEXT_LENGTH['glm-tts']!; // 1024
|
||
const long = '句子。'.repeat(400); // 1200 chars > 1024
|
||
const out = splitLongSpeechActions([speech('a', long)], 'glm-tts') as SpeechAction[];
|
||
|
||
expect(out.length).toBeGreaterThan(1);
|
||
expect(out.every((a) => a.type === 'speech' && a.text.length <= max)).toBe(true);
|
||
// deterministic sub-action ids…
|
||
expect(out.map((a) => a.id)).toEqual(out.map((_, i) => `a_tts_${i + 1}`));
|
||
// …each gets its own audio (the parent audioId is dropped)…
|
||
expect(out.every((a) => a.audioId === undefined)).toBe(true);
|
||
// …and the text is preserved across the split.
|
||
expect(out.map((a) => a.text).join('')).toBe(long);
|
||
});
|
||
});
|