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.
60 lines
2.1 KiB
TypeScript
60 lines
2.1 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
|
|
import {
|
|
buildMonotonicWordToTokenMap,
|
|
tokenizeCanonicalSegment,
|
|
} from '../../src/lib/shared/epub-word-highlight';
|
|
import type { CanonicalTtsSegment } from '../../src/lib/shared/tts-segment-plan';
|
|
import type { TTSSentenceAlignment } from '../../src/types/tts';
|
|
|
|
const segment = (text: string, offset = 0): CanonicalTtsSegment => ({
|
|
key: `segment:${offset}:${text}`,
|
|
ordinal: 0,
|
|
text,
|
|
ownerSourceKey: 'str:epubcfi(/6/2)',
|
|
ownerLocator: { location: 'epubcfi(/6/2)', readerType: 'epub' },
|
|
startAnchor: { sourceKey: 'str:epubcfi(/6/2)', offset },
|
|
endAnchor: { sourceKey: 'str:epubcfi(/6/2)', offset: offset + text.length },
|
|
spansSourceBoundary: false,
|
|
});
|
|
|
|
const alignmentWords = (words: string[]): TTSSentenceAlignment['words'] =>
|
|
words.map((word, index) => ({
|
|
text: word,
|
|
startSec: index,
|
|
endSec: index + 0.5,
|
|
charStart: 0,
|
|
charEnd: word.length,
|
|
}));
|
|
|
|
test.describe('EPUB word highlight mapping', () => {
|
|
test('tokenizes canonical segment words with source offsets', () => {
|
|
const tokens = tokenizeCanonicalSegment(segment('"Hello," she said.', 12));
|
|
|
|
expect(tokens).toEqual([
|
|
{ norm: 'hello', sourceStart: 13, sourceEnd: 18 },
|
|
{ norm: 'she', sourceStart: 21, sourceEnd: 24 },
|
|
{ norm: 'said', sourceStart: 25, sourceEnd: 29 },
|
|
]);
|
|
});
|
|
|
|
test('maps repeated words monotonically instead of jumping to later duplicates', () => {
|
|
const tokens = tokenizeCanonicalSegment(segment('the light and the shadow and the light returned'));
|
|
const map = buildMonotonicWordToTokenMap(
|
|
alignmentWords(['the', 'light', 'and', 'the', 'shadow', 'and', 'the', 'light', 'returned']),
|
|
tokens,
|
|
);
|
|
|
|
expect(map).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
|
});
|
|
|
|
test('leaves unmatched alignment words unhighlighted instead of borrowing a neighbor', () => {
|
|
const tokens = tokenizeCanonicalSegment(segment('alpha beta gamma'));
|
|
const map = buildMonotonicWordToTokenMap(
|
|
alignmentWords(['alpha', 'missing', 'gamma']),
|
|
tokens,
|
|
);
|
|
|
|
expect(map).toEqual([0, -1, 2]);
|
|
});
|
|
});
|