diff --git a/compute/core/package.json b/compute/core/package.json index 90bd875..a149a85 100644 --- a/compute/core/package.json +++ b/compute/core/package.json @@ -13,6 +13,7 @@ }, "exports": { ".": "./src/index.ts", - "./local-runtime": "./src/local-runtime.ts" + "./local-runtime": "./src/local-runtime.ts", + "./types": "./src/types/index.ts" } } diff --git a/compute/core/src/types/index.ts b/compute/core/src/types/index.ts new file mode 100644 index 0000000..9215dce --- /dev/null +++ b/compute/core/src/types/index.ts @@ -0,0 +1,35 @@ +export type { + TTSAudioBuffer, + TTSAudioBytes, + TTSSentenceAlignment, + TTSSentenceWord, +} from './tts'; + +export type { + ParsedPdfBlock, + ParsedPdfBlockFragment, + ParsedPdfBlockKind, + ParsedPdfDocument, + ParsedPdfPage, + PdfParsePhase, + PdfParseProgress, + PdfParseStatus, +} from './parsed-pdf'; + +export type { + PdfLayoutJobRequest, + PdfLayoutJobResult, + PdfLayoutProgress, + PdfLayoutProgressPhase, + PdfLayoutOperationRequest, + WhisperAlignJobRequest, + WhisperAlignJobResult, + WhisperAlignOperationRequest, + WorkerJobErrorShape, + WorkerJobState, + WorkerJobStatusResponse, + WorkerJobTiming, + WorkerOperationKind, + WorkerOperationRequest, + WorkerOperationState, +} from '../contracts'; diff --git a/compute/core/src/types/parsed-pdf.ts b/compute/core/src/types/parsed-pdf.ts index 137eec4..01d7067 100644 --- a/compute/core/src/types/parsed-pdf.ts +++ b/compute/core/src/types/parsed-pdf.ts @@ -51,3 +51,13 @@ export interface ParsedPdfDocument { parsedAt: number; pages: ParsedPdfPage[]; } + +export type PdfParseStatus = 'pending' | 'running' | 'ready' | 'failed'; +export type PdfParsePhase = 'infer' | 'merge'; + +export interface PdfParseProgress { + totalPages: number; + pagesParsed: number; + currentPage?: number; + phase: PdfParsePhase; +} diff --git a/src/app/api/documents/[id]/parsed/events/route.ts b/src/app/api/documents/[id]/parsed/events/route.ts index 6853160..0cd37c4 100644 --- a/src/app/api/documents/[id]/parsed/events/route.ts +++ b/src/app/api/documents/[id]/parsed/events/route.ts @@ -8,13 +8,8 @@ import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/li import { isS3Configured } from '@/lib/server/storage/s3'; import type { PdfParseProgress, PdfParseStatus } from '@/types/parsed-pdf'; import { isValidDocumentId } from '@/lib/server/documents/blobstore'; -import { - isDocumentParseStateStale, - normalizeParseStatus, - parseDocumentParseState, - stringifyDocumentParseState, -} from '@/lib/server/documents/parse-state'; -import { getComputeOpStaleMs } from '@openreader/compute-core'; +import { normalizeParseStatus, parseDocumentParseState } from '@/lib/server/documents/parse-state'; +import { healStaleDocumentParseState } from '@/lib/server/documents/parse-state-healing'; export const dynamic = 'force-dynamic'; @@ -42,32 +37,12 @@ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -async function maybeHealStaleParseState(row: ParseRow): Promise { - const state = parseDocumentParseState(row.parseState); - const staleMs = getComputeOpStaleMs(); - if (!isDocumentParseStateStale(state, staleMs)) return row; - - const nextState = stringifyDocumentParseState({ - status: 'failed', - progress: null, - updatedAt: Date.now(), - error: `Parse state stale for more than ${staleMs}ms; marked failed for retry`, - }); - - await db - .update(documents) - .set({ parseState: nextState }) - .where(and(eq(documents.id, row.id), eq(documents.userId, row.userId))); - - return { - ...row, - parseState: nextState, - }; -} - async function toSnapshot(row: ParseRow): Promise { - const healed = await maybeHealStaleParseState(row); - const state = parseDocumentParseState(healed.parseState); + const state = await healStaleDocumentParseState({ + documentId: row.id, + userId: row.userId, + state: parseDocumentParseState(row.parseState), + }); const parseStatus = normalizeParseStatus(state.status); return { parseStatus, diff --git a/src/app/api/documents/[id]/parsed/route.ts b/src/app/api/documents/[id]/parsed/route.ts index c2e6c13..9e7fac2 100644 --- a/src/app/api/documents/[id]/parsed/route.ts +++ b/src/app/api/documents/[id]/parsed/route.ts @@ -12,15 +12,14 @@ import { } from '@/lib/server/documents/blobstore'; import { enqueueParsePdfJob } from '@/lib/server/jobs/parsePdfJob'; import { - isDocumentParseStateStale, normalizeParseStatus, parseDocumentParseState, stringifyDocumentParseState, } from '@/lib/server/documents/parse-state'; +import { healStaleDocumentParseState } from '@/lib/server/documents/parse-state-healing'; import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; import { isS3Configured } from '@/lib/server/storage/s3'; import type { ParsedPdfDocument } from '@/types/parsed-pdf'; -import { getComputeOpStaleMs } from '@openreader/compute-core'; export const dynamic = 'force-dynamic'; @@ -36,29 +35,6 @@ function hasAnyParsedBlocks(doc: ParsedPdfDocument | null): boolean { return doc.pages.some((page) => Array.isArray(page.blocks) && page.blocks.length > 0); } -async function healStaleInProgressParseState(input: { - documentId: string; - userId: string; - state: ReturnType; -}): Promise> { - const staleMs = getComputeOpStaleMs(); - if (!isDocumentParseStateStale(input.state, staleMs)) return input.state; - - const nextState = parseDocumentParseState(stringifyDocumentParseState({ - status: 'failed', - progress: null, - updatedAt: Date.now(), - error: `Parse state stale for more than ${staleMs}ms; marked failed for retry`, - })); - - await db - .update(documents) - .set({ parseState: stringifyDocumentParseState(nextState) }) - .where(and(eq(documents.id, input.documentId), eq(documents.userId, input.userId))); - - return nextState; -} - export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { if (!isS3Configured()) return s3NotConfiguredResponse(); @@ -99,7 +75,7 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string } let state = parseDocumentParseState(row.parseState); - state = await healStaleInProgressParseState({ + state = await healStaleDocumentParseState({ documentId: id, userId: row.userId, state, @@ -216,7 +192,7 @@ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string } let state = parseDocumentParseState(row.parseState); - state = await healStaleInProgressParseState({ + state = await healStaleDocumentParseState({ documentId: id, userId: row.userId, state, diff --git a/src/lib/server/compute/types.ts b/src/lib/server/compute/types.ts index 92322a1..b973519 100644 --- a/src/lib/server/compute/types.ts +++ b/src/lib/server/compute/types.ts @@ -3,7 +3,7 @@ import type { TTSSentenceAlignment, ParsedPdfDocument, PdfLayoutProgress, -} from '@openreader/compute-core'; +} from '@openreader/compute-core/types'; export type ComputeMode = 'local' | 'worker'; diff --git a/src/lib/server/compute/worker-contract.ts b/src/lib/server/compute/worker-contract.ts deleted file mode 100644 index 83d5a7a..0000000 --- a/src/lib/server/compute/worker-contract.ts +++ /dev/null @@ -1,17 +0,0 @@ -export { - type PdfLayoutJobRequest, - type PdfLayoutJobResult, - type PdfLayoutProgress, - type PdfLayoutProgressPhase, - type PdfLayoutOperationRequest, - type WhisperAlignJobRequest, - type WhisperAlignJobResult, - type WhisperAlignOperationRequest, - type WorkerJobErrorShape, - type WorkerOperationKind, - type WorkerOperationRequest, - type WorkerOperationState, - type WorkerJobState, - type WorkerJobTiming, - type WorkerJobStatusResponse, -} from '@openreader/compute-core'; diff --git a/src/lib/server/compute/worker.ts b/src/lib/server/compute/worker.ts index 2fa7cdf..c941030 100644 --- a/src/lib/server/compute/worker.ts +++ b/src/lib/server/compute/worker.ts @@ -7,7 +7,7 @@ import type { WhisperAlignJobRequest, WhisperAlignJobResult, WorkerOperationState, -} from '@/lib/server/compute/worker-contract'; +} from '@openreader/compute-core/types'; class WorkerHttpError extends Error { status: number; diff --git a/src/lib/server/documents/parse-state-healing.ts b/src/lib/server/documents/parse-state-healing.ts new file mode 100644 index 0000000..b7a946c --- /dev/null +++ b/src/lib/server/documents/parse-state-healing.ts @@ -0,0 +1,34 @@ +import { and, eq } from 'drizzle-orm'; +import { getComputeOpStaleMs } from '@openreader/compute-core'; +import { db } from '@/db'; +import { documents } from '@/db/schema'; +import { + isDocumentParseStateStale, + parseDocumentParseState, + stringifyDocumentParseState, + type DocumentParseState, +} from '@/lib/server/documents/parse-state'; + +export async function healStaleDocumentParseState(input: { + documentId: string; + userId: string; + state: DocumentParseState; +}): Promise { + const staleMs = getComputeOpStaleMs(); + if (!isDocumentParseStateStale(input.state, staleMs)) return input.state; + + const nextState = parseDocumentParseState(stringifyDocumentParseState({ + status: 'failed', + progress: null, + updatedAt: Date.now(), + error: `Parse state stale for more than ${staleMs}ms; marked failed for retry`, + })); + + await db + .update(documents) + .set({ parseState: stringifyDocumentParseState(nextState) }) + .where(and(eq(documents.id, input.documentId), eq(documents.userId, input.userId))); + + return nextState; +} + diff --git a/src/lib/server/jobs/parsePdfJob.ts b/src/lib/server/jobs/parsePdfJob.ts index a5fbef0..e753d4b 100644 --- a/src/lib/server/jobs/parsePdfJob.ts +++ b/src/lib/server/jobs/parsePdfJob.ts @@ -5,7 +5,7 @@ import { documentKey, putParsedDocumentBlob } from '@/lib/server/documents/blobs import { stringifyDocumentParseState } from '@/lib/server/documents/parse-state'; import { getCompute } from '@/lib/server/compute'; import { clearTtsSegmentCache } from '@/lib/server/tts/segments-cache'; -import type { PdfLayoutProgress } from '@openreader/compute-core'; +import type { PdfLayoutProgress } from '@openreader/compute-core/types'; interface ParsePdfJobInput { documentId: string; diff --git a/src/types/parsed-pdf.ts b/src/types/parsed-pdf.ts index a7d5650..2c7bdf7 100644 --- a/src/types/parsed-pdf.ts +++ b/src/types/parsed-pdf.ts @@ -1,25 +1,12 @@ -export type ParsedPdfBlockKind = - | 'abstract' - | 'algorithm' - | 'aside_text' - | 'chart' - | 'content' - | 'formula' - | 'doc_title' - | 'figure_title' - | 'footer' - | 'footnote' - | 'formula_number' - | 'header' - | 'image' - | 'number' - | 'paragraph_title' - | 'reference' - | 'reference_content' - | 'seal' - | 'table' - | 'text' - | 'vision_footnote'; +import type { ParsedPdfBlockKind } from '@openreader/compute-core/types'; + +export { + type ParsedPdfBlock, + type ParsedPdfBlockFragment, + type ParsedPdfBlockKind, + type ParsedPdfDocument, + type ParsedPdfPage, +} from '@openreader/compute-core/types'; export const PARSED_PDF_BLOCK_KINDS: ParsedPdfBlockKind[] = [ 'abstract', @@ -45,43 +32,8 @@ export const PARSED_PDF_BLOCK_KINDS: ParsedPdfBlockKind[] = [ 'vision_footnote', ]; -export interface ParsedPdfBlockFragment { - page: number; - bbox: [number, number, number, number]; - text: string; - readingOrder: number; - modelConfidence?: number; -} - -export interface ParsedPdfBlock { - id: string; - kind: ParsedPdfBlockKind; - fragments: ParsedPdfBlockFragment[]; - text: string; - parentSectionId?: string; -} - -export interface ParsedPdfPage { - pageNumber: number; - width: number; - height: number; - blocks: ParsedPdfBlock[]; -} - -export interface ParsedPdfDocument { - schemaVersion: 1; - documentId: string; - parserVersion: string; - parsedAt: number; - pages: ParsedPdfPage[]; -} - -export type PdfParseStatus = 'pending' | 'running' | 'ready' | 'failed'; -export type PdfParsePhase = 'infer' | 'merge'; - -export interface PdfParseProgress { - totalPages: number; - pagesParsed: number; - currentPage?: number; - phase: PdfParsePhase; -} +export type { + PdfParsePhase, + PdfParseProgress, + PdfParseStatus, +} from '@openreader/compute-core/types'; diff --git a/src/types/tts.ts b/src/types/tts.ts index 0ed05cf..a3f76d8 100644 --- a/src/types/tts.ts +++ b/src/types/tts.ts @@ -1,11 +1,13 @@ import type { CanonicalTtsSegment } from '@/lib/shared/tts-segment-plan'; +export type { + TTSAudioBuffer, + TTSAudioBytes, + TTSSentenceAlignment, + TTSSentenceWord, +} from '@openreader/compute-core/types'; export type TTSLocation = string | number; -// Core audio representations used across TTS and audiobook flows -export type TTSAudioBuffer = ArrayBuffer; -export type TTSAudioBytes = number[]; // JSON-safe representation (Array.from(new Uint8Array(buffer))) - // Core playback state exposed by the TTS context export interface TTSPlaybackState { isPlaying: boolean; @@ -24,21 +26,6 @@ export interface TTSPageTurnEstimate { fraction: number; } -// Word-level alignment within a single spoken sentence/block -export interface TTSSentenceWord { - text: string; - startSec: number; - endSec: number; - charStart: number; - charEnd: number; -} - -// Alignment metadata for a single TTS sentence/block -export interface TTSSentenceAlignment { - sentence: string; - sentenceIndex: number; - words: TTSSentenceWord[]; -} interface EpubRenderedLocationWalkItem { /** Page-start CFI from the rendition — best-effort jump hint only. */ diff --git a/tsconfig.json b/tsconfig.json index 47f6fae..d590d49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,8 @@ "paths": { "@/*": ["./src/*"], "@openreader/compute-core": ["./compute/core/src/index.ts"], - "@openreader/compute-core/local-runtime": ["./compute/core/src/local-runtime.ts"] + "@openreader/compute-core/local-runtime": ["./compute/core/src/local-runtime.ts"], + "@openreader/compute-core/types": ["./compute/core/src/types/index.ts"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],