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,
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -137,7 +137,6 @@ function sanitizePreferencesPatch(
|
|||
break;
|
||||
case 'skipBlank':
|
||||
case 'epubTheme':
|
||||
case 'smartSentenceSplitting':
|
||||
case 'pdfHighlightEnabled':
|
||||
case 'pdfWordHighlightEnabled':
|
||||
case 'epubHighlightEnabled':
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
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;
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ export const SYNCED_PREFERENCE_KEYS = [
|
|||
'voice',
|
||||
'skipBlank',
|
||||
'epubTheme',
|
||||
'smartSentenceSplitting',
|
||||
'segmentPreloadDepthPages',
|
||||
'segmentPreloadSentenceLookahead',
|
||||
'ttsSegmentMaxBlockLength',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
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([]);
|
||||
});
|
||||
|
||||
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']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue