From f5d7408f179eb91ddca1c16fd0d5a446b9219741 Mon Sep 17 00:00:00 2001 From: Richard R Date: Tue, 19 May 2026 18:25:27 -0600 Subject: [PATCH] refactor(pdf-layout): remove compute "none" mode and unsupported parse status Eliminate the "none" compute mode and all related code paths, including the NoneComputeBackend, "unsupported" parse status, and PDF margin settings. Parsing is now always available if the app starts successfully, and configuration is limited to "local" or "worker" compute modes. Update types, API routes, client adapters, and documentation to reflect this simplification. BREAKING CHANGE: "none" is no longer a valid COMPUTE_MODE; only "local" and "worker" are supported. "unsupported" parse status and PDF margin settings are removed. --- .env.example | 1 - docs-site/docs/deploy/vercel-deployment.md | 1 - .../docs/reference/environment-variables.md | 3 +- docs-site/docs/reference/stack.md | 2 +- src/app/(app)/pdf/[id]/page.tsx | 99 +++++- src/app/(app)/pdf/[id]/usePdfDocument.ts | 114 +------ src/app/api/documents/[id]/parsed/route.ts | 36 +- src/app/api/documents/route.ts | 9 +- src/components/documents/DocumentSettings.tsx | 308 +++++++----------- src/components/views/PDFViewer.tsx | 57 +++- src/lib/client/api/documents.ts | 4 +- src/lib/client/audiobooks/adapters/pdf.ts | 51 +-- src/lib/client/pdf.ts | 32 +- src/lib/server/compute/index.ts | 8 +- src/lib/server/compute/mode.ts | 16 +- src/lib/server/compute/none.ts | 17 - src/lib/server/compute/types.ts | 9 +- src/lib/server/jobs/parsePdfJob.ts | 3 +- src/lib/server/runtime-config.ts | 4 +- src/lib/shared/document-settings.ts | 15 - src/types/document-settings.ts | 1 - src/types/documents.ts | 2 +- src/types/parsed-pdf.ts | 2 +- tests/unit/pdf-audiobook-adapter.spec.ts | 1 - 24 files changed, 343 insertions(+), 452 deletions(-) delete mode 100644 src/lib/server/compute/none.ts 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, });