From b679bf736cd541d13b3a6fe8fd096a433d1857ca Mon Sep 17 00:00:00 2001 From: Richard R Date: Wed, 20 May 2026 04:51:10 -0600 Subject: [PATCH] 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. --- src/app/(app)/epub/[id]/useEpubDocument.ts | 26 ++--- src/app/(app)/html/[id]/useHtmlDocument.ts | 4 +- src/app/(app)/pdf/[id]/usePdfDocument.ts | 39 ++----- src/app/api/user/state/preferences/route.ts | 1 - src/components/documents/DocumentSettings.tsx | 8 -- src/contexts/ConfigContext.tsx | 3 - src/contexts/TTSContext.tsx | 32 +----- src/lib/client/audiobooks/adapters/html.ts | 6 +- src/lib/client/audiobooks/adapters/pdf.ts | 10 +- src/lib/client/dexie.ts | 2 - src/lib/client/epub/tts-epub-preload.ts | 2 - src/lib/client/pdf-tts-planning.ts | 58 ++++++++++ src/types/config.ts | 2 - src/types/user-state.ts | 1 - tests/unit/html-audiobook-adapter.spec.ts | 8 -- tests/unit/pdf-audiobook-adapter.spec.ts | 2 - tests/unit/pdf-tts-planning.spec.ts | 107 ++++++++++++++++++ tests/unit/tts-epub-preload.spec.ts | 17 +-- 18 files changed, 197 insertions(+), 131 deletions(-) create mode 100644 src/lib/client/pdf-tts-planning.ts create mode 100644 tests/unit/pdf-tts-planning.spec.ts diff --git a/src/app/(app)/epub/[id]/useEpubDocument.ts b/src/app/(app)/epub/[id]/useEpubDocument.ts index d487ce2..8c5a78a 100644 --- a/src/app/(app)/epub/[id]/useEpubDocument.ts +++ b/src/app/(app)/epub/[id]/useEpubDocument.ts @@ -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: diff --git a/src/app/(app)/html/[id]/useHtmlDocument.ts b/src/app/(app)/html/[id]/useHtmlDocument.ts index e8e854d..fe23c0f 100644 --- a/src/app/(app)/html/[id]/useHtmlDocument.ts +++ b/src/app/(app)/html/[id]/useHtmlDocument.ts @@ -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( diff --git a/src/app/(app)/pdf/[id]/usePdfDocument.ts b/src/app/(app)/pdf/[id]/usePdfDocument.ts index 5aafa0a..3476931 100644 --- a/src/app/(app)/pdf/[id]/usePdfDocument.ts +++ b/src/app/(app)/pdf/[id]/usePdfDocument.ts @@ -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>(new Map()); const [currDocPage, setCurrDocPage] = useState(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 => { @@ -327,16 +313,15 @@ export function usePdfDocument(): PdfDocumentState { prevPageNumber ? getPageText(prevPageNumber) : Promise.resolve(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; diff --git a/src/app/api/user/state/preferences/route.ts b/src/app/api/user/state/preferences/route.ts index 96191fe..34c5e54 100644 --- a/src/app/api/user/state/preferences/route.ts +++ b/src/app/api/user/state/preferences/route.ts @@ -137,7 +137,6 @@ function sanitizePreferencesPatch( break; case 'skipBlank': case 'epubTheme': - case 'smartSentenceSplitting': case 'pdfHighlightEnabled': case 'pdfWordHighlightEnabled': case 'epubHighlightEnabled': diff --git a/src/components/documents/DocumentSettings.tsx b/src/components/documents/DocumentSettings.tsx index 27d1576..0516b02 100644 --- a/src/components/documents/DocumentSettings.tsx +++ b/src/components/documents/DocumentSettings.tsx @@ -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 }: { /> )} - updateConfigKey('smartSentenceSplitting', checked)} - variant="flat" - />
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, diff --git a/src/lib/client/audiobooks/adapters/html.ts b/src/lib/client/audiobooks/adapters/html.ts index 9880c5a..f44e8ca 100644 --- a/src/lib/client/audiobooks/adapters/html.ts +++ b/src/lib/client/audiobooks/adapters/html.ts @@ -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, diff --git a/src/lib/client/audiobooks/adapters/pdf.ts b/src/lib/client/audiobooks/adapters/pdf.ts index a462c32..19c86e8 100644 --- a/src/lib/client/audiobooks/adapters/pdf.ts +++ b/src/lib/client/audiobooks/adapters/pdf.ts @@ -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 { if (!parsed) { @@ -87,7 +82,6 @@ async function extractPreparedPdfChapters({ return prepareParsedChapters({ parsed, settings, - smartSentenceSplitting, maxBlockLength, }); } diff --git a/src/lib/client/dexie.ts b/src/lib/client/dexie.ts index cf8e3ce..3bb4d0a 100644 --- a/src/lib/client/dexie.ts +++ b/src/lib/client/dexie.ts @@ -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, diff --git a/src/lib/client/epub/tts-epub-preload.ts b/src/lib/client/epub/tts-epub-preload.ts index 36858ea..5f23e14 100644 --- a/src/lib/client/epub/tts-epub-preload.ts +++ b/src/lib/client/epub/tts-epub-preload.ts @@ -29,10 +29,8 @@ export function selectUpcomingWalkerItems( * (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]; } diff --git a/src/lib/client/pdf-tts-planning.ts b/src/lib/client/pdf-tts-planning.ts new file mode 100644 index 0000000..23e2074 --- /dev/null +++ b/src/lib/client/pdf-tts-planning.ts @@ -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, + }; +} diff --git a/src/types/config.ts b/src/types/config.ts index 58bb388..53db98c 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -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, diff --git a/src/types/user-state.ts b/src/types/user-state.ts index 7a0e275..3fc796e 100644 --- a/src/types/user-state.ts +++ b/src/types/user-state.ts @@ -7,7 +7,6 @@ export const SYNCED_PREFERENCE_KEYS = [ 'voice', 'skipBlank', 'epubTheme', - 'smartSentenceSplitting', 'segmentPreloadDepthPages', 'segmentPreloadSentenceLookahead', 'ttsSegmentMaxBlockLength', diff --git a/tests/unit/html-audiobook-adapter.spec.ts b/tests/unit/html-audiobook-adapter.spec.ts index 15c1a31..9395533 100644 --- a/tests/unit/html-audiobook-adapter.spec.ts +++ b/tests/unit/html-audiobook-adapter.spec.ts @@ -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); }); diff --git a/tests/unit/pdf-audiobook-adapter.spec.ts b/tests/unit/pdf-audiobook-adapter.spec.ts index 4acdeaa..dd110b5 100644 --- a/tests/unit/pdf-audiobook-adapter.spec.ts +++ b/tests/unit/pdf-audiobook-adapter.spec.ts @@ -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(); diff --git a/tests/unit/pdf-tts-planning.spec.ts b/tests/unit/pdf-tts-planning.spec.ts new file mode 100644 index 0000000..1066d7b --- /dev/null +++ b/tests/unit/pdf-tts-planning.spec.ts @@ -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([ + [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([ + [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); + }); +}); diff --git a/tests/unit/tts-epub-preload.spec.ts b/tests/unit/tts-epub-preload.spec.ts index ea24f88..d780e51 100644 --- a/tests/unit/tts-epub-preload.spec.ts +++ b/tests/unit/tts-epub-preload.spec.ts @@ -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']); - }); });