omnius 1.0.419 → 1.0.421

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
@@ -603509,6 +603509,8 @@ var init_listen = __esm({
603509
603509
  model: this.model,
603510
603510
  lastStatus: `loading whisper ${this.model} model...`
603511
603511
  });
603512
+ const consensusEnv = String(process.env["OMNIUS_ASR_CONSENSUS"] ?? "").trim().toLowerCase();
603513
+ const consensusModel = consensusEnv === "0" || consensusEnv === "off" || consensusEnv === "false" ? "off" : consensusEnv || (this.model === "tiny" ? "base" : "tiny");
603512
603514
  this.process = spawn29(
603513
603515
  pyPath,
603514
603516
  [
@@ -603518,7 +603520,9 @@ var init_listen = __esm({
603518
603520
  "--chunk-seconds",
603519
603521
  "3",
603520
603522
  "--window-seconds",
603521
- "10"
603523
+ "10",
603524
+ "--consensus-model",
603525
+ consensusModel
603522
603526
  ],
603523
603527
  {
603524
603528
  stdio: ["pipe", "pipe", "pipe"],
@@ -603555,6 +603559,11 @@ var init_listen = __esm({
603555
603559
  isFinal: evt.isFinal ?? false
603556
603560
  });
603557
603561
  break;
603562
+ case "consensus_rejected":
603563
+ updateListenLiveState({
603564
+ lastStatus: `consensus rejected (${evt.reason ?? "mismatch"}): ${String(evt.primary ?? "").slice(0, 80)}`
603565
+ });
603566
+ break;
603558
603567
  case "error":
603559
603568
  this.emit("error", new Error(evt.message));
603560
603569
  break;
@@ -605469,7 +605478,7 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
605469
605478
  }
605470
605479
  if (snapshot.config.selectedAudioInput) lines.push(`Selected audio input: ${snapshot.config.selectedAudioInput}`);
605471
605480
  if (snapshot.config.selectedAudioOutput) lines.push(`Selected audio output: ${snapshot.config.selectedAudioOutput}`);
605472
- const cameraEvents = (snapshot.events ?? []).filter((event) => event.kind.startsWith("camera_") || event.kind === "clip_recognition" || event.kind === "visual_trigger").sort((a2, b) => b.observedAt - a2.observedAt).slice(0, 18);
605481
+ const cameraEvents = (snapshot.events ?? []).filter((event) => event.kind.startsWith("camera_") || event.kind === "clip_recognition" || event.kind === "visual_trigger").sort((a2, b) => b.observedAt - a2.observedAt).slice(0, toolCapable ? 18 : 6);
605473
605482
  if (activeVideo) {
605474
605483
  lines.push("");
605475
605484
  lines.push("## LIVE CAMERA EVENTS");
@@ -605519,10 +605528,10 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
605519
605528
  if (snapshot.video.framePath) lines.push(`frame: ${snapshot.video.framePath}`);
605520
605529
  const classifications = cameraClassificationsSummary(snapshot.video);
605521
605530
  if (classifications) lines.push(`Live classifications: ${classifications}`);
605522
- if (snapshot.video.asciiContext) lines.push(snapshot.video.asciiContext);
605531
+ if (toolCapable && snapshot.video.asciiContext) lines.push(snapshot.video.asciiContext);
605523
605532
  if (snapshot.video.inference) {
605524
605533
  lines.push("Video inference:");
605525
- lines.push(cleanPreview(snapshot.video.inference, 1800));
605534
+ lines.push(cleanPreview(snapshot.video.inference, toolCapable ? 1800 : 600));
605526
605535
  }
605527
605536
  }
605528
605537
  if (cameraSnapshots.length > 1) {
@@ -614228,6 +614237,7 @@ __export(render_exports, {
614228
614237
  SLASH_COMMANDS: () => SLASH_COMMANDS2,
614229
614238
  applyCollapseToBox: () => applyCollapseToBox,
614230
614239
  breakTelegramCoalesce: () => breakTelegramCoalesce,
614240
+ breakVoiceChatCoalesce: () => breakVoiceChatCoalesce,
614231
614241
  c: () => c3,
614232
614242
  charWidth: () => charWidth2,
614233
614243
  fileLink: () => fileLink,
@@ -614264,6 +614274,7 @@ __export(render_exports, {
614264
614274
  renderUserInterrupt: () => renderUserInterrupt,
614265
614275
  renderUserMessage: () => renderUserMessage,
614266
614276
  renderVerbose: () => renderVerbose,
614277
+ renderVoiceChatCoalescedRow: () => renderVoiceChatCoalescedRow,
614267
614278
  renderVoiceText: () => renderVoiceText,
614268
614279
  renderWarning: () => renderWarning,
614269
614280
  setColorsEnabled: () => setColorsEnabled,
@@ -615020,6 +615031,63 @@ function renderTelegramCoalescedRow(row, opts) {
615020
615031
  );
615021
615032
  host.appendDynamicBlock(id2);
615022
615033
  }
615034
+ function breakVoiceChatCoalesce() {
615035
+ _voiceChatCoalesce = null;
615036
+ }
615037
+ function renderVoiceChatCoalescedRow(row, opts) {
615038
+ const colorCode = opts?.colorCode ?? 45;
615039
+ const title = opts?.title ?? "☏ VoiceChat";
615040
+ const redir = _contentWriteHook?.redirect?.();
615041
+ const host = _contentWriteHook?.dynamicBlockHost?.() ?? null;
615042
+ if (redir || !host || typeof host.refreshDynamicBlocks !== "function") {
615043
+ _voiceChatCoalesce = null;
615044
+ const text2 = `${row}
615045
+ `;
615046
+ if (redir) redir(text2);
615047
+ else process.stdout.write(text2);
615048
+ return;
615049
+ }
615050
+ if (_voiceChatCoalesce) {
615051
+ _voiceChatCoalesce.rows.push(row);
615052
+ host.refreshDynamicBlocks();
615053
+ return;
615054
+ }
615055
+ const id2 = `voicechat-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
615056
+ const state = { id: id2, rows: [row], expanded: false, colorCode };
615057
+ _voiceChatCoalesce = state;
615058
+ host.registerDynamicBlock(id2, (width) => {
615059
+ const visible = state.expanded ? state.rows : state.rows.slice(-VOICECHAT_VISIBLE_ROWS);
615060
+ const hidden = state.rows.length - visible.length;
615061
+ const body = [];
615062
+ if (!state.expanded && hidden > 0) {
615063
+ body.push({ text: `▸ ${hidden} ${VOICECHAT_EXPAND_LABEL}`, mode: "truncate", kind: "dim" });
615064
+ } else if (state.expanded && state.rows.length > VOICECHAT_VISIBLE_ROWS) {
615065
+ body.push({ text: `▾ ${VOICECHAT_COLLAPSE_LABEL}`, mode: "truncate", kind: "dim" });
615066
+ }
615067
+ for (const text2 of visible) {
615068
+ body.push({ text: text2, mode: "wrap", kind: "plain" });
615069
+ }
615070
+ return buildToolBoxLines(
615071
+ {
615072
+ title,
615073
+ metrics: state.rows.length > 2 ? `${Math.ceil(state.rows.length / 2)} turns` : "",
615074
+ body,
615075
+ colorCode: state.colorCode
615076
+ },
615077
+ width
615078
+ );
615079
+ });
615080
+ host.registerDynamicBlockClickHandler?.(id2, (event) => {
615081
+ const line = event.lineText ?? "";
615082
+ if (line.includes(VOICECHAT_EXPAND_LABEL) || line.includes(VOICECHAT_COLLAPSE_LABEL)) {
615083
+ state.expanded = !state.expanded;
615084
+ host.refreshDynamicBlocks?.();
615085
+ return true;
615086
+ }
615087
+ return false;
615088
+ });
615089
+ host.appendDynamicBlock(id2);
615090
+ }
615023
615091
  function appendContextBoxSection(body, label, value2) {
615024
615092
  const text2 = String(value2 ?? "").trim();
615025
615093
  if (!text2) return;
@@ -615893,7 +615961,7 @@ function formatDuration4(ms) {
615893
615961
  const secs = Math.floor(totalSecs % 60);
615894
615962
  return `${mins}m ${secs}s`;
615895
615963
  }
615896
- var c3, ui, pastel, _emojisEnabled, _colorsEnabled, MD, TOOL_LABELS, TOOL_COLOR_CODES, TOOL_ERROR_COLOR_CODE, BOX_TL2, BOX_TR2, BOX_BL2, BOX_BR2, BOX_H2, BOX_V2, BOX_TJ_L2, BOX_TJ_R2, RESET2, _telegramCoalesce, _pendingToolCall, DIFF_LINE_RE, _contentWriteHook, HINTS, TOOL_NAMES, COMMAND_NAMES, SLASH_COMMANDS2;
615964
+ var c3, ui, pastel, _emojisEnabled, _colorsEnabled, MD, TOOL_LABELS, TOOL_COLOR_CODES, TOOL_ERROR_COLOR_CODE, BOX_TL2, BOX_TR2, BOX_BL2, BOX_BR2, BOX_H2, BOX_V2, BOX_TJ_L2, BOX_TJ_R2, RESET2, _telegramCoalesce, _pendingToolCall, _voiceChatCoalesce, VOICECHAT_VISIBLE_ROWS, VOICECHAT_EXPAND_LABEL, VOICECHAT_COLLAPSE_LABEL, DIFF_LINE_RE, _contentWriteHook, HINTS, TOOL_NAMES, COMMAND_NAMES, SLASH_COMMANDS2;
615897
615965
  var init_render = __esm({
615898
615966
  "packages/cli/src/tui/render.ts"() {
615899
615967
  "use strict";
@@ -616054,6 +616122,10 @@ var init_render = __esm({
616054
616122
  RESET2 = "\x1B[0m";
616055
616123
  _telegramCoalesce = null;
616056
616124
  _pendingToolCall = null;
616125
+ _voiceChatCoalesce = null;
616126
+ VOICECHAT_VISIBLE_ROWS = 20;
616127
+ VOICECHAT_EXPAND_LABEL = "earlier turns — click to read full transcript";
616128
+ VOICECHAT_COLLAPSE_LABEL = "full transcript — click to show recent only";
616057
616129
  DIFF_LINE_RE = /^\s*\d+\s([-+])\s/;
616058
616130
  _contentWriteHook = null;
616059
616131
  HINTS = [
@@ -696639,11 +696711,25 @@ function buildRealtimeVoiceMessages(turns, maxMessages = MAX_REALTIME_MODEL_MESS
696639
696711
  dynamic.push(turn);
696640
696712
  }
696641
696713
  }
696642
- if (latestSnapshot) dynamic.push(latestSnapshot);
696714
+ if (latestSnapshot) {
696715
+ let lastUserIdx = -1;
696716
+ for (let i2 = dynamic.length - 1; i2 >= 0; i2--) {
696717
+ if (dynamic[i2].role === "user") {
696718
+ lastUserIdx = i2;
696719
+ break;
696720
+ }
696721
+ }
696722
+ if (lastUserIdx >= 0) dynamic.splice(lastUserIdx, 0, latestSnapshot);
696723
+ else dynamic.push(latestSnapshot);
696724
+ }
696643
696725
  const available = Math.max(0, maxMessages - pinned.length);
696644
- return [...pinned, ...dynamic.slice(-available)];
696726
+ let windowed = dynamic.slice(-available);
696727
+ if (latestSnapshot && available > 0 && !windowed.includes(latestSnapshot)) {
696728
+ windowed = [latestSnapshot, ...windowed.slice(-(available - 1))];
696729
+ }
696730
+ return [...pinned, ...windowed];
696645
696731
  }
696646
- var VAD_SILENCE_MS, MAX_SEGMENT_MS, SUMMARY_INJECTION_INTERVAL, MAX_CONTEXT_TURNS, MAX_REALTIME_MODEL_MESSAGES, CONTEXT_SNAPSHOT_PREFIX, MAX_VOICE_REPLY_CHARS, AGENT_ECHO_WINDOW_MS, AGENT_ECHO_SIMILARITY, SYSTEM_PROMPT2, MIN_SIGNAL_SCORE, NOISE_ONLY_RE, VoiceChatSession;
696732
+ var VAD_SILENCE_MS, MAX_SEGMENT_MS, SUMMARY_INJECTION_INTERVAL, MAX_CONTEXT_TURNS, MAX_REALTIME_MODEL_MESSAGES, CONTEXT_SNAPSHOT_PREFIX, CONTEXT_SNAPSHOT_SCOPE, MAX_VOICE_REPLY_CHARS, AGENT_ECHO_WINDOW_MS, AGENT_ECHO_SIMILARITY, SYSTEM_PROMPT2, MIN_SIGNAL_SCORE, NOISE_ONLY_RE, VoiceChatSession;
696647
696733
  var init_voicechat = __esm({
696648
696734
  "packages/cli/src/tui/voicechat.ts"() {
696649
696735
  "use strict";
@@ -696652,14 +696738,17 @@ var init_voicechat = __esm({
696652
696738
  SUMMARY_INJECTION_INTERVAL = 4;
696653
696739
  MAX_CONTEXT_TURNS = 20;
696654
696740
  MAX_REALTIME_MODEL_MESSAGES = 16;
696655
- CONTEXT_SNAPSHOT_PREFIX = "Context snapshot (read-only):";
696741
+ CONTEXT_SNAPSHOT_PREFIX = "[ACTIVE CONTEXT FRAME]";
696742
+ CONTEXT_SNAPSHOT_SCOPE = "Scope: live session + perception state for your next reply. Treat this as the single merged context intake — background state, not a topic. Answer the user's latest message; do not narrate or re-emit this frame unless the user asks about it.";
696656
696743
  MAX_VOICE_REPLY_CHARS = 700;
696657
696744
  AGENT_ECHO_WINDOW_MS = 12e3;
696658
696745
  AGENT_ECHO_SIMILARITY = 0.72;
696659
696746
  SYSTEM_PROMPT2 = `You are a voice assistant having a live spoken conversation. Keep responses extremely brief — 1-2 sentences max. You're speaking aloud, not writing. Be conversational, direct, and helpful. Don't use markdown or formatting — just natural speech.
696660
696747
 
696661
696748
  Rules:
696662
- - Live perception: each turn you may receive a read-only "Context snapshot" system message containing what the camera currently sees (objects, people, classifications, recent visual events) and what is currently heard (sound scene, recent sounds, transcripts). Treat it as your own live sight and hearing. When the user asks what you see, who is there, or what you hear, answer directly from that snapshot. If the snapshot is missing or stale, say you can't see/hear clearly right now — do not invent observations.
696749
+ - ALWAYS answer the user's most recent message directly. The live context snapshot is background perception never the topic. Do not describe cameras, feeds, or surroundings unless the user's latest message asks about them.
696750
+ - Never repeat your previous answer. If you already described the scene, don't describe it again unless explicitly asked again — respond to what the user just said.
696751
+ - Live perception: each turn you may receive a read-only "[ACTIVE CONTEXT FRAME]" system message containing what the camera currently sees (objects, people, classifications, recent visual events) and what is currently heard (sound scene, recent sounds, transcripts). Treat it as your own live sight and hearing. When the user asks what you SEE, answer from the camera fields; when the user asks what you HEAR, answer from the audio/sound fields. If the snapshot is missing or stale, say you can't see/hear clearly right now — do not invent observations.
696663
696752
  - Never invent environment facts (cwd, OS, specs, repo state). If you need a precise fact from the main agent, request a tool by outputting on a single line EXACTLY one JSON object: {"tool": string, "args": object} and nothing else. Then wait for the tool result before answering.
696664
696753
  - You may also request to relay a user task to the main agent by emitting {"tool":"voice_to_main","args":{"message":"...","start":true}}.
696665
696754
  - Prefer tools for factual queries; otherwise, answer directly with a short reply.
@@ -696948,8 +697037,14 @@ Rules:
696948
697037
  const snap = await Promise.resolve(this.toolRelay.contextSnapshot());
696949
697038
  if (snap && snap.trim()) {
696950
697039
  this.dropTransientSystemTurns();
696951
- this.context.push({ role: "system", content: `Context snapshot (read-only):
696952
- ${snap.trim()}` });
697040
+ this.context.push({
697041
+ role: "system",
697042
+ content: `${CONTEXT_SNAPSHOT_PREFIX}
697043
+ turn: ${this.turnCount}
697044
+ ${CONTEXT_SNAPSHOT_SCOPE}
697045
+
697046
+ ${snap.trim()}`
697047
+ });
696953
697048
  this.trimContext();
696954
697049
  }
696955
697050
  } catch {
@@ -697015,6 +697110,19 @@ ${toolOutput}` });
697015
697110
  this.context.push({ role: "system", content: `You have tools. Use them. ${this.toolCatalogNote}` });
697016
697111
  response = await this.streamOllamaInference(this.abortController.signal);
697017
697112
  }
697113
+ if (this.lastAgentSpeech?.text && response.trim()) {
697114
+ const prevTokens = comparableTokens(this.lastAgentSpeech.text);
697115
+ const nextTokens = comparableTokens(stripToolJsonLines(response));
697116
+ const similarity3 = jaccardSimilarity(prevTokens, nextTokens);
697117
+ if (similarity3 >= 0.8 && nextTokens.length >= 8) {
697118
+ this.context.push({
697119
+ role: "system",
697120
+ content: `Your draft reply repeats your previous answer. Do NOT describe the scene or cameras again. Respond directly and briefly to the user's latest message: "${truncateForLog(lastUser ?? "", 200)}"`
697121
+ });
697122
+ const retry = await this.streamOllamaInference(this.abortController.signal);
697123
+ if (retry.trim()) response = retry;
697124
+ }
697125
+ }
697018
697126
  if (response.trim()) {
697019
697127
  const reply = extractVoiceModelReply(stripToolJsonLines(response.trim()));
697020
697128
  const finalSpoken = reply.text;
@@ -736377,14 +736485,16 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
736377
736485
  writeContent(() => renderInfo(`[voicechat] ${msg}`));
736378
736486
  },
736379
736487
  onUserSpeech(text2) {
736380
- writeContent(() => renderInfo(`\x1B[38;5;45m[you]\x1B[0m ${text2}`));
736488
+ writeContent(
736489
+ () => renderVoiceChatCoalescedRow(`\x1B[38;5;45m[you]\x1B[0m ${text2}`)
736490
+ );
736381
736491
  },
736382
736492
  // Suppressed to keep main loop quiet
736383
736493
  onPartialTranscript(_text) {
736384
736494
  },
736385
736495
  onAgentSpeech(text2) {
736386
736496
  writeContent(
736387
- () => renderInfo(
736497
+ () => renderVoiceChatCoalescedRow(
736388
736498
  `\x1B[38;5;37m[agent]\x1B[0m ${truncateByLines(text2, 5, 600)}`
736389
736499
  )
736390
736500
  );
@@ -736400,6 +736510,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
736400
736510
  await _voiceChatSession2.stop();
736401
736511
  _voiceChatSession2 = null;
736402
736512
  }
736513
+ breakVoiceChatCoalesce();
736403
736514
  },
736404
736515
  isVoiceChatActive() {
736405
736516
  return _voiceChatSession2?.isActive ?? false;
@@ -129,6 +129,44 @@ import numpy as np
129
129
  # Main transcription loop
130
130
  # ---------------------------------------------------------------------------
131
131
 
132
+ def _norm_tokens(text):
133
+ """Normalize a transcript for cross-model comparison: lowercase, strip
134
+ punctuation, collapse whitespace, drop empty tokens."""
135
+ import re
136
+ return [t for t in re.sub(r"[^\w\s]", " ", text.lower()).split() if t]
137
+
138
+
139
+ def _texts_agree(a: str, b: str, threshold: float) -> bool:
140
+ """Consensus check between two model transcripts. Real speech produces
141
+ similar output from different whisper models; hallucinations on noise are
142
+ high-variance and virtually never match across models."""
143
+ ta, tb = _norm_tokens(a), _norm_tokens(b)
144
+ if not ta or not tb:
145
+ return False
146
+ # Containment: short utterances often differ only by dropped filler words
147
+ ja, jb = " ".join(ta), " ".join(tb)
148
+ if ja in jb or jb in ja:
149
+ return True
150
+ import difflib
151
+ return difflib.SequenceMatcher(None, ta, tb).ratio() >= threshold
152
+
153
+
154
+ def _segment_filtered_text(result) -> str:
155
+ """Rebuild transcript from segments, dropping low-confidence and
156
+ likely-non-speech segments (classic hallucination carriers)."""
157
+ segments = result.get("segments") or []
158
+ if not segments:
159
+ return result.get("text", "").strip()
160
+ kept = []
161
+ for seg in segments:
162
+ if float(seg.get("no_speech_prob", 0.0)) > 0.6:
163
+ continue
164
+ if float(seg.get("avg_logprob", 0.0)) < -1.2:
165
+ continue
166
+ kept.append(str(seg.get("text", "")))
167
+ return " ".join(part.strip() for part in kept if part.strip()).strip()
168
+
169
+
132
170
  def main():
133
171
  import argparse
134
172
  parser = argparse.ArgumentParser(description="Live Whisper transcription worker")
@@ -136,6 +174,10 @@ def main():
136
174
  parser.add_argument("--chunk-seconds", type=float, default=CHUNK_SECONDS, help="Transcribe interval")
137
175
  parser.add_argument("--window-seconds", type=float, default=WINDOW_SECONDS, help="Sliding window size")
138
176
  parser.add_argument("--language", default=None, help="Language code (e.g. en, es, fr). Auto-detect if omitted.")
177
+ parser.add_argument("--consensus-model", default="tiny",
178
+ help="Second whisper model run in parallel on the same audio; transcripts are only emitted when both models agree. 'off' disables.")
179
+ parser.add_argument("--consensus-threshold", type=float, default=0.55,
180
+ help="Token similarity (0-1) required between the two models' transcripts.")
139
181
  args = parser.parse_args()
140
182
 
141
183
  import whisper
@@ -156,6 +198,19 @@ def main():
156
198
  emit_error(f"Failed to load model: {e}")
157
199
  sys.exit(1)
158
200
 
201
+ # Consensus model: a second (usually smaller) model transcribes the same
202
+ # window in parallel. Hallucinations (mixed-language garbage on noisy or
203
+ # quiet audio) don't reproduce across models, so disagreement = reject.
204
+ consensus_model = None
205
+ consensus_name = (args.consensus_model or "off").strip().lower()
206
+ if consensus_name not in ("off", "none", "", args.model):
207
+ emit_status(f"Loading consensus Whisper {consensus_name} model...")
208
+ try:
209
+ consensus_model = whisper.load_model(consensus_name, device=device)
210
+ except Exception as e:
211
+ emit_status(f"Consensus model unavailable ({e}); running single-model.")
212
+ consensus_model = None
213
+
159
214
  emit({"type": "ready"})
160
215
 
161
216
  # Audio buffer — accumulates PCM16 as float32 @ 16kHz
@@ -235,17 +290,48 @@ def main():
235
290
  # Amplify toward full scale before transcription
236
291
  window = amplify(window)
237
292
 
238
- # Transcribe
293
+ # Transcribe — primary and consensus models in parallel threads
294
+ # (torch releases the GIL during compute, so this is real
295
+ # parallelism on multicore ARM).
239
296
  try:
240
297
  fp16 = (device == "cuda")
241
- result = model.transcribe(
242
- window,
243
- fp16=fp16,
244
- language=args.language,
245
- no_speech_threshold=0.6,
246
- condition_on_previous_text=False,
247
- )
248
- text = result.get("text", "").strip()
298
+
299
+ def run_transcribe(m):
300
+ return m.transcribe(
301
+ window,
302
+ fp16=fp16,
303
+ language=args.language,
304
+ no_speech_threshold=0.6,
305
+ condition_on_previous_text=False,
306
+ )
307
+
308
+ if consensus_model is not None:
309
+ from concurrent.futures import ThreadPoolExecutor
310
+ with ThreadPoolExecutor(max_workers=2) as pool:
311
+ primary_future = pool.submit(run_transcribe, model)
312
+ secondary_future = pool.submit(run_transcribe, consensus_model)
313
+ result = primary_future.result()
314
+ secondary = secondary_future.result()
315
+ else:
316
+ result = run_transcribe(model)
317
+ secondary = None
318
+
319
+ text = _segment_filtered_text(result)
320
+
321
+ if text and secondary is not None:
322
+ secondary_text = _segment_filtered_text(secondary)
323
+ lang_a = str(result.get("language", "")).lower()
324
+ lang_b = str(secondary.get("language", "")).lower()
325
+ lang_disagree = bool(lang_a and lang_b and lang_a != lang_b and not args.language)
326
+ if lang_disagree or not _texts_agree(text, secondary_text, args.consensus_threshold):
327
+ emit({
328
+ "type": "consensus_rejected",
329
+ "primary": text[:200],
330
+ "secondary": (secondary_text or "")[:200],
331
+ "reason": "language_mismatch" if lang_disagree else "low_similarity",
332
+ })
333
+ continue
334
+
249
335
  if text and text != last_text:
250
336
  last_text = text
251
337
  emit_transcript(text, is_final=False)
@@ -270,7 +356,7 @@ def main():
270
356
  fp16=fp16,
271
357
  language=args.language,
272
358
  )
273
- text = result.get("text", "").strip()
359
+ text = _segment_filtered_text(result)
274
360
  if text:
275
361
  emit_transcript(text, is_final=True)
276
362
  except Exception:
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.419",
3
+ "version": "1.0.421",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omnius",
9
- "version": "1.0.419",
9
+ "version": "1.0.421",
10
10
  "bundleDependencies": [
11
11
  "image-to-ascii"
12
12
  ],
@@ -2505,9 +2505,9 @@
2505
2505
  }
2506
2506
  },
2507
2507
  "node_modules/bare-fs": {
2508
- "version": "4.7.2",
2509
- "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz",
2510
- "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==",
2508
+ "version": "4.7.3",
2509
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.3.tgz",
2510
+ "integrity": "sha512-xRgplks8SvcKkdlv2M6Z2LZmRsmqd+x0nXXGXeMEjwdibj1HSDrlnqBRLeYdMvsgCox7Bq0e+DHwfczOfsn6IA==",
2511
2511
  "license": "Apache-2.0",
2512
2512
  "optional": true,
2513
2513
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.419",
3
+ "version": "1.0.421",
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/index.js",