From 1bd13c02fe2a3e1cbf2fdb308af648b0738ab6b8 Mon Sep 17 00:00:00 2001 From: Richard R Date: Wed, 20 May 2026 15:07:22 -0600 Subject: [PATCH] refactor(compute): modularize core contracts and runtime, enable flexible backend resolution Restructure compute core by extracting job contracts and runtime logic into separate modules (`contracts.ts`, `local-runtime.ts`) for improved modularity and clearer API boundaries. Update exports and imports across worker, server, and local compute backends to use the new modules. Enhance Next.js config to support dynamic backend selection via build-time constants and aliasing, enabling seamless switching between local and worker compute modes. Adjust TypeScript paths and Dockerfile entrypoint to align with the new structure. --- compute/core/package.json | 4 +- compute/core/src/contracts.ts | 62 +++++++++++++ compute/core/src/index.ts | 102 ++-------------------- compute/core/src/local-runtime.ts | 34 ++++++++ compute/worker/Dockerfile | 2 +- compute/worker/package.json | 4 +- compute/worker/src/server.ts | 4 +- next.config.ts | 33 ++++++- pnpm-lock.yaml | 7 +- src/lib/server/compute/index.ts | 7 ++ src/lib/server/compute/index.worker.ts | 20 +++++ src/lib/server/compute/local.ts | 2 +- src/lib/server/compute/types.ts | 2 +- src/lib/server/compute/worker-contract.ts | 2 +- tsconfig.json | 4 +- 15 files changed, 178 insertions(+), 111 deletions(-) create mode 100644 compute/core/src/contracts.ts create mode 100644 compute/core/src/local-runtime.ts create mode 100644 src/lib/server/compute/index.worker.ts diff --git a/compute/core/package.json b/compute/core/package.json index fe9bb45..9141f07 100644 --- a/compute/core/package.json +++ b/compute/core/package.json @@ -12,6 +12,8 @@ "pdfjs-dist": "4.8.69" }, "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./contracts": "./src/contracts.ts", + "./local-runtime": "./src/local-runtime.ts" } } diff --git a/compute/core/src/contracts.ts b/compute/core/src/contracts.ts new file mode 100644 index 0000000..04e1fb4 --- /dev/null +++ b/compute/core/src/contracts.ts @@ -0,0 +1,62 @@ +import type { TTSSentenceAlignment } from './types/tts'; +import type { ParsedPdfDocument } from './types/parsed-pdf'; + +export type { + TTSAudioBuffer, + TTSAudioBytes, + TTSSentenceAlignment, + TTSSentenceWord, +} from './types/tts'; +export type { + ParsedPdfBlockKind, + ParsedPdfBlockFragment, + ParsedPdfBlock, + ParsedPdfPage, + ParsedPdfDocument, +} from './types/parsed-pdf'; + +export const ALIGN_QUEUE_NAME = 'whisper-align'; +export const PDF_LAYOUT_QUEUE_NAME = 'pdf-layout'; + +export interface WhisperAlignJobRequest { + text: string; + lang?: string; + cacheKey?: string; + audioObjectKey: string; +} + +export interface WhisperAlignJobResult { + alignments: TTSSentenceAlignment[]; + timing?: WorkerJobTiming; +} + +export interface PdfLayoutJobRequest { + documentId: string; + namespace: string | null; + documentObjectKey: string; +} + +export interface PdfLayoutJobResult { + parsed: ParsedPdfDocument; + timing?: WorkerJobTiming; +} + +export type WorkerJobState = 'queued' | 'running' | 'succeeded' | 'failed'; + +export interface WorkerJobErrorShape { + message: string; + code?: string; +} + +export interface WorkerJobTiming { + queueWaitMs?: number; + s3FetchMs?: number; + computeMs?: number; +} + +export interface WorkerJobStatusResponse { + status: WorkerJobState; + result?: Result; + error?: WorkerJobErrorShape; + timing?: WorkerJobTiming; +} diff --git a/compute/core/src/index.ts b/compute/core/src/index.ts index 24afe87..54397e2 100644 --- a/compute/core/src/index.ts +++ b/compute/core/src/index.ts @@ -1,96 +1,6 @@ -import type { TTSSentenceAlignment } from './types/tts'; -import type { ParsedPdfDocument } from './types/parsed-pdf'; -import { ensureWhisperModel } from './whisper/ensureModel'; -import { alignAudioWithText } from './whisper/alignment'; -import { ensureModel as ensurePdfLayoutModel } from './pdf-layout/ensureModel'; -import { parsePdf } from './pdf-layout/parsePdf'; - -export type { - TTSAudioBuffer, - TTSAudioBytes, - TTSSentenceAlignment, - TTSSentenceWord, -} from './types/tts'; -export type { - ParsedPdfBlockKind, - ParsedPdfBlockFragment, - ParsedPdfBlock, - ParsedPdfPage, - ParsedPdfDocument, -} from './types/parsed-pdf'; - -export const ALIGN_QUEUE_NAME = 'whisper-align'; -export const PDF_LAYOUT_QUEUE_NAME = 'pdf-layout'; - -export interface WhisperAlignJobRequest { - text: string; - lang?: string; - cacheKey?: string; - audioObjectKey: string; -} - -export interface WhisperAlignJobResult { - alignments: TTSSentenceAlignment[]; - timing?: WorkerJobTiming; -} - -export interface PdfLayoutJobRequest { - documentId: string; - namespace: string | null; - documentObjectKey: string; -} - -export interface PdfLayoutJobResult { - parsed: ParsedPdfDocument; - timing?: WorkerJobTiming; -} - -export type WorkerJobState = 'queued' | 'running' | 'succeeded' | 'failed'; - -export interface WorkerJobErrorShape { - message: string; - code?: string; -} - -export interface WorkerJobTiming { - queueWaitMs?: number; - s3FetchMs?: number; - computeMs?: number; -} - -export interface WorkerJobStatusResponse { - status: WorkerJobState; - result?: Result; - error?: WorkerJobErrorShape; - timing?: WorkerJobTiming; -} - -export async function ensureComputeModels(): Promise { - await Promise.all([ensureWhisperModel(), ensurePdfLayoutModel()]); -} - -export async function runWhisperAlignmentFromAudioBuffer(input: { - audioBuffer: ArrayBuffer; - text: string; - cacheKey?: string; - lang?: string; -}): Promise { - const alignments = await alignAudioWithText( - input.audioBuffer, - input.text, - input.cacheKey, - { lang: input.lang }, - ); - return { alignments }; -} - -export async function runPdfLayoutFromPdfBuffer(input: { - documentId: string; - pdfBytes: ArrayBuffer; -}): Promise { - const parsed = await parsePdf({ - documentId: input.documentId, - pdfBytes: input.pdfBytes, - }); - return { parsed }; -} +export * from './contracts'; +export { + ensureComputeModels, + runPdfLayoutFromPdfBuffer, + runWhisperAlignmentFromAudioBuffer, +} from './local-runtime'; diff --git a/compute/core/src/local-runtime.ts b/compute/core/src/local-runtime.ts new file mode 100644 index 0000000..5a794b8 --- /dev/null +++ b/compute/core/src/local-runtime.ts @@ -0,0 +1,34 @@ +import { ensureWhisperModel } from './whisper/ensureModel'; +import { alignAudioWithText } from './whisper/alignment'; +import { ensureModel as ensurePdfLayoutModel } from './pdf-layout/ensureModel'; +import { parsePdf } from './pdf-layout/parsePdf'; + +export async function ensureComputeModels(): Promise { + await Promise.all([ensureWhisperModel(), ensurePdfLayoutModel()]); +} + +export async function runWhisperAlignmentFromAudioBuffer(input: { + audioBuffer: ArrayBuffer; + text: string; + cacheKey?: string; + lang?: string; +}) { + const alignments = await alignAudioWithText( + input.audioBuffer, + input.text, + input.cacheKey, + { lang: input.lang }, + ); + return { alignments }; +} + +export async function runPdfLayoutFromPdfBuffer(input: { + documentId: string; + pdfBytes: ArrayBuffer; +}) { + const parsed = await parsePdf({ + documentId: input.documentId, + pdfBytes: input.pdfBytes, + }); + return { parsed }; +} diff --git a/compute/worker/Dockerfile b/compute/worker/Dockerfile index 2e6d613..9872e70 100644 --- a/compute/worker/Dockerfile +++ b/compute/worker/Dockerfile @@ -26,4 +26,4 @@ ENV COMPUTE_WORKER_PORT=8081 EXPOSE 8081 -CMD ["pnpm", "dev"] +CMD ["node", "--import", "tsx", "src/server.ts"] diff --git a/compute/worker/package.json b/compute/worker/package.json index bd85999..4b3b1c7 100644 --- a/compute/worker/package.json +++ b/compute/worker/package.json @@ -16,9 +16,7 @@ "fastify": "^5.6.2", "pino": "^10.3.1", "pino-pretty": "^13.1.2", + "tsx": "^4.22.3", "zod": "^4.1.12" - }, - "devDependencies": { - "tsx": "^4.22.3" } } diff --git a/compute/worker/src/server.ts b/compute/worker/src/server.ts index 66551aa..4e62162 100644 --- a/compute/worker/src/server.ts +++ b/compute/worker/src/server.ts @@ -24,13 +24,15 @@ import { ensureComputeModels, runPdfLayoutFromPdfBuffer, runWhisperAlignmentFromAudioBuffer, +} from '@openreader/compute-core/local-runtime'; +import { type PdfLayoutJobRequest, type PdfLayoutJobResult, type WhisperAlignJobRequest, type WhisperAlignJobResult, type WorkerJobStatusResponse, type WorkerJobTiming, -} from '@openreader/compute-core'; +} from '@openreader/compute-core/contracts'; import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; const JOBS_STREAM_NAME = 'compute_jobs'; diff --git a/next.config.ts b/next.config.ts index 3340a9e..73e514e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,5 @@ import type { NextConfig } from "next"; +import path from "node:path"; const securityHeaders = [ { key: 'X-Content-Type-Options', value: 'nosniff' }, @@ -43,7 +44,7 @@ const nextConfig: NextConfig = { canvas: '@napi-rs/canvas', }, }, - transpilePackages: ['@openreader/compute-core'], + transpilePackages: computeLocal ? ['@openreader/compute-core'] : [], serverExternalPackages, outputFileTracingIncludes: { '/api/audiobook': [ @@ -75,6 +76,36 @@ const nextConfig: NextConfig = { }, } : {}), + webpack: (config, { isServer }) => { + if (isServer) { + // Use runtime require to avoid adding an explicit webpack TS dependency. + const { DefinePlugin } = require('webpack') as { DefinePlugin: new (defs: Record) => unknown }; + config.plugins = config.plugins || []; + config.plugins.push( + new DefinePlugin({ + __OPENREADER_COMPUTE_MODE__: JSON.stringify(computeMode), + }), + ); + } + if (isServer && !computeLocal) { + const workerComputeEntry = path.resolve(__dirname, 'src/lib/server/compute/index.worker.ts'); + const computeIndexTs = path.resolve(__dirname, 'src/lib/server/compute/index.ts'); + const computeIndexNoExt = path.resolve(__dirname, 'src/lib/server/compute/index'); + const computeDir = path.resolve(__dirname, 'src/lib/server/compute'); + config.resolve.alias = { + ...(config.resolve.alias || {}), + '@/lib/server/compute$': workerComputeEntry, + '@/lib/server/compute/index$': workerComputeEntry, + [`${computeIndexTs}$`]: workerComputeEntry, + [`${computeIndexNoExt}$`]: workerComputeEntry, + [`${computeDir}$`]: workerComputeEntry, + '@openreader/compute-core/local-runtime$': false, + 'onnxruntime-node$': false, + '@huggingface/tokenizers$': false, + }; + } + return config; + }, }; export default nextConfig; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7303f48..fdbeb0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -233,13 +233,12 @@ importers: pino-pretty: specifier: ^13.1.2 version: 13.1.3 - zod: - specifier: ^4.1.12 - version: 4.4.3 - devDependencies: tsx: specifier: ^4.22.3 version: 4.22.3 + zod: + specifier: ^4.1.12 + version: 4.4.3 packages: diff --git a/src/lib/server/compute/index.ts b/src/lib/server/compute/index.ts index 5442429..c83d790 100644 --- a/src/lib/server/compute/index.ts +++ b/src/lib/server/compute/index.ts @@ -2,9 +2,16 @@ import type { ComputeBackend, ComputeMode } from '@/lib/server/compute/types'; import { isComputeModeAvailable, readComputeMode } from '@/lib/server/compute/mode'; import { WorkerComputeBackend } from '@/lib/server/compute/worker'; +declare const __OPENREADER_COMPUTE_MODE__: 'local' | 'worker' | 'none'; + let backendPromise: Promise | null = null; async function createBackend(): Promise { + if (__OPENREADER_COMPUTE_MODE__ === 'worker') return new WorkerComputeBackend(); + if (__OPENREADER_COMPUTE_MODE__ === 'local') { + const { LocalComputeBackend } = await import('@/lib/server/compute/local'); + return new LocalComputeBackend(); + } const mode: ComputeMode = readComputeMode(); if (mode === 'worker') return new WorkerComputeBackend(); const { LocalComputeBackend } = await import('@/lib/server/compute/local'); diff --git a/src/lib/server/compute/index.worker.ts b/src/lib/server/compute/index.worker.ts new file mode 100644 index 0000000..fbbaf7b --- /dev/null +++ b/src/lib/server/compute/index.worker.ts @@ -0,0 +1,20 @@ +import type { ComputeBackend } from '@/lib/server/compute/types'; +import { WorkerComputeBackend } from '@/lib/server/compute/worker'; + +let backendPromise: Promise | null = null; + +export async function getCompute(): Promise { + if (!backendPromise) { + backendPromise = Promise.resolve() + .then(() => new WorkerComputeBackend()) + .catch((error) => { + backendPromise = null; + throw error; + }); + } + return backendPromise; +} + +export function isComputeAvailable(): boolean { + return true; +} diff --git a/src/lib/server/compute/local.ts b/src/lib/server/compute/local.ts index c1f1475..41cea36 100644 --- a/src/lib/server/compute/local.ts +++ b/src/lib/server/compute/local.ts @@ -4,7 +4,7 @@ import { getTtsSegmentAudioObject } from '@/lib/server/tts/segments-blobstore'; import { runPdfLayoutFromPdfBuffer, runWhisperAlignmentFromAudioBuffer, -} from '@openreader/compute-core'; +} from '@openreader/compute-core/local-runtime'; export class LocalComputeBackend implements ComputeBackend { readonly mode = 'local' as const; diff --git a/src/lib/server/compute/types.ts b/src/lib/server/compute/types.ts index f316756..4a5ce41 100644 --- a/src/lib/server/compute/types.ts +++ b/src/lib/server/compute/types.ts @@ -1,4 +1,4 @@ -import type { TTSAudioBuffer, TTSSentenceAlignment, ParsedPdfDocument } from '@openreader/compute-core'; +import type { TTSAudioBuffer, TTSSentenceAlignment, ParsedPdfDocument } from '@openreader/compute-core/contracts'; export type ComputeMode = 'local' | 'worker'; diff --git a/src/lib/server/compute/worker-contract.ts b/src/lib/server/compute/worker-contract.ts index b43cae9..a93c984 100644 --- a/src/lib/server/compute/worker-contract.ts +++ b/src/lib/server/compute/worker-contract.ts @@ -9,4 +9,4 @@ export { type WorkerJobState, type WorkerJobTiming, type WorkerJobStatusResponse, -} from '@openreader/compute-core'; +} from '@openreader/compute-core/contracts'; diff --git a/tsconfig.json b/tsconfig.json index 19d4e79..a06fb0d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,9 @@ ], "paths": { "@/*": ["./src/*"], - "@openreader/compute-core": ["./compute/core/src/index.ts"] + "@openreader/compute-core": ["./compute/core/src/index.ts"], + "@openreader/compute-core/contracts": ["./compute/core/src/contracts.ts"], + "@openreader/compute-core/local-runtime": ["./compute/core/src/local-runtime.ts"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],