188 lines
6.7 KiB
TypeScript
188 lines
6.7 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import {
|
|
preprocessSentenceForAudio,
|
|
splitTextToTtsBlocks,
|
|
splitTextToTtsBlocksEPUB,
|
|
normalizeTextForTts,
|
|
MAX_BLOCK_LENGTH
|
|
} from '../../src/lib/shared/nlp';
|
|
|
|
const PDF_MAX_BLOCK_LENGTH = MAX_BLOCK_LENGTH * 2; // splitTextToTtsBlocks can overflow to reach punctuation
|
|
|
|
const expectNormalizedBlocks = (blocks: string[], maxLen = Number.POSITIVE_INFINITY) => {
|
|
for (const block of blocks) {
|
|
expect(block.trim().length).toBeGreaterThan(0);
|
|
expect(block.length).toBeLessThanOrEqual(maxLen);
|
|
expect(block).not.toMatch(/\n/);
|
|
expect(block).not.toMatch(/\s{2,}/);
|
|
}
|
|
};
|
|
|
|
test.describe('preprocessSentenceForAudio', () => {
|
|
test('normalizes common extraction artifacts', () => {
|
|
const cases: Array<{ input: string; expected: string }> = [
|
|
{
|
|
input: 'Check out https://example.com/page for more info',
|
|
expected: 'Check out - (link to example.com) - for more info',
|
|
},
|
|
{
|
|
input: 'This is a hyp- henated word',
|
|
expected: 'This is a hyphenated word',
|
|
},
|
|
{
|
|
input: 'This is *bold* text',
|
|
expected: 'This is bold text',
|
|
},
|
|
{
|
|
input: 'Multiple spaces',
|
|
expected: 'Multiple spaces',
|
|
},
|
|
];
|
|
|
|
for (const { input, expected } of cases) {
|
|
expect(preprocessSentenceForAudio(input)).toBe(expected);
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('splitTextToTtsBlocks (PDF-oriented)', () => {
|
|
test('returns [] for empty input', () => {
|
|
expect(splitTextToTtsBlocks('')).toEqual([]);
|
|
expect(splitTextToTtsBlocks(' ')).toEqual([]);
|
|
expect(splitTextToTtsBlocks('\n\n')).toEqual([]);
|
|
});
|
|
|
|
test('does not treat single newlines as paragraph boundaries', () => {
|
|
const input =
|
|
'The first line ends with a comma,\n' +
|
|
'but the sentence continues on the next line and ends here.\n' +
|
|
'And this is the second sentence.';
|
|
const result = splitTextToTtsBlocks(input);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expectNormalizedBlocks(result, PDF_MAX_BLOCK_LENGTH);
|
|
expect(result[0]).toBe(
|
|
'The first line ends with a comma, but the sentence continues on the next line and ends here. And this is the second sentence.'
|
|
);
|
|
});
|
|
|
|
test('treats blank lines (double newlines) as paragraph boundaries', () => {
|
|
const input = 'First paragraph.\n\nSecond paragraph.';
|
|
const result = splitTextToTtsBlocks(input);
|
|
|
|
expect(result.length).toBeGreaterThanOrEqual(2);
|
|
expectNormalizedBlocks(result, PDF_MAX_BLOCK_LENGTH);
|
|
});
|
|
|
|
test('repairs missing whitespace between sentences (common PDF artifact)', () => {
|
|
const input = 'This ends.Here starts.';
|
|
const normalized = normalizeTextForTts(input);
|
|
expect(normalized).toContain('ends. Here');
|
|
});
|
|
|
|
test('does not break decimals when repairing sentence boundaries', () => {
|
|
const input = 'Pi is 3.14.Next sentence.';
|
|
const normalized = normalizeTextForTts(input);
|
|
expect(normalized).toContain('3.14');
|
|
});
|
|
|
|
test('enforces max block length on long content', () => {
|
|
const sentence = `${'A'.repeat(100)}.`; // 101 chars
|
|
const input = Array(8).fill(sentence).join(' '); // guaranteed to exceed MAX_BLOCK_LENGTH
|
|
const result = splitTextToTtsBlocks(input);
|
|
|
|
expect(result.length).toBeGreaterThan(1);
|
|
expectNormalizedBlocks(result, MAX_BLOCK_LENGTH);
|
|
});
|
|
|
|
test('splits oversized content with no punctuation', () => {
|
|
const input = Array(1200).fill('word').join(' ');
|
|
const result = splitTextToTtsBlocks(input);
|
|
|
|
expect(result.length).toBeGreaterThan(1);
|
|
expectNormalizedBlocks(result, MAX_BLOCK_LENGTH);
|
|
});
|
|
|
|
test('splits extremely long unbroken tokens', () => {
|
|
const input = 'A'.repeat(1200);
|
|
const result = splitTextToTtsBlocks(input);
|
|
|
|
expect(result.length).toBeGreaterThan(1);
|
|
expectNormalizedBlocks(result, MAX_BLOCK_LENGTH);
|
|
});
|
|
|
|
test('prefers sentence punctuation when chunking long PDF-like text', () => {
|
|
const sentences = Array.from(
|
|
{ length: 80 },
|
|
(_, i) => `Sentence ${i} has filler words to vary length slightly number ${i}.`
|
|
);
|
|
const input = sentences.join(''); // no whitespace after '.' between sentences
|
|
const result = splitTextToTtsBlocks(input);
|
|
|
|
expect(result.length).toBeGreaterThan(1);
|
|
expectNormalizedBlocks(result, PDF_MAX_BLOCK_LENGTH);
|
|
|
|
// When sentence punctuation exists, blocks should usually end at punctuation/closers.
|
|
// This guards against regressions where we cut mid-word/mid-sentence too often.
|
|
for (const block of result) {
|
|
expect(block).toMatch(/[.!?]["'”’)\]]*$/);
|
|
}
|
|
});
|
|
|
|
test('allows a long sentence to extend to its ending punctuation', () => {
|
|
// Create a single sentence that exceeds MAX_BLOCK_LENGTH, but ends with a period
|
|
// within the forward-search overflow window.
|
|
const input = `${'word '.repeat(110)}end. Next.`;
|
|
const result = splitTextToTtsBlocks(input);
|
|
|
|
expect(result.length).toBeGreaterThan(1);
|
|
// This case is specifically asserting we may exceed MAX_BLOCK_LENGTH to reach punctuation,
|
|
// but should still remain bounded by the overflow policy.
|
|
expectNormalizedBlocks(result, PDF_MAX_BLOCK_LENGTH);
|
|
expect(result[0]).toMatch(/end\.$/);
|
|
});
|
|
|
|
test('merges multi-sentence quoted dialogue', () => {
|
|
const input = 'He said, "First. Second." Then left.';
|
|
const result = splitTextToTtsBlocks(input);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expectNormalizedBlocks(result, PDF_MAX_BLOCK_LENGTH);
|
|
expect(result[0]).toContain('"First. Second."');
|
|
});
|
|
});
|
|
|
|
test.describe('splitTextToTtsBlocksEPUB (highlight-friendly)', () => {
|
|
test('returns [] for empty input', () => {
|
|
expect(splitTextToTtsBlocksEPUB('')).toEqual([]);
|
|
expect(splitTextToTtsBlocksEPUB(' ')).toEqual([]);
|
|
expect(splitTextToTtsBlocksEPUB('\n\n')).toEqual([]);
|
|
});
|
|
|
|
test('treats single newlines as paragraph boundaries', () => {
|
|
const input = 'One.\nTwo.';
|
|
const result = splitTextToTtsBlocksEPUB(input);
|
|
expect(result).toHaveLength(2);
|
|
expectNormalizedBlocks(result, MAX_BLOCK_LENGTH);
|
|
expect(result[0]).toBe('One.');
|
|
expect(result[1]).toBe('Two.');
|
|
});
|
|
|
|
test('splits oversized sentences to keep blocks bounded', () => {
|
|
const input = Array(1200).fill('word').join(' '); // no punctuation; guaranteed to exceed MAX_BLOCK_LENGTH
|
|
const result = splitTextToTtsBlocksEPUB(input);
|
|
|
|
expect(result.length).toBeGreaterThan(1);
|
|
expectNormalizedBlocks(result, MAX_BLOCK_LENGTH);
|
|
});
|
|
});
|
|
|
|
test.describe('normalizeTextForTts', () => {
|
|
test('returns a single normalized string without newlines', () => {
|
|
const input = 'Hello.\nWorld.\n\nNext paragraph.';
|
|
const normalized = normalizeTextForTts(input);
|
|
expect(normalized).not.toMatch(/\n/);
|
|
expect(normalized).not.toMatch(/\s{2,}/);
|
|
expect(normalized.length).toBeGreaterThan(0);
|
|
});
|
|
});
|