omnius 1.0.426 → 1.0.428

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 CHANGED
@@ -603384,6 +603384,15 @@ async function findLiveWhisperScript() {
603384
603384
  }
603385
603385
  return null;
603386
603386
  }
603387
+ function defaultConsensusModel(primaryModel) {
603388
+ const model = primaryModel.trim().toLowerCase();
603389
+ if (model === "tiny") return "base";
603390
+ if (model === "base") return "small";
603391
+ if (model === "small") return "base";
603392
+ if (model === "medium") return "base";
603393
+ if (model === "large-v3" || model === "large") return "small";
603394
+ return "base";
603395
+ }
603387
603396
  async function ensureVenvForTranscribeCli() {
603388
603397
  const bin = process.platform === "win32" ? "Scripts" : "bin";
603389
603398
  const exe = process.platform === "win32" ? "python.exe" : "python3";
@@ -603573,7 +603582,9 @@ var init_listen = __esm({
603573
603582
  lastStatus: `loading whisper ${this.model} model...`
603574
603583
  });
603575
603584
  const consensusEnv = String(process.env["OMNIUS_ASR_CONSENSUS"] ?? "").trim().toLowerCase();
603576
- const consensusModel = consensusEnv === "0" || consensusEnv === "off" || consensusEnv === "false" ? "off" : consensusEnv || (this.model === "tiny" ? "base" : "tiny");
603585
+ const consensusModel = consensusEnv === "0" || consensusEnv === "off" || consensusEnv === "false" ? "off" : consensusEnv || defaultConsensusModel(this.model);
603586
+ const consensusThreshold = String(process.env["OMNIUS_ASR_CONSENSUS_THRESHOLD"] ?? "0.45");
603587
+ const silenceMs = String(process.env["OMNIUS_ASR_SILENCE_MS"] ?? "800");
603577
603588
  this.process = spawn29(
603578
603589
  pyPath,
603579
603590
  [
@@ -603585,7 +603596,11 @@ var init_listen = __esm({
603585
603596
  "--window-seconds",
603586
603597
  "10",
603587
603598
  "--consensus-model",
603588
- consensusModel
603599
+ consensusModel,
603600
+ "--consensus-threshold",
603601
+ consensusThreshold,
603602
+ "--silence-ms",
603603
+ silenceMs
603589
603604
  ],
603590
603605
  {
603591
603606
  stdio: ["pipe", "pipe", "pipe"],
@@ -603619,13 +603634,19 @@ var init_listen = __esm({
603619
603634
  case "transcript":
603620
603635
  this.emit("transcript", {
603621
603636
  text: evt.text,
603622
- isFinal: evt.isFinal ?? false
603637
+ isFinal: evt.isFinal ?? false,
603638
+ consensus: typeof evt.consensus === "boolean" ? evt.consensus : void 0,
603639
+ doa: typeof evt.doa === "number" ? evt.doa : void 0
603623
603640
  });
603624
603641
  break;
603625
603642
  case "consensus_rejected":
603626
- updateListenLiveState({
603627
- lastStatus: `consensus rejected (${evt.reason ?? "mismatch"}): ${String(evt.primary ?? "").slice(0, 80)}`
603628
- });
603643
+ {
603644
+ const message2 = `consensus rejected (${evt.reason ?? "mismatch"}): ${String(evt.primary ?? "").slice(0, 80)}`;
603645
+ updateListenLiveState({
603646
+ lastStatus: message2
603647
+ });
603648
+ this.emit("status", message2);
603649
+ }
603629
603650
  break;
603630
603651
  case "vad": {
603631
603652
  lastHardwareVadAt = Date.now();
@@ -604016,7 +604037,7 @@ var init_listen = __esm({
604016
604037
  this.lastTranscriptTime = Date.now();
604017
604038
  this.pendingText = evt.text.trim();
604018
604039
  updateListenLiveState({ lastTranscript: this.pendingText, lastTranscriptAt: this.lastTranscriptTime });
604019
- this.emit("transcript", this.pendingText, evt.isFinal);
604040
+ this.emit("transcript", this.pendingText, evt.isFinal, { consensus: evt.consensus, doa: evt.doa });
604020
604041
  if (this.config.mode === "auto") this.resetSilenceTimer();
604021
604042
  });
604022
604043
  fallback.on("error", (err) => {
@@ -604031,8 +604052,9 @@ transcribe-cli error: ${transcribeCliError}` : "";
604031
604052
  return `Failed to start live transcription: ${msg}${tcHint}`;
604032
604053
  }
604033
604054
  }
604034
- this.spawnMicProcess(micCmd);
604035
604055
  this.active = true;
604056
+ this.paused = false;
604057
+ this.spawnMicProcess(micCmd);
604036
604058
  this.blinkState = true;
604037
604059
  this.blinkTimer = setInterval(() => {
604038
604060
  this.blinkState = !this.blinkState;
@@ -617229,6 +617251,25 @@ function rms(arr) {
617229
617251
  return Math.sqrt(sum / arr.length);
617230
617252
  }
617231
617253
 
617254
+ // Linear resampler: browsers frequently IGNORE the requested AudioContext
617255
+ // sample rate and run at the hardware rate (typically 48kHz). Sending those
617256
+ // frames as-if-16kHz made the audio unpack 3x slow/pitch-dropped server-side,
617257
+ // wrecking ASR. Always resample the actual context rate down to 16kHz.
617258
+ function resampleTo16k(input, srcRate) {
617259
+ if (srcRate === 16000) return input;
617260
+ const ratio = srcRate / 16000;
617261
+ const outLen = Math.floor(input.length / ratio);
617262
+ const out = new Float32Array(outLen);
617263
+ for (let i = 0; i < outLen; i++) {
617264
+ const pos = i * ratio;
617265
+ const i0 = Math.floor(pos);
617266
+ const i1 = Math.min(input.length - 1, i0 + 1);
617267
+ const frac = pos - i0;
617268
+ out[i] = input[i0] * (1 - frac) + input[i1] * frac;
617269
+ }
617270
+ return out;
617271
+ }
617272
+
617232
617273
  async function toggleMic() {
617233
617274
  if (micActive) { stopMic(); return; }
617234
617275
  try {
@@ -617237,17 +617278,19 @@ async function toggleMic() {
617237
617278
  audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true, noiseSuppression: true }
617238
617279
  });
617239
617280
  const source = micCtx.createMediaStreamSource(micStream);
617281
+ const actualRate = micCtx.sampleRate; // may be 48000 regardless of the request
617240
617282
  scriptProcessor = micCtx.createScriptProcessor(4096, 1, 1);
617241
617283
  scriptProcessor.onaudioprocess = (e) => {
617242
617284
  if (!micActive || !ws || ws.readyState !== 1) return;
617243
- const input = e.inputBuffer.getChannelData(0);
617285
+ const raw = e.inputBuffer.getChannelData(0);
617286
+ const input = resampleTo16k(raw, actualRate);
617244
617287
  const int16 = new Int16Array(input.length);
617245
617288
  for (let i = 0; i < input.length; i++) {
617246
617289
  const s = Math.max(-1, Math.min(1, input[i]));
617247
617290
  int16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
617248
617291
  }
617249
617292
  ws.send(int16.buffer);
617250
- micLevel = Math.min(1, rms(input) * 5);
617293
+ micLevel = Math.min(1, rms(raw) * 5);
617251
617294
  };
617252
617295
  source.connect(scriptProcessor);
617253
617296
  scriptProcessor.connect(micCtx.destination);
@@ -617256,7 +617299,8 @@ async function toggleMic() {
617256
617299
  micBtn.textContent = 'stop';
617257
617300
  micBtn.classList.add('active');
617258
617301
  hintEl.textContent = 'listening...';
617259
- ws.send(JSON.stringify({ type: 'mic_start', username: 'web-user' }));
617302
+ // Declare the post-resample rate (and the raw context rate for debugging)
617303
+ ws.send(JSON.stringify({ type: 'mic_start', username: 'web-user', sampleRate: 16000, contextRate: actualRate }));
617260
617304
  } catch (err) {
617261
617305
  hintEl.textContent = 'mic error: ' + err.message;
617262
617306
  }
@@ -617629,6 +617673,23 @@ connectPP();
617629
617673
  </body>
617630
617674
  </html>`;
617631
617675
  }
617676
+ function resamplePcm16Mono(pcm, srcRate, dstRate) {
617677
+ if (srcRate === dstRate || srcRate <= 0 || dstRate <= 0) return pcm;
617678
+ const srcSamples = Math.floor(pcm.length / 2);
617679
+ if (srcSamples === 0) return pcm;
617680
+ const ratio = srcRate / dstRate;
617681
+ const outSamples = Math.max(1, Math.floor(srcSamples / ratio));
617682
+ const out = Buffer.allocUnsafe(outSamples * 2);
617683
+ for (let i2 = 0; i2 < outSamples; i2++) {
617684
+ const pos = i2 * ratio;
617685
+ const i0 = Math.min(srcSamples - 1, Math.floor(pos));
617686
+ const i1 = Math.min(srcSamples - 1, i0 + 1);
617687
+ const frac = pos - i0;
617688
+ const sample = pcm.readInt16LE(i0 * 2) * (1 - frac) + pcm.readInt16LE(i1 * 2) * frac;
617689
+ out.writeInt16LE(Math.max(-32768, Math.min(32767, Math.round(sample))), i2 * 2);
617690
+ }
617691
+ return out;
617692
+ }
617632
617693
  function renderVoiceSessionStart(tunnelUrl) {
617633
617694
  renderBoxedBlock({
617634
617695
  title: "☁ Live Voice Session",
@@ -617936,17 +617997,26 @@ var init_voice_session = __esm({
617936
617997
  clearInterval(keepaliveInterval);
617937
617998
  }
617938
617999
  }, 1e4);
618000
+ let clientPcmRate = 16e3;
617939
618001
  ws.on("message", (data, isBinary) => {
617940
618002
  if (isBinary) {
617941
618003
  if (!this.ttsSpeaking) {
617942
- this.onUserAudio?.(Buffer.isBuffer(data) ? data : Buffer.from(data), clientId);
618004
+ let pcm = Buffer.isBuffer(data) ? data : Buffer.from(data);
618005
+ if (clientPcmRate !== 16e3 && clientPcmRate > 0) {
618006
+ pcm = resamplePcm16Mono(pcm, clientPcmRate, 16e3);
618007
+ }
618008
+ this.onUserAudio?.(pcm, clientId);
617943
618009
  }
617944
618010
  } else {
617945
618011
  try {
617946
618012
  const msg = JSON.parse(data.toString());
617947
- if (msg.type === "mic_start" && msg.username) {
617948
- const entry = this.state.connectedUsers.get(clientId);
617949
- if (entry) entry.username = msg.username;
618013
+ if (msg.type === "mic_start") {
618014
+ if (msg.username) {
618015
+ const entry = this.state.connectedUsers.get(clientId);
618016
+ if (entry) entry.username = msg.username;
618017
+ }
618018
+ const declared = Number(msg.sampleRate);
618019
+ clientPcmRate = Number.isFinite(declared) && declared >= 8e3 && declared <= 192e3 ? declared : 16e3;
617950
618020
  }
617951
618021
  } catch {
617952
618022
  }
@@ -697514,6 +697584,9 @@ Rules:
697514
697584
  // VAD segment capture
697515
697585
  captureBuffer = "";
697516
697586
  captureStartTime = 0;
697587
+ consensusMetadataSeen = false;
697588
+ consensusText = null;
697589
+ consensusSignalScore = null;
697517
697590
  silenceTimer = null;
697518
697591
  maxSegmentTimer = null;
697519
697592
  lastSignalScore = null;
@@ -697604,18 +697677,25 @@ Rules:
697604
697677
  let isFinal;
697605
697678
  let snr;
697606
697679
  let confidence2;
697680
+ let consensus;
697607
697681
  if (typeof args[0] === "object" && args[0] !== null) {
697608
697682
  const evt = args[0];
697609
697683
  text2 = evt.text ?? "";
697610
697684
  isFinal = evt.isFinal ?? false;
697611
697685
  snr = evt.snr;
697612
697686
  confidence2 = evt.confidence;
697687
+ consensus = typeof evt.consensus === "boolean" ? evt.consensus : void 0;
697613
697688
  } else {
697614
697689
  text2 = String(args[0] ?? "");
697615
697690
  isFinal = Boolean(args[1]);
697691
+ const meta = args[2];
697692
+ if (meta && typeof meta === "object") {
697693
+ const m2 = meta;
697694
+ if (typeof m2.consensus === "boolean") consensus = m2.consensus;
697695
+ }
697616
697696
  }
697617
697697
  if (!text2.trim()) return;
697618
- this.handleTranscript(text2.trim(), isFinal, snr, confidence2);
697698
+ this.handleTranscript(text2.trim(), isFinal, snr, confidence2, consensus);
697619
697699
  };
697620
697700
  this._onError = (err) => {
697621
697701
  const msg = err instanceof Error ? err.message : String(err);
@@ -697682,7 +697762,7 @@ Rules:
697682
697762
  // ---------------------------------------------------------------------------
697683
697763
  // Transcript handling — VAD-style segment capture (Voryn pattern)
697684
697764
  // ---------------------------------------------------------------------------
697685
- handleTranscript(text2, isFinal, snr, confidence2) {
697765
+ handleTranscript(text2, isFinal, snr, confidence2, consensus) {
697686
697766
  if (!this.active) return;
697687
697767
  if (this._state !== "LISTENING" && this._state !== "CAPTURING") {
697688
697768
  return;
@@ -697695,7 +697775,7 @@ Rules:
697695
697775
  }
697696
697776
  if (this._state === "LISTENING") {
697697
697777
  this.setState("CAPTURING");
697698
- this.captureBuffer = "";
697778
+ this.resetCaptureState();
697699
697779
  this.captureStartTime = Date.now();
697700
697780
  this.maxSegmentTimer = setTimeout(() => {
697701
697781
  if (this._state === "CAPTURING") {
@@ -697703,23 +697783,44 @@ Rules:
697703
697783
  }
697704
697784
  }, MAX_SEGMENT_MS);
697705
697785
  }
697706
- this.captureBuffer = text2;
697707
- this.lastSignalScore = typeof snr === "number" && !Number.isNaN(snr) ? clamp0116(snr) : computeSignalFromText(text2, confidence2);
697786
+ const signalScore = typeof snr === "number" && !Number.isNaN(snr) ? clamp0116(snr) : computeSignalFromText(text2, confidence2);
697787
+ this.lastSignalScore = signalScore;
697788
+ if (consensus !== void 0) {
697789
+ this.consensusMetadataSeen = true;
697790
+ if (consensus === true) {
697791
+ this.consensusText = text2;
697792
+ this.consensusSignalScore = signalScore;
697793
+ this.captureBuffer = text2;
697794
+ }
697795
+ } else {
697796
+ this.captureBuffer = text2;
697797
+ }
697708
697798
  this.emit("snr", { score: this.lastSignalScore });
697709
697799
  this.onPartialTranscript(text2);
697710
697800
  if (this.silenceTimer) clearTimeout(this.silenceTimer);
697711
- const waitMs = this._vadSilenceMs ?? VAD_SILENCE_MS;
697801
+ const waitMs = isFinal ? 0 : this._vadSilenceMs ?? VAD_SILENCE_MS;
697712
697802
  this.silenceTimer = setTimeout(() => {
697713
697803
  if (this._state === "CAPTURING") {
697714
697804
  this.finalizeSegment();
697715
697805
  }
697716
697806
  }, waitMs);
697717
697807
  }
697808
+ resetCaptureState() {
697809
+ this.captureBuffer = "";
697810
+ this.captureStartTime = 0;
697811
+ this.lastSignalScore = null;
697812
+ this.consensusMetadataSeen = false;
697813
+ this.consensusText = null;
697814
+ this.consensusSignalScore = null;
697815
+ }
697718
697816
  // ---------------------------------------------------------------------------
697719
697817
  // Segment finalization → Transcribing → Thinking → Speaking
697720
697818
  // ---------------------------------------------------------------------------
697721
697819
  finalizeSegment() {
697722
- const text2 = this.captureBuffer.trim();
697820
+ const rawText = this.captureBuffer.trim();
697821
+ const requiresConsensus = this.consensusMetadataSeen;
697822
+ const text2 = requiresConsensus ? (this.consensusText ?? "").trim() : rawText;
697823
+ const signalScore = requiresConsensus ? this.consensusSignalScore ?? this.lastSignalScore : this.lastSignalScore;
697723
697824
  if (this.silenceTimer) {
697724
697825
  clearTimeout(this.silenceTimer);
697725
697826
  this.silenceTimer = null;
@@ -697728,18 +697829,20 @@ Rules:
697728
697829
  clearTimeout(this.maxSegmentTimer);
697729
697830
  this.maxSegmentTimer = null;
697730
697831
  }
697731
- this.captureBuffer = "";
697832
+ this.resetCaptureState();
697732
697833
  if (!text2) {
697834
+ if (requiresConsensus && rawText) {
697835
+ if (this.debugSnr) this.onStatus(`Ignoring utterance without dual-ASR consensus: ${truncateForLog(rawText, 48)}`);
697836
+ this.emit("consensusFiltered", { text: rawText });
697837
+ }
697733
697838
  this.setState("LISTENING");
697734
697839
  return;
697735
697840
  }
697736
- const score = Math.min(this.lastSignalScore ?? 1, computeSignalFromText(text2));
697841
+ const score = Math.min(signalScore ?? 1, computeSignalFromText(text2));
697737
697842
  if (score < MIN_SIGNAL_SCORE || NOISE_ONLY_RE.test(text2)) {
697738
697843
  if (this.debugSnr) this.onStatus(`Ignoring low-signal utterance (SNR:${score.toFixed(2)}): ${truncateForLog(text2, 48)}`);
697739
697844
  this.emit("snrFiltered", { score, text: text2 });
697740
697845
  this.setState("LISTENING");
697741
- this.captureBuffer = "";
697742
- this.lastSignalScore = null;
697743
697846
  return;
697744
697847
  }
697745
697848
  this.setState("TRANSCRIBING");
@@ -697862,35 +697965,35 @@ ${toolOutput}` });
697862
697965
  const reply = extractVoiceModelReply(stripToolJsonLines(response.trim()));
697863
697966
  const finalSpoken = reply.text;
697864
697967
  if (!finalSpoken || reply.action === "silent") {
697865
- return;
697866
- }
697867
- if (typeof this.voice.waitUntilIdle === "function") {
697868
- try {
697869
- await this.voice.waitUntilIdle();
697870
- } catch {
697968
+ } else {
697969
+ if (typeof this.voice.waitUntilIdle === "function") {
697970
+ try {
697971
+ await this.voice.waitUntilIdle();
697972
+ } catch {
697973
+ }
697871
697974
  }
697872
- }
697873
- this.context.push({ role: "assistant", content: finalSpoken });
697874
- this.trimContext();
697875
- this.setState("SPEAKING");
697876
- this.onAgentSpeech(finalSpoken);
697877
- this.lastAgentSpeech = { text: finalSpoken, startedAt: Date.now() };
697878
- this.voice.speak(finalSpoken);
697879
- this.voiceTranscript.push({ role: "assistant", content: finalSpoken, ts: Date.now() });
697880
- if (this.runner && this.turnCount % SUMMARY_INJECTION_INTERVAL === 0) {
697881
- this.injectSummary();
697882
- }
697883
- if (typeof this.voice.waitUntilIdle === "function") {
697884
- try {
697885
- await this.voice.waitUntilIdle();
697886
- } catch {
697975
+ this.context.push({ role: "assistant", content: finalSpoken });
697976
+ this.trimContext();
697977
+ this.setState("SPEAKING");
697978
+ this.onAgentSpeech(finalSpoken);
697979
+ this.lastAgentSpeech = { text: finalSpoken, startedAt: Date.now() };
697980
+ this.voice.speak(finalSpoken);
697981
+ this.voiceTranscript.push({ role: "assistant", content: finalSpoken, ts: Date.now() });
697982
+ if (this.runner && this.turnCount % SUMMARY_INJECTION_INTERVAL === 0) {
697983
+ this.injectSummary();
697984
+ }
697985
+ if (typeof this.voice.waitUntilIdle === "function") {
697986
+ try {
697987
+ await this.voice.waitUntilIdle();
697988
+ } catch {
697989
+ }
697990
+ } else {
697991
+ const estimatedMs = Math.max(1500, finalSpoken.length / 5 * (6e4 / 150));
697992
+ await new Promise((r2) => setTimeout(r2, estimatedMs));
697993
+ }
697994
+ if (reply.action === "hangup") {
697995
+ this.active = false;
697887
697996
  }
697888
- } else {
697889
- const estimatedMs = Math.max(1500, finalSpoken.length / 5 * (6e4 / 150));
697890
- await new Promise((r2) => setTimeout(r2, estimatedMs));
697891
- }
697892
- if (reply.action === "hangup") {
697893
- this.active = false;
697894
697997
  }
697895
697998
  }
697896
697999
  } catch (err) {
@@ -737057,6 +737160,11 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
737057
737160
  listenEng.on("level", (evt) => {
737058
737161
  statusBar.setMicActivity(evt.speechActive, evt.levelDb);
737059
737162
  });
737163
+ listenEng.on("info", (msg) => {
737164
+ if (/consensus rejected|loading whisper|whisper model loaded/i.test(msg)) {
737165
+ writeContent(() => renderInfo(`[voicechat/asr] ${msg}`));
737166
+ }
737167
+ });
737060
737168
  const summaryRunner = {
737061
737169
  injectUserMessage(content) {
737062
737170
  if (activeTask?.runner) {
@@ -76,10 +76,12 @@ def emit_error(msg: str):
76
76
  emit({"type": "error", "message": msg})
77
77
 
78
78
 
79
- def emit_transcript(text: str, is_final: bool = False, doa=None):
79
+ def emit_transcript(text: str, is_final: bool = False, doa=None, consensus=None):
80
80
  event = {"type": "transcript", "text": text, "isFinal": is_final}
81
81
  if doa is not None:
82
82
  event["doa"] = doa
83
+ if consensus is not None:
84
+ event["consensus"] = bool(consensus)
83
85
  emit(event)
84
86
 
85
87
  # ---------------------------------------------------------------------------
@@ -441,7 +443,7 @@ def main():
441
443
  result = run_transcribe(model, amplify(samples))
442
444
  text = _segment_filtered_text(result)
443
445
  if text:
444
- emit_transcript(text, is_final=False)
446
+ emit_transcript(text, is_final=False, consensus=False)
445
447
  except Exception as e:
446
448
  emit_error(f"Transcription error: {e}")
447
449
 
@@ -481,7 +483,16 @@ def main():
481
483
  if text == last_final_text:
482
484
  return
483
485
  last_final_text = text
484
- emit_transcript(text, is_final=True, doa=tuning.doa() if tuning else None)
486
+ # consensus=True only when BOTH models ran and converged. When the
487
+ # second model is off/unavailable, omit consensus metadata entirely
488
+ # so legacy single-model consumers do not interpret consensus=false
489
+ # as an explicit dual-ASR rejection.
490
+ emit_transcript(
491
+ text,
492
+ is_final=True,
493
+ doa=tuning.doa() if tuning else None,
494
+ consensus=True if consensus_model is not None else None,
495
+ )
485
496
  except Exception as e:
486
497
  emit_error(f"Transcription error: {e}")
487
498
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.426",
3
+ "version": "1.0.428",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omnius",
9
- "version": "1.0.426",
9
+ "version": "1.0.428",
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.426",
3
+ "version": "1.0.428",
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",