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:
parent
bb3eb40966
commit
b679bf736c
18 changed files with 197 additions and 131 deletions
|
|
@ -101,7 +101,6 @@ export function useEpubDocument(documentId?: string): EpubDocumentState {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
providerRef,
|
providerRef,
|
||||||
ttsSegmentMaxBlockLength,
|
ttsSegmentMaxBlockLength,
|
||||||
smartSentenceSplitting,
|
|
||||||
epubTheme,
|
epubTheme,
|
||||||
epubHighlightEnabled,
|
epubHighlightEnabled,
|
||||||
} = useConfig();
|
} = useConfig();
|
||||||
|
|
@ -239,22 +238,13 @@ export function useEpubDocument(documentId?: string): EpubDocumentState {
|
||||||
const leadingPreview = collectLeadingContextFromRange(range);
|
const leadingPreview = collectLeadingContextFromRange(range);
|
||||||
const continuationPreview = collectContinuationFromRange(range);
|
const continuationPreview = collectContinuationFromRange(range);
|
||||||
|
|
||||||
if (smartSentenceSplitting) {
|
setTTSText(textContent, {
|
||||||
setTTSText(textContent, {
|
shouldPause,
|
||||||
shouldPause,
|
location: start.cfi,
|
||||||
location: start.cfi,
|
previousText: leadingPreview,
|
||||||
previousText: leadingPreview,
|
nextLocation: end.cfi,
|
||||||
nextLocation: end.cfi,
|
nextText: continuationPreview
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setCurrDocText(textContent);
|
setCurrDocText(textContent);
|
||||||
|
|
||||||
return textContent;
|
return textContent;
|
||||||
|
|
@ -262,7 +252,7 @@ export function useEpubDocument(documentId?: string): EpubDocumentState {
|
||||||
console.error('Error extracting EPUB text:', error);
|
console.error('Error extracting EPUB text:', error);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}, [setRenderedTextMaps, setTTSText, smartSentenceSplitting]);
|
}, [setRenderedTextMaps, setTTSText]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a draft EPUB locator (typically `{ readerType: 'epub', location:
|
* Resolves a draft EPUB locator (typically `{ readerType: 'epub', location:
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,6 @@ export function useHtmlDocument(): HtmlDocumentState {
|
||||||
apiKey,
|
apiKey,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
providerRef,
|
providerRef,
|
||||||
smartSentenceSplitting,
|
|
||||||
ttsSegmentMaxBlockLength,
|
ttsSegmentMaxBlockLength,
|
||||||
} = useConfig();
|
} = useConfig();
|
||||||
|
|
||||||
|
|
@ -133,10 +132,9 @@ export function useHtmlDocument(): HtmlDocumentState {
|
||||||
createHtmlAudiobookSourceAdapter({
|
createHtmlAudiobookSourceAdapter({
|
||||||
blocks,
|
blocks,
|
||||||
isTxt,
|
isTxt,
|
||||||
smartSentenceSplitting,
|
|
||||||
maxBlockLength: ttsSegmentMaxBlockLength,
|
maxBlockLength: ttsSegmentMaxBlockLength,
|
||||||
}),
|
}),
|
||||||
[blocks, isTxt, smartSentenceSplitting, ttsSegmentMaxBlockLength],
|
[blocks, isTxt, ttsSegmentMaxBlockLength],
|
||||||
);
|
);
|
||||||
|
|
||||||
const createFullAudioBook = useCallback(
|
const createFullAudioBook = useCallback(
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import {
|
||||||
highlightWordIndex,
|
highlightWordIndex,
|
||||||
} from '@/lib/client/pdf';
|
} from '@/lib/client/pdf';
|
||||||
import { buildPageTextFromBlocks } from '@/lib/client/pdf-block-text';
|
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 type { CanonicalTtsSourceUnit } from '@/lib/shared/tts-segment-plan';
|
||||||
import {
|
import {
|
||||||
DEFAULT_DOCUMENT_SETTINGS,
|
DEFAULT_DOCUMENT_SETTINGS,
|
||||||
|
|
@ -130,7 +131,6 @@ export function usePdfDocument(): PdfDocumentState {
|
||||||
apiKey,
|
apiKey,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
providerRef,
|
providerRef,
|
||||||
smartSentenceSplitting,
|
|
||||||
segmentPreloadDepthPages,
|
segmentPreloadDepthPages,
|
||||||
ttsSegmentMaxBlockLength,
|
ttsSegmentMaxBlockLength,
|
||||||
} = useConfig();
|
} = useConfig();
|
||||||
|
|
@ -149,9 +149,8 @@ export function usePdfDocument(): PdfDocumentState {
|
||||||
const audiobookAdapter = useMemo(() => createPdfAudiobookSourceAdapter({
|
const audiobookAdapter = useMemo(() => createPdfAudiobookSourceAdapter({
|
||||||
parsed: parsedDocument ?? undefined,
|
parsed: parsedDocument ?? undefined,
|
||||||
settings: documentSettings,
|
settings: documentSettings,
|
||||||
smartSentenceSplitting,
|
|
||||||
maxBlockLength: ttsSegmentMaxBlockLength,
|
maxBlockLength: ttsSegmentMaxBlockLength,
|
||||||
}), [parsedDocument, documentSettings, smartSentenceSplitting, ttsSegmentMaxBlockLength]);
|
}), [parsedDocument, documentSettings, ttsSegmentMaxBlockLength]);
|
||||||
const pageTextCacheRef = useRef<Map<number, string>>(new Map());
|
const pageTextCacheRef = useRef<Map<number, string>>(new Map());
|
||||||
const [currDocPage, setCurrDocPage] = useState<number>(currDocPageNumber);
|
const [currDocPage, setCurrDocPage] = useState<number>(currDocPageNumber);
|
||||||
|
|
||||||
|
|
@ -266,20 +265,7 @@ export function usePdfDocument(): PdfDocumentState {
|
||||||
|
|
||||||
const sourceUnitsFromParsedPage = (pageNum: number): CanonicalTtsSourceUnit[] => {
|
const sourceUnitsFromParsedPage = (pageNum: number): CanonicalTtsSourceUnit[] => {
|
||||||
const page = pageFromParsed(pageNum);
|
const page = pageFromParsed(pageNum);
|
||||||
if (!page) return [];
|
return buildPdfPageSourceUnits(page, pageNum, documentSettings.pdf?.skipBlockKinds ?? []);
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPageText = async (pageNumber: number, shouldCache = false): Promise<string> => {
|
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),
|
prevPageNumber ? getPageText(prevPageNumber) : Promise.resolve<string | undefined>(undefined),
|
||||||
...upcomingPageNumbers.map((pageNum) => getPageText(pageNum, true)),
|
...upcomingPageNumbers.map((pageNum) => getPageText(pageNum, true)),
|
||||||
]);
|
]);
|
||||||
const nextText = upcomingTexts[0];
|
const {
|
||||||
const nextSourceUnits = nextPageNumber ? sourceUnitsFromParsedPage(nextPageNumber) : [];
|
nextText,
|
||||||
const additionalUpcoming = upcomingPageNumbers
|
nextSourceUnits,
|
||||||
.slice(1)
|
additionalUpcoming,
|
||||||
.map((pageNum, idx) => ({
|
} = buildPdfPrefetchPayload(
|
||||||
location: pageNum,
|
upcomingPageNumbers,
|
||||||
text: upcomingTexts[idx + 1] || '',
|
upcomingTexts,
|
||||||
sourceUnits: sourceUnitsFromParsedPage(pageNum),
|
sourceUnitsFromParsedPage,
|
||||||
}))
|
);
|
||||||
.filter((item) => item.text.trim().length > 0);
|
|
||||||
|
|
||||||
if (generation !== pdfDocGenerationRef.current || pdfDocumentRef.current !== currentPdf) {
|
if (generation !== pdfDocGenerationRef.current || pdfDocumentRef.current !== currentPdf) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,6 @@ function sanitizePreferencesPatch(
|
||||||
break;
|
break;
|
||||||
case 'skipBlank':
|
case 'skipBlank':
|
||||||
case 'epubTheme':
|
case 'epubTheme':
|
||||||
case 'smartSentenceSplitting':
|
|
||||||
case 'pdfHighlightEnabled':
|
case 'pdfHighlightEnabled':
|
||||||
case 'pdfWordHighlightEnabled':
|
case 'pdfWordHighlightEnabled':
|
||||||
case 'epubHighlightEnabled':
|
case 'epubHighlightEnabled':
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,6 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: {
|
||||||
viewType,
|
viewType,
|
||||||
skipBlank,
|
skipBlank,
|
||||||
epubTheme,
|
epubTheme,
|
||||||
smartSentenceSplitting,
|
|
||||||
segmentPreloadDepthPages,
|
segmentPreloadDepthPages,
|
||||||
segmentPreloadSentenceLookahead,
|
segmentPreloadSentenceLookahead,
|
||||||
ttsSegmentMaxBlockLength,
|
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">
|
<div className="space-y-3 pt-1">
|
||||||
<RangeSetting
|
<RangeSetting
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ interface ConfigContextType {
|
||||||
voice: string;
|
voice: string;
|
||||||
skipBlank: boolean;
|
skipBlank: boolean;
|
||||||
epubTheme: boolean;
|
epubTheme: boolean;
|
||||||
smartSentenceSplitting: boolean;
|
|
||||||
segmentPreloadDepthPages: number;
|
segmentPreloadDepthPages: number;
|
||||||
segmentPreloadSentenceLookahead: number;
|
segmentPreloadSentenceLookahead: number;
|
||||||
ttsSegmentMaxBlockLength: number;
|
ttsSegmentMaxBlockLength: number;
|
||||||
|
|
@ -352,7 +351,6 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
ttsModel,
|
ttsModel,
|
||||||
ttsInstructions,
|
ttsInstructions,
|
||||||
savedVoices,
|
savedVoices,
|
||||||
smartSentenceSplitting,
|
|
||||||
segmentPreloadDepthPages,
|
segmentPreloadDepthPages,
|
||||||
segmentPreloadSentenceLookahead,
|
segmentPreloadSentenceLookahead,
|
||||||
ttsSegmentMaxBlockLength,
|
ttsSegmentMaxBlockLength,
|
||||||
|
|
@ -475,7 +473,6 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
voice,
|
voice,
|
||||||
skipBlank,
|
skipBlank,
|
||||||
epubTheme,
|
epubTheme,
|
||||||
smartSentenceSplitting,
|
|
||||||
segmentPreloadDepthPages,
|
segmentPreloadDepthPages,
|
||||||
segmentPreloadSentenceLookahead,
|
segmentPreloadSentenceLookahead,
|
||||||
ttsSegmentMaxBlockLength,
|
ttsSegmentMaxBlockLength,
|
||||||
|
|
|
||||||
|
|
@ -386,7 +386,6 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
ttsInstructions: configTTSInstructions,
|
ttsInstructions: configTTSInstructions,
|
||||||
updateConfigKey,
|
updateConfigKey,
|
||||||
skipBlank,
|
skipBlank,
|
||||||
smartSentenceSplitting,
|
|
||||||
segmentPreloadDepthPages,
|
segmentPreloadDepthPages,
|
||||||
segmentPreloadSentenceLookahead,
|
segmentPreloadSentenceLookahead,
|
||||||
ttsSegmentMaxBlockLength,
|
ttsSegmentMaxBlockLength,
|
||||||
|
|
@ -1164,7 +1163,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
const currentSourceKeySet = new Set(effectiveCurrentUnits.map((unit) => unit.sourceKey));
|
const currentSourceKeySet = new Set(effectiveCurrentUnits.map((unit) => unit.sourceKey));
|
||||||
|
|
||||||
const contextSourceUnits: CanonicalTtsSourceUnit[] = [];
|
const contextSourceUnits: CanonicalTtsSourceUnit[] = [];
|
||||||
if (smartSentenceSplitting && normalizedOptions.previousText?.trim()) {
|
if (normalizedOptions.previousText?.trim()) {
|
||||||
const previousLocation = normalizedOptions.previousLocation;
|
const previousLocation = normalizedOptions.previousLocation;
|
||||||
contextSourceUnits.push({
|
contextSourceUnits.push({
|
||||||
sourceKey: previousLocation !== undefined
|
sourceKey: previousLocation !== undefined
|
||||||
|
|
@ -1221,9 +1220,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const item of pendingPrefetches) {
|
for (const item of pendingPrefetches) {
|
||||||
if (smartSentenceSplitting) {
|
sourceUnits.push(...item.sourceUnits);
|
||||||
sourceUnits.push(...item.sourceUnits);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const plan = planCanonicalTtsSegments(sourceUnits, {
|
const plan = planCanonicalTtsSegments(sourceUnits, {
|
||||||
|
|
@ -1232,27 +1229,12 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
keyPrefix: buildSegmentKeyPrefix(documentId, activeReaderType),
|
keyPrefix: buildSegmentKeyPrefix(documentId, activeReaderType),
|
||||||
enforceSourceBoundaries: activeReaderType === 'pdf' && currentUnits !== null && currentUnits.length > 0,
|
enforceSourceBoundaries: activeReaderType === 'pdf' && currentUnits !== null && currentUnits.length > 0,
|
||||||
});
|
});
|
||||||
const currentSegments = smartSentenceSplitting
|
const currentSegments = plan.segments.filter((segment) => currentSourceKeySet.has(segment.ownerSourceKey));
|
||||||
? 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 newSentences = currentSegments.map((segment) => segment.text);
|
const newSentences = currentSegments.map((segment) => segment.text);
|
||||||
|
|
||||||
for (const item of pendingPrefetches) {
|
for (const item of pendingPrefetches) {
|
||||||
const sourceKeys = new Set(item.sourceUnits.map((unit) => unit.sourceKey));
|
const sourceKeys = new Set(item.sourceUnits.map((unit) => unit.sourceKey));
|
||||||
const planned = smartSentenceSplitting
|
const planned = plan.segments.filter((segment) => sourceKeys.has(segment.ownerSourceKey));
|
||||||
? 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;
|
|
||||||
if (planned.length > 0) {
|
if (planned.length > 0) {
|
||||||
plannedSegmentsByLocationRef.current.set(normalizeLocationKey(item.location), planned);
|
plannedSegmentsByLocationRef.current.set(normalizeLocationKey(item.location), planned);
|
||||||
}
|
}
|
||||||
|
|
@ -1333,8 +1315,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
setCurrentWordIndex(null);
|
setCurrentWordIndex(null);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
smartSentenceSplitting
|
!isEPUB
|
||||||
&& !isEPUB
|
|
||||||
&& normalizedOptions.nextLocation !== undefined
|
&& normalizedOptions.nextLocation !== undefined
|
||||||
&& effectiveCurrentUnits.length === 1
|
&& effectiveCurrentUnits.length === 1
|
||||||
) {
|
) {
|
||||||
|
|
@ -1384,7 +1365,6 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
abortAudio,
|
abortAudio,
|
||||||
isEPUB,
|
isEPUB,
|
||||||
activeReaderType,
|
activeReaderType,
|
||||||
smartSentenceSplitting,
|
|
||||||
invalidatePlaybackRun,
|
invalidatePlaybackRun,
|
||||||
currDocPage,
|
currDocPage,
|
||||||
documentId,
|
documentId,
|
||||||
|
|
@ -2483,7 +2463,6 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
? currentSourceContextUnitsRef.current
|
? currentSourceContextUnitsRef.current
|
||||||
: (currentSourceUnitRef.current ? [currentSourceUnitRef.current] : []);
|
: (currentSourceUnitRef.current ? [currentSourceUnitRef.current] : []);
|
||||||
const sourceUnits: CanonicalTtsSourceUnit[] = buildWalkerPlanningSourceUnits(
|
const sourceUnits: CanonicalTtsSourceUnit[] = buildWalkerPlanningSourceUnits(
|
||||||
smartSentenceSplitting,
|
|
||||||
liveContextUnits,
|
liveContextUnits,
|
||||||
upcomingUnits,
|
upcomingUnits,
|
||||||
);
|
);
|
||||||
|
|
@ -2835,7 +2814,6 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
openApiBaseUrl,
|
openApiBaseUrl,
|
||||||
providerModelPolicy.supportsInstructions,
|
providerModelPolicy.supportsInstructions,
|
||||||
ttsInstructions,
|
ttsInstructions,
|
||||||
smartSentenceSplitting,
|
|
||||||
onTTSStart,
|
onTTSStart,
|
||||||
onTTSComplete,
|
onTTSComplete,
|
||||||
processSentence,
|
processSentence,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import type { HtmlBlock } from '@/lib/client/html/blocks';
|
||||||
interface HtmlAudiobookAdapterOptions {
|
interface HtmlAudiobookAdapterOptions {
|
||||||
blocks: HtmlBlock[];
|
blocks: HtmlBlock[];
|
||||||
isTxt: boolean;
|
isTxt: boolean;
|
||||||
smartSentenceSplitting: boolean;
|
|
||||||
maxBlockLength?: number;
|
maxBlockLength?: number;
|
||||||
/**
|
/**
|
||||||
* For markdown: any heading with `headingLevel <= chapterHeadingLevel`
|
* For markdown: any heading with `headingLevel <= chapterHeadingLevel`
|
||||||
|
|
@ -83,7 +82,6 @@ function buildChapterDrafts({
|
||||||
|
|
||||||
function chapterText(
|
function chapterText(
|
||||||
draft: ChapterDraft,
|
draft: ChapterDraft,
|
||||||
smartSentenceSplitting: boolean,
|
|
||||||
maxBlockLength?: number,
|
maxBlockLength?: number,
|
||||||
): string {
|
): string {
|
||||||
const joined = draft.blocks
|
const joined = draft.blocks
|
||||||
|
|
@ -91,14 +89,14 @@ function chapterText(
|
||||||
.filter((t) => t && t.trim())
|
.filter((t) => t && t.trim())
|
||||||
.join('\n\n');
|
.join('\n\n');
|
||||||
if (!joined) return '';
|
if (!joined) return '';
|
||||||
return smartSentenceSplitting ? normalizeTextForTts(joined, { maxBlockLength }) : joined;
|
return normalizeTextForTts(joined, { maxBlockLength });
|
||||||
}
|
}
|
||||||
|
|
||||||
function preparedChapters(options: HtmlAudiobookAdapterOptions): PreparedAudiobookChapter[] {
|
function preparedChapters(options: HtmlAudiobookAdapterOptions): PreparedAudiobookChapter[] {
|
||||||
const drafts = buildChapterDrafts(options);
|
const drafts = buildChapterDrafts(options);
|
||||||
const out: PreparedAudiobookChapter[] = [];
|
const out: PreparedAudiobookChapter[] = [];
|
||||||
for (const draft of drafts) {
|
for (const draft of drafts) {
|
||||||
const text = chapterText(draft, options.smartSentenceSplitting, options.maxBlockLength);
|
const text = chapterText(draft, options.maxBlockLength);
|
||||||
if (!text.trim()) continue;
|
if (!text.trim()) continue;
|
||||||
out.push({
|
out.push({
|
||||||
index: out.length,
|
index: out.length,
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,11 @@ import { DEFAULT_DOCUMENT_SETTINGS } from '@/types/document-settings';
|
||||||
interface PdfAudiobookAdapterOptions {
|
interface PdfAudiobookAdapterOptions {
|
||||||
parsed?: ParsedPdfDocument;
|
parsed?: ParsedPdfDocument;
|
||||||
settings?: DocumentSettings;
|
settings?: DocumentSettings;
|
||||||
smartSentenceSplitting: boolean;
|
|
||||||
maxBlockLength?: number;
|
maxBlockLength?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function chapterTextFromBlocks(
|
function chapterTextFromBlocks(
|
||||||
blocks: ParsedPdfBlock[],
|
blocks: ParsedPdfBlock[],
|
||||||
smartSentenceSplitting: boolean,
|
|
||||||
maxBlockLength?: number,
|
maxBlockLength?: number,
|
||||||
): string {
|
): string {
|
||||||
const text = blocks
|
const text = blocks
|
||||||
|
|
@ -21,18 +19,16 @@ function chapterTextFromBlocks(
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n\n');
|
.join('\n\n');
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
return smartSentenceSplitting ? normalizeTextForTts(text, { maxBlockLength }) : text;
|
return normalizeTextForTts(text, { maxBlockLength });
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareParsedChapters({
|
function prepareParsedChapters({
|
||||||
parsed,
|
parsed,
|
||||||
settings,
|
settings,
|
||||||
smartSentenceSplitting,
|
|
||||||
maxBlockLength,
|
maxBlockLength,
|
||||||
}: {
|
}: {
|
||||||
parsed: ParsedPdfDocument;
|
parsed: ParsedPdfDocument;
|
||||||
settings: DocumentSettings;
|
settings: DocumentSettings;
|
||||||
smartSentenceSplitting: boolean;
|
|
||||||
maxBlockLength?: number;
|
maxBlockLength?: number;
|
||||||
}): PreparedAudiobookChapter[] {
|
}): PreparedAudiobookChapter[] {
|
||||||
const skip = new Set(settings.pdf?.skipBlockKinds ?? DEFAULT_DOCUMENT_SETTINGS.pdf?.skipBlockKinds ?? []);
|
const skip = new Set(settings.pdf?.skipBlockKinds ?? DEFAULT_DOCUMENT_SETTINGS.pdf?.skipBlockKinds ?? []);
|
||||||
|
|
@ -49,7 +45,7 @@ function prepareParsedChapters({
|
||||||
|
|
||||||
const flush = () => {
|
const flush = () => {
|
||||||
if (!currentBlocks.length) return;
|
if (!currentBlocks.length) return;
|
||||||
const text = chapterTextFromBlocks(currentBlocks, smartSentenceSplitting, maxBlockLength);
|
const text = chapterTextFromBlocks(currentBlocks, maxBlockLength);
|
||||||
if (text) {
|
if (text) {
|
||||||
chapters.push({
|
chapters.push({
|
||||||
index: chapters.length,
|
index: chapters.length,
|
||||||
|
|
@ -77,7 +73,6 @@ function prepareParsedChapters({
|
||||||
async function extractPreparedPdfChapters({
|
async function extractPreparedPdfChapters({
|
||||||
parsed,
|
parsed,
|
||||||
settings = DEFAULT_DOCUMENT_SETTINGS,
|
settings = DEFAULT_DOCUMENT_SETTINGS,
|
||||||
smartSentenceSplitting,
|
|
||||||
maxBlockLength,
|
maxBlockLength,
|
||||||
}: PdfAudiobookAdapterOptions): Promise<PreparedAudiobookChapter[]> {
|
}: PdfAudiobookAdapterOptions): Promise<PreparedAudiobookChapter[]> {
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
|
|
@ -87,7 +82,6 @@ async function extractPreparedPdfChapters({
|
||||||
return prepareParsedChapters({
|
return prepareParsedChapters({
|
||||||
parsed,
|
parsed,
|
||||||
settings,
|
settings,
|
||||||
smartSentenceSplitting,
|
|
||||||
maxBlockLength,
|
maxBlockLength,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -194,8 +194,6 @@ function buildAppConfigFromRaw(raw: RawConfigMap): AppConfigRow {
|
||||||
voice: '',
|
voice: '',
|
||||||
skipBlank: raw.skipBlank === 'false' ? false : APP_CONFIG_DEFAULTS.skipBlank,
|
skipBlank: raw.skipBlank === 'false' ? false : APP_CONFIG_DEFAULTS.skipBlank,
|
||||||
epubTheme: raw.epubTheme === 'true',
|
epubTheme: raw.epubTheme === 'true',
|
||||||
smartSentenceSplitting:
|
|
||||||
raw.smartSentenceSplitting === 'false' ? false : APP_CONFIG_DEFAULTS.smartSentenceSplitting,
|
|
||||||
headerMargin: raw.headerMargin ? parseFloat(raw.headerMargin) : APP_CONFIG_DEFAULTS.headerMargin,
|
headerMargin: raw.headerMargin ? parseFloat(raw.headerMargin) : APP_CONFIG_DEFAULTS.headerMargin,
|
||||||
footerMargin: raw.footerMargin ? parseFloat(raw.footerMargin) : APP_CONFIG_DEFAULTS.footerMargin,
|
footerMargin: raw.footerMargin ? parseFloat(raw.footerMargin) : APP_CONFIG_DEFAULTS.footerMargin,
|
||||||
leftMargin: raw.leftMargin ? parseFloat(raw.leftMargin) : APP_CONFIG_DEFAULTS.leftMargin,
|
leftMargin: raw.leftMargin ? parseFloat(raw.leftMargin) : APP_CONFIG_DEFAULTS.leftMargin,
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,8 @@ export function selectUpcomingWalkerItems<T extends EpubWalkerLocationItem>(
|
||||||
* (previous/current) so walker boundary behavior aligns with setText.
|
* (previous/current) so walker boundary behavior aligns with setText.
|
||||||
*/
|
*/
|
||||||
export function buildWalkerPlanningSourceUnits(
|
export function buildWalkerPlanningSourceUnits(
|
||||||
smartSentenceSplitting: boolean,
|
|
||||||
contextUnits: readonly CanonicalTtsSourceUnit[],
|
contextUnits: readonly CanonicalTtsSourceUnit[],
|
||||||
upcomingUnits: readonly CanonicalTtsSourceUnit[],
|
upcomingUnits: readonly CanonicalTtsSourceUnit[],
|
||||||
): CanonicalTtsSourceUnit[] {
|
): CanonicalTtsSourceUnit[] {
|
||||||
if (!smartSentenceSplitting) return [...upcomingUnits];
|
|
||||||
return [...contextUnits, ...upcomingUnits];
|
return [...contextUnits, ...upcomingUnits];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
58
src/lib/client/pdf-tts-planning.ts
Normal file
58
src/lib/client/pdf-tts-planning.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -62,7 +62,6 @@ export interface AppConfigValues {
|
||||||
ttsModel: string;
|
ttsModel: string;
|
||||||
ttsInstructions: string;
|
ttsInstructions: string;
|
||||||
savedVoices: SavedVoices;
|
savedVoices: SavedVoices;
|
||||||
smartSentenceSplitting: boolean;
|
|
||||||
segmentPreloadDepthPages: number;
|
segmentPreloadDepthPages: number;
|
||||||
segmentPreloadSentenceLookahead: number;
|
segmentPreloadSentenceLookahead: number;
|
||||||
ttsSegmentMaxBlockLength: number;
|
ttsSegmentMaxBlockLength: number;
|
||||||
|
|
@ -111,7 +110,6 @@ export function getAppConfigDefaults(): AppConfigValues {
|
||||||
ttsModel: defaultModel,
|
ttsModel: defaultModel,
|
||||||
ttsInstructions: '',
|
ttsInstructions: '',
|
||||||
savedVoices: {},
|
savedVoices: {},
|
||||||
smartSentenceSplitting: true,
|
|
||||||
segmentPreloadDepthPages: 1,
|
segmentPreloadDepthPages: 1,
|
||||||
segmentPreloadSentenceLookahead: 3,
|
segmentPreloadSentenceLookahead: 3,
|
||||||
ttsSegmentMaxBlockLength: 450,
|
ttsSegmentMaxBlockLength: 450,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ export const SYNCED_PREFERENCE_KEYS = [
|
||||||
'voice',
|
'voice',
|
||||||
'skipBlank',
|
'skipBlank',
|
||||||
'epubTheme',
|
'epubTheme',
|
||||||
'smartSentenceSplitting',
|
|
||||||
'segmentPreloadDepthPages',
|
'segmentPreloadDepthPages',
|
||||||
'segmentPreloadSentenceLookahead',
|
'segmentPreloadSentenceLookahead',
|
||||||
'ttsSegmentMaxBlockLength',
|
'ttsSegmentMaxBlockLength',
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ test.describe('createHtmlAudiobookSourceAdapter (markdown chapter splitting)', (
|
||||||
const adapter = createHtmlAudiobookSourceAdapter({
|
const adapter = createHtmlAudiobookSourceAdapter({
|
||||||
blocks,
|
blocks,
|
||||||
isTxt: false,
|
isTxt: false,
|
||||||
smartSentenceSplitting: false,
|
|
||||||
});
|
});
|
||||||
const chapters = await adapter.prepareChapters();
|
const chapters = await adapter.prepareChapters();
|
||||||
expect(chapters.map((c) => c.title)).toEqual(['Alpha', 'Beta', 'Delta']);
|
expect(chapters.map((c) => c.title)).toEqual(['Alpha', 'Beta', 'Delta']);
|
||||||
|
|
@ -46,7 +45,6 @@ test.describe('createHtmlAudiobookSourceAdapter (markdown chapter splitting)', (
|
||||||
const adapter = createHtmlAudiobookSourceAdapter({
|
const adapter = createHtmlAudiobookSourceAdapter({
|
||||||
blocks,
|
blocks,
|
||||||
isTxt: false,
|
isTxt: false,
|
||||||
smartSentenceSplitting: false,
|
|
||||||
});
|
});
|
||||||
const chapters = await adapter.prepareChapters();
|
const chapters = await adapter.prepareChapters();
|
||||||
expect(chapters.map((c) => c.title)).toEqual(['Introduction', 'First Heading']);
|
expect(chapters.map((c) => c.title)).toEqual(['Introduction', 'First Heading']);
|
||||||
|
|
@ -64,7 +62,6 @@ test.describe('createHtmlAudiobookSourceAdapter (markdown chapter splitting)', (
|
||||||
const adapter = createHtmlAudiobookSourceAdapter({
|
const adapter = createHtmlAudiobookSourceAdapter({
|
||||||
blocks,
|
blocks,
|
||||||
isTxt: false,
|
isTxt: false,
|
||||||
smartSentenceSplitting: false,
|
|
||||||
fallbackBlocksPerChapter: 3,
|
fallbackBlocksPerChapter: 3,
|
||||||
});
|
});
|
||||||
const chapters = await adapter.prepareChapters();
|
const chapters = await adapter.prepareChapters();
|
||||||
|
|
@ -88,7 +85,6 @@ test.describe('createHtmlAudiobookSourceAdapter (markdown chapter splitting)', (
|
||||||
const adapter = createHtmlAudiobookSourceAdapter({
|
const adapter = createHtmlAudiobookSourceAdapter({
|
||||||
blocks,
|
blocks,
|
||||||
isTxt: false,
|
isTxt: false,
|
||||||
smartSentenceSplitting: false,
|
|
||||||
chapterHeadingLevel: 1,
|
chapterHeadingLevel: 1,
|
||||||
});
|
});
|
||||||
const chapters = await adapter.prepareChapters();
|
const chapters = await adapter.prepareChapters();
|
||||||
|
|
@ -109,7 +105,6 @@ test.describe('createHtmlAudiobookSourceAdapter (txt chapter splitting)', () =>
|
||||||
const adapter = createHtmlAudiobookSourceAdapter({
|
const adapter = createHtmlAudiobookSourceAdapter({
|
||||||
blocks,
|
blocks,
|
||||||
isTxt: true,
|
isTxt: true,
|
||||||
smartSentenceSplitting: false,
|
|
||||||
});
|
});
|
||||||
const chapters = await adapter.prepareChapters();
|
const chapters = await adapter.prepareChapters();
|
||||||
expect(chapters.map((c) => c.title)).toEqual(['Part 1', 'Part 2', 'Part 3']);
|
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({
|
const adapter = createHtmlAudiobookSourceAdapter({
|
||||||
blocks,
|
blocks,
|
||||||
isTxt: true,
|
isTxt: true,
|
||||||
smartSentenceSplitting: false,
|
|
||||||
});
|
});
|
||||||
const chapters = await adapter.prepareChapters();
|
const chapters = await adapter.prepareChapters();
|
||||||
expect(chapters.length).toBe(1);
|
expect(chapters.length).toBe(1);
|
||||||
|
|
@ -142,7 +136,6 @@ test.describe('createHtmlAudiobookSourceAdapter — prepareChapter', () => {
|
||||||
const adapter = createHtmlAudiobookSourceAdapter({
|
const adapter = createHtmlAudiobookSourceAdapter({
|
||||||
blocks,
|
blocks,
|
||||||
isTxt: false,
|
isTxt: false,
|
||||||
smartSentenceSplitting: false,
|
|
||||||
});
|
});
|
||||||
const list = await adapter.prepareChapters();
|
const list = await adapter.prepareChapters();
|
||||||
const second = await adapter.prepareChapter(1);
|
const second = await adapter.prepareChapter(1);
|
||||||
|
|
@ -155,7 +148,6 @@ test.describe('createHtmlAudiobookSourceAdapter — prepareChapter', () => {
|
||||||
const adapter = createHtmlAudiobookSourceAdapter({
|
const adapter = createHtmlAudiobookSourceAdapter({
|
||||||
blocks,
|
blocks,
|
||||||
isTxt: false,
|
isTxt: false,
|
||||||
smartSentenceSplitting: false,
|
|
||||||
});
|
});
|
||||||
await expect(adapter.prepareChapter(42)).rejects.toThrow(/invalid chapter index/i);
|
await expect(adapter.prepareChapter(42)).rejects.toThrow(/invalid chapter index/i);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ test.describe('pdf audiobook adapter', () => {
|
||||||
const adapter = createPdfAudiobookSourceAdapter({
|
const adapter = createPdfAudiobookSourceAdapter({
|
||||||
parsed,
|
parsed,
|
||||||
settings,
|
settings,
|
||||||
smartSentenceSplitting: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const chapters = await adapter.prepareChapters();
|
const chapters = await adapter.prepareChapters();
|
||||||
|
|
@ -125,7 +124,6 @@ test.describe('pdf audiobook adapter', () => {
|
||||||
const adapter = createPdfAudiobookSourceAdapter({
|
const adapter = createPdfAudiobookSourceAdapter({
|
||||||
parsed,
|
parsed,
|
||||||
settings,
|
settings,
|
||||||
smartSentenceSplitting: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const chapters = await adapter.prepareChapters();
|
const chapters = await adapter.prepareChapters();
|
||||||
|
|
|
||||||
107
tests/unit/pdf-tts-planning.spec.ts
Normal file
107
tests/unit/pdf-tts-planning.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -30,7 +30,7 @@ test.describe('EPUB walker preload helpers', () => {
|
||||||
expect(selectUpcomingWalkerItems(items, 'epubcfi(/6/2!/4/2)', 0)).toEqual([]);
|
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[] = [
|
const contextUnits: CanonicalTtsSourceUnit[] = [
|
||||||
{ sourceKey: 'previous:page-a', text: 'prev sentence', locator: null },
|
{ sourceKey: 'previous:page-a', text: 'prev sentence', locator: null },
|
||||||
{ sourceKey: 'page-a', text: 'current sentence', locator: { readerType: 'epub', location: 'page-a' } },
|
{ 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' } },
|
{ 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([
|
expect(planned.map((item) => item.sourceKey)).toEqual([
|
||||||
'previous:page-a',
|
'previous:page-a',
|
||||||
'page-a',
|
'page-a',
|
||||||
|
|
@ -48,17 +48,4 @@ test.describe('EPUB walker preload helpers', () => {
|
||||||
'page-c',
|
'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']);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue