refactor(compute): unify job concurrency and CPU thread allocation logic
Standardize compute job concurrency configuration by introducing a shared COMPUTE_JOB_CONCURRENCY setting, replacing separate PDF and Whisper concurrency controls. Add cross-platform CPU core detection and thread budgeting utilities, and update both ONNX model execution and concurrency limiters to use dynamic thread allocation per job. Refactor environment variable docs and examples to reflect unified concurrency management. Streamline PDF.js font path resolution for improved reliability across environments.
This commit is contained in:
parent
671d5c6adb
commit
f8672b073e
15 changed files with 214 additions and 46 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import path from 'path';
|
||||
import { createRequire } from 'node:module';
|
||||
import type { TextItem } from 'pdfjs-dist/types/src/display/api';
|
||||
import type { ParsedPdfDocument, ParsedPdfPage } from '../types/parsed-pdf';
|
||||
import type { PdfTextItem } from './types';
|
||||
|
|
@ -19,6 +20,13 @@ interface ParsePdfInput {
|
|||
}
|
||||
|
||||
const LAYOUT_RENDER_SCALE = 1.5;
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
function resolvePdfjsStandardFontDataUrl(): string {
|
||||
const pdfjsPackageJson = require.resolve('pdfjs-dist/package.json');
|
||||
const standardFontDir = path.join(path.dirname(pdfjsPackageJson), 'standard_fonts');
|
||||
return `${standardFontDir.replace(/\/?$/, '/')}`;
|
||||
}
|
||||
|
||||
export function normalizeTextItemsForLayout(items: TextItem[], pageHeight: number): PdfTextItem[] {
|
||||
return items
|
||||
|
|
@ -68,8 +76,7 @@ export async function parsePdf(input: ParsePdfInput): Promise<ParsedPdfDocument>
|
|||
pdfjs.GlobalWorkerOptions.workerSrc = 'pdfjs-dist/legacy/build/pdf.worker.mjs';
|
||||
pdfjs.GlobalWorkerOptions.workerPort = null;
|
||||
}
|
||||
const standardFontDir = path.join(process.cwd(), 'node_modules', 'pdfjs-dist', 'standard_fonts');
|
||||
const standardFontDataUrl = `${standardFontDir.replace(/\/?$/, '/')}`;
|
||||
const standardFontDataUrl = resolvePdfjsStandardFontDataUrl();
|
||||
|
||||
const loadingTask = pdfjs.getDocument({
|
||||
data: pdfBytesForText,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import path from 'path';
|
||||
import { createRequire } from 'node:module';
|
||||
import type { Canvas } from '@napi-rs/canvas';
|
||||
|
||||
type CanvasRuntime = {
|
||||
|
|
@ -8,6 +9,13 @@ type CanvasRuntime = {
|
|||
};
|
||||
|
||||
let canvasRuntimePromise: Promise<CanvasRuntime> | null = null;
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
function resolvePdfjsStandardFontDataUrl(): string {
|
||||
const pdfjsPackageJson = require.resolve('pdfjs-dist/package.json');
|
||||
const standardFontDir = path.join(path.dirname(pdfjsPackageJson), 'standard_fonts');
|
||||
return `${standardFontDir.replace(/\/?$/, '/')}`;
|
||||
}
|
||||
|
||||
async function loadCanvasRuntime(): Promise<CanvasRuntime> {
|
||||
if (!canvasRuntimePromise) {
|
||||
|
|
@ -112,8 +120,7 @@ export async function renderPage({
|
|||
pdfjs.GlobalWorkerOptions.workerPort = null;
|
||||
}
|
||||
|
||||
const standardFontDir = path.join(process.cwd(), 'node_modules', 'pdfjs-dist', 'standard_fonts');
|
||||
const standardFontDataUrl = `${standardFontDir.replace(/\/?$/, '/')}`;
|
||||
const standardFontDataUrl = resolvePdfjsStandardFontDataUrl();
|
||||
|
||||
const loadingTask = pdfjs.getDocument({
|
||||
data: isolatedBytes,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as ort from 'onnxruntime-node';
|
|||
import { readFile } from 'fs/promises';
|
||||
import type { LayoutRegion, PdfTextItem } from './types';
|
||||
import { ensureModel, MODEL_CONFIG_PATH, MODEL_PREPROCESSOR_PATH } from './ensureModel';
|
||||
import { getOnnxThreadsPerJob } from '../runtime/cpu-budget';
|
||||
|
||||
interface RunLayoutInput {
|
||||
pageWidth: number;
|
||||
|
|
@ -115,9 +116,16 @@ async function getSession(): Promise<ort.InferenceSession> {
|
|||
if (!sessionPromise) {
|
||||
sessionPromise = (async () => {
|
||||
const modelPath = await ensureModel();
|
||||
return ort.InferenceSession.create(modelPath, {
|
||||
const onnxThreadsPerJob = getOnnxThreadsPerJob();
|
||||
const stableSessionOptions: ort.InferenceSession.SessionOptions = {
|
||||
executionProviders: ['cpu'],
|
||||
graphOptimizationLevel: 'all',
|
||||
intraOpNumThreads: onnxThreadsPerJob,
|
||||
interOpNumThreads: 1,
|
||||
executionMode: 'sequential',
|
||||
};
|
||||
return ort.InferenceSession.create(modelPath, {
|
||||
...stableSessionOptions,
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
|
|
|||
28
compute/core/src/runtime/cpu-budget.ts
Normal file
28
compute/core/src/runtime/cpu-budget.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import os from 'node:os';
|
||||
|
||||
function readPositiveInt(name: string, fallback: number): number {
|
||||
const raw = process.env[name]?.trim();
|
||||
if (!raw) return fallback;
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
|
||||
export function getComputeJobConcurrency(): number {
|
||||
return readPositiveInt('COMPUTE_JOB_CONCURRENCY', 1);
|
||||
}
|
||||
|
||||
export function getAvailableCpuCores(): number {
|
||||
if (typeof os.availableParallelism === 'function') {
|
||||
const value = os.availableParallelism();
|
||||
if (Number.isFinite(value) && value >= 1) return Math.floor(value);
|
||||
}
|
||||
const fallback = os.cpus().length;
|
||||
return Number.isFinite(fallback) && fallback >= 1 ? Math.floor(fallback) : 1;
|
||||
}
|
||||
|
||||
export function getOnnxThreadsPerJob(): number {
|
||||
const concurrency = getComputeJobConcurrency();
|
||||
const usableCores = Math.max(1, getAvailableCpuCores() - 1);
|
||||
return Math.max(1, Math.floor(usableCores / concurrency));
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import { Tokenizer } from '@huggingface/tokenizers';
|
|||
import JSZip from 'jszip';
|
||||
import type { TTSAudioBuffer, TTSAudioBytes, TTSSentenceAlignment } from '../types/tts';
|
||||
import { getFFmpegPath } from '../runtime/ffmpeg';
|
||||
import { getOnnxThreadsPerJob } from '../runtime/cpu-budget';
|
||||
import {
|
||||
mapWordsToSentenceOffsets,
|
||||
type WhisperWord,
|
||||
|
|
@ -575,10 +576,11 @@ async function getRuntime(): Promise<WhisperRuntime> {
|
|||
const defaultLanguageToken = Number(defaultLanguageFromForced ?? tokenizer.token_to_id('<|en|>') ?? 50259);
|
||||
const transcribeToken = Number(transcribeFromForced ?? tokenizer.token_to_id('<|transcribe|>') ?? 50359);
|
||||
|
||||
const onnxThreadsPerJob = getOnnxThreadsPerJob();
|
||||
const stableSessionOptions: ort.InferenceSession.SessionOptions = {
|
||||
executionProviders: ['cpu'],
|
||||
graphOptimizationLevel: 'disabled',
|
||||
intraOpNumThreads: 1,
|
||||
intraOpNumThreads: onnxThreadsPerJob,
|
||||
interOpNumThreads: 1,
|
||||
executionMode: 'sequential',
|
||||
enableCpuMemArena: false,
|
||||
|
|
|
|||
|
|
@ -30,10 +30,9 @@ S3_FORCE_PATH_STYLE=true
|
|||
|
||||
# Queue + execution tuning
|
||||
# COMPUTE_PREWARM_MODELS=true
|
||||
# COMPUTE_WHISPER_CONCURRENCY=1
|
||||
# COMPUTE_PDF_CONCURRENCY=2
|
||||
# COMPUTE_JOB_CONCURRENCY=1
|
||||
# COMPUTE_WHISPER_TIMEOUT_MS=30000
|
||||
# COMPUTE_PDF_TIMEOUT_MS=90000
|
||||
# COMPUTE_PDF_TIMEOUT_MS=300000
|
||||
# COMPUTE_PDF_JOB_ATTEMPTS=2
|
||||
# COMPUTE_JOBS_STREAM_MAX_BYTES=268435456
|
||||
# COMPUTE_JOB_STATES_MAX_BYTES=67108864
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createHash } from 'node:crypto';
|
||||
import os from 'node:os';
|
||||
import Fastify, { type FastifyRequest } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
|
|
@ -112,6 +113,20 @@ function parseBoolEnv(name: string, fallback: boolean): boolean {
|
|||
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
||||
}
|
||||
|
||||
function getAvailableCpuCores(): number {
|
||||
if (typeof os.availableParallelism === 'function') {
|
||||
const value = os.availableParallelism();
|
||||
if (Number.isFinite(value) && value >= 1) return Math.floor(value);
|
||||
}
|
||||
const fallback = os.cpus().length;
|
||||
return Number.isFinite(fallback) && fallback >= 1 ? Math.floor(fallback) : 1;
|
||||
}
|
||||
|
||||
function getOnnxThreadsPerJob(jobConcurrency: number): number {
|
||||
const usableCores = Math.max(1, getAvailableCpuCores() - 1);
|
||||
return Math.max(1, Math.floor(usableCores / Math.max(1, jobConcurrency)));
|
||||
}
|
||||
|
||||
function buildLoggerConfig(): boolean | Record<string, unknown> {
|
||||
const format = (process.env.COMPUTE_LOG_FORMAT?.trim().toLowerCase() || 'pretty');
|
||||
if (format === 'json') return true;
|
||||
|
|
@ -311,6 +326,36 @@ function sleep(ms: number): Promise<void> {
|
|||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
class ConcurrencyGate {
|
||||
private readonly maxInFlight: number;
|
||||
private inFlight = 0;
|
||||
private readonly queue: Array<() => void> = [];
|
||||
|
||||
constructor(limit: number) {
|
||||
this.maxInFlight = Number.isFinite(limit) && limit >= 1 ? Math.floor(limit) : 1;
|
||||
}
|
||||
|
||||
async acquire(): Promise<void> {
|
||||
if (this.inFlight < this.maxInFlight) {
|
||||
this.inFlight += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
this.queue.push(() => {
|
||||
this.inFlight += 1;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
release(): void {
|
||||
this.inFlight = Math.max(0, this.inFlight - 1);
|
||||
const next = this.queue.shift();
|
||||
if (next) next();
|
||||
}
|
||||
}
|
||||
|
||||
function isTerminalStatus(status: WorkerJobState): boolean {
|
||||
return status === 'succeeded' || status === 'failed';
|
||||
}
|
||||
|
|
@ -428,10 +473,9 @@ async function main(): Promise<void> {
|
|||
const workerToken = requireEnv('COMPUTE_WORKER_TOKEN');
|
||||
const natsUrl = requireEnv('NATS_URL');
|
||||
|
||||
const whisperConcurrency = readIntEnv('COMPUTE_WHISPER_CONCURRENCY', 1);
|
||||
const pdfConcurrency = readIntEnv('COMPUTE_PDF_CONCURRENCY', 2);
|
||||
const jobConcurrency = readIntEnv('COMPUTE_JOB_CONCURRENCY', 1);
|
||||
const whisperTimeoutMs = readIntEnv('COMPUTE_WHISPER_TIMEOUT_MS', 30_000);
|
||||
const pdfTimeoutMs = readIntEnv('COMPUTE_PDF_TIMEOUT_MS', 90_000);
|
||||
const pdfTimeoutMs = readIntEnv('COMPUTE_PDF_TIMEOUT_MS', 5 * 60_000);
|
||||
const pdfAttempts = readIntEnv('COMPUTE_PDF_JOB_ATTEMPTS', 2);
|
||||
const prewarmModels = parseBoolEnv('COMPUTE_PREWARM_MODELS', true);
|
||||
const jobsStreamMaxBytes = readIntEnv('COMPUTE_JOBS_STREAM_MAX_BYTES', 256 * 1024 * 1024);
|
||||
|
|
@ -507,6 +551,17 @@ async function main(): Promise<void> {
|
|||
}
|
||||
|
||||
const app = Fastify({ logger: buildLoggerConfig() });
|
||||
app.log.info({
|
||||
jobConcurrency,
|
||||
whisperTimeoutMs,
|
||||
pdfTimeoutMs,
|
||||
pdfAttempts,
|
||||
opStaleMs,
|
||||
availableCpuCores: getAvailableCpuCores(),
|
||||
onnxThreadsPerJob: getOnnxThreadsPerJob(jobConcurrency),
|
||||
natsApiTimeoutMs: NATS_API_TIMEOUT_MS,
|
||||
pdfLayoutHardCapMs: PDF_LAYOUT_HARD_CAP_MS,
|
||||
}, 'compute runtime config');
|
||||
|
||||
const opIndexCodec = createJsonCodec<OpIndexEntry>();
|
||||
const opStateCodec = createJsonCodec<WorkerOperationState<WhisperAlignJobResult | PdfLayoutJobResult>>();
|
||||
|
|
@ -806,6 +861,7 @@ async function main(): Promise<void> {
|
|||
|
||||
const whisperConsumer = await js.consumers.get(JOBS_STREAM_NAME, WHISPER_CONSUMER_NAME);
|
||||
const layoutConsumer = await js.consumers.get(JOBS_STREAM_NAME, LAYOUT_CONSUMER_NAME);
|
||||
const jobGate = new ConcurrencyGate(jobConcurrency);
|
||||
|
||||
let stopping = false;
|
||||
|
||||
|
|
@ -1125,29 +1181,39 @@ async function main(): Promise<void> {
|
|||
workerLabel: string;
|
||||
}): Promise<void> {
|
||||
while (!stopping) {
|
||||
let msg: JsMsg | null = null;
|
||||
try {
|
||||
msg = await input.consumer.next({ expires: PULL_EXPIRES_MS });
|
||||
} catch (error) {
|
||||
if (stopping) return;
|
||||
app.log.error({ error: toErrorMessage(error), worker: input.workerLabel }, 'worker pull failed');
|
||||
await sleep(LOOP_ERROR_BACKOFF_MS);
|
||||
continue;
|
||||
await jobGate.acquire();
|
||||
if (stopping) {
|
||||
jobGate.release();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!msg) continue;
|
||||
await processMessage({
|
||||
msg,
|
||||
codec: input.codec,
|
||||
run: input.run,
|
||||
workerLabel: input.workerLabel,
|
||||
});
|
||||
let msg: JsMsg | null = null;
|
||||
try {
|
||||
try {
|
||||
msg = await input.consumer.next({ expires: PULL_EXPIRES_MS });
|
||||
} catch (error) {
|
||||
if (stopping) return;
|
||||
app.log.error({ error: toErrorMessage(error), worker: input.workerLabel }, 'worker pull failed');
|
||||
await sleep(LOOP_ERROR_BACKOFF_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!msg) continue;
|
||||
await processMessage({
|
||||
msg,
|
||||
codec: input.codec,
|
||||
run: input.run,
|
||||
workerLabel: input.workerLabel,
|
||||
});
|
||||
} finally {
|
||||
jobGate.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workerLoops: Promise<void>[] = [];
|
||||
|
||||
for (let i = 0; i < whisperConcurrency; i += 1) {
|
||||
for (let i = 0; i < jobConcurrency; i += 1) {
|
||||
workerLoops.push(createWorkerLoop({
|
||||
consumer: whisperConsumer,
|
||||
codec: whisperJobCodec,
|
||||
|
|
@ -1156,7 +1222,7 @@ async function main(): Promise<void> {
|
|||
}));
|
||||
}
|
||||
|
||||
for (let i = 0; i < pdfConcurrency; i += 1) {
|
||||
for (let i = 0; i < jobConcurrency; i += 1) {
|
||||
workerLoops.push(createWorkerLoop({
|
||||
consumer: layoutConsumer,
|
||||
codec: layoutJobCodec,
|
||||
|
|
|
|||
|
|
@ -48,10 +48,9 @@ Common optional:
|
|||
Advanced tuning (usually leave unset unless you need overrides):
|
||||
|
||||
- `COMPUTE_PREWARM_MODELS=true`
|
||||
- `COMPUTE_WHISPER_CONCURRENCY=1`
|
||||
- `COMPUTE_PDF_CONCURRENCY=2`
|
||||
- `COMPUTE_JOB_CONCURRENCY=1` (shared total compute jobs across whisper + PDF)
|
||||
- `COMPUTE_WHISPER_TIMEOUT_MS=30000`
|
||||
- `COMPUTE_PDF_TIMEOUT_MS=90000`
|
||||
- `COMPUTE_PDF_TIMEOUT_MS=300000`
|
||||
- `COMPUTE_PDF_JOB_ATTEMPTS=2` (PDF layout retry attempts)
|
||||
- `COMPUTE_JOBS_STREAM_MAX_BYTES=268435456` (256MB JetStream jobs stream cap)
|
||||
- `COMPUTE_JOB_STATES_MAX_BYTES=67108864` (64MB JetStream KV bucket cap)
|
||||
|
|
@ -121,10 +120,9 @@ COMPUTE_WORKER_HOST=0.0.0.0
|
|||
COMPUTE_WORKER_TOKEN=<long-random-shared-token>
|
||||
# Optional advanced tuning overrides (defaults shown):
|
||||
# COMPUTE_PREWARM_MODELS=true
|
||||
# COMPUTE_WHISPER_CONCURRENCY=1
|
||||
# COMPUTE_PDF_CONCURRENCY=2
|
||||
# COMPUTE_JOB_CONCURRENCY=1
|
||||
# COMPUTE_WHISPER_TIMEOUT_MS=30000
|
||||
# COMPUTE_PDF_TIMEOUT_MS=90000
|
||||
# COMPUTE_PDF_TIMEOUT_MS=300000
|
||||
# COMPUTE_PDF_JOB_ATTEMPTS=2
|
||||
# COMPUTE_JOBS_STREAM_MAX_BYTES=268435456
|
||||
# COMPUTE_JOB_STATES_MAX_BYTES=67108864
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { getComputeJobConcurrency } from '@/lib/server/compute/cpu-budget';
|
||||
|
||||
export class ConcurrencyLimiter {
|
||||
private readonly maxInFlight: number;
|
||||
private inFlight = 0;
|
||||
|
|
@ -36,5 +38,4 @@ export class ConcurrencyLimiter {
|
|||
}
|
||||
}
|
||||
|
||||
export const LOCAL_PDF_LIMITER = new ConcurrencyLimiter(2);
|
||||
export const LOCAL_WHISPER_LIMITER = new ConcurrencyLimiter(1);
|
||||
export const LOCAL_COMPUTE_LIMITER = new ConcurrencyLimiter(getComputeJobConcurrency());
|
||||
|
|
|
|||
28
src/lib/server/compute/cpu-budget.ts
Normal file
28
src/lib/server/compute/cpu-budget.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import os from 'node:os';
|
||||
|
||||
function readPositiveInt(name: string, fallback: number): number {
|
||||
const raw = process.env[name]?.trim();
|
||||
if (!raw) return fallback;
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
|
||||
export function getComputeJobConcurrency(): number {
|
||||
return readPositiveInt('COMPUTE_JOB_CONCURRENCY', 1);
|
||||
}
|
||||
|
||||
export function getAvailableCpuCores(): number {
|
||||
if (typeof os.availableParallelism === 'function') {
|
||||
const value = os.availableParallelism();
|
||||
if (Number.isFinite(value) && value >= 1) return Math.floor(value);
|
||||
}
|
||||
const fallback = os.cpus().length;
|
||||
return Number.isFinite(fallback) && fallback >= 1 ? Math.floor(fallback) : 1;
|
||||
}
|
||||
|
||||
export function getOnnxThreadsPerJob(): number {
|
||||
const concurrency = getComputeJobConcurrency();
|
||||
const usableCores = Math.max(1, getAvailableCpuCores() - 1);
|
||||
return Math.max(1, Math.floor(usableCores / concurrency));
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ComputeBackend, PdfLayoutInput, WhisperAlignInput, WhisperAlignResult } from '@/lib/server/compute/types';
|
||||
import { LOCAL_PDF_LIMITER, LOCAL_WHISPER_LIMITER } from '@/lib/server/compute/concurrency-limiter';
|
||||
import { LOCAL_COMPUTE_LIMITER } from '@/lib/server/compute/concurrency-limiter';
|
||||
import { getDocumentBlob } from '@/lib/server/documents/blobstore';
|
||||
import { getTtsSegmentAudioObject } from '@/lib/server/tts/segments-blobstore';
|
||||
import {
|
||||
|
|
@ -11,7 +11,7 @@ export class LocalComputeBackend implements ComputeBackend {
|
|||
readonly mode = 'local' as const;
|
||||
|
||||
async alignWords(input: WhisperAlignInput): Promise<WhisperAlignResult> {
|
||||
return LOCAL_WHISPER_LIMITER.run(async () => {
|
||||
return LOCAL_COMPUTE_LIMITER.run(async () => {
|
||||
let audioBuffer = input.audioBuffer ?? null;
|
||||
if (!audioBuffer && input.audioObjectKey) {
|
||||
const bytes = new Uint8Array(await getTtsSegmentAudioObject(input.audioObjectKey));
|
||||
|
|
@ -30,7 +30,7 @@ export class LocalComputeBackend implements ComputeBackend {
|
|||
}
|
||||
|
||||
async parsePdfLayout(input: PdfLayoutInput) {
|
||||
return LOCAL_PDF_LIMITER.run(async () => {
|
||||
return LOCAL_COMPUTE_LIMITER.run(async () => {
|
||||
let pdfBytes = input.pdfBytes ?? null;
|
||||
if (!pdfBytes && input.documentId && typeof input.namespace !== 'undefined') {
|
||||
const bytes = new Uint8Array(await getDocumentBlob(input.documentId, input.namespace));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import path from 'path';
|
||||
import { createRequire } from 'node:module';
|
||||
import type { TextItem } from 'pdfjs-dist/types/src/display/api';
|
||||
import type { ParsedPdfDocument, ParsedPdfPage } from '@/types/parsed-pdf';
|
||||
import type { PdfTextItem } from '@/lib/server/pdf-layout/types';
|
||||
|
|
@ -14,6 +15,13 @@ interface ParsePdfInput {
|
|||
}
|
||||
|
||||
const LAYOUT_RENDER_SCALE = 1.5;
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
function resolvePdfjsStandardFontDataUrl(): string {
|
||||
const pdfjsPackageJson = require.resolve('pdfjs-dist/package.json');
|
||||
const standardFontDir = path.join(path.dirname(pdfjsPackageJson), 'standard_fonts');
|
||||
return `${standardFontDir.replace(/\/?$/, '/')}`;
|
||||
}
|
||||
|
||||
export function normalizeTextItemsForLayout(items: TextItem[], pageHeight: number): PdfTextItem[] {
|
||||
return items
|
||||
|
|
@ -63,8 +71,7 @@ export async function parsePdf(input: ParsePdfInput): Promise<ParsedPdfDocument>
|
|||
pdfjs.GlobalWorkerOptions.workerSrc = 'pdfjs-dist/legacy/build/pdf.worker.mjs';
|
||||
pdfjs.GlobalWorkerOptions.workerPort = null;
|
||||
}
|
||||
const standardFontDir = path.join(process.cwd(), 'node_modules', 'pdfjs-dist', 'standard_fonts');
|
||||
const standardFontDataUrl = `${standardFontDir.replace(/\/?$/, '/')}`;
|
||||
const standardFontDataUrl = resolvePdfjsStandardFontDataUrl();
|
||||
|
||||
const loadingTask = pdfjs.getDocument({
|
||||
data: pdfBytesForText,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import path from 'path';
|
||||
import { createRequire } from 'node:module';
|
||||
import type { Canvas } from '@napi-rs/canvas';
|
||||
|
||||
type CanvasRuntime = {
|
||||
|
|
@ -8,6 +9,13 @@ type CanvasRuntime = {
|
|||
};
|
||||
|
||||
let canvasRuntimePromise: Promise<CanvasRuntime> | null = null;
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
function resolvePdfjsStandardFontDataUrl(): string {
|
||||
const pdfjsPackageJson = require.resolve('pdfjs-dist/package.json');
|
||||
const standardFontDir = path.join(path.dirname(pdfjsPackageJson), 'standard_fonts');
|
||||
return `${standardFontDir.replace(/\/?$/, '/')}`;
|
||||
}
|
||||
|
||||
async function loadCanvasRuntime(): Promise<CanvasRuntime> {
|
||||
if (!canvasRuntimePromise) {
|
||||
|
|
@ -112,8 +120,7 @@ export async function renderPage({
|
|||
pdfjs.GlobalWorkerOptions.workerPort = null;
|
||||
}
|
||||
|
||||
const standardFontDir = path.join(process.cwd(), 'node_modules', 'pdfjs-dist', 'standard_fonts');
|
||||
const standardFontDataUrl = `${standardFontDir.replace(/\/?$/, '/')}`;
|
||||
const standardFontDataUrl = resolvePdfjsStandardFontDataUrl();
|
||||
|
||||
const loadingTask = pdfjs.getDocument({
|
||||
data: isolatedBytes,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as ort from 'onnxruntime-node';
|
|||
import { readFile } from 'fs/promises';
|
||||
import type { LayoutRegion, PdfTextItem } from '@/lib/server/pdf-layout/types';
|
||||
import { ensureModel, MODEL_CONFIG_PATH, MODEL_PREPROCESSOR_PATH } from '@/lib/server/pdf-layout/ensureModel';
|
||||
import { getOnnxThreadsPerJob } from '@/lib/server/compute/cpu-budget';
|
||||
|
||||
interface RunLayoutInput {
|
||||
pageWidth: number;
|
||||
|
|
@ -115,9 +116,16 @@ async function getSession(): Promise<ort.InferenceSession> {
|
|||
if (!sessionPromise) {
|
||||
sessionPromise = (async () => {
|
||||
const modelPath = await ensureModel();
|
||||
return ort.InferenceSession.create(modelPath, {
|
||||
const onnxThreadsPerJob = getOnnxThreadsPerJob();
|
||||
const stableSessionOptions: ort.InferenceSession.SessionOptions = {
|
||||
executionProviders: ['cpu'],
|
||||
graphOptimizationLevel: 'all',
|
||||
intraOpNumThreads: onnxThreadsPerJob,
|
||||
interOpNumThreads: 1,
|
||||
executionMode: 'sequential',
|
||||
};
|
||||
return ort.InferenceSession.create(modelPath, {
|
||||
...stableSessionOptions,
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Tokenizer } from '@huggingface/tokenizers';
|
|||
import JSZip from 'jszip';
|
||||
import type { TTSAudioBuffer, TTSAudioBytes, TTSSentenceAlignment } from '@/types/tts';
|
||||
import { getFFmpegPath } from '@/lib/server/audiobooks/ffmpeg-bin';
|
||||
import { getOnnxThreadsPerJob } from '@/lib/server/compute/cpu-budget';
|
||||
import {
|
||||
mapWordsToSentenceOffsets,
|
||||
type WhisperWord,
|
||||
|
|
@ -573,10 +574,11 @@ async function getRuntime(): Promise<WhisperRuntime> {
|
|||
const defaultLanguageToken = Number(defaultLanguageFromForced ?? tokenizer.token_to_id('<|en|>') ?? 50259);
|
||||
const transcribeToken = Number(transcribeFromForced ?? tokenizer.token_to_id('<|transcribe|>') ?? 50359);
|
||||
|
||||
const onnxThreadsPerJob = getOnnxThreadsPerJob();
|
||||
const stableSessionOptions: ort.InferenceSession.SessionOptions = {
|
||||
executionProviders: ['cpu'],
|
||||
graphOptimizationLevel: 'disabled',
|
||||
intraOpNumThreads: 1,
|
||||
intraOpNumThreads: onnxThreadsPerJob,
|
||||
interOpNumThreads: 1,
|
||||
executionMode: 'sequential',
|
||||
enableCpuMemArena: false,
|
||||
|
|
|
|||
Loading…
Reference in a new issue