@exulu/backend 3.4.0 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{catalog-PZTI2MJZ.js → catalog-3QLU2G7R.js} +1 -1
- package/dist/{chunk-NUVMV5FC.js → chunk-4PDWNVNT.js} +15 -4
- package/dist/{chunk-UIIUBZCO.js → chunk-7AZH4ETH.js} +7 -6
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-AB4F55SD.js → convert-exulu-tools-to-ai-sdk-tools-TZ2UKWR4.js} +2 -2
- package/dist/index.cjs +86 -14
- package/dist/index.js +70 -7
- package/ee/python/documents/processing/doc_processor.ts +14 -2
- package/ee/python/documents/processing/split_pdf.py +19 -2
- package/package.json +1 -1
|
@@ -17,9 +17,14 @@ function litellmBase() {
|
|
|
17
17
|
}
|
|
18
18
|
return { url: `http://${host}:${port}`, masterKey };
|
|
19
19
|
}
|
|
20
|
+
var _clientMode = false;
|
|
21
|
+
var isLiteLLMClientMode = () => _clientMode;
|
|
22
|
+
var setLiteLLMClientMode = (value) => {
|
|
23
|
+
_clientMode = value;
|
|
24
|
+
};
|
|
20
25
|
function resolveLiteLLMTarget() {
|
|
21
26
|
const rawBase = process.env.LITELLM_BASE_URL;
|
|
22
|
-
if (rawBase && rawBase.trim().length > 0) {
|
|
27
|
+
if (isLiteLLMClientMode() && rawBase && rawBase.trim().length > 0) {
|
|
23
28
|
const apiKey = process.env.EXULU_API_KEY;
|
|
24
29
|
if (!apiKey) {
|
|
25
30
|
throw new Error("EXULU_API_KEY is required when LITELLM_BASE_URL is set (remote LiteLLM client mode).");
|
|
@@ -46,7 +51,7 @@ var _cache;
|
|
|
46
51
|
var __resetLiteLLMCatalogCacheForTesting = () => {
|
|
47
52
|
_cache = void 0;
|
|
48
53
|
};
|
|
49
|
-
var
|
|
54
|
+
var fetchFullCatalog = async () => {
|
|
50
55
|
if (process.env.EXULU_USE_LITELLM !== "true") return [];
|
|
51
56
|
if (_cache && _cache.expiresAt > Date.now()) {
|
|
52
57
|
return _cache.items;
|
|
@@ -105,21 +110,27 @@ var fetchLiteLLMCatalog = async () => {
|
|
|
105
110
|
}
|
|
106
111
|
const uniqueItems = Array.from(map.values());
|
|
107
112
|
_cache = { expiresAt: Date.now() + CACHE_TTL_MS, items: uniqueItems };
|
|
108
|
-
return uniqueItems
|
|
113
|
+
return uniqueItems;
|
|
109
114
|
} catch (err) {
|
|
110
115
|
console.error("[EXULU] litellmCatalog: failed to fetch /model/info:", err);
|
|
111
116
|
return [];
|
|
112
117
|
}
|
|
113
118
|
};
|
|
119
|
+
var fetchLiteLLMCatalog = async () => {
|
|
120
|
+
const items = await fetchFullCatalog();
|
|
121
|
+
return items.filter((m) => m.type !== "speech_to_text" && m.type !== "text_to_speech");
|
|
122
|
+
};
|
|
114
123
|
var findLiteLLMModel = async (modelName) => {
|
|
115
124
|
if (!modelName) return void 0;
|
|
116
|
-
const items = await
|
|
125
|
+
const items = await fetchFullCatalog();
|
|
117
126
|
return items.find((m) => m.model_name === modelName);
|
|
118
127
|
};
|
|
119
128
|
|
|
120
129
|
export {
|
|
121
130
|
LiteLLMAdminError,
|
|
122
131
|
litellmBase,
|
|
132
|
+
isLiteLLMClientMode,
|
|
133
|
+
setLiteLLMClientMode,
|
|
123
134
|
resolveLiteLLMTarget,
|
|
124
135
|
__resetLiteLLMCatalogCacheForTesting,
|
|
125
136
|
fetchLiteLLMCatalog,
|
|
@@ -2,9 +2,11 @@ import "dotenv/config";
|
|
|
2
2
|
import {
|
|
3
3
|
LiteLLMAdminError,
|
|
4
4
|
findLiteLLMModel,
|
|
5
|
+
isLiteLLMClientMode,
|
|
5
6
|
litellmBase,
|
|
6
|
-
resolveLiteLLMTarget
|
|
7
|
-
|
|
7
|
+
resolveLiteLLMTarget,
|
|
8
|
+
setLiteLLMClientMode
|
|
9
|
+
} from "./chunk-4PDWNVNT.js";
|
|
8
10
|
|
|
9
11
|
// src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
|
|
10
12
|
import { S3Client as S3Client3, PutObjectCommand as PutObjectCommand3, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -169,13 +171,12 @@ var supervise = async (cfg) => {
|
|
|
169
171
|
}
|
|
170
172
|
};
|
|
171
173
|
var _packageRoot;
|
|
172
|
-
var _clientMode = false;
|
|
173
174
|
var setLiteLLMPackageRoot = (root) => {
|
|
174
175
|
_packageRoot = root;
|
|
175
176
|
};
|
|
176
177
|
var enableLiteLLMClientMode = () => {
|
|
177
178
|
if (internal.readyPromise) return;
|
|
178
|
-
|
|
179
|
+
setLiteLLMClientMode(true);
|
|
179
180
|
};
|
|
180
181
|
var startLiteLLMSupervisor = async (options = {}) => {
|
|
181
182
|
if (!isLiteLLMEnabled()) return;
|
|
@@ -225,7 +226,7 @@ var startLiteLLMSupervisor = async (options = {}) => {
|
|
|
225
226
|
};
|
|
226
227
|
var waitForLiteLLMReady = async () => {
|
|
227
228
|
if (!isLiteLLMEnabled()) return;
|
|
228
|
-
if (
|
|
229
|
+
if (isLiteLLMClientMode()) {
|
|
229
230
|
if (internal.state === "ready") return;
|
|
230
231
|
const { baseUrl, authHeaders, remote } = resolveLiteLLMTarget();
|
|
231
232
|
const url = remote ? `${baseUrl}/v1/models` : `${baseUrl}/health/liveliness`;
|
|
@@ -1712,7 +1713,7 @@ var ExuluTool = class _ExuluTool {
|
|
|
1712
1713
|
if (!agent) {
|
|
1713
1714
|
throw new Error("Agent not found.");
|
|
1714
1715
|
}
|
|
1715
|
-
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-
|
|
1716
|
+
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-TZ2UKWR4.js");
|
|
1716
1717
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
1717
1718
|
[this],
|
|
1718
1719
|
[],
|
|
@@ -2,8 +2,8 @@ import "dotenv/config";
|
|
|
2
2
|
import {
|
|
3
3
|
convertExuluToolsToAiSdkTools,
|
|
4
4
|
hydrateVariables
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-7AZH4ETH.js";
|
|
6
|
+
import "./chunk-4PDWNVNT.js";
|
|
7
7
|
export {
|
|
8
8
|
convertExuluToolsToAiSdkTools,
|
|
9
9
|
hydrateVariables
|
package/dist/index.cjs
CHANGED
|
@@ -1245,7 +1245,7 @@ function litellmBase() {
|
|
|
1245
1245
|
}
|
|
1246
1246
|
function resolveLiteLLMTarget() {
|
|
1247
1247
|
const rawBase = process.env.LITELLM_BASE_URL;
|
|
1248
|
-
if (rawBase && rawBase.trim().length > 0) {
|
|
1248
|
+
if (isLiteLLMClientMode() && rawBase && rawBase.trim().length > 0) {
|
|
1249
1249
|
const apiKey = process.env.EXULU_API_KEY;
|
|
1250
1250
|
if (!apiKey) {
|
|
1251
1251
|
throw new Error("EXULU_API_KEY is required when LITELLM_BASE_URL is set (remote LiteLLM client mode).");
|
|
@@ -1265,7 +1265,7 @@ function resolveLiteLLMTarget() {
|
|
|
1265
1265
|
remote: false
|
|
1266
1266
|
};
|
|
1267
1267
|
}
|
|
1268
|
-
var LiteLLMAdminError;
|
|
1268
|
+
var LiteLLMAdminError, _clientMode, isLiteLLMClientMode, setLiteLLMClientMode;
|
|
1269
1269
|
var init_env = __esm({
|
|
1270
1270
|
"src/exulu/litellm/env.ts"() {
|
|
1271
1271
|
"use strict";
|
|
@@ -1277,11 +1277,16 @@ var init_env = __esm({
|
|
|
1277
1277
|
this.name = "LiteLLMAdminError";
|
|
1278
1278
|
}
|
|
1279
1279
|
};
|
|
1280
|
+
_clientMode = false;
|
|
1281
|
+
isLiteLLMClientMode = () => _clientMode;
|
|
1282
|
+
setLiteLLMClientMode = (value) => {
|
|
1283
|
+
_clientMode = value;
|
|
1284
|
+
};
|
|
1280
1285
|
}
|
|
1281
1286
|
});
|
|
1282
1287
|
|
|
1283
1288
|
// src/exulu/litellm/supervisor.ts
|
|
1284
|
-
var import_node_child_process, import_node_fs3, import_node_path2, LITELLM_UI_PATH, MAX_CRASHES, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, READY_TIMEOUT_MS, WAIT_TIMEOUT_MS, READY_POLL_INTERVAL_MS, SHUTDOWN_GRACE_MS, internal, isLiteLLMEnabled, resolveConfig, log2, pollHealth, spawnLiteLLM, supervise, _packageRoot,
|
|
1289
|
+
var import_node_child_process, import_node_fs3, import_node_path2, LITELLM_UI_PATH, MAX_CRASHES, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, READY_TIMEOUT_MS, WAIT_TIMEOUT_MS, READY_POLL_INTERVAL_MS, SHUTDOWN_GRACE_MS, internal, isLiteLLMEnabled, resolveConfig, log2, pollHealth, spawnLiteLLM, supervise, _packageRoot, setLiteLLMPackageRoot, enableLiteLLMClientMode, startLiteLLMSupervisor, waitForLiteLLMReady, stopLiteLLM, shutdownHandlersRegistered, registerShutdownHandlers, getSupervisorState;
|
|
1285
1290
|
var init_supervisor = __esm({
|
|
1286
1291
|
"src/exulu/litellm/supervisor.ts"() {
|
|
1287
1292
|
"use strict";
|
|
@@ -1416,13 +1421,12 @@ var init_supervisor = __esm({
|
|
|
1416
1421
|
internal.backoffMs = Math.min(internal.backoffMs * 2, MAX_BACKOFF_MS);
|
|
1417
1422
|
}
|
|
1418
1423
|
};
|
|
1419
|
-
_clientMode = false;
|
|
1420
1424
|
setLiteLLMPackageRoot = (root) => {
|
|
1421
1425
|
_packageRoot = root;
|
|
1422
1426
|
};
|
|
1423
1427
|
enableLiteLLMClientMode = () => {
|
|
1424
1428
|
if (internal.readyPromise) return;
|
|
1425
|
-
|
|
1429
|
+
setLiteLLMClientMode(true);
|
|
1426
1430
|
};
|
|
1427
1431
|
startLiteLLMSupervisor = async (options = {}) => {
|
|
1428
1432
|
if (!isLiteLLMEnabled()) return;
|
|
@@ -1472,7 +1476,7 @@ var init_supervisor = __esm({
|
|
|
1472
1476
|
};
|
|
1473
1477
|
waitForLiteLLMReady = async () => {
|
|
1474
1478
|
if (!isLiteLLMEnabled()) return;
|
|
1475
|
-
if (
|
|
1479
|
+
if (isLiteLLMClientMode()) {
|
|
1476
1480
|
if (internal.state === "ready") return;
|
|
1477
1481
|
const { baseUrl, authHeaders, remote } = resolveLiteLLMTarget();
|
|
1478
1482
|
const url = remote ? `${baseUrl}/v1/models` : `${baseUrl}/health/liveliness`;
|
|
@@ -4979,7 +4983,7 @@ __export(catalog_exports, {
|
|
|
4979
4983
|
fetchLiteLLMCatalog: () => fetchLiteLLMCatalog,
|
|
4980
4984
|
findLiteLLMModel: () => findLiteLLMModel
|
|
4981
4985
|
});
|
|
4982
|
-
var CACHE_TTL_MS, _cache, __resetLiteLLMCatalogCacheForTesting, fetchLiteLLMCatalog, findLiteLLMModel;
|
|
4986
|
+
var CACHE_TTL_MS, _cache, __resetLiteLLMCatalogCacheForTesting, fetchFullCatalog, fetchLiteLLMCatalog, findLiteLLMModel;
|
|
4983
4987
|
var init_catalog = __esm({
|
|
4984
4988
|
"src/exulu/litellm/catalog.ts"() {
|
|
4985
4989
|
"use strict";
|
|
@@ -4989,7 +4993,7 @@ var init_catalog = __esm({
|
|
|
4989
4993
|
__resetLiteLLMCatalogCacheForTesting = () => {
|
|
4990
4994
|
_cache = void 0;
|
|
4991
4995
|
};
|
|
4992
|
-
|
|
4996
|
+
fetchFullCatalog = async () => {
|
|
4993
4997
|
if (process.env.EXULU_USE_LITELLM !== "true") return [];
|
|
4994
4998
|
if (_cache && _cache.expiresAt > Date.now()) {
|
|
4995
4999
|
return _cache.items;
|
|
@@ -5048,15 +5052,19 @@ var init_catalog = __esm({
|
|
|
5048
5052
|
}
|
|
5049
5053
|
const uniqueItems = Array.from(map.values());
|
|
5050
5054
|
_cache = { expiresAt: Date.now() + CACHE_TTL_MS, items: uniqueItems };
|
|
5051
|
-
return uniqueItems
|
|
5055
|
+
return uniqueItems;
|
|
5052
5056
|
} catch (err) {
|
|
5053
5057
|
console.error("[EXULU] litellmCatalog: failed to fetch /model/info:", err);
|
|
5054
5058
|
return [];
|
|
5055
5059
|
}
|
|
5056
5060
|
};
|
|
5061
|
+
fetchLiteLLMCatalog = async () => {
|
|
5062
|
+
const items = await fetchFullCatalog();
|
|
5063
|
+
return items.filter((m) => m.type !== "speech_to_text" && m.type !== "text_to_speech");
|
|
5064
|
+
};
|
|
5057
5065
|
findLiteLLMModel = async (modelName) => {
|
|
5058
5066
|
if (!modelName) return void 0;
|
|
5059
|
-
const items = await
|
|
5067
|
+
const items = await fetchFullCatalog();
|
|
5060
5068
|
return items.find((m) => m.model_name === modelName);
|
|
5061
5069
|
};
|
|
5062
5070
|
}
|
|
@@ -23875,6 +23883,18 @@ ${summary}` }],
|
|
|
23875
23883
|
// src/exulu/transcribe.ts
|
|
23876
23884
|
init_cjs_shims();
|
|
23877
23885
|
init_env();
|
|
23886
|
+
init_catalog();
|
|
23887
|
+
var TRANSCRIBE_SYSTEM_PROMPT = "You are a speech-to-text transcription engine. Detect the language actually spoken and transcribe it word-for-word in that same language. Never translate. Output only the transcript text \u2014 no quotes, labels, or commentary. If there is no intelligible speech, output nothing.";
|
|
23888
|
+
function isGeminiChatTranscriptionModel(entry) {
|
|
23889
|
+
const upstream = entry?.upstream_model ?? "";
|
|
23890
|
+
return /^vertex_ai\//i.test(upstream) && /gemini/i.test(upstream);
|
|
23891
|
+
}
|
|
23892
|
+
function cleanTranscript(raw) {
|
|
23893
|
+
let text = (raw ?? "").trim();
|
|
23894
|
+
const wrapped = text.match(/^(["'`])([\s\S]*)\1$/);
|
|
23895
|
+
if (wrapped) text = (wrapped[2] ?? "").trim();
|
|
23896
|
+
return text;
|
|
23897
|
+
}
|
|
23878
23898
|
var TranscriptionError = class extends Error {
|
|
23879
23899
|
constructor(upstreamStatus, message) {
|
|
23880
23900
|
super(message);
|
|
@@ -23883,9 +23903,16 @@ var TranscriptionError = class extends Error {
|
|
|
23883
23903
|
}
|
|
23884
23904
|
};
|
|
23885
23905
|
async function transcribeAudio(args) {
|
|
23886
|
-
const
|
|
23906
|
+
const target = resolveLiteLLMTarget();
|
|
23887
23907
|
const model = process.env.TRANSCRIPTION_MODEL;
|
|
23888
23908
|
if (!model) throw new Error("TRANSCRIPTION_MODEL is not set");
|
|
23909
|
+
const entry = await findLiteLLMModel(model).catch(() => void 0);
|
|
23910
|
+
if (isGeminiChatTranscriptionModel(entry)) {
|
|
23911
|
+
return transcribeViaChat(args, target, model);
|
|
23912
|
+
}
|
|
23913
|
+
return transcribeViaAudioEndpoint(args, target, model);
|
|
23914
|
+
}
|
|
23915
|
+
async function transcribeViaAudioEndpoint(args, target, model) {
|
|
23889
23916
|
const form = new FormData();
|
|
23890
23917
|
form.append(
|
|
23891
23918
|
"file",
|
|
@@ -23894,9 +23921,9 @@ async function transcribeAudio(args) {
|
|
|
23894
23921
|
);
|
|
23895
23922
|
form.append("model", model);
|
|
23896
23923
|
if (args.language) form.append("language", args.language);
|
|
23897
|
-
const res = await fetch(`${baseUrl}/v1/audio/transcriptions`, {
|
|
23924
|
+
const res = await fetch(`${target.baseUrl}/v1/audio/transcriptions`, {
|
|
23898
23925
|
method: "POST",
|
|
23899
|
-
headers: { ...authHeaders },
|
|
23926
|
+
headers: { ...target.authHeaders },
|
|
23900
23927
|
body: form
|
|
23901
23928
|
});
|
|
23902
23929
|
if (!res.ok) {
|
|
@@ -23909,6 +23936,42 @@ async function transcribeAudio(args) {
|
|
|
23909
23936
|
const json = await res.json();
|
|
23910
23937
|
return { text: typeof json.text === "string" ? json.text : "" };
|
|
23911
23938
|
}
|
|
23939
|
+
async function transcribeViaChat(args, target, model) {
|
|
23940
|
+
const subtype = args.file.mimetype.replace(/^audio\//, "").split(";")[0];
|
|
23941
|
+
const format = (subtype && subtype.length > 0 ? subtype : "wav").toLowerCase();
|
|
23942
|
+
const body = {
|
|
23943
|
+
model,
|
|
23944
|
+
temperature: 0,
|
|
23945
|
+
reasoning_effort: "disable",
|
|
23946
|
+
messages: [
|
|
23947
|
+
{ role: "system", content: TRANSCRIBE_SYSTEM_PROMPT },
|
|
23948
|
+
{
|
|
23949
|
+
role: "user",
|
|
23950
|
+
content: [
|
|
23951
|
+
{ type: "text", text: "Transcribe this audio." },
|
|
23952
|
+
{
|
|
23953
|
+
type: "input_audio",
|
|
23954
|
+
input_audio: { data: args.file.buffer.toString("base64"), format }
|
|
23955
|
+
}
|
|
23956
|
+
]
|
|
23957
|
+
}
|
|
23958
|
+
]
|
|
23959
|
+
};
|
|
23960
|
+
const res = await fetch(`${target.baseUrl}/v1/chat/completions`, {
|
|
23961
|
+
method: "POST",
|
|
23962
|
+
headers: { ...target.authHeaders, "Content-Type": "application/json" },
|
|
23963
|
+
body: JSON.stringify(body)
|
|
23964
|
+
});
|
|
23965
|
+
if (!res.ok) {
|
|
23966
|
+
const errBody = await res.text().catch(() => "");
|
|
23967
|
+
throw new TranscriptionError(
|
|
23968
|
+
res.status,
|
|
23969
|
+
`LiteLLM transcription failed (status ${res.status}): ${errBody}`.trim()
|
|
23970
|
+
);
|
|
23971
|
+
}
|
|
23972
|
+
const json = await res.json();
|
|
23973
|
+
return { text: cleanTranscript(json.choices?.[0]?.message?.content) };
|
|
23974
|
+
}
|
|
23912
23975
|
|
|
23913
23976
|
// src/exulu/speech.ts
|
|
23914
23977
|
init_cjs_shims();
|
|
@@ -33335,7 +33398,16 @@ ${setupResult.output || ""}`);
|
|
|
33335
33398
|
args: [paths.source, chunksDir, "--chunk-size", String(maxPagesPerChunk), "--max-size-mb", "25"],
|
|
33336
33399
|
timeout: 5 * 60 * 1e3
|
|
33337
33400
|
});
|
|
33338
|
-
|
|
33401
|
+
let pdfChunks;
|
|
33402
|
+
try {
|
|
33403
|
+
pdfChunks = JSON.parse(splitResult.stdout);
|
|
33404
|
+
} catch (err) {
|
|
33405
|
+
throw new Error(
|
|
33406
|
+
`[EXULU] split_pdf.py returned invalid JSON on stdout: ${err.message}
|
|
33407
|
+
stdout: ${splitResult.stdout.slice(0, 500)}
|
|
33408
|
+
stderr: ${splitResult.stderr.slice(-1e3)}`
|
|
33409
|
+
);
|
|
33410
|
+
}
|
|
33339
33411
|
console.log(`[EXULU] PDF split into ${pdfChunks.length} chunk(s) for OCR (max ${maxPagesPerChunk} pages each)`);
|
|
33340
33412
|
const chunkLimit = (0, import_p_limit.default)(3);
|
|
33341
33413
|
const chunkResults = await Promise.all(
|
package/dist/index.js
CHANGED
|
@@ -87,12 +87,12 @@ import {
|
|
|
87
87
|
verifyCredentialNonce,
|
|
88
88
|
waitForLiteLLMReady,
|
|
89
89
|
withRetry
|
|
90
|
-
} from "./chunk-
|
|
90
|
+
} from "./chunk-7AZH4ETH.js";
|
|
91
91
|
import {
|
|
92
92
|
LiteLLMAdminError,
|
|
93
93
|
findLiteLLMModel,
|
|
94
94
|
resolveLiteLLMTarget
|
|
95
|
-
} from "./chunk-
|
|
95
|
+
} from "./chunk-4PDWNVNT.js";
|
|
96
96
|
|
|
97
97
|
// src/index.ts
|
|
98
98
|
import "dotenv/config";
|
|
@@ -12867,7 +12867,7 @@ type LiteLLMModel {
|
|
|
12867
12867
|
}
|
|
12868
12868
|
`;
|
|
12869
12869
|
resolvers.Query["litellmCatalog"] = async () => {
|
|
12870
|
-
const { fetchLiteLLMCatalog } = await import("./catalog-
|
|
12870
|
+
const { fetchLiteLLMCatalog } = await import("./catalog-3QLU2G7R.js");
|
|
12871
12871
|
return fetchLiteLLMCatalog();
|
|
12872
12872
|
};
|
|
12873
12873
|
resolvers.Query["workflowSchedule"] = async (_, args, context, info) => {
|
|
@@ -14821,6 +14821,17 @@ ${summary}` }],
|
|
|
14821
14821
|
};
|
|
14822
14822
|
|
|
14823
14823
|
// src/exulu/transcribe.ts
|
|
14824
|
+
var TRANSCRIBE_SYSTEM_PROMPT = "You are a speech-to-text transcription engine. Detect the language actually spoken and transcribe it word-for-word in that same language. Never translate. Output only the transcript text \u2014 no quotes, labels, or commentary. If there is no intelligible speech, output nothing.";
|
|
14825
|
+
function isGeminiChatTranscriptionModel(entry) {
|
|
14826
|
+
const upstream = entry?.upstream_model ?? "";
|
|
14827
|
+
return /^vertex_ai\//i.test(upstream) && /gemini/i.test(upstream);
|
|
14828
|
+
}
|
|
14829
|
+
function cleanTranscript(raw) {
|
|
14830
|
+
let text = (raw ?? "").trim();
|
|
14831
|
+
const wrapped = text.match(/^(["'`])([\s\S]*)\1$/);
|
|
14832
|
+
if (wrapped) text = (wrapped[2] ?? "").trim();
|
|
14833
|
+
return text;
|
|
14834
|
+
}
|
|
14824
14835
|
var TranscriptionError = class extends Error {
|
|
14825
14836
|
constructor(upstreamStatus, message) {
|
|
14826
14837
|
super(message);
|
|
@@ -14829,9 +14840,16 @@ var TranscriptionError = class extends Error {
|
|
|
14829
14840
|
}
|
|
14830
14841
|
};
|
|
14831
14842
|
async function transcribeAudio(args) {
|
|
14832
|
-
const
|
|
14843
|
+
const target = resolveLiteLLMTarget();
|
|
14833
14844
|
const model = process.env.TRANSCRIPTION_MODEL;
|
|
14834
14845
|
if (!model) throw new Error("TRANSCRIPTION_MODEL is not set");
|
|
14846
|
+
const entry = await findLiteLLMModel(model).catch(() => void 0);
|
|
14847
|
+
if (isGeminiChatTranscriptionModel(entry)) {
|
|
14848
|
+
return transcribeViaChat(args, target, model);
|
|
14849
|
+
}
|
|
14850
|
+
return transcribeViaAudioEndpoint(args, target, model);
|
|
14851
|
+
}
|
|
14852
|
+
async function transcribeViaAudioEndpoint(args, target, model) {
|
|
14835
14853
|
const form = new FormData();
|
|
14836
14854
|
form.append(
|
|
14837
14855
|
"file",
|
|
@@ -14840,9 +14858,9 @@ async function transcribeAudio(args) {
|
|
|
14840
14858
|
);
|
|
14841
14859
|
form.append("model", model);
|
|
14842
14860
|
if (args.language) form.append("language", args.language);
|
|
14843
|
-
const res = await fetch(`${baseUrl}/v1/audio/transcriptions`, {
|
|
14861
|
+
const res = await fetch(`${target.baseUrl}/v1/audio/transcriptions`, {
|
|
14844
14862
|
method: "POST",
|
|
14845
|
-
headers: { ...authHeaders },
|
|
14863
|
+
headers: { ...target.authHeaders },
|
|
14846
14864
|
body: form
|
|
14847
14865
|
});
|
|
14848
14866
|
if (!res.ok) {
|
|
@@ -14855,6 +14873,42 @@ async function transcribeAudio(args) {
|
|
|
14855
14873
|
const json = await res.json();
|
|
14856
14874
|
return { text: typeof json.text === "string" ? json.text : "" };
|
|
14857
14875
|
}
|
|
14876
|
+
async function transcribeViaChat(args, target, model) {
|
|
14877
|
+
const subtype = args.file.mimetype.replace(/^audio\//, "").split(";")[0];
|
|
14878
|
+
const format = (subtype && subtype.length > 0 ? subtype : "wav").toLowerCase();
|
|
14879
|
+
const body = {
|
|
14880
|
+
model,
|
|
14881
|
+
temperature: 0,
|
|
14882
|
+
reasoning_effort: "disable",
|
|
14883
|
+
messages: [
|
|
14884
|
+
{ role: "system", content: TRANSCRIBE_SYSTEM_PROMPT },
|
|
14885
|
+
{
|
|
14886
|
+
role: "user",
|
|
14887
|
+
content: [
|
|
14888
|
+
{ type: "text", text: "Transcribe this audio." },
|
|
14889
|
+
{
|
|
14890
|
+
type: "input_audio",
|
|
14891
|
+
input_audio: { data: args.file.buffer.toString("base64"), format }
|
|
14892
|
+
}
|
|
14893
|
+
]
|
|
14894
|
+
}
|
|
14895
|
+
]
|
|
14896
|
+
};
|
|
14897
|
+
const res = await fetch(`${target.baseUrl}/v1/chat/completions`, {
|
|
14898
|
+
method: "POST",
|
|
14899
|
+
headers: { ...target.authHeaders, "Content-Type": "application/json" },
|
|
14900
|
+
body: JSON.stringify(body)
|
|
14901
|
+
});
|
|
14902
|
+
if (!res.ok) {
|
|
14903
|
+
const errBody = await res.text().catch(() => "");
|
|
14904
|
+
throw new TranscriptionError(
|
|
14905
|
+
res.status,
|
|
14906
|
+
`LiteLLM transcription failed (status ${res.status}): ${errBody}`.trim()
|
|
14907
|
+
);
|
|
14908
|
+
}
|
|
14909
|
+
const json = await res.json();
|
|
14910
|
+
return { text: cleanTranscript(json.choices?.[0]?.message?.content) };
|
|
14911
|
+
}
|
|
14858
14912
|
|
|
14859
14913
|
// src/exulu/speech.ts
|
|
14860
14914
|
var SpeechError = class extends Error {
|
|
@@ -23938,7 +23992,16 @@ ${setupResult.output || ""}`);
|
|
|
23938
23992
|
args: [paths.source, chunksDir, "--chunk-size", String(maxPagesPerChunk), "--max-size-mb", "25"],
|
|
23939
23993
|
timeout: 5 * 60 * 1e3
|
|
23940
23994
|
});
|
|
23941
|
-
|
|
23995
|
+
let pdfChunks;
|
|
23996
|
+
try {
|
|
23997
|
+
pdfChunks = JSON.parse(splitResult.stdout);
|
|
23998
|
+
} catch (err) {
|
|
23999
|
+
throw new Error(
|
|
24000
|
+
`[EXULU] split_pdf.py returned invalid JSON on stdout: ${err.message}
|
|
24001
|
+
stdout: ${splitResult.stdout.slice(0, 500)}
|
|
24002
|
+
stderr: ${splitResult.stderr.slice(-1e3)}`
|
|
24003
|
+
);
|
|
24004
|
+
}
|
|
23942
24005
|
console.log(`[EXULU] PDF split into ${pdfChunks.length} chunk(s) for OCR (max ${maxPagesPerChunk} pages each)`);
|
|
23943
24006
|
const chunkLimit = pLimit(3);
|
|
23944
24007
|
const chunkResults = await Promise.all(
|
|
@@ -837,8 +837,20 @@ async function processPdf(
|
|
|
837
837
|
timeout: 5 * 60 * 1000,
|
|
838
838
|
});
|
|
839
839
|
|
|
840
|
-
|
|
841
|
-
|
|
840
|
+
// split_pdf.py contracts to put nothing but the JSON payload on stdout.
|
|
841
|
+
// If something slips in anyway, the bare SyntaxError only quotes the first
|
|
842
|
+
// few characters ("Unexpected token 'w'") and names neither the script nor
|
|
843
|
+
// the offending output — so re-throw with the actual streams attached.
|
|
844
|
+
let pdfChunks: Array<{ path: string; start_page: number; end_page: number }>;
|
|
845
|
+
try {
|
|
846
|
+
pdfChunks = JSON.parse(splitResult.stdout);
|
|
847
|
+
} catch (err) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
`[EXULU] split_pdf.py returned invalid JSON on stdout: ${(err as Error).message}\n` +
|
|
850
|
+
`stdout: ${splitResult.stdout.slice(0, 500)}\n` +
|
|
851
|
+
`stderr: ${splitResult.stderr.slice(-1000)}`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
842
854
|
|
|
843
855
|
console.log(`[EXULU] PDF split into ${pdfChunks.length} chunk(s) for OCR (max ${maxPagesPerChunk} pages each)`);
|
|
844
856
|
|
|
@@ -20,7 +20,24 @@ import os
|
|
|
20
20
|
import json
|
|
21
21
|
import argparse
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
# stdout is this script's result channel and the caller does JSON.parse() on it,
|
|
24
|
+
# so nothing else may write there. Our own prints all pass file=sys.stderr, but
|
|
25
|
+
# dependencies do not honour that: PyMuPDF sends its messages to sys.stdout by
|
|
26
|
+
# default, and importing the legacy `fitz` alias emits
|
|
27
|
+
# "warning: The `fitz` API is deprecated ..." — which lands ahead of the payload
|
|
28
|
+
# and fails the caller with "Unexpected token 'w'". PyMuPDF arrives unpinned as
|
|
29
|
+
# a docling transitive dependency, so a routine rebuild is enough to introduce a
|
|
30
|
+
# banner like that. Point sys.stdout at stderr before importing anything and
|
|
31
|
+
# keep a private handle for the result, so any library that prints — now or
|
|
32
|
+
# after a future dependency bump — is shunted to the log channel instead of
|
|
33
|
+
# corrupting the payload.
|
|
34
|
+
_stdout = sys.stdout
|
|
35
|
+
sys.stdout = sys.stderr
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
import pymupdf as fitz # PyMuPDF >= 1.24.3, where the module was renamed
|
|
39
|
+
except ImportError: # older releases only ship the legacy `fitz` module
|
|
40
|
+
import fitz
|
|
24
41
|
|
|
25
42
|
|
|
26
43
|
def _write_chunk(
|
|
@@ -145,7 +162,7 @@ if __name__ == "__main__":
|
|
|
145
162
|
|
|
146
163
|
try:
|
|
147
164
|
chunks = split_pdf(args.input_pdf, args.output_dir, args.chunk_size, max_size_bytes)
|
|
148
|
-
print(json.dumps(chunks))
|
|
165
|
+
print(json.dumps(chunks), file=_stdout)
|
|
149
166
|
except Exception as e:
|
|
150
167
|
print(f"[split_pdf] ERROR: {e}", file=sys.stderr)
|
|
151
168
|
sys.exit(1)
|