omnius 1.0.410 → 1.0.412

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
@@ -41456,17 +41456,30 @@ function isTranscribable(path12) {
41456
41456
  const ext = extname4(path12).toLowerCase();
41457
41457
  return AUDIO_EXTS.has(ext) || VIDEO_EXTS.has(ext);
41458
41458
  }
41459
+ async function requireTranscribeCliFrom(packageDir) {
41460
+ const { createRequire: createRequire11 } = await import("node:module");
41461
+ const req3 = createRequire11(import.meta.url);
41462
+ const distPath = join40(packageDir, "dist", "index.js");
41463
+ if (existsSync38(distPath))
41464
+ return req3(distPath);
41465
+ return req3(packageDir);
41466
+ }
41459
41467
  async function loadTranscribeCli() {
41460
41468
  if (_tcChecked)
41461
41469
  return _tcModule;
41462
41470
  _tcChecked = true;
41471
+ try {
41472
+ const { createRequire: createRequire11 } = await import("node:module");
41473
+ const req3 = createRequire11(import.meta.url);
41474
+ _tcModule = req3("transcribe-cli");
41475
+ return _tcModule;
41476
+ } catch {
41477
+ }
41463
41478
  try {
41464
41479
  const globalRoot = (await execFileText("npm", ["root", "-g"], { timeout: 5e3 })).trim();
41465
41480
  const tcPath = join40(globalRoot, "transcribe-cli");
41466
41481
  if (existsSync38(join40(tcPath, "dist", "index.js"))) {
41467
- const { createRequire: createRequire11 } = await import("node:module");
41468
- const req3 = createRequire11(import.meta.url);
41469
- _tcModule = req3(join40(tcPath, "dist", "index.js"));
41482
+ _tcModule = await requireTranscribeCliFrom(tcPath);
41470
41483
  return _tcModule;
41471
41484
  }
41472
41485
  } catch {
@@ -41478,9 +41491,7 @@ async function loadTranscribeCli() {
41478
41491
  for (const ver of readdirSync61(nvmBase)) {
41479
41492
  const tcPath = join40(nvmBase, ver, "lib", "node_modules", "transcribe-cli");
41480
41493
  if (existsSync38(join40(tcPath, "dist", "index.js"))) {
41481
- const { createRequire: createRequire11 } = await import("node:module");
41482
- const req3 = createRequire11(import.meta.url);
41483
- _tcModule = req3(join40(tcPath, "dist", "index.js"));
41494
+ _tcModule = await requireTranscribeCliFrom(tcPath);
41484
41495
  return _tcModule;
41485
41496
  }
41486
41497
  }
@@ -41489,6 +41500,58 @@ async function loadTranscribeCli() {
41489
41500
  }
41490
41501
  return null;
41491
41502
  }
41503
+ async function ensureManagedTranscribeCliRuntime() {
41504
+ const packageDir = join40(MANAGED_TRANSCRIBE_CLI_DIR, "node_modules", "transcribe-cli");
41505
+ if (existsSync38(join40(packageDir, "dist", "index.js"))) {
41506
+ try {
41507
+ _managedTranscribeCliReady = true;
41508
+ _tcModule = await requireTranscribeCliFrom(packageDir);
41509
+ _tcChecked = true;
41510
+ return _tcModule;
41511
+ } catch {
41512
+ }
41513
+ }
41514
+ if (_managedTranscribeCliChecked && !_managedTranscribeCliReady)
41515
+ return null;
41516
+ _managedTranscribeCliChecked = true;
41517
+ try {
41518
+ mkdirSync22(MANAGED_TRANSCRIBE_CLI_DIR, { recursive: true });
41519
+ if (!existsSync38(join40(MANAGED_TRANSCRIBE_CLI_DIR, "package.json"))) {
41520
+ writeFileSync21(join40(MANAGED_TRANSCRIBE_CLI_DIR, "package.json"), JSON.stringify({ private: true, dependencies: {} }, null, 2), "utf8");
41521
+ }
41522
+ await execFileText("npm", ["install", "--prefix", MANAGED_TRANSCRIBE_CLI_DIR, "transcribe-cli@^2.0.1"], {
41523
+ timeout: 18e4,
41524
+ env: transcriptionPythonEnv()
41525
+ });
41526
+ _managedTranscribeCliReady = true;
41527
+ _tcModule = await requireTranscribeCliFrom(packageDir);
41528
+ _tcChecked = true;
41529
+ return _tcModule;
41530
+ } catch {
41531
+ _managedTranscribeCliReady = false;
41532
+ return null;
41533
+ }
41534
+ }
41535
+ async function ensureTranscribeCliPythonModule(python) {
41536
+ if (_transcribeCliPythonModuleChecked)
41537
+ return;
41538
+ _transcribeCliPythonModuleChecked = true;
41539
+ try {
41540
+ await execFileText(python, ["-c", "import transcribe_cli"], {
41541
+ timeout: 1e4,
41542
+ env: transcriptionPythonEnv()
41543
+ });
41544
+ return;
41545
+ } catch {
41546
+ }
41547
+ try {
41548
+ await execFileText(python, ["-m", "pip", "install", "-U", "transcribe-cli"], {
41549
+ timeout: 3e5,
41550
+ env: transcriptionPythonEnv()
41551
+ });
41552
+ } catch {
41553
+ }
41554
+ }
41492
41555
  function isYouTubeUrl(url) {
41493
41556
  return /(?:youtube\.com\/(?:watch|shorts|live|embed|v\/)|youtu\.be\/)/i.test(url);
41494
41557
  }
@@ -41519,7 +41582,7 @@ function formatTime(seconds) {
41519
41582
  const s2 = Math.floor(seconds % 60);
41520
41583
  return `${String(m2).padStart(2, "0")}:${String(s2).padStart(2, "0")}`;
41521
41584
  }
41522
- var AUDIO_EXTS, VIDEO_EXTS, MAX_TRANSCRIBE_URL_BYTES, MANAGED_ASR_VENV, _tcModule, _tcChecked, _managedAsrReady, TranscribeFileTool, YT_DLP_VENV, TranscribeUrlTool, YouTubeDownloadTool;
41585
+ var AUDIO_EXTS, VIDEO_EXTS, MAX_TRANSCRIBE_URL_BYTES, MANAGED_ASR_VENV, MANAGED_TRANSCRIBE_CLI_DIR, _tcModule, _tcChecked, _managedAsrReady, _managedTranscribeCliChecked, _managedTranscribeCliReady, _transcribeCliPythonModuleChecked, TranscribeFileTool, YT_DLP_VENV, TranscribeUrlTool, YouTubeDownloadTool;
41523
41586
  var init_transcribe_tool = __esm({
41524
41587
  "packages/execution/dist/tools/transcribe-tool.js"() {
41525
41588
  "use strict";
@@ -41551,9 +41614,13 @@ var init_transcribe_tool = __esm({
41551
41614
  ]);
41552
41615
  MAX_TRANSCRIBE_URL_BYTES = 100 * 1024 * 1024;
41553
41616
  MANAGED_ASR_VENV = join40(homedir11(), ".omnius", "runtimes", "asr", ".venv-whisper");
41617
+ MANAGED_TRANSCRIBE_CLI_DIR = join40(homedir11(), ".omnius", "runtimes", "asr", "transcribe-cli-node");
41554
41618
  _tcModule = null;
41555
41619
  _tcChecked = false;
41556
41620
  _managedAsrReady = false;
41621
+ _managedTranscribeCliChecked = false;
41622
+ _managedTranscribeCliReady = false;
41623
+ _transcribeCliPythonModuleChecked = false;
41557
41624
  TranscribeFileTool = class {
41558
41625
  name = "transcribe_file";
41559
41626
  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.";
@@ -41628,7 +41695,15 @@ var init_transcribe_tool = __esm({
41628
41695
  if (effectiveModel !== askedModel)
41629
41696
  model = effectiveModel;
41630
41697
  const failures = [];
41631
- const tc = await loadTranscribeCli();
41698
+ let managedPython = "";
41699
+ try {
41700
+ managedPython = await this.ensureManagedWhisperRuntime();
41701
+ process.env.TRANSCRIBE_PYTHON = managedPython;
41702
+ await ensureTranscribeCliPythonModule(managedPython);
41703
+ } catch (err) {
41704
+ failures.push(`managed ASR runtime bootstrap: ${err instanceof Error ? err.message : String(err)}`);
41705
+ }
41706
+ const tc = await loadTranscribeCli() ?? await ensureManagedTranscribeCliRuntime();
41632
41707
  if (tc) {
41633
41708
  try {
41634
41709
  const result = await withProcessEnv(transcriptionPythonEnv(), () => tc.transcribe(filePath, {
@@ -41641,6 +41716,8 @@ var init_transcribe_tool = __esm({
41641
41716
  } catch (err) {
41642
41717
  failures.push(`transcribe-cli module: ${err instanceof Error ? err.message : String(err)}`);
41643
41718
  }
41719
+ } else {
41720
+ failures.push("transcribe-cli npm package unavailable after managed auto-install");
41644
41721
  }
41645
41722
  const cli = await this.execViaCli(filePath, model, diarize, start2);
41646
41723
  if (cli.success)
@@ -555654,6 +555731,169 @@ function buildYolo26BootstrapPlan(model = DEFAULT_YOLO26_MODEL) {
555654
555731
  packages: ["ultralytics>=8.4.0", "opencv-python-headless", "yt-dlp", "numpy"]
555655
555732
  };
555656
555733
  }
555734
+ function buildYolo26DeploymentOptions() {
555735
+ const yolo26Models = YOLO26_SCALES.flatMap((scale) => Object.values(YOLO26_TASKS).map((task) => `yolo26${scale}${task.suffix}.pt`));
555736
+ const yoloeModels = YOLO26_SCALES.flatMap((scale) => [`yoloe-26${scale}-seg.pt`, `yoloe-26${scale}-seg-pf.pt`]);
555737
+ return {
555738
+ defaultModel: DEFAULT_YOLO26_MODEL,
555739
+ scales: [...YOLO26_SCALES],
555740
+ tasks: YOLO26_TASKS,
555741
+ families: {
555742
+ yolo26: {
555743
+ description: "YOLO26 closed-vocabulary models for detection, segmentation, semantic segmentation, pose, OBB, and classification.",
555744
+ models: yolo26Models
555745
+ },
555746
+ "yoloe-26": {
555747
+ description: "YOLOE-26 open-vocabulary segmentation models for text, visual, or prompt-free detection/segmentation workflows.",
555748
+ models: yoloeModels
555749
+ }
555750
+ },
555751
+ yoloePromptModes: [
555752
+ { mode: "text", description: "Set open-vocabulary classes with model.set_classes([...]) before prediction or export." },
555753
+ { mode: "prompt_free", description: "Use YOLOE prompt-free weights such as yoloe-26s-seg-pf.pt for built-in large-vocabulary discovery." },
555754
+ { mode: "visual", description: "Use YOLOE visual prompts where supported by the installed Ultralytics version; exported prompts are static." }
555755
+ ],
555756
+ exportFormats: YOLO26_EXPORT_FORMATS,
555757
+ headModes: [
555758
+ { value: true, name: "end2end", description: "YOLO26 default one-to-one head; NMS-free output optimized for deployment latency." },
555759
+ { value: false, name: "one-to-many", description: "Traditional output requiring NMS post-processing; useful for compatibility or accuracy-sensitive pipelines." }
555760
+ ],
555761
+ exportArguments: [
555762
+ "format",
555763
+ "imgsz",
555764
+ "quantize",
555765
+ "dynamic",
555766
+ "simplify",
555767
+ "opset",
555768
+ "workspace",
555769
+ "nms",
555770
+ "batch",
555771
+ "device",
555772
+ "data",
555773
+ "fraction",
555774
+ "end2end",
555775
+ "optimize",
555776
+ "keras",
555777
+ "name"
555778
+ ]
555779
+ };
555780
+ }
555781
+ function normalizeYolo26Scale(value2) {
555782
+ const scale = String(value2 ?? "n").trim().toLowerCase();
555783
+ return YOLO26_SCALES.includes(scale) ? scale : "n";
555784
+ }
555785
+ function normalizeYolo26Task(value2) {
555786
+ const raw = String(value2 ?? "detect").trim().toLowerCase();
555787
+ const aliases = {
555788
+ detection: "detect",
555789
+ det: "detect",
555790
+ instance: "segment",
555791
+ segmentation: "segment",
555792
+ seg: "segment",
555793
+ sem: "semantic",
555794
+ semantic_segmentation: "semantic",
555795
+ keypoint: "pose",
555796
+ keypoints: "pose",
555797
+ classify: "classify",
555798
+ classification: "classify",
555799
+ cls: "classify",
555800
+ oriented: "obb",
555801
+ oriented_detection: "obb"
555802
+ };
555803
+ const normalized = aliases[raw] ?? raw;
555804
+ return Object.prototype.hasOwnProperty.call(YOLO26_TASKS, normalized) ? normalized : "detect";
555805
+ }
555806
+ function normalizeYolo26Family(value2) {
555807
+ const raw = String(value2 ?? "yolo26").trim().toLowerCase();
555808
+ if (["yoloe", "yoloe26", "yoloe-26", "open-vocabulary", "open_vocab"].includes(raw))
555809
+ return "yoloe-26";
555810
+ return "yolo26";
555811
+ }
555812
+ function resolveYolo26Model(args) {
555813
+ const explicit = args["yolo_model"] ?? args["model"];
555814
+ if (typeof explicit === "string" && explicit.trim())
555815
+ return explicit.trim();
555816
+ const family = normalizeYolo26Family(args["yolo_family"] ?? args["family"] ?? args["variant"]);
555817
+ const scale = normalizeYolo26Scale(args["yolo_scale"] ?? args["scale"]);
555818
+ const promptMode = String(args["yoloe_prompt_mode"] ?? args["prompt_mode"] ?? "").trim().toLowerCase();
555819
+ const promptFree = promptMode === "prompt_free" || promptMode === "prompt-free" || promptMode === "pf" || args["prompt_free"] === true;
555820
+ if (family === "yoloe-26")
555821
+ return `yoloe-26${scale}-seg${promptFree ? "-pf" : ""}.pt`;
555822
+ const task = normalizeYolo26Task(args["yolo_task"] ?? args["task"]);
555823
+ return `yolo26${scale}${YOLO26_TASKS[task].suffix}.pt`;
555824
+ }
555825
+ function normalizeYolo26ExportFormat(value2) {
555826
+ const raw = String(value2 ?? "onnx").trim().toLowerCase().replace(/-/g, "_");
555827
+ const aliases = {
555828
+ trt: "engine",
555829
+ tensorrt: "engine",
555830
+ tf_saved_model: "saved_model",
555831
+ savedmodel: "saved_model",
555832
+ tensorflow_saved_model: "saved_model",
555833
+ graphdef: "pb",
555834
+ edge_tpu: "edgetpu",
555835
+ tflite: "litert",
555836
+ tensorflow_lite: "litert",
555837
+ lite_rt: "litert",
555838
+ torch: "pytorch",
555839
+ pt: "pytorch"
555840
+ };
555841
+ const normalized = aliases[raw] ?? raw;
555842
+ if (YOLO26_EXPORT_FORMATS.some((entry) => entry.format === normalized))
555843
+ return normalized;
555844
+ throw new Error(`Unsupported YOLO26 export format: ${String(value2)}. Use one of: ${YOLO26_EXPORT_FORMATS.map((entry) => entry.format).join(", ")}`);
555845
+ }
555846
+ function optionalBoolean(args, key) {
555847
+ if (!(key in args) || args[key] === void 0 || args[key] === null || args[key] === "")
555848
+ return void 0;
555849
+ if (typeof args[key] === "boolean")
555850
+ return args[key];
555851
+ const raw = String(args[key]).trim().toLowerCase();
555852
+ if (["1", "true", "yes", "on"].includes(raw))
555853
+ return true;
555854
+ if (["0", "false", "no", "off"].includes(raw))
555855
+ return false;
555856
+ return void 0;
555857
+ }
555858
+ function optionalNumber(args, key) {
555859
+ if (!(key in args) || args[key] === void 0 || args[key] === null || args[key] === "")
555860
+ return void 0;
555861
+ const n2 = Number(args[key]);
555862
+ return Number.isFinite(n2) ? n2 : void 0;
555863
+ }
555864
+ function exportOptionsFromArgs(args, format3) {
555865
+ const options2 = { format: format3 };
555866
+ for (const key of ["imgsz", "opset", "workspace", "batch", "fraction"]) {
555867
+ const n2 = optionalNumber(args, key);
555868
+ if (n2 !== void 0)
555869
+ options2[key] = n2;
555870
+ }
555871
+ for (const key of ["dynamic", "simplify", "nms", "optimize", "keras", "end2end"]) {
555872
+ const b = optionalBoolean(args, key);
555873
+ if (b !== void 0)
555874
+ options2[key] = b;
555875
+ }
555876
+ const quantize = args["quantize"] ?? args["precision"];
555877
+ if (quantize !== void 0 && quantize !== null && String(quantize).trim()) {
555878
+ const asNumber2 = Number(quantize);
555879
+ options2["quantize"] = Number.isFinite(asNumber2) ? asNumber2 : String(quantize).trim();
555880
+ }
555881
+ for (const key of ["device", "data", "name"]) {
555882
+ const value2 = args[key];
555883
+ if (typeof value2 === "string" && value2.trim())
555884
+ options2[key] = value2.trim();
555885
+ }
555886
+ return options2;
555887
+ }
555888
+ function stringListFromArg(value2) {
555889
+ if (Array.isArray(value2)) {
555890
+ return value2.map((item) => String(item).trim()).filter(Boolean);
555891
+ }
555892
+ if (typeof value2 === "string") {
555893
+ return value2.split(/[,;\n]/).map((item) => item.trim()).filter(Boolean);
555894
+ }
555895
+ return [];
555896
+ }
555657
555897
  function sourceKind(args) {
555658
555898
  const explicit = String(args["source_kind"] ?? args["source_type"] ?? "").toLowerCase();
555659
555899
  if (["camera", "youtube", "file", "direct"].includes(explicit))
@@ -555797,6 +556037,59 @@ async function ensureRuntime(autoBootstrap) {
555797
556037
  return { ok: false, python: plan.python, error: err instanceof Error ? err.message : String(err) };
555798
556038
  }
555799
556039
  }
556040
+ async function exportYolo26Model(python, model, options2, yoloeClasses = []) {
556041
+ const cfgPath = join92(tmpdir19(), `omnius-yolo26-export-${Date.now()}.json`);
556042
+ const scriptPath2 = join92(tmpdir19(), `omnius-yolo26-export-${Date.now()}.py`);
556043
+ writeFileSync39(cfgPath, JSON.stringify({ model, options: options2, yoloeClasses }, null, 2), "utf8");
556044
+ writeFileSync39(scriptPath2, String.raw`
556045
+ import json, sys
556046
+ from ultralytics import YOLO
556047
+ try:
556048
+ from ultralytics import YOLOE
556049
+ except Exception:
556050
+ YOLOE = None
556051
+
556052
+ cfg_path = sys.argv[1]
556053
+ with open(cfg_path, "r", encoding="utf-8") as fh:
556054
+ cfg = json.load(fh)
556055
+
556056
+ model_name = cfg["model"]
556057
+ options = cfg["options"]
556058
+ yoloe_classes = [str(x).strip() for x in cfg.get("yoloeClasses", []) if str(x).strip()]
556059
+ model = YOLOE(model_name) if str(model_name).startswith("yoloe-") and YOLOE is not None else YOLO(model_name)
556060
+ if yoloe_classes and hasattr(model, "set_classes"):
556061
+ model.set_classes(yoloe_classes)
556062
+ artifact = model.export(**options)
556063
+ print(json.dumps({
556064
+ "success": True,
556065
+ "model": model_name,
556066
+ "format": options.get("format"),
556067
+ "artifact": str(artifact),
556068
+ "options": options,
556069
+ "yoloe_classes": yoloe_classes,
556070
+ }))
556071
+ `, "utf8");
556072
+ try {
556073
+ const raw = await execFileText(python, [scriptPath2, cfgPath], {
556074
+ timeout: 30 * 6e4,
556075
+ maxBuffer: 64 * 1024 * 1024,
556076
+ env: { ...process.env, PYTHONUNBUFFERED: "1" }
556077
+ });
556078
+ const jsonLine2 = raw.trim().split("\n").reverse().find((line) => line.trim().startsWith("{") && line.trim().endsWith("}"));
556079
+ if (!jsonLine2)
556080
+ throw new Error(`YOLO26 export returned no JSON result; stdout tail: ${raw.slice(-1e3)}`);
556081
+ return JSON.parse(jsonLine2);
556082
+ } finally {
556083
+ try {
556084
+ rmSync10(cfgPath, { force: true });
556085
+ } catch {
556086
+ }
556087
+ try {
556088
+ rmSync10(scriptPath2, { force: true });
556089
+ } catch {
556090
+ }
556091
+ }
556092
+ }
555800
556093
  function makePythonScript() {
555801
556094
  return String.raw`
555802
556095
  import json, os, sys, time
@@ -555818,6 +556111,8 @@ def main():
555818
556111
  os.makedirs(out_dir, exist_ok=True)
555819
556112
  crop_dir = os.path.join(out_dir, "crops")
555820
556113
  os.makedirs(crop_dir, exist_ok=True)
556114
+ segment_dir = os.path.join(out_dir, "segments")
556115
+ os.makedirs(segment_dir, exist_ok=True)
555821
556116
  source_kind = cfg["source_kind"]
555822
556117
  source = cfg["source"]
555823
556118
  media_path = None
@@ -555827,6 +556122,10 @@ def main():
555827
556122
  import cv2
555828
556123
  import numpy as np
555829
556124
  from ultralytics import YOLO
556125
+ try:
556126
+ from ultralytics import YOLOE
556127
+ except Exception:
556128
+ YOLOE = None
555830
556129
  except Exception as exc:
555831
556130
  print(json.dumps({"success": False, "error": "python imports failed: " + str(exc)}))
555832
556131
  return
@@ -555869,16 +556168,26 @@ def main():
555869
556168
  return
555870
556169
 
555871
556170
  yolo_model = cfg.get("yolo_model") or "yolo26n.pt"
555872
- model = YOLO(yolo_model) if cfg.get("detect_objects", True) else None
556171
+ yoloe_classes = [str(x).strip() for x in (cfg.get("yoloe_classes") or []) if str(x).strip()]
556172
+ yoloe_prompt_mode = cfg.get("yoloe_prompt_mode") or ("text" if yoloe_classes else "prompt_free" if str(yolo_model).endswith("-pf.pt") else "closed_set")
556173
+ use_yoloe = str(yolo_model).startswith("yoloe-") and YOLOE is not None
556174
+ model = (YOLOE(yolo_model) if use_yoloe else YOLO(yolo_model)) if cfg.get("detect_objects", True) else None
556175
+ if model is not None and yoloe_classes and hasattr(model, "set_classes"):
556176
+ try:
556177
+ model.set_classes(yoloe_classes)
556178
+ except TypeError:
556179
+ model.set_classes(list(yoloe_classes))
555873
556180
  face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
555874
556181
  objects = []
555875
556182
  faces = []
556183
+ segments = []
555876
556184
  track_stats = {}
555877
556185
  max_frames = int(cfg.get("max_frames") or 8)
555878
556186
  interval = float(cfg.get("sample_interval_sec") or 1.0)
555879
556187
  duration = float(cfg.get("duration_sec") or 8.0)
555880
556188
  conf = float(cfg.get("confidence") or 0.25)
555881
556189
  imgsz = int(cfg.get("imgsz") or 640)
556190
+ end2end = cfg.get("end2end", None)
555882
556191
  start_wall = time.time()
555883
556192
  frame_index = 0
555884
556193
 
@@ -555902,10 +556211,17 @@ def main():
555902
556211
 
555903
556212
  if model is not None:
555904
556213
  try:
556214
+ predict_kwargs = {"verbose": False, "conf": conf, "imgsz": imgsz}
556215
+ if end2end is not None:
556216
+ predict_kwargs["end2end"] = bool(end2end)
556217
+ if cfg.get("agnostic_nms", None) is not None:
556218
+ predict_kwargs["agnostic_nms"] = bool(cfg.get("agnostic_nms"))
556219
+ elif use_yoloe:
556220
+ predict_kwargs["agnostic_nms"] = True
555905
556221
  if cfg.get("track_objects", True):
555906
- results = model.track(frame, persist=True, verbose=False, conf=conf, imgsz=imgsz)
556222
+ results = model.track(frame, persist=True, **predict_kwargs)
555907
556223
  else:
555908
- results = model.predict(frame, verbose=False, conf=conf, imgsz=imgsz)
556224
+ results = model.predict(frame, **predict_kwargs)
555909
556225
  r = results[0] if results else None
555910
556226
  if r is not None and getattr(r, "boxes", None) is not None:
555911
556227
  boxes = r.boxes
@@ -555935,6 +556251,44 @@ def main():
555935
556251
  st["last_seen_sec"] = max(st["last_seen_sec"], ts)
555936
556252
  st["observations"] += 1
555937
556253
  st["confidence_sum"] += c
556254
+ want_segments = bool(cfg.get("segment_objects") or cfg.get("extract_segments") or use_yoloe)
556255
+ if want_segments and getattr(r, "masks", None) is not None and getattr(r.masks, "data", None) is not None:
556256
+ mask_data = r.masks.data.cpu().numpy()
556257
+ max_segments = int(cfg.get("max_segments_per_frame") or 8)
556258
+ for i, raw_mask in enumerate(mask_data[:max_segments]):
556259
+ label = str(names.get(int(cls[i]), int(cls[i]))) if i < len(cls) else "object"
556260
+ c = float(confs[i]) if i < len(confs) else 0.0
556261
+ box = xyxy[i] if i < len(xyxy) else [0, 0, frame.shape[1], frame.shape[0]]
556262
+ mask = (raw_mask > 0.5).astype("uint8") * 255
556263
+ if mask.shape[:2] != frame.shape[:2]:
556264
+ mask = cv2.resize(mask, (frame.shape[1], frame.shape[0]), interpolation=cv2.INTER_NEAREST)
556265
+ x1, y1, x2, y2 = [int(max(0, round(float(v)))) for v in box]
556266
+ x1 = min(x1, frame.shape[1] - 1)
556267
+ x2 = min(max(x2, x1 + 1), frame.shape[1])
556268
+ y1 = min(y1, frame.shape[0] - 1)
556269
+ y2 = min(max(y2, y1 + 1), frame.shape[0])
556270
+ mask_path = os.path.join(segment_dir, f"seg_f{frame_index:04d}_{i:02d}_mask.png")
556271
+ cv2.imwrite(mask_path, mask)
556272
+ crop_path = None
556273
+ if cfg.get("extract_segments", False):
556274
+ crop_bgr = frame[y1:y2, x1:x2]
556275
+ crop_alpha = mask[y1:y2, x1:x2]
556276
+ if crop_bgr.size > 0 and crop_alpha.size > 0:
556277
+ crop_rgba = cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2BGRA)
556278
+ crop_rgba[:, :, 3] = crop_alpha
556279
+ crop_path = os.path.join(segment_dir, f"seg_f{frame_index:04d}_{i:02d}_{label.replace('/', '_')}.png")
556280
+ cv2.imwrite(crop_path, crop_rgba)
556281
+ area_ratio = float((mask > 0).sum()) / max(1.0, float(frame.shape[0] * frame.shape[1]))
556282
+ segments.append({
556283
+ "frame_index": frame_index,
556284
+ "timestamp_sec": round(float(ts), 3),
556285
+ "label": label,
556286
+ "confidence": c,
556287
+ "bbox_xyxy": [int(x1), int(y1), int(x2), int(y2)],
556288
+ "mask_path": mask_path,
556289
+ "crop_path": crop_path,
556290
+ "area_ratio": round(area_ratio, 6),
556291
+ })
555938
556292
  except Exception as exc:
555939
556293
  objects.append({"frame_index": frame_index, "timestamp_sec": round(float(ts), 3), "label": "yolo_error", "confidence": 0.0, "bbox_xyxy": [0,0,0,0], "error": str(exc)})
555940
556294
 
@@ -555984,6 +556338,9 @@ def main():
555984
556338
  "objects": objects,
555985
556339
  "tracks": tracks,
555986
556340
  "faces": faces,
556341
+ "segments": segments,
556342
+ "yoloe_classes": yoloe_classes,
556343
+ "yoloe_prompt_mode": yoloe_prompt_mode,
555987
556344
  }))
555988
556345
 
555989
556346
  if __name__ == "__main__":
@@ -555995,8 +556352,13 @@ function mockResult(args, workingDir) {
555995
556352
  const rotation = normalizeCameraRotationDegrees(args["rotate_degrees"] ?? args["rotation_degrees"] ?? args["rotate"]);
555996
556353
  const outDir = join92(workingDir, ".omnius", "live-media", `mock-${Date.now().toString(36)}`);
555997
556354
  mkdirSync47(join92(outDir, "crops"), { recursive: true });
556355
+ mkdirSync47(join92(outDir, "segments"), { recursive: true });
555998
556356
  const facePath = join92(outDir, "crops", "face_f0000_00.jpg");
556357
+ const maskPath = join92(outDir, "segments", "seg_f0001_00_mask.png");
556358
+ const segmentPath = join92(outDir, "segments", "seg_f0001_00_red_cup.png");
555999
556359
  writeFileSync39(facePath, "mock face crop", "utf8");
556360
+ writeFileSync39(maskPath, "mock segment mask", "utf8");
556361
+ writeFileSync39(segmentPath, "mock extracted segment", "utf8");
556000
556362
  return {
556001
556363
  success: true,
556002
556364
  source_kind: kind,
@@ -556016,10 +556378,22 @@ function mockResult(args, workingDir) {
556016
556378
  { track_id: "1", label: "person", first_seen_sec: 0, last_seen_sec: 0, observations: 1, mean_confidence: 0.91 },
556017
556379
  { track_id: "2", label: "red cup", first_seen_sec: 1, last_seen_sec: 2, observations: 2, mean_confidence: 0.84 }
556018
556380
  ],
556019
- faces: [{ frame_index: 0, timestamp_sec: 0, crop_path: facePath, bbox_xyxy: [20, 30, 80, 90], confidence: 0.5 }]
556381
+ faces: [{ frame_index: 0, timestamp_sec: 0, crop_path: facePath, bbox_xyxy: [20, 30, 80, 90], confidence: 0.5 }],
556382
+ segments: [{
556383
+ frame_index: 1,
556384
+ timestamp_sec: 1,
556385
+ label: "red cup",
556386
+ confidence: 0.84,
556387
+ bbox_xyxy: [130, 140, 180, 210],
556388
+ mask_path: maskPath,
556389
+ crop_path: segmentPath,
556390
+ area_ratio: 0.012
556391
+ }],
556392
+ yoloe_classes: stringListFromArg(args["yoloe_classes"] ?? args["classes"]),
556393
+ yoloe_prompt_mode: String(args["yoloe_prompt_mode"] ?? "closed_set")
556020
556394
  };
556021
556395
  }
556022
- var LIVE_MEDIA_ROOT, YOLO26_VENV, DEFAULT_YOLO26_MODEL, LiveMediaLoopTool;
556396
+ var LIVE_MEDIA_ROOT, YOLO26_VENV, DEFAULT_YOLO26_MODEL, YOLO26_SCALES, YOLO26_TASKS, YOLO26_EXPORT_FORMATS, LiveMediaLoopTool;
556023
556397
  var init_live_media_loop = __esm({
556024
556398
  "packages/execution/dist/tools/live-media-loop.js"() {
556025
556399
  "use strict";
@@ -556033,14 +556407,44 @@ var init_live_media_loop = __esm({
556033
556407
  LIVE_MEDIA_ROOT = join92(homedir28(), ".omnius", "runtimes", "live-media");
556034
556408
  YOLO26_VENV = join92(LIVE_MEDIA_ROOT, ".venv-yolo26");
556035
556409
  DEFAULT_YOLO26_MODEL = "yolo26n.pt";
556410
+ YOLO26_SCALES = ["n", "s", "m", "l", "x"];
556411
+ YOLO26_TASKS = {
556412
+ detect: { suffix: "", description: "Object detection" },
556413
+ segment: { suffix: "-seg", description: "Instance segmentation" },
556414
+ semantic: { suffix: "-sem", description: "Semantic segmentation" },
556415
+ pose: { suffix: "-pose", description: "Pose/keypoints" },
556416
+ obb: { suffix: "-obb", description: "Oriented object detection" },
556417
+ classify: { suffix: "-cls", description: "Image classification" }
556418
+ };
556419
+ YOLO26_EXPORT_FORMATS = [
556420
+ { format: "pytorch", artifact: "yolo26n.pt", description: "Native PyTorch checkpoint", arguments: [], precisions: ["fp32"] },
556421
+ { format: "torchscript", artifact: "yolo26n.torchscript", description: "TorchScript runtime artifact", arguments: ["imgsz", "quantize", "dynamic", "optimize", "nms", "batch", "device"], precisions: ["fp32", "fp16"] },
556422
+ { format: "onnx", artifact: "yolo26n.onnx", description: "ONNX Runtime and cross-platform inference", arguments: ["imgsz", "quantize", "dynamic", "simplify", "opset", "nms", "batch", "data", "fraction", "device"], precisions: ["fp32", "fp16", "int8"] },
556423
+ { format: "openvino", artifact: "yolo26n_openvino_model/", description: "Intel OpenVINO CPU/iGPU/NPU deployment", arguments: ["imgsz", "quantize", "dynamic", "nms", "batch", "data", "fraction", "device"], precisions: ["fp32", "fp16", "int8"] },
556424
+ { format: "engine", artifact: "yolo26n.engine", description: "NVIDIA TensorRT engine", arguments: ["imgsz", "quantize", "dynamic", "simplify", "workspace", "nms", "batch", "data", "fraction", "device"], precisions: ["fp32", "fp16", "int8"] },
556425
+ { format: "coreml", artifact: "yolo26n.mlpackage", description: "Apple CoreML deployment", arguments: ["imgsz", "dynamic", "quantize", "nms", "batch", "device"], precisions: ["fp32", "fp16", "int8", "w8a16"] },
556426
+ { format: "saved_model", artifact: "yolo26n_saved_model/", description: "TensorFlow SavedModel", arguments: ["imgsz", "keras", "quantize", "nms", "batch", "data", "fraction", "device"], precisions: ["fp32", "int8"] },
556427
+ { format: "pb", artifact: "yolo26n.pb", description: "TensorFlow GraphDef", arguments: ["imgsz", "batch", "device"], precisions: ["fp32"] },
556428
+ { format: "edgetpu", artifact: "yolo26n_edgetpu.tflite", description: "Google Coral Edge TPU", arguments: ["imgsz", "quantize", "data", "fraction", "device"], precisions: ["int8"] },
556429
+ { format: "paddle", artifact: "yolo26n_paddle_model/", description: "PaddlePaddle", arguments: ["imgsz", "batch", "device"], precisions: ["fp32"] },
556430
+ { format: "mnn", artifact: "yolo26n.mnn", description: "MNN mobile/embedded runtime", arguments: ["imgsz", "batch", "quantize", "device"], precisions: ["fp32", "fp16", "int8"] },
556431
+ { format: "ncnn", artifact: "yolo26n_ncnn_model/", description: "Tencent NCNN mobile/embedded runtime", arguments: ["imgsz", "quantize", "batch", "device"], precisions: ["fp32", "fp16"] },
556432
+ { format: "imx", artifact: "yolo26n_imx_model/", description: "Sony IMX500", arguments: ["imgsz", "quantize", "data", "fraction", "nms", "device"], precisions: ["int8", "w8a16"] },
556433
+ { format: "rknn", artifact: "yolo26n_rknn_model/", description: "Rockchip RKNN", arguments: ["imgsz", "batch", "name", "quantize", "data", "fraction", "device"], precisions: ["fp16", "int8"] },
556434
+ { format: "executorch", artifact: "yolo26n_executorch_model/", description: "PyTorch ExecuTorch", arguments: ["imgsz", "batch", "device"], precisions: ["fp32"] },
556435
+ { format: "axelera", artifact: "yolo26n_axelera_model/", description: "Axelera AI deployment", arguments: ["imgsz", "batch", "quantize", "data", "fraction", "device"], precisions: ["int8"] },
556436
+ { format: "deepx", artifact: "yolo26n_deepx_model/", description: "DEEPX accelerator deployment", arguments: ["imgsz", "quantize", "data", "optimize", "device"], precisions: ["int8"] },
556437
+ { format: "qnn", artifact: "yolo26n_qnn.onnx", description: "Qualcomm QNN HTP", arguments: ["imgsz", "batch", "name", "quantize", "data", "fraction", "device"], precisions: ["w8a16"] },
556438
+ { format: "litert", artifact: "yolo26n.tflite", description: "Google LiteRT / TensorFlow Lite successor", arguments: ["imgsz", "quantize", "batch", "data", "fraction", "device"], precisions: ["fp32", "int8", "w8a16", "w8a32"] }
556439
+ ];
556036
556440
  LiveMediaLoopTool = class {
556037
556441
  workingDir;
556038
556442
  name = "live_media_loop";
556039
- description = "Watch a live or file media source in a bounded loop: YouTube/direct video/local file/camera, YOLO26 object detection/tracking, face cropping, optional face familiarity recall, ASR/audio comprehension, and visual trigger evaluation. Use this for live video input, object tracking, face crops, visual reminders/triggers, and 'watch this video/camera and react when X appears'.";
556443
+ description = "Watch a live or file media source in a bounded loop: YouTube/direct video/local file/camera, YOLO26/YOLOE object detection/tracking, open-vocabulary segmentation masks, extracted transparent object crops, face cropping, optional face familiarity recall, ASR/audio comprehension, and visual trigger evaluation. Use this for live video input, object tracking, object segmentation/extraction, face crops, visual reminders/triggers, and 'watch this video/camera and react when X appears'.";
556040
556444
  parameters = {
556041
556445
  type: "object",
556042
556446
  properties: {
556043
- action: { type: "string", enum: ["watch", "analyze", "ensure", "status"] },
556447
+ action: { type: "string", enum: ["watch", "analyze", "ensure", "status", "deployment_options", "options", "export"] },
556044
556448
  source_kind: { type: "string", enum: ["youtube", "camera", "file", "direct"] },
556045
556449
  url: { type: "string", description: "YouTube or direct video URL" },
556046
556450
  path: { type: "string", description: "Local media path" },
@@ -556053,6 +556457,36 @@ var init_live_media_loop = __esm({
556053
556457
  sample_interval_sec: { type: "number", description: "Seconds between sampled frames, default 1" },
556054
556458
  max_frames: { type: "number", description: "Max sampled frames, default 8" },
556055
556459
  yolo_model: { type: "string", description: "Ultralytics model, default yolo26n.pt" },
556460
+ yolo_family: { type: "string", enum: ["yolo26", "yoloe-26"], description: "Model family when yolo_model is omitted." },
556461
+ yolo_task: { type: "string", enum: ["detect", "segment", "semantic", "pose", "obb", "classify"], description: "YOLO26 task suffix when yolo_model is omitted." },
556462
+ yolo_scale: { type: "string", enum: ["n", "s", "m", "l", "x"], description: "YOLO26 model scale when yolo_model is omitted." },
556463
+ yoloe_prompt_mode: { type: "string", enum: ["closed_set", "text", "prompt_free", "visual"], description: "YOLOE prompting mode. text uses yoloe_classes; prompt_free uses *-seg-pf weights when no explicit yolo_model is set." },
556464
+ prompt_mode: { type: "string", description: "Alias for yoloe_prompt_mode." },
556465
+ prompt_free: { type: "boolean", description: "Alias for yoloe_prompt_mode=prompt_free." },
556466
+ yoloe_classes: { type: ["array", "string"], description: "YOLOE text prompt classes, e.g. ['person','bus'] or comma-separated string." },
556467
+ classes: { type: ["array", "string"], description: "Alias for yoloe_classes." },
556468
+ segment_objects: { type: "boolean", description: "Capture instance segmentation masks when model returns masks. Enabled automatically for YOLOE." },
556469
+ extract_segments: { type: "boolean", description: "Save transparent RGBA crops for segmented objects." },
556470
+ max_segments_per_frame: { type: "number", description: "Limit saved mask/crop artifacts per sampled frame, default 8." },
556471
+ agnostic_nms: { type: "boolean", description: "Override YOLOE agnostic NMS behavior." },
556472
+ export_format: { type: "string", description: "YOLO26 export format for action=export, e.g. onnx, engine, openvino, coreml, saved_model, rknn, ncnn, litert." },
556473
+ format: { type: "string", description: "Alias for export_format when action=export." },
556474
+ quantize: { type: ["string", "number"], description: "Export precision: 8/int8/w8a8, 16/fp16/w16a16, 32/fp32, w8a16, or w8a32 where supported." },
556475
+ precision: { type: ["string", "number"], description: "Alias for quantize." },
556476
+ end2end: { type: "boolean", description: "YOLO26 one-to-one NMS-free head when true; set false for traditional one-to-many/NMS pipeline." },
556477
+ imgsz: { type: "number", description: "YOLO input/export image size, default 640." },
556478
+ dynamic: { type: "boolean", description: "Enable dynamic input size for supported export formats." },
556479
+ simplify: { type: "boolean", description: "Simplify ONNX/TensorRT export graphs when supported." },
556480
+ opset: { type: "number", description: "ONNX opset version." },
556481
+ workspace: { type: "number", description: "TensorRT workspace size in GiB." },
556482
+ nms: { type: "boolean", description: "Embed NMS in supported exports. Not available for YOLO26 end2end exports." },
556483
+ batch: { type: "number", description: "Export batch size." },
556484
+ device: { type: "string", description: "Export/inference device, e.g. cpu, 0, mps, npu:0, dla:0." },
556485
+ data: { type: "string", description: "Calibration dataset yaml for INT8/W8A16 exports." },
556486
+ fraction: { type: "number", description: "Calibration data fraction for quantized export." },
556487
+ optimize: { type: "boolean", description: "Export optimization for supported targets." },
556488
+ keras: { type: "boolean", description: "Keras SavedModel export option." },
556489
+ name: { type: "string", description: "Target/chip/name option for formats such as RKNN or QNN." },
556056
556490
  auto_bootstrap: { type: "boolean", description: "Create managed YOLO26 venv if missing, default true" },
556057
556491
  detect_objects: { type: "boolean" },
556058
556492
  track_objects: { type: "boolean" },
@@ -556071,9 +556505,27 @@ var init_live_media_loop = __esm({
556071
556505
  async execute(args) {
556072
556506
  const start2 = performance.now();
556073
556507
  const action = String(args["action"] ?? "watch").toLowerCase();
556074
- const yoloModel = String(args["yolo_model"] ?? DEFAULT_YOLO26_MODEL);
556508
+ const yoloModel = resolveYolo26Model(args);
556075
556509
  const rotation = normalizeCameraRotationDegrees(args["rotate_degrees"] ?? args["rotation_degrees"] ?? args["rotate"]);
556076
556510
  const plan = buildYolo26BootstrapPlan(yoloModel);
556511
+ if (action === "deployment_options" || action === "options") {
556512
+ const options2 = buildYolo26DeploymentOptions();
556513
+ const text2 = [
556514
+ "YOLO26 / YOLOE deployment options",
556515
+ `default model: ${options2.defaultModel}`,
556516
+ `scales: ${options2.scales.join(", ")}`,
556517
+ `tasks: ${Object.entries(options2.tasks).map(([task, meta]) => `${task}${meta.suffix}`).join(", ")}`,
556518
+ `formats: ${options2.exportFormats.map((entry) => entry.format).join(", ")}`,
556519
+ `YOLOE prompt modes: ${options2.yoloePromptModes.map((entry) => entry.mode).join(", ")}`,
556520
+ "head modes: end2end=true for default NMS-free one-to-one head; end2end=false for traditional one-to-many/NMS pipeline"
556521
+ ].join("\n");
556522
+ return {
556523
+ success: true,
556524
+ output: text2,
556525
+ llmContent: JSON.stringify(options2, null, 2),
556526
+ durationMs: performance.now() - start2
556527
+ };
556528
+ }
556077
556529
  if (action === "status" || action === "ensure") {
556078
556530
  const runtime = action === "ensure" ? await ensureRuntime(args["auto_bootstrap"] !== false) : await ensureRuntime(false);
556079
556531
  return {
@@ -556084,13 +556536,69 @@ var init_live_media_loop = __esm({
556084
556536
  `python: ${plan.python}`,
556085
556537
  `model: ${plan.model}`,
556086
556538
  `packages: ${plan.packages.join(", ")}`,
556539
+ `deployment formats: ${YOLO26_EXPORT_FORMATS.map((entry) => entry.format).join(", ")}`,
556087
556540
  runtime.error ? `error: ${runtime.error}` : ""
556088
556541
  ].filter(Boolean).join("\n"),
556089
556542
  error: runtime.ok ? void 0 : runtime.error,
556090
- llmContent: JSON.stringify({ ...plan, ready: runtime.ok }, null, 2),
556543
+ llmContent: JSON.stringify({ ...plan, ready: runtime.ok, deploymentOptions: buildYolo26DeploymentOptions() }, null, 2),
556091
556544
  durationMs: performance.now() - start2
556092
556545
  };
556093
556546
  }
556547
+ if (action === "export") {
556548
+ let format3;
556549
+ try {
556550
+ format3 = normalizeYolo26ExportFormat(args["export_format"] ?? args["format"] ?? args["deploy_format"]);
556551
+ } catch (err) {
556552
+ return { success: false, output: "", error: err instanceof Error ? err.message : String(err), durationMs: performance.now() - start2 };
556553
+ }
556554
+ if (format3 === "pytorch") {
556555
+ const payload = { success: true, model: yoloModel, format: format3, artifact: yoloModel, options: { format: format3 } };
556556
+ return {
556557
+ success: true,
556558
+ output: `YOLO26 native PyTorch model selected: ${yoloModel}`,
556559
+ llmContent: JSON.stringify(payload, null, 2),
556560
+ durationMs: performance.now() - start2
556561
+ };
556562
+ }
556563
+ const exportOptions = exportOptionsFromArgs(args, format3);
556564
+ const yoloeClasses = stringListFromArg(args["yoloe_classes"] ?? args["classes"]);
556565
+ if (Boolean(args["mock"]) || process.env["OMNIUS_LIVE_MEDIA_MOCK"] === "1") {
556566
+ const payload = {
556567
+ success: true,
556568
+ model: yoloModel,
556569
+ format: format3,
556570
+ artifact: `${basename19(yoloModel, ".pt")}.${format3 === "engine" ? "engine" : format3}`,
556571
+ options: exportOptions,
556572
+ yoloe_classes: yoloeClasses,
556573
+ mock: true
556574
+ };
556575
+ return {
556576
+ success: true,
556577
+ output: `YOLO26 export plan (mock): ${yoloModel} -> ${format3}`,
556578
+ llmContent: JSON.stringify(payload, null, 2),
556579
+ durationMs: performance.now() - start2
556580
+ };
556581
+ }
556582
+ const runtime = await ensureRuntime(args["auto_bootstrap"] !== false);
556583
+ if (!runtime.ok)
556584
+ return { success: false, output: "", error: runtime.error, durationMs: performance.now() - start2 };
556585
+ try {
556586
+ const exported = await exportYolo26Model(runtime.python, yoloModel, exportOptions, yoloeClasses);
556587
+ return {
556588
+ success: true,
556589
+ output: [
556590
+ `YOLO26 export complete: ${yoloModel}`,
556591
+ `format: ${format3}`,
556592
+ `artifact: ${String(exported["artifact"] ?? "")}`,
556593
+ `options: ${JSON.stringify(exportOptions)}`
556594
+ ].join("\n"),
556595
+ llmContent: JSON.stringify(exported, null, 2),
556596
+ durationMs: performance.now() - start2
556597
+ };
556598
+ } catch (err) {
556599
+ return { success: false, output: "", error: err instanceof Error ? err.message : String(err), durationMs: performance.now() - start2 };
556600
+ }
556601
+ }
556094
556602
  const mock = Boolean(args["mock"]) || process.env["OMNIUS_LIVE_MEDIA_MOCK"] === "1";
556095
556603
  let result;
556096
556604
  if (mock) {
@@ -556118,6 +556626,13 @@ var init_live_media_loop = __esm({
556118
556626
  sample_interval_sec: Number(args["sample_interval_sec"] ?? 1),
556119
556627
  max_frames: Number(args["max_frames"] ?? 8),
556120
556628
  rotate_degrees: rotation,
556629
+ end2end: optionalBoolean(args, "end2end"),
556630
+ yoloe_classes: stringListFromArg(args["yoloe_classes"] ?? args["classes"]),
556631
+ yoloe_prompt_mode: String(args["yoloe_prompt_mode"] ?? args["prompt_mode"] ?? ""),
556632
+ segment_objects: args["segment_objects"] !== false,
556633
+ extract_segments: args["extract_segments"] !== false,
556634
+ max_segments_per_frame: Number(args["max_segments_per_frame"] ?? 8),
556635
+ agnostic_nms: optionalBoolean(args, "agnostic_nms"),
556121
556636
  detect_objects: args["detect_objects"] !== false,
556122
556637
  track_objects: args["track_objects"] !== false,
556123
556638
  crop_faces: args["crop_faces"] !== false,
@@ -556215,10 +556730,13 @@ var init_live_media_loop = __esm({
556215
556730
  if (result.media_path)
556216
556731
  lines.push(`media: ${result.media_path}`);
556217
556732
  lines.push(`YOLO model: ${result.yolo_model} (${result.yolo_live ? "live" : "mock/harness"})`);
556733
+ if (result.yoloe_prompt_mode && result.yoloe_prompt_mode !== "closed_set") {
556734
+ lines.push(`YOLOE prompt mode: ${result.yoloe_prompt_mode}${result.yoloe_classes?.length ? ` classes=${result.yoloe_classes.join(", ")}` : ""}`);
556735
+ }
556218
556736
  if (result.rotate_degrees)
556219
556737
  lines.push(`frame rotation correction: ${result.rotate_degrees} degrees clockwise`);
556220
556738
  lines.push(`sampled: ${result.frames_sampled} frame(s) over ${result.duration_observed_sec.toFixed(1)}s`);
556221
- lines.push(`objects: ${result.objects.length} detection(s), tracks: ${result.tracks.length}, face crops: ${result.faces.length}`);
556739
+ lines.push(`objects: ${result.objects.length} detection(s), tracks: ${result.tracks.length}, face crops: ${result.faces.length}, segments: ${result.segments?.length ?? 0}`);
556222
556740
  const objectSummary = summarizeObjects(result.objects);
556223
556741
  if (objectSummary.length) {
556224
556742
  lines.push("");
@@ -556240,6 +556758,13 @@ var init_live_media_loop = __esm({
556240
556758
  lines.push(`- ${basename19(face.crop_path)} at ${face.timestamp_sec.toFixed(1)}s ${face.crop_path}`);
556241
556759
  }
556242
556760
  }
556761
+ if (result.segments?.length) {
556762
+ lines.push("");
556763
+ lines.push("## Segments");
556764
+ for (const segment of result.segments.slice(0, 12)) {
556765
+ lines.push(`- ${segment.label} conf=${segment.confidence.toFixed(2)} mask=${segment.mask_path}${segment.crop_path ? ` crop=${segment.crop_path}` : ""} area=${(segment.area_ratio ?? 0).toFixed(4)}`);
556766
+ }
556767
+ }
556243
556768
  if (audioEvidence.length) {
556244
556769
  lines.push("");
556245
556770
  lines.push("## Audio Evidence");
@@ -556263,11 +556788,14 @@ var init_live_media_loop = __esm({
556263
556788
  media_path: result.media_path,
556264
556789
  yolo_model: result.yolo_model,
556265
556790
  yolo_live: result.yolo_live,
556791
+ yoloe_classes: result.yoloe_classes ?? [],
556792
+ yoloe_prompt_mode: result.yoloe_prompt_mode,
556266
556793
  rotate_degrees: result.rotate_degrees ?? 0,
556267
556794
  frames_sampled: result.frames_sampled,
556268
556795
  objects: result.objects.slice(0, 80),
556269
556796
  tracks: result.tracks.slice(0, 40),
556270
556797
  faces: result.faces.slice(0, 20),
556798
+ segments: (result.segments ?? []).slice(0, 40),
556271
556799
  audio_evidence: audioEvidence,
556272
556800
  trigger_hits: hits,
556273
556801
  artifacts_dir: result.output_dir
@@ -557548,6 +558076,7 @@ __export(dist_exports2, {
557548
558076
  buildToolManifestFromModule: () => buildToolManifestFromModule,
557549
558077
  buildWorkboardSynthesisContext: () => buildWorkboardSynthesisContext,
557550
558078
  buildYolo26BootstrapPlan: () => buildYolo26BootstrapPlan,
558079
+ buildYolo26DeploymentOptions: () => buildYolo26DeploymentOptions,
557551
558080
  builtInMediaModelCatalog: () => builtInMediaModelCatalog,
557552
558081
  canInvokeTool: () => canInvokeTool,
557553
558082
  checkConstraints: () => checkConstraints,
@@ -557781,6 +558310,7 @@ __export(dist_exports2, {
557781
558310
  resolveMediaCudaVisibleDevicesForEnv: () => resolveMediaCudaVisibleDevicesForEnv,
557782
558311
  resolveMediaModel: () => resolveMediaModel,
557783
558312
  resolveSecret: () => resolveSecret,
558313
+ resolveYolo26Model: () => resolveYolo26Model,
557784
558314
  revokeSecret: () => revokeSecret,
557785
558315
  runBenchmark: () => runBenchmark,
557786
558316
  runBuild: () => runBuild,
@@ -602192,7 +602722,9 @@ async function buildImageAsciiPreview(inputPath, options2 = {}) {
602192
602722
  plainAscii,
602193
602723
  renderer: "image-to-ascii",
602194
602724
  width,
602195
- height
602725
+ height,
602726
+ imageWidth: dimensions?.width,
602727
+ imageHeight: dimensions?.height
602196
602728
  };
602197
602729
  }
602198
602730
  return {
@@ -602200,11 +602732,24 @@ async function buildImageAsciiPreview(inputPath, options2 = {}) {
602200
602732
  ascii: previewFailure(result.error || "renderer unavailable", width),
602201
602733
  renderer: "image-to-ascii",
602202
602734
  width,
602203
- height
602735
+ height,
602736
+ imageWidth: dimensions?.width,
602737
+ imageHeight: dimensions?.height
602204
602738
  };
602205
602739
  }
602206
602740
  const fallback = await convertWithFfmpeg(imagePath, width, height, timeoutMs);
602207
- if (fallback) return { path: imagePath, ascii: fallback, plainAscii: fallback, renderer: "ffmpeg", width, height };
602741
+ if (fallback) {
602742
+ return {
602743
+ path: imagePath,
602744
+ ascii: fallback,
602745
+ plainAscii: fallback,
602746
+ renderer: "ffmpeg",
602747
+ width,
602748
+ height,
602749
+ imageWidth: dimensions?.width,
602750
+ imageHeight: dimensions?.height
602751
+ };
602752
+ }
602208
602753
  return null;
602209
602754
  }
602210
602755
  function formatImageAsciiContext(preview, label) {
@@ -602323,10 +602868,45 @@ function parseJsonObjectFromText(value2) {
602323
602868
  const direct = parseJsonObject3(value2);
602324
602869
  if (direct) return direct;
602325
602870
  const text2 = String(value2 ?? "");
602326
- const start2 = text2.indexOf("{");
602327
- const end = text2.lastIndexOf("}");
602328
- if (start2 < 0 || end <= start2) return null;
602329
- return parseJsonObject3(text2.slice(start2, end + 1));
602871
+ const candidates = [];
602872
+ let depth = 0;
602873
+ let start2 = -1;
602874
+ let inString = false;
602875
+ let escaped = false;
602876
+ for (let i2 = 0; i2 < text2.length; i2++) {
602877
+ const ch = text2[i2];
602878
+ if (inString) {
602879
+ if (escaped) {
602880
+ escaped = false;
602881
+ } else if (ch === "\\") {
602882
+ escaped = true;
602883
+ } else if (ch === '"') {
602884
+ inString = false;
602885
+ }
602886
+ continue;
602887
+ }
602888
+ if (ch === '"') {
602889
+ inString = true;
602890
+ continue;
602891
+ }
602892
+ if (ch === "{") {
602893
+ if (depth === 0) start2 = i2;
602894
+ depth++;
602895
+ continue;
602896
+ }
602897
+ if (ch === "}" && depth > 0) {
602898
+ depth--;
602899
+ if (depth === 0 && start2 >= 0) {
602900
+ candidates.push(text2.slice(start2, i2 + 1));
602901
+ start2 = -1;
602902
+ }
602903
+ }
602904
+ }
602905
+ for (const candidate of candidates.reverse()) {
602906
+ const parsed = parseJsonObject3(candidate);
602907
+ if (parsed) return parsed;
602908
+ }
602909
+ return null;
602330
602910
  }
602331
602911
  function normalizeLiveCameraRotation(value2) {
602332
602912
  if (value2 === void 0 || value2 === null || value2 === "") return 0;
@@ -602361,9 +602941,9 @@ function parseCameraOrientationDecision(value2) {
602361
602941
  }
602362
602942
  function formatCameraRotation(rotation) {
602363
602943
  if (rotation === 0) return "upright/no correction";
602364
- if (rotation === 90) return "90 degrees clockwise";
602365
- if (rotation === 180) return "180 degrees";
602366
- return "90 degrees counter-clockwise";
602944
+ if (rotation === 90) return "90 degrees clockwise correction";
602945
+ if (rotation === 180) return "180 degrees correction";
602946
+ return "90 degrees counter-clockwise correction";
602367
602947
  }
602368
602948
  function sanitizeCameraOrientation(value2) {
602369
602949
  if (typeof value2 !== "object" || value2 === null || Array.isArray(value2)) return {};
@@ -602418,21 +602998,59 @@ function truncateCell(value2, width) {
602418
602998
  function stripDashboardAnsi(value2) {
602419
602999
  return value2.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
602420
603000
  }
603001
+ function dashboardVisibleWidth(value2) {
603002
+ return stripDashboardAnsi(value2).length;
603003
+ }
603004
+ function truncateAnsiCell(value2, width) {
603005
+ value2 = String(value2 ?? "").replace(/\r?\n/g, " ");
603006
+ if (width <= 0) return "";
603007
+ let out = "";
603008
+ let visible = 0;
603009
+ let i2 = 0;
603010
+ let sawAnsi = false;
603011
+ while (i2 < value2.length && visible < width) {
603012
+ DASHBOARD_ANSI_STICKY_PATTERN.lastIndex = i2;
603013
+ const ansi5 = DASHBOARD_ANSI_STICKY_PATTERN.exec(value2);
603014
+ if (ansi5) {
603015
+ sawAnsi = true;
603016
+ out += ansi5[0];
603017
+ i2 = DASHBOARD_ANSI_STICKY_PATTERN.lastIndex;
603018
+ continue;
603019
+ }
603020
+ const codePoint = value2.codePointAt(i2);
603021
+ if (codePoint == null) break;
603022
+ const ch = String.fromCodePoint(codePoint);
603023
+ out += ch;
603024
+ i2 += ch.length;
603025
+ visible++;
603026
+ }
603027
+ const currentWidth = dashboardVisibleWidth(out);
603028
+ if (currentWidth < width) out += " ".repeat(width - currentWidth);
603029
+ return sawAnsi ? `${out}\x1B[0m` : out;
603030
+ }
602421
603031
  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);
603032
+ const raw = camera.ascii || camera.plainAscii || "";
603033
+ return raw.replace(/\r/g, "").split("\n").map((line) => line.replace(/\s+$/g, "")).filter((line) => stripDashboardAnsi(line).trim().length > 0);
602424
603034
  }
602425
603035
  function dashboardPreviewAspect(lines) {
602426
603036
  const widths = lines.map((line) => stripDashboardAnsi(line).length).filter((n2) => n2 > 0);
602427
603037
  const maxWidth = Math.max(1, ...widths);
602428
603038
  return Math.max(0.12, Math.min(1.6, lines.length / maxWidth));
602429
603039
  }
603040
+ function cameraDashboardAspect(camera, lines) {
603041
+ const frameWidth = Number(camera.frameWidth ?? 0);
603042
+ const frameHeight = Number(camera.frameHeight ?? 0);
603043
+ if (frameWidth > 0 && frameHeight > 0) {
603044
+ return Math.max(0.12, Math.min(1.4, frameHeight / frameWidth * 0.52));
603045
+ }
603046
+ return dashboardPreviewAspect(lines);
603047
+ }
602430
603048
  function fitDashboardPane(lines, paneWidth, paneHeight, fallback) {
602431
603049
  const source = lines.length > 0 ? lines : [fallback];
602432
603050
  const normalized = [];
602433
603051
  for (let i2 = 0; i2 < paneHeight; i2++) {
602434
603052
  const sourceIndex = source.length <= paneHeight ? i2 : Math.min(source.length - 1, Math.floor(i2 * source.length / paneHeight));
602435
- normalized.push(truncateCell(source[sourceIndex] ?? "", paneWidth));
603053
+ normalized.push(truncateAnsiCell(source[sourceIndex] ?? "", paneWidth));
602436
603054
  }
602437
603055
  while (normalized.length < paneHeight) normalized.push(" ".repeat(paneWidth));
602438
603056
  return normalized;
@@ -602451,7 +603069,7 @@ function liveDashboardPreviewWidthForSources(sourceCount, termWidth = process.st
602451
603069
  function formatCameraPane(camera, paneWidth, paneHeight, now2) {
602452
603070
  const contentWidth = Math.max(10, paneWidth - 2);
602453
603071
  const age = Math.max(0, Math.round((now2 - camera.updatedAt) / 1e3));
602454
- const title = `${compactSourceId(camera.source)} ${age}s`;
603072
+ const title = `${compactSourceId(camera.source)} ${age}s${cameraPaneBadge(camera)}`;
602455
603073
  const topTitle = ` ${title} `;
602456
603074
  const left = Math.max(0, Math.floor((contentWidth - topTitle.length) / 2));
602457
603075
  const right = Math.max(0, contentWidth - topTitle.length - left);
@@ -602460,7 +603078,7 @@ function formatCameraPane(camera, paneWidth, paneHeight, now2) {
602460
603078
  const previewLines = dashboardPreviewLines(camera);
602461
603079
  const fallback = camera.framePath ? "preview refreshing" : camera.error ? "capture failed" : "awaiting frame";
602462
603080
  const body = fitDashboardPane(previewLines, contentWidth, paneHeight, fallback);
602463
- return [top, ...body.map((line) => `│${truncateCell(line, contentWidth)}│`), bottom];
603081
+ return [top, ...body.map((line) => `│${truncateAnsiCell(line, contentWidth)}│`), bottom];
602464
603082
  }
602465
603083
  function pushDashboardWrapped(lines, value2, width) {
602466
603084
  const contentWidth = Math.max(10, width - 2);
@@ -602586,6 +603204,116 @@ function fallbackLiveAudioIntakeDecision(transcript) {
602586
603204
  reason: addressed ? "fallback detected direct address or question shape" : "fallback could not establish that ambient speech was addressed to Omnius"
602587
603205
  };
602588
603206
  }
603207
+ function parseLiveSpeechDecision(value2) {
603208
+ const parsed = parseJsonObjectFromText(value2);
603209
+ if (!parsed) return null;
603210
+ const speak = parsed["speak"];
603211
+ const confidence2 = Math.max(0, Math.min(1, Number(parsed["confidence"] ?? 0)));
603212
+ if (typeof speak !== "boolean" || !Number.isFinite(confidence2)) return null;
603213
+ const actionRaw = String(parsed["action"] ?? (speak ? "speak" : "silent")).toLowerCase();
603214
+ const action = actionRaw === "speak" || actionRaw === "silent" || actionRaw === "queue_review" ? actionRaw : speak ? "speak" : "silent";
603215
+ return {
603216
+ speak,
603217
+ text: trimOneLine(parsed["text"] ?? "", 180),
603218
+ confidence: confidence2,
603219
+ action,
603220
+ reason: trimOneLine(parsed["reason"] ?? "", 220) || "live speech arbiter decision"
603221
+ };
603222
+ }
603223
+ function fallbackLiveSpeechDecision(proposal) {
603224
+ if (proposal.kind === "unknown_person") {
603225
+ return {
603226
+ speak: true,
603227
+ text: proposal.defaultText,
603228
+ confidence: 0.72,
603229
+ action: "speak",
603230
+ reason: "unknown person visible and identity clarification is useful"
603231
+ };
603232
+ }
603233
+ if (proposal.kind === "audio_intent") {
603234
+ return {
603235
+ speak: true,
603236
+ text: proposal.defaultText,
603237
+ confidence: 0.68,
603238
+ action: "speak",
603239
+ reason: "audio intake classified speech as addressed to Omnius"
603240
+ };
603241
+ }
603242
+ if (proposal.urgency === "high") {
603243
+ return {
603244
+ speak: true,
603245
+ text: proposal.defaultText,
603246
+ confidence: 0.6,
603247
+ action: "speak",
603248
+ reason: "high-urgency live trigger"
603249
+ };
603250
+ }
603251
+ return {
603252
+ speak: false,
603253
+ text: "",
603254
+ confidence: 0.55,
603255
+ action: "silent",
603256
+ reason: "ambient event did not justify interrupting with speech"
603257
+ };
603258
+ }
603259
+ async function classifyLiveSpeech(proposal, config) {
603260
+ const now2 = Date.now();
603261
+ if (config?.backendUrl && config.model) {
603262
+ const url = `${config.backendUrl.replace(/\/+$/, "")}/v1/chat/completions`;
603263
+ const body = {
603264
+ model: config.model,
603265
+ temperature: 0,
603266
+ max_tokens: 180,
603267
+ think: false,
603268
+ messages: [
603269
+ {
603270
+ role: "system",
603271
+ content: [
603272
+ "You are Omnius' live audio/video speech arbiter.",
603273
+ "Decide whether the agent should speak aloud into the physical environment right now.",
603274
+ "Speak rarely. Do not narrate routine detections or every frame.",
603275
+ "Speak when directly addressed, when an unknown person should be politely identified, or when an important visual/audio trigger needs immediate clarification.",
603276
+ "Never invent a person's identity. If asking identity, ask one short natural question.",
603277
+ "Keep spoken text under 16 words.",
603278
+ 'Return only JSON: {"speak": boolean, "text": "short utterance", "confidence": 0.0-1.0, "action": "speak"|"silent"|"queue_review", "reason": "brief evidence"}.'
603279
+ ].join("\n")
603280
+ },
603281
+ {
603282
+ role: "user",
603283
+ content: [
603284
+ `kind=${proposal.kind}`,
603285
+ `source=${proposal.source}`,
603286
+ `urgency=${proposal.urgency}`,
603287
+ `summary=${proposal.summary}`,
603288
+ `evidence=${proposal.evidence}`,
603289
+ `default_utterance=${proposal.defaultText}`
603290
+ ].join("\n").slice(0, 1600)
603291
+ }
603292
+ ]
603293
+ };
603294
+ try {
603295
+ const controller = new AbortController();
603296
+ const timer = setTimeout(() => controller.abort(), 5e3).unref();
603297
+ const resp = await fetch(url, {
603298
+ method: "POST",
603299
+ headers: {
603300
+ "Content-Type": "application/json",
603301
+ ...config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}
603302
+ },
603303
+ body: JSON.stringify(body),
603304
+ signal: controller.signal
603305
+ });
603306
+ clearTimeout(timer);
603307
+ if (resp.ok) {
603308
+ const json = await resp.json();
603309
+ const parsed = parseLiveSpeechDecision(json.choices?.[0]?.message?.content ?? "");
603310
+ if (parsed) return { ...parsed, source: "live-arbiter", updatedAt: now2 };
603311
+ }
603312
+ } catch {
603313
+ }
603314
+ }
603315
+ return { ...fallbackLiveSpeechDecision(proposal), source: "fallback", updatedAt: now2 };
603316
+ }
602589
603317
  async function classifyLiveAudioIntake(transcript, config) {
602590
603318
  const now2 = Date.now();
602591
603319
  const clean5 = transcript.trim();
@@ -602654,9 +603382,152 @@ function summarizeInference(value2) {
602654
603382
  const sampled = lines.find((line) => /^objects:/i.test(line));
602655
603383
  return sampled ? trimOneLine(sampled, 160) : trimOneLine(lines.slice(0, 4).join("; "), 160);
602656
603384
  }
603385
+ function boundedConfidence(value2) {
603386
+ const n2 = Number(value2);
603387
+ return Number.isFinite(n2) ? Math.max(0, Math.min(1, n2)) : void 0;
603388
+ }
603389
+ function mergeClassificationInto(map2, item) {
603390
+ const label = trimOneLine(item.label || item.kind, 80) || item.kind;
603391
+ const key = `${item.kind}:${label}:${item.trackId ?? ""}:${item.evidencePath ?? ""}`;
603392
+ const existing = map2.get(key);
603393
+ const count = Math.max(1, Number(item.count ?? 1));
603394
+ if (!existing) {
603395
+ map2.set(key, { ...item, label, count });
603396
+ return;
603397
+ }
603398
+ const total = existing.count + count;
603399
+ const confidence2 = existing.confidence !== void 0 || item.confidence !== void 0 ? ((existing.confidence ?? 0) * existing.count + (item.confidence ?? 0) * count) / total : void 0;
603400
+ map2.set(key, {
603401
+ ...existing,
603402
+ count: total,
603403
+ confidence: confidence2,
603404
+ updatedAt: Math.max(existing.updatedAt, item.updatedAt)
603405
+ });
603406
+ }
603407
+ function classificationsFromLiveMediaPayload(payload, sampledAt, fallbackSource) {
603408
+ if (!payload) return [];
603409
+ const map2 = /* @__PURE__ */ new Map();
603410
+ void fallbackSource;
603411
+ for (const object of payload.objects ?? []) {
603412
+ const label = trimOneLine(object.label || "object", 80) || "object";
603413
+ mergeClassificationInto(map2, {
603414
+ kind: "object",
603415
+ label,
603416
+ count: 1,
603417
+ confidence: boundedConfidence(object.confidence),
603418
+ trackId: object.track_id === void 0 || object.track_id === null ? void 0 : String(object.track_id),
603419
+ updatedAt: observedAtFromOffset(sampledAt, payload, Number(object.timestamp_sec ?? 0))
603420
+ });
603421
+ }
603422
+ for (const track of payload.tracks ?? []) {
603423
+ const label = trimOneLine(track.label || "object", 80) || "object";
603424
+ mergeClassificationInto(map2, {
603425
+ kind: "track",
603426
+ label,
603427
+ count: 1,
603428
+ confidence: boundedConfidence(track.mean_confidence),
603429
+ trackId: track.track_id === void 0 || track.track_id === null ? void 0 : String(track.track_id),
603430
+ updatedAt: observedAtFromOffset(sampledAt, payload, Number(track.last_seen_sec ?? track.first_seen_sec ?? 0))
603431
+ });
603432
+ }
603433
+ for (const face of payload.faces ?? []) {
603434
+ const identity3 = parseFaceIdentity(face);
603435
+ const label = identity3.status === "known" && identity3.name ? identity3.name : "unknown face";
603436
+ mergeClassificationInto(map2, {
603437
+ kind: identity3.status === "known" ? "identity" : "face",
603438
+ label,
603439
+ count: 1,
603440
+ confidence: boundedConfidence(identity3.confidence ?? face.confidence),
603441
+ evidencePath: typeof face.crop_path === "string" ? face.crop_path : void 0,
603442
+ updatedAt: observedAtFromOffset(sampledAt, payload, Number(face.timestamp_sec ?? 0))
603443
+ });
603444
+ }
603445
+ for (const segment of payload.segments ?? []) {
603446
+ const label = trimOneLine(segment.label || "segment", 80) || "segment";
603447
+ mergeClassificationInto(map2, {
603448
+ kind: "segment",
603449
+ label,
603450
+ count: 1,
603451
+ confidence: boundedConfidence(segment.confidence),
603452
+ evidencePath: typeof segment.crop_path === "string" && segment.crop_path ? segment.crop_path : typeof segment.mask_path === "string" ? segment.mask_path : void 0,
603453
+ updatedAt: observedAtFromOffset(sampledAt, payload, Number(segment.timestamp_sec ?? 0))
603454
+ });
603455
+ }
603456
+ for (const hit of payload.trigger_hits ?? []) {
603457
+ const label = trimOneLine(hit.triggerName || hit.matched || "visual trigger", 80) || "visual trigger";
603458
+ mergeClassificationInto(map2, {
603459
+ kind: "trigger",
603460
+ label,
603461
+ count: 1,
603462
+ confidence: boundedConfidence(hit.confidence),
603463
+ updatedAt: sampledAt
603464
+ });
603465
+ }
603466
+ return [...map2.values()].sort((a2, b) => {
603467
+ const aPriority = a2.kind === "face" || a2.kind === "identity" ? 0 : a2.kind === "segment" ? 1 : a2.kind === "object" ? 2 : 3;
603468
+ const bPriority = b.kind === "face" || b.kind === "identity" ? 0 : b.kind === "segment" ? 1 : b.kind === "object" ? 2 : 3;
603469
+ if (aPriority !== bPriority) return aPriority - bPriority;
603470
+ return (b.confidence ?? 0) - (a2.confidence ?? 0);
603471
+ }).slice(0, 24).map((item) => ({ ...item }));
603472
+ }
603473
+ function classificationsFromClipSummary(summary, observedAt, evidencePath) {
603474
+ const parsed = parseJsonObjectFromText(summary);
603475
+ const matches = Array.isArray(parsed?.["matches"]) ? parsed["matches"] : [];
603476
+ return matches.slice(0, 8).map((match) => {
603477
+ const label = trimOneLine(
603478
+ match["label"] ?? match["name"] ?? match["title"] ?? match["id"] ?? "visual-memory match",
603479
+ 80
603480
+ ) || "visual-memory match";
603481
+ return {
603482
+ kind: "clip",
603483
+ label,
603484
+ count: 1,
603485
+ confidence: boundedConfidence(match["score"] ?? match["similarity"] ?? match["confidence"]),
603486
+ evidencePath,
603487
+ updatedAt: observedAt
603488
+ };
603489
+ });
603490
+ }
603491
+ function mergeCameraClassifications(existing, incoming, limit = 24) {
603492
+ const map2 = /* @__PURE__ */ new Map();
603493
+ for (const item of [...existing ?? [], ...incoming]) {
603494
+ mergeClassificationInto(map2, item);
603495
+ }
603496
+ const merged = [...map2.values()].sort((a2, b) => b.updatedAt - a2.updatedAt || (b.confidence ?? 0) - (a2.confidence ?? 0)).slice(0, limit);
603497
+ return merged.length > 0 ? merged : existing;
603498
+ }
603499
+ function cameraClassificationsSummary(camera, maxChars = 220) {
603500
+ const items = (camera.classifications ?? []).sort((a2, b) => {
603501
+ const aPriority = a2.kind === "face" || a2.kind === "identity" ? 0 : a2.kind === "segment" ? 1 : a2.kind === "object" ? 2 : a2.kind === "track" ? 3 : 4;
603502
+ const bPriority = b.kind === "face" || b.kind === "identity" ? 0 : b.kind === "segment" ? 1 : b.kind === "object" ? 2 : b.kind === "track" ? 3 : 4;
603503
+ if (aPriority !== bPriority) return aPriority - bPriority;
603504
+ return (b.confidence ?? 0) - (a2.confidence ?? 0);
603505
+ }).slice(0, 8).map((item) => {
603506
+ const count = item.count > 1 ? ` x${item.count}` : "";
603507
+ const confidence2 = item.confidence !== void 0 ? ` ${item.confidence.toFixed(2)}` : "";
603508
+ return `${item.kind}:${item.label}${count}${confidence2}`;
603509
+ });
603510
+ return trimOneLine(items.join("; "), maxChars);
603511
+ }
603512
+ function cameraPaneBadge(camera) {
603513
+ const classifications = camera.classifications ?? [];
603514
+ const faceCount = classifications.filter((item) => item.kind === "face" || item.kind === "identity").reduce((sum, item) => sum + Math.max(1, item.count), 0);
603515
+ const objectCount = classifications.filter((item) => item.kind === "object").reduce((sum, item) => sum + Math.max(1, item.count), 0);
603516
+ const segmentCount = classifications.filter((item) => item.kind === "segment").reduce((sum, item) => sum + Math.max(1, item.count), 0);
603517
+ const trackCount = classifications.filter((item) => item.kind === "track").reduce((sum, item) => sum + Math.max(1, item.count), 0);
603518
+ const triggerCount = classifications.filter((item) => item.kind === "trigger").reduce((sum, item) => sum + Math.max(1, item.count), 0);
603519
+ const parts = [
603520
+ faceCount > 0 ? `F${faceCount}` : "",
603521
+ segmentCount > 0 ? `S${segmentCount}` : "",
603522
+ objectCount > 0 ? `O${objectCount}` : "",
603523
+ trackCount > 0 ? `T${trackCount}` : "",
603524
+ triggerCount > 0 ? `!${triggerCount}` : ""
603525
+ ].filter(Boolean);
603526
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
603527
+ }
602657
603528
  function cameraSnapshotSummary(camera) {
602658
603529
  if (camera.error) return `error: ${trimOneLine(camera.error, 160)}`;
602659
- return camera.summary || summarizeInference(camera.inference);
603530
+ return camera.summary || (camera.classifications?.length ? `classifications: ${cameraClassificationsSummary(camera)}` : "") || summarizeInference(camera.inference);
602660
603531
  }
602661
603532
  function formatLiveDashboardFromSnapshot(snapshot, opts = {}) {
602662
603533
  const width = Math.max(60, opts.width ?? 100);
@@ -602681,13 +603552,14 @@ function formatLiveDashboardFromSnapshot(snapshot, opts = {}) {
602681
603552
  for (let start2 = 0; start2 < cameras.length; start2 += paneCount) {
602682
603553
  const group = cameras.slice(start2, start2 + paneCount);
602683
603554
  const sourceLines = group.map((camera) => dashboardPreviewLines(camera));
602684
- const aspect = Math.max(0.22, Math.min(0.7, Math.max(...sourceLines.map(dashboardPreviewAspect))));
603555
+ const aspects = group.map((camera, idx) => cameraDashboardAspect(camera, sourceLines[idx] ?? []));
603556
+ const aspect = Math.max(0.18, Math.min(0.92, Math.max(...aspects)));
602685
603557
  const paneHeight = Math.max(10, Math.min(32, Math.round(paneContentWidth * aspect)));
602686
603558
  const panes = group.map((camera) => formatCameraPane(camera, paneWidth, paneHeight, now2));
602687
603559
  const rowCount = Math.max(...panes.map((pane) => pane.length));
602688
603560
  for (let row = 0; row < rowCount; row++) {
602689
603561
  const body = panes.map((pane) => pane[row] ?? " ".repeat(paneWidth)).join(" ");
602690
- lines.push(`│${truncateCell(body, innerWidth)}│`);
603562
+ lines.push(`│${truncateAnsiCell(body, innerWidth)}│`);
602691
603563
  }
602692
603564
  }
602693
603565
  }
@@ -602698,8 +603570,10 @@ function formatLiveDashboardFromSnapshot(snapshot, opts = {}) {
602698
603570
  for (const camera of cameras) {
602699
603571
  const age = Math.max(0, Math.round((now2 - camera.updatedAt) / 1e3));
602700
603572
  const rotation = camera.orientation ? formatCameraRotation(camera.orientation.rotation) : "upright/no correction";
603573
+ const classifications = cameraClassificationsSummary(camera);
602701
603574
  const detailLines = [
602702
603575
  `${compactSourceId(camera.source)} age=${age}s rotation=${rotation}`,
603576
+ classifications ? `classifications: ${classifications}` : "",
602703
603577
  cameraSnapshotSummary(camera),
602704
603578
  camera.framePath ? `frame: ${camera.displayPath || camera.framePath}` : "",
602705
603579
  camera.orientation?.evidence ? `orientation: ${trimOneLine(camera.orientation.evidence, 120)}` : ""
@@ -602729,7 +603603,7 @@ function formatLiveDashboardFromSnapshot(snapshot, opts = {}) {
602729
603603
  function parseLiveMediaPayload(value2) {
602730
603604
  const parsed = parseJsonObject3(value2);
602731
603605
  if (!parsed) return null;
602732
- if (!Array.isArray(parsed["objects"]) && !Array.isArray(parsed["tracks"]) && !Array.isArray(parsed["faces"])) return null;
603606
+ if (!Array.isArray(parsed["objects"]) && !Array.isArray(parsed["tracks"]) && !Array.isArray(parsed["faces"]) && !Array.isArray(parsed["segments"])) return null;
602733
603607
  return parsed;
602734
603608
  }
602735
603609
  function observedAtFromOffset(sampledAt, payload, offsetSec) {
@@ -602826,6 +603700,21 @@ function observationsFromLiveMediaPayload(payload, sampledAt, fallbackSource) {
602826
603700
  evidence: identity3.evidence
602827
603701
  });
602828
603702
  }
603703
+ for (const segment of (payload.segments ?? []).slice(0, 24)) {
603704
+ const label = String(segment.label || "segment");
603705
+ const observedAt = observedAtFromOffset(sampledAt, payload, Number(segment.timestamp_sec ?? 0));
603706
+ const evidencePath = typeof segment.crop_path === "string" && segment.crop_path ? segment.crop_path : typeof segment.mask_path === "string" ? segment.mask_path : void 0;
603707
+ events.push({
603708
+ id: stableEventId("camera_segment", source, observedAt, `${label}:${segment.frame_index ?? ""}:${evidencePath ?? ""}`),
603709
+ kind: "camera_segment",
603710
+ observedAt,
603711
+ source,
603712
+ title: `Segmented ${label}`,
603713
+ summary: `frame=${segment.frame_index ?? "?"} t=${Number(segment.timestamp_sec ?? 0).toFixed(1)}s conf=${Number(segment.confidence ?? 0).toFixed(2)} area=${Number(segment.area_ratio ?? 0).toFixed(4)} mask=${segment.mask_path ?? "none"} crop=${segment.crop_path ?? "none"} bbox=${JSON.stringify(segment.bbox_xyxy ?? [])}`,
603714
+ confidence: typeof segment.confidence === "number" ? segment.confidence : void 0,
603715
+ evidencePath
603716
+ });
603717
+ }
602829
603718
  const faceCount = people.length;
602830
603719
  if (faceCount === 0) {
602831
603720
  const personTracks = (payload.tracks ?? []).filter((track) => String(track.label || "").toLowerCase() === "person").slice(0, 8);
@@ -602951,8 +603840,8 @@ function loadLiveSensorConfig(repoRoot) {
602951
603840
  ...DEFAULT_CONFIG7,
602952
603841
  ...parsed,
602953
603842
  cameraOrientation: sanitizeCameraOrientation(parsed.cameraOrientation),
602954
- videoIntervalMs: Math.max(1500, Number(parsed.videoIntervalMs ?? DEFAULT_CONFIG7.videoIntervalMs)),
602955
- audioIntervalMs: Math.max(3e3, Number(parsed.audioIntervalMs ?? DEFAULT_CONFIG7.audioIntervalMs))
603843
+ videoIntervalMs: Math.max(MIN_VIDEO_INTERVAL_MS, Number(parsed.videoIntervalMs ?? DEFAULT_CONFIG7.videoIntervalMs)),
603844
+ audioIntervalMs: Math.max(MIN_AUDIO_INTERVAL_MS, Number(parsed.audioIntervalMs ?? DEFAULT_CONFIG7.audioIntervalMs))
602956
603845
  };
602957
603846
  } catch {
602958
603847
  return { ...DEFAULT_CONFIG7 };
@@ -602977,6 +603866,8 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
602977
603866
  lines.push("<live-sensor-context>");
602978
603867
  lines.push("## LIVE SENSOR STATUS");
602979
603868
  lines.push("Live sensor context is operator-enabled ambient input. Treat it as current perceptual evidence for this turn; if it is stale, missing, or insufficient, use camera/audio tools to refresh before answering what you see or hear.");
603869
+ lines.push('Live world-model rule: when the user asks what is around, who is present, what changed, what is heard, or asks a deictic question like "what do you see", treat the live camera/audio fields as first-class context. Be curious and tool-capable: refresh stale sensors, inspect frame paths, query vision/CLIP/visual_memory/audio tools, and combine metadata into a concise answer. Do not rely only on summaries when the task requires current perception.');
603870
+ lines.push("Live speech rule: in low-latency voice/live modes, short spoken replies are appropriate for direct address, identity clarification, or important visual/audio triggers. Do not narrate every frame.");
602980
603871
  lines.push("Identity rule: never invent names for observed people. If a person is unknown and identity matters, ask the user or the person for their name, then store the association with visual_memory(action='enroll', image='<face crop>', name='<name>') when a face crop is available, or multimodal_memory(action='meet', person_name='<name>') when live voice/name context is available.");
602981
603872
  lines.push(`Snapshot file: ${liveSnapshotPath(snapshot.repoRoot)}`);
602982
603873
  lines.push(`Streams: video=${snapshot.config.videoEnabled ? "on" : "off"} infer=${snapshot.config.inferEnabled ? "on" : "off"} clip=${snapshot.config.clipEnabled ? "on" : "off"} audio=${snapshot.config.audioEnabled ? "on" : "off"} asr=${snapshot.config.asrEnabled ? "on" : "off"} sounds=${snapshot.config.audioAnalysisEnabled ? "on" : "off"} output-monitor=${snapshot.config.audioOutputEnabled ? "on" : "off"}`);
@@ -602996,6 +603887,8 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
602996
603887
  const age = Math.max(0, Math.round((now2 - camera.updatedAt) / 1e3));
602997
603888
  const rotation = camera.orientation ? formatCameraRotation(camera.orientation.rotation) : "upright/no correction";
602998
603889
  lines.push(`- ${camera.source} age=${age}s rotation=${rotation}${camera.framePath ? ` frame=${camera.framePath}` : ""}${camera.error ? ` error=${trimOneLine(camera.error, 240)}` : ""}`);
603890
+ const classifications = cameraClassificationsSummary(camera);
603891
+ if (classifications) lines.push(` classifications: ${classifications}`);
602999
603892
  const summary = cameraSnapshotSummary(camera);
603000
603893
  if (summary) lines.push(` summary: ${summary}`);
603001
603894
  }
@@ -603050,6 +603943,8 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
603050
603943
  lines.push(`timestamp: ${nowIso3(snapshot.video.updatedAt)} age=${Math.max(0, Math.round(age / 1e3))}s source=${snapshot.video.source}${snapshot.video.orientation ? ` rotation=${formatCameraRotation(snapshot.video.orientation.rotation)}` : ""}${age > maxAgeMs ? " STALE" : ""}`);
603051
603944
  if (snapshot.video.error) lines.push(`error: ${snapshot.video.error}`);
603052
603945
  if (snapshot.video.framePath) lines.push(`frame: ${snapshot.video.framePath}`);
603946
+ const classifications = cameraClassificationsSummary(snapshot.video);
603947
+ if (classifications) lines.push(`Live classifications: ${classifications}`);
603053
603948
  if (snapshot.video.asciiContext) lines.push(snapshot.video.asciiContext);
603054
603949
  if (snapshot.video.inference) {
603055
603950
  lines.push("Video inference:");
@@ -603066,6 +603961,8 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
603066
603961
  lines.push(`timestamp: ${nowIso3(camera.updatedAt)} age=${Math.max(0, Math.round(age / 1e3))}s rotation=${rotation}${age > maxAgeMs ? " STALE" : ""}`);
603067
603962
  if (camera.error) lines.push(`error: ${camera.error}`);
603068
603963
  if (camera.framePath) lines.push(`frame: ${camera.framePath}`);
603964
+ const classifications = cameraClassificationsSummary(camera);
603965
+ if (classifications) lines.push(`classifications: ${classifications}`);
603069
603966
  lines.push(`summary: ${cameraSnapshotSummary(camera)}`);
603070
603967
  }
603071
603968
  }
@@ -603117,6 +604014,16 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
603117
604014
  function formatLiveSensorContext(repoRoot) {
603118
604015
  return formatLiveSensorContextFromSnapshot(readLiveSensorSnapshot(repoRoot));
603119
604016
  }
604017
+ function isLiveSensorSnapshotActive(snapshot) {
604018
+ const cfg = snapshot?.config;
604019
+ if (!cfg) return false;
604020
+ return Boolean(
604021
+ cfg.videoEnabled || cfg.audioEnabled || cfg.audioOutputEnabled || cfg.inferEnabled || cfg.clipEnabled || cfg.asrEnabled || cfg.audioAnalysisEnabled
604022
+ );
604023
+ }
604024
+ function isLiveSensorActiveForRepo(repoRoot) {
604025
+ return isLiveSensorSnapshotActive(readLiveSensorSnapshot(repoRoot));
604026
+ }
603120
604027
  async function discoverAudioInputs() {
603121
604028
  const devices = [];
603122
604029
  const errors = [];
@@ -603250,31 +604157,101 @@ print(json.dumps({"ok": True, "scores": scores}))
603250
604157
  async function tryRecordAudioDevice(device, durationSec, outputPath3) {
603251
604158
  await recordAudioSample(device, durationSec, outputPath3);
603252
604159
  }
604160
+ function hasLiveAudioSignal(activity) {
604161
+ if (!activity) return false;
604162
+ return activity.peak >= 0.025 || activity.rmsDb > -48 || activity.activeRatio >= 0.025;
604163
+ }
604164
+ function audioCaptureMethod(device) {
604165
+ return device.startsWith("pulse:") ? "pulse/ffmpeg" : "arecord";
604166
+ }
603253
604167
  async function recordAudioSampleWithRecovery(preferred, devices, durationSec, outputPath3) {
603254
604168
  const ordered = [];
603255
604169
  const add3 = (id2) => {
603256
604170
  const clean5 = String(id2 ?? "").trim();
603257
604171
  if (clean5 && !ordered.includes(clean5)) ordered.push(clean5);
603258
604172
  };
603259
- add3(preferred);
603260
- for (const device of devices.filter((entry) => entry.source === "pulse" || entry.source === "pipewire")) add3(device.id);
604173
+ const preferredInput = preferredLiveAudioInputId(devices, preferred);
604174
+ add3(preferredInput);
604175
+ if (preferred && !isAudioOutputMonitorId(preferred)) add3(preferred);
604176
+ for (const device of rankLiveAudioInputDevices(devices, preferred).filter((entry) => !isAudioOutputMonitorDevice(entry))) add3(device.id);
603261
604177
  add3("pulse:default");
603262
604178
  add3("default");
603263
- for (const device of devices.filter((entry) => entry.source !== "pulse" && entry.source !== "pipewire")) add3(device.id);
604179
+ for (const device of rankLiveAudioInputDevices(devices, preferred).filter((entry) => isAudioOutputMonitorDevice(entry))) add3(device.id);
603264
604180
  const errors = [];
604181
+ const attempts = [];
604182
+ let firstQuietCapture = null;
603265
604183
  for (const candidate of ordered) {
604184
+ attempts.push(candidate);
603266
604185
  try {
603267
604186
  await tryRecordAudioDevice(candidate, durationSec, outputPath3);
603268
- return { ok: true, device: candidate, method: candidate.startsWith("pulse:") ? "pulse/ffmpeg" : "arecord", errors };
604187
+ const activity = readPcm16WavActivity(outputPath3);
604188
+ const method = audioCaptureMethod(candidate);
604189
+ if (hasLiveAudioSignal(activity)) {
604190
+ return { ok: true, device: candidate, method, errors, attempts, activity, signalDetected: true };
604191
+ }
604192
+ if (!firstQuietCapture) {
604193
+ firstQuietCapture = {
604194
+ device: candidate,
604195
+ method,
604196
+ activity,
604197
+ bytes: readFileSync88(outputPath3),
604198
+ errors: [...errors]
604199
+ };
604200
+ }
604201
+ errors.push(`${candidate}: captured but no live input signal detected`);
603269
604202
  } catch (err) {
603270
604203
  errors.push(`${candidate}: ${err instanceof Error ? err.message : String(err)}`.slice(0, 360));
603271
604204
  }
603272
604205
  }
603273
- return { ok: false, device: preferred || "default", method: "none", errors };
604206
+ if (firstQuietCapture) {
604207
+ writeFileSync56(outputPath3, firstQuietCapture.bytes);
604208
+ return {
604209
+ ok: true,
604210
+ device: firstQuietCapture.device,
604211
+ method: firstQuietCapture.method,
604212
+ errors,
604213
+ attempts,
604214
+ activity: firstQuietCapture.activity,
604215
+ signalDetected: false
604216
+ };
604217
+ }
604218
+ return { ok: false, device: preferredInput || preferred || "default", method: "none", errors, attempts };
603274
604219
  }
603275
604220
  function firstDeviceId(devices) {
603276
604221
  return devices.find((device) => device.id && !/metadata\/non-capture/i.test(device.detail ?? ""))?.id ?? devices[0]?.id;
603277
604222
  }
604223
+ function isAudioOutputMonitorId(id2) {
604224
+ return /(?:\.monitor\b|sink\.monitor|alsa_output|output\.monitor)/i.test(String(id2 ?? ""));
604225
+ }
604226
+ function isAudioOutputMonitorDevice(device) {
604227
+ if (!device) return false;
604228
+ return isAudioOutputMonitorId(device.id) || isAudioOutputMonitorId(device.label) || isAudioOutputMonitorId(device.detail);
604229
+ }
604230
+ function preferredLiveAudioInputId(devices, configured) {
604231
+ const usable = devices.filter((device) => device.id && !isAudioOutputMonitorDevice(device));
604232
+ if (configured && usable.some((device) => device.id === configured)) return configured;
604233
+ return rankLiveAudioInputDevices(devices, configured)[0]?.id ?? firstDeviceId(devices);
604234
+ }
604235
+ function liveAudioDeviceRank(device, configured) {
604236
+ let rank = 0;
604237
+ if (device.id === configured) rank -= 40;
604238
+ if (isAudioOutputMonitorDevice(device)) rank += 500;
604239
+ if (device.source === "pulse" || device.source === "pipewire") rank -= 20;
604240
+ if (device.source === "alsa") rank -= 10;
604241
+ if (device.source === "default") rank += 30;
604242
+ if (/metadata\/non-capture/i.test(device.detail ?? "")) rank += 300;
604243
+ return rank;
604244
+ }
604245
+ function rankLiveAudioInputDevices(devices, configured) {
604246
+ const seen = /* @__PURE__ */ new Set();
604247
+ const ranked = [];
604248
+ for (const device of devices) {
604249
+ if (!device.id || seen.has(device.id)) continue;
604250
+ seen.add(device.id);
604251
+ ranked.push(device);
604252
+ }
604253
+ return ranked.sort((a2, b) => liveAudioDeviceRank(a2, configured) - liveAudioDeviceRank(b, configured));
604254
+ }
603278
604255
  function getLiveSensorManager(repoRoot) {
603279
604256
  const key = repoRoot;
603280
604257
  let manager = managers.get(key);
@@ -603316,13 +604293,17 @@ function formatLiveStatus(snapshot) {
603316
604293
  }
603317
604294
  return lines.join("\n");
603318
604295
  }
603319
- var DEFAULT_CONFIG7, managers, LiveSensorManager;
604296
+ var MIN_VIDEO_INTERVAL_MS, LOW_LATENCY_VIDEO_INTERVAL_MS, MIN_AUDIO_INTERVAL_MS, LOW_LATENCY_AUDIO_INTERVAL_MS, DEFAULT_CONFIG7, managers, DASHBOARD_ANSI_STICKY_PATTERN, LiveSensorManager;
603320
604297
  var init_live_sensors = __esm({
603321
604298
  "packages/cli/src/tui/live-sensors.ts"() {
603322
604299
  "use strict";
603323
604300
  init_dist5();
603324
604301
  init_async_process();
603325
604302
  init_image_ascii_preview();
604303
+ MIN_VIDEO_INTERVAL_MS = 250;
604304
+ LOW_LATENCY_VIDEO_INTERVAL_MS = 250;
604305
+ MIN_AUDIO_INTERVAL_MS = 1500;
604306
+ LOW_LATENCY_AUDIO_INTERVAL_MS = 2e3;
603326
604307
  DEFAULT_CONFIG7 = {
603327
604308
  videoEnabled: false,
603328
604309
  audioEnabled: false,
@@ -603332,10 +604313,11 @@ var init_live_sensors = __esm({
603332
604313
  asrEnabled: false,
603333
604314
  audioAnalysisEnabled: false,
603334
604315
  cameraOrientation: {},
603335
- videoIntervalMs: 5e3,
603336
- audioIntervalMs: 9e3
604316
+ videoIntervalMs: 1e3,
604317
+ audioIntervalMs: 6e3
603337
604318
  };
603338
604319
  managers = /* @__PURE__ */ new Map();
604320
+ DASHBOARD_ANSI_STICKY_PATTERN = /\x1B\[[0-?]*[ -/]*[@-~]/y;
603339
604321
  LiveSensorManager = class {
603340
604322
  constructor(repoRoot) {
603341
604323
  this.repoRoot = repoRoot;
@@ -603363,6 +604345,13 @@ var init_live_sensors = __esm({
603363
604345
  lastAgentReviewAt = 0;
603364
604346
  autoOrientationInFlight = /* @__PURE__ */ new Set();
603365
604347
  lastFeedbackAt = /* @__PURE__ */ new Map();
604348
+ lastClipAt = /* @__PURE__ */ new Map();
604349
+ nextVideoSourceIndex = 0;
604350
+ liveSpeechSink = null;
604351
+ liveSpeechEnabled = false;
604352
+ lastLiveSpeechAt = 0;
604353
+ lastLiveSpeechByKey = /* @__PURE__ */ new Map();
604354
+ liveSpeechInFlight = /* @__PURE__ */ new Set();
603366
604355
  intakeAgentConfig;
603367
604356
  setStatusSink(sink) {
603368
604357
  this.statusSink = sink ?? null;
@@ -603374,6 +604363,12 @@ var init_live_sensors = __esm({
603374
604363
  setAgentActionSink(sink) {
603375
604364
  this.agentActionSink = sink ?? null;
603376
604365
  }
604366
+ setLiveSpeechSink(sink) {
604367
+ this.liveSpeechSink = sink ?? null;
604368
+ }
604369
+ setLiveSpeechEnabled(enabled2) {
604370
+ this.liveSpeechEnabled = enabled2;
604371
+ }
603377
604372
  setAudioIntakeAgentConfig(config) {
603378
604373
  this.intakeAgentConfig = config && config.backendUrl && config.model ? { backendUrl: config.backendUrl, model: config.model, apiKey: config.apiKey } : void 0;
603379
604374
  }
@@ -603469,16 +604464,14 @@ var init_live_sensors = __esm({
603469
604464
  if (!framePath) return `Camera orientation detection captured ${target}, but no frame path was returned.`;
603470
604465
  const cvScores = await scoreCameraOrientationWithOpenCv(framePath);
603471
604466
  const cvDecision = cvScores ? chooseCameraOrientationFromScores(cvScores) : null;
603472
- if (cvDecision && cvDecision.confidence >= 0.68) {
603473
- const orientation2 = this.setCameraRotation(target, cvDecision.rotation, "auto", cvDecision.reason, cvDecision.confidence);
603474
- return orientation2 ? `Auto orientation for ${target}: ${describeCameraOrientation(orientation2)}` : `Auto orientation for ${target} could not be persisted.`;
603475
- }
603476
604467
  const prompt = [
603477
604468
  "Determine whether this camera frame is upright for a human viewer.",
603478
604469
  "Return only JSON with this schema:",
603479
604470
  '{"rotation_degrees":0|90|180|270,"confidence":0.0-1.0,"reason":"brief visual evidence"}',
603480
604471
  "rotation_degrees is the clockwise correction to apply to future frames so they are upright.",
603481
- "Use 90 if the image must rotate clockwise; use 270 if it must rotate counter-clockwise; use 180 if upside down; use 0 if already upright or inconclusive."
604472
+ "Use 90 if the image must rotate clockwise; use 270 if it must rotate counter-clockwise; use 180 if upside down; use 0 if already upright or inconclusive.",
604473
+ "If the visible scene is sideways, choose the correction direction that makes people, faces, screens, and text upright; if the opposite direction would make them sideways, do not choose it.",
604474
+ cvDecision ? `Computer-vision candidate: ${formatCameraRotation(cvDecision.rotation)} confidence=${cvDecision.confidence.toFixed(2)} evidence=${cvDecision.reason}` : "Computer-vision candidate: unavailable."
603482
604475
  ].join("\n");
603483
604476
  const vision = await new VisionTool(this.repoRoot).execute({
603484
604477
  action: "query",
@@ -603501,9 +604494,10 @@ var init_live_sensors = __esm({
603501
604494
  }
603502
604495
  return `Camera orientation detection for ${target} was inconclusive. Vision output did not contain a structured rotation decision.`;
603503
604496
  }
603504
- const useCv = cvDecision && (cvDecision.confidence > (decision2.confidence ?? 0) + 0.12 || cvDecision.rotation !== decision2.rotation && cvDecision.confidence >= 0.62 && (decision2.confidence ?? 0) < 0.76);
603505
- const selected = useCv ? cvDecision : decision2;
603506
- const evidence = useCv ? cvDecision.reason : `${decision2.reason || boundedText(vision.llmContent || vision.output, 320)}${cvDecision ? `; cv=${cvDecision.reason}` : ""}`;
604497
+ const visionConfidence = decision2.confidence ?? 0;
604498
+ const useCv = Boolean(cvDecision) && (visionConfidence < 0.55 || cvDecision.rotation === decision2.rotation && cvDecision.confidence > visionConfidence + 0.18);
604499
+ const selected = useCv && cvDecision ? cvDecision : decision2;
604500
+ const evidence = useCv ? cvDecision?.reason ?? "" : `${decision2.reason || boundedText(vision.llmContent || vision.output, 320)}${cvDecision ? `; cv=${cvDecision.reason}` : ""}`;
603507
604501
  const orientation = this.setCameraRotation(target, selected.rotation, "auto", evidence, selected.confidence);
603508
604502
  return orientation ? `Auto orientation for ${target}: ${describeCameraOrientation(orientation)}` : `Auto orientation for ${target} could not be persisted.`;
603509
604503
  } finally {
@@ -603532,7 +604526,10 @@ var init_live_sensors = __esm({
603532
604526
  errors
603533
604527
  };
603534
604528
  if (!this.config.selectedCamera) this.config.selectedCamera = firstDeviceId(video);
603535
- if (!this.config.selectedAudioInput) this.config.selectedAudioInput = firstDeviceId(inputs.devices);
604529
+ const preferredInput = preferredLiveAudioInputId(inputs.devices, this.config.selectedAudioInput);
604530
+ if (!this.config.selectedAudioInput || isAudioOutputMonitorId(this.config.selectedAudioInput) && preferredInput && !isAudioOutputMonitorId(preferredInput)) {
604531
+ this.config.selectedAudioInput = preferredInput;
604532
+ }
603536
604533
  if (!this.config.selectedAudioOutput) this.config.selectedAudioOutput = firstDeviceId(outputs.devices);
603537
604534
  this.persist();
603538
604535
  return this.devices;
@@ -603541,16 +604538,18 @@ var init_live_sensors = __esm({
603541
604538
  this.config = {
603542
604539
  ...this.config,
603543
604540
  ...patch,
603544
- videoIntervalMs: Math.max(1500, Number(patch.videoIntervalMs ?? this.config.videoIntervalMs)),
603545
- audioIntervalMs: Math.max(3e3, Number(patch.audioIntervalMs ?? this.config.audioIntervalMs))
604541
+ videoIntervalMs: Math.max(MIN_VIDEO_INTERVAL_MS, Number(patch.videoIntervalMs ?? this.config.videoIntervalMs)),
604542
+ audioIntervalMs: Math.max(MIN_AUDIO_INTERVAL_MS, Number(patch.audioIntervalMs ?? this.config.audioIntervalMs))
603546
604543
  };
603547
604544
  this.persist();
604545
+ this.clearLoopTimers();
603548
604546
  this.ensureLoops();
603549
604547
  this.emitStatus();
603550
604548
  return this.getConfig();
603551
604549
  }
603552
604550
  stopAll() {
603553
604551
  this.agentActionEnabled = false;
604552
+ this.liveSpeechEnabled = false;
603554
604553
  this.configure({
603555
604554
  videoEnabled: false,
603556
604555
  audioEnabled: false,
@@ -603563,6 +604562,7 @@ var init_live_sensors = __esm({
603563
604562
  }
603564
604563
  startRunMode(options2 = {}) {
603565
604564
  this.agentActionEnabled = Boolean(options2.agent);
604565
+ this.liveSpeechEnabled = Boolean(options2.speech ?? options2.agent);
603566
604566
  this.configure({
603567
604567
  videoEnabled: true,
603568
604568
  inferEnabled: true,
@@ -603570,8 +604570,8 @@ var init_live_sensors = __esm({
603570
604570
  audioEnabled: true,
603571
604571
  asrEnabled: true,
603572
604572
  audioAnalysisEnabled: true,
603573
- videoIntervalMs: options2.lowLatency === false ? this.config.videoIntervalMs : Math.min(this.config.videoIntervalMs, 1500),
603574
- audioIntervalMs: options2.lowLatency === false ? this.config.audioIntervalMs : Math.min(this.config.audioIntervalMs, 3e3)
604573
+ videoIntervalMs: options2.lowLatency === false ? this.config.videoIntervalMs : Math.min(this.config.videoIntervalMs, LOW_LATENCY_VIDEO_INTERVAL_MS),
604574
+ audioIntervalMs: options2.lowLatency === false ? this.config.audioIntervalMs : Math.min(this.config.audioIntervalMs, LOW_LATENCY_AUDIO_INTERVAL_MS)
603575
604575
  });
603576
604576
  for (const source of this.activeVideoSources()) {
603577
604577
  const current = this.getCameraOrientation(source);
@@ -603594,7 +604594,7 @@ var init_live_sensors = __esm({
603594
604594
  }, 6e4);
603595
604595
  });
603596
604596
  }
603597
- void this.sampleVideoNow(true).catch((err) => {
604597
+ void this.sampleVideoNow(true, { mode: "all" }).catch((err) => {
603598
604598
  this.emitFeedback({
603599
604599
  kind: "error",
603600
604600
  title: "Live video sample failed",
@@ -603684,6 +604684,10 @@ var init_live_sensors = __esm({
603684
604684
  ascii: preview?.ascii,
603685
604685
  plainAscii: preview?.plainAscii,
603686
604686
  renderer: preview?.renderer,
604687
+ previewWidth: preview?.width,
604688
+ previewHeight: preview?.height,
604689
+ frameWidth: preview?.imageWidth,
604690
+ frameHeight: preview?.imageHeight,
603687
604691
  asciiContext,
603688
604692
  rotation,
603689
604693
  orientation
@@ -603694,7 +604698,8 @@ var init_live_sensors = __esm({
603694
604698
  [source]: {
603695
604699
  ...cameraView,
603696
604700
  summary: this.snapshot.cameras?.[source]?.summary,
603697
- inference: this.snapshot.cameras?.[source]?.inference
604701
+ inference: this.snapshot.cameras?.[source]?.inference,
604702
+ classifications: this.snapshot.cameras?.[source]?.classifications
603698
604703
  }
603699
604704
  };
603700
604705
  this.snapshot.events = mergeRecentById(this.snapshot.events, [{
@@ -603727,7 +604732,11 @@ var init_live_sensors = __esm({
603727
604732
  displayPath,
603728
604733
  ascii: preview?.ascii,
603729
604734
  plainAscii: preview?.plainAscii,
603730
- renderer: preview?.renderer
604735
+ renderer: preview?.renderer,
604736
+ frameWidth: preview?.imageWidth,
604737
+ frameHeight: preview?.imageHeight,
604738
+ previewWidth: preview?.width,
604739
+ previewHeight: preview?.height
603731
604740
  };
603732
604741
  }
603733
604742
  async recognizeFrameObjects(framePath, source, observedAt = Date.now(), options2 = {}) {
@@ -603780,24 +604789,33 @@ var init_live_sensors = __esm({
603780
604789
  const sampledAt = Date.now();
603781
604790
  if (forceInfer && source) {
603782
604791
  const loop = new LiveMediaLoopTool(this.repoRoot);
604792
+ const inferFromCapturedFrame = preview.ok && Boolean(preview.framePath);
603783
604793
  const result = await loop.execute({
603784
604794
  action: "watch",
603785
- source_kind: "camera",
603786
- camera: source,
603787
- duration_sec: 2.5,
603788
- sample_interval_sec: 1,
603789
- max_frames: 2,
603790
- rotate_degrees: orientation?.rotation ?? 0,
604795
+ source_kind: inferFromCapturedFrame ? "file" : "camera",
604796
+ ...inferFromCapturedFrame ? { path: preview.framePath } : { camera: source },
604797
+ yolo_family: "yoloe-26",
604798
+ yolo_scale: "n",
604799
+ yoloe_prompt_mode: "prompt_free",
604800
+ duration_sec: inferFromCapturedFrame ? 0.1 : 0.75,
604801
+ sample_interval_sec: inferFromCapturedFrame ? 0.1 : 0.25,
604802
+ max_frames: 1,
604803
+ rotate_degrees: inferFromCapturedFrame ? 0 : orientation?.rotation ?? 0,
603791
604804
  auto_bootstrap: true,
603792
604805
  audio_mode: "none",
603793
604806
  detect_objects: true,
603794
604807
  track_objects: true,
604808
+ segment_objects: true,
604809
+ extract_segments: true,
604810
+ max_segments_per_frame: 8,
603795
604811
  crop_faces: true,
603796
604812
  recognize_faces: true
603797
604813
  });
603798
604814
  inference = result.success ? String(result.output || result.llmContent || "") : `Video inference failed: ${result.error || result.output || "unknown error"}`;
603799
604815
  const livePayload = result.success ? parseLiveMediaPayload(result.llmContent || result.output) : null;
604816
+ if (livePayload) livePayload.source = source;
603800
604817
  const observations = observationsFromLiveMediaPayload(livePayload, sampledAt, source);
604818
+ const classifications = classificationsFromLiveMediaPayload(livePayload, sampledAt, source);
603801
604819
  this.snapshot.events = mergeRecentById(this.snapshot.events, observations.events, 50);
603802
604820
  this.snapshot.people = mergeRecentById(this.snapshot.people, observations.people, 24);
603803
604821
  const existing = this.snapshot.cameras?.[source] ?? this.snapshot.video ?? { updatedAt: Date.now(), source };
@@ -603807,6 +604825,7 @@ var init_live_sensors = __esm({
603807
604825
  source,
603808
604826
  inference,
603809
604827
  summary: summarizeInference(inference),
604828
+ classifications: mergeCameraClassifications(existing.classifications, classifications),
603810
604829
  rotation: orientation?.rotation ?? 0,
603811
604830
  orientation,
603812
604831
  ...preview.ok ? { error: void 0 } : { error: preview.message }
@@ -603831,6 +604850,16 @@ var init_live_sensors = __esm({
603831
604850
  `${unknownPeople.length} unknown individual(s) observed on live camera ${source}`,
603832
604851
  unknownPeople.map((entry) => `${entry.cropPath || entry.trackId || entry.source} ${entry.evidence || ""}`).join("; ")
603833
604852
  );
604853
+ this.maybeSpeakLive({
604854
+ key: `unknown-person:${source}`,
604855
+ kind: "unknown_person",
604856
+ source,
604857
+ summary: `${unknownPeople.length} unknown individual(s) observed on live camera ${source}`,
604858
+ evidence: unknownPeople.map((entry) => `${entry.cropPath || entry.trackId || entry.source} ${entry.evidence || ""}`).join("; "),
604859
+ defaultText: "I see someone here, but I do not know who. Who is there?",
604860
+ urgency: "high",
604861
+ observedAt: sampledAt
604862
+ }, 9e4);
603834
604863
  }
603835
604864
  const triggerEvents = observations.events.filter((entry) => entry.kind === "visual_trigger");
603836
604865
  if (triggerEvents.length > 0) {
@@ -603838,13 +604867,29 @@ var init_live_sensors = __esm({
603838
604867
  `Visual trigger hit in live camera stream ${source}`,
603839
604868
  triggerEvents.map((entry) => entry.summary).join("; ")
603840
604869
  );
604870
+ this.maybeSpeakLive({
604871
+ key: `visual-trigger:${source}:${triggerEvents[0]?.title ?? "trigger"}`,
604872
+ kind: "visual_trigger",
604873
+ source,
604874
+ summary: `Visual trigger hit in live camera stream ${source}`,
604875
+ evidence: triggerEvents.map((entry) => entry.summary).join("; "),
604876
+ defaultText: "I noticed something important in view. Should I inspect it?",
604877
+ urgency: "high",
604878
+ observedAt: sampledAt
604879
+ }, 45e3);
603841
604880
  }
603842
604881
  }
603843
- if (this.config.clipEnabled && preview.ok && preview.framePath && source) {
604882
+ const lastClip = this.lastClipAt.get(source) ?? 0;
604883
+ if (this.config.clipEnabled && preview.ok && preview.framePath && source && sampledAt - lastClip >= 5e3) {
604884
+ this.lastClipAt.set(source, sampledAt);
603844
604885
  clipSummary = await this.recognizeFrameObjects(preview.framePath, source, sampledAt, { emit: false });
603845
604886
  const current = this.snapshot.cameras?.[source];
603846
604887
  if (current) {
603847
604888
  current.summary = [current.summary, clipSummary ? `clip: ${trimOneLine(clipSummary, 160)}` : ""].filter(Boolean).join(" | ");
604889
+ current.classifications = mergeCameraClassifications(
604890
+ current.classifications,
604891
+ classificationsFromClipSummary(clipSummary, sampledAt, preview.framePath)
604892
+ );
603848
604893
  this.snapshot.cameras = { ...this.snapshot.cameras ?? {}, [source]: current };
603849
604894
  if (this.snapshot.video?.source === source) this.snapshot.video = current;
603850
604895
  this.persist();
@@ -603855,14 +604900,16 @@ ${inference}` : "", clipSummary ? `
603855
604900
  CLIP visual memory:
603856
604901
  ${clipSummary}` : ""].filter(Boolean).join("\n");
603857
604902
  }
603858
- async sampleVideoNow(forceInfer = this.config.inferEnabled) {
604903
+ async sampleVideoNow(forceInfer = this.config.inferEnabled, options2 = {}) {
603859
604904
  if (this.videoInFlight) return "Video sampler is already running.";
603860
604905
  this.videoInFlight = true;
603861
604906
  try {
603862
- const sources = this.activeVideoSources();
604907
+ const allSources = this.activeVideoSources();
604908
+ if (allSources.length === 0) return "No camera selected. Use /live refresh after connecting hardware.";
604909
+ const sources = options2.mode === "round-robin" ? [allSources[this.nextVideoSourceIndex++ % allSources.length]] : allSources;
603863
604910
  if (sources.length === 0) return "No camera selected. Use /live refresh after connecting hardware.";
603864
604911
  const outputs = [];
603865
- const previewWidth = liveDashboardPreviewWidthForSources(sources.length);
604912
+ const previewWidth = liveDashboardPreviewWidthForSources(allSources.length);
603866
604913
  for (const source of sources) {
603867
604914
  outputs.push(await this.sampleSingleVideoNow(source, forceInfer, previewWidth));
603868
604915
  }
@@ -603877,7 +604924,9 @@ ${output}`).join("\n\n");
603877
604924
  if (this.audioInFlight) return "Audio sampler is already running.";
603878
604925
  this.audioInFlight = true;
603879
604926
  try {
603880
- const input = this.config.selectedAudioInput || firstDeviceId(this.devices.audioInputs) || "default";
604927
+ const preferredInput = preferredLiveAudioInputId(this.devices.audioInputs, this.config.selectedAudioInput) || "default";
604928
+ const input = this.config.selectedAudioInput && !isAudioOutputMonitorId(this.config.selectedAudioInput) ? this.config.selectedAudioInput : preferredInput;
604929
+ if (input !== this.config.selectedAudioInput) this.config.selectedAudioInput = input;
603881
604930
  const outputDir2 = liveDir(this.repoRoot);
603882
604931
  ensureLiveDir(this.repoRoot);
603883
604932
  const recordingPath = join124(outputDir2, `audio-${Date.now()}.wav`);
@@ -603908,7 +604957,10 @@ ${output}`).join("\n\n");
603908
604957
  if (capture.device !== this.config.selectedAudioInput) {
603909
604958
  this.config.selectedAudioInput = capture.device;
603910
604959
  }
603911
- const activity = readPcm16WavActivity(recordingPath);
604960
+ const activity = capture.activity ?? readPcm16WavActivity(recordingPath);
604961
+ if (capture.ok && capture.signalDetected === false) {
604962
+ audioErrors.push(`No live input signal detected after probing ${capture.attempts?.length ?? 1} audio source(s); using quiet capture from ${capture.device}`);
604963
+ }
603912
604964
  if (this.config.asrEnabled) {
603913
604965
  try {
603914
604966
  const asr = await new TranscribeFileTool(this.repoRoot).execute({ path: recordingPath, model: "tiny" });
@@ -603985,6 +605037,16 @@ ${output}`).join("\n\n");
603985
605037
  "Live audio intake classified speech as intended for Omnius",
603986
605038
  `transcript=${transcript}; confidence=${intake.confidence.toFixed(2)}; reason=${intake.reason}`
603987
605039
  );
605040
+ this.maybeSpeakLive({
605041
+ key: `audio-intent:${capture.device}:${transcript.slice(0, 48)}`,
605042
+ kind: "audio_intent",
605043
+ source: capture.device,
605044
+ summary: "Live audio intake classified speech as intended for Omnius",
605045
+ evidence: `transcript=${transcript}; confidence=${intake.confidence.toFixed(2)}; reason=${intake.reason}`,
605046
+ defaultText: "I heard you. I am checking the live context now.",
605047
+ urgency: "high",
605048
+ observedAt: this.snapshot.audio.updatedAt
605049
+ }, 2e4);
603988
605050
  }
603989
605051
  this.emitDashboard();
603990
605052
  return [
@@ -604002,30 +605064,63 @@ ${analysis}` : ""
604002
605064
  this.audioInFlight = false;
604003
605065
  }
604004
605066
  }
605067
+ clearLoopTimers() {
605068
+ if (this.videoTimer) {
605069
+ clearTimeout(this.videoTimer);
605070
+ this.videoTimer = null;
605071
+ }
605072
+ if (this.audioTimer) {
605073
+ clearTimeout(this.audioTimer);
605074
+ this.audioTimer = null;
605075
+ }
605076
+ }
605077
+ scheduleVideoLoop(delayMs) {
605078
+ if (!this.config.videoEnabled || this.videoTimer) return;
605079
+ this.videoTimer = setTimeout(async () => {
605080
+ this.videoTimer = null;
605081
+ if (!this.config.videoEnabled) return;
605082
+ try {
605083
+ await this.sampleVideoNow(this.config.inferEnabled || this.config.clipEnabled, { mode: "round-robin" });
605084
+ } catch {
605085
+ } finally {
605086
+ if (this.config.videoEnabled) {
605087
+ this.scheduleVideoLoop(this.config.videoIntervalMs);
605088
+ }
605089
+ }
605090
+ }, Math.max(0, delayMs));
605091
+ this.videoTimer.unref?.();
605092
+ }
605093
+ scheduleAudioLoop(delayMs) {
605094
+ const wantsAudio = this.config.audioEnabled && (this.config.asrEnabled || this.config.audioAnalysisEnabled);
605095
+ if (!wantsAudio || this.audioTimer) return;
605096
+ this.audioTimer = setTimeout(async () => {
605097
+ this.audioTimer = null;
605098
+ const stillWantsAudio = this.config.audioEnabled && (this.config.asrEnabled || this.config.audioAnalysisEnabled);
605099
+ if (!stillWantsAudio) return;
605100
+ try {
605101
+ await this.sampleAudioNow();
605102
+ } catch {
605103
+ } finally {
605104
+ const wantsNext = this.config.audioEnabled && (this.config.asrEnabled || this.config.audioAnalysisEnabled);
605105
+ if (wantsNext) {
605106
+ this.scheduleAudioLoop(this.config.audioIntervalMs);
605107
+ }
605108
+ }
605109
+ }, Math.max(0, delayMs));
605110
+ this.audioTimer.unref?.();
605111
+ }
604005
605112
  ensureLoops() {
604006
605113
  if (this.config.videoEnabled && !this.videoTimer) {
604007
- void this.sampleVideoNow(this.config.inferEnabled || this.config.clipEnabled).catch(() => {
604008
- });
604009
- this.videoTimer = setInterval(() => {
604010
- void this.sampleVideoNow(this.config.inferEnabled || this.config.clipEnabled).catch(() => {
604011
- });
604012
- }, this.config.videoIntervalMs);
604013
- this.videoTimer.unref?.();
605114
+ this.scheduleVideoLoop(0);
604014
605115
  } else if (!this.config.videoEnabled && this.videoTimer) {
604015
- clearInterval(this.videoTimer);
605116
+ clearTimeout(this.videoTimer);
604016
605117
  this.videoTimer = null;
604017
605118
  }
604018
605119
  const wantsAudio = this.config.audioEnabled && (this.config.asrEnabled || this.config.audioAnalysisEnabled);
604019
605120
  if (wantsAudio && !this.audioTimer) {
604020
- void this.sampleAudioNow().catch(() => {
604021
- });
604022
- this.audioTimer = setInterval(() => {
604023
- void this.sampleAudioNow().catch(() => {
604024
- });
604025
- }, this.config.audioIntervalMs);
604026
- this.audioTimer.unref?.();
605121
+ this.scheduleAudioLoop(0);
604027
605122
  } else if (!wantsAudio && this.audioTimer) {
604028
- clearInterval(this.audioTimer);
605123
+ clearTimeout(this.audioTimer);
604029
605124
  this.audioTimer = null;
604030
605125
  }
604031
605126
  }
@@ -604060,7 +605155,7 @@ ${analysis}` : ""
604060
605155
  maybeRequestAgentReview(reason, evidence) {
604061
605156
  if (!this.agentActionEnabled || !this.agentActionSink) return;
604062
605157
  const now2 = Date.now();
604063
- if (now2 - this.lastAgentReviewAt < 2e4) return;
605158
+ if (now2 - this.lastAgentReviewAt < 6e4) return;
604064
605159
  this.lastAgentReviewAt = now2;
604065
605160
  const prompt = [
604066
605161
  "Live environment PFC review.",
@@ -604083,6 +605178,38 @@ ${analysis}` : ""
604083
605178
  } catch {
604084
605179
  }
604085
605180
  }
605181
+ maybeSpeakLive(proposal, perKeyCooldownMs) {
605182
+ if (!this.liveSpeechEnabled || !this.liveSpeechSink) return;
605183
+ const now2 = Date.now();
605184
+ if (now2 - this.lastLiveSpeechAt < 12e3) return;
605185
+ const lastForKey = this.lastLiveSpeechByKey.get(proposal.key) ?? 0;
605186
+ if (now2 - lastForKey < perKeyCooldownMs) return;
605187
+ if (this.liveSpeechInFlight.has(proposal.key)) return;
605188
+ this.liveSpeechInFlight.add(proposal.key);
605189
+ void classifyLiveSpeech(proposal, this.intakeAgentConfig).then((decision2) => {
605190
+ this.lastLiveSpeechByKey.set(proposal.key, Date.now());
605191
+ if (!decision2.speak || !decision2.text.trim()) return;
605192
+ this.lastLiveSpeechAt = Date.now();
605193
+ const text2 = trimOneLine(decision2.text, 180);
605194
+ this.liveSpeechSink?.(text2, decision2);
605195
+ this.emitFeedbackThrottled(`speech:${proposal.key}`, {
605196
+ kind: "speech",
605197
+ title: "Live speech emitted",
605198
+ observedAt: decision2.updatedAt,
605199
+ message: `${text2}
605200
+ reason=${decision2.reason} confidence=${decision2.confidence.toFixed(2)} source=${decision2.source}`
605201
+ }, 15e3);
605202
+ }).catch((err) => {
605203
+ this.emitFeedbackThrottled(`speech-error:${proposal.key}`, {
605204
+ kind: "error",
605205
+ title: "Live speech arbiter failed",
605206
+ observedAt: Date.now(),
605207
+ message: err instanceof Error ? err.message : String(err)
605208
+ }, 6e4);
605209
+ }).finally(() => {
605210
+ this.liveSpeechInFlight.delete(proposal.key);
605211
+ });
605212
+ }
604086
605213
  persist() {
604087
605214
  ensureLiveDir(this.repoRoot);
604088
605215
  writeFileSync56(liveConfigPath(this.repoRoot), JSON.stringify(this.config, null, 2), "utf8");
@@ -638440,6 +639567,10 @@ async function showPruneMenu(options2) {
638440
639567
  });
638441
639568
  if (!result.confirmed || !result.key || result.key === "back") return;
638442
639569
  const start2 = (label, o2) => {
639570
+ if (isLiveSensorActiveForRepo(workingDir)) {
639571
+ renderWarning("Live audio/video mode is active; pruning is deferred so sensor, ASR, vision, and reasoning inference stay available. Stop live mode before running prune.");
639572
+ return;
639573
+ }
638443
639574
  renderInfo(`${label} (running in background — summary will follow)…`);
638444
639575
  runPruneWorkerOnce({
638445
639576
  repoRoot: workingDir,
@@ -638475,6 +639606,7 @@ var init_prune_menu = __esm({
638475
639606
  init_render();
638476
639607
  init_kg_prune();
638477
639608
  init_memory_maintenance();
639609
+ init_live_sensors();
638478
639610
  import_chalk2 = __toESM(require_source(), 1);
638479
639611
  }
638480
639612
  });
@@ -647474,6 +648606,7 @@ async function handleSlashCommand(input, ctx3) {
647474
648606
  case "prune": {
647475
648607
  const sub2 = (arg?.trim().split(/\s+/)[0] ?? "").toLowerCase();
647476
648608
  const workingDir = ctx3.repoRoot || process.cwd();
648609
+ const force = /\b--force\b/i.test(arg ?? "");
647477
648610
  const targetMatch = arg?.match(/--target\s+(\d+)/);
647478
648611
  const targetNodes = targetMatch ? parseInt(targetMatch[1], 10) : void 0;
647479
648612
  const { knowledgeGraphStats: knowledgeGraphStats2, formatKgStatsLine: formatKgStatsLine2 } = await Promise.resolve().then(() => (init_kg_prune(), kg_prune_exports));
@@ -647482,6 +648615,10 @@ async function handleSlashCommand(input, ctx3) {
647482
648615
  renderInfo(summary);
647483
648616
  };
647484
648617
  const startBackground = (label, o2) => {
648618
+ if (isLiveSensorActiveForRepo(workingDir) && !force) {
648619
+ renderWarning("Live audio/video mode is active; pruning is deferred so sensor, ASR, vision, and reasoning inference stay available. Stop live mode first, or rerun with --force if you intentionally want pruning to compete with live inference.");
648620
+ return;
648621
+ }
647485
648622
  renderInfo(`${label} (running in background — summary will follow)…`);
647486
648623
  runPruneWorkerOnce2({
647487
648624
  repoRoot: workingDir,
@@ -655805,6 +656942,12 @@ function attachLiveSinks(ctx3, manager, agentEnabled = false) {
655805
656942
  model: ctx3.config.model,
655806
656943
  apiKey: ctx3.config.apiKey
655807
656944
  });
656945
+ const speechEnabled = agentEnabled && Boolean(ctx3.voiceSpeak);
656946
+ manager.setLiveSpeechEnabled(speechEnabled);
656947
+ manager.setLiveSpeechSink(speechEnabled ? (text2) => {
656948
+ if (ctx3.voiceIsEnabled && !ctx3.voiceIsEnabled()) return;
656949
+ ctx3.voiceSpeak?.(text2);
656950
+ } : void 0);
655808
656951
  if (agentEnabled && ctx3.startBackgroundPrompt) {
655809
656952
  manager.setAgentActionSink((prompt) => {
655810
656953
  const id2 = ctx3.startBackgroundPrompt?.(prompt);
@@ -655818,13 +656961,14 @@ async function startLiveVoicechat(ctx3, manager) {
655818
656961
  const agentEnabled = Boolean(ctx3.startBackgroundPrompt);
655819
656962
  attachLiveSinks(ctx3, manager, agentEnabled);
655820
656963
  await ensureLiveInventory(manager);
655821
- renderInfo(manager.startRunMode({ agent: agentEnabled, lowLatency: true }));
655822
656964
  if (!ctx3.voiceChatStart) {
656965
+ renderInfo(manager.startRunMode({ agent: agentEnabled, lowLatency: true, speech: false }));
655823
656966
  renderWarning("Voice chat is not available in this context; live audio/video feedback remains active.");
655824
656967
  return;
655825
656968
  }
655826
656969
  ctx3.voiceSetMode?.("voicechat");
655827
656970
  if (ctx3.isVoiceChatActive?.()) {
656971
+ renderInfo(manager.startRunMode({ agent: agentEnabled, lowLatency: true, speech: Boolean(ctx3.voiceSpeak) }));
655828
656972
  renderInfo("Voice chat is already active. Live audio/video feedback remains attached.");
655829
656973
  return;
655830
656974
  }
@@ -655835,6 +656979,7 @@ async function startLiveVoicechat(ctx3, manager) {
655835
656979
  try {
655836
656980
  renderInfo("Starting live voicechat with audio/video context...");
655837
656981
  await ctx3.voiceChatStart();
656982
+ renderInfo(manager.startRunMode({ agent: agentEnabled, lowLatency: true, speech: Boolean(ctx3.voiceSpeak) }));
655838
656983
  renderInfo("Live voicechat active: low-latency audio/video context is updating while voice conversation continues.");
655839
656984
  } catch (err) {
655840
656985
  renderError(`Live voicechat failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -655895,7 +657040,7 @@ async function handleLiveCommand(ctx3, arg) {
655895
657040
  renderWarning("Live PFC agent review is unavailable in this context; starting visual/audio feedback only.");
655896
657041
  }
655897
657042
  await ensureLiveInventory(manager);
655898
- renderInfo(manager.startRunMode({ agent: effectiveAgent, lowLatency: true }));
657043
+ renderInfo(manager.startRunMode({ agent: effectiveAgent, lowLatency: true, speech: effectiveAgent && Boolean(ctx3.voiceSpeak) }));
655899
657044
  return;
655900
657045
  }
655901
657046
  if (sub2 === "chat" || sub2 === "voicechat" || sub2 === "voice") {
@@ -656150,14 +657295,14 @@ async function showLiveMenu(ctx3, manager) {
656150
657295
  continue;
656151
657296
  case "run-live":
656152
657297
  attachLiveSinks(ctx3, manager, false);
656153
- renderInfo(manager.startRunMode({ agent: false, lowLatency: true }));
657298
+ renderInfo(manager.startRunMode({ agent: false, lowLatency: true, speech: false }));
656154
657299
  continue;
656155
657300
  case "run-agent":
656156
657301
  attachLiveSinks(ctx3, manager, true);
656157
657302
  if (!ctx3.startBackgroundPrompt) {
656158
657303
  renderWarning("Live PFC agent review is unavailable in this context; starting visual/audio feedback only.");
656159
657304
  }
656160
- renderInfo(manager.startRunMode({ agent: Boolean(ctx3.startBackgroundPrompt), lowLatency: true }));
657305
+ renderInfo(manager.startRunMode({ agent: Boolean(ctx3.startBackgroundPrompt), lowLatency: true, speech: Boolean(ctx3.startBackgroundPrompt && ctx3.voiceSpeak) }));
656161
657306
  continue;
656162
657307
  case "run-chat":
656163
657308
  await startLiveVoicechat(ctx3, manager);
@@ -731385,9 +732530,10 @@ Rationale: ${proposal.rationale}${provenanceNote}${dmnDevDiscipline(proposal.cat
731385
732530
  }, delayMs);
731386
732531
  }
731387
732532
  let activeTask = null;
732533
+ const liveSensorsBusy = () => isLiveSensorActiveForRepo(repoRoot);
731388
732534
  idleMemoryMaintenance = startIdleMemoryMaintenance({
731389
732535
  repoRoot,
731390
- isBusy: () => Boolean(activeTask) || _interactiveSessionActive,
732536
+ isBusy: () => Boolean(activeTask) || _interactiveSessionActive || liveSensorsBusy(),
731391
732537
  onSummary: (summary) => {
731392
732538
  if (!summary) return;
731393
732539
  writeContent(() => renderInfo(summary));
@@ -733941,6 +735087,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
733941
735087
  },
733942
735088
  setLiveMediaStatus(status) {
733943
735089
  statusBar.setLiveMediaStatus(status);
735090
+ if (status.audio || status.video) idleMemoryMaintenance?.notifyActivity();
733944
735091
  },
733945
735092
  liveDynamicBlockHost: {
733946
735093
  registerDynamicBlock: (id2, render2) => statusBar.registerDynamicBlock(id2, render2),