chore(config): remove smartSentenceSplitting option and related code paths

Eliminate the smartSentenceSplitting configuration flag from application state, UI, and all TTS and audiobook adapter logic. Consolidate TTS segment planning to always include context units, streamlining code and reducing conditional branches. Update tests and type definitions to reflect the removal of this feature toggle.
This commit is contained in:
Richard R 2026-05-20 04:51:10 -06:00
parent bb3eb40966
commit b679bf736c
18 changed files with 197 additions and 131 deletions

View file

@ -101,7 +101,6 @@ export function useEpubDocument(documentId?: string): EpubDocumentState {
baseUrl,
providerRef,
ttsSegmentMaxBlockLength,
smartSentenceSplitting,
epubTheme,
epubHighlightEnabled,
} = useConfig();
@ -239,22 +238,13 @@ export function useEpubDocument(documentId?: string): EpubDocumentState {
const leadingPreview = collectLeadingContextFromRange(range);
const continuationPreview = collectContinuationFromRange(range);
if (smartSentenceSplitting) {
setTTSText(textContent, {
shouldPause,
location: start.cfi,
previousText: leadingPreview,
nextLocation: end.cfi,
nextText: continuationPreview
});
} else {
// When smart splitting is disabled, behave like the original implementation:
// send only the current page/location text without any continuation preview.
setTTSText(textContent, {
shouldPause,
location: start.cfi,
});
}
setTTSText(textContent, {
shouldPause,
location: start.cfi,
previousText: leadingPreview,
nextLocation: end.cfi,
nextText: continuationPreview
});
setCurrDocText(textContent);
return textContent;
@ -262,7 +252,7 @@ export function useEpubDocument(documentId?: string): EpubDocumentState {
console.error('Error extracting EPUB text:', error);
return '';
}
}, [setRenderedTextMaps, setTTSText, smartSentenceSplitting]);
}, [setRenderedTextMaps, setTTSText]);
/**
* Resolves a draft EPUB locator (typically `{ readerType: 'epub', location:

View file

@ -68,7 +68,6 @@ export function useHtmlDocument(): HtmlDocumentState {
apiKey,
baseUrl,
providerRef,
smartSentenceSplitting,
ttsSegmentMaxBlockLength,
} = useConfig();
@ -133,10 +132,9 @@ export function useHtmlDocument(): HtmlDocumentState {
createHtmlAudiobookSourceAdapter({
blocks,
isTxt,
smartSentenceSplitting,
maxBlockLength: ttsSegmentMaxBlockLength,
}),
[blocks, isTxt, smartSentenceSplitting, ttsSegmentMaxBlockLength],
[blocks, isTxt, ttsSegmentMaxBlockLength],
);
const createFullAudioBook = useCallback(

View file

@ -37,6 +37,7 @@ import {
highlightWordIndex,
} from '@/lib/client/pdf';
import { buildPageTextFromBlocks } from '@/lib/client/pdf-block-text';
import { buildPdfPageSourceUnits, buildPdfPrefetchPayload } from '@/lib/client/pdf-tts-planning';
import type { CanonicalTtsSourceUnit } from '@/lib/shared/tts-segment-plan';
import {
DEFAULT_DOCUMENT_SETTINGS,
@ -130,7 +131,6 @@ export function usePdfDocument(): PdfDocumentState {
apiKey,
baseUrl,
providerRef,
smartSentenceSplitting,
segmentPreloadDepthPages,
ttsSegmentMaxBlockLength,
} = useConfig();
@ -149,9 +149,8 @@ export function usePdfDocument(): PdfDocumentState {
const audiobookAdapter = useMemo(() => createPdfAudiobookSourceAdapter({
parsed: parsedDocument ?? undefined,
settings: documentSettings,
smartSentenceSplitting,
maxBlockLength: ttsSegmentMaxBlockLength,
}), [parsedDocument, documentSettings, smartSentenceSplitting, ttsSegmentMaxBlockLength]);
}), [parsedDocument, documentSettings, ttsSegmentMaxBlockLength]);
const pageTextCacheRef = useRef<Map<number, string>>(new Map());
const [currDocPage, setCurrDocPage] = useState<number>(currDocPageNumber);
@ -266,20 +265,7 @@ export function usePdfDocument(): PdfDocumentState {
const sourceUnitsFromParsedPage = (pageNum: number): CanonicalTtsSourceUnit[] => {
const page = pageFromParsed(pageNum);
if (!page) return [];
const skipKinds = new Set(documentSettings.pdf?.skipBlockKinds ?? []);
return page.blocks
.filter((block) => !skipKinds.has(block.kind))
.map((block) => ({
sourceKey: `pdf:${pageNum}:${block.id}`,
text: block.text,
locator: {
readerType: 'pdf',
page: pageNum,
blockId: block.id,
} as TTSSegmentLocator,
}))
.filter((unit) => unit.text.trim().length > 0);
return buildPdfPageSourceUnits(page, pageNum, documentSettings.pdf?.skipBlockKinds ?? []);
};
const getPageText = async (pageNumber: number, shouldCache = false): Promise<string> => {
@ -327,16 +313,15 @@ export function usePdfDocument(): PdfDocumentState {
prevPageNumber ? getPageText(prevPageNumber) : Promise.resolve<string | undefined>(undefined),
...upcomingPageNumbers.map((pageNum) => getPageText(pageNum, true)),
]);
const nextText = upcomingTexts[0];
const nextSourceUnits = nextPageNumber ? sourceUnitsFromParsedPage(nextPageNumber) : [];
const additionalUpcoming = upcomingPageNumbers
.slice(1)
.map((pageNum, idx) => ({
location: pageNum,
text: upcomingTexts[idx + 1] || '',
sourceUnits: sourceUnitsFromParsedPage(pageNum),
}))
.filter((item) => item.text.trim().length > 0);
const {
nextText,
nextSourceUnits,
additionalUpcoming,
} = buildPdfPrefetchPayload(
upcomingPageNumbers,
upcomingTexts,
sourceUnitsFromParsedPage,
);
if (generation !== pdfDocGenerationRef.current || pdfDocumentRef.current !== currentPdf) {
return;

View file

@ -137,7 +137,6 @@ function sanitizePreferencesPatch(
break;
case 'skipBlank':
case 'epubTheme':
case 'smartSentenceSplitting':
case 'pdfHighlightEnabled':
case 'pdfWordHighlightEnabled':
case 'epubHighlightEnabled':

View file

@ -113,7 +113,6 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: {
viewType,
skipBlank,
epubTheme,
smartSentenceSplitting,
segmentPreloadDepthPages,
segmentPreloadSentenceLookahead,
ttsSegmentMaxBlockLength,
@ -272,13 +271,6 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: {
/>
)}
<ToggleRow
label="Smart sentence splitting"
description="Merge fragments across pages/sections."
checked={smartSentenceSplitting}
onChange={(checked) => updateConfigKey('smartSentenceSplitting', checked)}
variant="flat"
/>
<div className="space-y-3 pt-1">
<RangeSetting

View file

@ -29,7 +29,6 @@ interface ConfigContextType {
voice: string;
skipBlank: boolean;
epubTheme: boolean;
smartSentenceSplitting: boolean;
segmentPreloadDepthPages: number;
segmentPreloadSentenceLookahead: number;
ttsSegmentMaxBlockLength: number;
@ -352,7 +351,6 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
ttsModel,
ttsInstructions,
savedVoices,
smartSentenceSplitting,
segmentPreloadDepthPages,
segmentPreloadSentenceLookahead,
ttsSegmentMaxBlockLength,
@ -475,7 +473,6 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
voice,
skipBlank,
epubTheme,
smartSentenceSplitting,
segmentPreloadDepthPages,
segmentPreloadSentenceLookahead,
ttsSegmentMaxBlockLength,

View file

@ -386,7 +386,6 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
ttsInstructions: configTTSInstructions,
updateConfigKey,
skipBlank,
smartSentenceSplitting,
segmentPreloadDepthPages,
segmentPreloadSentenceLookahead,
ttsSegmentMaxBlockLength,
@ -1164,7 +1163,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
const currentSourceKeySet = new Set(effectiveCurrentUnits.map((unit) => unit.sourceKey));
const contextSourceUnits: CanonicalTtsSourceUnit[] = [];
if (smartSentenceSplitting && normalizedOptions.previousText?.trim()) {
if (normalizedOptions.previousText?.trim()) {
const previousLocation = normalizedOptions.previousLocation;
contextSourceUnits.push({
sourceKey: previousLocation !== undefined
@ -1221,9 +1220,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
}
}
for (const item of pendingPrefetches) {
if (smartSentenceSplitting) {
sourceUnits.push(...item.sourceUnits);
}
sourceUnits.push(...item.sourceUnits);
}
const plan = planCanonicalTtsSegments(sourceUnits, {
@ -1232,27 +1229,12 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
keyPrefix: buildSegmentKeyPrefix(documentId, activeReaderType),
enforceSourceBoundaries: activeReaderType === 'pdf' && currentUnits !== null && currentUnits.length > 0,
});
const currentSegments = smartSentenceSplitting
? plan.segments.filter((segment) => currentSourceKeySet.has(segment.ownerSourceKey))
: effectiveCurrentUnits.flatMap((unit) =>
planCanonicalTtsSegments([unit], {
readerType: activeReaderType,
maxBlockLength: ttsSegmentMaxBlockLength,
keyPrefix: buildSegmentKeyPrefix(documentId, activeReaderType),
enforceSourceBoundaries: activeReaderType === 'pdf' && currentUnits !== null && currentUnits.length > 0,
}).segments);
const currentSegments = plan.segments.filter((segment) => currentSourceKeySet.has(segment.ownerSourceKey));
const newSentences = currentSegments.map((segment) => segment.text);
for (const item of pendingPrefetches) {
const sourceKeys = new Set(item.sourceUnits.map((unit) => unit.sourceKey));
const planned = smartSentenceSplitting
? plan.segments.filter((segment) => sourceKeys.has(segment.ownerSourceKey))
: planCanonicalTtsSegments(item.sourceUnits, {
readerType: activeReaderType,
maxBlockLength: ttsSegmentMaxBlockLength,
keyPrefix: buildSegmentKeyPrefix(documentId, activeReaderType),
enforceSourceBoundaries: activeReaderType === 'pdf' && currentUnits !== null && currentUnits.length > 0,
}).segments;
const planned = plan.segments.filter((segment) => sourceKeys.has(segment.ownerSourceKey));
if (planned.length > 0) {
plannedSegmentsByLocationRef.current.set(normalizeLocationKey(item.location), planned);
}
@ -1333,8 +1315,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
setCurrentWordIndex(null);
if (
smartSentenceSplitting
&& !isEPUB
!isEPUB
&& normalizedOptions.nextLocation !== undefined
&& effectiveCurrentUnits.length === 1
) {
@ -1384,7 +1365,6 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
abortAudio,
isEPUB,
activeReaderType,
smartSentenceSplitting,
invalidatePlaybackRun,
currDocPage,
documentId,
@ -2483,7 +2463,6 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
? currentSourceContextUnitsRef.current
: (currentSourceUnitRef.current ? [currentSourceUnitRef.current] : []);
const sourceUnits: CanonicalTtsSourceUnit[] = buildWalkerPlanningSourceUnits(
smartSentenceSplitting,
liveContextUnits,
upcomingUnits,
);
@ -2835,7 +2814,6 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
openApiBaseUrl,
providerModelPolicy.supportsInstructions,
ttsInstructions,
smartSentenceSplitting,
onTTSStart,
onTTSComplete,
processSentence,

View file

@ -5,7 +5,6 @@ import type { HtmlBlock } from '@/lib/client/html/blocks';
interface HtmlAudiobookAdapterOptions {
blocks: HtmlBlock[];
isTxt: boolean;
smartSentenceSplitting: boolean;
maxBlockLength?: number;
/**
* For markdown: any heading with `headingLevel <= chapterHeadingLevel`
@ -83,7 +82,6 @@ function buildChapterDrafts({
function chapterText(
draft: ChapterDraft,
smartSentenceSplitting: boolean,
maxBlockLength?: number,
): string {
const joined = draft.blocks
@ -91,14 +89,14 @@ function chapterText(
.filter((t) => t && t.trim())
.join('\n\n');
if (!joined) return '';
return smartSentenceSplitting ? normalizeTextForTts(joined, { maxBlockLength }) : joined;
return normalizeTextForTts(joined, { maxBlockLength });
}
function preparedChapters(options: HtmlAudiobookAdapterOptions): PreparedAudiobookChapter[] {
const drafts = buildChapterDrafts(options);
const out: PreparedAudiobookChapter[] = [];
for (const draft of drafts) {
const text = chapterText(draft, options.smartSentenceSplitting, options.maxBlockLength);
const text = chapterText(draft, options.maxBlockLength);
if (!text.trim()) continue;
out.push({
index: out.length,

View file

@ -7,13 +7,11 @@ import { DEFAULT_DOCUMENT_SETTINGS } from '@/types/document-settings';
interface PdfAudiobookAdapterOptions {
parsed?: ParsedPdfDocument;
settings?: DocumentSettings;
smartSentenceSplitting: boolean;
maxBlockLength?: number;
}
function chapterTextFromBlocks(
blocks: ParsedPdfBlock[],
smartSentenceSplitting: boolean,
maxBlockLength?: number,
): string {
const text = blocks
@ -21,18 +19,16 @@ function chapterTextFromBlocks(
.filter(Boolean)
.join('\n\n');
if (!text) return '';
return smartSentenceSplitting ? normalizeTextForTts(text, { maxBlockLength }) : text;
return normalizeTextForTts(text, { maxBlockLength });
}
function prepareParsedChapters({
parsed,
settings,
smartSentenceSplitting,
maxBlockLength,
}: {
parsed: ParsedPdfDocument;
settings: DocumentSettings;
smartSentenceSplitting: boolean;
maxBlockLength?: number;
}): PreparedAudiobookChapter[] {
const skip = new Set(settings.pdf?.skipBlockKinds ?? DEFAULT_DOCUMENT_SETTINGS.pdf?.skipBlockKinds ?? []);
@ -49,7 +45,7 @@ function prepareParsedChapters({
const flush = () => {
if (!currentBlocks.length) return;
const text = chapterTextFromBlocks(currentBlocks, smartSentenceSplitting, maxBlockLength);
const text = chapterTextFromBlocks(currentBlocks, maxBlockLength);
if (text) {
chapters.push({
index: chapters.length,
@ -77,7 +73,6 @@ function prepareParsedChapters({
async function extractPreparedPdfChapters({
parsed,
settings = DEFAULT_DOCUMENT_SETTINGS,
smartSentenceSplitting,
maxBlockLength,
}: PdfAudiobookAdapterOptions): Promise<PreparedAudiobookChapter[]> {
if (!parsed) {
@ -87,7 +82,6 @@ async function extractPreparedPdfChapters({
return prepareParsedChapters({
parsed,
settings,
smartSentenceSplitting,
maxBlockLength,
});
}

View file

@ -194,8 +194,6 @@ function buildAppConfigFromRaw(raw: RawConfigMap): AppConfigRow {
voice: '',
skipBlank: raw.skipBlank === 'false' ? false : APP_CONFIG_DEFAULTS.skipBlank,
epubTheme: raw.epubTheme === 'true',
smartSentenceSplitting:
raw.smartSentenceSplitting === 'false' ? false : APP_CONFIG_DEFAULTS.smartSentenceSplitting,
headerMargin: raw.headerMargin ? parseFloat(raw.headerMargin) : APP_CONFIG_DEFAULTS.headerMargin,
footerMargin: raw.footerMargin ? parseFloat(raw.footerMargin) : APP_CONFIG_DEFAULTS.footerMargin,
leftMargin: raw.leftMargin ? parseFloat(raw.leftMargin) : APP_CONFIG_DEFAULTS.leftMargin,

View file

@ -29,10 +29,8 @@ export function selectUpcomingWalkerItems<T extends EpubWalkerLocationItem>(
* (previous/current) so walker boundary behavior aligns with setText.
*/
export function buildWalkerPlanningSourceUnits(
smartSentenceSplitting: boolean,
contextUnits: readonly CanonicalTtsSourceUnit[],
upcomingUnits: readonly CanonicalTtsSourceUnit[],
): CanonicalTtsSourceUnit[] {
if (!smartSentenceSplitting) return [...upcomingUnits];
return [...contextUnits, ...upcomingUnits];
}

View file

@ -0,0 +1,58 @@
import type { CanonicalTtsSourceUnit } from '@/lib/shared/tts-segment-plan';
import type { TTSSegmentLocator } from '@/types/client';
import type { ParsedPdfBlockKind, ParsedPdfPage } from '@/types/parsed-pdf';
type PdfUpcomingLocation = {
location: number;
text: string;
sourceUnits: CanonicalTtsSourceUnit[];
};
export function buildPdfPageSourceUnits(
page: ParsedPdfPage | undefined,
pageNum: number,
skipKinds: ParsedPdfBlockKind[] = [],
): CanonicalTtsSourceUnit[] {
if (!page) return [];
const skip = new Set(skipKinds);
return page.blocks
.filter((block) => !skip.has(block.kind))
.map((block) => ({
sourceKey: `pdf:${pageNum}:${block.id}`,
text: block.text,
locator: {
readerType: 'pdf',
page: pageNum,
blockId: block.id,
} as TTSSegmentLocator,
}))
.filter((unit) => unit.text.trim().length > 0);
}
export function buildPdfPrefetchPayload(
upcomingPageNumbers: number[],
upcomingTexts: string[],
sourceUnitsForPage: (pageNum: number) => CanonicalTtsSourceUnit[],
): {
nextText: string | undefined;
nextSourceUnits: CanonicalTtsSourceUnit[];
additionalUpcoming: PdfUpcomingLocation[];
} {
const nextPageNumber = upcomingPageNumbers[0];
const nextText = upcomingTexts[0];
const nextSourceUnits = nextPageNumber ? sourceUnitsForPage(nextPageNumber) : [];
const additionalUpcoming = upcomingPageNumbers
.slice(1)
.map((pageNum, idx) => ({
location: pageNum,
text: upcomingTexts[idx + 1] || '',
sourceUnits: sourceUnitsForPage(pageNum),
}))
.filter((item) => item.text.trim().length > 0);
return {
nextText,
nextSourceUnits,
additionalUpcoming,
};
}

View file

@ -62,7 +62,6 @@ export interface AppConfigValues {
ttsModel: string;
ttsInstructions: string;
savedVoices: SavedVoices;
smartSentenceSplitting: boolean;
segmentPreloadDepthPages: number;
segmentPreloadSentenceLookahead: number;
ttsSegmentMaxBlockLength: number;
@ -111,7 +110,6 @@ export function getAppConfigDefaults(): AppConfigValues {
ttsModel: defaultModel,
ttsInstructions: '',
savedVoices: {},
smartSentenceSplitting: true,
segmentPreloadDepthPages: 1,
segmentPreloadSentenceLookahead: 3,
ttsSegmentMaxBlockLength: 450,

View file

@ -7,7 +7,6 @@ export const SYNCED_PREFERENCE_KEYS = [
'voice',
'skipBlank',
'epubTheme',
'smartSentenceSplitting',
'segmentPreloadDepthPages',
'segmentPreloadSentenceLookahead',
'ttsSegmentMaxBlockLength',

View file

@ -30,7 +30,6 @@ test.describe('createHtmlAudiobookSourceAdapter (markdown chapter splitting)', (
const adapter = createHtmlAudiobookSourceAdapter({
blocks,
isTxt: false,
smartSentenceSplitting: false,
});
const chapters = await adapter.prepareChapters();
expect(chapters.map((c) => c.title)).toEqual(['Alpha', 'Beta', 'Delta']);
@ -46,7 +45,6 @@ test.describe('createHtmlAudiobookSourceAdapter (markdown chapter splitting)', (
const adapter = createHtmlAudiobookSourceAdapter({
blocks,
isTxt: false,
smartSentenceSplitting: false,
});
const chapters = await adapter.prepareChapters();
expect(chapters.map((c) => c.title)).toEqual(['Introduction', 'First Heading']);
@ -64,7 +62,6 @@ test.describe('createHtmlAudiobookSourceAdapter (markdown chapter splitting)', (
const adapter = createHtmlAudiobookSourceAdapter({
blocks,
isTxt: false,
smartSentenceSplitting: false,
fallbackBlocksPerChapter: 3,
});
const chapters = await adapter.prepareChapters();
@ -88,7 +85,6 @@ test.describe('createHtmlAudiobookSourceAdapter (markdown chapter splitting)', (
const adapter = createHtmlAudiobookSourceAdapter({
blocks,
isTxt: false,
smartSentenceSplitting: false,
chapterHeadingLevel: 1,
});
const chapters = await adapter.prepareChapters();
@ -109,7 +105,6 @@ test.describe('createHtmlAudiobookSourceAdapter (txt chapter splitting)', () =>
const adapter = createHtmlAudiobookSourceAdapter({
blocks,
isTxt: true,
smartSentenceSplitting: false,
});
const chapters = await adapter.prepareChapters();
expect(chapters.map((c) => c.title)).toEqual(['Part 1', 'Part 2', 'Part 3']);
@ -128,7 +123,6 @@ test.describe('createHtmlAudiobookSourceAdapter (txt chapter splitting)', () =>
const adapter = createHtmlAudiobookSourceAdapter({
blocks,
isTxt: true,
smartSentenceSplitting: false,
});
const chapters = await adapter.prepareChapters();
expect(chapters.length).toBe(1);
@ -142,7 +136,6 @@ test.describe('createHtmlAudiobookSourceAdapter — prepareChapter', () => {
const adapter = createHtmlAudiobookSourceAdapter({
blocks,
isTxt: false,
smartSentenceSplitting: false,
});
const list = await adapter.prepareChapters();
const second = await adapter.prepareChapter(1);
@ -155,7 +148,6 @@ test.describe('createHtmlAudiobookSourceAdapter — prepareChapter', () => {
const adapter = createHtmlAudiobookSourceAdapter({
blocks,
isTxt: false,
smartSentenceSplitting: false,
});
await expect(adapter.prepareChapter(42)).rejects.toThrow(/invalid chapter index/i);
});

View file

@ -61,7 +61,6 @@ test.describe('pdf audiobook adapter', () => {
const adapter = createPdfAudiobookSourceAdapter({
parsed,
settings,
smartSentenceSplitting: false,
});
const chapters = await adapter.prepareChapters();
@ -125,7 +124,6 @@ test.describe('pdf audiobook adapter', () => {
const adapter = createPdfAudiobookSourceAdapter({
parsed,
settings,
smartSentenceSplitting: false,
});
const chapters = await adapter.prepareChapters();

View file

@ -0,0 +1,107 @@
import { expect, test } from '@playwright/test';
import { buildPdfPageSourceUnits, buildPdfPrefetchPayload } from '../../src/lib/client/pdf-tts-planning';
import type { ParsedPdfPage } from '../../src/types/parsed-pdf';
function buildPage(pageNumber: number): ParsedPdfPage {
return {
pageNumber,
width: 800,
height: 1200,
blocks: [
{
id: `h-${pageNumber}`,
kind: 'header',
text: 'Header text',
fragments: [{ page: pageNumber, bbox: [0, 0, 1, 1], text: 'Header text', readingOrder: 0 }],
},
{
id: `p-${pageNumber}-a`,
kind: 'text',
text: `Paragraph A on page ${pageNumber}.`,
// Regression guard: fragment page can drift; locator must stay pinned
// to the requested page number for stable planning/grouping.
fragments: [{ page: pageNumber + 100, bbox: [0, 0, 1, 1], text: 'x', readingOrder: 1 }],
},
{
id: `p-${pageNumber}-b`,
kind: 'paragraph_title',
text: `Section title ${pageNumber}`,
fragments: [{ page: pageNumber, bbox: [0, 0, 1, 1], text: 'y', readingOrder: 2 }],
},
{
id: `empty-${pageNumber}`,
kind: 'text',
text: ' ',
fragments: [{ page: pageNumber, bbox: [0, 0, 1, 1], text: 'z', readingOrder: 3 }],
},
],
};
}
test.describe('pdf tts planning helpers', () => {
test('buildPdfPageSourceUnits uses parsed blocks, honors skip kinds, and pins locator page', () => {
const page = buildPage(2);
const units = buildPdfPageSourceUnits(page, 2, ['header']);
expect(units.map((u) => u.sourceKey)).toEqual([
'pdf:2:p-2-a',
'pdf:2:p-2-b',
]);
expect(units.map((u) => u.text)).toEqual([
'Paragraph A on page 2.',
'Section title 2',
]);
expect(units.map((u) => u.locator)).toEqual([
{ readerType: 'pdf', page: 2, blockId: 'p-2-a' },
{ readerType: 'pdf', page: 2, blockId: 'p-2-b' },
]);
});
test('buildPdfPrefetchPayload includes parsed sourceUnits for next and upcoming pages', () => {
const pages = new Map<number, ParsedPdfPage>([
[2, buildPage(2)],
[3, buildPage(3)],
[4, buildPage(4)],
]);
const payload = buildPdfPrefetchPayload(
[2, 3, 4],
['Page 2 text', 'Page 3 text', 'Page 4 text'],
(pageNum) => buildPdfPageSourceUnits(pages.get(pageNum), pageNum, ['header']),
);
expect(payload.nextText).toBe('Page 2 text');
expect(payload.nextSourceUnits.map((u) => u.sourceKey)).toEqual([
'pdf:2:p-2-a',
'pdf:2:p-2-b',
]);
expect(payload.additionalUpcoming).toHaveLength(2);
expect(payload.additionalUpcoming[0]).toMatchObject({
location: 3,
text: 'Page 3 text',
});
expect(payload.additionalUpcoming[0].sourceUnits.map((u) => u.sourceKey)).toEqual([
'pdf:3:p-3-a',
'pdf:3:p-3-b',
]);
expect(payload.additionalUpcoming[1].sourceUnits.map((u) => u.sourceKey)).toEqual([
'pdf:4:p-4-a',
'pdf:4:p-4-b',
]);
});
test('buildPdfPrefetchPayload drops blank upcoming text entries', () => {
const pages = new Map<number, ParsedPdfPage>([
[2, buildPage(2)],
[3, buildPage(3)],
]);
const payload = buildPdfPrefetchPayload(
[2, 3],
['Page 2 text', ' '],
(pageNum) => buildPdfPageSourceUnits(pages.get(pageNum), pageNum, ['header']),
);
expect(payload.nextText).toBe('Page 2 text');
expect(payload.additionalUpcoming).toHaveLength(0);
});
});

View file

@ -30,7 +30,7 @@ test.describe('EPUB walker preload helpers', () => {
expect(selectUpcomingWalkerItems(items, 'epubcfi(/6/2!/4/2)', 0)).toEqual([]);
});
test('buildWalkerPlanningSourceUnits includes live context when smart splitting is enabled', () => {
test('buildWalkerPlanningSourceUnits includes live context', () => {
const contextUnits: CanonicalTtsSourceUnit[] = [
{ sourceKey: 'previous:page-a', text: 'prev sentence', locator: null },
{ sourceKey: 'page-a', text: 'current sentence', locator: { readerType: 'epub', location: 'page-a' } },
@ -40,7 +40,7 @@ test.describe('EPUB walker preload helpers', () => {
{ sourceKey: 'page-c', text: 'upcoming two', locator: { readerType: 'epub', location: 'page-c' } },
];
const planned = buildWalkerPlanningSourceUnits(true, contextUnits, upcomingUnits);
const planned = buildWalkerPlanningSourceUnits(contextUnits, upcomingUnits);
expect(planned.map((item) => item.sourceKey)).toEqual([
'previous:page-a',
'page-a',
@ -48,17 +48,4 @@ test.describe('EPUB walker preload helpers', () => {
'page-c',
]);
});
test('buildWalkerPlanningSourceUnits uses only upcoming units when smart splitting is disabled', () => {
const contextUnits: CanonicalTtsSourceUnit[] = [
{ sourceKey: 'previous:page-a', text: 'prev sentence', locator: null },
{ sourceKey: 'page-a', text: 'current sentence', locator: { readerType: 'epub', location: 'page-a' } },
];
const upcomingUnits: CanonicalTtsSourceUnit[] = [
{ sourceKey: 'page-b', text: 'upcoming one', locator: { readerType: 'epub', location: 'page-b' } },
];
const planned = buildWalkerPlanningSourceUnits(false, contextUnits, upcomingUnits);
expect(planned.map((item) => item.sourceKey)).toEqual(['page-b']);
});
});