Refactor TTS segment identification to support canonical segment keys, decoupling segment IDs from locator and index for more robust deduplication and handoff. Add `segmentKey` to segment types, manifest, and ensure route. Implement natural sorting for EPUB CFI locations and segment locators using new comparison utilities. Update sidebar and manifest helpers to use locator-aware sorting. Add foundational modules for EPUB word highlighting, TTS segment planning, and EPUB handoff logic. Expand types and tests to cover new canonical segment and locator behaviors. BREAKING CHANGE: TTS segment IDs now support canonical segment keys; APIs and manifest consumers must handle `segmentKey` and updated locator comparison logic.
125 lines
4 KiB
TypeScript
125 lines
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('sorts EPUB CFI locations naturally instead of lexicographically', () => {
|
|
const rows = [
|
|
{ groupKey: 'ten', segmentIndex: 0, locator: { location: 'epubcfi(/6/10!/4/2)', readerType: 'epub' as const } },
|
|
{ groupKey: 'two-b', segmentIndex: 1, locator: { location: 'epubcfi(/6/2!/4/2)', readerType: 'epub' as const } },
|
|
{ groupKey: 'two-a', segmentIndex: 0, locator: { location: 'epubcfi(/6/2!/4/2)', readerType: 'epub' as const } },
|
|
];
|
|
|
|
const sorted = rows.sort(compareManifestSegments);
|
|
expect(sorted.map((row) => row.groupKey)).toEqual(['two-a', 'two-b', 'ten']);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|