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"],