omnius 1.0.435 → 1.0.437

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.
@@ -85,6 +85,33 @@ def emit_transcript(text: str, is_final: bool = False, doa=None, consensus=None)
85
85
  emit(event)
86
86
 
87
87
 
88
+ def emit_consensus_rejected(primary: str, secondary: str, reason: str):
89
+ emit({
90
+ "type": "consensus_rejected",
91
+ "primary": primary[:200],
92
+ "secondary": (secondary or "")[:200],
93
+ "reason": reason,
94
+ })
95
+
96
+
97
+ def cuda_memory_reserved_mb(device: str) -> int:
98
+ if not str(device).startswith("cuda"):
99
+ return 0
100
+ try:
101
+ import torch
102
+ raw = str(device).split(":", 1)[1] if ":" in str(device) else ""
103
+ index = int(raw) if raw.strip().isdigit() else torch.cuda.current_device()
104
+ try:
105
+ torch.cuda.synchronize(index)
106
+ except Exception:
107
+ pass
108
+ reserved = int(torch.cuda.memory_reserved(index))
109
+ allocated = int(torch.cuda.memory_allocated(index))
110
+ return max(0, round(max(reserved, allocated) / (1024 * 1024)))
111
+ except Exception:
112
+ return 0
113
+
114
+
88
115
  def host_cuda_hardware_present() -> bool:
89
116
  """Best-effort CUDA hardware hint, including Jetson/Orin nodes."""
90
117
  for path in (
@@ -100,6 +127,32 @@ def host_cuda_hardware_present() -> bool:
100
127
  return bool(visible and visible not in ("none", "void", "no"))
101
128
 
102
129
 
130
+ def asr_cpu_allowed() -> bool:
131
+ raw = os.environ.get("OMNIUS_ASR_ALLOW_CPU", "").strip().lower()
132
+ return raw in ("1", "true", "yes", "on")
133
+
134
+
135
+ def jetson_host_hint() -> str:
136
+ try:
137
+ model = Path("/proc/device-tree/model").read_text(errors="ignore").replace("\x00", " ").strip()
138
+ except Exception:
139
+ model = ""
140
+ if model and any(token in model.lower() for token in ("jetson", "orin", "tegra")):
141
+ return (
142
+ f" Detected {model}. Install a JetPack/L4T-compatible CUDA PyTorch wheel in the Omnius ASR venv; "
143
+ "the generic pip torch wheel is often CPU-only or CUDA-incompatible on Jetson."
144
+ )
145
+ return ""
146
+
147
+
148
+ def cuda_required_error(reason: str):
149
+ emit_error(
150
+ f"CUDA-only ASR is enabled and Whisper cannot use CUDA: {reason}."
151
+ f"{jetson_host_hint()} Set OMNIUS_ASR_ALLOW_CPU=1 only for explicit emergency CPU fallback."
152
+ )
153
+ sys.exit(1)
154
+
155
+
103
156
  def select_whisper_device() -> str:
104
157
  """Force Whisper onto CUDA whenever PyTorch can see a CUDA device.
105
158
 
@@ -110,9 +163,8 @@ def select_whisper_device() -> str:
110
163
  try:
111
164
  import torch
112
165
  except Exception as e:
113
- if host_cuda_hardware_present():
114
- emit_error(f"CUDA hardware appears present, but PyTorch could not be imported for CUDA Whisper ({e}); refusing CPU fallback.")
115
- sys.exit(1)
166
+ if host_cuda_hardware_present() or not asr_cpu_allowed():
167
+ cuda_required_error(f"PyTorch could not be imported ({e})")
116
168
  emit_status(f"PyTorch unavailable for CUDA probe ({e}); using CPU because CUDA is not available to Whisper.")
117
169
  return "cpu"
118
170
 
@@ -120,17 +172,15 @@ def select_whisper_device() -> str:
120
172
  cuda_count = int(torch.cuda.device_count())
121
173
  cuda_available = bool(torch.cuda.is_available() and cuda_count > 0)
122
174
  except Exception as e:
123
- if host_cuda_hardware_present():
124
- emit_error(f"CUDA hardware appears present, but PyTorch CUDA probe failed ({e}); refusing CPU fallback.")
125
- sys.exit(1)
175
+ if host_cuda_hardware_present() or not asr_cpu_allowed():
176
+ cuda_required_error(f"PyTorch CUDA probe failed ({e})")
126
177
  emit_status(f"CUDA probe failed ({e}); using CPU because CUDA is not available to Whisper.")
127
178
  return "cpu"
128
179
 
129
180
  if not cuda_available:
130
181
  cuda_version = getattr(getattr(torch, "version", None), "cuda", None)
131
- if host_cuda_hardware_present():
132
- emit_error(f"CUDA hardware appears present, but PyTorch reports CUDA unavailable (torch.version.cuda={cuda_version}, device_count={cuda_count}); refusing CPU fallback.")
133
- sys.exit(1)
182
+ if host_cuda_hardware_present() or not asr_cpu_allowed():
183
+ cuda_required_error(f"PyTorch reports CUDA unavailable (torch.version.cuda={cuda_version}, device_count={cuda_count})")
134
184
  emit_status(f"CUDA unavailable to PyTorch (torch.version.cuda={cuda_version}, device_count={cuda_count}); using CPU fallback.")
135
185
  return "cpu"
136
186
 
@@ -151,7 +201,7 @@ def select_whisper_device() -> str:
151
201
  emit_error(f"CUDA is available but cuda:{device_index} could not be selected ({e}); refusing CPU fallback.")
152
202
  sys.exit(1)
153
203
 
154
- emit_status(f"CUDA available; forcing Whisper onto cuda:{device_index} ({name}).")
204
+ emit_status(f"CUDA available; forcing Whisper onto cuda:{device_index} ({name}); torch={getattr(torch, '__version__', 'unknown')} cuda={getattr(getattr(torch, 'version', None), 'cuda', None)}.")
155
205
  return f"cuda:{device_index}"
156
206
 
157
207
  # ---------------------------------------------------------------------------
@@ -442,9 +492,15 @@ def main():
442
492
  if tuning is not None:
443
493
  emit_status("ReSpeaker DSP active: hardware VAD, echo cancellation, DOA.")
444
494
 
445
- emit({"type": "ready"})
446
-
447
495
  fp16 = str(device).startswith("cuda")
496
+ emit({
497
+ "type": "ready",
498
+ "model": args.model,
499
+ "device": str(device),
500
+ "cuda": bool(fp16),
501
+ "consensusModel": consensus_name if consensus_model is not None else None,
502
+ "vramUsedMB": cuda_memory_reserved_mb(str(device)),
503
+ })
448
504
 
449
505
  def run_transcribe(m, samples):
450
506
  return m.transcribe(
@@ -525,47 +581,46 @@ def main():
525
581
  return
526
582
  samples = amplify(samples)
527
583
  try:
528
- if consensus_model is not None:
529
- from concurrent.futures import ThreadPoolExecutor
530
- with ThreadPoolExecutor(max_workers=2) as pool:
531
- primary_future = pool.submit(run_transcribe, model, samples)
532
- secondary_future = pool.submit(run_transcribe, consensus_model, samples)
533
- result = primary_future.result()
534
- secondary = secondary_future.result()
535
- else:
536
- result = run_transcribe(model, samples)
537
- secondary = None
584
+ result = run_transcribe(model, samples)
538
585
 
539
586
  text = _segment_filtered_text(result)
540
587
  if not text:
541
588
  emit_status("Whisper final produced no accepted speech text.")
542
589
  return
543
- if secondary is not None:
544
- secondary_text = _segment_filtered_text(secondary)
545
- lang_a = str(result.get("language", "")).lower()
546
- lang_b = str(secondary.get("language", "")).lower()
547
- lang_disagree = bool(lang_a and lang_b and lang_a != lang_b and not args.language)
548
- if lang_disagree or not _texts_agree(text, secondary_text, args.consensus_threshold):
549
- emit({
550
- "type": "consensus_rejected",
551
- "primary": text[:200],
552
- "secondary": (secondary_text or "")[:200],
553
- "reason": "language_mismatch" if lang_disagree else "low_similarity",
554
- })
555
- return
556
590
  if text == last_final_text:
557
591
  return
558
- last_final_text = text
559
- # consensus=True only when BOTH models ran and converged. When the
560
- # second model is off/unavailable, omit consensus metadata entirely
561
- # so legacy single-model consumers do not interpret consensus=false
562
- # as an explicit dual-ASR rejection.
563
- emit_transcript(
564
- text,
565
- is_final=True,
566
- doa=tuning.doa() if tuning else None,
567
- consensus=True if consensus_model is not None else None,
568
- )
592
+ doa = tuning.doa() if tuning else None
593
+ if consensus_model is None:
594
+ last_final_text = text
595
+ emit_transcript(text, is_final=True, doa=doa, consensus=None)
596
+ return
597
+
598
+ # Fast UX path: surface the primary model's finalized text
599
+ # immediately as a non-final transcript. The secondary model then
600
+ # validates whether it is safe to accept as a final utterance.
601
+ emit_transcript(text, is_final=False, doa=doa, consensus=False)
602
+
603
+ def validate_consensus(primary_text, primary_result, audio_samples, primary_doa):
604
+ nonlocal last_final_text
605
+ try:
606
+ secondary = run_transcribe(consensus_model, audio_samples)
607
+ secondary_text = _segment_filtered_text(secondary)
608
+ lang_a = str(primary_result.get("language", "")).lower()
609
+ lang_b = str(secondary.get("language", "")).lower()
610
+ lang_disagree = bool(lang_a and lang_b and lang_a != lang_b and not args.language)
611
+ if lang_disagree or not _texts_agree(primary_text, secondary_text, args.consensus_threshold):
612
+ emit_consensus_rejected(primary_text, secondary_text, "language_mismatch" if lang_disagree else "low_similarity")
613
+ return
614
+ last_final_text = primary_text
615
+ emit_transcript(primary_text, is_final=True, doa=primary_doa, consensus=True)
616
+ except Exception as e:
617
+ emit_error(f"Consensus validation error: {e}")
618
+
619
+ threading.Thread(
620
+ target=validate_consensus,
621
+ args=(text, result, samples.copy(), doa),
622
+ daemon=True,
623
+ ).start()
569
624
  except Exception as e:
570
625
  emit_error(f"Transcription error: {e}")
571
626
 
@@ -1,20 +1,16 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Transcribe an audio file using openai-whisper (CPU-only).
3
+ Transcribe an audio file using openai-whisper (CUDA-only by default).
4
4
 
5
5
  stdin: JSON {"path": "...", "model": "tiny|base|small|medium|large-v3"}
6
6
  stdout: JSON {"text": "...", "duration": null, "segments": []}
7
7
  or {"error": "<kind>", "message": "..."}
8
8
 
9
- Force-loads on CPU because the venv's torch may not match the local GPU
10
- (e.g. a GT 1030 with sm_61 against a sm_75+ torch build) — those mismatches
11
- print noisy CUDA warnings and then crash. CPU is slower but always works.
9
+ ASR is GPU-first: CPU fallback is refused unless OMNIUS_ASR_ALLOW_CPU=1.
12
10
  """
13
11
  import sys, os, json, warnings
14
12
 
15
- # Quiet CUDA warnings from torch even though we won't use the GPU.
16
13
  warnings.filterwarnings("ignore")
17
- os.environ.setdefault("CUDA_VISIBLE_DEVICES", "") # hide all GPUs
18
14
  os.environ.setdefault("PYTHONWARNINGS", "ignore")
19
15
 
20
16
  try:
@@ -39,8 +35,22 @@ def main() -> None:
39
35
 
40
36
  model_name = data.get("model") or "tiny"
41
37
  try:
42
- model = whisper.load_model(model_name, device="cpu")
43
- result = model.transcribe(path, fp16=False)
38
+ device = os.environ.get("OMNIUS_ASR_DEVICE", "cuda").strip().lower() or "cuda"
39
+ allow_cpu = os.environ.get("OMNIUS_ASR_ALLOW_CPU", "").strip().lower() in ("1", "true", "yes", "on")
40
+ if device == "auto":
41
+ device = "cuda"
42
+ if device.startswith("cuda"):
43
+ import torch
44
+ if not torch.cuda.is_available() or torch.cuda.device_count() <= 0:
45
+ raise RuntimeError(
46
+ f"CUDA-only ASR requested but torch cannot use CUDA "
47
+ f"(torch.version.cuda={getattr(torch.version, 'cuda', None)}, device_count={torch.cuda.device_count()}). "
48
+ "Install a CUDA-enabled PyTorch build for this device, or set OMNIUS_ASR_ALLOW_CPU=1 only for emergency fallback."
49
+ )
50
+ elif device == "cpu" and not allow_cpu:
51
+ raise RuntimeError("CPU ASR refused. Set OMNIUS_ASR_ALLOW_CPU=1 only for explicit emergency fallback.")
52
+ model = whisper.load_model(model_name, device=device)
53
+ result = model.transcribe(path, fp16=device.startswith("cuda"), condition_on_previous_text=False)
44
54
  except Exception as e:
45
55
  print(json.dumps({"error": "whisper_failed", "message": str(e)}))
46
56
  return
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.435",
3
+ "version": "1.0.437",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omnius",
9
- "version": "1.0.435",
9
+ "version": "1.0.437",
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.435",
3
+ "version": "1.0.437",
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",