omnius 1.0.434 → 1.0.436
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 +916 -125
- package/dist/scripts/live-whisper.py +101 -46
- package/dist/scripts/transcribe-file.py +18 -8
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnius",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.436",
|
|
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.436",
|
|
10
10
|
"bundleDependencies": [
|
|
11
11
|
"image-to-ascii"
|
|
12
12
|
],
|
package/package.json
CHANGED