omnius 1.0.641 → 1.0.642
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/index.js
CHANGED
|
@@ -345959,6 +345959,13 @@ function assessAdvancedOcrEvidence(input) {
|
|
|
345959
345959
|
alnum_ratio: Number(alnumRatio2.toFixed(3))
|
|
345960
345960
|
};
|
|
345961
345961
|
}
|
|
345962
|
+
function isTerminalSmallCropOcrRejection(evidence) {
|
|
345963
|
+
if (evidence.state !== "rejected")
|
|
345964
|
+
return false;
|
|
345965
|
+
if (evidence.reason === "high_volume_low_confidence_text")
|
|
345966
|
+
return true;
|
|
345967
|
+
return evidence.chars >= TERMINAL_SYMBOL_GARBAGE_CHARS && (evidence.alnum_ratio ?? 1) < TERMINAL_SYMBOL_GARBAGE_ALNUM_RATIO;
|
|
345968
|
+
}
|
|
345962
345969
|
function ocrDiagnostic(code8, message2, extra = {}) {
|
|
345963
345970
|
return {
|
|
345964
345971
|
schema: "omnius.ocr-diagnostic.v1",
|
|
@@ -345977,7 +345984,7 @@ function parsePipelineDiagnostic(error) {
|
|
|
345977
345984
|
return null;
|
|
345978
345985
|
}
|
|
345979
345986
|
}
|
|
345980
|
-
var OCR_PIPELINE_TIMEOUT_MS, OCR_PROCESS_DRAIN_MS, MIN_ACCEPTED_OCR_CONFIDENCE, MIN_SUBSTANTIVE_OCR_CHARS, HIGH_VOLUME_GARBAGE_CHARS, HIGH_VOLUME_GARBAGE_CONFIDENCE, OcrImageAdvancedTool;
|
|
345987
|
+
var OCR_PIPELINE_TIMEOUT_MS, OCR_PROCESS_DRAIN_MS, MIN_ACCEPTED_OCR_CONFIDENCE, MIN_SUBSTANTIVE_OCR_CHARS, HIGH_VOLUME_GARBAGE_CHARS, HIGH_VOLUME_GARBAGE_CONFIDENCE, TERMINAL_SYMBOL_GARBAGE_CHARS, TERMINAL_SYMBOL_GARBAGE_ALNUM_RATIO, OcrImageAdvancedTool;
|
|
345981
345988
|
var init_ocr_image_advanced = __esm({
|
|
345982
345989
|
"packages/execution/dist/tools/ocr-image-advanced.js"() {
|
|
345983
345990
|
"use strict";
|
|
@@ -345989,6 +345996,8 @@ var init_ocr_image_advanced = __esm({
|
|
|
345989
345996
|
MIN_SUBSTANTIVE_OCR_CHARS = 12;
|
|
345990
345997
|
HIGH_VOLUME_GARBAGE_CHARS = 64;
|
|
345991
345998
|
HIGH_VOLUME_GARBAGE_CONFIDENCE = 35;
|
|
345999
|
+
TERMINAL_SYMBOL_GARBAGE_CHARS = 24;
|
|
346000
|
+
TERMINAL_SYMBOL_GARBAGE_ALNUM_RATIO = 0.2;
|
|
345992
346001
|
OcrImageAdvancedTool = class {
|
|
345993
346002
|
workingDir;
|
|
345994
346003
|
name = "ocr_image_advanced";
|
|
@@ -346184,7 +346193,9 @@ var init_ocr_image_advanced = __esm({
|
|
|
346184
346193
|
deadline_ms: result.diagnostic?.deadline_ms ?? OCR_PIPELINE_TIMEOUT_MS,
|
|
346185
346194
|
attempts_completed: result.diagnostic?.attempts_completed ?? null,
|
|
346186
346195
|
attempts_planned: result.diagnostic?.attempts_planned ?? null,
|
|
346187
|
-
evidence: result.diagnostic?.evidence ?? null
|
|
346196
|
+
evidence: result.diagnostic?.evidence ?? null,
|
|
346197
|
+
terminal_small_crop_rejection: result.diagnostic?.terminal_small_crop_rejection ?? null,
|
|
346198
|
+
attempt_cap_seconds: result.diagnostic?.attempt_cap_seconds ?? null
|
|
346188
346199
|
})
|
|
346189
346200
|
};
|
|
346190
346201
|
}
|
|
@@ -346220,7 +346231,8 @@ var init_ocr_image_advanced = __esm({
|
|
|
346220
346231
|
error: "OCR evidence rejected: low-confidence text was suppressed and is not presented as extracted evidence.",
|
|
346221
346232
|
durationMs: performance.now() - start2,
|
|
346222
346233
|
data: ocrDiagnostic("ocr_evidence_rejected", "OCR output did not meet evidence-quality requirements.", {
|
|
346223
|
-
evidence: boundaryEvidence
|
|
346234
|
+
evidence: boundaryEvidence,
|
|
346235
|
+
terminal_small_crop_rejection: isTerminalSmallCropOcrRejection(boundaryEvidence)
|
|
346224
346236
|
})
|
|
346225
346237
|
};
|
|
346226
346238
|
}
|
|
@@ -688747,6 +688759,11 @@ function updateListenLiveState(patch) {
|
|
|
688747
688759
|
function getListenLiveState() {
|
|
688748
688760
|
return listenLiveState;
|
|
688749
688761
|
}
|
|
688762
|
+
function childProcessIsRunning(process4) {
|
|
688763
|
+
return Boolean(
|
|
688764
|
+
process4 && process4.exitCode === null && process4.signalCode === null && !process4.killed
|
|
688765
|
+
);
|
|
688766
|
+
}
|
|
688750
688767
|
function clamp0112(value2) {
|
|
688751
688768
|
if (!Number.isFinite(value2)) return 0;
|
|
688752
688769
|
return Math.max(0, Math.min(1, value2));
|
|
@@ -689408,7 +689425,7 @@ var init_listen = __esm({
|
|
|
689408
689425
|
lastVisibleVadAt = 0;
|
|
689409
689426
|
stderrTail = "";
|
|
689410
689427
|
get ready() {
|
|
689411
|
-
return this._ready;
|
|
689428
|
+
return this._ready && childProcessIsRunning(this.process);
|
|
689412
689429
|
}
|
|
689413
689430
|
async start() {
|
|
689414
689431
|
let pyPath = "python3";
|
|
@@ -689547,17 +689564,32 @@ ${text3}`.slice(-2e3);
|
|
|
689547
689564
|
});
|
|
689548
689565
|
onChildError(this.process, (err) => {
|
|
689549
689566
|
clearTimeout(timeout2);
|
|
689550
|
-
|
|
689567
|
+
const wasReady = this._ready;
|
|
689568
|
+
this._ready = false;
|
|
689569
|
+
this.process = null;
|
|
689570
|
+
if (wasReady) {
|
|
689571
|
+
updateListenLiveState({ phase: "error", lastStatus: err.message.slice(0, 160) });
|
|
689572
|
+
this.emit("error", err);
|
|
689573
|
+
} else {
|
|
689574
|
+
reject(err);
|
|
689575
|
+
}
|
|
689551
689576
|
});
|
|
689552
689577
|
onChildClose(this.process, (code8) => {
|
|
689553
689578
|
this.unregisterBrokerModel();
|
|
689554
|
-
|
|
689579
|
+
const wasReady = this._ready;
|
|
689580
|
+
this._ready = false;
|
|
689581
|
+
this.process = null;
|
|
689582
|
+
if (!wasReady) {
|
|
689555
689583
|
clearTimeout(timeout2);
|
|
689556
689584
|
reject(
|
|
689557
689585
|
new Error(
|
|
689558
689586
|
`${this.engineId} worker exited with code ${code8} before ready` + (this.stderrTail ? `: ${this.stderrTail.trim()}` : "")
|
|
689559
689587
|
)
|
|
689560
689588
|
);
|
|
689589
|
+
} else {
|
|
689590
|
+
const message2 = `${this.engineId} worker exited with code ${code8}` + (this.stderrTail ? `: ${this.stderrTail.trim()}` : "");
|
|
689591
|
+
updateListenLiveState({ phase: "error", lastStatus: message2.slice(0, 160) });
|
|
689592
|
+
this.emit("error", new Error(message2));
|
|
689561
689593
|
}
|
|
689562
689594
|
});
|
|
689563
689595
|
});
|
|
@@ -689622,6 +689654,10 @@ ${text3}`.slice(-2e3);
|
|
|
689622
689654
|
liveTranscriber = null;
|
|
689623
689655
|
// TranscribeLive from transcribe-cli or WhisperFallbackTranscriber
|
|
689624
689656
|
active = false;
|
|
689657
|
+
/** Set only after the selected worker has emitted/finished its ready signal. */
|
|
689658
|
+
liveWorkerReady = false;
|
|
689659
|
+
/** Identity captured when that worker became ready; never infer it from a later selection. */
|
|
689660
|
+
runningSelection = null;
|
|
689625
689661
|
paused = false;
|
|
689626
689662
|
// Pause state for voicechat (stops mic but keeps transcriber)
|
|
689627
689663
|
silenceTimer = null;
|
|
@@ -689664,6 +689700,27 @@ ${text3}`.slice(-2e3);
|
|
|
689664
689700
|
get currentMode() {
|
|
689665
689701
|
return this.config.mode;
|
|
689666
689702
|
}
|
|
689703
|
+
/**
|
|
689704
|
+
* Runtime truth for a resident live ASR pipeline. This does not claim that
|
|
689705
|
+
* a selected-but-unstarted model is ready, and it verifies fallback worker
|
|
689706
|
+
* process liveness rather than trusting an old `ready` event.
|
|
689707
|
+
*/
|
|
689708
|
+
get liveRuntimeReadiness() {
|
|
689709
|
+
const fallbackHealthy = !(this.liveTranscriber instanceof WhisperFallbackTranscriber) || this.liveTranscriber.ready === true;
|
|
689710
|
+
const workerReady = Boolean(
|
|
689711
|
+
this.active && this.liveWorkerReady && this.runningSelection && this.liveTranscriber && fallbackHealthy
|
|
689712
|
+
);
|
|
689713
|
+
return {
|
|
689714
|
+
// Keep the captured identity while reporting a failed worker so the
|
|
689715
|
+
// daemon can surface its error against the exact selection rather than
|
|
689716
|
+
// silently falling back to persisted weights state.
|
|
689717
|
+
engineId: this.runningSelection?.engineId ?? null,
|
|
689718
|
+
modelId: this.runningSelection?.modelId ?? null,
|
|
689719
|
+
workerReady,
|
|
689720
|
+
micActive: workerReady && !this.paused && childProcessIsRunning(this.micProcess),
|
|
689721
|
+
paused: this.paused
|
|
689722
|
+
};
|
|
689723
|
+
}
|
|
689667
689724
|
get pendingTranscript() {
|
|
689668
689725
|
return this.pendingText;
|
|
689669
689726
|
}
|
|
@@ -689934,6 +689991,8 @@ ${text3}`.slice(-2e3);
|
|
|
689934
689991
|
*/
|
|
689935
689992
|
async start() {
|
|
689936
689993
|
if (this.active) return "Already listening.";
|
|
689994
|
+
this.liveWorkerReady = false;
|
|
689995
|
+
this.runningSelection = null;
|
|
689937
689996
|
const unavailableReason = getAsrEngineUnavailableReason(this.config.engineId);
|
|
689938
689997
|
if (unavailableReason) return unavailableReason;
|
|
689939
689998
|
if (this.config.engineId === "vibevoice-transformers") {
|
|
@@ -690063,6 +690122,8 @@ ${text3}`.slice(-2e3);
|
|
|
690063
690122
|
}
|
|
690064
690123
|
);
|
|
690065
690124
|
this.liveTranscriber.on("error", (err) => {
|
|
690125
|
+
this.liveWorkerReady = false;
|
|
690126
|
+
updateListenLiveState({ phase: "error", lastStatus: err.message.slice(0, 160) });
|
|
690066
690127
|
this.emit("error", err);
|
|
690067
690128
|
});
|
|
690068
690129
|
await new Promise((resolve90, reject) => {
|
|
@@ -690121,6 +690182,8 @@ ${text3}`.slice(-2e3);
|
|
|
690121
690182
|
if (this.config.mode === "auto") this.resetSilenceTimer();
|
|
690122
690183
|
});
|
|
690123
690184
|
fallback.on("error", (err) => {
|
|
690185
|
+
this.liveWorkerReady = false;
|
|
690186
|
+
updateListenLiveState({ phase: "error", lastStatus: err.message.slice(0, 160) });
|
|
690124
690187
|
this.emit("error", err);
|
|
690125
690188
|
});
|
|
690126
690189
|
this.liveTranscriber = fallback;
|
|
@@ -690133,6 +690196,11 @@ transcribe-cli error: ${transcribeCliError}` : "";
|
|
|
690133
690196
|
}
|
|
690134
690197
|
}
|
|
690135
690198
|
this.active = true;
|
|
690199
|
+
this.liveWorkerReady = true;
|
|
690200
|
+
this.runningSelection = {
|
|
690201
|
+
engineId: this.config.engineId,
|
|
690202
|
+
modelId: this.config.modelId
|
|
690203
|
+
};
|
|
690136
690204
|
this.paused = false;
|
|
690137
690205
|
this.spawnMicProcess(micCmd);
|
|
690138
690206
|
this.blinkState = true;
|
|
@@ -690159,6 +690227,8 @@ transcribe-cli error: ${transcribeCliError}` : "";
|
|
|
690159
690227
|
async stop() {
|
|
690160
690228
|
if (!this.active) return "Not listening.";
|
|
690161
690229
|
this.active = false;
|
|
690230
|
+
this.liveWorkerReady = false;
|
|
690231
|
+
this.runningSelection = null;
|
|
690162
690232
|
this.owners.clear();
|
|
690163
690233
|
this.blinkState = false;
|
|
690164
690234
|
this.micWaterfall = [];
|
|
@@ -793053,6 +793123,12 @@ function getRuntimeStatus() {
|
|
|
793053
793123
|
const persisted = _listenEngine ? null : loadGlobalSettings();
|
|
793054
793124
|
const asrEngineId = _listenEngine?.currentEngine ?? persisted?.asrEngine ?? "openai-whisper";
|
|
793055
793125
|
const asrModelId = _listenEngine?.currentModel ?? persisted?.asrModel ?? "medium";
|
|
793126
|
+
const liveAsr = _listenEngine?.liveRuntimeReadiness ?? null;
|
|
793127
|
+
const selectedLivePipeline = Boolean(
|
|
793128
|
+
liveAsr?.engineId === asrEngineId && liveAsr?.modelId === asrModelId
|
|
793129
|
+
);
|
|
793130
|
+
const selectedLiveWorker = selectedLivePipeline && liveAsr?.workerReady === true;
|
|
793131
|
+
const selectedLiveMic = selectedLiveWorker && liveAsr?.micActive === true;
|
|
793056
793132
|
const managedAsr = asrEngineId === "openai-whisper" || asrEngineId === "nemotron-streaming" || asrEngineId === "voxtral-transformers" ? getManagedAsrReadiness(asrEngineId, asrModelId) : null;
|
|
793057
793133
|
const vibePhase = vibe.active ? "ready" : vibe.lastError ? "error" : vibe.weightsReady ? "weights-ready" : vibe.installed ? "runtime-installed" : "not-installed";
|
|
793058
793134
|
return {
|
|
@@ -793066,8 +793142,8 @@ function getRuntimeStatus() {
|
|
|
793066
793142
|
asrEngineId,
|
|
793067
793143
|
asrModelId,
|
|
793068
793144
|
asrBackend: asrEngineId === "vibevoice-transformers" ? "vibevoice-transformers" : managedAsr ? asrEngineId : listenState.backend,
|
|
793069
|
-
asrPhase: asrEngineId === "vibevoice-transformers" ? vibePhase : managedAsr?.active ? "ready" : managedAsr?.lastError ? "error" : managedAsr?.weightsReady ? "weights-ready" :
|
|
793070
|
-
asrReady: asrEngineId === "vibevoice-transformers" ? vibe.active : managedAsr ? managedAsr.active : Boolean(_listenEngine?.isActive),
|
|
793145
|
+
asrPhase: asrEngineId === "vibevoice-transformers" ? vibePhase : selectedLiveMic ? "listening" : selectedLiveWorker && liveAsr?.paused && listenState.phase === "paused" ? "paused" : selectedLivePipeline && listenState.phase === "error" ? "error" : managedAsr?.active ? "ready" : managedAsr?.lastError ? "error" : managedAsr?.weightsReady ? "weights-ready" : "not-started",
|
|
793146
|
+
asrReady: asrEngineId === "vibevoice-transformers" ? vibe.active : managedAsr ? managedAsr.active || selectedLiveMic : Boolean(_listenEngine?.isActive),
|
|
793071
793147
|
micDevice: listenState.micDevice || null,
|
|
793072
793148
|
micSourceKind: listenState.micSourceKind,
|
|
793073
793149
|
micSourceChannels: listenState.micSourceChannels,
|
|
@@ -828376,8 +828452,9 @@ function getOpenApiSpec() {
|
|
|
828376
828452
|
daemon_tool_names: { type: "array", items: { type: "string" }, description: "Optional exact allowlist of daemon tool names. Use this to keep local-model tool prompts small." },
|
|
828377
828453
|
agent_timeout_s: { type: "number", minimum: 5, maximum: 600, default: 45, description: "Total server-side agent-loop deadline. Distinct from per-backend timeout_s." },
|
|
828378
828454
|
agent_max_tool_rounds: { type: "integer", minimum: 1, maximum: 8, default: 1, description: "Maximum daemon-tool planning rounds. The default executes one tool round, then removes daemon schemas for lower-latency final synthesis." },
|
|
828455
|
+
agent_prefetch_web_search: { type: "boolean", default: false, description: "Explicitly execute authorized web_search with the latest user text before one backend synthesis. factual-first enables this automatically." },
|
|
828379
828456
|
max_turns: { type: "integer", description: "Q2 — agent_loop max iterations (default 8, max 64)." },
|
|
828380
|
-
prompt_template: { type: "string", enum: ["factual-first"], description: "
|
|
828457
|
+
prompt_template: { type: "string", enum: ["factual-first"], description: "Factual-first prefetches authorized web_search from the latest user turn and performs one grounded synthesis generation." }
|
|
828381
828458
|
} } } } },
|
|
828382
828459
|
responses: { 200: { description: "OpenAI chat.completion shape, SSE if stream=true. agent_loop responses include _agent_loop:{turns,log,done,reason,elapsed_ms,backend_transport}." }, 504: { description: "Backend round or total agent-loop deadline expired" }, 508: { description: "The model repeated an identical daemon tool call" }, ...ErrorResponses }
|
|
828383
828460
|
}
|
|
@@ -833272,6 +833349,76 @@ ${messages2[firstSystemIdx].content}`
|
|
|
833272
833349
|
const turnsLog = [];
|
|
833273
833350
|
const seenDaemonCalls = /* @__PURE__ */ new Set();
|
|
833274
833351
|
let daemonToolRoundsExecuted = 0;
|
|
833352
|
+
const prefetchWebSearch = (promptTemplate === "factual-first" || requestBody["agent_prefetch_web_search"] === true) && advertisedDaemonNames.has("web_search") && callerTools.length === 0 && !messages2.some((message2) => message2.role === "tool" || (message2.tool_calls?.length ?? 0) > 0);
|
|
833353
|
+
const latestUserQuery = [...messages2].reverse().find((message2) => message2.role === "user" && typeof message2.content === "string")?.content?.trim().slice(0, 1e3);
|
|
833354
|
+
if (prefetchWebSearch && latestUserQuery) {
|
|
833355
|
+
const meta = daemonTools.get("web_search");
|
|
833356
|
+
let tool = null;
|
|
833357
|
+
if (meta) {
|
|
833358
|
+
try {
|
|
833359
|
+
tool = new meta.ToolClass(process.cwd());
|
|
833360
|
+
} catch {
|
|
833361
|
+
try {
|
|
833362
|
+
tool = new meta.ToolClass();
|
|
833363
|
+
} catch {
|
|
833364
|
+
tool = null;
|
|
833365
|
+
}
|
|
833366
|
+
}
|
|
833367
|
+
}
|
|
833368
|
+
if (tool) {
|
|
833369
|
+
const callId = `prefetch_web_search_${randomBytes30(6).toString("hex")}`;
|
|
833370
|
+
const args = { query: latestUserQuery };
|
|
833371
|
+
const fingerprint3 = `web_search:${JSON.stringify(args)}`;
|
|
833372
|
+
messages2.push({
|
|
833373
|
+
role: "assistant",
|
|
833374
|
+
content: "",
|
|
833375
|
+
tool_calls: [{
|
|
833376
|
+
id: callId,
|
|
833377
|
+
type: "function",
|
|
833378
|
+
function: { name: "web_search", arguments: JSON.stringify(args) }
|
|
833379
|
+
}]
|
|
833380
|
+
});
|
|
833381
|
+
let toolResult;
|
|
833382
|
+
try {
|
|
833383
|
+
activeTool = tool;
|
|
833384
|
+
const remainingMs = totalDeadline - Date.now();
|
|
833385
|
+
if (remainingMs <= 0) throw new Error("agent loop deadline expired before factual-first search");
|
|
833386
|
+
const execution = await executeAgentLoopToolBounded(
|
|
833387
|
+
tool,
|
|
833388
|
+
args,
|
|
833389
|
+
Math.min(getAgentLoopToolTimeoutMs(), remainingMs),
|
|
833390
|
+
clientDisconnect
|
|
833391
|
+
);
|
|
833392
|
+
if (execution.clientDisconnected || isClientDisconnected()) return;
|
|
833393
|
+
toolResult = execution.result;
|
|
833394
|
+
} catch (error) {
|
|
833395
|
+
if (isClientDisconnected()) return;
|
|
833396
|
+
toolResult = {
|
|
833397
|
+
success: false,
|
|
833398
|
+
output: "",
|
|
833399
|
+
error: error instanceof Error ? error.message : String(error),
|
|
833400
|
+
durationMs: 0
|
|
833401
|
+
};
|
|
833402
|
+
} finally {
|
|
833403
|
+
activeTool = null;
|
|
833404
|
+
}
|
|
833405
|
+
messages2.push({
|
|
833406
|
+
role: "tool",
|
|
833407
|
+
tool_call_id: callId,
|
|
833408
|
+
name: "web_search",
|
|
833409
|
+
content: compactAgentLoopToolResult(toolResult)
|
|
833410
|
+
});
|
|
833411
|
+
seenDaemonCalls.add(fingerprint3);
|
|
833412
|
+
daemonToolRoundsExecuted = 1;
|
|
833413
|
+
turnsLog.push({
|
|
833414
|
+
turn: 0,
|
|
833415
|
+
tool_calls: 1,
|
|
833416
|
+
daemon_executed: 1,
|
|
833417
|
+
client_yielded: 0,
|
|
833418
|
+
prefetched: true
|
|
833419
|
+
});
|
|
833420
|
+
}
|
|
833421
|
+
}
|
|
833275
833422
|
for (let turn = 1; turn <= maxTurns; turn++) {
|
|
833276
833423
|
if (isClientDisconnected()) return;
|
|
833277
833424
|
if (Date.now() > totalDeadline) {
|
|
@@ -833326,6 +833473,7 @@ ${messages2[firstSystemIdx].content}`
|
|
|
833326
833473
|
daemon_tool_names: void 0,
|
|
833327
833474
|
agent_timeout_s: void 0,
|
|
833328
833475
|
agent_max_tool_rounds: void 0,
|
|
833476
|
+
agent_prefetch_web_search: void 0,
|
|
833329
833477
|
max_turns: void 0,
|
|
833330
833478
|
timeout_s: void 0,
|
|
833331
833479
|
realtime: void 0,
|
|
@@ -179,11 +179,17 @@ SMALL_CROP_AREA_PX = 512_000
|
|
|
179
179
|
MEDIUM_IMAGE_AREA_PX = 2_000_000
|
|
180
180
|
MAX_PIPELINE_DEADLINE_MS = 80_000
|
|
181
181
|
MAX_TESSERACT_ATTEMPT_SECONDS = 12.0
|
|
182
|
+
# Small conditioned crops normally complete in well under a second. A
|
|
183
|
+
# six-second cap still permits a busy Tesseract child, while keeping a failed
|
|
184
|
+
# recovery pair from consuming most of the REST deadline.
|
|
185
|
+
SMALL_CROP_TESSERACT_ATTEMPT_SECONDS = 6.0
|
|
182
186
|
MIN_TESSERACT_ATTEMPT_SECONDS = 0.25
|
|
183
187
|
MIN_ACCEPTED_CONFIDENCE = 50.0
|
|
184
188
|
MIN_SUBSTANTIVE_TEXT_CHARS = 12
|
|
185
189
|
HIGH_VOLUME_GARBAGE_CHARS = 64
|
|
186
190
|
HIGH_VOLUME_GARBAGE_CONFIDENCE = 35.0
|
|
191
|
+
TERMINAL_SYMBOL_GARBAGE_CHARS = 24
|
|
192
|
+
TERMINAL_SYMBOL_GARBAGE_ALNUM_RATIO = 0.20
|
|
187
193
|
ACTIVE_DEADLINE = None
|
|
188
194
|
|
|
189
195
|
|
|
@@ -211,11 +217,11 @@ class OcrDeadline:
|
|
|
211
217
|
if self.remaining_seconds() <= 0:
|
|
212
218
|
raise OcrPipelineTimeout(f"OCR deadline exceeded during {stage}")
|
|
213
219
|
|
|
214
|
-
def tesseract_timeout_seconds(self):
|
|
220
|
+
def tesseract_timeout_seconds(self, attempt_cap_seconds=MAX_TESSERACT_ATTEMPT_SECONDS):
|
|
215
221
|
self.check("Tesseract scheduling")
|
|
216
222
|
return max(
|
|
217
223
|
MIN_TESSERACT_ATTEMPT_SECONDS,
|
|
218
|
-
min(
|
|
224
|
+
min(attempt_cap_seconds, self.remaining_seconds()),
|
|
219
225
|
)
|
|
220
226
|
|
|
221
227
|
|
|
@@ -251,7 +257,8 @@ def text_from_tesseract_data(data):
|
|
|
251
257
|
return "\n".join(" ".join(words) for words in lines.values()).strip()
|
|
252
258
|
|
|
253
259
|
|
|
254
|
-
def run_tesseract(binary_img, deadline, language="eng", psm=6
|
|
260
|
+
def run_tesseract(binary_img, deadline, language="eng", psm=6,
|
|
261
|
+
attempt_cap_seconds=MAX_TESSERACT_ATTEMPT_SECONDS):
|
|
255
262
|
"""Run one bounded Tesseract TSV pass and derive text plus confidence."""
|
|
256
263
|
deadline.check("Tesseract")
|
|
257
264
|
pil_img = Image.fromarray(binary_img)
|
|
@@ -261,7 +268,7 @@ def run_tesseract(binary_img, deadline, language="eng", psm=6):
|
|
|
261
268
|
lang=language,
|
|
262
269
|
config=config,
|
|
263
270
|
output_type=pytesseract.Output.DICT,
|
|
264
|
-
timeout=deadline.tesseract_timeout_seconds(),
|
|
271
|
+
timeout=deadline.tesseract_timeout_seconds(attempt_cap_seconds),
|
|
265
272
|
)
|
|
266
273
|
text = text_from_tesseract_data(data)
|
|
267
274
|
confs = []
|
|
@@ -331,6 +338,24 @@ def assess_ocr_evidence(text, confidence, line_count):
|
|
|
331
338
|
}
|
|
332
339
|
|
|
333
340
|
|
|
341
|
+
def is_terminal_small_crop_rejection(evidence):
|
|
342
|
+
"""Whether a first small-crop pass proves another recovery pass is futile.
|
|
343
|
+
|
|
344
|
+
Blanks and plausible alphanumeric low-confidence text remain recoverable
|
|
345
|
+
through the second variant. A large very-low-confidence transcript or a
|
|
346
|
+
distinctly symbol-heavy stream cannot become safe evidence through another
|
|
347
|
+
PSM6 pass, and should not make callers wait for one.
|
|
348
|
+
"""
|
|
349
|
+
if evidence.get("state") != "rejected":
|
|
350
|
+
return False
|
|
351
|
+
if evidence.get("reason") == "high_volume_low_confidence_text":
|
|
352
|
+
return True
|
|
353
|
+
return (
|
|
354
|
+
evidence.get("chars", 0) >= TERMINAL_SYMBOL_GARBAGE_CHARS
|
|
355
|
+
and evidence.get("alnum_ratio", 1.0) < TERMINAL_SYMBOL_GARBAGE_ALNUM_RATIO
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
334
359
|
def compute_score(text, confidence, line_count, evidence=None):
|
|
335
360
|
"""Combined scoring heuristic:
|
|
336
361
|
- confidence * sqrt(char_count) — rewards quality and coverage
|
|
@@ -451,15 +476,18 @@ def write_all_outputs(text, base_name, output_dir):
|
|
|
451
476
|
# Main pipeline
|
|
452
477
|
# ---------------------------------------------------------------------------
|
|
453
478
|
|
|
454
|
-
def run_variant_plan(gray, plan, deadline, language, debug_dir=None, debug_prefix="full"
|
|
479
|
+
def run_variant_plan(gray, plan, deadline, language, debug_dir=None, debug_prefix="full",
|
|
480
|
+
attempt_cap_seconds=MAX_TESSERACT_ATTEMPT_SECONDS,
|
|
481
|
+
stop_on_terminal_rejection=False):
|
|
455
482
|
"""Run a bounded plan, stopping early once a legible result is proven."""
|
|
456
483
|
all_results = {}
|
|
457
484
|
ocr_errors = []
|
|
458
485
|
best_key = None
|
|
459
486
|
best_score = -1
|
|
460
487
|
early_exit = False
|
|
488
|
+
terminal_rejection = False
|
|
461
489
|
|
|
462
|
-
for vname, psm in plan:
|
|
490
|
+
for attempt_index, (vname, psm) in enumerate(plan):
|
|
463
491
|
deadline.check("preprocessing")
|
|
464
492
|
key = f"{vname}_psm{psm}"
|
|
465
493
|
try:
|
|
@@ -471,7 +499,9 @@ def run_variant_plan(gray, plan, deadline, language, debug_dir=None, debug_prefi
|
|
|
471
499
|
os.makedirs(debug_dir, exist_ok=True)
|
|
472
500
|
cv2.imwrite(os.path.join(debug_dir, f"{debug_prefix}_{vname}.png"), binary)
|
|
473
501
|
try:
|
|
474
|
-
text, confidence, line_count = run_tesseract(
|
|
502
|
+
text, confidence, line_count = run_tesseract(
|
|
503
|
+
binary, deadline, language, psm, attempt_cap_seconds
|
|
504
|
+
)
|
|
475
505
|
except OcrPipelineTimeout:
|
|
476
506
|
raise
|
|
477
507
|
except OcrPipelineCancelled:
|
|
@@ -496,8 +526,15 @@ def run_variant_plan(gray, plan, deadline, language, debug_dir=None, debug_prefi
|
|
|
496
526
|
if has_sufficient_evidence(text, confidence, line_count):
|
|
497
527
|
early_exit = True
|
|
498
528
|
break
|
|
529
|
+
if (
|
|
530
|
+
stop_on_terminal_rejection
|
|
531
|
+
and attempt_index == 0
|
|
532
|
+
and is_terminal_small_crop_rejection(evidence)
|
|
533
|
+
):
|
|
534
|
+
terminal_rejection = True
|
|
535
|
+
break
|
|
499
536
|
|
|
500
|
-
return all_results, ocr_errors, best_key, early_exit
|
|
537
|
+
return all_results, ocr_errors, best_key, early_exit, terminal_rejection
|
|
501
538
|
|
|
502
539
|
|
|
503
540
|
def run_pipeline(image_path, deadline, language="eng", do_regions=False, debug_dir=None,
|
|
@@ -522,10 +559,22 @@ def run_pipeline(image_path, deadline, language="eng", do_regions=False, debug_d
|
|
|
522
559
|
deadline.check("image conditioning")
|
|
523
560
|
gray_2x = upscale_2x(gray)
|
|
524
561
|
plan = build_ocr_plan(effective_area, single_psm)
|
|
562
|
+
is_small_crop = effective_area <= SMALL_CROP_AREA_PX
|
|
563
|
+
attempt_cap_seconds = (
|
|
564
|
+
SMALL_CROP_TESSERACT_ATTEMPT_SECONDS
|
|
565
|
+
if is_small_crop
|
|
566
|
+
else MAX_TESSERACT_ATTEMPT_SECONDS
|
|
567
|
+
)
|
|
525
568
|
attempts_completed = 0
|
|
526
569
|
try:
|
|
527
|
-
all_results, ocr_errors, best_key, early_exit = run_variant_plan(
|
|
528
|
-
gray_2x,
|
|
570
|
+
all_results, ocr_errors, best_key, early_exit, terminal_rejection = run_variant_plan(
|
|
571
|
+
gray_2x,
|
|
572
|
+
plan,
|
|
573
|
+
deadline,
|
|
574
|
+
language,
|
|
575
|
+
debug_dir,
|
|
576
|
+
attempt_cap_seconds=attempt_cap_seconds,
|
|
577
|
+
stop_on_terminal_rejection=is_small_crop,
|
|
529
578
|
)
|
|
530
579
|
attempts_completed = len(all_results)
|
|
531
580
|
if not best_key:
|
|
@@ -545,6 +594,8 @@ def run_pipeline(image_path, deadline, language="eng", do_regions=False, debug_d
|
|
|
545
594
|
"diagnostic": {
|
|
546
595
|
**diagnostic("ocr_evidence_rejected", message, deadline, "evidence_quality", attempts_completed, len(plan)),
|
|
547
596
|
"evidence": evidence,
|
|
597
|
+
"terminal_small_crop_rejection": terminal_rejection,
|
|
598
|
+
"attempt_cap_seconds": attempt_cap_seconds,
|
|
548
599
|
},
|
|
549
600
|
}
|
|
550
601
|
# Empty or tiny non-substantive detections are valid observations:
|
|
@@ -579,6 +630,8 @@ def run_pipeline(image_path, deadline, language="eng", do_regions=False, debug_d
|
|
|
579
630
|
"attempts_planned": len(plan),
|
|
580
631
|
"attempts_completed": attempts_completed,
|
|
581
632
|
"early_exit": False,
|
|
633
|
+
"terminal_small_crop_rejection": terminal_rejection,
|
|
634
|
+
"attempt_cap_seconds": attempt_cap_seconds,
|
|
582
635
|
"deadline_ms": deadline.deadline_ms,
|
|
583
636
|
},
|
|
584
637
|
}
|
|
@@ -615,6 +668,8 @@ def run_pipeline(image_path, deadline, language="eng", do_regions=False, debug_d
|
|
|
615
668
|
"attempts_planned": len(plan),
|
|
616
669
|
"attempts_completed": attempts_completed,
|
|
617
670
|
"early_exit": early_exit,
|
|
671
|
+
"terminal_small_crop_rejection": terminal_rejection,
|
|
672
|
+
"attempt_cap_seconds": attempt_cap_seconds,
|
|
618
673
|
"deadline_ms": deadline.deadline_ms,
|
|
619
674
|
},
|
|
620
675
|
}
|
|
@@ -627,9 +682,22 @@ def run_pipeline(image_path, deadline, language="eng", do_regions=False, debug_d
|
|
|
627
682
|
region_gray = extract_region(gray_2x, y_start, y_end)
|
|
628
683
|
# Region requests use at most two high-yield attempts. The
|
|
629
684
|
# main result already provides full-frame coverage.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
685
|
+
region_area = region_gray.shape[0] * region_gray.shape[1]
|
|
686
|
+
region_plan = build_ocr_plan(region_area, single_psm)[:2]
|
|
687
|
+
region_is_small = region_area <= SMALL_CROP_AREA_PX
|
|
688
|
+
region_results, _errors, region_best_key, _early_exit, _terminal_rejection = run_variant_plan(
|
|
689
|
+
region_gray,
|
|
690
|
+
region_plan,
|
|
691
|
+
deadline,
|
|
692
|
+
language,
|
|
693
|
+
debug_dir,
|
|
694
|
+
f"region_{rname}",
|
|
695
|
+
attempt_cap_seconds=(
|
|
696
|
+
SMALL_CROP_TESSERACT_ATTEMPT_SECONDS
|
|
697
|
+
if region_is_small
|
|
698
|
+
else MAX_TESSERACT_ATTEMPT_SECONDS
|
|
699
|
+
),
|
|
700
|
+
stop_on_terminal_rejection=region_is_small,
|
|
633
701
|
)
|
|
634
702
|
regions[rname] = region_results[region_best_key]["text"] if region_best_key else ""
|
|
635
703
|
result["regions"] = regions
|
package/docs/DISCOVERY.json
CHANGED
|
@@ -6163,6 +6163,11 @@
|
|
|
6163
6163
|
"default": 1,
|
|
6164
6164
|
"description": "Maximum daemon-tool planning rounds. The default executes one tool round, then removes daemon schemas for lower-latency final synthesis."
|
|
6165
6165
|
},
|
|
6166
|
+
"agent_prefetch_web_search": {
|
|
6167
|
+
"type": "boolean",
|
|
6168
|
+
"default": false,
|
|
6169
|
+
"description": "Explicitly execute authorized web_search with the latest user text before one backend synthesis. factual-first enables this automatically."
|
|
6170
|
+
},
|
|
6166
6171
|
"max_turns": {
|
|
6167
6172
|
"type": "integer",
|
|
6168
6173
|
"description": "Q2 — agent_loop max iterations (default 8, max 64)."
|
|
@@ -6172,7 +6177,7 @@
|
|
|
6172
6177
|
"enum": [
|
|
6173
6178
|
"factual-first"
|
|
6174
6179
|
],
|
|
6175
|
-
"description": "
|
|
6180
|
+
"description": "Factual-first prefetches authorized web_search from the latest user turn and performs one grounded synthesis generation."
|
|
6176
6181
|
}
|
|
6177
6182
|
}
|
|
6178
6183
|
}
|
|
@@ -47,6 +47,7 @@ Important body fields:
|
|
|
47
47
|
| `daemon_tool_names` | array | Exact daemon-tool allowlist; recommended for local models |
|
|
48
48
|
| `agent_timeout_s` | number | Whole-loop deadline, default 45 seconds and maximum 600 |
|
|
49
49
|
| `agent_max_tool_rounds` | integer | Daemon tool rounds before forced final synthesis; default 1 |
|
|
50
|
+
| `agent_prefetch_web_search` | boolean | Explicit one-generation web-search prefetch; factual-first enables it automatically |
|
|
50
51
|
| `max_turns` | integer | Server-side agent loop turn cap |
|
|
51
52
|
| `prompt_template` | string | Optional template such as `factual-first` |
|
|
52
53
|
|
|
@@ -130,4 +131,4 @@ For ASR/TTS systems that only need the text brain, use `/realtime` or `/v1/realt
|
|
|
130
131
|
|
|
131
132
|
`/v1/chat/completions` can run an internal tool loop when `agent_loop: true`. This lets clients collapse multiple model/tool round trips into one daemon request. Daemon tool calls execute inline; client-owned tool calls can still be yielded in OpenAI-compatible shape.
|
|
132
133
|
|
|
133
|
-
Ollama-backed loops use its native `/api/chat` tool protocol. `timeout_s` applies to each backend round, while `agent_timeout_s` bounds the complete loop and defaults to 45 seconds.
|
|
134
|
+
Ollama-backed loops use its native `/api/chat` tool protocol. `timeout_s` applies to each backend round, while `agent_timeout_s` bounds the complete loop and defaults to 45 seconds. For a normal loop, the planning turn is capped at 96 output tokens; one daemon-tool round is the default, after which Omnius removes daemon schemas for final synthesis. `prompt_template: "factual-first"` skips that model-planning round entirely: Omnius executes the already-mandated, authorized `web_search` using the latest user turn, inserts the protocol-correct tool evidence, and performs one grounded synthesis generation. `agent_prefetch_web_search: true` opts into the same path directly. Generic/deeper tool workflows remain available through `agent_max_tool_rounds`. Omnius returns typed HTTP 504/508 failures, caps tool evidence at 6,000 characters, and limits the implicit catalog unless `daemon_tool_names` requests exact additions.
|
|
@@ -128,9 +128,12 @@ needs `run` scope because optional
|
|
|
128
128
|
|
|
129
129
|
The managed pipeline is bounded: it starts with high-yield preprocessing and
|
|
130
130
|
expands variants only for low-evidence or larger images. Small crops avoid the
|
|
131
|
-
former all-variant/all-PSM explosion
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
former all-variant/all-PSM explosion and use a six-second cap per Tesseract
|
|
132
|
+
attempt. If the first small-crop pass proves high-volume very-low-confidence or
|
|
133
|
+
distinctly symbol-heavy garbage, recovery stops immediately; blanks and
|
|
134
|
+
plausibly recoverable text still receive the second high-yield attempt. The
|
|
135
|
+
REST default is 90 seconds (maximum 180 seconds), while the worker has an
|
|
136
|
+
80-second internal deadline so it can return diagnostics. A timeout or cancellation returns
|
|
134
137
|
`result.data.schema=omnius.ocr-diagnostic.v1` with code `ocr_timeout` or
|
|
135
138
|
`ocr_cancelled`; cancellation terminates the Python/Tesseract process group
|
|
136
139
|
with TERM followed by KILL.
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnius",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.642",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "omnius",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.642",
|
|
10
10
|
"bundleDependencies": [
|
|
11
11
|
"image-to-ascii"
|
|
12
12
|
],
|
package/package.json
CHANGED