diff --git a/.env.example b/.env.example index 74afbf0..7457a88 100644 --- a/.env.example +++ b/.env.example @@ -79,7 +79,6 @@ IMPORT_LIBRARY_DIRS= # Heavy compute backend mode for ONNX whisper alignment + PDF layout parsing. # local = run compute in-process (default) -# none = disable both capabilities (good for preview/serverless) # worker = external durable worker mode (requires Redis-backed compute-worker service) COMPUTE_MODE=local # Required when COMPUTE_MODE=worker diff --git a/docs-site/docs/deploy/vercel-deployment.md b/docs-site/docs/deploy/vercel-deployment.md index e9a1162..3c1502b 100644 --- a/docs-site/docs/deploy/vercel-deployment.md +++ b/docs-site/docs/deploy/vercel-deployment.md @@ -40,7 +40,6 @@ ADMIN_EMAILS=you@example.com # comma-separated; admins manage TTS + features in # Heavy compute (recommended on Vercel in v1) # local = requires native binaries/models in-process (not recommended on Vercel) # worker = external durable compute worker (recommended) -# none = disable ONNX whisper alignment + PDF layout parsing COMPUTE_MODE=worker COMPUTE_WORKER_URL=https://your-compute-worker.example.com COMPUTE_WORKER_TOKEN=... diff --git a/docs-site/docs/reference/environment-variables.md b/docs-site/docs/reference/environment-variables.md index 3053452..207b30a 100644 --- a/docs-site/docs/reference/environment-variables.md +++ b/docs-site/docs/reference/environment-variables.md @@ -53,7 +53,7 @@ For auth-enabled deployments, use **Settings → Admin** as the primary source o | `RUN_FS_MIGRATIONS` | Storage migrations | `true` | Set `false` to skip startup filesystem -> S3/DB migration pass | | `IMPORT_LIBRARY_DIR` | Library import | `docstore/library` fallback | Set a single server library root | | `IMPORT_LIBRARY_DIRS` | Library import | unset | Set multiple roots (comma/colon/semicolon separated) | -| `COMPUTE_MODE` | Heavy compute backend | `local` | Set to `none` to disable ONNX word alignment + PDF layout parsing | +| `COMPUTE_MODE` | Heavy compute backend | `local` | Select `local` (in-process) or `worker` (external worker service) | | `COMPUTE_WORKER_URL` | Heavy compute backend | unset | Required when `COMPUTE_MODE=worker`; base URL for external compute worker | | `COMPUTE_WORKER_TOKEN` | Heavy compute backend | unset | Required bearer token for external compute worker auth | | `PDF_LAYOUT_MODEL_BASE_URL` | PDF layout model | PP-DocLayoutV3 ONNX base URL | Optional base URL override for `ensureModel()` | @@ -358,7 +358,6 @@ Selects the backend for heavy compute features (ONNX word alignment + PDF layout - Supported in v1: - `local`: run compute in-process on the app server - `worker`: enqueue async jobs in an external durable compute worker (Redis + BullMQ) - - `none`: disable these features cleanly - `worker` requires `COMPUTE_WORKER_URL` and `COMPUTE_WORKER_TOKEN` - `worker` assumes the external worker can directly reach shared object storage (S3-compatible endpoint) - `worker` is not compatible with non-exposed embedded `weed mini` storage topologies diff --git a/docs-site/docs/reference/stack.md b/docs-site/docs/reference/stack.md index 7534a86..7f4061a 100644 --- a/docs-site/docs/reference/stack.md +++ b/docs-site/docs/reference/stack.md @@ -59,7 +59,7 @@ Monorepo packages under `compute/`: - Storage: AWS SDK v3 S3 client for reading/writing blobs - Logging: [Pino](https://getpino.io/) - Validation: [Zod](https://zod.dev/) -- Compute mode is controlled by `COMPUTE_MODE` env var: `local` (in-process), `worker` (remote queue via HTTP + Redis), or disabled +- Compute mode is controlled by `COMPUTE_MODE` env var: `local` (in-process) or `worker` (remote queue via HTTP + Redis) ## Tooling and testing diff --git a/src/app/(app)/pdf/[id]/page.tsx b/src/app/(app)/pdf/[id]/page.tsx index c11441b..ba23846 100644 --- a/src/app/(app)/pdf/[id]/page.tsx +++ b/src/app/(app)/pdf/[id]/page.tsx @@ -3,7 +3,7 @@ import dynamic from 'next/dynamic'; import { useParams, useRouter } from 'next/navigation'; import Link from 'next/link'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState, type MouseEvent } from 'react'; import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton'; import { useTTS } from '@/contexts/TTSContext'; import { DocumentSettings } from '@/components/documents/DocumentSettings'; @@ -19,6 +19,7 @@ import { resolveDocumentId } from '@/lib/client/dexie'; import { RateLimitBanner } from '@/components/auth/RateLimitBanner'; import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext'; import { useFeatureFlag } from '@/contexts/RuntimeConfigContext'; +import { LoadingSpinner } from '@/components/Spinner'; import { usePdfDocument } from './usePdfDocument'; // Dynamic import for client-side rendering only @@ -32,7 +33,6 @@ const PDFViewer = dynamic( export default function PDFViewerPage() { const canExportAudiobook = useFeatureFlag('enableAudiobookExport'); - const computeAvailable = useFeatureFlag('computeAvailable'); const { id } = useParams(); const router = useRouter(); const pdfState = usePdfDocument(); @@ -60,6 +60,11 @@ export default function PDFViewerPage() { const [containerHeight, setContainerHeight] = useState('auto'); const inFlightDocIdRef = useRef(null); const loadedDocIdRef = useRef(null); + const backNavTimeoutRef = useRef | null>(null); + const clearCurrDocRef = useRef(clearCurrDoc); + const [isNavigatingBack, setIsNavigatingBack] = useState(false); + const parseState = parseStatus ?? 'pending'; + const isParseReady = parseState === 'ready'; useEffect(() => { setIsLoading(true); @@ -115,6 +120,25 @@ export default function PDFViewerPage() { loadDocument(); }, [loadDocument]); + useEffect(() => { + clearCurrDocRef.current = clearCurrDoc; + }, [clearCurrDoc]); + + useEffect(() => { + return () => { + if (backNavTimeoutRef.current) { + clearTimeout(backNavTimeoutRef.current); + } + clearCurrDocRef.current(); + }; + }, []); + + useEffect(() => { + if (isLoading) return; + if (isParseReady) return; + stop(); + }, [isLoading, isParseReady, stop]); + // Compute available height = viewport - (header height + tts bar height) useEffect(() => { const compute = () => { @@ -134,14 +158,33 @@ export default function PDFViewerPage() { const handleZoomIn = () => setZoomLevel(prev => Math.min(prev + 10, 300)); const handleZoomOut = () => setZoomLevel(prev => Math.max(prev - 10, 50)); + const handleBackToDocuments = useCallback((event?: MouseEvent) => { + event?.preventDefault(); + if (isNavigatingBack) return; + setIsNavigatingBack(true); + stop(); + const hadOpenSidebar = activeSidebar !== null; + setActiveSidebar(null); + const delayMs = hadOpenSidebar ? 220 : 0; + if (backNavTimeoutRef.current) { + clearTimeout(backNavTimeoutRef.current); + } + backNavTimeoutRef.current = setTimeout(() => { + router.push('/app'); + }, delayMs); + }, [isNavigatingBack, stop, activeSidebar, router]); + const handleGenerateAudiobook = useCallback(async ( onProgress: (progress: number) => void, signal: AbortSignal, onChapterComplete: (chapter: TTSAudiobookChapter) => void, settings: AudiobookGenerationSettings ) => { + if (!isParseReady) { + throw new Error('PDF parsing is not ready yet.'); + } return createPDFAudioBook(onProgress, signal, onChapterComplete, id as string, settings.format, settings); - }, [createPDFAudioBook, id]); + }, [createPDFAudioBook, id, isParseReady]); const handleRegenerateChapter = useCallback(async ( chapterIndex: number, @@ -149,8 +192,11 @@ export default function PDFViewerPage() { settings: AudiobookGenerationSettings, signal: AbortSignal ) => { + if (!isParseReady) { + throw new Error('PDF parsing is not ready yet.'); + } return regeneratePDFChapter(chapterIndex, bookId, settings.format, signal, settings); - }, [regeneratePDFChapter]); + }, [regeneratePDFChapter, isParseReady]); if (error) { return ( @@ -158,7 +204,7 @@ export default function PDFViewerPage() {

{error}

{ clearCurrDoc(); }} + onClick={handleBackToDocuments} className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition-all duration-200 ease-in-out hover:scale-[1.04] hover:text-accent" > @@ -170,13 +216,42 @@ export default function PDFViewerPage() { ); } + const renderPdfStatusLoader = () => { + let statusText = 'Loading PDF...'; + if (!isLoading) { + if (parseState === 'pending') { + statusText = 'Preparing PDF layout...'; + } else if (parseState === 'running') { + statusText = 'Parsing PDF layout blocks...'; + } else if (parseState === 'failed') { + statusText = 'PDF parsing failed. Retry to continue.'; + } + } + + return ( +
+ +

{statusText}

+ {!isLoading && parseState === 'failed' ? ( + + ) : null} +
+ ); + }; + return ( <>
clearCurrDoc()} + onClick={handleBackToDocuments} className="inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase transition-all duration-200 ease-in-out hover:scale-[1.04] hover:text-accent" aria-label="Back to documents" > @@ -207,11 +282,8 @@ export default function PDFViewerPage() { } />
- - {isLoading ? ( -
- -
+ {isLoading || !isParseReady ? ( + renderPdfStatusLoader() ) : ( )} @@ -233,14 +305,13 @@ export default function PDFViewerPage() {
- ) : ( + ) : isParseReady ? ( - )} + ) : null} setActiveSidebar((prev) => isOpen ? 'settings' : (prev === 'settings' ? null : prev))} pdf={{ - computeAvailable, parseStatus, parsedOverlayEnabled, skipBlockKinds: documentSettings.pdf?.skipBlockKinds ?? [], diff --git a/src/app/(app)/pdf/[id]/usePdfDocument.ts b/src/app/(app)/pdf/[id]/usePdfDocument.ts index 774f46d..6cb221e 100644 --- a/src/app/(app)/pdf/[id]/usePdfDocument.ts +++ b/src/app/(app)/pdf/[id]/usePdfDocument.ts @@ -31,12 +31,12 @@ import { ensureCachedDocument } from '@/lib/client/cache/documents'; import { useTTS } from '@/contexts/TTSContext'; import { useConfig } from '@/contexts/ConfigContext'; import { - extractTextFromPDF, highlightPattern, clearHighlights, clearWordHighlights, highlightWordIndex, } from '@/lib/client/pdf'; +import { buildPageTextFromBlocks } from '@/lib/client/pdf-block-text'; import type { CanonicalTtsSourceUnit } from '@/lib/shared/tts-segment-plan'; import { DEFAULT_DOCUMENT_SETTINGS, @@ -113,9 +113,6 @@ export interface PdfDocumentState { isAudioCombining: boolean; } -const EMPTY_TEXT_RETRY_DELAY_MS = 120; -const EMPTY_TEXT_MAX_RETRIES = 6; - /** * Main PDF route hook. */ @@ -130,10 +127,6 @@ export function usePdfDocument(): PdfDocumentState { registerVisualPageChangeHandler, } = useTTS(); const { - headerMargin, - footerMargin, - leftMargin, - rightMargin, apiKey, baseUrl, providerRef, @@ -154,18 +147,11 @@ export function usePdfDocument(): PdfDocumentState { const [parsedOverlayEnabled, setParsedOverlayEnabled] = useState(false); const [isAudioCombining] = useState(false); const audiobookAdapter = useMemo(() => createPdfAudiobookSourceAdapter({ - pdfDocument, parsed: parsedDocument ?? undefined, settings: documentSettings, - margins: { - header: documentSettings.pdf?.margins?.header ?? headerMargin, - footer: documentSettings.pdf?.margins?.footer ?? footerMargin, - left: documentSettings.pdf?.margins?.left ?? leftMargin, - right: documentSettings.pdf?.margins?.right ?? rightMargin, - }, smartSentenceSplitting, maxBlockLength: ttsSegmentMaxBlockLength, - }), [pdfDocument, parsedDocument, documentSettings, headerMargin, footerMargin, leftMargin, rightMargin, smartSentenceSplitting, ttsSegmentMaxBlockLength]); + }), [pdfDocument, parsedDocument, documentSettings, smartSentenceSplitting, ttsSegmentMaxBlockLength]); const pageTextCacheRef = useRef>(new Map()); const [currDocPage, setCurrDocPage] = useState(currDocPageNumber); @@ -174,7 +160,6 @@ export function usePdfDocument(): PdfDocumentState { const pdfDocGenerationRef = useRef(0); const pdfDocumentRef = useRef(undefined); const loadSeqRef = useRef(0); - const emptyRetryRef = useRef<{ page: number; attempt: number; timer: ReturnType | null } | null>(null); // Guards for setCurrentDocument to prevent stale loads from overwriting newer selections. const docLoadSeqRef = useRef(0); @@ -190,10 +175,6 @@ export function usePdfDocument(): PdfDocumentState { // document backfills parse output via the parsed endpoint polling path. const effectiveInitialStatus: PdfParseStatus = initialStatus ?? 'pending'; setParseStatus(effectiveInitialStatus); - if (effectiveInitialStatus === 'unsupported') { - setParsedDocument(null); - return; - } const maxAttempts = 25; const delayMs = 1200; @@ -210,7 +191,7 @@ export function usePdfDocument(): PdfDocumentState { return; } setParseStatus(result.status); - if (result.status === 'failed' || result.status === 'unsupported') { + if (result.status === 'failed') { setParsedDocument(null); return; } @@ -262,7 +243,7 @@ export function usePdfDocument(): PdfDocumentState { /** * Loads and processes text from the current document page - * Extracts text from the PDF and updates both document text and TTS text states + * Uses parsed PDF blocks only and updates both document text and TTS text states. * * @returns {Promise} */ @@ -274,30 +255,14 @@ export function usePdfDocument(): PdfDocumentState { const seq = ++loadSeqRef.current; const pageNumber = currDocPageNumber; - const existingRetry = emptyRetryRef.current; - if (existingRetry?.timer) { - clearTimeout(existingRetry.timer); - } - emptyRetryRef.current = - existingRetry && existingRetry.page === pageNumber - ? { ...existingRetry, timer: null } - : null; - - const margins = { - header: documentSettings.pdf?.margins?.header ?? headerMargin, - footer: documentSettings.pdf?.margins?.footer ?? footerMargin, - left: documentSettings.pdf?.margins?.left ?? leftMargin, - right: documentSettings.pdf?.margins?.right ?? rightMargin - }; - const pageFromParsed = (pageNum: number): ParsedPdfPage | undefined => parsedDocument?.pages.find((page) => page.pageNumber === pageNum); - const hasUsableParsedBlocks = (page: ParsedPdfPage | undefined): page is ParsedPdfPage => { - if (!page) return false; - const skipKinds = new Set(documentSettings.pdf?.skipBlockKinds ?? []); - return page.blocks.some((block) => !skipKinds.has(block.kind) && block.text.trim().length > 0); - }; + if (parseStatus !== 'ready' || !parsedDocument) { + setCurrDocText(undefined); + setTTSText('', { location: currDocPageNumber }); + return; + } const sourceUnitsFromParsedPage = (pageNum: number): CanonicalTtsSourceUnit[] => { const page = pageFromParsed(pageNum); @@ -332,14 +297,9 @@ export function usePdfDocument(): PdfDocumentState { } const parsedPage = pageFromParsed(pageNumber); - const useParsedPage = hasUsableParsedBlocks(parsedPage) ? parsedPage : undefined; - const extracted = await extractTextFromPDF( - currentPdf, - pageNumber, - margins, - useParsedPage, - useParsedPage ? documentSettings.pdf?.skipBlockKinds : undefined, - ); + const extracted = parsedPage + ? buildPageTextFromBlocks(parsedPage, documentSettings.pdf?.skipBlockKinds ?? []) + : ''; if (generation !== pdfDocGenerationRef.current || pdfDocumentRef.current !== currentPdf) { throw new DOMException('Stale PDF extraction', 'AbortError'); @@ -383,44 +343,15 @@ export function usePdfDocument(): PdfDocumentState { return; } - const trimmed = text.trim(); - if (!trimmed) { - const prevAttempt = emptyRetryRef.current?.page === pageNumber ? emptyRetryRef.current.attempt : 0; - const attempt = prevAttempt + 1; - - // Avoid pushing empty text into TTS immediately; transient empty extractions can happen - // during page turns or react-pdf worker churn. Retry a few times before treating it as - // a truly blank page. - if (attempt <= EMPTY_TEXT_MAX_RETRIES) { - const timer = setTimeout(() => { - if (generation !== pdfDocGenerationRef.current || pdfDocumentRef.current !== currentPdf) { - return; - } - if (pageNumber !== currDocPageNumber) { - return; - } - void loadCurrDocText(); - }, EMPTY_TEXT_RETRY_DELAY_MS); - - emptyRetryRef.current = { page: pageNumber, attempt, timer }; - return; - } - } else { - emptyRetryRef.current = null; - } - if (text !== currDocText || text === '') { setCurrDocText(text); const sourceUnits = sourceUnitsFromParsedPage(currDocPageNumber); - const useBlockStructuredPlanning = sourceUnits.length > 0; setTTSText(text, { location: currDocPageNumber, - ...(!useBlockStructuredPlanning ? { - previousText: prevText, - nextLocation: nextPageNumber, - nextText: nextText, - upcomingLocations: additionalUpcoming, - } : {}), + previousText: prevText, + nextLocation: nextPageNumber, + nextText: nextText, + upcomingLocations: additionalUpcoming, ...(sourceUnits.length > 0 ? { sourceUnits } : {}), }); } @@ -435,12 +366,9 @@ export function usePdfDocument(): PdfDocumentState { currDocPages, setTTSText, currDocText, - headerMargin, - footerMargin, - leftMargin, - rightMargin, segmentPreloadDepthPages, parsedDocument, + parseStatus, documentSettings, ]); @@ -474,10 +402,6 @@ export function usePdfDocument(): PdfDocumentState { // or fast refresh. pdfDocGenerationRef.current += 1; loadSeqRef.current += 1; - if (emptyRetryRef.current?.timer) { - clearTimeout(emptyRetryRef.current.timer); - } - emptyRetryRef.current = null; parsePollAbortRef.current?.abort(); parsePollAbortRef.current = null; pageTextCacheRef.current.clear(); @@ -570,10 +494,6 @@ export function usePdfDocument(): PdfDocumentState { docLoadAbortRef.current = null; parsePollAbortRef.current?.abort(); parsePollAbortRef.current = null; - if (emptyRetryRef.current?.timer) { - clearTimeout(emptyRetryRef.current.timer); - } - emptyRetryRef.current = null; setCurrDocId(undefined); setCurrDocName(undefined); setCurrDocData(undefined); diff --git a/src/app/api/documents/[id]/parsed/route.ts b/src/app/api/documents/[id]/parsed/route.ts index 96e281c..d23bfa2 100644 --- a/src/app/api/documents/[id]/parsed/route.ts +++ b/src/app/api/documents/[id]/parsed/route.ts @@ -23,6 +23,13 @@ function hasAnyParsedBlocks(doc: ParsedPdfDocument | null): boolean { return doc.pages.some((page) => Array.isArray(page.blocks) && page.blocks.length > 0); } +function normalizeParseStatus( + status: 'pending' | 'running' | 'ready' | 'failed' | 'unsupported' | null, +): 'pending' | 'running' | 'ready' | 'failed' { + if (status === 'unsupported' || status === null) return 'pending'; + return status; +} + export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { if (!isS3Configured()) return s3NotConfiguredResponse(); @@ -60,7 +67,7 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string return NextResponse.json({ error: 'Not found' }, { status: 404 }); } - if (row.parseStatus === 'failed' && retryFailed) { + if (row.parseStatus === 'unsupported') { await db .update(documents) .set({ parseStatus: 'pending' }) @@ -73,15 +80,30 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string return NextResponse.json({ parseStatus: 'pending' }, { status: 202 }); } - if (row.parseStatus !== 'ready') { - if (row.parseStatus === 'pending' || row.parseStatus === 'running' || row.parseStatus === null) { + const effectiveStatus = normalizeParseStatus(row.parseStatus); + + if (effectiveStatus === 'failed' && retryFailed) { + await db + .update(documents) + .set({ parseStatus: 'pending' }) + .where(and(eq(documents.id, id), eq(documents.userId, row.userId))); + enqueueParsePdfJob({ + documentId: id, + userId: row.userId, + namespace: testNamespace, + }); + return NextResponse.json({ parseStatus: 'pending' }, { status: 202 }); + } + + if (effectiveStatus !== 'ready') { + if (effectiveStatus === 'pending' || effectiveStatus === 'running') { enqueueParsePdfJob({ documentId: id, userId: row.userId, namespace: testNamespace, }); } - return NextResponse.json({ parseStatus: row.parseStatus ?? 'pending' }, { status: 202 }); + return NextResponse.json({ parseStatus: effectiveStatus }, { status: 202 }); } try { @@ -161,7 +183,9 @@ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string return NextResponse.json({ error: 'Not found' }, { status: 404 }); } - if (row.parseStatus !== 'running') { + const effectiveStatus = normalizeParseStatus(row.parseStatus); + + if (effectiveStatus !== 'running') { await db .update(documents) .set({ parseStatus: 'pending' }) @@ -175,7 +199,7 @@ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string }); return NextResponse.json( - { parseStatus: row.parseStatus === 'running' ? 'running' : 'pending' }, + { parseStatus: effectiveStatus === 'running' ? 'running' : 'pending' }, { status: 202 }, ); } catch (error) { diff --git a/src/app/api/documents/route.ts b/src/app/api/documents/route.ts index af1e835..966604c 100644 --- a/src/app/api/documents/route.ts +++ b/src/app/api/documents/route.ts @@ -25,6 +25,13 @@ type RegisterDocument = { lastModified: number; }; +function normalizeParseStatus( + status: 'pending' | 'running' | 'ready' | 'failed' | 'unsupported' | null, +): 'pending' | 'running' | 'ready' | 'failed' | null { + if (status === 'unsupported') return 'pending'; + return status; +} + function s3NotConfiguredResponse(): NextResponse { return NextResponse.json( { error: 'Documents storage is not configured. Set S3_* environment variables.' }, @@ -245,7 +252,7 @@ export async function GET(req: NextRequest) { size: Number(doc.size), lastModified: Number(doc.lastModified), type, - parseStatus: doc.parseStatus, + parseStatus: normalizeParseStatus(doc.parseStatus), parsedJsonKey: doc.parsedJsonKey, scope: doc.userId === unclaimedUserId ? 'unclaimed' : 'user', }; diff --git a/src/components/documents/DocumentSettings.tsx b/src/components/documents/DocumentSettings.tsx index ed6aa92..9a5ec8a 100644 --- a/src/components/documents/DocumentSettings.tsx +++ b/src/components/documents/DocumentSettings.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, type ChangeEvent } from 'react'; +import { useState, useEffect } from 'react'; import { useConfig, ViewType } from '@/contexts/ConfigContext'; import { ReaderSidebarShell } from '@/components/reader/ReaderSidebarShell'; import { @@ -15,7 +15,6 @@ import { clampSegmentPreloadSentenceLookahead, clampTtsSegmentMaxBlockLength, } from '@/types/config'; -import { useFeatureFlag } from '@/contexts/RuntimeConfigContext'; import { Section, ToggleRow, CheckItem, buttonClass, segmentedButtonClass, segmentedGroupClass } from '@/components/formPrimitives'; import { RefreshIcon, SparkleIcon } from '@/components/icons/Icons'; import type { ParsedPdfBlockKind, PdfParseStatus } from '@/types/parsed-pdf'; @@ -52,8 +51,6 @@ const viewTypeTextMapping = [ const rangeInputClassName = 'w-full bg-offbase rounded-md appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-md [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-md [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-3.5 [&::-moz-range-thumb]:w-3.5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent'; -type MarginKey = 'header' | 'footer' | 'left' | 'right'; - type RangeSettingProps = { label: string; value: number; @@ -103,7 +100,6 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: { epub?: boolean, html?: boolean, pdf?: { - computeAvailable: boolean; parseStatus: PdfParseStatus | null; parsedOverlayEnabled: boolean; skipBlockKinds: ParsedPdfBlockKind[]; @@ -114,8 +110,7 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: { onForceReparse: () => void; } }) { - const computeAvailable = useFeatureFlag('computeAvailable'); - const canWordHighlight = computeAvailable; + const canWordHighlight = true; const { viewType, skipBlank, @@ -124,10 +119,6 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: { segmentPreloadDepthPages, segmentPreloadSentenceLookahead, ttsSegmentMaxBlockLength, - headerMargin, - footerMargin, - leftMargin, - rightMargin, updateConfigKey, pdfHighlightEnabled, epubHighlightEnabled, @@ -136,31 +127,11 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: { htmlHighlightEnabled, htmlWordHighlightEnabled, } = useConfig(); - const [localMargins, setLocalMargins] = useState({ - header: headerMargin, - footer: footerMargin, - left: leftMargin, - right: rightMargin - }); const selectedView = viewTypeTextMapping.find(v => v.id === viewType) || viewTypeTextMapping[0]; + const isPdfMode = !epub && !html && !!pdf; const [localPreloadDepth, setLocalPreloadDepth] = useState(segmentPreloadDepthPages); const [localSentenceLookahead, setLocalSentenceLookahead] = useState(segmentPreloadSentenceLookahead); const [localMaxBlockLength, setLocalMaxBlockLength] = useState(ttsSegmentMaxBlockLength); - const marginValues: Record = { - header: headerMargin, - footer: footerMargin, - left: leftMargin, - right: rightMargin, - }; - - useEffect(() => { - setLocalMargins({ - header: headerMargin, - footer: footerMargin, - left: leftMargin, - right: rightMargin - }); - }, [headerMargin, footerMargin, leftMargin, rightMargin]); useEffect(() => { setLocalPreloadDepth(segmentPreloadDepthPages); @@ -174,22 +145,6 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: { setLocalMaxBlockLength(ttsSegmentMaxBlockLength); }, [ttsSegmentMaxBlockLength]); - // Handler for slider release - const handleMarginChangeComplete = (margin: MarginKey) => () => { - const value = localMargins[margin]; - const configKey = `${margin}Margin`; - if (value !== marginValues[margin]) { - updateConfigKey(configKey as 'headerMargin' | 'footerMargin' | 'leftMargin' | 'rightMargin', value); - } - }; - - const handleMarginSliderChange = (margin: MarginKey) => (event: ChangeEvent) => { - setLocalMargins((previous) => ({ - ...previous, - [margin]: Number(event.target.value), - })); - }; - return (
+ {isPdfMode && pdf && ( +
+
+ +
+ {viewTypeTextMapping.map((view) => { + const active = selectedView.id === view.id; + return ( + + ); + })} +
+ {selectedView.id === 'scroll' ? ( +

Scroll mode may be slower on large PDFs.

+ ) : null} +
+ updateConfigKey('pdfHighlightEnabled', checked)} + variant="flat" + /> + updateConfigKey('pdfWordHighlightEnabled', checked)} + variant="flat" + /> + +
+ )} + + {isPdfMode && pdf && ( +
+ + + PP-DocLayout-V3 + + + {pdf.parseStatus ?? 'pending'} + + +
+ } + > + +
+ + Skip Block Kinds While Reading Aloud + +
+ {PDF_SKIP_KIND_OPTIONS.map((option) => ( + pdf.onToggleSkipKind(option.kind, enabled)} + /> + ))} +
+
+ + )} +
- {!epub && !html && ( -
-
- -
- {viewTypeTextMapping.map((view) => { - const active = selectedView.id === view.id; - return ( - - ); - })} -
- {selectedView.id === 'scroll' ? ( -

Scroll mode may be slower on large PDFs.

- ) : null} -
- -
-

Text extraction margins

-

- Ignore edge content before extraction. -

-
- {(['header', 'footer', 'left', 'right'] as MarginKey[]).map((margin) => ( -
-
- {margin} - {Math.round(localMargins[margin] * 100)}% -
- -
- ))} -
-
-
- )} - - {!epub && !html && ( -
- updateConfigKey('pdfHighlightEnabled', checked)} - variant="flat" - /> - updateConfigKey('pdfWordHighlightEnabled', checked)} - variant="flat" - /> -
- )} - {epub && (
)} - - {!epub && !html && pdf && ( -
- - - PP-DocLayout-V3 - - - {pdf.parseStatus ?? 'pending'} - - - - } - > - - -
-

Skip while reading aloud

-
- {PDF_SKIP_KIND_OPTIONS.map((option) => ( - pdf.onToggleSkipKind(option.kind, enabled)} - /> - ))} -
-
-
- )}
); diff --git a/src/components/views/PDFViewer.tsx b/src/components/views/PDFViewer.tsx index 48c961b..8bbecec 100644 --- a/src/components/views/PDFViewer.tsx +++ b/src/components/views/PDFViewer.tsx @@ -119,6 +119,13 @@ export function PDFViewer({ zoomLevel, pdfState }: PDFViewerProps) { wordHighlightTimeoutsRef.current.push(t); }, []); + useEffect(() => { + return () => { + clearHighlights(); + clearWordHighlights(); + }; + }, [clearHighlights, clearWordHighlights]); + useEffect(() => { /* * Handles highlighting the current sentence being read by TTS. @@ -146,6 +153,12 @@ export function PDFViewer({ zoomLevel, pdfState }: PDFViewerProps) { return; } + // Root-cause guard: do not repaint highlights while react-pdf is still + // replacing page/text layers for a new page or viewport layout. + if (isPageRendering) { + return; + } + const seq = ++sentenceHighlightSeqRef.current; const isLayoutChange = layoutKey !== lastSentenceLayoutKeyRef.current; lastSentenceLayoutKeyRef.current = layoutKey; @@ -156,38 +169,40 @@ export function PDFViewer({ zoomLevel, pdfState }: PDFViewerProps) { && typeof activeLocator.blockId === 'string' && activeLocator.blockId.length > 0; - if (isLayoutChange) { + if (isLayoutChange || !hasParsedBlockLocator) { clearHighlights(); } + if (!hasParsedBlockLocator) { + return; + } + + const useBlockGeometryOnly = !pdfWordHighlightEnabled; + const tryApply = (attempt: number) => { if (seq !== sentenceHighlightSeqRef.current) return; const container = containerRef.current; if (!container) return; - if (hasParsedBlockLocator) { - highlightPattern(currDocText, currentSentence, containerRef as RefObject, { - parsedDocument, - locator: activeLocator, - useBlockGeometryOnly: !pdfWordHighlightEnabled, - }); - return; + if (!useBlockGeometryOnly) { + const spans = container.querySelectorAll('.react-pdf__Page__textContent span'); + if (!spans.length) { + if (attempt < 1) scheduleSentenceTimeout(() => tryApply(attempt + 1), 90); + return; + } } - const spans = container.querySelectorAll('.react-pdf__Page__textContent span'); - if (!spans.length) { - if (attempt < 10) scheduleSentenceTimeout(() => tryApply(attempt + 1), 75); - return; - } - - highlightPattern(currDocText, currentSentence, containerRef as RefObject); + highlightPattern(currDocText, currentSentence, containerRef as RefObject, { + parsedDocument, + locator: activeLocator, + useBlockGeometryOnly, + }); }; - scheduleSentenceTimeout(() => tryApply(0), 200); + scheduleSentenceTimeout(() => tryApply(0), useBlockGeometryOnly ? 80 : 120); return () => { clearSentenceHighlightTimeouts(); - clearHighlights(); }; }, [ currDocText, @@ -199,6 +214,7 @@ export function PDFViewer({ zoomLevel, pdfState }: PDFViewerProps) { pdfWordHighlightEnabled, parsedDocument, layoutKey, + isPageRendering, clearSentenceHighlightTimeouts, scheduleSentenceTimeout ]); @@ -228,6 +244,10 @@ export function PDFViewer({ zoomLevel, pdfState }: PDFViewerProps) { return; } + if (isPageRendering) { + return; + } + const seq = ++wordHighlightSeqRef.current; const isLayoutChange = layoutKey !== lastWordLayoutKeyRef.current; lastWordLayoutKeyRef.current = layoutKey; @@ -276,7 +296,8 @@ export function PDFViewer({ zoomLevel, pdfState }: PDFViewerProps) { highlightWordIndex, layoutKey, clearWordHighlightTimeouts, - scheduleWordTimeout + scheduleWordTimeout, + isPageRendering ]); // Add page dimensions state diff --git a/src/lib/client/api/documents.ts b/src/lib/client/api/documents.ts index 829a392..882ad4c 100644 --- a/src/lib/client/api/documents.ts +++ b/src/lib/client/api/documents.ts @@ -81,7 +81,7 @@ export async function getDocumentMetadata(id: string, options?: { signal?: Abort export async function getParsedPdfDocument( id: string, options?: { signal?: AbortSignal; retryFailed?: boolean }, -): Promise<{ status: 'ready'; parsed: ParsedPdfDocument } | { status: 'pending' | 'running' | 'failed' | 'unsupported' }> { +): Promise<{ status: 'ready'; parsed: ParsedPdfDocument } | { status: 'pending' | 'running' | 'failed' }> { const query = options?.retryFailed ? '?retry=1' : ''; const res = await fetch(`/api/documents/${encodeURIComponent(id)}/parsed${query}`, { signal: options?.signal, @@ -91,7 +91,7 @@ export async function getParsedPdfDocument( if (res.status === 202) { const data = (await res.json().catch(() => null)) as { parseStatus?: string } | null; const parseStatus = data?.parseStatus; - if (parseStatus === 'pending' || parseStatus === 'running' || parseStatus === 'failed' || parseStatus === 'unsupported') { + if (parseStatus === 'pending' || parseStatus === 'running' || parseStatus === 'failed') { return { status: parseStatus }; } return { status: 'pending' }; diff --git a/src/lib/client/audiobooks/adapters/pdf.ts b/src/lib/client/audiobooks/adapters/pdf.ts index 6c38631..9aff5ac 100644 --- a/src/lib/client/audiobooks/adapters/pdf.ts +++ b/src/lib/client/audiobooks/adapters/pdf.ts @@ -1,5 +1,3 @@ -import type { PDFDocumentProxy } from 'pdfjs-dist'; - import type { AudiobookSourceAdapter, PreparedAudiobookChapter } from '@/lib/client/audiobooks/pipeline'; import { normalizeTextForTts } from '@/lib/shared/nlp'; import type { ParsedPdfDocument, ParsedPdfBlock } from '@/types/parsed-pdf'; @@ -7,15 +5,8 @@ import type { DocumentSettings } from '@/types/document-settings'; import { DEFAULT_DOCUMENT_SETTINGS } from '@/types/document-settings'; interface PdfAudiobookAdapterOptions { - pdfDocument?: PDFDocumentProxy; parsed?: ParsedPdfDocument; settings?: DocumentSettings; - margins: { - header: number; - footer: number; - left: number; - right: number; - }; smartSentenceSplitting: boolean; maxBlockLength?: number; } @@ -90,47 +81,21 @@ function prepareParsedChapters({ } async function extractPreparedPdfChapters({ - pdfDocument, parsed, settings = DEFAULT_DOCUMENT_SETTINGS, - margins, smartSentenceSplitting, maxBlockLength, }: PdfAudiobookAdapterOptions): Promise { - if (parsed) { - const parsedChapters = prepareParsedChapters({ - parsed, - settings, - smartSentenceSplitting, - maxBlockLength, - }); - if (parsedChapters.length > 0) { - return parsedChapters; - } + if (!parsed) { + throw new Error('PDF parsing is not ready yet.'); } - if (!pdfDocument) { - throw new Error('No PDF document loaded'); - } - - const { extractTextFromPDF } = await import('@/lib/client/pdf'); - - const chapters: PreparedAudiobookChapter[] = []; - for (let pageNum = 1; pageNum <= pdfDocument.numPages; pageNum++) { - const rawText = await extractTextFromPDF(pdfDocument, pageNum, margins); - const trimmedText = rawText.trim(); - if (!trimmedText) { - continue; - } - - chapters.push({ - index: chapters.length, - title: `Page ${chapters.length + 1}`, - text: smartSentenceSplitting ? normalizeTextForTts(trimmedText, { maxBlockLength }) : trimmedText, - }); - } - - return chapters; + return prepareParsedChapters({ + parsed, + settings, + smartSentenceSplitting, + maxBlockLength, + }); } export function createPdfAudiobookSourceAdapter(options: PdfAudiobookAdapterOptions): AudiobookSourceAdapter { diff --git a/src/lib/client/pdf.ts b/src/lib/client/pdf.ts index 5941e8d..e1719dc 100644 --- a/src/lib/client/pdf.ts +++ b/src/lib/client/pdf.ts @@ -559,23 +559,20 @@ export function highlightPattern( const parsedDocument = options?.parsedDocument ?? null; const locator = options?.locator ?? null; - if ( - parsedDocument - && locator - && options?.useBlockGeometryOnly - && highlightParsedBlockGeometry(containerRef, parsedDocument, locator) - ) { + // Canonical path: parsed block locator is required for PDF sentence + // highlighting. Avoid broad full-page text matching fallbacks. + if (!parsedDocument || !locator || locator.readerType !== 'pdf' || !locator.blockId) { return; } - const spanNodes = ( - parsedDocument && locator - ? (collectSpanNodesForParsedBlock(container, parsedDocument, locator) - ?? Array.from(container.querySelectorAll('.react-pdf__Page__textContent span')) as HTMLElement[]) - : Array.from(container.querySelectorAll('.react-pdf__Page__textContent span')) as HTMLElement[] - ); + const spanNodes = collectSpanNodesForParsedBlock(container, parsedDocument, locator) ?? []; - if (!spanNodes.length) return; + if (!spanNodes.length) { + if (options?.useBlockGeometryOnly) { + highlightParsedBlockGeometry(containerRef, parsedDocument, locator); + } + return; + } lastSpanNodes = spanNodes; const tokens: PDFToken[] = []; @@ -604,6 +601,15 @@ export function highlightPattern( if (!tokens.length) return; lastTokens = tokens; + if (options?.useBlockGeometryOnly) { + lastSentenceTokenWindow = { + start: 0, + end: tokens.length - 1, + }; + highlightParsedBlockGeometry(containerRef, parsedDocument, locator); + return; + } + const patternLen = cleanPattern.length; // Core application of highlight logic once we know the best token window (if any) diff --git a/src/lib/server/compute/index.ts b/src/lib/server/compute/index.ts index d9f847d..4424f1f 100644 --- a/src/lib/server/compute/index.ts +++ b/src/lib/server/compute/index.ts @@ -1,5 +1,4 @@ import type { ComputeBackend, ComputeMode } from '@/lib/server/compute/types'; -import { NoneComputeBackend } from '@/lib/server/compute/none'; import { isComputeModeAvailable, readComputeMode } from '@/lib/server/compute/mode'; import { WorkerComputeBackend } from '@/lib/server/compute/worker'; @@ -7,10 +6,9 @@ let backend: ComputeBackend | null = null; function createBackend(): ComputeBackend { const mode: ComputeMode = readComputeMode(); - if (mode === 'none') return new NoneComputeBackend(); if (mode === 'worker') return new WorkerComputeBackend(); - // Intentionally lazy-load local compute so COMPUTE_MODE=none builds - // can avoid tracing heavy ONNX dependencies. + // Intentionally lazy-load local compute to avoid tracing heavy ONNX + // dependencies unless the backend is actually local. // eslint-disable-next-line @typescript-eslint/no-require-imports const { LocalComputeBackend } = require('@/lib/server/compute/local') as typeof import('@/lib/server/compute/local'); return new LocalComputeBackend(); @@ -22,5 +20,5 @@ export function getCompute(): ComputeBackend { } export function isComputeAvailable(): boolean { - return isComputeModeAvailable(readComputeMode()); + return isComputeModeAvailable(); } diff --git a/src/lib/server/compute/mode.ts b/src/lib/server/compute/mode.ts index 42b066e..5c2b83d 100644 --- a/src/lib/server/compute/mode.ts +++ b/src/lib/server/compute/mode.ts @@ -1,11 +1,17 @@ import type { ComputeMode } from '@/lib/server/compute/types'; export function readComputeMode(): ComputeMode { - const raw = (process.env.COMPUTE_MODE || 'local').trim().toLowerCase(); - if (raw === 'local' || raw === 'none' || raw === 'worker') return raw; - return 'local'; + const envValue = process.env.COMPUTE_MODE; + const raw = (envValue || '').trim().toLowerCase(); + if (!raw) return 'local'; + if (raw === 'local' || raw === 'worker') return raw; + throw new Error( + `Invalid COMPUTE_MODE="${envValue}". Expected "local" or "worker".`, + ); } -export function isComputeModeAvailable(mode: ComputeMode): boolean { - return mode !== 'none'; +export function isComputeModeAvailable(): boolean { + // Throws on invalid values so startup/runtime config fails fast. + readComputeMode(); + return true; } diff --git a/src/lib/server/compute/none.ts b/src/lib/server/compute/none.ts deleted file mode 100644 index 64202e8..0000000 --- a/src/lib/server/compute/none.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ComputeBackend, PdfLayoutInput, WhisperAlignInput, WhisperAlignResult } from '@/lib/server/compute/types'; -import { UnsupportedComputeError } from '@/lib/server/compute/types'; -import type { ParsedPdfDocument } from '@/types/parsed-pdf'; - -export class NoneComputeBackend implements ComputeBackend { - readonly mode = 'none' as const; - - async alignWords(input: WhisperAlignInput): Promise { - void input; - throw new UnsupportedComputeError('Word alignment is unavailable: COMPUTE_MODE=none'); - } - - async parsePdfLayout(input: PdfLayoutInput): Promise { - void input; - throw new UnsupportedComputeError('PDF layout parsing is unavailable: COMPUTE_MODE=none'); - } -} diff --git a/src/lib/server/compute/types.ts b/src/lib/server/compute/types.ts index 2acea80..f316756 100644 --- a/src/lib/server/compute/types.ts +++ b/src/lib/server/compute/types.ts @@ -1,6 +1,6 @@ import type { TTSAudioBuffer, TTSSentenceAlignment, ParsedPdfDocument } from '@openreader/compute-core'; -export type ComputeMode = 'local' | 'worker' | 'none'; +export type ComputeMode = 'local' | 'worker'; export interface WhisperAlignInput { audioBuffer?: TTSAudioBuffer; @@ -26,10 +26,3 @@ export interface ComputeBackend { alignWords(input: WhisperAlignInput): Promise; parsePdfLayout(input: PdfLayoutInput): Promise; } - -export class UnsupportedComputeError extends Error { - constructor(message: string) { - super(message); - this.name = 'UnsupportedComputeError'; - } -} diff --git a/src/lib/server/jobs/parsePdfJob.ts b/src/lib/server/jobs/parsePdfJob.ts index fba48e4..1410949 100644 --- a/src/lib/server/jobs/parsePdfJob.ts +++ b/src/lib/server/jobs/parsePdfJob.ts @@ -1,7 +1,6 @@ import { and, eq } from 'drizzle-orm'; import { db } from '@/db'; import { documents } from '@/db/schema'; -import { UnsupportedComputeError } from '@/lib/server/compute/types'; import { documentKey, putParsedDocumentBlob } from '@/lib/server/documents/blobstore'; import { getCompute } from '@/lib/server/compute'; import { clearTtsSegmentCache } from '@/lib/server/tts/segments-cache'; @@ -58,7 +57,7 @@ export async function parsePdfJob(input: ParsePdfJobInput): Promise { const message = error instanceof Error ? error.message : String(error); const stack = error instanceof Error ? error.stack : undefined; const cause = error instanceof Error ? error.cause : undefined; - const parseStatus = error instanceof UnsupportedComputeError ? 'unsupported' : 'failed'; + const parseStatus = 'failed'; try { await db .update(documents) diff --git a/src/lib/server/runtime-config.ts b/src/lib/server/runtime-config.ts index e1585c3..14be70f 100644 --- a/src/lib/server/runtime-config.ts +++ b/src/lib/server/runtime-config.ts @@ -6,7 +6,7 @@ import { type RuntimeConfigKey, type RuntimeConfigSource, } from '@/lib/server/admin/settings'; -import { isComputeModeAvailable, readComputeMode } from '@/lib/server/compute/mode'; +import { isComputeModeAvailable } from '@/lib/server/compute/mode'; export type ResolvedRuntimeConfig = RuntimeConfig & { computeAvailable: boolean; @@ -29,7 +29,7 @@ export async function getResolvedRuntimeConfig(): Promise const values = await getRuntimeConfig(); return { ...values, - computeAvailable: isComputeModeAvailable(readComputeMode()), + computeAvailable: isComputeModeAvailable(), }; } diff --git a/src/lib/shared/document-settings.ts b/src/lib/shared/document-settings.ts index 537038a..323a039 100644 --- a/src/lib/shared/document-settings.ts +++ b/src/lib/shared/document-settings.ts @@ -16,19 +16,6 @@ function normalizeSkipKinds(value: unknown): ParsedPdfBlockKind[] { return Array.from(new Set(out)); } -type PdfMargins = { header: number; footer: number; left: number; right: number }; - -function normalizeMargins(value: unknown): PdfMargins | undefined { - if (!value || typeof value !== 'object') return undefined; - const rec = value as Record; - const header = Number(rec.header); - const footer = Number(rec.footer); - const left = Number(rec.left); - const right = Number(rec.right); - if (![header, footer, left, right].every((n) => Number.isFinite(n))) return undefined; - return { header, footer, left, right }; -} - export function mergeDocumentSettings( defaults: DocumentSettings = DEFAULT_DOCUMENT_SETTINGS, stored: unknown, @@ -38,7 +25,6 @@ export function mergeDocumentSettings( pdf: { skipBlockKinds: [...(defaults.pdf?.skipBlockKinds ?? [])], chaptersFromSections: defaults.pdf?.chaptersFromSections ?? true, - ...(defaults.pdf?.margins ? { margins: defaults.pdf.margins } : {}), }, }; @@ -56,7 +42,6 @@ export function mergeDocumentSettings( typeof pdfRec.chaptersFromSections === 'boolean' ? pdfRec.chaptersFromSections : (base.pdf?.chaptersFromSections ?? true), - ...(normalizeMargins(pdfRec.margins) ? { margins: normalizeMargins(pdfRec.margins) } : {}), }, }; } diff --git a/src/types/document-settings.ts b/src/types/document-settings.ts index 0262ed5..1bfd1ba 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[]; - margins?: { header: number; footer: number; left: number; right: number }; chaptersFromSections: boolean; }; } diff --git a/src/types/documents.ts b/src/types/documents.ts index b25d8fe..3fa90c7 100644 --- a/src/types/documents.ts +++ b/src/types/documents.ts @@ -6,7 +6,7 @@ export interface BaseDocument { size: number; lastModified: number; type: DocumentType; - parseStatus?: 'pending' | 'running' | 'ready' | 'failed' | 'unsupported' | null; + parseStatus?: 'pending' | 'running' | 'ready' | 'failed' | null; parsedJsonKey?: string | null; scope?: 'user' | 'unclaimed'; folderId?: string; diff --git a/src/types/parsed-pdf.ts b/src/types/parsed-pdf.ts index c1c1daa..2ee67af 100644 --- a/src/types/parsed-pdf.ts +++ b/src/types/parsed-pdf.ts @@ -76,4 +76,4 @@ export interface ParsedPdfDocument { pages: ParsedPdfPage[]; } -export type PdfParseStatus = 'pending' | 'running' | 'ready' | 'failed' | 'unsupported'; +export type PdfParseStatus = 'pending' | 'running' | 'ready' | 'failed'; diff --git a/tests/unit/pdf-audiobook-adapter.spec.ts b/tests/unit/pdf-audiobook-adapter.spec.ts index cf357cf..c9d9bd8 100644 --- a/tests/unit/pdf-audiobook-adapter.spec.ts +++ b/tests/unit/pdf-audiobook-adapter.spec.ts @@ -62,7 +62,6 @@ test.describe('pdf audiobook adapter', () => { const adapter = createPdfAudiobookSourceAdapter({ parsed, settings, - margins: { header: 0.07, footer: 0.07, left: 0.07, right: 0.07 }, smartSentenceSplitting: false, });