From 12c586d7bed38c99fe4cdbb5c86df2bedbd4c9f3 Mon Sep 17 00:00:00 2001 From: Richard R Date: Sun, 24 May 2026 11:39:25 -0600 Subject: [PATCH] refactor(core,server): migrate contracts to api-contracts and modularize job handlers Move shared type contracts from `contracts` to new `api-contracts` module in `compute/core` for clearer API boundaries. Remove legacy `contracts`, `pdf`, and `whisper` index files. Update all imports to use `api-contracts`. Refactor server job logic by replacing `parsePdfJob.ts` with modular `user-pdf-layout-job.ts` and `user-whisper-align-job.ts`, updating all relevant API routes and compute integration. This improves maintainability and separation of concerns across compute and server layers. --- compute/core/package.json | 1 + .../src/{contracts => api-contracts}/index.ts | 10 ++++++-- compute/core/src/index.ts | 2 +- compute/core/src/pdf/index.ts | 3 --- compute/core/src/types/index.ts | 18 ------------- compute/core/src/whisper/index.ts | 18 ------------- compute/worker/src/server.ts | 24 ++++++++++-------- eslint.config.mjs | 3 ++- .../api/documents/[id]/parsed/events/route.ts | 2 +- src/app/api/documents/[id]/parsed/route.ts | 2 +- src/app/api/documents/route.ts | 2 +- src/app/api/tts/segments/ensure/route.ts | 21 ++++++---------- src/lib/server/compute/local.ts | 2 +- src/lib/server/compute/types.ts | 7 ++---- src/lib/server/compute/worker.ts | 2 +- ...{parsePdfJob.ts => user-pdf-layout-job.ts} | 14 +++++------ src/lib/server/jobs/user-whisper-align-job.ts | 25 +++++++++++++++++++ 17 files changed, 70 insertions(+), 86 deletions(-) rename compute/core/src/{contracts => api-contracts}/index.ts (91%) delete mode 100644 compute/core/src/pdf/index.ts delete mode 100644 compute/core/src/whisper/index.ts rename src/lib/server/jobs/{parsePdfJob.ts => user-pdf-layout-job.ts} (92%) create mode 100644 src/lib/server/jobs/user-whisper-align-job.ts diff --git a/compute/core/package.json b/compute/core/package.json index a149a85..9f83093 100644 --- a/compute/core/package.json +++ b/compute/core/package.json @@ -14,6 +14,7 @@ "exports": { ".": "./src/index.ts", "./local-runtime": "./src/local-runtime.ts", + "./api-contracts": "./src/api-contracts/index.ts", "./types": "./src/types/index.ts" } } diff --git a/compute/core/src/contracts/index.ts b/compute/core/src/api-contracts/index.ts similarity index 91% rename from compute/core/src/contracts/index.ts rename to compute/core/src/api-contracts/index.ts index 280e2d9..ff4b2a0 100644 --- a/compute/core/src/contracts/index.ts +++ b/compute/core/src/api-contracts/index.ts @@ -18,10 +18,13 @@ export type { export const ALIGN_QUEUE_NAME = 'whisper-align'; export const PDF_LAYOUT_QUEUE_NAME = 'pdf-layout'; -export interface WhisperAlignJobRequest { +export interface WhisperAlignJobBase { text: string; lang?: string; cacheKey?: string; +} + +export interface WhisperAlignJobRequest extends WhisperAlignJobBase { audioObjectKey: string; } @@ -30,9 +33,12 @@ export interface WhisperAlignJobResult { timing?: WorkerJobTiming; } -export interface PdfLayoutJobRequest { +export interface PdfLayoutJobBase { documentId: string; namespace: string | null; +} + +export interface PdfLayoutJobRequest extends PdfLayoutJobBase { documentObjectKey: string; } diff --git a/compute/core/src/index.ts b/compute/core/src/index.ts index 74f96a6..c709b34 100644 --- a/compute/core/src/index.ts +++ b/compute/core/src/index.ts @@ -1,4 +1,4 @@ -export * from './contracts'; +export * from './api-contracts'; export { getComputeJobConcurrency, getAvailableCpuCores, diff --git a/compute/core/src/pdf/index.ts b/compute/core/src/pdf/index.ts deleted file mode 100644 index fe3093a..0000000 --- a/compute/core/src/pdf/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { renderPage } from './render'; -export { mergeTextWithRegions } from './merge'; -export { stitchCrossPageBlocks } from './stitch'; diff --git a/compute/core/src/types/index.ts b/compute/core/src/types/index.ts index 9215dce..7408914 100644 --- a/compute/core/src/types/index.ts +++ b/compute/core/src/types/index.ts @@ -15,21 +15,3 @@ export type { 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/whisper/index.ts b/compute/core/src/whisper/index.ts deleted file mode 100644 index 5246a13..0000000 --- a/compute/core/src/whisper/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -export { - alignAudioWithText, - makeWhisperCacheKey, - type WhisperRequestBody, -} from './align'; - -export { - ensureWhisperModel, - ensureWhisperArtifacts, - createSingleflightRunner, - type WhisperArtifactSpec, - type WhisperStaticArtifactSpec, - type WhisperFetch, -} from './model'; - -export { mapWordsToSentenceOffsets, type WhisperWord } from './alignment-map'; -export { buildGoertzelCoefficients, goertzelPower } from './spectral'; -export { buildWordsFromTimestampedTokens, extractTokenStartTimestamps } from './token-timestamps'; diff --git a/compute/worker/src/server.ts b/compute/worker/src/server.ts index 84b6bd9..0b3a381 100644 --- a/compute/worker/src/server.ts +++ b/compute/worker/src/server.ts @@ -33,18 +33,20 @@ import { getOnnxThreadsPerJob, withIdleTimeoutAndHardCap, withTimeout, - type PdfLayoutJobRequest, - type PdfLayoutJobResult, - type WhisperAlignJobRequest, - type WhisperAlignJobResult, - type WorkerJobErrorShape, - type WorkerJobState, - type WorkerJobTiming, - type WorkerOperationKind, - type WorkerOperationRequest, - type WorkerOperationState, - type PdfLayoutProgress, } from '@openreader/compute-core'; +import type { + PdfLayoutJobRequest, + PdfLayoutJobResult, + WhisperAlignJobRequest, + WhisperAlignJobResult, + WorkerJobErrorShape, + WorkerJobState, + WorkerJobTiming, + WorkerOperationKind, + WorkerOperationRequest, + WorkerOperationState, + PdfLayoutProgress, +} from '@openreader/compute-core/api-contracts'; import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; const JOBS_STREAM_NAME = 'compute_jobs'; diff --git a/eslint.config.mjs b/eslint.config.mjs index 06ee366..bc5dd4a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -22,9 +22,10 @@ const eslintConfig = [ "@openreader/compute-core/*", "!@openreader/compute-core/local-runtime", "!@openreader/compute-core/types", + "!@openreader/compute-core/api-contracts", ], message: - "Use '@openreader/compute-core' root imports for light APIs. Allowed subpaths are '@openreader/compute-core/local-runtime' and '@openreader/compute-core/types'.", + "Use '@openreader/compute-core' root imports for light APIs. Allowed subpaths are '@openreader/compute-core/local-runtime', '@openreader/compute-core/types', and '@openreader/compute-core/api-contracts'.", }, ], }, diff --git a/src/app/api/documents/[id]/parsed/events/route.ts b/src/app/api/documents/[id]/parsed/events/route.ts index 0cd37c4..39738f2 100644 --- a/src/app/api/documents/[id]/parsed/events/route.ts +++ b/src/app/api/documents/[id]/parsed/events/route.ts @@ -3,7 +3,7 @@ import { and, eq, inArray } from 'drizzle-orm'; import { db } from '@/db'; import { documents } from '@/db/schema'; import { requireAuthContext } from '@/lib/server/auth/auth'; -import { enqueueParsePdfJob } from '@/lib/server/jobs/parsePdfJob'; +import { enqueueParsePdfJob } from '@/lib/server/jobs/user-pdf-layout-job'; import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; import { isS3Configured } from '@/lib/server/storage/s3'; import type { PdfParseProgress, PdfParseStatus } from '@/types/parsed-pdf'; diff --git a/src/app/api/documents/[id]/parsed/route.ts b/src/app/api/documents/[id]/parsed/route.ts index 9e7fac2..98b5c25 100644 --- a/src/app/api/documents/[id]/parsed/route.ts +++ b/src/app/api/documents/[id]/parsed/route.ts @@ -10,7 +10,7 @@ import { isMissingBlobError, isValidDocumentId, } from '@/lib/server/documents/blobstore'; -import { enqueueParsePdfJob } from '@/lib/server/jobs/parsePdfJob'; +import { enqueueParsePdfJob } from '@/lib/server/jobs/user-pdf-layout-job'; import { normalizeParseStatus, parseDocumentParseState, diff --git a/src/app/api/documents/route.ts b/src/app/api/documents/route.ts index 05c6aeb..433738c 100644 --- a/src/app/api/documents/route.ts +++ b/src/app/api/documents/route.ts @@ -9,7 +9,7 @@ import { deleteDocumentPreviewRows, enqueueDocumentPreview, } from '@/lib/server/documents/previews'; -import { enqueueParsePdfJob } from '@/lib/server/jobs/parsePdfJob'; +import { enqueueParsePdfJob } from '@/lib/server/jobs/user-pdf-layout-job'; import { deleteDocumentBlob, headDocumentBlob, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents/blobstore'; import { normalizeParseStatus, diff --git a/src/app/api/tts/segments/ensure/route.ts b/src/app/api/tts/segments/ensure/route.ts index 2c13715..31bb9d6 100644 --- a/src/app/api/tts/segments/ensure/route.ts +++ b/src/app/api/tts/segments/ensure/route.ts @@ -31,7 +31,7 @@ import { getClientIp } from '@/lib/server/rate-limit/request-ip'; import { getOrCreateDeviceId, setDeviceIdCookie } from '@/lib/server/rate-limit/device-id'; import { buildDailyQuotaExceededResponse } from '@/lib/server/rate-limit/problem-response'; import { getUpstreamRetryAfterSeconds, getUpstreamStatus } from '@/lib/server/tts/upstream-response'; -import { getCompute } from '@/lib/server/compute'; +import { userWhisperAlignJob } from '@/lib/server/jobs/user-whisper-align-job'; import { getResolvedRuntimeConfig } from '@/lib/server/runtime-config'; import { resolveTtsModelForProvider } from '@/lib/shared/tts-provider-policy'; import { resolveSegmentAudioUrls } from '@/lib/server/tts/segment-audio-urls'; @@ -154,11 +154,6 @@ export async function POST(request: NextRequest) { let deviceIdToSet: string | null = null; const requestId = randomUUID(); const requestStartedAt = Date.now(); - let computeBackendPromise: ReturnType | null = null; - const getComputeBackend = async () => { - if (!computeBackendPromise) computeBackendPromise = getCompute(); - return computeBackendPromise; - }; try { if (!isS3Configured()) return s3NotConfiguredResponse(); @@ -395,13 +390,12 @@ export async function POST(request: NextRequest) { if (!alignment && !request.signal.aborted) { try { const alignStartedAt = Date.now(); - const computeBackend = await getComputeBackend(); - const aligned = (await computeBackend.alignWords({ + alignment = await userWhisperAlignJob({ audioObjectKey: existing.audioKey, text: segment.text, - })).alignments; + sentenceIndex: segment.original.segmentIndex, + }); stageTimings.selfHealAlignMs = Date.now() - alignStartedAt; - alignment = aligned[0] ? { ...aligned[0], sentenceIndex: segment.original.segmentIndex } : null; if (alignment) { await db @@ -649,13 +643,12 @@ export async function POST(request: NextRequest) { try { failedStage = 'whisper.align'; const alignStartedAt = Date.now(); - const computeBackend = await getComputeBackend(); - const aligned = (await computeBackend.alignWords({ + alignment = await userWhisperAlignJob({ audioObjectKey: audioKey, text: segment.text, - })).alignments; + sentenceIndex: segment.original.segmentIndex, + }); stageTimings.whisperAlignMs = Date.now() - alignStartedAt; - alignment = aligned[0] ? { ...aligned[0], sentenceIndex: segment.original.segmentIndex } : null; } catch (alignError) { const aborted = isAbortLikeError(alignError) || request.signal.aborted; const log = aborted ? console.info : console.warn; diff --git a/src/lib/server/compute/local.ts b/src/lib/server/compute/local.ts index 2d87965..001ed1e 100644 --- a/src/lib/server/compute/local.ts +++ b/src/lib/server/compute/local.ts @@ -6,8 +6,8 @@ import { getComputeTimeoutConfig, withIdleTimeoutAndHardCap, withTimeout, - type PdfLayoutProgress, } from '@openreader/compute-core'; +import type { PdfLayoutProgress } from '@openreader/compute-core/api-contracts'; import { runPdfLayoutFromPdfBuffer, runWhisperAlignmentFromAudioBuffer, diff --git a/src/lib/server/compute/types.ts b/src/lib/server/compute/types.ts index b973519..ab178ac 100644 --- a/src/lib/server/compute/types.ts +++ b/src/lib/server/compute/types.ts @@ -2,17 +2,14 @@ import type { TTSAudioBuffer, TTSSentenceAlignment, ParsedPdfDocument, - PdfLayoutProgress, } from '@openreader/compute-core/types'; +import type { PdfLayoutProgress, WhisperAlignJobBase } from '@openreader/compute-core/api-contracts'; export type ComputeMode = 'local' | 'worker'; -export interface WhisperAlignInput { +export interface WhisperAlignInput extends WhisperAlignJobBase { audioBuffer?: TTSAudioBuffer; audioObjectKey?: string; - text: string; - cacheKey?: string; - lang?: string; } export interface WhisperAlignResult { diff --git a/src/lib/server/compute/worker.ts b/src/lib/server/compute/worker.ts index c941030..0113fe1 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 '@openreader/compute-core/types'; +} from '@openreader/compute-core/api-contracts'; class WorkerHttpError extends Error { status: number; diff --git a/src/lib/server/jobs/parsePdfJob.ts b/src/lib/server/jobs/user-pdf-layout-job.ts similarity index 92% rename from src/lib/server/jobs/parsePdfJob.ts rename to src/lib/server/jobs/user-pdf-layout-job.ts index d8f4a24..141d029 100644 --- a/src/lib/server/jobs/parsePdfJob.ts +++ b/src/lib/server/jobs/user-pdf-layout-job.ts @@ -5,22 +5,20 @@ 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/types'; +import type { PdfLayoutJobBase, PdfLayoutProgress } from '@openreader/compute-core/api-contracts'; -interface ParsePdfJobInput { - documentId: string; +type UserPdfLayoutJobRequest = PdfLayoutJobBase & { userId: string; - namespace: string | null; forceToken?: string; -} +}; const running = new Set(); -function keyFor(input: ParsePdfJobInput): string { +function keyFor(input: UserPdfLayoutJobRequest): string { return `${input.userId}:${input.documentId}:${input.namespace || ''}`; } -export async function parsePdfJob(input: ParsePdfJobInput): Promise { +export async function parsePdfJob(input: UserPdfLayoutJobRequest): Promise { const key = keyFor(input); if (running.has(key)) return; running.add(key); @@ -137,7 +135,7 @@ export async function parsePdfJob(input: ParsePdfJobInput): Promise { } } -export function enqueueParsePdfJob(input: ParsePdfJobInput): void { +export function enqueueParsePdfJob(input: UserPdfLayoutJobRequest): void { Promise.resolve() .then(() => parsePdfJob(input)) .catch((error) => { diff --git a/src/lib/server/jobs/user-whisper-align-job.ts b/src/lib/server/jobs/user-whisper-align-job.ts new file mode 100644 index 0000000..afe0127 --- /dev/null +++ b/src/lib/server/jobs/user-whisper-align-job.ts @@ -0,0 +1,25 @@ +import { getCompute } from '@/lib/server/compute'; +import type { WhisperAlignJobRequest } from '@openreader/compute-core/api-contracts'; +import type { TTSSentenceAlignment } from '@/types/tts'; + +export type UserWhisperAlignJobRequest = WhisperAlignJobRequest & { + sentenceIndex?: number; +}; + +export async function userWhisperAlignJob(input: UserWhisperAlignJobRequest): Promise { + const compute = await getCompute(); + const { alignments } = await compute.alignWords({ + audioObjectKey: input.audioObjectKey, + text: input.text, + cacheKey: input.cacheKey, + lang: input.lang, + }); + + const first = alignments[0]; + if (!first) return null; + + if (typeof input.sentenceIndex === 'number' && Number.isFinite(input.sentenceIndex)) { + return { ...first, sentenceIndex: input.sentenceIndex }; + } + return first; +}