omnius 1.0.408 → 1.0.409

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
  }
@@ -601694,10 +601899,13 @@ function defaultAsciiPreviewSize(dimensions) {
601694
601899
  const columns = process.stdout.columns || 100;
601695
601900
  const rows = process.stdout.rows || 32;
601696
601901
  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);
601902
+ const height = defaultAsciiPreviewHeight(width, dimensions, rows);
601699
601903
  return { width, height };
601700
601904
  }
601905
+ function defaultAsciiPreviewHeight(width, dimensions, rows = process.stdout.rows || 32) {
601906
+ const imageAspect = dimensions && dimensions.width > 0 && dimensions.height > 0 ? dimensions.height / dimensions.width : 1;
601907
+ return clamp7(Math.min(Math.round(width * imageAspect * TERMINAL_CELL_ASPECT), rows - 10), 8, 42);
601908
+ }
601701
601909
  function readImageDimensions2(imagePath) {
601702
601910
  try {
601703
601911
  const buf = readFileSync87(imagePath);
@@ -601959,7 +602167,7 @@ async function buildImageAsciiPreview(inputPath, options2 = {}) {
601959
602167
  const dimensions = readImageDimensions2(imagePath);
601960
602168
  const defaults3 = defaultAsciiPreviewSize(dimensions);
601961
602169
  const width = clamp7(options2.width ?? defaults3.width, 24, 140);
601962
- const height = clamp7(options2.height ?? defaults3.height, 6, 60);
602170
+ const height = clamp7(options2.height ?? defaultAsciiPreviewHeight(width, dimensions), 6, 60);
601963
602171
  const timeoutMs = clamp7(options2.timeoutMs ?? 5e3, 500, 3e4);
601964
602172
  if (options2.preferPackage !== false) {
601965
602173
  const result = await convertWithImageToAscii(imagePath, width, height, timeoutMs);
@@ -602189,9 +602397,66 @@ function compactSourceId(source) {
602189
602397
  return source.replace(/^\/dev\//, "");
602190
602398
  }
602191
602399
  function truncateCell(value2, width) {
602400
+ value2 = stripDashboardAnsi(String(value2 ?? "")).replace(/\r?\n/g, " ");
602192
602401
  if (width <= 1) return value2.slice(0, Math.max(0, width));
602193
602402
  return value2.length <= width ? value2.padEnd(width, " ") : `${value2.slice(0, Math.max(1, width - 1))}…`;
602194
602403
  }
602404
+ function stripDashboardAnsi(value2) {
602405
+ return value2.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
602406
+ }
602407
+ function dashboardPreviewLines(camera) {
602408
+ const raw = camera.plainAscii || camera.ascii || "";
602409
+ return raw.replace(/\r/g, "").split("\n").map((line) => stripDashboardAnsi(line).replace(/\s+$/g, "")).filter((line) => line.trim().length > 0).slice(0, 10);
602410
+ }
602411
+ function centerDashboardCell(value2, width) {
602412
+ const text2 = stripDashboardAnsi(String(value2 ?? "")).replace(/\r?\n/g, " ");
602413
+ if (width <= 1) return text2.slice(0, Math.max(0, width));
602414
+ const clipped = text2.length > width ? `${text2.slice(0, Math.max(1, width - 1))}…` : text2;
602415
+ const left = Math.floor((width - clipped.length) / 2);
602416
+ return `${" ".repeat(Math.max(0, left))}${clipped}${" ".repeat(Math.max(0, width - clipped.length - left))}`;
602417
+ }
602418
+ function dashboardPreviewAspect(lines) {
602419
+ const widths = lines.map((line) => stripDashboardAnsi(line).length).filter((n2) => n2 > 0);
602420
+ const maxWidth = Math.max(1, ...widths);
602421
+ return Math.max(0.12, Math.min(1.6, lines.length / maxWidth));
602422
+ }
602423
+ function fitDashboardPane(lines, paneWidth, paneHeight) {
602424
+ const source = lines.length > 0 ? lines : ["preview pending"];
602425
+ const normalized = [];
602426
+ for (let i2 = 0; i2 < paneHeight; i2++) {
602427
+ const sourceIndex = source.length <= paneHeight ? i2 : Math.min(source.length - 1, Math.floor(i2 * source.length / paneHeight));
602428
+ normalized.push(truncateCell(source[sourceIndex] ?? "", paneWidth));
602429
+ }
602430
+ while (normalized.length < paneHeight) normalized.push(" ".repeat(paneWidth));
602431
+ return normalized;
602432
+ }
602433
+ function pushDashboardWrapped(lines, value2, width) {
602434
+ const contentWidth = Math.max(10, width - 2);
602435
+ const words = stripDashboardAnsi(value2).replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
602436
+ if (words.length === 0) {
602437
+ lines.push(`│${" ".repeat(contentWidth)}│`);
602438
+ return;
602439
+ }
602440
+ let current = "";
602441
+ for (const word2 of words) {
602442
+ if (!current) {
602443
+ current = word2.length > contentWidth ? word2.slice(0, contentWidth) : word2;
602444
+ continue;
602445
+ }
602446
+ if (current.length + 1 + word2.length <= contentWidth) {
602447
+ current += ` ${word2}`;
602448
+ } else {
602449
+ lines.push(`│${truncateCell(` ${current}`, contentWidth)}│`);
602450
+ current = word2.length > contentWidth ? word2.slice(0, contentWidth) : word2;
602451
+ }
602452
+ }
602453
+ if (current) lines.push(`│${truncateCell(` ${current}`, contentWidth)}│`);
602454
+ }
602455
+ function isAudioFailureText(value2) {
602456
+ const text2 = String(value2 ?? "").trim();
602457
+ if (!text2) return false;
602458
+ return /^(?:ASR|VAD|Transcription)\s+(?:failed|unavailable)|No module named ['"]?transcribe_cli|torchcodec|torchaudio version/i.test(text2);
602459
+ }
602195
602460
  function summarizeInference(value2) {
602196
602461
  if (!value2) return "objects: pending";
602197
602462
  const lines = value2.split("\n").map((line) => line.trim()).filter(Boolean);
@@ -602224,38 +602489,54 @@ function formatLiveDashboardFromSnapshot(snapshot, opts = {}) {
602224
602489
  if (cameras.length === 0) {
602225
602490
  lines.push(`│${truncateCell(" no camera frames captured yet", width - 2)}│`);
602226
602491
  }
602492
+ if (cameras.length > 0) {
602493
+ lines.push(`├${horizontal}┤`);
602494
+ const paneCount = Math.min(cameras.length, width >= 116 ? 3 : width >= 78 ? 2 : 1);
602495
+ const innerWidth = width - 2;
602496
+ const gutter = paneCount - 1;
602497
+ const paneWidth = Math.max(24, Math.floor((innerWidth - gutter) / paneCount));
602498
+ for (let start2 = 0; start2 < cameras.length; start2 += paneCount) {
602499
+ const group = cameras.slice(start2, start2 + paneCount);
602500
+ const sourceLines = group.map((camera) => dashboardPreviewLines(camera));
602501
+ const paneHeight = Math.max(7, Math.min(22, Math.round(paneWidth * Math.max(...sourceLines.map(dashboardPreviewAspect)))));
602502
+ const panes = sourceLines.map((previewLines) => fitDashboardPane(previewLines, paneWidth, paneHeight));
602503
+ for (let row = 0; row < paneHeight; row++) {
602504
+ const body = panes.map((pane) => pane[row] ?? " ".repeat(paneWidth)).join(" ");
602505
+ lines.push(`│${truncateCell(body, innerWidth)}│`);
602506
+ }
602507
+ const labels = group.map((camera) => {
602508
+ const age = Math.max(0, Math.round((now2 - camera.updatedAt) / 1e3));
602509
+ return centerDashboardCell(`${compactSourceId(camera.source)} age=${age}s`, paneWidth);
602510
+ }).join(" ");
602511
+ lines.push(`│${truncateCell(labels, innerWidth)}│`);
602512
+ }
602513
+ }
602514
+ if (cameras.length > 0) {
602515
+ lines.push(`├${horizontal}┤`);
602516
+ lines.push(`│${truncateCell(" camera observations", width - 2)}│`);
602517
+ }
602227
602518
  for (const camera of cameras) {
602228
602519
  const age = Math.max(0, Math.round((now2 - camera.updatedAt) / 1e3));
602229
602520
  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
602521
  const detailLines = [
602522
+ `${compactSourceId(camera.source)} age=${age}s rotation=${rotation}`,
602234
602523
  cameraSnapshotSummary(camera),
602235
602524
  camera.framePath ? `frame: ${camera.displayPath || camera.framePath}` : "",
602236
602525
  camera.orientation?.evidence ? `orientation: ${trimOneLine(camera.orientation.evidence, 120)}` : ""
602237
602526
  ].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}│`);
602527
+ for (const detail of detailLines.length ? detailLines : ["preview pending"]) {
602528
+ pushDashboardWrapped(lines, detail, width);
602251
602529
  }
602252
602530
  }
602253
602531
  if (snapshot.audio) {
602254
602532
  lines.push(`├${horizontal}┤`);
602533
+ const transcriptFailure = isAudioFailureText(snapshot.audio.transcript) ? snapshot.audio.transcript : "";
602534
+ const transcript = transcriptFailure ? "" : snapshot.audio.transcript;
602535
+ const audioError = [snapshot.audio.error, transcriptFailure].filter(Boolean).join(" | ");
602255
602536
  const audioBits = [
602256
602537
  `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)}` : "",
602538
+ audioError ? `error=${trimOneLine(audioError, 120)}` : "",
602539
+ transcript ? `asr=${trimOneLine(transcript, 120)}` : "",
602259
602540
  snapshot.audio.analysis ? `sound=${trimOneLine(snapshot.audio.analysis, 120)}` : ""
602260
602541
  ].filter(Boolean);
602261
602542
  for (const item of audioBits.slice(0, 4)) lines.push(`│${truncateCell(` ${item}`, width - 2)}│`);
@@ -602607,6 +602888,9 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
602607
602888
  }
602608
602889
  }
602609
602890
  if (snapshot.audio) {
602891
+ const transcriptFailure = isAudioFailureText(snapshot.audio.transcript) ? snapshot.audio.transcript : "";
602892
+ const transcript = transcriptFailure ? "" : snapshot.audio.transcript;
602893
+ const audioError = [snapshot.audio.error, transcriptFailure].filter(Boolean).join(" | ");
602610
602894
  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
602895
  if (audioEvents.length > 0) {
602612
602896
  lines.push("");
@@ -602623,11 +602907,11 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
602623
602907
  lines.push("");
602624
602908
  lines.push("## LIVE AUDIO LATEST SAMPLE");
602625
602909
  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}`);
602910
+ if (audioError) lines.push(`error: ${audioError}`);
602627
602911
  if (snapshot.audio.recordingPath) lines.push(`recording: ${snapshot.audio.recordingPath}`);
602628
- if (snapshot.audio.transcript) {
602912
+ if (transcript) {
602629
602913
  lines.push("Live ASR:");
602630
- lines.push(cleanPreview(snapshot.audio.transcript, 1200));
602914
+ lines.push(cleanPreview(transcript, 1200));
602631
602915
  }
602632
602916
  if (snapshot.audio.analysis) {
602633
602917
  lines.push("Live sound analysis:");
@@ -603195,7 +603479,8 @@ var init_live_sensors = __esm({
603195
603479
  const framePath = extractSavedImagePath(result.output, this.repoRoot);
603196
603480
  if (!framePath) return { ok: false, message: "Camera capture succeeded but no saved image path was returned." };
603197
603481
  const displayPath = relative11(this.repoRoot, framePath).startsWith("..") ? framePath : relative11(this.repoRoot, framePath);
603198
- const preview = await buildImageAsciiPreview(framePath, { width: 72, height: 22 });
603482
+ const previewWidth = Math.max(48, Math.min(96, (process.stdout.columns || 100) - 14));
603483
+ const preview = await buildImageAsciiPreview(framePath, { width: previewWidth });
603199
603484
  const asciiContext = preview ? formatImageAsciiContext(preview, displayPath) : void 0;
603200
603485
  const observedAt = Date.now();
603201
603486
  const cameraView = {
@@ -603204,6 +603489,7 @@ var init_live_sensors = __esm({
603204
603489
  framePath,
603205
603490
  displayPath,
603206
603491
  ascii: preview?.ascii,
603492
+ plainAscii: preview?.plainAscii,
603207
603493
  renderer: preview?.renderer,
603208
603494
  asciiContext,
603209
603495
  rotation,
@@ -603401,6 +603687,7 @@ ${output}`).join("\n\n");
603401
603687
  const recordingPath = join124(outputDir2, `audio-${Date.now()}.wav`);
603402
603688
  let transcript = "";
603403
603689
  let analysis = "";
603690
+ const audioErrors = [];
603404
603691
  const capture = await recordAudioSampleWithRecovery(input, this.devices.audioInputs, 3, recordingPath);
603405
603692
  if (!capture.ok) {
603406
603693
  const message2 = `Audio capture failed from ${input}; tried fallbacks: ${capture.errors.slice(0, 5).join(" | ")}`;
@@ -603426,9 +603713,13 @@ ${output}`).join("\n\n");
603426
603713
  if (this.config.asrEnabled) {
603427
603714
  try {
603428
603715
  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"}`;
603716
+ if (asr.success) {
603717
+ transcript = cleanPreview(typeof asr.llmContent === "string" ? asr.llmContent : "", 1600);
603718
+ } else {
603719
+ audioErrors.push(`ASR unavailable: ${asr.error || asr.output || "unknown error"}`);
603720
+ }
603430
603721
  } catch (err) {
603431
- transcript = `ASR failed: ${err instanceof Error ? err.message : String(err)}`;
603722
+ audioErrors.push(`ASR unavailable: ${err instanceof Error ? err.message : String(err)}`);
603432
603723
  }
603433
603724
  }
603434
603725
  if (this.config.audioAnalysisEnabled) {
@@ -603442,9 +603733,10 @@ ${output}`).join("\n\n");
603442
603733
  }
603443
603734
  try {
603444
603735
  const vad = await analyzer.execute({ action: "vad", file: recordingPath });
603445
- parts.push(vad.success ? cleanPreview(vad.output, 700) : `vad failed: ${vad.error}`);
603736
+ if (vad.success) parts.push(cleanPreview(vad.output, 700));
603737
+ else audioErrors.push(`VAD unavailable: ${vad.error || vad.output || "unknown error"}`);
603446
603738
  } catch (err) {
603447
- parts.push(`vad failed: ${err instanceof Error ? err.message : String(err)}`);
603739
+ audioErrors.push(`VAD unavailable: ${err instanceof Error ? err.message : String(err)}`);
603448
603740
  }
603449
603741
  analysis = parts.filter(Boolean).join("\n");
603450
603742
  }
@@ -603454,7 +603746,8 @@ ${output}`).join("\n\n");
603454
603746
  output: this.config.selectedAudioOutput,
603455
603747
  recordingPath,
603456
603748
  transcript,
603457
- analysis
603749
+ analysis,
603750
+ error: audioErrors.length > 0 ? audioErrors.join("\n") : void 0
603458
603751
  };
603459
603752
  const audioEvents = [];
603460
603753
  if (transcript) {
@@ -655292,12 +655585,17 @@ function renderLiveFeedback(ctx3, manager, feedback) {
655292
655585
  return;
655293
655586
  }
655294
655587
  const stamp = new Date(feedback.observedAt).toISOString();
655588
+ const explicitImagePreview = (feedback.kind === "camera-frame" || feedback.kind === "face-crop") && feedback.ascii && feedback.displayPath && feedback.renderer;
655589
+ if (ctx3.liveDynamicBlockHost && !explicitImagePreview) {
655590
+ renderLiveDashboard(ctx3, manager);
655591
+ return;
655592
+ }
655295
655593
  if (feedback.kind === "error") {
655296
655594
  renderWarning(`[live ${stamp}] ${feedback.title}
655297
655595
  ${feedback.message}`);
655298
655596
  return;
655299
655597
  }
655300
- if ((feedback.kind === "camera-frame" || feedback.kind === "face-crop") && feedback.ascii && feedback.displayPath && feedback.renderer) {
655598
+ if (explicitImagePreview && feedback.displayPath && feedback.ascii && feedback.renderer) {
655301
655599
  renderInfo(`[live ${stamp}] ${feedback.message}`);
655302
655600
  renderImageAsciiPreview(feedback.title, feedback.displayPath, feedback.ascii, feedback.renderer);
655303
655601
  return;
@@ -733836,6 +734134,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
733836
734134
  } else {
733837
734135
  parts.push("active: no");
733838
734136
  }
734137
+ const liveContext = formatLiveSensorContext(repoRoot);
734138
+ if (liveContext) parts.push(liveContext);
733839
734139
  return parts.join("\n");
733840
734140
  }
733841
734141
  },
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.408",
3
+ "version": "1.0.409",
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.409",
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.409",
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",