Skip to content

Commit 567de99

Browse files
committed
feat: speed up ultra fast translation
- Batch paragraphs per page through POST /api/translate-batch for OpenAI-compatible and ChatGPT Subscription providers, sharing the single-paragraph cache and falling back per paragraph on bad replies. - Adapt per-provider concurrency (AIMD) on HTTP 429, honoring retry-after. - Let reader tasks carry a page priority so the visible and requested pages translate first. - Prewarm pdf.js once and again on document open to cut first-page latency.
1 parent cda3584 commit 567de99

11 files changed

Lines changed: 895 additions & 244 deletions

File tree

‎server/app.mjs‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { createDocumentCache } from './documents/document-cache.mjs';
77
import { createDocumentStore, isDocumentId, MAX_DOCUMENT_BYTES } from './documents/documents.mjs';
88
import { createCompanionDocumentStore } from './documents/companion-document-store.mjs';
99
import { createLayoutExtraction } from './documents/layout-extraction.mjs';
10-
import { extractTextWithPositionsAsync } from './documents/pdf-extractor.mjs';
10+
import { extractTextWithPositionsAsync, prewarmPdfJs } from './documents/pdf-extractor.mjs';
1111
import { createPerformanceTracker } from './diagnostics/performance.mjs';
1212
import { createDeveloperDiagnostics } from './diagnostics/developer-diagnostics.mjs';
1313
import {
@@ -53,6 +53,7 @@ import { registerProviderModelRoutes } from './http/provider-model-routes.mjs';
5353
import { registerReaderTaskRoutes } from './http/reader-task-routes.mjs';
5454
import { createFormulaOcrService } from './formula/formula-ocr-service.mjs';
5555
import { createReaderTasks } from './translation/reader-tasks.mjs';
56+
import { createAdaptiveLimits } from './translation/adaptive-limits.mjs';
5657

5758
export async function startServer({
5859
port = 5173,
@@ -121,6 +122,7 @@ export async function startServer({
121122
const documentCache = createDocumentCache(cacheDir);
122123
const translationVersions = createTranslationVersions({ directory: cacheDir, documentCache });
123124
const providerLimiter = createLimiter(4, { label: 'provider' });
125+
const adaptiveLimits = createAdaptiveLimits();
124126
const pageLimiter = createLimiter(2, { label: 'kernel-pages' });
125127
const proxyJobs = new Map();
126128
const kernelReports = [];
@@ -173,6 +175,7 @@ export async function startServer({
173175
isBusy: () =>
174176
proxyJobs.size > 0 ||
175177
providerLimiter.snapshot().tasks.length > 0 ||
178+
adaptiveLimits.snapshots().some((snapshot) => snapshot.tasks.length > 0) ||
176179
pageLimiter.snapshot().tasks.length > 0 ||
177180
(engines.tasks?.().length || 0) > 0 ||
178181
(readerTasks?.active?.() || 0) > 0,
@@ -188,7 +191,11 @@ export async function startServer({
188191
proxyJobs,
189192
proxyUrl: () => `${origin}/kernel-proxy/v1`,
190193
});
191-
const diagnosticQueues = () => [pageLimiter.snapshot(), providerLimiter.snapshot()];
194+
const diagnosticQueues = () => [
195+
pageLimiter.snapshot(),
196+
providerLimiter.snapshot(),
197+
...adaptiveLimits.snapshots(),
198+
];
192199
const diagnosticTasks = () => {
193200
const tasks = [...(engines.tasks?.() || [])];
194201
for (const queue of diagnosticQueues()) tasks.push(...queue.tasks);
@@ -252,6 +259,7 @@ export async function startServer({
252259
maxDocumentBytes: MAX_DOCUMENT_BYTES,
253260
translationVersions,
254261
onDocumentDelete: (id) => readerTasks.cancelForDocument(id),
262+
onDocumentOpen: () => void prewarmPdfJs(),
255263
});
256264
registerKernelRoutes(app, {
257265
engines,
@@ -288,6 +296,7 @@ export async function startServer({
288296
providerRuntime,
289297
getApiKey,
290298
translationVersions,
299+
adaptiveLimits,
291300
});
292301
registerReaderTaskRoutes(app, { readerTasks });
293302

‎server/documents/pdf-extractor.mjs‎

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export function hasNativeInspector(platform = process.platform, arch = process.a
1010
: platform === 'linux' && ['x64', 'arm64'].includes(arch);
1111
}
1212

13-
let native, pdfjs, recoveryWarned;
14-
export async function extractPdfJsPositions(bytes, pages) {
13+
let native, pdfjs, recoveryWarned, prewarm;
14+
function loadPdfJs() {
1515
pdfjs ??= (async () => {
1616
if (!globalThis.DOMMatrix) globalThis.DOMMatrix = (await import('@thednp/dommatrix')).default;
1717
const module = await import('pdfjs-dist/legacy/build/pdf.mjs');
@@ -22,7 +22,45 @@ export async function extractPdfJsPositions(bytes, pages) {
2222
).href;
2323
return { module, root };
2424
})();
25-
const { module, root } = await pdfjs;
25+
return pdfjs;
26+
}
27+
28+
function blankPdf() {
29+
const objects = [
30+
'<< /Type /Catalog /Pages 2 0 R >>',
31+
'<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
32+
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >>',
33+
];
34+
let body = '%PDF-1.4\n';
35+
const offsets = objects.map((object, index) => {
36+
const offset = body.length;
37+
body += `${index + 1} 0 obj\n${object}\nendobj\n`;
38+
return offset;
39+
});
40+
const xref = body.length;
41+
body += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`;
42+
for (const offset of offsets) body += `${String(offset).padStart(10, '0')} 00000 n \n`;
43+
body += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
44+
return Buffer.from(body, 'latin1');
45+
}
46+
47+
// Scanned-page recovery cold-loads pdf.js and its fake worker on first use,
48+
// which costs most of a second. Warm it in the background once a document is
49+
// open so the first scanned page does not pay that cost on the reading path.
50+
export function prewarmPdfJs({ extract = extractPdfJsPositions } = {}) {
51+
prewarm ??= new Promise((resolveWarm) => {
52+
const timer = setTimeout(() => {
53+
extract(blankPdf(), [1])
54+
.catch(() => {})
55+
.then(() => resolveWarm());
56+
}, 0);
57+
timer.unref?.();
58+
});
59+
return prewarm;
60+
}
61+
62+
export async function extractPdfJsPositions(bytes, pages) {
63+
const { module, root } = await loadPdfJs();
2664
const task = module.getDocument({
2765
data: new Uint8Array(bytes),
2866
isEvalSupported: false,

‎server/http/document-routes.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export function registerDocumentRoutes(
6464
maxDocumentBytes,
6565
translationVersions,
6666
onDocumentDelete,
67+
onDocumentOpen,
6768
},
6869
) {
6970
app.post(
@@ -79,6 +80,7 @@ export function registerDocumentRoutes(
7980
.register(entry.documentHash, req.headers['x-document-name'])
8081
.catch(() => {});
8182
res.status(201).json({ id });
83+
onDocumentOpen?.(id);
8284
} catch (e) {
8385
if (e?.status) return res.status(e.status).json({ error: e.message });
8486
res.status(400).json({ error: e.message });

0 commit comments

Comments
 (0)