omnius 1.0.435 → 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 CHANGED
@@ -906,9 +906,9 @@ var init_model_broker = __esm({
906
906
  // VRAM total/free comes from system-metrics; broker computes its own snapshot
907
907
  ]);
908
908
  const snapshot = this.buildSnapshot();
909
+ await this.checkPressure(snapshot);
909
910
  this._lastSnapshot = snapshot;
910
911
  this.emit("snapshot", snapshot);
911
- this.checkPressure(snapshot);
912
912
  return snapshot;
913
913
  }
914
914
  /** Best-known current snapshot. */
@@ -42867,6 +42867,8 @@ import { join as join41, basename as basename7, extname as extname4, resolve as
42867
42867
  import { homedir as homedir11, tmpdir as tmpdir6 } from "node:os";
42868
42868
  function transcriptionPythonEnv(extra = {}) {
42869
42869
  const env2 = { ...process.env, ...extra };
42870
+ if (!env2["OMNIUS_ASR_DEVICE"])
42871
+ env2["OMNIUS_ASR_DEVICE"] = "cuda";
42870
42872
  applyMediaCudaDeviceFilterToEnv(env2, "asr");
42871
42873
  return env2;
42872
42874
  }
@@ -43379,17 +43381,18 @@ audio_file = ${JSON.stringify(filePath)}
43379
43381
  model_name = ${JSON.stringify(model)}
43380
43382
  try:
43381
43383
  import whisper
43382
- device = os.environ.get("OMNIUS_ASR_DEVICE", "cpu").strip().lower() or "cpu"
43384
+ device = os.environ.get("OMNIUS_ASR_DEVICE", "cuda").strip().lower() or "cuda"
43385
+ allow_cpu = os.environ.get("OMNIUS_ASR_ALLOW_CPU", "").strip().lower() in ("1", "true", "yes", "on")
43383
43386
  if device == "auto":
43384
- device = "cpu"
43385
- try:
43386
- import torch
43387
- if torch.cuda.is_available():
43388
- device = "cuda"
43389
- except Exception:
43390
- pass
43387
+ device = "cuda"
43388
+ if device.startswith("cuda"):
43389
+ import torch
43390
+ if not torch.cuda.is_available() or torch.cuda.device_count() <= 0:
43391
+ raise RuntimeError(f"CUDA-only ASR requested but torch cannot use CUDA (torch.version.cuda={getattr(torch.version, 'cuda', None)}, device_count={torch.cuda.device_count()}). Install a CUDA-enabled PyTorch build for this device, or set OMNIUS_ASR_ALLOW_CPU=1 only for emergency fallback.")
43392
+ elif device == "cpu" and not allow_cpu:
43393
+ raise RuntimeError("CPU ASR refused. Set OMNIUS_ASR_ALLOW_CPU=1 only for explicit emergency fallback.")
43391
43394
  m = whisper.load_model(model_name, device=device)
43392
- result = m.transcribe(audio_file, fp16=(device == "cuda"), condition_on_previous_text=False)
43395
+ result = m.transcribe(audio_file, fp16=device.startswith("cuda"), condition_on_previous_text=False)
43393
43396
  segments = []
43394
43397
  for seg in result.get("segments", []) or []:
43395
43398
  segments.append({"start": float(seg.get("start", 0)), "end": float(seg.get("end", seg.get("start", 0))), "text": str(seg.get("text", "")).strip()})
@@ -603816,6 +603819,10 @@ function asrConsensusEnabled() {
603816
603819
  const consensusEnv = String(process.env["OMNIUS_ASR_CONSENSUS"] ?? "").trim().toLowerCase();
603817
603820
  return !(consensusEnv === "0" || consensusEnv === "off" || consensusEnv === "false");
603818
603821
  }
603822
+ function liveAsrUseTranscribeCli() {
603823
+ const value2 = String(process.env["OMNIUS_ASR_USE_TRANSCRIBE_CLI"] ?? "").trim().toLowerCase();
603824
+ return value2 === "1" || value2 === "true" || value2 === "yes" || value2 === "on";
603825
+ }
603819
603826
  async function ensureVenvForTranscribeCli() {
603820
603827
  const bin = process.platform === "win32" ? "Scripts" : "bin";
603821
603828
  const exe = process.platform === "win32" ? "python.exe" : "python3";
@@ -603930,6 +603937,7 @@ var init_listen = __esm({
603930
603937
  "use strict";
603931
603938
  init_typed_node_events();
603932
603939
  init_async_process();
603940
+ init_dist5();
603933
603941
  MANAGED_TRANSCRIBE_CLI_DIR2 = join126(homedir40(), ".omnius", "runtimes", "asr", "transcribe-cli-node");
603934
603942
  AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
603935
603943
  ".mp3",
@@ -603963,6 +603971,8 @@ var init_listen = __esm({
603963
603971
  doa: null,
603964
603972
  lastTranscript: "",
603965
603973
  lastTranscriptAt: 0,
603974
+ lastPartialTranscript: "",
603975
+ lastPartialTranscriptAt: 0,
603966
603976
  lastStatus: "",
603967
603977
  updatedAt: 0
603968
603978
  };
@@ -603977,6 +603987,7 @@ var init_listen = __esm({
603977
603987
  scriptPath;
603978
603988
  process = null;
603979
603989
  _ready = false;
603990
+ registeredBrokerName = null;
603980
603991
  get ready() {
603981
603992
  return this._ready;
603982
603993
  }
@@ -604051,6 +604062,7 @@ var init_listen = __esm({
604051
604062
  this._ready = true;
604052
604063
  clearTimeout(timeout2);
604053
604064
  updateListenLiveState({ phase: "ready", lastStatus: "whisper model loaded" });
604065
+ this.registerBrokerModel(evt);
604054
604066
  this.emit("ready");
604055
604067
  resolve76();
604056
604068
  break;
@@ -604097,6 +604109,7 @@ var init_listen = __esm({
604097
604109
  reject(err);
604098
604110
  });
604099
604111
  onChildClose(this.process, (code8) => {
604112
+ this.unregisterBrokerModel();
604100
604113
  if (!this._ready) {
604101
604114
  clearTimeout(timeout2);
604102
604115
  reject(
@@ -604114,6 +604127,7 @@ var init_listen = __esm({
604114
604127
  }
604115
604128
  /** Stop the worker. */
604116
604129
  stop() {
604130
+ this.unregisterBrokerModel();
604117
604131
  if (this.process) {
604118
604132
  try {
604119
604133
  this.process.stdin?.end();
@@ -604124,6 +604138,40 @@ var init_listen = __esm({
604124
604138
  }
604125
604139
  this._ready = false;
604126
604140
  }
604141
+ registerBrokerModel(evt) {
604142
+ const device = String(evt["device"] ?? "");
604143
+ const cuda = evt["cuda"] === true || device.startsWith("cuda");
604144
+ const gpuIndexMatch = device.match(/^cuda:(\d+)/);
604145
+ const gpuIndex = gpuIndexMatch ? Number(gpuIndexMatch[1]) : null;
604146
+ const consensus = typeof evt["consensusModel"] === "string" && String(evt["consensusModel"]).trim() ? String(evt["consensusModel"]).trim() : "";
604147
+ const name10 = `whisper:${this.model}${consensus ? `+${consensus}` : ""}`;
604148
+ const vramMB = Math.max(0, Math.round(Number(evt["vramUsedMB"] ?? 0)));
604149
+ try {
604150
+ getModelBroker().registerLoaded({
604151
+ key: `whisper-cli:${name10}`,
604152
+ name: name10,
604153
+ domain: "asr",
604154
+ host: "whisper-cli",
604155
+ owner: "live-asr",
604156
+ vramMB: cuda ? vramMB : 0,
604157
+ ramMB: cuda ? 0 : vramMB,
604158
+ priority: 88,
604159
+ gpuIndex,
604160
+ gpuUuid: null
604161
+ });
604162
+ this.registeredBrokerName = name10;
604163
+ } catch {
604164
+ this.registeredBrokerName = null;
604165
+ }
604166
+ }
604167
+ unregisterBrokerModel() {
604168
+ if (!this.registeredBrokerName) return;
604169
+ try {
604170
+ getModelBroker().unregisterLoaded("whisper-cli", this.registeredBrokerName, "live-asr stopped");
604171
+ } catch {
604172
+ }
604173
+ this.registeredBrokerName = null;
604174
+ }
604127
604175
  };
604128
604176
  ListenEngine = class extends EventEmitter6 {
604129
604177
  config;
@@ -604140,6 +604188,7 @@ var init_listen = __esm({
604140
604188
  blinkTimer = null;
604141
604189
  blinkState = false;
604142
604190
  transcribeCliAvailable = null;
604191
+ owners = /* @__PURE__ */ new Set();
604143
604192
  constructor(config) {
604144
604193
  super();
604145
604194
  this.config = {
@@ -604168,6 +604217,29 @@ var init_listen = __esm({
604168
604217
  get pendingTranscript() {
604169
604218
  return this.pendingText;
604170
604219
  }
604220
+ hasOwner(owner) {
604221
+ return this.owners.has(owner);
604222
+ }
604223
+ ownerCount() {
604224
+ return this.owners.size;
604225
+ }
604226
+ async acquire(owner) {
604227
+ const clean5 = owner.trim() || "shared";
604228
+ this.owners.add(clean5);
604229
+ if (this.active) return `Live ASR already active (${[...this.owners].join(", ")}).`;
604230
+ const result = await this.start();
604231
+ if (!this.active) this.owners.delete(clean5);
604232
+ return result;
604233
+ }
604234
+ async release(owner) {
604235
+ const clean5 = owner.trim() || "shared";
604236
+ this.owners.delete(clean5);
604237
+ if (this.owners.size > 0) {
604238
+ return `Live ASR remains active (${[...this.owners].join(", ")}).`;
604239
+ }
604240
+ if (!this.active) return "Live ASR is not active.";
604241
+ return this.stop();
604242
+ }
604171
604243
  setModel(model) {
604172
604244
  this.config.model = model;
604173
604245
  }
@@ -604318,19 +604390,21 @@ var init_listen = __esm({
604318
604390
  updateListenLiveState({ phase: "error", lastStatus: "no mic capture tool (arecord/sox/ffmpeg)" });
604319
604391
  return "No microphone capture tool found. Install arecord (Linux), sox (macOS), or ffmpeg.";
604320
604392
  }
604321
- const preferWhisperFallback = asrConsensusEnabled();
604393
+ const consensusEnabled = asrConsensusEnabled();
604394
+ const useTranscribeCli = liveAsrUseTranscribeCli() && !consensusEnabled;
604395
+ const preferWhisperFallback = !useTranscribeCli;
604322
604396
  if (preferWhisperFallback) {
604323
604397
  this.emit(
604324
604398
  "info",
604325
- "ASR consensus enabled — using live-whisper.py (CUDA when available)."
604399
+ consensusEnabled ? "ASR consensus enabled — using live-whisper.py with CUDA-only Whisper guard." : "Using live-whisper.py with CUDA-only Whisper guard."
604326
604400
  );
604327
604401
  updateListenLiveState({
604328
604402
  backend: "openai-whisper",
604329
- lastStatus: "starting consensus Whisper backend..."
604403
+ lastStatus: consensusEnabled ? "starting consensus Whisper backend..." : "starting CUDA Whisper backend..."
604330
604404
  });
604331
604405
  }
604332
- let tc = preferWhisperFallback ? null : await this.loadTranscribeCli();
604333
- if (!preferWhisperFallback && !tc) {
604406
+ let tc = useTranscribeCli ? await this.loadTranscribeCli() : null;
604407
+ if (useTranscribeCli && !tc) {
604334
604408
  if (_bgInstallPromise) {
604335
604409
  this.emit("info", "Waiting for transcribe-cli install...");
604336
604410
  await _bgInstallPromise;
@@ -604413,7 +604487,7 @@ var init_listen = __esm({
604413
604487
  if (!evt.text.trim()) return;
604414
604488
  this.lastTranscriptTime = Date.now();
604415
604489
  this.pendingText = evt.text.trim();
604416
- updateListenLiveState({ lastTranscript: this.pendingText, lastTranscriptAt: this.lastTranscriptTime });
604490
+ updateListenLiveState(evt.isFinal ? { lastTranscript: this.pendingText, lastTranscriptAt: this.lastTranscriptTime } : { lastPartialTranscript: this.pendingText, lastPartialTranscriptAt: this.lastTranscriptTime });
604417
604491
  this.emit("transcript", this.pendingText, evt.isFinal);
604418
604492
  if (this.config.mode === "auto") this.resetSilenceTimer();
604419
604493
  }
@@ -604471,7 +604545,7 @@ var init_listen = __esm({
604471
604545
  if (!evt.text.trim()) return;
604472
604546
  this.lastTranscriptTime = Date.now();
604473
604547
  this.pendingText = evt.text.trim();
604474
- updateListenLiveState({ lastTranscript: this.pendingText, lastTranscriptAt: this.lastTranscriptTime });
604548
+ updateListenLiveState(evt.isFinal ? { lastTranscript: this.pendingText, lastTranscriptAt: this.lastTranscriptTime } : { lastPartialTranscript: this.pendingText, lastPartialTranscriptAt: this.lastTranscriptTime });
604475
604549
  this.emit("transcript", this.pendingText, evt.isFinal, { consensus: evt.consensus, doa: evt.doa });
604476
604550
  if (this.config.mode === "auto") this.resetSilenceTimer();
604477
604551
  });
@@ -604514,6 +604588,7 @@ transcribe-cli error: ${transcribeCliError}` : "";
604514
604588
  async stop() {
604515
604589
  if (!this.active) return "Not listening.";
604516
604590
  this.active = false;
604591
+ this.owners.clear();
604517
604592
  this.blinkState = false;
604518
604593
  updateListenLiveState({ phase: "idle", speechActive: false, micLevelDb: null, lastStatus: "stopped" });
604519
604594
  if (this.blinkTimer) {
@@ -604609,13 +604684,14 @@ transcribe-cli error: ${transcribeCliError}` : "";
604609
604684
  */
604610
604685
  async createCallTranscriber() {
604611
604686
  const requireConsensus = asrConsensusEnabled();
604612
- let tc = requireConsensus ? null : await this.loadTranscribeCli();
604613
- if (!requireConsensus && !tc && _bgInstallPromise) {
604687
+ const useTranscribeCli = liveAsrUseTranscribeCli() && !requireConsensus;
604688
+ let tc = useTranscribeCli ? await this.loadTranscribeCli() : null;
604689
+ if (useTranscribeCli && !tc && _bgInstallPromise) {
604614
604690
  await _bgInstallPromise;
604615
604691
  this.transcribeCliAvailable = null;
604616
604692
  tc = await this.loadTranscribeCli();
604617
604693
  }
604618
- if (!requireConsensus && !tc) {
604694
+ if (useTranscribeCli && !tc) {
604619
604695
  try {
604620
604696
  await ensureManagedTranscribeCliNode();
604621
604697
  this.transcribeCliAvailable = null;
@@ -604623,7 +604699,7 @@ transcribe-cli error: ${transcribeCliError}` : "";
604623
604699
  } catch {
604624
604700
  }
604625
604701
  }
604626
- if (!requireConsensus && tc?.TranscribeLive && await ensureVenvForTranscribeCli()) {
604702
+ if (useTranscribeCli && tc?.TranscribeLive && await ensureVenvForTranscribeCli()) {
604627
604703
  try {
604628
604704
  const transcriber = new tc.TranscribeLive({
604629
604705
  model: this.config.model,
@@ -605205,6 +605281,135 @@ function formatLiveResourceBudgetLines(width = 100) {
605205
605281
  `└─ leases: ${truncateAnsi(leaseText, inner - 12)}`
605206
605282
  ];
605207
605283
  }
605284
+ async function refreshLiveResourceModelSnapshot(minIntervalMs = 2500) {
605285
+ const now2 = Date.now();
605286
+ if (now2 - lastModelSnapshotRefreshAt < minIntervalMs) return;
605287
+ lastModelSnapshotRefreshAt = now2;
605288
+ await getModelBroker().pollOnce();
605289
+ }
605290
+ function formatLiveMenuResourceSummary() {
605291
+ const snap = getLiveResourceArbiter().snapshot();
605292
+ const broker = getModelBroker().snapshot();
605293
+ const totalGB = snap.totalMemMB / 1024;
605294
+ const usedGB = snap.usedMemMB / 1024;
605295
+ const vram = broker.vramMB;
605296
+ const vramText = vram ? `GPU/UMA ${mbToGb(vram.used)}/${mbToGb(vram.total)}GB` : "GPU/UMA unknown";
605297
+ const cuda = broker.vramPerDevice.length > 0 ? "CUDA on" : "CUDA off/unknown";
605298
+ return `RAM ${usedGB.toFixed(1)}/${totalGB.toFixed(1)}GB · ${vramText} · ${cuda} · models ${broker.loaded.length} loaded/${broker.inflight.length} loading`;
605299
+ }
605300
+ function formatLiveModelRuntimeLines(width = 100, opts = {}) {
605301
+ const broker = getModelBroker().snapshot();
605302
+ const inner = Math.max(24, width - 2);
605303
+ const loaded = broker.loaded ?? [];
605304
+ const inflight = broker.inflight ?? [];
605305
+ const modelsShown = Math.max(0, opts.maxModels ?? 4);
605306
+ const cudaAvailable = broker.vramPerDevice.length > 0;
605307
+ const gpuLine = formatGpuRuntimeLine(broker);
605308
+ const subsystemLine = formatSubsystemMemoryLine(loaded, inner);
605309
+ const unattributedLine = formatUnattributedMemoryLine(loaded, broker);
605310
+ const pollAge = broker.lastPollAt > 0 ? Math.max(0, Math.round((Date.now() - broker.lastPollAt) / 1e3)) : null;
605311
+ const lines = [
605312
+ colorText(" model runtime", 81),
605313
+ gpuLine,
605314
+ subsystemLine,
605315
+ unattributedLine,
605316
+ `├─ state: loaded=${loaded.length} loading=${inflight.length} slots=${broker.slots.inUse}/${broker.slots.capacity} queue=${broker.slots.queueDepth}/${broker.slots.queueCapacity}${pollAge !== null ? ` poll=${pollAge}s` : ""}`
605317
+ ];
605318
+ if (loaded.length === 0 && inflight.length === 0) {
605319
+ lines.push(`└─ models: ${colorText("none tracked yet", 244)}${cudaAvailable ? "" : colorText(" (CUDA state unknown until broker/JTOP poll succeeds)", 244)}`);
605320
+ return lines;
605321
+ }
605322
+ const modelLines = loaded.sort((a2, b) => b.vramMB + b.ramMB - (a2.vramMB + a2.ramMB)).slice(0, modelsShown).map((model, index, shown) => {
605323
+ const last2 = index === shown.length - 1 && inflight.length === 0 && loaded.length <= modelsShown;
605324
+ return `${last2 ? "└" : "├"}─ ${formatLoadedModelRuntime(model, broker)}`;
605325
+ });
605326
+ lines.push(...modelLines.map((line) => truncateAnsi(line, inner)));
605327
+ if (loaded.length > modelsShown) {
605328
+ lines.push(`├─ ${colorText(`+${loaded.length - modelsShown} more loaded model(s)`, 244)}`);
605329
+ }
605330
+ if (inflight.length > 0) {
605331
+ const loading = inflight.slice(0, 3).map((entry) => {
605332
+ const age = Math.max(0, Math.round((Date.now() - entry.startedMs) / 1e3));
605333
+ return `${entry.key} owner=${entry.owner} ${age}s`;
605334
+ }).join("; ");
605335
+ lines.push(`└─ loading: ${truncateAnsi(loading, inner - 12)}`);
605336
+ }
605337
+ return lines;
605338
+ }
605339
+ function formatLiveResourceModelReport(width = 100) {
605340
+ return [
605341
+ ...formatLiveResourceBudgetLines(width),
605342
+ ...formatLiveModelRuntimeLines(width, { maxModels: 20 })
605343
+ ].join("\n");
605344
+ }
605345
+ function formatGpuRuntimeLine(broker) {
605346
+ const devices = broker.vramPerDevice ?? [];
605347
+ if (devices.length === 0) {
605348
+ return `├─ accelerator: ${colorText("CUDA off/unknown", 208)} vram=unavailable`;
605349
+ }
605350
+ const parts = devices.map((device) => {
605351
+ const label = device.uuid === "jetson-uma" ? "jetson-uma" : `gpu${device.index}`;
605352
+ const pct = device.total > 0 ? device.used / device.total : 0;
605353
+ return `${label} ${pressureColor(pct, 0.78)(`${mbToGb(device.used)}/${mbToGb(device.total)}GB`)}`;
605354
+ });
605355
+ return `├─ accelerator: ${colorText("CUDA on", 82)} ${parts.join(" ")}`;
605356
+ }
605357
+ function formatSubsystemMemoryLine(loaded, width) {
605358
+ const totals = /* @__PURE__ */ new Map();
605359
+ for (const model of loaded) {
605360
+ const key = subsystemForModel(model);
605361
+ const current = totals.get(key) ?? { vram: 0, ram: 0, count: 0 };
605362
+ current.vram += Math.max(0, Number(model.vramMB ?? 0));
605363
+ current.ram += Math.max(0, Number(model.ramMB ?? 0));
605364
+ current.count += 1;
605365
+ totals.set(key, current);
605366
+ }
605367
+ if (totals.size === 0) return `├─ subsystems(tracked): ${colorText("no tracked model memory", 244)}`;
605368
+ const preferred = ["llm", "vision", "asr", "embedding", "image", "video", "audio", "other"];
605369
+ const parts = [...totals.entries()].sort((a2, b) => {
605370
+ const ai = preferred.indexOf(a2[0]);
605371
+ const bi = preferred.indexOf(b[0]);
605372
+ return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
605373
+ }).map(([name10, total]) => `${name10}=${mbToGb(total.vram)}Gv/${mbToGb(total.ram)}Gr`);
605374
+ return `├─ subsystems(tracked): ${truncateAnsi(parts.join(" "), width - 25)}`;
605375
+ }
605376
+ function formatUnattributedMemoryLine(loaded, broker) {
605377
+ const resource = getLiveResourceArbiter().snapshot();
605378
+ const trackedRam = loaded.reduce((sum, model) => sum + Math.max(0, Number(model.ramMB ?? 0)), 0);
605379
+ const trackedVram = loaded.reduce((sum, model) => sum + Math.max(0, Number(model.vramMB ?? 0)), 0);
605380
+ const ramGap = Math.max(0, resource.usedMemMB - trackedRam);
605381
+ const vramGap = broker.vramMB ? Math.max(0, broker.vramMB.used - trackedVram) : null;
605382
+ return `├─ unattributed/system: ram=${mbToGb(ramGap)}G${vramGap !== null ? ` vram/uma=${mbToGb(vramGap)}G` : ""} ${colorText("(OS, camera ffmpeg, CV libs, CUDA overhead, untracked subprocesses)", 244)}`;
605383
+ }
605384
+ function formatLoadedModelRuntime(model, broker) {
605385
+ const slots = broker.slots.byModel[model.name] ?? broker.slots.byModel[model.key] ?? null;
605386
+ const active = slots && slots.inUse > 0;
605387
+ const state = active ? colorText(`running x${slots.inUse}`, 82) : colorText("loaded idle", 244);
605388
+ const cuda = cudaStateForModel(model, broker.vramPerDevice.length > 0);
605389
+ const idle = Math.max(0, Math.round((Date.now() - model.lastUsedAt) / 1e3));
605390
+ const tps = slots && slots.samples > 0 ? ` ${slots.tokensPerSec.toFixed(1)}tok/s` : "";
605391
+ const ctx3 = model.numCtx ? ` ctx=${model.numCtx}` : "";
605392
+ return `${subsystemForModel(model)} ${model.host}:${model.name} ${state} ${cuda} vram=${model.vramMB}MB ram=${model.ramMB}MB idle=${idle}s${tps}${ctx3}`;
605393
+ }
605394
+ function cudaStateForModel(model, cudaAvailable) {
605395
+ if (model.gpuIndex !== null && model.gpuIndex !== void 0) return colorText(`cuda:on gpu${model.gpuIndex}`, 82);
605396
+ if (model.vramMB > 0) return colorText(cudaAvailable ? "cuda:on" : "gpu:on", 82);
605397
+ if (model.host === "external") return colorText("cuda:external", 244);
605398
+ return colorText(cudaAvailable ? "cuda:off/cpu" : "cuda:off", 208);
605399
+ }
605400
+ function subsystemForModel(model) {
605401
+ if (model.domain === "chat" || model.domain === "subagent") return "llm";
605402
+ if (model.domain === "vision" || model.host.includes("moondream")) return "vision";
605403
+ if (model.domain === "asr" || model.host.includes("whisper")) return "asr";
605404
+ if (model.domain === "embedding") return "embedding";
605405
+ if (model.domain === "image-gen") return "image";
605406
+ if (model.domain === "video-gen") return "video";
605407
+ if (model.domain === "tts" || model.domain === "music" || model.domain === "sound") return "audio";
605408
+ return "other";
605409
+ }
605410
+ function mbToGb(mb) {
605411
+ return (Math.max(0, Number(mb) || 0) / 1024).toFixed(1);
605412
+ }
605208
605413
  function readBudget() {
605209
605414
  const raw = String(process.env["OMNIUS_LIVE_RESOURCE_BUDGET"] ?? "").trim().toLowerCase();
605210
605415
  if (raw === "low" || raw === "conserve" || raw === "conservative" || raw === "battery") return "conserve";
@@ -605240,10 +605445,11 @@ function truncateAnsi(text2, max) {
605240
605445
  if (plain.length <= max) return text2;
605241
605446
  return `${plain.slice(0, Math.max(0, max - 3))}...`;
605242
605447
  }
605243
- var LiveResourceArbiter, singleton;
605448
+ var LiveResourceArbiter, singleton, lastModelSnapshotRefreshAt;
605244
605449
  var init_live_resource_arbiter = __esm({
605245
605450
  "packages/cli/src/tui/live-resource-arbiter.ts"() {
605246
605451
  "use strict";
605452
+ init_dist5();
605247
605453
  LiveResourceArbiter = class {
605248
605454
  leases = /* @__PURE__ */ new Map();
605249
605455
  seq = 0;
@@ -605400,6 +605606,7 @@ var init_live_resource_arbiter = __esm({
605400
605606
  }
605401
605607
  };
605402
605608
  singleton = null;
605609
+ lastModelSnapshotRefreshAt = 0;
605403
605610
  }
605404
605611
  });
605405
605612
 
@@ -605645,6 +605852,18 @@ function truncateCell(value2, width) {
605645
605852
  function stripDashboardAnsi(value2) {
605646
605853
  return value2.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
605647
605854
  }
605855
+ function dashboardAnsi(code8, text2) {
605856
+ return `\x1B[${code8}m${text2}\x1B[0m`;
605857
+ }
605858
+ function dashboardDim(text2) {
605859
+ return dashboardAnsi("38;5;244", text2);
605860
+ }
605861
+ function dashboardSoundColor(score) {
605862
+ if (score === void 0) return "38;5;81";
605863
+ if (score >= 0.75) return "38;5;82";
605864
+ if (score >= 0.5) return "38;5;220";
605865
+ return "38;5;208";
605866
+ }
605648
605867
  function dashboardVisibleWidth(value2) {
605649
605868
  return stripDashboardAnsi(value2).length;
605650
605869
  }
@@ -605893,13 +606112,6 @@ function readPcm16WavActivity(filePath, bins = 36) {
605893
606112
  return void 0;
605894
606113
  }
605895
606114
  }
605896
- function extractAsrBackend(output) {
605897
- const match = String(output ?? "").match(/^Backend:\s*(.+)$/m);
605898
- if (!match?.[1]) return void 0;
605899
- const backend = match[1].trim();
605900
- if (/managed openai-whisper|transcribe-cli|whisper/i.test(backend)) return backend;
605901
- return backend.slice(0, 80);
605902
- }
605903
606115
  function parseLiveAudioIntakeDecision(value2) {
605904
606116
  const parsed = parseJsonObjectFromText(value2);
605905
606117
  if (!parsed) return null;
@@ -606293,6 +606505,23 @@ function formatAudioAnalysisDashboardSummary(value2) {
606293
606505
  }
606294
606506
  return `sound: ${trimOneLine(text2.replace(/\{[\s\S]*$/, ""), 140) || trimOneLine(text2, 140)}`;
606295
606507
  }
606508
+ function formatAudioAnalysisDashboardSummaryColored(value2) {
606509
+ const text2 = String(value2 ?? "").trim();
606510
+ if (!text2) return "";
606511
+ const parsed = parseJsonObjectFromText(text2);
606512
+ const classifications = Array.isArray(parsed?.["classifications"]) ? parsed["classifications"] : [];
606513
+ if (classifications.length > 0) {
606514
+ const parts = classifications.slice(0, 4).map((entry) => {
606515
+ const label = trimOneLine(entry["class"] ?? entry["label"] ?? entry["name"] ?? "sound", 44);
606516
+ const score = boundedConfidence(entry["score"] ?? entry["confidence"]);
606517
+ const coloredLabel = dashboardAnsi(dashboardSoundColor(score), label || "sound");
606518
+ return `${coloredLabel}${score !== void 0 ? ` ${dashboardDim(score.toFixed(2))}` : ""}`;
606519
+ });
606520
+ return `${dashboardAnsi("38;5;45", "sounds heard")}: ${parts.join(dashboardDim("; "))}`;
606521
+ }
606522
+ const summary = trimOneLine(text2.replace(/\{[\s\S]*$/, ""), 140) || trimOneLine(text2, 140);
606523
+ return `${dashboardAnsi("38;5;45", "sound")}: ${dashboardAnsi("38;5;250", summary)}`;
606524
+ }
606296
606525
  function formatCameraRotationDashboard(camera) {
606297
606526
  const orientation = camera.orientation;
606298
606527
  const rotation = orientation?.rotation ?? camera.rotation ?? 0;
@@ -606378,6 +606607,9 @@ function formatLiveDashboardFromSnapshot(snapshot, opts = {}) {
606378
606607
  for (const budgetLine of formatLiveResourceBudgetLines(width - 2)) {
606379
606608
  lines.push(`│${truncateAnsiCell(budgetLine, width - 2)}│`);
606380
606609
  }
606610
+ for (const modelLine of formatLiveModelRuntimeLines(width - 2, { maxModels: 4 })) {
606611
+ lines.push(`│${truncateAnsiCell(modelLine, width - 2)}│`);
606612
+ }
606381
606613
  const enabledStreams = Object.entries(snapshot.config.cameraStreams ?? {}).filter(([, stream]) => stream.enabled !== false);
606382
606614
  const selectedResolution = snapshot.config.selectedCamera ? formatLiveCameraResolution(snapshot.config.cameraStreams?.[snapshot.config.selectedCamera]?.resolution) : "none";
606383
606615
  lines.push(`│${truncateCell(` systems: camera-streams=${enabledStreams.length}/${snapshot.devices.video.length} selected-res=${selectedResolution} yolo=${snapshot.config.inferEnabled ? "on" : "off"} clip=${snapshot.config.clipEnabled ? "on" : "off"} asr=${snapshot.config.asrEnabled ? "on" : "off"} sounds=${snapshot.config.audioAnalysisEnabled ? "on" : "off"}`, width - 2)}│`);
@@ -606423,13 +606655,16 @@ function formatLiveDashboardFromSnapshot(snapshot, opts = {}) {
606423
606655
  pushDashboardWrapped(lines, detail, width);
606424
606656
  }
606425
606657
  });
606426
- if (snapshot.audio) {
606658
+ const dashboardAsr = getListenLiveState();
606659
+ const showDashboardAudio = Boolean(snapshot.audio) || dashboardAsr.phase !== "idle";
606660
+ if (showDashboardAudio) {
606661
+ const audioSnapshot = snapshot.audio;
606427
606662
  lines.push(`├${horizontal}┤`);
606428
606663
  lines.push(`│${truncateCell(" audio monitor", width - 2)}│`);
606429
- const transcriptFailure = isAudioFailureText(snapshot.audio.transcript) ? snapshot.audio.transcript : "";
606430
- const transcript = transcriptFailure ? "" : snapshot.audio.transcript;
606431
- const audioError = sanitizeLiveAudioError([snapshot.audio.error, transcriptFailure].filter(Boolean).join(" | "));
606432
- const asr = getListenLiveState();
606664
+ const transcriptFailure = audioSnapshot && isAudioFailureText(audioSnapshot.transcript) ? audioSnapshot.transcript : "";
606665
+ const transcript = audioSnapshot && !transcriptFailure ? audioSnapshot.transcript : "";
606666
+ const audioError = sanitizeLiveAudioError([audioSnapshot?.error, transcriptFailure].filter(Boolean).join(" | "));
606667
+ const asr = dashboardAsr;
606433
606668
  const asrPhaseLabel = {
606434
606669
  bootstrapping: "bootstrapping deps…",
606435
606670
  "loading-model": "loading model…",
@@ -606440,21 +606675,26 @@ function formatLiveDashboardFromSnapshot(snapshot, opts = {}) {
606440
606675
  };
606441
606676
  const asrMicBits = asr.micLevelDb !== null ? ` mic=${asr.micLevelDb.toFixed(1)}dB${asr.speechActive ? " ●speech" : ""}${asr.doa !== null ? ` dir=${asr.doa}°` : ""}` : "";
606442
606677
  const asrLine = asr.phase !== "idle" ? `│ ├─ whisper: ${[asr.backend, asr.model].filter(Boolean).join(" ") || "starting"} — ${asrPhaseLabel[asr.phase] ?? asr.phase}${asrMicBits}` : "";
606443
- const asrHeardAge = asr.lastTranscriptAt > 0 ? Math.max(0, Math.round((now2 - asr.lastTranscriptAt) / 1e3)) : null;
606444
- const asrHeardLine = asr.phase !== "idle" && asr.lastTranscript ? `│ ├─ heard${asrHeardAge !== null ? ` ${asrHeardAge}s ago` : ""}: ${trimOneLine(asr.lastTranscript, 120)}` : "";
606678
+ const asrFinalAge = asr.lastTranscriptAt > 0 ? Math.max(0, Math.round((now2 - asr.lastTranscriptAt) / 1e3)) : null;
606679
+ const asrPartialAge = asr.lastPartialTranscriptAt > 0 ? Math.max(0, Math.round((now2 - asr.lastPartialTranscriptAt) / 1e3)) : null;
606680
+ const showPartial = asr.lastPartialTranscript && (!asr.lastTranscriptAt || asr.lastPartialTranscriptAt >= asr.lastTranscriptAt);
606681
+ const asrHeardLine = asr.phase !== "idle" && (showPartial || asr.lastTranscript) ? showPartial ? `│ ├─ hearing${asrPartialAge !== null ? ` ${asrPartialAge}s ago` : ""}: ${trimOneLine(asr.lastPartialTranscript, 120)}` : `│ ├─ heard${asrFinalAge !== null ? ` ${asrFinalAge}s ago` : ""}: ${trimOneLine(asr.lastTranscript, 120)}` : "";
606445
606682
  const asrStatusLine = (asr.phase === "bootstrapping" || asr.phase === "loading-model" || asr.phase === "error") && asr.lastStatus ? `│ ├─ whisper status: ${trimOneLine(asr.lastStatus, 120)}` : "";
606683
+ const coloredSoundSummary = audioSnapshot?.analysis ? formatAudioAnalysisDashboardSummaryColored(audioSnapshot.analysis) : "";
606446
606684
  const audioBits = [
606447
- `├─ input: ${snapshot.audio.input || snapshot.config.selectedAudioInput || "none"}`,
606685
+ `├─ input: ${audioSnapshot?.input || snapshot.config.selectedAudioInput || "none"}`,
606448
606686
  asrLine,
606449
606687
  asrStatusLine,
606450
606688
  asrHeardLine,
606451
- snapshot.audio.activity ? `│ ├─ activity: ${snapshot.audio.activity.waveform} rms=${snapshot.audio.activity.rmsDb.toFixed(1)}dB active=${Math.round(snapshot.audio.activity.activeRatio * 100)}%` : "",
606689
+ audioSnapshot?.activity ? `│ ├─ activity: ${audioSnapshot.activity.waveform} rms=${audioSnapshot.activity.rmsDb.toFixed(1)}dB active=${Math.round(audioSnapshot.activity.activeRatio * 100)}%` : "",
606452
606690
  audioError ? `│ ├─ error: ${trimOneLine(audioError, 140)}` : "",
606453
- transcript ? `│ ├─ asr${snapshot.audio.asrBackend ? ` (${snapshot.audio.asrBackend})` : ""}: ${trimOneLine(transcript, 140)}` : "",
606454
- snapshot.audio.intake ? `│ ├─ intake: ${snapshot.audio.intake.intendedForAgent ? "for-agent" : "ambient"} conf=${snapshot.audio.intake.confidence.toFixed(2)} action=${snapshot.audio.intake.action}` : "",
606455
- snapshot.audio.analysis ? `│ └─ ${formatAudioAnalysisDashboardSummary(snapshot.audio.analysis)}` : ""
606691
+ transcript ? `│ ├─ asr${audioSnapshot?.asrBackend ? ` (${audioSnapshot.asrBackend})` : ""}: ${trimOneLine(transcript, 140)}` : "",
606692
+ audioSnapshot?.intake ? `│ ├─ intake: ${audioSnapshot.intake.intendedForAgent ? "for-agent" : "ambient"} conf=${audioSnapshot.intake.confidence.toFixed(2)} action=${audioSnapshot.intake.action}` : ""
606456
606693
  ].filter(Boolean);
606457
606694
  for (const item of audioBits.slice(0, 9)) pushDashboardWrapped(lines, item, width);
606695
+ if (coloredSoundSummary) {
606696
+ lines.push(`│${truncateAnsiCell(` └─ ${coloredSoundSummary}`, width - 2)}│`);
606697
+ }
606458
606698
  }
606459
606699
  lines.push(`╰${horizontal}╯`);
606460
606700
  return lines;
@@ -606847,6 +607087,17 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
606847
607087
  lines.push(`summary: ${cameraSnapshotSummary(camera)}`);
606848
607088
  }
606849
607089
  }
607090
+ const liveAsr = getListenLiveState();
607091
+ const liveAsrFinalAge = liveAsr.lastTranscriptAt > 0 ? now2 - liveAsr.lastTranscriptAt : Number.POSITIVE_INFINITY;
607092
+ const liveAsrPartialAge = liveAsr.lastPartialTranscriptAt > 0 ? now2 - liveAsr.lastPartialTranscriptAt : Number.POSITIVE_INFINITY;
607093
+ const liveAsrText = liveAsr.lastTranscript && liveAsrFinalAge < maxAgeMs ? liveAsr.lastTranscript : liveAsr.lastPartialTranscript && liveAsrPartialAge < Math.min(maxAgeMs, 3e4) ? liveAsr.lastPartialTranscript : "";
607094
+ if (liveAsrText) {
607095
+ const label = liveAsr.lastTranscript && liveAsrFinalAge < maxAgeMs ? "Live streaming ASR" : "Live streaming ASR partial";
607096
+ lines.push("");
607097
+ lines.push("## LIVE STREAMING ASR");
607098
+ lines.push(`${label}${liveAsr.backend ? ` (${liveAsr.backend} ${liveAsr.model})` : ""}:`);
607099
+ lines.push(cleanPreview(liveAsrText, 1200));
607100
+ }
606850
607101
  if (snapshot.audio) {
606851
607102
  const transcriptFailure = isAudioFailureText(snapshot.audio.transcript) ? snapshot.audio.transcript : "";
606852
607103
  const transcript = transcriptFailure ? "" : snapshot.audio.transcript;
@@ -606881,6 +607132,7 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
606881
607132
  lines.push(`intake_reason: ${snapshot.audio.intake.reason}`);
606882
607133
  }
606883
607134
  if (snapshot.audio.analysis) {
607135
+ lines.push(`Live sound summary: ${formatAudioAnalysisDashboardSummary(snapshot.audio.analysis)}`);
606884
607136
  lines.push("Live sound analysis:");
606885
607137
  lines.push(cleanPreview(snapshot.audio.analysis, 1600));
606886
607138
  }
@@ -607210,6 +607462,7 @@ function formatLiveStatus(snapshot) {
607210
607462
  `Camera orientation: ${cfg.selectedCamera ? describeCameraOrientation(cfg.cameraOrientation?.[cfg.selectedCamera]) : "none"}`,
607211
607463
  `Audio input: ${cfg.selectedAudioInput || "none"}`,
607212
607464
  `Audio output: ${cfg.selectedAudioOutput || "none"}`,
607465
+ `Resources: ${formatLiveMenuResourceSummary()}`,
607213
607466
  `Snapshot: ${liveSnapshotPath(snapshot.repoRoot)}`
607214
607467
  ];
607215
607468
  if (snapshot.video?.updatedAt) {
@@ -607265,6 +607518,10 @@ var init_live_sensors = __esm({
607265
607518
  config: this.config,
607266
607519
  devices: this.devices
607267
607520
  };
607521
+ try {
607522
+ getModelBroker().startPolling(2500);
607523
+ } catch {
607524
+ }
607268
607525
  this.ensureLoops();
607269
607526
  }
607270
607527
  repoRoot;
@@ -608486,27 +608743,20 @@ ${output}`).join("\n\n");
608486
608743
  audioErrors.push(`No live input signal detected from selected input ${capture.device}; keeping the explicit selection.`);
608487
608744
  }
608488
608745
  if (this.config.asrEnabled) {
608489
- asrLease = this.resourceArbiter.claim({
608490
- owner: "live-asr",
608491
- priority: 88,
608492
- ttlMs: 18e3,
608493
- reason: "live audio ASR capture/transcription",
608494
- suppress: ["vision"]
608495
- });
608496
- try {
608497
- const asr = await new TranscribeFileTool(this.repoRoot).execute({
608498
- path: recordingPath,
608499
- model: "tiny",
608500
- backend: "managed-whisper"
608501
- });
608502
- if (asr.success) {
608503
- transcript = cleanPreview(typeof asr.llmContent === "string" ? asr.llmContent : "", 1600);
608504
- asrBackend = extractAsrBackend(asr.output) ?? "whisper";
608505
- } else {
608506
- audioErrors.push(`ASR unavailable: ${asr.error || asr.output || "unknown error"}`);
608507
- }
608508
- } catch (err) {
608509
- audioErrors.push(`ASR unavailable: ${err instanceof Error ? err.message : String(err)}`);
608746
+ const liveAsr = getListenLiveState();
608747
+ const now2 = Date.now();
608748
+ const finalAge = liveAsr.lastTranscriptAt > 0 ? now2 - liveAsr.lastTranscriptAt : Number.POSITIVE_INFINITY;
608749
+ const partialAge = liveAsr.lastPartialTranscriptAt > 0 ? now2 - liveAsr.lastPartialTranscriptAt : Number.POSITIVE_INFINITY;
608750
+ if (liveAsr.lastTranscript && finalAge < 45e3) {
608751
+ transcript = cleanPreview(liveAsr.lastTranscript, 1600);
608752
+ asrBackend = [liveAsr.backend, liveAsr.model, "streaming"].filter(Boolean).join(" ");
608753
+ } else if (liveAsr.lastPartialTranscript && partialAge < 2e4) {
608754
+ transcript = cleanPreview(liveAsr.lastPartialTranscript, 1600);
608755
+ asrBackend = [liveAsr.backend, liveAsr.model, "streaming-partial"].filter(Boolean).join(" ");
608756
+ } else if (liveAsr.phase === "idle") {
608757
+ audioErrors.push("Streaming ASR is not running. Start /live run or /listen to enable CUDA Whisper streaming.");
608758
+ } else if (liveAsr.lastStatus) {
608759
+ audioErrors.push(`Streaming ASR status: ${liveAsr.lastStatus}`);
608510
608760
  }
608511
608761
  }
608512
608762
  if (this.config.audioAnalysisEnabled) {
@@ -608528,6 +608778,13 @@ ${output}`).join("\n\n");
608528
608778
  analysis = parts.filter(Boolean).join("\n");
608529
608779
  }
608530
608780
  if (transcript) {
608781
+ asrLease = this.resourceArbiter.claim({
608782
+ owner: "live-asr",
608783
+ priority: 88,
608784
+ ttlMs: 18e3,
608785
+ reason: "live streaming ASR transcript accepted",
608786
+ suppress: ["vision"]
608787
+ });
608531
608788
  intake = await classifyLiveAudioIntake(transcript, this.intakeAgentConfig);
608532
608789
  keepAsrLease = true;
608533
608790
  asrLease?.extend(
@@ -608735,17 +608992,17 @@ ${analysis}` : ""
608735
608992
  this.videoTimer.unref?.();
608736
608993
  }
608737
608994
  scheduleAudioLoop(delayMs) {
608738
- const wantsAudio = this.config.audioEnabled && (this.config.asrEnabled || this.config.audioAnalysisEnabled);
608995
+ const wantsAudio = this.config.audioEnabled && this.config.audioAnalysisEnabled;
608739
608996
  if (!wantsAudio || this.audioTimer) return;
608740
608997
  this.audioTimer = setTimeout(async () => {
608741
608998
  this.audioTimer = null;
608742
- const stillWantsAudio = this.config.audioEnabled && (this.config.asrEnabled || this.config.audioAnalysisEnabled);
608999
+ const stillWantsAudio = this.config.audioEnabled && this.config.audioAnalysisEnabled;
608743
609000
  if (!stillWantsAudio) return;
608744
609001
  try {
608745
609002
  await this.sampleAudioNow();
608746
609003
  } catch {
608747
609004
  } finally {
608748
- const wantsNext = this.config.audioEnabled && (this.config.asrEnabled || this.config.audioAnalysisEnabled);
609005
+ const wantsNext = this.config.audioEnabled && this.config.audioAnalysisEnabled;
608749
609006
  if (wantsNext) {
608750
609007
  this.scheduleAudioLoop(this.config.audioIntervalMs);
608751
609008
  }
@@ -608783,7 +609040,7 @@ ${analysis}` : ""
608783
609040
  clearTimeout(this.previewTickTimer);
608784
609041
  this.previewTickTimer = null;
608785
609042
  }
608786
- const wantsAudio = this.config.audioEnabled && (this.config.asrEnabled || this.config.audioAnalysisEnabled);
609043
+ const wantsAudio = this.config.audioEnabled && this.config.audioAnalysisEnabled;
608787
609044
  if (wantsAudio && !this.audioTimer) {
608788
609045
  this.scheduleAudioLoop(0);
608789
609046
  } else if (!wantsAudio && this.audioTimer) {
@@ -616949,7 +617206,13 @@ function renderVoiceChatCoalescedRow(row, opts) {
616949
617206
  return;
616950
617207
  }
616951
617208
  if (_voiceChatCoalesce) {
616952
- _voiceChatCoalesce.rows.push(row);
617209
+ const replacePrefix = opts?.replaceLastPrefix;
617210
+ const lastIndex = _voiceChatCoalesce.rows.length - 1;
617211
+ if (replacePrefix && lastIndex >= 0 && _voiceChatCoalesce.rows[lastIndex]?.startsWith(replacePrefix)) {
617212
+ _voiceChatCoalesce.rows[lastIndex] = row;
617213
+ } else {
617214
+ _voiceChatCoalesce.rows.push(row);
617215
+ }
616953
617216
  host.refreshDynamicBlocks();
616954
617217
  return;
616955
617218
  }
@@ -659689,6 +659952,19 @@ function attachLiveSinks(ctx3, manager, agentEnabled = false, speechEnabledOverr
659689
659952
  manager.setAgentActionSink(void 0);
659690
659953
  }
659691
659954
  }
659955
+ async function ensureLiveStreamingAsr(ctx3, manager) {
659956
+ if (!manager.getConfig().asrEnabled) return;
659957
+ if (!ctx3.listenStart) return;
659958
+ ctx3.listenSetMode?.("auto");
659959
+ const msg = await ctx3.listenStart("live");
659960
+ if (!/already active/i.test(msg)) renderInfo(`Live ASR: ${msg}`);
659961
+ }
659962
+ async function releaseLiveStreamingAsr(ctx3) {
659963
+ try {
659964
+ await ctx3.listenRelease?.("live");
659965
+ } catch {
659966
+ }
659967
+ }
659692
659968
  async function startLiveVoicechat(ctx3, manager) {
659693
659969
  const speechEnabled = Boolean(ctx3.voiceSpeak);
659694
659970
  attachLiveSinks(ctx3, manager, false, speechEnabled);
@@ -659920,6 +660196,7 @@ async function handleLiveCommand(ctx3, arg) {
659920
660196
  if (sub2 === "stop" || sub2 === "off" || sub2 === "disable") {
659921
660197
  manager.stopAll();
659922
660198
  manager.setAgentActionSink(void 0);
660199
+ await releaseLiveStreamingAsr(ctx3);
659923
660200
  if (ctx3.isVoiceChatActive?.()) {
659924
660201
  try {
659925
660202
  await ctx3.voiceChatStop?.();
@@ -659938,6 +660215,7 @@ async function handleLiveCommand(ctx3, arg) {
659938
660215
  }
659939
660216
  await ensureLiveInventory(manager);
659940
660217
  renderInfo(manager.startRunMode({ agent: effectiveAgent, lowLatency: true, speech: effectiveAgent && Boolean(ctx3.voiceSpeak) }));
660218
+ await ensureLiveStreamingAsr(ctx3, manager);
659941
660219
  return;
659942
660220
  }
659943
660221
  if (sub2 === "chat" || sub2 === "voicechat" || sub2 === "voice") {
@@ -660037,10 +660315,13 @@ async function handleLiveCommand(ctx3, arg) {
660037
660315
  }
660038
660316
  if (sub2 === "asr") {
660039
660317
  const cfg = manager.getConfig();
660318
+ const enabled2 = setBool(value2, !cfg.asrEnabled);
660040
660319
  manager.configure({
660041
660320
  audioEnabled: true,
660042
- asrEnabled: setBool(value2, !cfg.asrEnabled)
660321
+ asrEnabled: enabled2
660043
660322
  });
660323
+ if (enabled2) await ensureLiveStreamingAsr(ctx3, manager);
660324
+ else await releaseLiveStreamingAsr(ctx3);
660044
660325
  renderInfo(formatLiveStatus(manager.getSnapshot()));
660045
660326
  return;
660046
660327
  }
@@ -660328,6 +660609,7 @@ async function handleCameraCommand(ctx3, arg) {
660328
660609
  async function showLiveMenu(ctx3, manager) {
660329
660610
  await ensureLiveInventory(manager);
660330
660611
  while (true) {
660612
+ await refreshLiveResourceModelSnapshot().catch(() => void 0);
660331
660613
  const snapshot = manager.getSnapshot();
660332
660614
  const cfg = manager.getConfig();
660333
660615
  const devices = snapshot.devices;
@@ -660339,6 +660621,15 @@ async function showLiveMenu(ctx3, manager) {
660339
660621
  ` video ${liveEnabled(cfg.videoEnabled)} · infer ${liveEnabled(cfg.inferEnabled)} · clip ${liveEnabled(cfg.clipEnabled)} · audio ${liveEnabled(cfg.audioEnabled)} · asr ${liveEnabled(cfg.asrEnabled)} · sounds ${liveEnabled(cfg.audioAnalysisEnabled)}`
660340
660622
  )
660341
660623
  },
660624
+ {
660625
+ key: "info:resources",
660626
+ label: selectColors.dim(` ${formatLiveMenuResourceSummary()}`)
660627
+ },
660628
+ {
660629
+ key: "resources",
660630
+ label: "Show Resource + Model State",
660631
+ detail: "RAM/VRAM by subsystem, CUDA per loaded model, active slots, inflight loads"
660632
+ },
660342
660633
  {
660343
660634
  key: "run-live",
660344
660635
  label: "Start Live Run",
@@ -660476,6 +660767,7 @@ async function showLiveMenu(ctx3, manager) {
660476
660767
  case "run-live":
660477
660768
  attachLiveSinks(ctx3, manager, false);
660478
660769
  renderInfo(manager.startRunMode({ agent: false, lowLatency: true, speech: false }));
660770
+ await ensureLiveStreamingAsr(ctx3, manager);
660479
660771
  continue;
660480
660772
  case "run-agent":
660481
660773
  attachLiveSinks(ctx3, manager, true);
@@ -660483,6 +660775,7 @@ async function showLiveMenu(ctx3, manager) {
660483
660775
  renderWarning("Live PFC agent review is unavailable in this context; starting visual/audio feedback only.");
660484
660776
  }
660485
660777
  renderInfo(manager.startRunMode({ agent: Boolean(ctx3.startBackgroundPrompt), lowLatency: true, speech: Boolean(ctx3.startBackgroundPrompt && ctx3.voiceSpeak) }));
660778
+ await ensureLiveStreamingAsr(ctx3, manager);
660486
660779
  continue;
660487
660780
  case "run-chat":
660488
660781
  await startLiveVoicechat(ctx3, manager);
@@ -660501,6 +660794,8 @@ async function showLiveMenu(ctx3, manager) {
660501
660794
  continue;
660502
660795
  case "toggle-asr":
660503
660796
  manager.configure({ audioEnabled: true, asrEnabled: !cfg.asrEnabled });
660797
+ if (!cfg.asrEnabled) await ensureLiveStreamingAsr(ctx3, manager);
660798
+ else await releaseLiveStreamingAsr(ctx3);
660504
660799
  continue;
660505
660800
  case "toggle-sounds":
660506
660801
  manager.configure({ audioEnabled: true, audioAnalysisEnabled: !cfg.audioAnalysisEnabled });
@@ -660561,6 +660856,10 @@ Previewing microphone activity...`);
660561
660856
  renderWarning("Live transcript UI is unavailable in this context.");
660562
660857
  }
660563
660858
  continue;
660859
+ case "resources":
660860
+ await refreshLiveResourceModelSnapshot(0).catch(() => void 0);
660861
+ renderInfo(formatLiveResourceModelReport(110));
660862
+ continue;
660564
660863
  case "refresh":
660565
660864
  renderInfo(formatLiveInventory(await manager.refreshDevices()));
660566
660865
  continue;
@@ -660570,6 +660869,7 @@ Previewing microphone activity...`);
660570
660869
  case "stop":
660571
660870
  manager.stopAll();
660572
660871
  manager.setAgentActionSink(void 0);
660872
+ await releaseLiveStreamingAsr(ctx3);
660573
660873
  if (ctx3.isVoiceChatActive?.()) {
660574
660874
  try {
660575
660875
  await ctx3.voiceChatStop?.();
@@ -665644,6 +665944,7 @@ var init_commands = __esm({
665644
665944
  init_generative_progress();
665645
665945
  init_cad_model_viewer();
665646
665946
  init_live_sensors();
665947
+ init_live_resource_arbiter();
665647
665948
  init_command_registry();
665648
665949
  init_hf_token_prompt();
665649
665950
  init_dist5();
@@ -699583,9 +699884,15 @@ Rules:
699583
699884
  this._retryMicTimer = null;
699584
699885
  if (!this.active) return;
699585
699886
  try {
699586
- await this.listen.stop().catch(() => {
699587
- });
699588
- await this.listen.start();
699887
+ if (typeof this.listen.release === "function" && typeof this.listen.acquire === "function") {
699888
+ await this.listen.release("voicechat").catch(() => {
699889
+ });
699890
+ await this.listen.acquire("voicechat");
699891
+ } else {
699892
+ await this.listen.stop().catch(() => {
699893
+ });
699894
+ await this.listen.start();
699895
+ }
699589
699896
  if (this.verbose) this.onStatus("Mic auto-recovered — LISTENING");
699590
699897
  } catch {
699591
699898
  }
@@ -699595,7 +699902,11 @@ Rules:
699595
699902
  this.listen.on("transcript", this._onTranscript);
699596
699903
  this.listen.on("error", this._onError);
699597
699904
  try {
699598
- await this.listen.start();
699905
+ if (typeof this.listen.acquire === "function") {
699906
+ await this.listen.acquire("voicechat");
699907
+ } else {
699908
+ await this.listen.start();
699909
+ }
699599
699910
  this.setState("LISTENING");
699600
699911
  if (this.verbose) this.onStatus("Mic active — LISTENING for speech...");
699601
699912
  } catch (err) {
@@ -699622,15 +699933,21 @@ Rules:
699622
699933
  this.finalizeSegment();
699623
699934
  }
699624
699935
  if (this._onTranscript) {
699625
- this.listen.removeAllListeners("transcript");
699936
+ if (typeof this.listen.off === "function") this.listen.off("transcript", this._onTranscript);
699937
+ else this.listen.removeAllListeners("transcript");
699626
699938
  this._onTranscript = null;
699627
699939
  }
699628
699940
  if (this._onError) {
699629
- this.listen.removeAllListeners("error");
699941
+ if (typeof this.listen.off === "function") this.listen.off("error", this._onError);
699942
+ else this.listen.removeAllListeners("error");
699630
699943
  this._onError = null;
699631
699944
  }
699632
699945
  try {
699633
- await this.listen.stop();
699946
+ if (typeof this.listen.release === "function") {
699947
+ await this.listen.release("voicechat");
699948
+ } else {
699949
+ await this.listen.stop();
699950
+ }
699634
699951
  } catch {
699635
699952
  }
699636
699953
  this.releaseLiveResources();
@@ -738730,8 +739047,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
738730
739047
  // Listen mode (transcribe-cli integration + live voice session)
738731
739048
  async listenToggle() {
738732
739049
  const engine = getListenEngine();
738733
- if (engine.isActive) {
738734
- const text2 = await engine.stop();
739050
+ if (engine.isActive && engine.hasOwner("listen")) {
739051
+ const text2 = await engine.release("listen");
738735
739052
  statusBar.setRecording(false);
738736
739053
  if (voiceSession?.isActive) {
738737
739054
  const sessionMsg = await voiceSession.stop();
@@ -738774,7 +739091,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
738774
739091
  engine.on("info", (msg2) => {
738775
739092
  writeContent(() => renderInfo(msg2));
738776
739093
  });
738777
- const msg = await engine.start();
739094
+ const msg = await engine.acquire("listen");
738778
739095
  if (voiceEngine.enabled && voiceEngine.ready) {
738779
739096
  try {
738780
739097
  voiceSession = new VoiceSession();
@@ -738803,6 +739120,39 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
738803
739120
  }
738804
739121
  return msg;
738805
739122
  },
739123
+ async listenStart(owner = "live") {
739124
+ const engine = getListenEngine();
739125
+ const available = await engine.isAvailable();
739126
+ if (!available) {
739127
+ return "ASR runtime unavailable. Omnius attempted managed Whisper setup; check ~/.omnius/runtimes/asr and CUDA PyTorch.";
739128
+ }
739129
+ const uiEngine = engine;
739130
+ if (!uiEngine.__omniusLiveAsrUiWired) {
739131
+ uiEngine.__omniusLiveAsrUiWired = true;
739132
+ engine.on("recording", (active) => {
739133
+ statusBar.setRecording(active);
739134
+ });
739135
+ engine.on("level", (evt) => {
739136
+ statusBar.setMicActivity(evt.speechActive, evt.levelDb);
739137
+ });
739138
+ engine.on("error", (err) => {
739139
+ writeContent(() => renderWarning(`Live ASR error: ${err.message}`));
739140
+ });
739141
+ engine.on("info", (msg) => {
739142
+ if (/CUDA|Whisper|consensus|ASR|VAD|torch/i.test(msg)) {
739143
+ writeContent(() => renderInfo(`[asr] ${msg}`));
739144
+ }
739145
+ });
739146
+ }
739147
+ return engine.acquire(owner);
739148
+ },
739149
+ async listenRelease(owner = "live") {
739150
+ const engine = getListenEngine();
739151
+ return engine.release(owner);
739152
+ },
739153
+ listenIsActive() {
739154
+ return getListenEngine().isActive;
739155
+ },
738806
739156
  async listenSetModel(model) {
738807
739157
  const engine = getListenEngine();
738808
739158
  engine.setModel(model);
@@ -738815,8 +739165,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
738815
739165
  },
738816
739166
  async listenStop() {
738817
739167
  const engine = getListenEngine();
738818
- const text2 = await engine.stop();
738819
- statusBar.setRecording(false);
739168
+ const text2 = engine.hasOwner("listen") ? await engine.release("listen") : engine.ownerCount() > 0 ? "Shared ASR is still running for /live, /voicechat, or /call." : await engine.stop();
739169
+ if (engine.ownerCount() === 0) statusBar.setRecording(false);
738820
739170
  if (voiceSession?.isActive) {
738821
739171
  const runtime = voiceSession.runtime;
738822
739172
  await voiceSession.stop();
@@ -739128,19 +739478,22 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
739128
739478
  writeContent(() => renderInfo(voiceMsg));
739129
739479
  }
739130
739480
  const { VoiceChatSession: VoiceChatSession2 } = await Promise.resolve().then(() => (init_voicechat(), voicechat_exports));
739131
- const { ListenEngine: ListenEngine2 } = await Promise.resolve().then(() => (init_listen(), listen_exports));
739132
- const listenEng = new ListenEngine2();
739133
- listenEng.on("recording", (active) => {
739134
- statusBar.setRecording(active);
739135
- });
739136
- listenEng.on("level", (evt) => {
739137
- statusBar.setMicActivity(evt.speechActive, evt.levelDb);
739138
- });
739139
- listenEng.on("info", (msg) => {
739140
- if (/consensus rejected|loading whisper|whisper model loaded|Whisper VAD|no accepted speech text|very short utterance/i.test(msg)) {
739141
- writeContent(() => renderInfo(`[voicechat/asr] ${msg}`));
739142
- }
739143
- });
739481
+ const listenEng = getListenEngine();
739482
+ const voicechatUiEngine = listenEng;
739483
+ if (!voicechatUiEngine.__omniusVoiceChatAsrUiWired) {
739484
+ voicechatUiEngine.__omniusVoiceChatAsrUiWired = true;
739485
+ listenEng.on("recording", (active) => {
739486
+ statusBar.setRecording(active);
739487
+ });
739488
+ listenEng.on("level", (evt) => {
739489
+ statusBar.setMicActivity(evt.speechActive, evt.levelDb);
739490
+ });
739491
+ listenEng.on("info", (msg) => {
739492
+ if (/consensus rejected|loading whisper|whisper model loaded|Whisper VAD|no accepted speech text|very short utterance/i.test(msg)) {
739493
+ writeContent(() => renderInfo(`[voicechat/asr] ${msg}`));
739494
+ }
739495
+ });
739496
+ }
739144
739497
  const summaryRunner = {
739145
739498
  injectUserMessage(content) {
739146
739499
  if (activeTask?.runner) {
@@ -739148,6 +739501,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
739148
739501
  }
739149
739502
  }
739150
739503
  };
739504
+ const voicechatPartialPrefix = `\x1B[38;5;244m[hearing]\x1B[0m`;
739151
739505
  _voiceChatSession2 = new VoiceChatSession2({
739152
739506
  voice: voiceEngine,
739153
739507
  listen: listenEng,
@@ -739305,11 +739659,20 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
739305
739659
  },
739306
739660
  onUserSpeech(text2) {
739307
739661
  writeContent(
739308
- () => renderVoiceChatCoalescedRow(`\x1B[38;5;45m[you]\x1B[0m ${text2}`)
739662
+ () => renderVoiceChatCoalescedRow(`\x1B[38;5;45m[you]\x1B[0m ${text2}`, {
739663
+ replaceLastPrefix: voicechatPartialPrefix
739664
+ })
739309
739665
  );
739310
739666
  },
739311
- // Suppressed to keep main loop quiet
739312
- onPartialTranscript(_text) {
739667
+ onPartialTranscript(text2) {
739668
+ const cleaned = text2.trim();
739669
+ if (!cleaned) return;
739670
+ writeContent(
739671
+ () => renderVoiceChatCoalescedRow(
739672
+ `${voicechatPartialPrefix} ${truncateByLines(cleaned, 2, 240)}`,
739673
+ { replaceLastPrefix: voicechatPartialPrefix }
739674
+ )
739675
+ );
739313
739676
  },
739314
739677
  onAgentSpeech(text2) {
739315
739678
  writeContent(
@@ -739322,6 +739685,15 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
739322
739685
  onStateChange(_state4) {
739323
739686
  }
739324
739687
  });
739688
+ _voiceChatSession2.on("consensusFiltered", (evt) => {
739689
+ const text2 = String(evt?.text ?? "").trim();
739690
+ writeContent(
739691
+ () => renderVoiceChatCoalescedRow(
739692
+ `\x1B[38;5;244m[filtered]\x1B[0m no ASR consensus${text2 ? `: ${truncateByLines(text2, 1, 180)}` : ""}`,
739693
+ { replaceLastPrefix: voicechatPartialPrefix }
739694
+ )
739695
+ );
739696
+ });
739325
739697
  await _voiceChatSession2.start();
739326
739698
  },
739327
739699
  async voiceChatStop() {
@@ -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.436",
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.436",
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.436",
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",