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
- reject(err);
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
- if (!this._ready) {
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" : listenState.phase,
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: "Q8 prepended system policy template. 'factual-first' instructs model to call web_search FIRST for any factual question." }
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(MAX_TESSERACT_ATTEMPT_SECONDS, self.remaining_seconds()),
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(binary, deadline, language, psm)
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, plan, deadline, language, debug_dir
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
- region_plan = build_ocr_plan(region_gray.shape[0] * region_gray.shape[1], single_psm)[:2]
631
- region_results, _errors, region_best_key, _early_exit = run_variant_plan(
632
- region_gray, region_plan, deadline, language, debug_dir, f"region_{rname}"
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
@@ -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": "Q8 prepended system policy template. 'factual-first' instructs model to call web_search FIRST for any factual question."
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. The planning turn is capped at 96 output tokens. By default Omnius executes one daemon-tool round, then removes daemon schemas for the final synthesis; set `agent_max_tool_rounds` only when a workflow genuinely needs deeper tool chaining. Omnius returns a typed HTTP 504 when the total budget expires and HTTP 508 when a model repeats the same daemon tool with identical arguments. Tool results are capped at 6,000 characters before the next prompt. Without `daemon_tool_names`, Omnius offers only a compact core catalog permitted by `include_daemon_tools`; `prompt_template: "factual-first"` narrows it further to `web_search` and `web_fetch`.
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. The REST default is 90 seconds (maximum
132
- 180 seconds), while the worker has an 80-second internal deadline so it can
133
- return diagnostics. A timeout or cancellation returns
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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.641",
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.641",
9
+ "version": "1.0.642",
10
10
  "bundleDependencies": [
11
11
  "image-to-ascii"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.641",
3
+ "version": "1.0.642",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/library.js",