omnius 1.0.408 → 1.0.410

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
@@ -41519,7 +41519,7 @@ function formatTime(seconds) {
41519
41519
  const s2 = Math.floor(seconds % 60);
41520
41520
  return `${String(m2).padStart(2, "0")}:${String(s2).padStart(2, "0")}`;
41521
41521
  }
41522
- var AUDIO_EXTS, VIDEO_EXTS, MAX_TRANSCRIBE_URL_BYTES, _tcModule, _tcChecked, TranscribeFileTool, YT_DLP_VENV, TranscribeUrlTool, YouTubeDownloadTool;
41522
+ var AUDIO_EXTS, VIDEO_EXTS, MAX_TRANSCRIBE_URL_BYTES, MANAGED_ASR_VENV, _tcModule, _tcChecked, _managedAsrReady, TranscribeFileTool, YT_DLP_VENV, TranscribeUrlTool, YouTubeDownloadTool;
41523
41523
  var init_transcribe_tool = __esm({
41524
41524
  "packages/execution/dist/tools/transcribe-tool.js"() {
41525
41525
  "use strict";
@@ -41550,8 +41550,10 @@ var init_transcribe_tool = __esm({
41550
41550
  ".ts"
41551
41551
  ]);
41552
41552
  MAX_TRANSCRIBE_URL_BYTES = 100 * 1024 * 1024;
41553
+ MANAGED_ASR_VENV = join40(homedir11(), ".omnius", "runtimes", "asr", ".venv-whisper");
41553
41554
  _tcModule = null;
41554
41555
  _tcChecked = false;
41556
+ _managedAsrReady = false;
41555
41557
  TranscribeFileTool = class {
41556
41558
  name = "transcribe_file";
41557
41559
  description = "Transcribe a local audio or video file to text using Whisper (faster-whisper). Supports MP3, WAV, FLAC, AAC, M4A, OGG, MP4, MKV, AVI, MOV, WebM. Returns the full transcription text with optional speaker diarization. Transcription is 100% local — no API keys needed.";
@@ -41625,102 +41627,131 @@ var init_transcribe_tool = __esm({
41625
41627
  }
41626
41628
  if (effectiveModel !== askedModel)
41627
41629
  model = effectiveModel;
41630
+ const failures = [];
41628
41631
  const tc = await loadTranscribeCli();
41629
- if (!tc) {
41630
- return this.execViaCli(filePath, model, diarize, start2);
41631
- }
41632
+ if (tc) {
41633
+ try {
41634
+ const result = await withProcessEnv(transcriptionPythonEnv(), () => tc.transcribe(filePath, {
41635
+ model,
41636
+ format: "json",
41637
+ diarize,
41638
+ wordTimestamps: true
41639
+ }));
41640
+ return this.formatTranscriptionResult(filePath, model, result, start2, "transcribe-cli");
41641
+ } catch (err) {
41642
+ failures.push(`transcribe-cli module: ${err instanceof Error ? err.message : String(err)}`);
41643
+ }
41644
+ }
41645
+ const cli = await this.execViaCli(filePath, model, diarize, start2);
41646
+ if (cli.success)
41647
+ return cli;
41648
+ if (cli.error)
41649
+ failures.push(cli.error);
41650
+ const managed = await this.execViaManagedWhisper(filePath, model, start2);
41651
+ if (managed.success)
41652
+ return managed;
41653
+ if (managed.error)
41654
+ failures.push(managed.error);
41655
+ return {
41656
+ success: false,
41657
+ output: "",
41658
+ error: `Transcription failed after all ASR backends. ${failures.join(" | ").slice(0, 1200)}`,
41659
+ durationMs: performance.now() - start2
41660
+ };
41661
+ }
41662
+ formatTranscriptionResult(filePath, model, result, start2, backend) {
41663
+ const segments = Array.isArray(result.segments) ? result.segments : [];
41664
+ const text2 = String(result.text ?? result.fullText ?? "").trim();
41665
+ const speakers = Array.isArray(result.speakers) ? result.speakers : [];
41666
+ const wordCount2 = Number(result.wordCount ?? (text2 ? text2.split(/\s+/).filter(Boolean).length : 0));
41667
+ const transcriptDir = join40(this.workingDir, ".omnius", "transcripts");
41668
+ mkdirSync22(transcriptDir, { recursive: true });
41669
+ const fileBase = basename7(filePath).replace(/\.[^.]+$/, "");
41670
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
41671
+ const txtFile = join40(transcriptDir, `${fileBase}-${timestamp}.txt`);
41672
+ const jsonFile = join40(transcriptDir, `${fileBase}-${timestamp}.json`);
41673
+ writeFileSync21(txtFile, text2, "utf-8");
41674
+ const structured = {
41675
+ source: filePath,
41676
+ model,
41677
+ backend,
41678
+ language: result.language,
41679
+ duration: result.duration,
41680
+ wordCount: wordCount2,
41681
+ speakers,
41682
+ transcribedAt: (/* @__PURE__ */ new Date()).toISOString(),
41683
+ segments: segments.map((seg) => ({
41684
+ start: Number(seg.start ?? 0),
41685
+ end: Number(seg.end ?? seg.start ?? 0),
41686
+ text: String(seg.text ?? ""),
41687
+ speaker: seg.speaker || void 0
41688
+ })),
41689
+ fullText: text2
41690
+ };
41691
+ writeFileSync21(jsonFile, JSON.stringify(structured, null, 2), "utf-8");
41632
41692
  try {
41633
- const result = await withProcessEnv(transcriptionPythonEnv(), () => tc.transcribe(filePath, {
41634
- model,
41635
- format: "json",
41636
- diarize,
41637
- wordTimestamps: true
41638
- // Always get timestamps for structured output
41639
- }));
41640
- const transcriptDir = join40(this.workingDir, ".omnius", "transcripts");
41641
- mkdirSync22(transcriptDir, { recursive: true });
41642
- const fileBase = basename7(filePath).replace(/\.[^.]+$/, "");
41643
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
41644
- const txtFile = join40(transcriptDir, `${fileBase}-${timestamp}.txt`);
41645
- writeFileSync21(txtFile, result.text, "utf-8");
41646
- const jsonFile = join40(transcriptDir, `${fileBase}-${timestamp}.json`);
41647
- const structured = {
41648
- source: filePath,
41649
- model,
41650
- language: result.language,
41651
- duration: result.duration,
41652
- wordCount: result.wordCount,
41653
- speakers: result.speakers,
41654
- transcribedAt: (/* @__PURE__ */ new Date()).toISOString(),
41655
- segments: result.segments.map((seg) => ({
41656
- start: seg.start,
41657
- end: seg.end,
41658
- text: seg.text,
41659
- speaker: seg.speaker || void 0
41660
- })),
41661
- fullText: result.text
41662
- };
41663
- writeFileSync21(jsonFile, JSON.stringify(structured, null, 2), "utf-8");
41693
+ const memDir = join40(this.workingDir, ".omnius", "memory");
41694
+ mkdirSync22(memDir, { recursive: true });
41695
+ const memFile = join40(memDir, "transcripts.json");
41696
+ let memEntries = [];
41664
41697
  try {
41665
- const memDir = join40(this.workingDir, ".omnius", "memory");
41666
- mkdirSync22(memDir, { recursive: true });
41667
- const memFile = join40(memDir, "transcripts.json");
41668
- let memEntries = [];
41669
- try {
41670
- memEntries = JSON.parse(readFileSync30(memFile, "utf-8"));
41671
- } catch {
41672
- }
41673
- memEntries.push({
41674
- source: basename7(filePath),
41675
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
41676
- duration: result.duration,
41677
- wordCount: result.wordCount,
41678
- language: result.language,
41679
- summary: result.text.slice(0, 200),
41680
- jsonPath: jsonFile,
41681
- txtPath: txtFile
41682
- });
41683
- if (memEntries.length > 50)
41684
- memEntries = memEntries.slice(-50);
41685
- writeFileSync21(memFile, JSON.stringify(memEntries, null, 2), "utf-8");
41698
+ memEntries = JSON.parse(readFileSync30(memFile, "utf-8"));
41686
41699
  } catch {
41687
41700
  }
41688
- const lines = [
41689
- `Transcription of: ${basename7(filePath)}`,
41690
- `Model: ${model} | Language: ${result.language} | Duration: ${result.duration ? `${result.duration.toFixed(1)}s` : "unknown"}`,
41691
- `Words: ${result.wordCount} | Segments: ${result.segments.length}`,
41692
- `Saved: ${txtFile} (text) + ${jsonFile} (structured with timestamps)`,
41693
- ""
41694
- ];
41695
- if (result.speakers.length > 1) {
41696
- lines.push(`Speakers: ${result.speakers.join(", ")}`);
41697
- lines.push("");
41698
- }
41699
- if (result.segments.length > 0) {
41700
- for (const seg of result.segments) {
41701
- const ts = `[${formatTime(seg.start)} → ${formatTime(seg.end)}]`;
41702
- const speaker = seg.speaker ? `${seg.speaker}: ` : "";
41703
- lines.push(`${ts} ${speaker}${seg.text}`);
41704
- }
41705
- } else {
41706
- lines.push(result.text);
41701
+ memEntries.push({
41702
+ source: basename7(filePath),
41703
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
41704
+ duration: result.duration,
41705
+ wordCount: wordCount2,
41706
+ language: result.language,
41707
+ backend,
41708
+ summary: text2.slice(0, 200),
41709
+ jsonPath: jsonFile,
41710
+ txtPath: txtFile
41711
+ });
41712
+ if (memEntries.length > 50)
41713
+ memEntries = memEntries.slice(-50);
41714
+ writeFileSync21(memFile, JSON.stringify(memEntries, null, 2), "utf-8");
41715
+ } catch {
41716
+ }
41717
+ const lines = [
41718
+ `Transcription of: ${basename7(filePath)}`,
41719
+ `Backend: ${backend}`,
41720
+ `Model: ${model} | Language: ${result.language ?? "unknown"} | Duration: ${result.duration ? `${Number(result.duration).toFixed(1)}s` : "unknown"}`,
41721
+ `Words: ${wordCount2} | Segments: ${segments.length}`,
41722
+ `Saved: ${txtFile} (text) + ${jsonFile} (structured with timestamps)`,
41723
+ ""
41724
+ ];
41725
+ if (speakers.length > 1) {
41726
+ lines.push(`Speakers: ${speakers.join(", ")}`);
41727
+ lines.push("");
41728
+ }
41729
+ if (segments.length > 0) {
41730
+ for (const seg of segments) {
41731
+ const ts = `[${formatTime(Number(seg.start ?? 0))} → ${formatTime(Number(seg.end ?? seg.start ?? 0))}]`;
41732
+ const speaker = seg.speaker ? `${seg.speaker}: ` : "";
41733
+ lines.push(`${ts} ${speaker}${String(seg.text ?? "").trim()}`);
41707
41734
  }
41708
- return {
41709
- success: true,
41710
- output: lines.join("\n"),
41711
- durationMs: performance.now() - start2
41712
- };
41713
- } catch (err) {
41735
+ } else {
41736
+ lines.push(text2 || "[silent — no speech detected]");
41737
+ }
41738
+ return {
41739
+ success: true,
41740
+ output: lines.join("\n"),
41741
+ llmContent: text2,
41742
+ durationMs: performance.now() - start2
41743
+ };
41744
+ }
41745
+ /** Fallback: invoke transcribe-cli as a subprocess. */
41746
+ async execViaCli(filePath, model, diarize, start2) {
41747
+ if (!await commandExists("transcribe-cli", 1500)) {
41714
41748
  return {
41715
41749
  success: false,
41716
41750
  output: "",
41717
- error: `Transcription failed: ${err instanceof Error ? err.message : String(err)}`,
41751
+ error: "transcribe-cli command unavailable",
41718
41752
  durationMs: performance.now() - start2
41719
41753
  };
41720
41754
  }
41721
- }
41722
- /** Fallback: invoke transcribe-cli as a subprocess. */
41723
- async execViaCli(filePath, model, diarize, start2) {
41724
41755
  try {
41725
41756
  const args = [filePath, "-m", model, "-f", "txt"];
41726
41757
  if (diarize)
@@ -41734,6 +41765,7 @@ var init_transcribe_tool = __esm({
41734
41765
  return {
41735
41766
  success: true,
41736
41767
  output: output.trim() || "(empty transcription)",
41768
+ llmContent: output.trim() || "",
41737
41769
  durationMs: performance.now() - start2
41738
41770
  };
41739
41771
  } catch (err) {
@@ -41747,6 +41779,112 @@ var init_transcribe_tool = __esm({
41747
41779
  };
41748
41780
  }
41749
41781
  }
41782
+ async ensureManagedWhisperRuntime() {
41783
+ const python = venvPython(MANAGED_ASR_VENV);
41784
+ if (!existsSync38(python)) {
41785
+ mkdirSync22(join40(homedir11(), ".omnius", "runtimes", "asr"), { recursive: true });
41786
+ await execFileText("python3", ["-m", "venv", MANAGED_ASR_VENV], {
41787
+ timeout: 6e4,
41788
+ env: transcriptionPythonEnv()
41789
+ });
41790
+ }
41791
+ if (_managedAsrReady)
41792
+ return python;
41793
+ try {
41794
+ await execFileText(python, ["-c", "import whisper, numpy"], {
41795
+ timeout: 1e4,
41796
+ env: transcriptionPythonEnv()
41797
+ });
41798
+ _managedAsrReady = true;
41799
+ return python;
41800
+ } catch {
41801
+ await execFileText(python, [
41802
+ "-m",
41803
+ "pip",
41804
+ "install",
41805
+ "-U",
41806
+ "pip",
41807
+ "wheel",
41808
+ "setuptools",
41809
+ "numpy",
41810
+ "openai-whisper"
41811
+ ], {
41812
+ timeout: 9e5,
41813
+ env: transcriptionPythonEnv()
41814
+ });
41815
+ _managedAsrReady = true;
41816
+ return python;
41817
+ }
41818
+ }
41819
+ async execViaManagedWhisper(filePath, model, start2) {
41820
+ const scriptPath2 = join40(this.workingDir, ".omnius", "tmp", `managed-whisper-${Date.now()}.py`);
41821
+ mkdirSync22(join40(this.workingDir, ".omnius", "tmp"), { recursive: true });
41822
+ const script = `
41823
+ import json, os, warnings
41824
+ warnings.filterwarnings("ignore")
41825
+ os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "3")
41826
+ audio_file = ${JSON.stringify(filePath)}
41827
+ model_name = ${JSON.stringify(model)}
41828
+ try:
41829
+ import whisper
41830
+ device = "cpu"
41831
+ try:
41832
+ import torch
41833
+ if torch.cuda.is_available():
41834
+ device = "cuda"
41835
+ except Exception:
41836
+ pass
41837
+ m = whisper.load_model(model_name, device=device)
41838
+ result = m.transcribe(audio_file, fp16=(device == "cuda"), condition_on_previous_text=False)
41839
+ segments = []
41840
+ for seg in result.get("segments", []) or []:
41841
+ segments.append({"start": float(seg.get("start", 0)), "end": float(seg.get("end", seg.get("start", 0))), "text": str(seg.get("text", "")).strip()})
41842
+ text = str(result.get("text", "")).strip()
41843
+ print(json.dumps({
41844
+ "ok": True,
41845
+ "text": text,
41846
+ "language": result.get("language"),
41847
+ "duration": segments[-1]["end"] if segments else None,
41848
+ "wordCount": len([w for w in text.split() if w]),
41849
+ "speakers": [],
41850
+ "segments": segments,
41851
+ }))
41852
+ except Exception as exc:
41853
+ print(json.dumps({"ok": False, "error": str(exc)}))
41854
+ `;
41855
+ try {
41856
+ writeFileSync21(scriptPath2, script, "utf-8");
41857
+ const python = await this.ensureManagedWhisperRuntime();
41858
+ const raw = await execFileText(python, [scriptPath2], {
41859
+ timeout: 6e5,
41860
+ cwd: this.workingDir,
41861
+ env: transcriptionPythonEnv({ PYTHONUNBUFFERED: "1" }),
41862
+ maxBuffer: 16 * 1024 * 1024
41863
+ });
41864
+ const parsed = JSON.parse(raw.trim().split("\n").reverse().find((line) => line.trim().startsWith("{")) ?? "{}");
41865
+ if (!parsed.ok) {
41866
+ return {
41867
+ success: false,
41868
+ output: "",
41869
+ error: `managed openai-whisper failed: ${parsed.error || raw.slice(-500)}`,
41870
+ durationMs: performance.now() - start2
41871
+ };
41872
+ }
41873
+ return this.formatTranscriptionResult(filePath, model, parsed, start2, "managed openai-whisper");
41874
+ } catch (err) {
41875
+ return {
41876
+ success: false,
41877
+ output: "",
41878
+ error: `managed openai-whisper failed: ${err instanceof Error ? err.message : String(err)}`,
41879
+ durationMs: performance.now() - start2
41880
+ };
41881
+ } finally {
41882
+ try {
41883
+ unlinkSync5(scriptPath2);
41884
+ } catch {
41885
+ }
41886
+ }
41887
+ }
41750
41888
  };
41751
41889
  YT_DLP_VENV = join40(homedir11(), ".omnius", "runtimes", "media-tools", ".venv-ytdlp");
41752
41890
  TranscribeUrlTool = class {
@@ -547804,17 +547942,23 @@ except Exception as e:
547804
547942
  const audioFile = await this.ensureAudioFile(args);
547805
547943
  if (!audioFile)
547806
547944
  return { success: false, output: "", error: "No audio available.", durationMs: performance.now() - start2 };
547807
- await this.ensureVenv(["torch", "torchaudio", "numpy", "soundfile"]);
547945
+ await this.ensureVenv(["torch", "numpy", "soundfile", "resampy"]);
547808
547946
  const audioLiteral = JSON.stringify(audioFile);
547809
547947
  const script = `
547810
547948
  import sys, json, numpy as np
547811
547949
  try:
547812
- import torch, torchaudio, soundfile as sf
547950
+ import torch, soundfile as sf, resampy
547813
547951
 
547814
547952
  model, utils = torch.hub.load(repo_or_dir='snakers4/silero-vad', model='silero_vad', force_reload=False, trust_repo=True)
547815
- (get_speech_timestamps, _, read_audio, *_) = utils
547953
+ (get_speech_timestamps, *_) = utils
547816
547954
 
547817
- wav = read_audio(${audioLiteral}, sampling_rate=16000)
547955
+ audio, sr = sf.read(${audioLiteral})
547956
+ if len(getattr(audio, "shape", [])) > 1:
547957
+ audio = audio.mean(axis=1)
547958
+ if sr != 16000:
547959
+ audio = resampy.resample(audio, sr, 16000)
547960
+ audio = audio.astype(np.float32)
547961
+ wav = torch.from_numpy(audio)
547818
547962
  speech_timestamps = get_speech_timestamps(wav, model, sampling_rate=16000, threshold=0.5)
547819
547963
 
547820
547964
  total_samples = len(wav)
@@ -547825,6 +547969,7 @@ try:
547825
547969
 
547826
547970
  print(json.dumps({
547827
547971
  "success": True,
547972
+ "backend": "silero-vad",
547828
547973
  "voice_detected": len(speech_timestamps) > 0,
547829
547974
  "speech_percentage": round(speech_pct, 1),
547830
547975
  "speech_segments": len(speech_timestamps),
@@ -547832,7 +547977,57 @@ try:
547832
547977
  "segments": segments[:10]
547833
547978
  }))
547834
547979
  except Exception as e:
547835
- print(json.dumps({"success": False, "error": str(e)}))
547980
+ try:
547981
+ import soundfile as sf
547982
+ import resampy
547983
+ audio, sr = sf.read(${audioLiteral})
547984
+ if len(getattr(audio, "shape", [])) > 1:
547985
+ audio = audio.mean(axis=1)
547986
+ if sr != 16000:
547987
+ audio = resampy.resample(audio, sr, 16000)
547988
+ audio = audio.astype(np.float32)
547989
+ frame = max(1, int(0.03 * 16000))
547990
+ hop = max(1, int(0.01 * 16000))
547991
+ rms = []
547992
+ for i in range(0, max(1, len(audio) - frame + 1), hop):
547993
+ chunk = audio[i:i+frame]
547994
+ if len(chunk) == 0:
547995
+ continue
547996
+ rms.append(float(np.sqrt(np.mean(chunk * chunk))))
547997
+ arr = np.array(rms, dtype=np.float32)
547998
+ if len(arr) == 0:
547999
+ active = np.array([], dtype=bool)
548000
+ else:
548001
+ floor = float(np.percentile(arr, 35))
548002
+ peak = float(np.max(arr))
548003
+ threshold = max(0.008, floor * 2.5, peak * 0.12)
548004
+ active = arr > threshold
548005
+ segments = []
548006
+ start_idx = None
548007
+ for idx, flag in enumerate(active.tolist()):
548008
+ if flag and start_idx is None:
548009
+ start_idx = idx
548010
+ if (not flag or idx == len(active) - 1) and start_idx is not None:
548011
+ end_idx = idx if not flag else idx + 1
548012
+ start_s = start_idx * hop / 16000.0
548013
+ end_s = min(len(audio) / 16000.0, (end_idx * hop + frame) / 16000.0)
548014
+ if end_s - start_s >= 0.12:
548015
+ segments.append({"start_s": start_s, "end_s": end_s, "duration_s": end_s - start_s})
548016
+ start_idx = None
548017
+ speech_duration = sum(seg["duration_s"] for seg in segments)
548018
+ total_duration = len(audio) / 16000.0 if len(audio) else 0
548019
+ print(json.dumps({
548020
+ "success": True,
548021
+ "backend": "energy-vad-fallback",
548022
+ "warning": "silero unavailable: " + str(e),
548023
+ "voice_detected": len(segments) > 0,
548024
+ "speech_percentage": round((speech_duration / total_duration * 100) if total_duration else 0, 1),
548025
+ "speech_segments": len(segments),
548026
+ "total_duration_s": round(total_duration, 2),
548027
+ "segments": segments[:10]
548028
+ }))
548029
+ except Exception as fallback:
548030
+ print(json.dumps({"success": False, "error": str(e) + "; fallback failed: " + str(fallback)}))
547836
548031
  `;
547837
548032
  return await this.runPythonScript(script, start2, "Voice activity detection");
547838
548033
  }
@@ -555516,6 +555711,9 @@ function summarizeObjects(objects) {
555516
555711
  }
555517
555712
  return [...counts.entries()].sort((a2, b) => b[1].count - a2[1].count).slice(0, 12).map(([label, value2]) => `${label} x${value2.count} mean_conf=${(value2.confidence / Math.max(1, value2.count)).toFixed(2)}`);
555518
555713
  }
555714
+ function isDiagnosticAudioEvidence(text2) {
555715
+ return /^(?:ASR|VAD|Audio comprehension)\s+(?:failed|unavailable|completed)|No module named ['"]?transcribe_cli|torchcodec|torchaudio version|no speech detected|empty transcription/i.test(text2.trim());
555716
+ }
555519
555717
  function observationsFromResult(result, extraText) {
555520
555718
  const observations = [];
555521
555719
  for (const obj of result.objects) {
@@ -555541,6 +555739,8 @@ function observationsFromResult(result, extraText) {
555541
555739
  });
555542
555740
  }
555543
555741
  for (const text2 of extraText) {
555742
+ if (isDiagnosticAudioEvidence(text2))
555743
+ continue;
555544
555744
  observations.push({
555545
555745
  type: "transcript",
555546
555746
  confidence: 0.75,
@@ -555958,7 +556158,12 @@ var init_live_media_loop = __esm({
555958
556158
  try {
555959
556159
  const transcribe = new TranscribeFileTool(this.workingDir);
555960
556160
  const asr = await transcribe.execute({ path: result.media_path, model: "tiny" });
555961
- audioEvidence.push(asr.success ? String(asr.llmContent || asr.output || "").slice(0, 3e3) : `ASR unavailable: ${asr.error}`);
556161
+ if (asr.success) {
556162
+ const transcript = typeof asr.llmContent === "string" ? asr.llmContent.trim() : "";
556163
+ audioEvidence.push(transcript ? transcript.slice(0, 3e3) : "ASR completed: no speech detected");
556164
+ } else {
556165
+ audioEvidence.push(`ASR unavailable: ${asr.error}`);
556166
+ }
555962
556167
  } catch (err) {
555963
556168
  audioEvidence.push(`ASR failed: ${err instanceof Error ? err.message : String(err)}`);
555964
556169
  }
@@ -601681,7 +601886,8 @@ __export(image_ascii_preview_exports, {
601681
601886
  buildImageAsciiPreview: () => buildImageAsciiPreview,
601682
601887
  defaultAsciiPreviewSize: () => defaultAsciiPreviewSize,
601683
601888
  extractSavedImagePath: () => extractSavedImagePath,
601684
- formatImageAsciiContext: () => formatImageAsciiContext
601889
+ formatImageAsciiContext: () => formatImageAsciiContext,
601890
+ isLowInformationAsciiPreview: () => isLowInformationAsciiPreview
601685
601891
  });
601686
601892
  import { createRequire as createRequire5 } from "node:module";
601687
601893
  import { existsSync as existsSync108, readFileSync as readFileSync87, statSync as statSync42 } from "node:fs";
@@ -601694,10 +601900,13 @@ function defaultAsciiPreviewSize(dimensions) {
601694
601900
  const columns = process.stdout.columns || 100;
601695
601901
  const rows = process.stdout.rows || 32;
601696
601902
  const width = clamp7(columns - 14, 42, 96);
601697
- const imageAspect = dimensions && dimensions.width > 0 && dimensions.height > 0 ? dimensions.height / dimensions.width : 1;
601698
- const height = clamp7(Math.min(Math.round(width * imageAspect * TERMINAL_CELL_ASPECT), rows - 10), 8, 42);
601903
+ const height = defaultAsciiPreviewHeight(width, dimensions, rows);
601699
601904
  return { width, height };
601700
601905
  }
601906
+ function defaultAsciiPreviewHeight(width, dimensions, rows = process.stdout.rows || 32) {
601907
+ const imageAspect = dimensions && dimensions.width > 0 && dimensions.height > 0 ? dimensions.height / dimensions.width : 1;
601908
+ return clamp7(Math.min(Math.round(width * imageAspect * TERMINAL_CELL_ASPECT), rows - 10), 8, 42);
601909
+ }
601701
601910
  function readImageDimensions2(imagePath) {
601702
601911
  try {
601703
601912
  const buf = readFileSync87(imagePath);
@@ -601846,6 +602055,15 @@ function normalizeImageToAsciiResult(converted, width, height) {
601846
602055
  if (!ascii2) return { ascii: null, error: "empty normalized renderer output" };
601847
602056
  return { ascii: ascii2, plainAscii: plainAscii || void 0 };
601848
602057
  }
602058
+ function isLowInformationAsciiPreview(ascii2) {
602059
+ const plain = stripAsciiAnsi(String(ascii2 ?? "")).replace(/\r/g, "").split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).join("\n");
602060
+ if (!plain) return true;
602061
+ const visible = plain.replace(/\s/g, "");
602062
+ if (visible.length < 12) return true;
602063
+ const unique2 = new Set(Array.from(visible));
602064
+ if (unique2.size <= 2 && /[█▓▒░#@]/u.test(visible)) return true;
602065
+ return false;
602066
+ }
601849
602067
  function previewFailure(message2, width) {
601850
602068
  const clean5 = message2.replace(/\s+/g, " ").trim();
601851
602069
  const text2 = `[image-to-ascii preview failed: ${clean5 || "unknown error"}]`;
@@ -601959,15 +602177,19 @@ async function buildImageAsciiPreview(inputPath, options2 = {}) {
601959
602177
  const dimensions = readImageDimensions2(imagePath);
601960
602178
  const defaults3 = defaultAsciiPreviewSize(dimensions);
601961
602179
  const width = clamp7(options2.width ?? defaults3.width, 24, 140);
601962
- const height = clamp7(options2.height ?? defaults3.height, 6, 60);
602180
+ const height = clamp7(options2.height ?? defaultAsciiPreviewHeight(width, dimensions), 6, 60);
601963
602181
  const timeoutMs = clamp7(options2.timeoutMs ?? 5e3, 500, 3e4);
601964
602182
  if (options2.preferPackage !== false) {
601965
602183
  const result = await convertWithImageToAscii(imagePath, width, height, timeoutMs);
601966
602184
  if (result.ascii) {
602185
+ let plainAscii = result.plainAscii;
602186
+ if (isLowInformationAsciiPreview(plainAscii)) {
602187
+ plainAscii = await convertWithFfmpeg(imagePath, width, height, timeoutMs) ?? plainAscii;
602188
+ }
601967
602189
  return {
601968
602190
  path: imagePath,
601969
602191
  ascii: result.ascii,
601970
- plainAscii: result.plainAscii,
602192
+ plainAscii,
601971
602193
  renderer: "image-to-ascii",
601972
602194
  width,
601973
602195
  height
@@ -602189,9 +602411,238 @@ function compactSourceId(source) {
602189
602411
  return source.replace(/^\/dev\//, "");
602190
602412
  }
602191
602413
  function truncateCell(value2, width) {
602414
+ value2 = stripDashboardAnsi(String(value2 ?? "")).replace(/\r?\n/g, " ");
602192
602415
  if (width <= 1) return value2.slice(0, Math.max(0, width));
602193
602416
  return value2.length <= width ? value2.padEnd(width, " ") : `${value2.slice(0, Math.max(1, width - 1))}…`;
602194
602417
  }
602418
+ function stripDashboardAnsi(value2) {
602419
+ return value2.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
602420
+ }
602421
+ function dashboardPreviewLines(camera) {
602422
+ const raw = camera.plainAscii && !isLowInformationAsciiPreview(camera.plainAscii) ? camera.plainAscii : "";
602423
+ return raw.replace(/\r/g, "").split("\n").map((line) => stripDashboardAnsi(line).replace(/\s+$/g, "")).filter((line) => line.trim().length > 0);
602424
+ }
602425
+ function dashboardPreviewAspect(lines) {
602426
+ const widths = lines.map((line) => stripDashboardAnsi(line).length).filter((n2) => n2 > 0);
602427
+ const maxWidth = Math.max(1, ...widths);
602428
+ return Math.max(0.12, Math.min(1.6, lines.length / maxWidth));
602429
+ }
602430
+ function fitDashboardPane(lines, paneWidth, paneHeight, fallback) {
602431
+ const source = lines.length > 0 ? lines : [fallback];
602432
+ const normalized = [];
602433
+ for (let i2 = 0; i2 < paneHeight; i2++) {
602434
+ const sourceIndex = source.length <= paneHeight ? i2 : Math.min(source.length - 1, Math.floor(i2 * source.length / paneHeight));
602435
+ normalized.push(truncateCell(source[sourceIndex] ?? "", paneWidth));
602436
+ }
602437
+ while (normalized.length < paneHeight) normalized.push(" ".repeat(paneWidth));
602438
+ return normalized;
602439
+ }
602440
+ function liveDashboardPaneLayout(width, cameraCount) {
602441
+ const innerWidth = Math.max(10, width - 2);
602442
+ const paneCount = Math.max(1, Math.min(cameraCount || 1, width >= 92 ? 2 : 1));
602443
+ const gutter = paneCount - 1;
602444
+ const paneWidth = Math.max(28, Math.floor((innerWidth - gutter) / paneCount));
602445
+ return { innerWidth, paneCount, paneWidth, paneContentWidth: Math.max(10, paneWidth - 2) };
602446
+ }
602447
+ function liveDashboardPreviewWidthForSources(sourceCount, termWidth = process.stdout.columns || 100) {
602448
+ const { paneContentWidth } = liveDashboardPaneLayout(Math.max(60, termWidth), sourceCount);
602449
+ return Math.max(42, Math.min(120, paneContentWidth));
602450
+ }
602451
+ function formatCameraPane(camera, paneWidth, paneHeight, now2) {
602452
+ const contentWidth = Math.max(10, paneWidth - 2);
602453
+ const age = Math.max(0, Math.round((now2 - camera.updatedAt) / 1e3));
602454
+ const title = `${compactSourceId(camera.source)} ${age}s`;
602455
+ const topTitle = ` ${title} `;
602456
+ const left = Math.max(0, Math.floor((contentWidth - topTitle.length) / 2));
602457
+ const right = Math.max(0, contentWidth - topTitle.length - left);
602458
+ const top = `╭${"─".repeat(left)}${topTitle.slice(0, contentWidth)}${"─".repeat(right)}╮`;
602459
+ const bottom = `╰${"─".repeat(contentWidth)}╯`;
602460
+ const previewLines = dashboardPreviewLines(camera);
602461
+ const fallback = camera.framePath ? "preview refreshing" : camera.error ? "capture failed" : "awaiting frame";
602462
+ const body = fitDashboardPane(previewLines, contentWidth, paneHeight, fallback);
602463
+ return [top, ...body.map((line) => `│${truncateCell(line, contentWidth)}│`), bottom];
602464
+ }
602465
+ function pushDashboardWrapped(lines, value2, width) {
602466
+ const contentWidth = Math.max(10, width - 2);
602467
+ const words = stripDashboardAnsi(value2).replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
602468
+ if (words.length === 0) {
602469
+ lines.push(`│${" ".repeat(contentWidth)}│`);
602470
+ return;
602471
+ }
602472
+ let current = "";
602473
+ for (const word2 of words) {
602474
+ if (!current) {
602475
+ current = word2.length > contentWidth ? word2.slice(0, contentWidth) : word2;
602476
+ continue;
602477
+ }
602478
+ if (current.length + 1 + word2.length <= contentWidth) {
602479
+ current += ` ${word2}`;
602480
+ } else {
602481
+ lines.push(`│${truncateCell(` ${current}`, contentWidth)}│`);
602482
+ current = word2.length > contentWidth ? word2.slice(0, contentWidth) : word2;
602483
+ }
602484
+ }
602485
+ if (current) lines.push(`│${truncateCell(` ${current}`, contentWidth)}│`);
602486
+ }
602487
+ function isAudioFailureText(value2) {
602488
+ const text2 = String(value2 ?? "").trim();
602489
+ if (!text2) return false;
602490
+ return /^(?:ASR|VAD|Transcription)\s+(?:failed|unavailable)|No module named ['"]?transcribe_cli|torchcodec|torchaudio version/i.test(text2);
602491
+ }
602492
+ function readPcm16WavActivity(filePath, bins = 36) {
602493
+ try {
602494
+ const buf = readFileSync88(filePath);
602495
+ if (buf.length < 44 || buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") return void 0;
602496
+ let offset = 12;
602497
+ let channels = 1;
602498
+ let bitsPerSample = 16;
602499
+ let dataStart = -1;
602500
+ let dataSize = 0;
602501
+ while (offset + 8 <= buf.length) {
602502
+ const id2 = buf.toString("ascii", offset, offset + 4);
602503
+ const size = buf.readUInt32LE(offset + 4);
602504
+ const start2 = offset + 8;
602505
+ if (id2 === "fmt " && start2 + 16 <= buf.length) {
602506
+ channels = Math.max(1, buf.readUInt16LE(start2 + 2));
602507
+ bitsPerSample = buf.readUInt16LE(start2 + 14);
602508
+ } else if (id2 === "data") {
602509
+ dataStart = start2;
602510
+ dataSize = Math.min(size, buf.length - start2);
602511
+ break;
602512
+ }
602513
+ offset = start2 + size + size % 2;
602514
+ }
602515
+ if (dataStart < 0 || dataSize <= 0 || bitsPerSample !== 16) return void 0;
602516
+ const frameBytes = channels * 2;
602517
+ const frames = Math.floor(dataSize / frameBytes);
602518
+ if (frames <= 0) return void 0;
602519
+ let sumSquares = 0;
602520
+ let peak = 0;
602521
+ let active = 0;
602522
+ const bucketSquares = new Array(Math.max(1, bins)).fill(0);
602523
+ const bucketCounts = new Array(Math.max(1, bins)).fill(0);
602524
+ for (let i2 = 0; i2 < frames; i2++) {
602525
+ let sampleSum = 0;
602526
+ for (let ch = 0; ch < channels; ch++) {
602527
+ sampleSum += buf.readInt16LE(dataStart + i2 * frameBytes + ch * 2) / 32768;
602528
+ }
602529
+ const sample = sampleSum / channels;
602530
+ const abs = Math.abs(sample);
602531
+ peak = Math.max(peak, abs);
602532
+ sumSquares += sample * sample;
602533
+ if (abs > 0.018) active++;
602534
+ const b = Math.min(bucketSquares.length - 1, Math.floor(i2 / frames * bucketSquares.length));
602535
+ bucketSquares[b] += sample * sample;
602536
+ bucketCounts[b] += 1;
602537
+ }
602538
+ const rms = Math.sqrt(sumSquares / frames);
602539
+ const chars = "▁▂▃▄▅▆▇█";
602540
+ const waveform = bucketSquares.map((sum, idx) => {
602541
+ const count = Math.max(1, bucketCounts[idx] ?? 1);
602542
+ const level = Math.sqrt(sum / count);
602543
+ const normalized = Math.max(0, Math.min(1, level / Math.max(0.02, peak || 0.02)));
602544
+ return chars[Math.min(chars.length - 1, Math.round(normalized * (chars.length - 1)))] ?? "▁";
602545
+ }).join("");
602546
+ return {
602547
+ rms: Number(rms.toFixed(4)),
602548
+ peak: Number(peak.toFixed(4)),
602549
+ rmsDb: Number((20 * Math.log10(Math.max(1e-6, rms))).toFixed(1)),
602550
+ activeRatio: Number((active / frames).toFixed(3)),
602551
+ waveform
602552
+ };
602553
+ } catch {
602554
+ return void 0;
602555
+ }
602556
+ }
602557
+ function extractAsrBackend(output) {
602558
+ const match = String(output ?? "").match(/^Backend:\s*(.+)$/m);
602559
+ if (!match?.[1]) return void 0;
602560
+ const backend = match[1].trim();
602561
+ if (/managed openai-whisper|transcribe-cli|whisper/i.test(backend)) return backend;
602562
+ return backend.slice(0, 80);
602563
+ }
602564
+ function parseLiveAudioIntakeDecision(value2) {
602565
+ const parsed = parseJsonObjectFromText(value2);
602566
+ if (!parsed) return null;
602567
+ const intended = parsed["intended_for_agent"] ?? parsed["intendedForAgent"] ?? parsed["addressed_to_agent"];
602568
+ const confidence2 = Math.max(0, Math.min(1, Number(parsed["confidence"] ?? 0)));
602569
+ if (typeof intended !== "boolean" || !Number.isFinite(confidence2)) return null;
602570
+ const actionRaw = String(parsed["action"] ?? (intended ? "respond" : "ignore")).toLowerCase();
602571
+ const action = actionRaw === "respond" || actionRaw === "monitor" || actionRaw === "ignore" ? actionRaw : intended ? "respond" : "ignore";
602572
+ return {
602573
+ intendedForAgent: intended,
602574
+ confidence: confidence2,
602575
+ action,
602576
+ reason: trimOneLine(parsed["reason"] ?? "", 220) || "intake classifier decision"
602577
+ };
602578
+ }
602579
+ function fallbackLiveAudioIntakeDecision(transcript) {
602580
+ const text2 = transcript.toLowerCase();
602581
+ const addressed = /\b(omnius|hey\s+omnius|assistant|agent|computer)\b/.test(text2) || /\?$/.test(transcript.trim());
602582
+ return {
602583
+ intendedForAgent: addressed,
602584
+ confidence: addressed ? 0.58 : 0.35,
602585
+ action: addressed ? "respond" : "monitor",
602586
+ reason: addressed ? "fallback detected direct address or question shape" : "fallback could not establish that ambient speech was addressed to Omnius"
602587
+ };
602588
+ }
602589
+ async function classifyLiveAudioIntake(transcript, config) {
602590
+ const now2 = Date.now();
602591
+ const clean5 = transcript.trim();
602592
+ if (!clean5) {
602593
+ return {
602594
+ intendedForAgent: false,
602595
+ confidence: 1,
602596
+ action: "ignore",
602597
+ reason: "empty transcript",
602598
+ source: "fallback",
602599
+ updatedAt: now2
602600
+ };
602601
+ }
602602
+ if (config?.backendUrl && config.model) {
602603
+ const url = `${config.backendUrl.replace(/\/+$/, "")}/v1/chat/completions`;
602604
+ const body = {
602605
+ model: config.model,
602606
+ temperature: 0,
602607
+ max_tokens: 180,
602608
+ think: false,
602609
+ messages: [
602610
+ {
602611
+ role: "system",
602612
+ content: [
602613
+ "You are a live-audio intake classifier for Omnius.",
602614
+ "Decide whether a transcribed utterance is intended for the agent or is ambient/background speech.",
602615
+ 'Return only JSON: {"intended_for_agent": boolean, "confidence": 0.0-1.0, "action": "respond"|"ignore"|"monitor", "reason": "brief evidence"}.',
602616
+ "Do not answer the utterance. Do not treat all room speech as addressed to the agent."
602617
+ ].join("\n")
602618
+ },
602619
+ { role: "user", content: clean5.slice(0, 1200) }
602620
+ ]
602621
+ };
602622
+ try {
602623
+ const controller = new AbortController();
602624
+ const timer = setTimeout(() => controller.abort(), 6e3).unref();
602625
+ const resp = await fetch(url, {
602626
+ method: "POST",
602627
+ headers: {
602628
+ "Content-Type": "application/json",
602629
+ ...config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}
602630
+ },
602631
+ body: JSON.stringify(body),
602632
+ signal: controller.signal
602633
+ });
602634
+ clearTimeout(timer);
602635
+ if (resp.ok) {
602636
+ const json = await resp.json();
602637
+ const content = json.choices?.[0]?.message?.content ?? "";
602638
+ const parsed = parseLiveAudioIntakeDecision(content);
602639
+ if (parsed) return { ...parsed, source: "intake-agent", updatedAt: now2 };
602640
+ }
602641
+ } catch {
602642
+ }
602643
+ }
602644
+ return { ...fallbackLiveAudioIntakeDecision(clean5), source: "fallback", updatedAt: now2 };
602645
+ }
602195
602646
  function summarizeInference(value2) {
602196
602647
  if (!value2) return "objects: pending";
602197
602648
  const lines = value2.split("\n").map((line) => line.trim()).filter(Boolean);
@@ -602224,41 +602675,53 @@ function formatLiveDashboardFromSnapshot(snapshot, opts = {}) {
602224
602675
  if (cameras.length === 0) {
602225
602676
  lines.push(`│${truncateCell(" no camera frames captured yet", width - 2)}│`);
602226
602677
  }
602678
+ if (cameras.length > 0) {
602679
+ lines.push(`├${horizontal}┤`);
602680
+ const { innerWidth, paneCount, paneWidth, paneContentWidth } = liveDashboardPaneLayout(width, cameras.length);
602681
+ for (let start2 = 0; start2 < cameras.length; start2 += paneCount) {
602682
+ const group = cameras.slice(start2, start2 + paneCount);
602683
+ const sourceLines = group.map((camera) => dashboardPreviewLines(camera));
602684
+ const aspect = Math.max(0.22, Math.min(0.7, Math.max(...sourceLines.map(dashboardPreviewAspect))));
602685
+ const paneHeight = Math.max(10, Math.min(32, Math.round(paneContentWidth * aspect)));
602686
+ const panes = group.map((camera) => formatCameraPane(camera, paneWidth, paneHeight, now2));
602687
+ const rowCount = Math.max(...panes.map((pane) => pane.length));
602688
+ for (let row = 0; row < rowCount; row++) {
602689
+ const body = panes.map((pane) => pane[row] ?? " ".repeat(paneWidth)).join(" ");
602690
+ lines.push(`│${truncateCell(body, innerWidth)}│`);
602691
+ }
602692
+ }
602693
+ }
602694
+ if (cameras.length > 0) {
602695
+ lines.push(`├${horizontal}┤`);
602696
+ lines.push(`│${truncateCell(" camera observations", width - 2)}│`);
602697
+ }
602227
602698
  for (const camera of cameras) {
602228
602699
  const age = Math.max(0, Math.round((now2 - camera.updatedAt) / 1e3));
602229
602700
  const rotation = camera.orientation ? formatCameraRotation(camera.orientation.rotation) : "upright/no correction";
602230
- lines.push(`├${horizontal}┤`);
602231
- lines.push(`│${truncateCell(` ${compactSourceId(camera.source)} age=${age}s rotation=${rotation}`, width - 2)}│`);
602232
- const asciiLines = (camera.ascii ?? "").split("\n").filter((line) => line.length > 0).slice(0, 10);
602233
602701
  const detailLines = [
602702
+ `${compactSourceId(camera.source)} age=${age}s rotation=${rotation}`,
602234
602703
  cameraSnapshotSummary(camera),
602235
602704
  camera.framePath ? `frame: ${camera.displayPath || camera.framePath}` : "",
602236
602705
  camera.orientation?.evidence ? `orientation: ${trimOneLine(camera.orientation.evidence, 120)}` : ""
602237
602706
  ].filter(Boolean);
602238
- if (asciiLines.length === 0) {
602239
- for (const detail of detailLines.length ? detailLines : ["preview pending"]) {
602240
- lines.push(`│${truncateCell(` ${detail}`, width - 2)}│`);
602241
- }
602242
- continue;
602243
- }
602244
- const imageWidth = Math.max(24, Math.min(52, Math.floor((width - 5) * 0.58)));
602245
- const detailWidth = Math.max(18, width - imageWidth - 5);
602246
- const rowCount = Math.max(asciiLines.length, detailLines.length);
602247
- for (let i2 = 0; i2 < rowCount; i2++) {
602248
- const image = truncateCell(asciiLines[i2] ?? "", imageWidth);
602249
- const detail = truncateCell(detailLines[i2] ?? "", detailWidth);
602250
- lines.push(`│ ${image} │ ${detail}│`);
602707
+ for (const detail of detailLines.length ? detailLines : ["preview pending"]) {
602708
+ pushDashboardWrapped(lines, detail, width);
602251
602709
  }
602252
602710
  }
602253
602711
  if (snapshot.audio) {
602254
602712
  lines.push(`├${horizontal}┤`);
602713
+ const transcriptFailure = isAudioFailureText(snapshot.audio.transcript) ? snapshot.audio.transcript : "";
602714
+ const transcript = transcriptFailure ? "" : snapshot.audio.transcript;
602715
+ const audioError = [snapshot.audio.error, transcriptFailure].filter(Boolean).join(" | ");
602255
602716
  const audioBits = [
602256
602717
  `audio input=${snapshot.audio.input || snapshot.config.selectedAudioInput || "none"}`,
602257
- snapshot.audio.error ? `error=${trimOneLine(snapshot.audio.error, 120)}` : "",
602258
- snapshot.audio.transcript ? `asr=${trimOneLine(snapshot.audio.transcript, 120)}` : "",
602718
+ snapshot.audio.activity ? `activity ${snapshot.audio.activity.waveform} rms=${snapshot.audio.activity.rmsDb.toFixed(1)}dB active=${Math.round(snapshot.audio.activity.activeRatio * 100)}%` : "",
602719
+ audioError ? `error=${trimOneLine(audioError, 120)}` : "",
602720
+ transcript ? `asr${snapshot.audio.asrBackend ? `(${snapshot.audio.asrBackend})` : ""}=${trimOneLine(transcript, 120)}` : "",
602721
+ snapshot.audio.intake ? `intake=${snapshot.audio.intake.intendedForAgent ? "for-agent" : "ambient"} conf=${snapshot.audio.intake.confidence.toFixed(2)} ${snapshot.audio.intake.action}` : "",
602259
602722
  snapshot.audio.analysis ? `sound=${trimOneLine(snapshot.audio.analysis, 120)}` : ""
602260
602723
  ].filter(Boolean);
602261
- for (const item of audioBits.slice(0, 4)) lines.push(`│${truncateCell(` ${item}`, width - 2)}│`);
602724
+ for (const item of audioBits.slice(0, 6)) lines.push(`│${truncateCell(` ${item}`, width - 2)}│`);
602262
602725
  }
602263
602726
  lines.push(`╰${horizontal}╯`);
602264
602727
  return lines;
@@ -602607,6 +603070,9 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
602607
603070
  }
602608
603071
  }
602609
603072
  if (snapshot.audio) {
603073
+ const transcriptFailure = isAudioFailureText(snapshot.audio.transcript) ? snapshot.audio.transcript : "";
603074
+ const transcript = transcriptFailure ? "" : snapshot.audio.transcript;
603075
+ const audioError = [snapshot.audio.error, transcriptFailure].filter(Boolean).join(" | ");
602610
603076
  const audioEvents = (snapshot.events ?? []).filter((event) => event.kind === "audio_transcript" || event.kind === "audio_sound").sort((a2, b) => b.observedAt - a2.observedAt).slice(0, 8);
602611
603077
  if (audioEvents.length > 0) {
602612
603078
  lines.push("");
@@ -602623,11 +603089,18 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
602623
603089
  lines.push("");
602624
603090
  lines.push("## LIVE AUDIO LATEST SAMPLE");
602625
603091
  lines.push(`timestamp: ${nowIso3(snapshot.audio.updatedAt)} age=${Math.max(0, Math.round(age / 1e3))}s${age > maxAgeMs ? " STALE" : ""}`);
602626
- if (snapshot.audio.error) lines.push(`error: ${snapshot.audio.error}`);
603092
+ if (audioError) lines.push(`error: ${audioError}`);
602627
603093
  if (snapshot.audio.recordingPath) lines.push(`recording: ${snapshot.audio.recordingPath}`);
602628
- if (snapshot.audio.transcript) {
602629
- lines.push("Live ASR:");
602630
- lines.push(cleanPreview(snapshot.audio.transcript, 1200));
603094
+ if (snapshot.audio.activity) {
603095
+ lines.push(`activity: ${snapshot.audio.activity.waveform} rms_db=${snapshot.audio.activity.rmsDb.toFixed(1)} peak=${snapshot.audio.activity.peak.toFixed(3)} active=${Math.round(snapshot.audio.activity.activeRatio * 100)}%`);
603096
+ }
603097
+ if (transcript) {
603098
+ lines.push(`Live ASR${snapshot.audio.asrBackend ? ` (${snapshot.audio.asrBackend})` : ""}:`);
603099
+ lines.push(cleanPreview(transcript, 1200));
603100
+ }
603101
+ if (snapshot.audio.intake) {
603102
+ lines.push(`Live audio intake: intended_for_agent=${snapshot.audio.intake.intendedForAgent ? "true" : "false"} confidence=${snapshot.audio.intake.confidence.toFixed(2)} action=${snapshot.audio.intake.action} source=${snapshot.audio.intake.source}`);
603103
+ lines.push(`intake_reason: ${snapshot.audio.intake.reason}`);
602631
603104
  }
602632
603105
  if (snapshot.audio.analysis) {
602633
603106
  lines.push("Live sound analysis:");
@@ -602890,6 +603363,7 @@ var init_live_sensors = __esm({
602890
603363
  lastAgentReviewAt = 0;
602891
603364
  autoOrientationInFlight = /* @__PURE__ */ new Set();
602892
603365
  lastFeedbackAt = /* @__PURE__ */ new Map();
603366
+ intakeAgentConfig;
602893
603367
  setStatusSink(sink) {
602894
603368
  this.statusSink = sink ?? null;
602895
603369
  this.emitStatus();
@@ -602900,6 +603374,9 @@ var init_live_sensors = __esm({
602900
603374
  setAgentActionSink(sink) {
602901
603375
  this.agentActionSink = sink ?? null;
602902
603376
  }
603377
+ setAudioIntakeAgentConfig(config) {
603378
+ this.intakeAgentConfig = config && config.backendUrl && config.model ? { backendUrl: config.backendUrl, model: config.model, apiKey: config.apiKey } : void 0;
603379
+ }
602903
603380
  getConfig() {
602904
603381
  return { ...this.config };
602905
603382
  }
@@ -603195,7 +603672,8 @@ var init_live_sensors = __esm({
603195
603672
  const framePath = extractSavedImagePath(result.output, this.repoRoot);
603196
603673
  if (!framePath) return { ok: false, message: "Camera capture succeeded but no saved image path was returned." };
603197
603674
  const displayPath = relative11(this.repoRoot, framePath).startsWith("..") ? framePath : relative11(this.repoRoot, framePath);
603198
- const preview = await buildImageAsciiPreview(framePath, { width: 72, height: 22 });
603675
+ const previewWidth = Math.max(42, Math.min(120, Math.floor(Number(options2.previewWidth ?? (process.stdout.columns || 100) - 14))));
603676
+ const preview = await buildImageAsciiPreview(framePath, { width: previewWidth });
603199
603677
  const asciiContext = preview ? formatImageAsciiContext(preview, displayPath) : void 0;
603200
603678
  const observedAt = Date.now();
603201
603679
  const cameraView = {
@@ -603204,6 +603682,7 @@ var init_live_sensors = __esm({
603204
603682
  framePath,
603205
603683
  displayPath,
603206
603684
  ascii: preview?.ascii,
603685
+ plainAscii: preview?.plainAscii,
603207
603686
  renderer: preview?.renderer,
603208
603687
  asciiContext,
603209
603688
  rotation,
@@ -603237,6 +603716,7 @@ var init_live_sensors = __esm({
603237
603716
  imagePath: framePath,
603238
603717
  displayPath,
603239
603718
  ascii: preview?.ascii,
603719
+ plainAscii: preview?.plainAscii,
603240
603720
  renderer: preview?.renderer
603241
603721
  });
603242
603722
  }
@@ -603246,6 +603726,7 @@ var init_live_sensors = __esm({
603246
603726
  framePath,
603247
603727
  displayPath,
603248
603728
  ascii: preview?.ascii,
603729
+ plainAscii: preview?.plainAscii,
603249
603730
  renderer: preview?.renderer
603250
603731
  };
603251
603732
  }
@@ -603291,8 +603772,8 @@ var init_live_sensors = __esm({
603291
603772
  return message2;
603292
603773
  }
603293
603774
  }
603294
- async sampleSingleVideoNow(source, forceInfer) {
603295
- const preview = await this.previewCamera(source, { emit: false });
603775
+ async sampleSingleVideoNow(source, forceInfer, previewWidth) {
603776
+ const preview = await this.previewCamera(source, { emit: false, previewWidth });
603296
603777
  let inference = "";
603297
603778
  let clipSummary = "";
603298
603779
  const orientation = this.getCameraOrientation(source);
@@ -603381,8 +603862,9 @@ ${clipSummary}` : ""].filter(Boolean).join("\n");
603381
603862
  const sources = this.activeVideoSources();
603382
603863
  if (sources.length === 0) return "No camera selected. Use /live refresh after connecting hardware.";
603383
603864
  const outputs = [];
603865
+ const previewWidth = liveDashboardPreviewWidthForSources(sources.length);
603384
603866
  for (const source of sources) {
603385
- outputs.push(await this.sampleSingleVideoNow(source, forceInfer));
603867
+ outputs.push(await this.sampleSingleVideoNow(source, forceInfer, previewWidth));
603386
603868
  }
603387
603869
  this.emitDashboard();
603388
603870
  return outputs.map((output, index) => `## Camera ${sources[index]}
@@ -603400,7 +603882,10 @@ ${output}`).join("\n\n");
603400
603882
  ensureLiveDir(this.repoRoot);
603401
603883
  const recordingPath = join124(outputDir2, `audio-${Date.now()}.wav`);
603402
603884
  let transcript = "";
603885
+ let asrBackend = "";
603403
603886
  let analysis = "";
603887
+ let intake;
603888
+ const audioErrors = [];
603404
603889
  const capture = await recordAudioSampleWithRecovery(input, this.devices.audioInputs, 3, recordingPath);
603405
603890
  if (!capture.ok) {
603406
603891
  const message2 = `Audio capture failed from ${input}; tried fallbacks: ${capture.errors.slice(0, 5).join(" | ")}`;
@@ -603423,12 +603908,18 @@ ${output}`).join("\n\n");
603423
603908
  if (capture.device !== this.config.selectedAudioInput) {
603424
603909
  this.config.selectedAudioInput = capture.device;
603425
603910
  }
603911
+ const activity = readPcm16WavActivity(recordingPath);
603426
603912
  if (this.config.asrEnabled) {
603427
603913
  try {
603428
603914
  const asr = await new TranscribeFileTool(this.repoRoot).execute({ path: recordingPath, model: "tiny" });
603429
- transcript = asr.success ? cleanPreview(asr.llmContent || asr.output, 1600) : `ASR failed: ${asr.error || asr.output || "unknown error"}`;
603915
+ if (asr.success) {
603916
+ transcript = cleanPreview(typeof asr.llmContent === "string" ? asr.llmContent : "", 1600);
603917
+ asrBackend = extractAsrBackend(asr.output) ?? "whisper";
603918
+ } else {
603919
+ audioErrors.push(`ASR unavailable: ${asr.error || asr.output || "unknown error"}`);
603920
+ }
603430
603921
  } catch (err) {
603431
- transcript = `ASR failed: ${err instanceof Error ? err.message : String(err)}`;
603922
+ audioErrors.push(`ASR unavailable: ${err instanceof Error ? err.message : String(err)}`);
603432
603923
  }
603433
603924
  }
603434
603925
  if (this.config.audioAnalysisEnabled) {
@@ -603442,19 +603933,27 @@ ${output}`).join("\n\n");
603442
603933
  }
603443
603934
  try {
603444
603935
  const vad = await analyzer.execute({ action: "vad", file: recordingPath });
603445
- parts.push(vad.success ? cleanPreview(vad.output, 700) : `vad failed: ${vad.error}`);
603936
+ if (vad.success) parts.push(cleanPreview(vad.output, 700));
603937
+ else audioErrors.push(`VAD unavailable: ${vad.error || vad.output || "unknown error"}`);
603446
603938
  } catch (err) {
603447
- parts.push(`vad failed: ${err instanceof Error ? err.message : String(err)}`);
603939
+ audioErrors.push(`VAD unavailable: ${err instanceof Error ? err.message : String(err)}`);
603448
603940
  }
603449
603941
  analysis = parts.filter(Boolean).join("\n");
603450
603942
  }
603943
+ if (transcript) {
603944
+ intake = await classifyLiveAudioIntake(transcript, this.intakeAgentConfig);
603945
+ }
603451
603946
  this.snapshot.audio = {
603452
603947
  updatedAt: Date.now(),
603453
603948
  input: capture.device,
603454
603949
  output: this.config.selectedAudioOutput,
603455
603950
  recordingPath,
603456
603951
  transcript,
603457
- analysis
603952
+ asrBackend: asrBackend || void 0,
603953
+ analysis,
603954
+ activity,
603955
+ intake,
603956
+ error: audioErrors.length > 0 ? audioErrors.join("\n") : void 0
603458
603957
  };
603459
603958
  const audioEvents = [];
603460
603959
  if (transcript) {
@@ -603481,12 +603980,20 @@ ${output}`).join("\n\n");
603481
603980
  }
603482
603981
  this.snapshot.events = mergeRecentById(this.snapshot.events, audioEvents, 50);
603483
603982
  this.persist();
603983
+ if (transcript && intake?.intendedForAgent) {
603984
+ this.maybeRequestAgentReview(
603985
+ "Live audio intake classified speech as intended for Omnius",
603986
+ `transcript=${transcript}; confidence=${intake.confidence.toFixed(2)}; reason=${intake.reason}`
603987
+ );
603988
+ }
603484
603989
  this.emitDashboard();
603485
603990
  return [
603486
603991
  `Audio sample captured from ${capture.device}${capture.device !== input ? ` (recovered from ${input})` : ""}: ${recordingPath}`,
603487
603992
  transcript ? `
603488
- ASR:
603993
+ ASR${asrBackend ? ` (${asrBackend})` : ""}:
603489
603994
  ${transcript}` : "",
603995
+ intake ? `
603996
+ Intake: ${intake.intendedForAgent ? "intended for agent" : "ambient/monitor"} confidence=${intake.confidence.toFixed(2)} (${intake.source})` : "",
603490
603997
  analysis ? `
603491
603998
  Sound analysis:
603492
603999
  ${analysis}` : ""
@@ -624102,15 +624609,6 @@ var init_status_bar = __esm({
624102
624609
  const cmdPrefix = cmd.startsWith("view:") ? "omnius-view:" + cmd.slice(5) : "omnius-cmd:" + cmd;
624103
624610
  return `\x1B]8;;${cmdPrefix}\x07${label}\x1B]8;;\x07`;
624104
624611
  };
624105
- const buttonFg = (cmd) => {
624106
- let fg2 = TEXT_DIM;
624107
- if (cmd === "voice" && this._voiceActive) fg2 = 82;
624108
- if (cmd.startsWith("live") && (this._liveMediaStatus.audio || this._liveMediaStatus.video)) fg2 = 82;
624109
- if (cmd === "nexus") {
624110
- fg2 = this._nexusStatus === "connected" ? 82 : this._nexusStatus === "connecting" ? 208 : 196;
624111
- }
624112
- return fg2;
624113
- };
624114
624612
  const decorateMenuButton = (cmd, label) => {
624115
624613
  return `${HEADER_BUTTON_GLYPH_FG}${HEADER_BUTTON_LEFT}\x1B[0m${HEADER_BUTTON_BG}${HEADER_BUTTON_FG}${label}\x1B[0m${PANEL_BG_SEQ}${HEADER_BUTTON_GLYPH_FG}${HEADER_BUTTON_RIGHT}\x1B[0m${PANEL_BG_SEQ}`;
624116
624614
  };
@@ -624121,14 +624619,7 @@ var init_status_bar = __esm({
624121
624619
  return `\x1B[38;5;${color}m${HEADER_BUTTON_LEFT}\x1B[0m${weight}${fg2}${bg}${content}\x1B[0m${PANEL_BG_SEQ}\x1B[38;5;${color}m${HEADER_BUTTON_RIGHT}\x1B[0m${PANEL_BG_SEQ}`;
624122
624620
  };
624123
624621
  const renderBtn = (cmd, label) => {
624124
- const fg2 = buttonFg(cmd);
624125
- const btnW = label.length + 2;
624126
- const availW2 = getTermWidth() - identity3.width - 1;
624127
- if (btnW > availW2) return linkify(cmd, `\x1B[38;5;${fg2}m${label}`);
624128
- return linkify(
624129
- cmd,
624130
- `${PANEL_BG_SEQ}${HEADER_BUTTON_GLYPH_FG}${HEADER_BUTTON_LEFT}\x1B[0m${PANEL_BG_SEQ}\x1B[38;5;${fg2}m${label}\x1B[0m${PANEL_BG_SEQ}${HEADER_BUTTON_GLYPH_FG}${HEADER_BUTTON_RIGHT}\x1B[0m${PANEL_BG_SEQ}`
624131
- );
624622
+ return linkify(cmd, decorateMenuButton(cmd, label));
624132
624623
  };
624133
624624
  const identity3 = this.buildHeaderIdentityRender();
624134
624625
  const modelLabel = this.summarizeHeaderModelName() || "model";
@@ -624201,10 +624692,9 @@ var init_status_bar = __esm({
624201
624692
  };
624202
624693
  if (this._activeViewId !== "main") {
624203
624694
  const mainLabel = ` ↩ main `;
624204
- const mainColored = `\x1B[38;5;110m${mainLabel}\x1B[0m`;
624205
624695
  sysItems.push({
624206
- render: () => mainColored + " ",
624207
- w: mainLabel.length + 1
624696
+ render: () => linkify("view:main", decorateMenuButton("view:main", mainLabel)) + " ",
624697
+ w: mainLabel.length + 2 + 1
624208
624698
  });
624209
624699
  }
624210
624700
  if (this._agentViews.size > 1) {
@@ -624242,10 +624732,7 @@ var init_status_bar = __esm({
624242
624732
  const telegramDot = this._telegramStatus.active ? "●" : "○";
624243
624733
  const telegramLabel = this._telegramStatus.activeSubAgents > 0 ? ` ✈ tg ${this._telegramStatus.activeSubAgents} ` : " ✈ tg ";
624244
624734
  sysItems.push({
624245
- render: () => renderBtn(
624246
- "telegram",
624247
- `${HEADER_TELEGRAM_FG}${telegramDot}${telegramLabel}\x1B[0m`
624248
- ) + " ",
624735
+ render: () => renderBtn("telegram", `${telegramDot}${telegramLabel}`) + " ",
624249
624736
  w: telegramLabel.length + 2
624250
624737
  });
624251
624738
  const liveMediaActive = this._liveMediaStatus.audio || this._liveMediaStatus.video;
@@ -624390,7 +624877,7 @@ var init_status_bar = __esm({
624390
624877
  const trunc3 = (s2) => s2.trim().split(/\s+/).slice(0, 3).join(" ");
624391
624878
  if (this._activeViewId !== "main") {
624392
624879
  const mainLabel = ` ↩ main `;
624393
- zones.push({ w: mainLabel.length + 1, id: "main", render: () => "" });
624880
+ zones.push({ w: mainLabel.length + 2 + 1, id: "main", render: () => "" });
624394
624881
  }
624395
624882
  if (this._agentViews.size > 1) {
624396
624883
  for (const view of this._agentViews.values()) {
@@ -655292,12 +655779,17 @@ function renderLiveFeedback(ctx3, manager, feedback) {
655292
655779
  return;
655293
655780
  }
655294
655781
  const stamp = new Date(feedback.observedAt).toISOString();
655782
+ const explicitImagePreview = (feedback.kind === "camera-frame" || feedback.kind === "face-crop") && feedback.ascii && feedback.displayPath && feedback.renderer;
655783
+ if (ctx3.liveDynamicBlockHost && !explicitImagePreview) {
655784
+ renderLiveDashboard(ctx3, manager);
655785
+ return;
655786
+ }
655295
655787
  if (feedback.kind === "error") {
655296
655788
  renderWarning(`[live ${stamp}] ${feedback.title}
655297
655789
  ${feedback.message}`);
655298
655790
  return;
655299
655791
  }
655300
- if ((feedback.kind === "camera-frame" || feedback.kind === "face-crop") && feedback.ascii && feedback.displayPath && feedback.renderer) {
655792
+ if (explicitImagePreview && feedback.displayPath && feedback.ascii && feedback.renderer) {
655301
655793
  renderInfo(`[live ${stamp}] ${feedback.message}`);
655302
655794
  renderImageAsciiPreview(feedback.title, feedback.displayPath, feedback.ascii, feedback.renderer);
655303
655795
  return;
@@ -655308,6 +655800,11 @@ ${feedback.message}`);
655308
655800
  function attachLiveSinks(ctx3, manager, agentEnabled = false) {
655309
655801
  manager.setStatusSink(ctx3.setLiveMediaStatus);
655310
655802
  manager.setFeedbackSink((feedback) => renderLiveFeedback(ctx3, manager, feedback));
655803
+ manager.setAudioIntakeAgentConfig({
655804
+ backendUrl: ctx3.config.backendUrl,
655805
+ model: ctx3.config.model,
655806
+ apiKey: ctx3.config.apiKey
655807
+ });
655311
655808
  if (agentEnabled && ctx3.startBackgroundPrompt) {
655312
655809
  manager.setAgentActionSink((prompt) => {
655313
655810
  const id2 = ctx3.startBackgroundPrompt?.(prompt);
@@ -733836,6 +734333,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
733836
734333
  } else {
733837
734334
  parts.push("active: no");
733838
734335
  }
734336
+ const liveContext = formatLiveSensorContext(repoRoot);
734337
+ if (liveContext) parts.push(liveContext);
733839
734338
  return parts.join("\n");
733840
734339
  }
733841
734340
  },
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.408",
3
+ "version": "1.0.410",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omnius",
9
- "version": "1.0.408",
9
+ "version": "1.0.410",
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.408",
3
+ "version": "1.0.410",
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",