diff --git a/src/app/(app)/pdf/[id]/page.tsx b/src/app/(app)/pdf/[id]/page.tsx index 4cfb922..bad2b1e 100644 --- a/src/app/(app)/pdf/[id]/page.tsx +++ b/src/app/(app)/pdf/[id]/page.tsx @@ -337,7 +337,6 @@ export default function PDFViewerPage() { parseStatus, parsedOverlayEnabled, skipBlockKinds: documentSettings.pdf?.skipBlockKinds ?? [], - chaptersFromSections: documentSettings.pdf?.chaptersFromSections ?? true, onToggleOverlay: (enabled) => setParsedOverlayEnabled(enabled), onToggleSkipKind: (kind, enabled) => { const current = new Set(documentSettings.pdf?.skipBlockKinds ?? []); @@ -347,20 +346,8 @@ export default function PDFViewerPage() { ...documentSettings, schemaVersion: 1, pdf: { - ...(documentSettings.pdf ?? { chaptersFromSections: true }), + ...(documentSettings.pdf ?? {}), skipBlockKinds: Array.from(current), - chaptersFromSections: documentSettings.pdf?.chaptersFromSections ?? true, - }, - }); - }, - onToggleChaptersFromSections: (enabled) => { - void updateDocumentSettings({ - ...documentSettings, - schemaVersion: 1, - pdf: { - ...(documentSettings.pdf ?? { skipBlockKinds: [] }), - skipBlockKinds: documentSettings.pdf?.skipBlockKinds ?? [], - chaptersFromSections: enabled, }, }); }, diff --git a/src/app/(app)/pdf/[id]/usePdfDocument.ts b/src/app/(app)/pdf/[id]/usePdfDocument.ts index f8f5cca..5aafa0a 100644 --- a/src/app/(app)/pdf/[id]/usePdfDocument.ts +++ b/src/app/(app)/pdf/[id]/usePdfDocument.ts @@ -275,7 +275,7 @@ export function usePdfDocument(): PdfDocumentState { text: block.text, locator: { readerType: 'pdf', - page: block.fragments[0]?.page ?? pageNum, + page: pageNum, blockId: block.id, } as TTSSegmentLocator, })) @@ -328,11 +328,13 @@ export function usePdfDocument(): PdfDocumentState { ...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); @@ -351,6 +353,7 @@ export function usePdfDocument(): PdfDocumentState { previousText: prevText, nextLocation: nextPageNumber, nextText: nextText, + nextSourceUnits, upcomingLocations: additionalUpcoming, ...(sourceUnits.length > 0 ? { sourceUnits } : {}), }); diff --git a/src/components/documents/DocumentSettings.tsx b/src/components/documents/DocumentSettings.tsx index 9a5ec8a..27d1576 100644 --- a/src/components/documents/DocumentSettings.tsx +++ b/src/components/documents/DocumentSettings.tsx @@ -103,10 +103,8 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: { parseStatus: PdfParseStatus | null; parsedOverlayEnabled: boolean; skipBlockKinds: ParsedPdfBlockKind[]; - chaptersFromSections: boolean; onToggleOverlay: (enabled: boolean) => void; onToggleSkipKind: (kind: ParsedPdfBlockKind, enabled: boolean) => void; - onToggleChaptersFromSections: (enabled: boolean) => void; onForceReparse: () => void; } }) { @@ -204,13 +202,6 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: { onChange={(checked) => updateConfigKey('pdfWordHighlightEnabled', checked)} variant="flat" /> - )} diff --git a/src/components/reader/SegmentsSidebar.tsx b/src/components/reader/SegmentsSidebar.tsx index 1e79ccc..5c9b2f1 100644 --- a/src/components/reader/SegmentsSidebar.tsx +++ b/src/components/reader/SegmentsSidebar.tsx @@ -249,6 +249,8 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }: const segmentsQuery = useInfiniteQuery({ queryKey: segmentsQueryKey, enabled: isOpen && !!documentId, + refetchInterval: isOpen ? 2500 : false, + refetchIntervalInBackground: false, initialPageParam: null as string | null, queryFn: async ({ pageParam, signal }) => { if (!documentId) { diff --git a/src/contexts/TTSContext.tsx b/src/contexts/TTSContext.tsx index d96d23e..b380bcd 100644 --- a/src/contexts/TTSContext.tsx +++ b/src/contexts/TTSContext.tsx @@ -163,8 +163,9 @@ interface SetTextOptions { previousLocation?: TTSLocation; nextLocation?: TTSLocation; nextText?: string; + nextSourceUnits?: CanonicalTtsSourceUnit[]; previousText?: string; - upcomingLocations?: Array<{ location: TTSLocation; text: string }>; + upcomingLocations?: Array<{ location: TTSLocation; text: string; sourceUnits?: CanonicalTtsSourceUnit[] }>; } type TTSSegmentPlaybackSource = { @@ -245,12 +246,16 @@ const buildCacheKey = ( speed: number, provider: string, model: string, + providerType: string, + instructions: string, ) => { return [ `provider=${provider || ''}`, + `providerType=${providerType || ''}`, `model=${model || ''}`, `voice=${voice || ''}`, `speed=${Number.isFinite(speed) ? speed : ''}`, + `instructions=${instructions || ''}`, `text=${sentence}`, ].join('|'); }; @@ -263,10 +268,12 @@ const buildScopedSegmentCacheKey = ( speed: number, provider: string, model: string, + providerType: string, + instructions: string, segmentKey?: string | null, ) => { return [ - buildCacheKey(sentence, voice, speed, provider, model), + buildCacheKey(sentence, voice, speed, provider, model, providerType, instructions), `segmentKey=${segmentKey || ''}`, `locator=${segmentKey ? '' : buildLocatorRequestKey(locator)}`, `segmentIndex=${segmentKey ? '' : segmentIndex}`, @@ -306,9 +313,11 @@ const buildSegmentRequestKey = ( segmentIndex: number, sentence: string, segmentKey?: string | null, -): string => segmentKey - ? `${segmentKey}::${sentence}` - : `${buildLocatorRequestKey(locator)}::${segmentIndex}::${sentence}`; +): string => { + return segmentKey + ? `${segmentKey}::${sentence}` + : `${buildLocatorRequestKey(locator)}::${segmentIndex}::${sentence}`; +}; const resolveJumpIndex = (input: JumpResolutionInput): JumpResolution => { if (input.newSentenceCount <= 0) { @@ -1172,29 +1181,48 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement plannedSegmentsByLocationRef.current.clear(); pendingNextLocationRef.current = normalizedOptions.nextLocation; - const pendingPrefetches: Array = []; + const pendingPrefetches: Array<{ + location: TTSLocation; + sourceUnits: CanonicalTtsSourceUnit[]; + }> = []; if (normalizedOptions.nextLocation !== undefined && normalizedOptions.nextText?.trim()) { - pendingPrefetches.push({ - sourceKey: sourceKeyForLocation(normalizedOptions.nextLocation, currDocPage), - location: normalizedOptions.nextLocation, - text: normalizedOptions.nextText, - locator: locatorForLocation(normalizedOptions.nextLocation, activeReaderType), - }); + const provided = normalizedOptions.nextSourceUnits?.filter((unit) => unit.text.trim().length > 0) ?? []; + pendingPrefetches.push(provided.length > 0 + ? { + location: normalizedOptions.nextLocation, + sourceUnits: provided, + } + : { + location: normalizedOptions.nextLocation, + sourceUnits: [{ + sourceKey: sourceKeyForLocation(normalizedOptions.nextLocation, currDocPage), + text: normalizedOptions.nextText, + locator: locatorForLocation(normalizedOptions.nextLocation, activeReaderType), + }], + }); } if (Array.isArray(normalizedOptions.upcomingLocations)) { for (const item of normalizedOptions.upcomingLocations) { if (item.location === undefined || !item.text?.trim()) continue; - pendingPrefetches.push({ - sourceKey: sourceKeyForLocation(item.location, currDocPage), - location: item.location, - text: item.text, - locator: locatorForLocation(item.location, activeReaderType), - }); + const provided = item.sourceUnits?.filter((unit) => unit.text.trim().length > 0) ?? []; + pendingPrefetches.push(provided.length > 0 + ? { + location: item.location, + sourceUnits: provided, + } + : { + location: item.location, + sourceUnits: [{ + sourceKey: sourceKeyForLocation(item.location, currDocPage), + text: item.text, + locator: locatorForLocation(item.location, activeReaderType), + }], + }); } } for (const item of pendingPrefetches) { if (smartSentenceSplitting) { - sourceUnits.push(item); + sourceUnits.push(...item.sourceUnits); } } @@ -1216,9 +1244,10 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement 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) => segment.ownerSourceKey === item.sourceKey) - : planCanonicalTtsSegments([item], { + ? plan.segments.filter((segment) => sourceKeys.has(segment.ownerSourceKey)) + : planCanonicalTtsSegments(item.sourceUnits, { readerType: activeReaderType, maxBlockLength: ttsSegmentMaxBlockLength, keyPrefix: buildSegmentKeyPrefix(documentId, activeReaderType), @@ -1554,6 +1583,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement effectiveNativeSpeed, configProviderRef, ttsModel, + configProviderType, + providerModelPolicy.supportsInstructions ? ttsInstructions : '', segmentKey, ); @@ -2216,6 +2247,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement effectiveNativeSpeed, configProviderRef, ttsModel, + configProviderType, + providerModelPolicy.supportsInstructions ? ttsInstructions : '', playbackSegment?.key, ); const cachedAlignment = sentenceAlignmentCacheRef.current.get(alignmentKey); @@ -2346,6 +2379,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement effectiveNativeSpeed, configProviderRef, ttsModel, + configProviderType, + providerModelPolicy.supportsInstructions ? ttsInstructions : '', nextSegment?.key, ); if (segmentManifestCacheRef.current.has(cacheKey)) { @@ -2477,6 +2512,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement effectiveNativeSpeed, configProviderRef, ttsModel, + configProviderType, + providerModelPolicy.supportsInstructions ? ttsInstructions : '', segment.key, ); if (seenCandidates.has(requestKey)) continue; @@ -2615,6 +2652,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement effectiveNativeSpeed, configProviderRef, ttsModel, + configProviderType, + providerModelPolicy.supportsInstructions ? ttsInstructions : '', plannedSegment?.key, ); const requestKey = buildSegmentRequestKey(locator, sentenceIndex, sentence, plannedSegment?.key); @@ -2644,6 +2683,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement effectiveNativeSpeed, configProviderRef, ttsModel, + configProviderType, + providerModelPolicy.supportsInstructions ? ttsInstructions : '', segment.key, ); const requestKey = buildSegmentRequestKey(segmentLocator, index, sentence, segment.key); diff --git a/src/lib/client/audiobooks/adapters/pdf.ts b/src/lib/client/audiobooks/adapters/pdf.ts index 9aff5ac..a462c32 100644 --- a/src/lib/client/audiobooks/adapters/pdf.ts +++ b/src/lib/client/audiobooks/adapters/pdf.ts @@ -41,12 +41,6 @@ function prepareParsedChapters({ .filter((block) => !skip.has(block.kind)); if (!allBlocks.length) return []; - const chaptersFromSections = settings.pdf?.chaptersFromSections ?? true; - if (!chaptersFromSections) { - const text = chapterTextFromBlocks(allBlocks, smartSentenceSplitting, maxBlockLength); - return text ? [{ index: 0, title: 'Document', text }] : []; - } - const chapters: PreparedAudiobookChapter[] = []; let currentTitle = 'Introduction'; let currentBlocks: ParsedPdfBlock[] = []; diff --git a/src/lib/shared/document-settings.ts b/src/lib/shared/document-settings.ts index 323a039..26b45e0 100644 --- a/src/lib/shared/document-settings.ts +++ b/src/lib/shared/document-settings.ts @@ -24,7 +24,6 @@ export function mergeDocumentSettings( schemaVersion: 1, pdf: { skipBlockKinds: [...(defaults.pdf?.skipBlockKinds ?? [])], - chaptersFromSections: defaults.pdf?.chaptersFromSections ?? true, }, }; @@ -38,10 +37,6 @@ export function mergeDocumentSettings( schemaVersion: 1, pdf: { skipBlockKinds: normalizeSkipKinds(pdfRec.skipBlockKinds), - chaptersFromSections: - typeof pdfRec.chaptersFromSections === 'boolean' - ? pdfRec.chaptersFromSections - : (base.pdf?.chaptersFromSections ?? true), }, }; } diff --git a/src/types/document-settings.ts b/src/types/document-settings.ts index 1bfd1ba..6908f33 100644 --- a/src/types/document-settings.ts +++ b/src/types/document-settings.ts @@ -4,7 +4,6 @@ export interface DocumentSettings { schemaVersion: 1; pdf?: { skipBlockKinds: ParsedPdfBlockKind[]; - chaptersFromSections: boolean; }; } @@ -12,6 +11,5 @@ export const DEFAULT_DOCUMENT_SETTINGS: DocumentSettings = { schemaVersion: 1, pdf: { skipBlockKinds: ['header', 'footer', 'footnote', 'vision_footnote'], - chaptersFromSections: true, }, }; diff --git a/tests/unit/pdf-audiobook-adapter.spec.ts b/tests/unit/pdf-audiobook-adapter.spec.ts index 20c7114..4acdeaa 100644 --- a/tests/unit/pdf-audiobook-adapter.spec.ts +++ b/tests/unit/pdf-audiobook-adapter.spec.ts @@ -55,7 +55,6 @@ test.describe('pdf audiobook adapter', () => { schemaVersion: 1, pdf: { skipBlockKinds: ['header'], - chaptersFromSections: true, }, }; @@ -120,7 +119,6 @@ test.describe('pdf audiobook adapter', () => { schemaVersion: 1, pdf: { skipBlockKinds: [], - chaptersFromSections: true, }, };