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.
- package/dist/index.js +2029 -1563
- package/dist/scripts/live-whisper.py +96 -10
- package/npm-shrinkwrap.json +5 -5
- package/package.json +1 -1
|
@@ -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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
359
|
+
text = _segment_filtered_text(result)
|
|
274
360
|
if text:
|
|
275
361
|
emit_transcript(text, is_final=True)
|
|
276
362
|
except Exception:
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnius",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
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.
|
|
2509
|
-
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.
|
|
2510
|
-
"integrity": "sha512-
|
|
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