openreader/tests/unit/tts-segments-manifest.spec.ts
Richard R 265e1cb56a feat(tts,config,ui): add advanced segment preloading and block length controls
Introduce configurable segment preloading depth, sentence lookahead, and TTS segment max block length for both PDF and EPUB readers. Add new settings to user preferences, config context, and document settings UI. Refactor TTS segment splitting logic to support per-user max block length and propagate these options through PDF/EPUB adapters and TTS segment generation. Update API, manifest pagination, and segment deduplication for improved performance and correctness. Add tests for new NLP options and manifest logic.

BREAKING CHANGE: TTS segment splitting and manifest APIs now require max block length and preloading parameters; user config schema updated.
2026-05-07 12:26:08 -06:00

114 lines
3.4 KiB
TypeScript

import { expect, test } from '@playwright/test';
import {
compareManifestSegments,
decodeManifestCursor,
dedupeManifestVariants,
encodeManifestCursor,
parseManifestPageSize,
} from '../../src/lib/server/tts/segments-manifest';
test.describe('tts segments manifest helpers', () => {
test('dedupe prefers completed variant over newer pending for same settings key', () => {
const variants = dedupeManifestVariants([
{
dedupeKey: 'settings:abc',
variant: {
segmentId: 'old-completed',
settings: null,
audioPresignUrl: '/audio/old',
audioFallbackUrl: '/audio/old/fallback',
durationMs: 1100,
status: 'completed',
textLength: 12,
alignmentWordCount: 2,
audioKey: 'old',
updatedAt: 100,
},
},
{
dedupeKey: 'settings:abc',
variant: {
segmentId: 'new-pending',
settings: null,
audioPresignUrl: null,
audioFallbackUrl: null,
durationMs: null,
status: 'pending',
textLength: 15,
alignmentWordCount: 0,
audioKey: null,
updatedAt: 200,
},
},
]);
expect(variants).toHaveLength(1);
expect(variants[0].segmentId).toBe('old-completed');
expect(variants[0].status).toBe('completed');
});
test('dedupe uses status rank as tiebreaker when updatedAt is equal', () => {
const variants = dedupeManifestVariants([
{
dedupeKey: 'settings:abc',
variant: {
segmentId: 'pending',
settings: null,
audioPresignUrl: null,
audioFallbackUrl: null,
durationMs: null,
status: 'pending',
textLength: 12,
alignmentWordCount: 0,
audioKey: null,
updatedAt: 123,
},
},
{
dedupeKey: 'settings:abc',
variant: {
segmentId: 'completed',
settings: null,
audioPresignUrl: '/audio/new',
audioFallbackUrl: '/audio/new/fallback',
durationMs: 1200,
status: 'completed',
textLength: 12,
alignmentWordCount: 2,
audioKey: 'new',
updatedAt: 123,
},
},
]);
expect(variants).toHaveLength(1);
expect(variants[0].segmentId).toBe('completed');
expect(variants[0].status).toBe('completed');
});
test('sorts segments deterministically by page, location, and segment index', () => {
const rows = [
{ groupKey: 'c', segmentIndex: 4, locator: { page: 2, location: 'z' } },
{ groupKey: 'a', segmentIndex: 1, locator: { page: 1, location: 'b' } },
{ groupKey: 'b', segmentIndex: 0, locator: { page: 1, location: 'a' } },
{ groupKey: 'd', segmentIndex: 2, locator: { page: 1, location: 'a' } },
];
const sorted = rows.sort(compareManifestSegments);
expect(sorted.map((row) => row.groupKey)).toEqual(['b', 'd', 'a', 'c']);
});
test('encodes and decodes cursors', () => {
const raw = '3|p:2|l:epubcfi(/6/2)|r:epub';
const encoded = encodeManifestCursor(raw);
expect(decodeManifestCursor(encoded)).toBe(raw);
expect(decodeManifestCursor('not-base64')).toBeNull();
});
test('clamps page size bounds', () => {
expect(parseManifestPageSize(null)).toBe(150);
expect(parseManifestPageSize('10')).toBe(25);
expect(parseManifestPageSize('900')).toBe(500);
expect(parseManifestPageSize('200')).toBe(200);
});
});