omnius 1.0.434 → 1.0.435

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
@@ -9286,6 +9286,74 @@ function uniqueStrings(values) {
9286
9286
  }
9287
9287
  return out;
9288
9288
  }
9289
+ function optionalPositiveInteger(value2) {
9290
+ if (value2 === void 0 || value2 === null || value2 === "")
9291
+ return void 0;
9292
+ const parsed = typeof value2 === "number" ? value2 : Number(String(value2).trim());
9293
+ if (!Number.isFinite(parsed) || parsed <= 0)
9294
+ return void 0;
9295
+ return Math.round(parsed);
9296
+ }
9297
+ function normalizeCameraPresetFps(value2) {
9298
+ const parsed = optionalPositiveInteger(typeof value2 === "string" ? value2.replace(/\s*fps$/i, "") : value2);
9299
+ return parsed === void 0 ? void 0 : Math.max(1, Math.min(30, parsed));
9300
+ }
9301
+ function parseCameraPresetResolution(value2) {
9302
+ const raw = String(value2 ?? "").trim().toLowerCase();
9303
+ if (!raw || raw === "native" || raw === "auto")
9304
+ return null;
9305
+ const alias = {
9306
+ "480": "640x480",
9307
+ "480p": "640x480",
9308
+ "720": "1280x720",
9309
+ "720p": "1280x720",
9310
+ "1080": "1920x1080",
9311
+ "1080p": "1920x1080",
9312
+ "1440": "2560x1440",
9313
+ "1440p": "2560x1440",
9314
+ "2k": "2560x1440",
9315
+ "2160": "3840x2160",
9316
+ "2160p": "3840x2160",
9317
+ "4k": "3840x2160",
9318
+ "uhd": "3840x2160"
9319
+ };
9320
+ const normalized = alias[raw] ?? raw;
9321
+ const match = normalized.match(/^(\d{3,5})x(\d{3,5})$/);
9322
+ if (!match)
9323
+ return null;
9324
+ const width = Number(match[1]);
9325
+ const height = Number(match[2]);
9326
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0)
9327
+ return null;
9328
+ return { width, height, label: `${width}x${height}` };
9329
+ }
9330
+ function loadLiveCameraPreset(requestedDevice) {
9331
+ const repoRoot = process.env["OMNIUS_REPO_ROOT"] || process.cwd();
9332
+ const configPath2 = join13(repoRoot, ".omnius", "live", "config.json");
9333
+ try {
9334
+ const parsed = JSON.parse(readFileSync13(configPath2, "utf8"));
9335
+ const streams = parsed["cameraStreams"] && typeof parsed["cameraStreams"] === "object" ? parsed["cameraStreams"] : {};
9336
+ const orientations = parsed["cameraOrientation"] && typeof parsed["cameraOrientation"] === "object" ? parsed["cameraOrientation"] : {};
9337
+ const selected = typeof parsed["selectedCamera"] === "string" ? parsed["selectedCamera"] : void 0;
9338
+ const device = requestedDevice || selected;
9339
+ if (!device)
9340
+ return {};
9341
+ const stream = streams[device] ?? {};
9342
+ const orientation = orientations[device] ?? {};
9343
+ const resolution = parseCameraPresetResolution(stream["resolution"]);
9344
+ const rotation = orientation["rotation"] !== void 0 ? normalizeCameraRotationDegrees(orientation["rotation"]) : void 0;
9345
+ return {
9346
+ device,
9347
+ width: resolution?.width,
9348
+ height: resolution?.height,
9349
+ resolution: resolution?.label ?? String(stream["resolution"] ?? "native"),
9350
+ rotation,
9351
+ fps: normalizeCameraPresetFps(stream["fps"] ?? stream["framerate"])
9352
+ };
9353
+ } catch {
9354
+ return {};
9355
+ }
9356
+ }
9289
9357
  function buildV4l2CaptureAttempts(width, height, formats) {
9290
9358
  const requested = width > 0 && height > 0 ? `${width}x${height}` : void 0;
9291
9359
  const sizeArea = (size) => {
@@ -9465,6 +9533,14 @@ var init_camera_capture = __esm({
9465
9533
  type: "number",
9466
9534
  description: "Capture height in pixels (default: camera native format)"
9467
9535
  },
9536
+ fps: {
9537
+ type: "number",
9538
+ description: "Requested capture frame rate for v4l2 devices. Defaults to the shared /live or /camera preset when available."
9539
+ },
9540
+ framerate: {
9541
+ type: "number",
9542
+ description: "Alias for fps."
9543
+ },
9468
9544
  rotate_degrees: {
9469
9545
  type: "number",
9470
9546
  enum: [0, 90, 180, 270],
@@ -9564,20 +9640,33 @@ ${formatRouterPlan(plan)}`,
9564
9640
  // Capture frame — auto-selects v4l2 or QooCam
9565
9641
  // =========================================================================
9566
9642
  async captureFrame(args, start2) {
9567
- const device = args["device"] || "";
9643
+ const requestedDevice = args["device"] || "";
9644
+ const preset = loadLiveCameraPreset(requestedDevice);
9645
+ const device = requestedDevice || preset.device || "";
9646
+ const effectiveArgs = { ...args, device };
9647
+ if (effectiveArgs["width"] === void 0 && preset.width !== void 0)
9648
+ effectiveArgs["width"] = preset.width;
9649
+ if (effectiveArgs["height"] === void 0 && preset.height !== void 0)
9650
+ effectiveArgs["height"] = preset.height;
9651
+ if (effectiveArgs["rotate_degrees"] === void 0 && effectiveArgs["rotation_degrees"] === void 0 && effectiveArgs["rotate"] === void 0 && preset.rotation !== void 0) {
9652
+ effectiveArgs["rotate_degrees"] = preset.rotation;
9653
+ }
9654
+ if (effectiveArgs["fps"] === void 0 && effectiveArgs["framerate"] === void 0 && preset.fps !== void 0) {
9655
+ effectiveArgs["framerate"] = preset.fps;
9656
+ }
9568
9657
  const outputPath3 = args["output_path"];
9569
9658
  if (device === "qoocam" || device.startsWith("qoocam")) {
9570
- return this.captureQooCam(args, start2);
9659
+ return this.captureQooCam(effectiveArgs, start2);
9571
9660
  }
9572
9661
  const v4l2Device = await this.resolveV4l2Device(device);
9573
9662
  if (!v4l2Device) {
9574
9663
  const qoocam = await this.discoverQooCam();
9575
9664
  if (qoocam.found) {
9576
- return this.captureQooCam(args, start2);
9665
+ return this.captureQooCam(effectiveArgs, start2);
9577
9666
  }
9578
9667
  return { success: false, output: "", error: "No camera found. Connect a USB camera or enable QooCam WiFi hotspot.", durationMs: performance.now() - start2 };
9579
9668
  }
9580
- return this.captureV4l2(v4l2Device, args, start2);
9669
+ return this.captureV4l2(v4l2Device, effectiveArgs, start2);
9581
9670
  }
9582
9671
  // =========================================================================
9583
9672
  // Camera info
@@ -9687,10 +9776,11 @@ ${formatRouterPlan(plan)}`,
9687
9776
  return rawDevice;
9688
9777
  }
9689
9778
  async captureV4l2(device, args, start2) {
9690
- const width = args["width"] || 0;
9691
- const height = args["height"] || 0;
9779
+ const width = optionalPositiveInteger(args["width"]) ?? 0;
9780
+ const height = optionalPositiveInteger(args["height"]) ?? 0;
9692
9781
  const outputPath3 = args["output_path"];
9693
9782
  const rotation = normalizeCameraRotationDegrees(args["rotate_degrees"] ?? args["rotation_degrees"] ?? args["rotate"]);
9783
+ const fps = normalizeCameraPresetFps(args["framerate"] ?? args["fps"]);
9694
9784
  const captureDir = join13(tmpdir2(), "omnius-camera");
9695
9785
  if (!existsSync15(captureDir))
9696
9786
  mkdirSync9(captureDir, { recursive: true });
@@ -9722,7 +9812,7 @@ ${formatRouterPlan(plan)}`,
9722
9812
  unlinkSync2(tempFile);
9723
9813
  } catch {
9724
9814
  }
9725
- const argv = this.ffmpegCaptureArgs(device, tempFile, attempt);
9815
+ const argv = this.ffmpegCaptureArgs(device, tempFile, attempt, fps);
9726
9816
  try {
9727
9817
  await runProcessBuffer(ffmpegBin(), argv, { timeout: 15e3 });
9728
9818
  if (existsSync15(tempFile) && readFileSync13(tempFile).length > 0) {
@@ -9765,8 +9855,10 @@ ${formatRouterPlan(plan)}`,
9765
9855
  Recovered with capture profile: ${captureState.successfulAttempt?.label ?? "unknown"}. Last failed attempt: ${errors[errors.length - 1].slice(0, 260)}` : "";
9766
9856
  const rotationNote = rotation !== 0 ? `
9767
9857
  Applied camera rotation correction: ${rotation} degrees clockwise.` : "";
9858
+ const fpsNote = fps ? `
9859
+ Applied camera capture FPS preset: ${fps} fps.` : "";
9768
9860
  const result = this.returnCapturedFile(tempFile, captureState.successfulAttempt?.videoSize ?? (width && height ? `${width}x${height}` : "native"), device, outputPath3, start2);
9769
- const notes = `${firstSuccess}${rotationNote}`;
9861
+ const notes = `${firstSuccess}${rotationNote}${fpsNote}`;
9770
9862
  return result.success && notes ? { ...result, output: result.output + notes, llmContent: result.llmContent ? result.llmContent + notes : result.llmContent } : result;
9771
9863
  }
9772
9864
  const allErrors = errors.join("\n\n");
@@ -9841,8 +9933,10 @@ ${allErrors.slice(0, 900)}`, durationMs: performance.now() - start2 };
9841
9933
  return [];
9842
9934
  }
9843
9935
  }
9844
- ffmpegCaptureArgs(device, outputPath3, attempt) {
9936
+ ffmpegCaptureArgs(device, outputPath3, attempt, fps) {
9845
9937
  const argv = ["-hide_banner", "-loglevel", "error", "-f", "v4l2", "-thread_queue_size", "4"];
9938
+ if (fps)
9939
+ argv.push("-framerate", String(fps));
9846
9940
  if (attempt.inputFormat)
9847
9941
  argv.push("-input_format", attempt.inputFormat);
9848
9942
  if (attempt.videoSize)
@@ -604669,8 +604763,8 @@ transcribe-cli error: ${transcribeCliError}` : "";
604669
604763
  import { spawn as spawn30 } from "node:child_process";
604670
604764
  import { existsSync as existsSync112, mkdirSync as mkdirSync69, statSync as statSync43, unlinkSync as unlinkSync20 } from "node:fs";
604671
604765
  import { join as join127 } from "node:path";
604672
- function streamFps() {
604673
- const raw = Number(process.env["OMNIUS_LIVE_STREAM_FPS"] ?? "");
604766
+ function streamFps(configured) {
604767
+ const raw = configured ?? Number(process.env["OMNIUS_LIVE_STREAM_FPS"] ?? "");
604674
604768
  if (Number.isFinite(raw) && raw > 0) return Math.max(1, Math.min(30, Math.round(raw)));
604675
604769
  return 8;
604676
604770
  }
@@ -604751,7 +604845,7 @@ var init_camera_streamer = __esm({
604751
604845
  * Only v4l2 device paths are streamable; other backends (OSC/WiFi cams)
604752
604846
  * keep the one-shot capture path.
604753
604847
  */
604754
- sync(sources, rotationFor, resolutionFor) {
604848
+ sync(sources, rotationFor, resolutionFor, fpsFor) {
604755
604849
  const wanted = new Set(sources.filter((source) => source.startsWith("/dev/video")));
604756
604850
  for (const [source, stream] of this.streams) {
604757
604851
  if (!wanted.has(source)) {
@@ -604762,20 +604856,21 @@ var init_camera_streamer = __esm({
604762
604856
  for (const source of wanted) {
604763
604857
  const rotation = rotationFor(source);
604764
604858
  const resolution = resolutionFor(source);
604859
+ const fps = streamFps(fpsFor(source));
604765
604860
  const existing = this.streams.get(source);
604766
- if (existing && existing.rotation === rotation && existing.resolution === resolution && !existing.stopped) continue;
604861
+ if (existing && existing.rotation === rotation && existing.resolution === resolution && existing.fps === fps && !existing.stopped) continue;
604767
604862
  if (existing) {
604768
604863
  this.stopStream(existing);
604769
604864
  this.streams.delete(source);
604770
604865
  }
604771
- this.startStream(source, rotation, resolution);
604866
+ this.startStream(source, rotation, resolution, fps);
604772
604867
  }
604773
604868
  }
604774
604869
  stopAll() {
604775
604870
  for (const stream of this.streams.values()) this.stopStream(stream);
604776
604871
  this.streams.clear();
604777
604872
  }
604778
- startStream(source, rotation, resolution) {
604873
+ startStream(source, rotation, resolution, fps) {
604779
604874
  try {
604780
604875
  mkdirSync69(this.streamDir, { recursive: true });
604781
604876
  } catch {
@@ -604785,6 +604880,7 @@ var init_camera_streamer = __esm({
604785
604880
  framePath: join127(this.streamDir, `${slugForSource(source)}.jpg`),
604786
604881
  rotation,
604787
604882
  resolution,
604883
+ fps,
604788
604884
  process: null,
604789
604885
  attempt: 0,
604790
604886
  consecutiveFailures: 0,
@@ -604798,7 +604894,7 @@ var init_camera_streamer = __esm({
604798
604894
  }
604799
604895
  spawnStream(stream) {
604800
604896
  if (stream.stopped) return;
604801
- const fps = streamFps();
604897
+ const fps = streamFps(stream.fps);
604802
604898
  const resolution = parseResolution(stream.resolution);
604803
604899
  const filters = [`fps=${fps}`];
604804
604900
  const scale = scaleFilter(resolution);
@@ -605482,6 +605578,17 @@ function formatLiveCameraResolution(value2) {
605482
605578
  if (res === "3840x2160") return "4K (3840x2160)";
605483
605579
  return res;
605484
605580
  }
605581
+ function normalizeLiveCameraFps(value2) {
605582
+ if (value2 === void 0 || value2 === null || value2 === "") return DEFAULT_CAMERA_FPS;
605583
+ const parsed = typeof value2 === "number" ? value2 : Number(String(value2).trim().toLowerCase().replace(/\s*fps$/, ""));
605584
+ if (!Number.isFinite(parsed) || parsed <= 0) {
605585
+ throw new Error(`Unsupported live camera FPS: ${String(value2)}. Use a number from 1 to 30.`);
605586
+ }
605587
+ return Math.max(1, Math.min(30, Math.round(parsed)));
605588
+ }
605589
+ function formatLiveCameraFps(value2) {
605590
+ return `${normalizeLiveCameraFps(value2)} fps`;
605591
+ }
605485
605592
  function liveCameraResolutionSize(value2) {
605486
605593
  const res = value2 ?? DEFAULT_CAMERA_RESOLUTION;
605487
605594
  if (res === "native") return null;
@@ -605499,10 +605606,11 @@ function sanitizeCameraStreams(value2) {
605499
605606
  try {
605500
605607
  out[device] = {
605501
605608
  enabled: entry["enabled"] !== false,
605502
- resolution: normalizeLiveCameraResolution(entry["resolution"] ?? DEFAULT_CAMERA_RESOLUTION)
605609
+ resolution: normalizeLiveCameraResolution(entry["resolution"] ?? DEFAULT_CAMERA_RESOLUTION),
605610
+ fps: normalizeLiveCameraFps(entry["fps"] ?? entry["framerate"] ?? DEFAULT_CAMERA_FPS)
605503
605611
  };
605504
605612
  } catch {
605505
- out[device] = { enabled: entry["enabled"] !== false, resolution: DEFAULT_CAMERA_RESOLUTION };
605613
+ out[device] = { enabled: entry["enabled"] !== false, resolution: DEFAULT_CAMERA_RESOLUTION, fps: DEFAULT_CAMERA_FPS };
605506
605614
  }
605507
605615
  }
605508
605616
  return out;
@@ -606641,7 +606749,7 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
606641
606749
  const orientation = snapshot.config.cameraOrientation?.[snapshot.config.selectedCamera];
606642
606750
  const stream = snapshot.config.cameraStreams?.[snapshot.config.selectedCamera];
606643
606751
  lines.push(`Selected camera: ${snapshot.config.selectedCamera}`);
606644
- lines.push(`Selected camera stream: ${stream?.enabled !== false ? "enabled" : "disabled"} ${formatLiveCameraResolution(stream?.resolution)}`);
606752
+ lines.push(`Selected camera stream: ${stream?.enabled !== false ? "enabled" : "disabled"} ${formatLiveCameraResolution(stream?.resolution)} ${formatLiveCameraFps(stream?.fps)}`);
606645
606753
  lines.push(`Camera orientation correction: ${describeCameraOrientation(orientation)}`);
606646
606754
  }
606647
606755
  const cameraSnapshots = Object.values(snapshot.cameras ?? (snapshot.video ? { [snapshot.video.source]: snapshot.video } : {})).sort((a2, b) => {
@@ -607098,7 +607206,7 @@ function formatLiveStatus(snapshot) {
607098
607206
  const lines = [
607099
607207
  `Live streams: video=${cfg.videoEnabled ? "on" : "off"} infer=${cfg.inferEnabled ? "on" : "off"} clip=${cfg.clipEnabled ? "on" : "off"} audio=${cfg.audioEnabled ? "on" : "off"} asr=${cfg.asrEnabled ? "on" : "off"} sounds=${cfg.audioAnalysisEnabled ? "on" : "off"} output=${cfg.audioOutputEnabled ? "on" : "off"}`,
607100
607208
  `Camera: ${cfg.selectedCamera || "none"}`,
607101
- `Camera stream: ${cfg.selectedCamera ? `${cfg.cameraStreams?.[cfg.selectedCamera]?.enabled !== false ? "enabled" : "disabled"} ${formatLiveCameraResolution(cfg.cameraStreams?.[cfg.selectedCamera]?.resolution)}` : "none"}`,
607209
+ `Camera stream: ${cfg.selectedCamera ? `${cfg.cameraStreams?.[cfg.selectedCamera]?.enabled !== false ? "enabled" : "disabled"} ${formatLiveCameraResolution(cfg.cameraStreams?.[cfg.selectedCamera]?.resolution)} ${formatLiveCameraFps(cfg.cameraStreams?.[cfg.selectedCamera]?.fps)}` : "none"}`,
607102
607210
  `Camera orientation: ${cfg.selectedCamera ? describeCameraOrientation(cfg.cameraOrientation?.[cfg.selectedCamera]) : "none"}`,
607103
607211
  `Audio input: ${cfg.selectedAudioInput || "none"}`,
607104
607212
  `Audio output: ${cfg.selectedAudioOutput || "none"}`,
@@ -607112,7 +607220,7 @@ function formatLiveStatus(snapshot) {
607112
607220
  }
607113
607221
  return lines.join("\n");
607114
607222
  }
607115
- var MIN_VIDEO_INTERVAL_MS, LOW_LATENCY_VIDEO_INTERVAL_MS, MIN_AUDIO_INTERVAL_MS, LOW_LATENCY_AUDIO_INTERVAL_MS, AUDIO_ACTIVITY_INTERVAL_MS, AUDIO_ACTIVITY_SAMPLE_SEC, DEFAULT_CAMERA_RESOLUTION, DEFAULT_CONFIG7, managers, DASHBOARD_ANSI_STICKY_PATTERN, _liveDashboardMaximizedCamera, LiveSensorManager;
607223
+ var MIN_VIDEO_INTERVAL_MS, LOW_LATENCY_VIDEO_INTERVAL_MS, MIN_AUDIO_INTERVAL_MS, LOW_LATENCY_AUDIO_INTERVAL_MS, AUDIO_ACTIVITY_INTERVAL_MS, AUDIO_ACTIVITY_SAMPLE_SEC, DEFAULT_CAMERA_RESOLUTION, DEFAULT_CAMERA_FPS, DEFAULT_CONFIG7, managers, DASHBOARD_ANSI_STICKY_PATTERN, _liveDashboardMaximizedCamera, LiveSensorManager;
607116
607224
  var init_live_sensors = __esm({
607117
607225
  "packages/cli/src/tui/live-sensors.ts"() {
607118
607226
  "use strict";
@@ -607130,6 +607238,7 @@ var init_live_sensors = __esm({
607130
607238
  AUDIO_ACTIVITY_INTERVAL_MS = 200;
607131
607239
  AUDIO_ACTIVITY_SAMPLE_SEC = 0.2;
607132
607240
  DEFAULT_CAMERA_RESOLUTION = "1920x1080";
607241
+ DEFAULT_CAMERA_FPS = 8;
607133
607242
  DEFAULT_CONFIG7 = {
607134
607243
  videoEnabled: false,
607135
607244
  audioEnabled: false,
@@ -607339,7 +607448,8 @@ var init_live_sensors = __esm({
607339
607448
  ...this.config.cameraStreams ?? {},
607340
607449
  [fallback]: {
607341
607450
  enabled: true,
607342
- resolution: this.config.cameraStreams?.[fallback]?.resolution ?? DEFAULT_CAMERA_RESOLUTION
607451
+ resolution: this.config.cameraStreams?.[fallback]?.resolution ?? DEFAULT_CAMERA_RESOLUTION,
607452
+ fps: this.config.cameraStreams?.[fallback]?.fps ?? DEFAULT_CAMERA_FPS
607343
607453
  }
607344
607454
  }
607345
607455
  };
@@ -607360,13 +607470,18 @@ var init_live_sensors = __esm({
607360
607470
  const target = device || this.config.selectedCamera;
607361
607471
  return target ? this.config.cameraStreams?.[target]?.resolution ?? DEFAULT_CAMERA_RESOLUTION : DEFAULT_CAMERA_RESOLUTION;
607362
607472
  }
607473
+ getCameraStreamFps(device = this.config.selectedCamera) {
607474
+ const target = device || this.config.selectedCamera;
607475
+ return target ? normalizeLiveCameraFps(this.config.cameraStreams?.[target]?.fps ?? DEFAULT_CAMERA_FPS) : DEFAULT_CAMERA_FPS;
607476
+ }
607363
607477
  setCameraStreamEnabled(device, enabled2) {
607364
607478
  const target = device || this.config.selectedCamera;
607365
607479
  if (!target) return null;
607366
607480
  const current = this.config.cameraStreams?.[target];
607367
607481
  const next = {
607368
607482
  enabled: enabled2,
607369
- resolution: current?.resolution ?? DEFAULT_CAMERA_RESOLUTION
607483
+ resolution: current?.resolution ?? DEFAULT_CAMERA_RESOLUTION,
607484
+ fps: current?.fps ?? DEFAULT_CAMERA_FPS
607370
607485
  };
607371
607486
  this.config = {
607372
607487
  ...this.config,
@@ -607388,7 +607503,30 @@ var init_live_sensors = __esm({
607388
607503
  const current = this.config.cameraStreams?.[target];
607389
607504
  const next = {
607390
607505
  enabled: current?.enabled ?? target === this.config.selectedCamera,
607391
- resolution: normalizeLiveCameraResolution(resolution)
607506
+ resolution: normalizeLiveCameraResolution(resolution),
607507
+ fps: current?.fps ?? DEFAULT_CAMERA_FPS
607508
+ };
607509
+ this.config = {
607510
+ ...this.config,
607511
+ cameraStreams: {
607512
+ ...this.config.cameraStreams ?? {},
607513
+ [target]: next
607514
+ }
607515
+ };
607516
+ this.videoGeneration++;
607517
+ this.lastStreamFrameMtime.delete(target);
607518
+ this.persist();
607519
+ this.ensureLoops();
607520
+ return next;
607521
+ }
607522
+ setCameraStreamFps(device, fps) {
607523
+ const target = device || this.config.selectedCamera;
607524
+ if (!target) return null;
607525
+ const current = this.config.cameraStreams?.[target];
607526
+ const next = {
607527
+ enabled: current?.enabled ?? target === this.config.selectedCamera,
607528
+ resolution: current?.resolution ?? DEFAULT_CAMERA_RESOLUTION,
607529
+ fps: normalizeLiveCameraFps(fps)
607392
607530
  };
607393
607531
  this.config = {
607394
607532
  ...this.config,
@@ -607642,7 +607780,8 @@ var init_live_sensors = __esm({
607642
607780
  if (!device.id || /metadata\/non-capture/i.test(device.detail ?? "")) continue;
607643
607781
  next[device.id] = {
607644
607782
  enabled: current[device.id]?.enabled ?? device.id === selected,
607645
- resolution: current[device.id]?.resolution ?? DEFAULT_CAMERA_RESOLUTION
607783
+ resolution: current[device.id]?.resolution ?? DEFAULT_CAMERA_RESOLUTION,
607784
+ fps: current[device.id]?.fps ?? DEFAULT_CAMERA_FPS
607646
607785
  };
607647
607786
  }
607648
607787
  return next;
@@ -607662,7 +607801,8 @@ var init_live_sensors = __esm({
607662
607801
  ...this.config.cameraStreams ?? {},
607663
607802
  [selected]: {
607664
607803
  enabled: true,
607665
- resolution: this.config.cameraStreams?.[selected]?.resolution ?? DEFAULT_CAMERA_RESOLUTION
607804
+ resolution: this.config.cameraStreams?.[selected]?.resolution ?? DEFAULT_CAMERA_RESOLUTION,
607805
+ fps: this.config.cameraStreams?.[selected]?.fps ?? DEFAULT_CAMERA_FPS
607666
607806
  }
607667
607807
  };
607668
607808
  }
@@ -607882,7 +608022,8 @@ var init_live_sensors = __esm({
607882
608022
  action: "capture",
607883
608023
  device: source,
607884
608024
  rotate_degrees: rotation,
607885
- ...captureSize ? { width: captureSize.width, height: captureSize.height } : {}
608025
+ ...captureSize ? { width: captureSize.width, height: captureSize.height } : {},
608026
+ framerate: this.getCameraStreamFps(source)
607886
608027
  });
607887
608028
  if (!this.isVideoWorkCurrent(generation)) return { ok: false, message: "Video context is disabled." };
607888
608029
  if (!result.success) {
@@ -608502,7 +608643,8 @@ ${analysis}` : ""
608502
608643
  this.cameraStreamers.sync(
608503
608644
  this.activeVideoSources(),
608504
608645
  (source) => this.getCameraOrientation(source)?.rotation ?? 0,
608505
- (source) => this.getCameraStreamResolution(source)
608646
+ (source) => this.getCameraStreamResolution(source),
608647
+ (source) => this.getCameraStreamFps(source)
608506
608648
  );
608507
608649
  }
608508
608650
  previewTickIntervalMs() {
@@ -613305,12 +613447,16 @@ var init_command_registry = __esm({
613305
613447
  ["/livechat", "Alias for /live chat, used by the top live button"],
613306
613448
  ["/live camera <device>", "Select a camera and render an ASCII preview"],
613307
613449
  ["/live resolution [camera] 720p|1080p|1440p|4k|native", "Set per-camera live stream resolution (default 1080p)"],
613450
+ ["/live fps [camera] 1|2|4|8|12|15|24|30", "Set per-camera live stream FPS (default 8)"],
613308
613451
  ["/live camera-on|camera-off [camera]", "Enable or disable an individual live camera stream"],
613309
613452
  ["/live rotate auto|cw|ccw|180|none [camera]", "Detect or set persistent camera orientation correction"],
613310
613453
  ["/live infer [now|on|off]", "Toggle or run camera object/location inference"],
613311
613454
  ["/live clip [on|off]", "Toggle CLIP visual-memory recognition on live frames"],
613312
613455
  ["/live audio|asr|sounds on|off", "Toggle live audio context streams"],
613313
613456
  ["/live stop", "Disable all live sensor streams"],
613457
+ ["/camera", "Open compact camera preset menu shared with /live"],
613458
+ ["/camera status", "Show selected camera and per-device presets"],
613459
+ ["/camera select|preview|resolution|fps|rotate", "Manage camera source, preview, resolution, FPS, and orientation"],
613314
613460
  ["/paste", "Attach clipboard image content to the current or next prompt"],
613315
613461
  ["/image", "Open image-generation model/setup menu"],
613316
613462
  [
@@ -653830,6 +653976,10 @@ sleep 1
653830
653976
  await handleLiveCommand(ctx3, arg);
653831
653977
  return "handled";
653832
653978
  }
653979
+ case "camera": {
653980
+ await handleCameraCommand(ctx3, arg);
653981
+ return "handled";
653982
+ }
653833
653983
  case "livechat": {
653834
653984
  await handleLiveCommand(ctx3, "chat");
653835
653985
  return "handled";
@@ -659588,7 +659738,7 @@ async function chooseLiveDevice(ctx3, title, devices, activeKey) {
659588
659738
  return devices.find((device) => device.id === result.key) ?? null;
659589
659739
  }
659590
659740
  function liveCameraStreamSummary(manager, deviceId) {
659591
- return `${manager.isCameraStreamEnabled(deviceId) ? liveEnabled(true) : liveEnabled(false)} · ${formatLiveCameraResolution(manager.getCameraStreamResolution(deviceId))} · ${formatCameraRotation(manager.getCameraRotation(deviceId))}`;
659741
+ return `${manager.isCameraStreamEnabled(deviceId) ? liveEnabled(true) : liveEnabled(false)} · ${formatLiveCameraResolution(manager.getCameraStreamResolution(deviceId))} · ${formatLiveCameraFps(manager.getCameraStreamFps(deviceId))} · ${formatCameraRotation(manager.getCameraRotation(deviceId))}`;
659592
659742
  }
659593
659743
  async function chooseLiveCameraResolution(ctx3, manager, deviceId) {
659594
659744
  const current = manager.getCameraStreamResolution(deviceId);
@@ -659607,11 +659757,29 @@ async function chooseLiveCameraResolution(ctx3, manager, deviceId) {
659607
659757
  if (!result.confirmed || !result.key) return null;
659608
659758
  return normalizeLiveCameraResolution(result.key);
659609
659759
  }
659760
+ async function chooseLiveCameraFps(ctx3, manager, deviceId) {
659761
+ const current = manager.getCameraStreamFps(deviceId);
659762
+ const choices = [1, 2, 4, 8, 12, 15, 24, 30];
659763
+ const result = await tuiSelect({
659764
+ items: choices.map((fps) => ({
659765
+ key: String(fps),
659766
+ label: formatLiveCameraFps(fps),
659767
+ detail: fps <= 4 ? "lowest hardware burden" : fps === 8 ? "default balanced live stream rate" : "smoother preview, higher camera/CPU burden"
659768
+ })),
659769
+ title: `Camera FPS — ${deviceId}`,
659770
+ activeKey: String(current),
659771
+ rl: ctx3.rl,
659772
+ availableRows: ctx3.availableContentRows?.()
659773
+ });
659774
+ if (!result.confirmed || !result.key) return null;
659775
+ return normalizeLiveCameraFps(result.key);
659776
+ }
659610
659777
  async function showLiveCameraMenu(ctx3, manager, device) {
659611
659778
  while (true) {
659612
659779
  const selected = manager.getConfig().selectedCamera === device.id;
659613
659780
  const enabled2 = manager.isCameraStreamEnabled(device.id);
659614
659781
  const resolution = manager.getCameraStreamResolution(device.id);
659782
+ const fps = manager.getCameraStreamFps(device.id);
659615
659783
  const items = [
659616
659784
  { key: "hdr:camera", label: selectColors.dim(`─── Camera ${device.id} ───`) },
659617
659785
  {
@@ -659633,6 +659801,11 @@ async function showLiveCameraMenu(ctx3, manager, device) {
659633
659801
  label: "Set Stream Resolution",
659634
659802
  detail: formatLiveCameraResolution(resolution)
659635
659803
  },
659804
+ {
659805
+ key: "fps",
659806
+ label: "Set Stream FPS",
659807
+ detail: formatLiveCameraFps(fps)
659808
+ },
659636
659809
  {
659637
659810
  key: "preview",
659638
659811
  label: "Preview Camera",
@@ -659685,6 +659858,14 @@ async function showLiveCameraMenu(ctx3, manager, device) {
659685
659858
  }
659686
659859
  continue;
659687
659860
  }
659861
+ case "fps": {
659862
+ const next = await chooseLiveCameraFps(ctx3, manager, device.id);
659863
+ if (next) {
659864
+ manager.setCameraStreamFps(device.id, next);
659865
+ renderInfo(`Camera ${device.id} stream FPS set to ${formatLiveCameraFps(next)}.`);
659866
+ }
659867
+ continue;
659868
+ }
659688
659869
  case "preview":
659689
659870
  renderLivePreview(await manager.previewCamera(device.id));
659690
659871
  continue;
@@ -659808,6 +659989,28 @@ async function handleLiveCommand(ctx3, arg) {
659808
659989
  }
659809
659990
  return;
659810
659991
  }
659992
+ if (sub2 === "fps" || sub2 === "framerate" || sub2 === "rate") {
659993
+ await ensureLiveInventory(manager);
659994
+ const tokens = parts.slice(1);
659995
+ const rawFps = tokens.pop();
659996
+ if (!rawFps) {
659997
+ renderWarning("Usage: /live fps [camera] 1|2|4|8|12|15|24|30");
659998
+ return;
659999
+ }
660000
+ const device = tokens.join(" ") || manager.getConfig().selectedCamera;
660001
+ if (!device) {
660002
+ renderWarning("No camera selected. Use /live camera <device> first.");
660003
+ return;
660004
+ }
660005
+ try {
660006
+ const fps = normalizeLiveCameraFps(rawFps);
660007
+ manager.setCameraStreamFps(device, fps);
660008
+ renderInfo(`Camera ${device} stream FPS set to ${formatLiveCameraFps(fps)}.`);
660009
+ } catch (err) {
660010
+ renderWarning(err instanceof Error ? err.message : String(err));
660011
+ }
660012
+ return;
660013
+ }
659811
660014
  if (sub2 === "camera-on" || sub2 === "camera-off" || sub2 === "camera-enable" || sub2 === "camera-disable") {
659812
660015
  await ensureLiveInventory(manager);
659813
660016
  const device = value2 || manager.getConfig().selectedCamera;
@@ -659906,6 +660109,222 @@ async function handleLiveCommand(ctx3, arg) {
659906
660109
  }
659907
660110
  await showLiveMenu(ctx3, manager);
659908
660111
  }
660112
+ function formatCameraStatus(manager) {
660113
+ const snapshot = manager.getSnapshot();
660114
+ const cfg = manager.getConfig();
660115
+ const lines = [
660116
+ `Selected camera: ${cfg.selectedCamera || "none"}`,
660117
+ `Video context: ${cfg.videoEnabled ? "enabled" : "disabled"}`,
660118
+ `Camera devices: ${snapshot.devices.video.length}`
660119
+ ];
660120
+ for (const device of snapshot.devices.video) {
660121
+ lines.push(`- ${device.id}: ${liveCameraStreamSummary(manager, device.id)} — ${device.label}${device.detail ? ` — ${device.detail}` : ""}`);
660122
+ }
660123
+ return lines.join("\n");
660124
+ }
660125
+ async function showCameraMenu(ctx3, manager) {
660126
+ await ensureLiveInventory(manager);
660127
+ while (true) {
660128
+ const snapshot = manager.getSnapshot();
660129
+ const cfg = manager.getConfig();
660130
+ const devices = snapshot.devices.video;
660131
+ const items = [
660132
+ { key: "hdr:camera", label: selectColors.dim("─── Camera Presets ───") },
660133
+ {
660134
+ key: "info:camera",
660135
+ label: selectColors.dim(` selected ${cfg.selectedCamera || "none"} · video ${liveEnabled(cfg.videoEnabled)} · ${devices.length} device(s)`)
660136
+ },
660137
+ {
660138
+ key: "select",
660139
+ label: "Select Primary Camera",
660140
+ detail: liveDeviceSummary(devices, cfg.selectedCamera)
660141
+ },
660142
+ {
660143
+ key: "preview",
660144
+ label: "Preview Selected Camera",
660145
+ detail: "uses selected resolution, fps, and orientation preset"
660146
+ },
660147
+ {
660148
+ key: "toggle-video",
660149
+ label: cfg.videoEnabled ? "Disable Video Context" : "Enable Video Context",
660150
+ detail: "does not delete camera presets"
660151
+ },
660152
+ ...devices.map((device, index) => ({
660153
+ key: `camera-device:${index}`,
660154
+ label: `Camera: ${device.id}`,
660155
+ detail: `${liveCameraStreamSummary(manager, device.id)} — ${device.label}${device.detail ? ` — ${device.detail}` : ""}`
660156
+ })),
660157
+ { key: "refresh", label: "Refresh Devices", detail: "rescan /dev/video* and audio/video inventory" },
660158
+ { key: "status", label: "Show Camera Status", detail: "selected camera and per-device presets" }
660159
+ ];
660160
+ const result = await tuiSelect({
660161
+ items,
660162
+ title: "Camera Presets",
660163
+ activeKey: cfg.selectedCamera ? "preview" : "select",
660164
+ rl: ctx3.rl,
660165
+ availableRows: ctx3.availableContentRows?.(),
660166
+ skipKeys: liveSkipKeys(items),
660167
+ customKeyHint: " r refresh",
660168
+ onCustomKey(_item, key, helpers) {
660169
+ if (key === "r" || key === "R") {
660170
+ void manager.refreshDevices().then(() => helpers.done()).catch(() => helpers.done());
660171
+ return true;
660172
+ }
660173
+ return false;
660174
+ }
660175
+ });
660176
+ if (!result.confirmed || !result.key) return;
660177
+ if (result.key.startsWith("camera-device:")) {
660178
+ const index = Number(result.key.slice("camera-device:".length));
660179
+ const device = devices[index];
660180
+ if (device) await showLiveCameraMenu(ctx3, manager, device);
660181
+ continue;
660182
+ }
660183
+ switch (result.key) {
660184
+ case "select": {
660185
+ const device = await chooseLiveDevice(ctx3, "Select Camera", devices, cfg.selectedCamera);
660186
+ if (device) {
660187
+ manager.configure({ selectedCamera: device.id, videoEnabled: true });
660188
+ manager.setCameraStreamEnabled(device.id, true);
660189
+ renderInfo(`Selected camera ${device.id}.`);
660190
+ }
660191
+ continue;
660192
+ }
660193
+ case "preview":
660194
+ renderLivePreview(await manager.previewCamera(cfg.selectedCamera));
660195
+ continue;
660196
+ case "toggle-video":
660197
+ manager.configure({ videoEnabled: !cfg.videoEnabled });
660198
+ continue;
660199
+ case "refresh":
660200
+ renderInfo(formatLiveInventory(await manager.refreshDevices()));
660201
+ continue;
660202
+ case "status":
660203
+ renderInfo(formatCameraStatus(manager));
660204
+ continue;
660205
+ default:
660206
+ return;
660207
+ }
660208
+ }
660209
+ }
660210
+ async function handleCameraCommand(ctx3, arg) {
660211
+ const manager = getLiveSensorManager(ctx3.repoRoot);
660212
+ attachLiveSinks(ctx3, manager);
660213
+ const parts = arg.trim().split(/\s+/).filter(Boolean);
660214
+ const sub2 = (parts[0] || "").toLowerCase();
660215
+ const value2 = parts.slice(1).join(" ");
660216
+ if (sub2 === "status") {
660217
+ await ensureLiveInventory(manager);
660218
+ renderInfo(formatCameraStatus(manager));
660219
+ return;
660220
+ }
660221
+ if (sub2 === "refresh" || sub2 === "devices" || sub2 === "list") {
660222
+ renderInfo(formatLiveInventory(await manager.refreshDevices()));
660223
+ return;
660224
+ }
660225
+ if (sub2 === "preview" || sub2 === "view" || sub2 === "capture") {
660226
+ await ensureLiveInventory(manager);
660227
+ renderLivePreview(await manager.previewCamera(value2 || manager.getConfig().selectedCamera));
660228
+ return;
660229
+ }
660230
+ if (sub2 === "select" || sub2 === "use") {
660231
+ await ensureLiveInventory(manager);
660232
+ const device = value2;
660233
+ if (!device) {
660234
+ const selected = await chooseLiveDevice(ctx3, "Select Camera", manager.getSnapshot().devices.video, manager.getConfig().selectedCamera);
660235
+ if (selected) {
660236
+ manager.configure({ selectedCamera: selected.id, videoEnabled: true });
660237
+ manager.setCameraStreamEnabled(selected.id, true);
660238
+ renderInfo(`Selected camera ${selected.id}.`);
660239
+ }
660240
+ return;
660241
+ }
660242
+ manager.configure({ selectedCamera: device, videoEnabled: true });
660243
+ manager.setCameraStreamEnabled(device, true);
660244
+ renderInfo(`Selected camera ${device}.`);
660245
+ return;
660246
+ }
660247
+ if (sub2 === "on" || sub2 === "enable" || sub2 === "off" || sub2 === "disable") {
660248
+ await ensureLiveInventory(manager);
660249
+ const device = value2 || manager.getConfig().selectedCamera;
660250
+ if (!device) {
660251
+ renderWarning("No camera selected. Use /camera select <device> first.");
660252
+ return;
660253
+ }
660254
+ const enabled2 = sub2 === "on" || sub2 === "enable";
660255
+ manager.setCameraStreamEnabled(device, enabled2);
660256
+ renderInfo(`${enabled2 ? "Enabled" : "Disabled"} camera stream ${device}.`);
660257
+ return;
660258
+ }
660259
+ if (sub2 === "resolution" || sub2 === "res" || sub2 === "size") {
660260
+ await ensureLiveInventory(manager);
660261
+ const tokens = parts.slice(1);
660262
+ const rawResolution = tokens.pop();
660263
+ if (!rawResolution) {
660264
+ renderWarning("Usage: /camera resolution [camera] 720p|1080p|1440p|4k|native");
660265
+ return;
660266
+ }
660267
+ const device = tokens.join(" ") || manager.getConfig().selectedCamera;
660268
+ if (!device) {
660269
+ renderWarning("No camera selected. Use /camera select <device> first.");
660270
+ return;
660271
+ }
660272
+ try {
660273
+ const resolution = normalizeLiveCameraResolution(rawResolution);
660274
+ manager.setCameraStreamResolution(device, resolution);
660275
+ renderInfo(`Camera ${device} stream resolution set to ${formatLiveCameraResolution(resolution)}.`);
660276
+ } catch (err) {
660277
+ renderWarning(err instanceof Error ? err.message : String(err));
660278
+ }
660279
+ return;
660280
+ }
660281
+ if (sub2 === "fps" || sub2 === "framerate" || sub2 === "rate") {
660282
+ await ensureLiveInventory(manager);
660283
+ const tokens = parts.slice(1);
660284
+ const rawFps = tokens.pop();
660285
+ if (!rawFps) {
660286
+ renderWarning("Usage: /camera fps [camera] 1|2|4|8|12|15|24|30");
660287
+ return;
660288
+ }
660289
+ const device = tokens.join(" ") || manager.getConfig().selectedCamera;
660290
+ if (!device) {
660291
+ renderWarning("No camera selected. Use /camera select <device> first.");
660292
+ return;
660293
+ }
660294
+ try {
660295
+ const fps = normalizeLiveCameraFps(rawFps);
660296
+ manager.setCameraStreamFps(device, fps);
660297
+ renderInfo(`Camera ${device} stream FPS set to ${formatLiveCameraFps(fps)}.`);
660298
+ } catch (err) {
660299
+ renderWarning(err instanceof Error ? err.message : String(err));
660300
+ }
660301
+ return;
660302
+ }
660303
+ if (sub2 === "rotate" || sub2 === "rotation" || sub2 === "orient" || sub2 === "orientation") {
660304
+ await ensureLiveInventory(manager);
660305
+ const mode = (parts[1] || "cycle").toLowerCase();
660306
+ const deviceArg = parts.slice(2).join(" ") || manager.getConfig().selectedCamera;
660307
+ if (mode === "auto" || mode === "detect") {
660308
+ renderInfo("Detecting camera orientation...");
660309
+ renderInfo(await manager.autoOrientCamera(deviceArg));
660310
+ return;
660311
+ }
660312
+ if (mode === "cycle") {
660313
+ const orientation = manager.cycleCameraRotation(deviceArg);
660314
+ renderInfo(orientation ? `Camera rotation set to ${formatCameraRotation(orientation.rotation)} for ${deviceArg || manager.getConfig().selectedCamera || "selected camera"}.` : "No camera selected. Use /camera select <device> first.");
660315
+ return;
660316
+ }
660317
+ try {
660318
+ const rotation = normalizeLiveCameraRotation(mode);
660319
+ const orientation = manager.setCameraRotation(deviceArg, rotation, "manual", `/camera rotate ${mode}`);
660320
+ renderInfo(orientation ? `Camera rotation set to ${formatCameraRotation(orientation.rotation)} for ${deviceArg || manager.getConfig().selectedCamera || "selected camera"}.` : "No camera selected. Use /camera select <device> first.");
660321
+ } catch (err) {
660322
+ renderWarning(err instanceof Error ? err.message : String(err));
660323
+ }
660324
+ return;
660325
+ }
660326
+ await showCameraMenu(ctx3, manager);
660327
+ }
659909
660328
  async function showLiveMenu(ctx3, manager) {
659910
660329
  await ensureLiveInventory(manager);
659911
660330
  while (true) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.434",
3
+ "version": "1.0.435",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omnius",
9
- "version": "1.0.434",
9
+ "version": "1.0.435",
10
10
  "bundleDependencies": [
11
11
  "image-to-ascii"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.434",
3
+ "version": "1.0.435",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",