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 +125 -14
- package/dist/scripts/live-whisper.py +96 -10
- package/npm-shrinkwrap.json +5 -5
- package/package.json +1 -1
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)
|
|
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
|
-
|
|
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 = "
|
|
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
|
-
-
|
|
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({
|
|
696952
|
-
|
|
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(
|
|
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
|
-
() =>
|
|
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
|
-
|
|
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.421",
|
|
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.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.
|
|
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