omnius 1.0.420 → 1.0.422

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.
@@ -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.420",
3
+ "version": "1.0.422",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omnius",
9
- "version": "1.0.420",
9
+ "version": "1.0.422",
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.420",
3
+ "version": "1.0.422",
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",