omnius 1.0.402 → 1.0.403

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
@@ -3675,6 +3675,69 @@ function ensureCommand(command) {
3675
3675
  _cache.set(command, result);
3676
3676
  return result;
3677
3677
  }
3678
+ function repairCommandPackage(command) {
3679
+ const dep = DESKTOP_DEPS.find((d2) => d2.command === command);
3680
+ if (!dep) {
3681
+ return {
3682
+ available: false,
3683
+ installed: false,
3684
+ error: `Unknown dependency: ${command}. Cannot repair package.`
3685
+ };
3686
+ }
3687
+ const pm = detectPackageManager();
3688
+ if (!pm) {
3689
+ return {
3690
+ available: hasCommand(command),
3691
+ installed: false,
3692
+ error: `No supported package manager detected — cannot repair ${command}.`
3693
+ };
3694
+ }
3695
+ const pkg = dep.packages[pm];
3696
+ if (!pkg) {
3697
+ return {
3698
+ available: hasCommand(command),
3699
+ installed: false,
3700
+ error: `No package mapping for ${command} on ${pm} — cannot repair package.`
3701
+ };
3702
+ }
3703
+ let installCmd;
3704
+ switch (pm) {
3705
+ case "apt":
3706
+ installCmd = `apt-get install -y ${pkg}`;
3707
+ break;
3708
+ case "dnf":
3709
+ installCmd = `dnf install -y ${pkg}`;
3710
+ break;
3711
+ case "pacman":
3712
+ installCmd = `pacman -S --noconfirm ${pkg}`;
3713
+ break;
3714
+ case "brew":
3715
+ installCmd = `brew install ${pkg}`;
3716
+ break;
3717
+ case "choco":
3718
+ installCmd = `choco install -y ${pkg}`;
3719
+ break;
3720
+ }
3721
+ try {
3722
+ runInstall(installCmd);
3723
+ } catch (err) {
3724
+ const msg = err instanceof Error ? err.message : String(err);
3725
+ const result = {
3726
+ available: hasCommand(command),
3727
+ installed: false,
3728
+ error: `Failed to repair ${pkg} via ${pm}: ${msg.slice(0, 200)}`
3729
+ };
3730
+ _cache.delete(command);
3731
+ return result;
3732
+ }
3733
+ _cache.delete(command);
3734
+ const available = hasCommand(command);
3735
+ return {
3736
+ available,
3737
+ installed: available,
3738
+ error: available ? void 0 : `Installed ${pkg} but ${command} still not found on PATH.`
3739
+ };
3740
+ }
3678
3741
  function ensureDepsForGroup(group) {
3679
3742
  const deps = DESKTOP_DEPS.filter((d2) => d2.group === group);
3680
3743
  const results = deps.map((d2) => ({ command: d2.command, result: ensureCommand(d2.command) }));
@@ -3781,6 +3844,18 @@ var init_system_deps = __esm({
3781
3844
  packages: { apt: "ghostscript", dnf: "ghostscript", pacman: "ghostscript", brew: "ghostscript", choco: "ghostscript" },
3782
3845
  description: "PostScript/PDF rendering (Ghostscript, required by ocrmypdf)",
3783
3846
  group: "pdf"
3847
+ },
3848
+ {
3849
+ command: "ffmpeg",
3850
+ packages: { apt: "ffmpeg libavcodec-extra", dnf: "ffmpeg", pacman: "ffmpeg", brew: "ffmpeg", choco: "ffmpeg" },
3851
+ description: "Camera/media capture and MJPEG decode/encode support",
3852
+ group: "camera"
3853
+ },
3854
+ {
3855
+ command: "v4l2-ctl",
3856
+ packages: { apt: "v4l-utils", dnf: "v4l-utils", pacman: "v4l-utils", brew: "v4l-utils" },
3857
+ description: "Linux Video4Linux device capability probing",
3858
+ group: "camera"
3784
3859
  }
3785
3860
  ];
3786
3861
  _cache = /* @__PURE__ */ new Map();
@@ -542806,6 +542881,103 @@ function sortedVideoDevicePaths() {
542806
542881
  return [];
542807
542882
  }
542808
542883
  }
542884
+ function ffmpegBin2() {
542885
+ return process.env["OMNIUS_FFMPEG"] || "ffmpeg";
542886
+ }
542887
+ function normalizePixelFormat(pixelFormat) {
542888
+ const fmt2 = pixelFormat.trim().toUpperCase();
542889
+ if (fmt2 === "MJPG" || fmt2 === "MJPEG")
542890
+ return "mjpeg";
542891
+ if (fmt2 === "YUYV" || fmt2 === "YUYV422")
542892
+ return "yuyv422";
542893
+ if (fmt2 === "UYVY" || fmt2 === "UYVY422")
542894
+ return "uyvy422";
542895
+ if (fmt2 === "NV12")
542896
+ return "nv12";
542897
+ if (fmt2 === "H264" || fmt2 === "H.264")
542898
+ return "h264";
542899
+ if (fmt2 === "HEVC" || fmt2 === "H265" || fmt2 === "H.265")
542900
+ return "hevc";
542901
+ return void 0;
542902
+ }
542903
+ function parseV4l2Formats(raw) {
542904
+ const formats = [];
542905
+ let current = null;
542906
+ for (const line of String(raw || "").split(/\r?\n/)) {
542907
+ const fmt2 = /(?:Pixel Format|^\s*\[\d+\])\s*:\s*'([^']+)'\s*(?:\(([^)]*)\))?/i.exec(line);
542908
+ if (fmt2) {
542909
+ current = {
542910
+ pixelFormat: fmt2[1].trim(),
542911
+ description: (fmt2[2] || "").trim(),
542912
+ ffmpegInputFormat: normalizePixelFormat(fmt2[1]),
542913
+ compressed: /compressed|mjpeg|h\.?264|hevc/i.test(fmt2[2] || fmt2[1] || ""),
542914
+ sizes: []
542915
+ };
542916
+ formats.push(current);
542917
+ continue;
542918
+ }
542919
+ const size = /Size:\s*Discrete\s+(\d+x\d+)/i.exec(line);
542920
+ if (size && current && !current.sizes.includes(size[1]))
542921
+ current.sizes.push(size[1]);
542922
+ }
542923
+ return formats;
542924
+ }
542925
+ function uniqueStrings(values) {
542926
+ const out = [];
542927
+ for (const value2 of values) {
542928
+ if (!value2 || out.includes(value2))
542929
+ continue;
542930
+ out.push(value2);
542931
+ }
542932
+ return out;
542933
+ }
542934
+ function buildV4l2CaptureAttempts(width, height, formats) {
542935
+ const requested = width > 0 && height > 0 ? `${width}x${height}` : void 0;
542936
+ const advertisedSizes = formats.flatMap((f2) => f2.sizes);
542937
+ const sizes = uniqueStrings([requested, ...advertisedSizes, ...DEFAULT_CAPTURE_SIZES]).slice(0, 10);
542938
+ const advertisedInputs = formats.map((f2) => f2.ffmpegInputFormat).filter((f2) => !!f2);
542939
+ const inputs = uniqueStrings([
542940
+ ...advertisedInputs.filter((f2) => f2 === "mjpeg"),
542941
+ ...advertisedInputs.filter((f2) => f2 !== "mjpeg"),
542942
+ "mjpeg",
542943
+ "yuyv422",
542944
+ void 0
542945
+ ]);
542946
+ const attempts = [];
542947
+ for (const size of sizes) {
542948
+ for (const input of inputs) {
542949
+ if (input === "mjpeg") {
542950
+ attempts.push({ label: `mjpeg packet copy ${size}`, inputFormat: input, videoSize: size, copyPacket: true });
542951
+ }
542952
+ attempts.push({
542953
+ label: `${input || "auto"} ${size}`,
542954
+ inputFormat: input,
542955
+ videoSize: size,
542956
+ copyPacket: false
542957
+ });
542958
+ }
542959
+ }
542960
+ attempts.push({ label: "auto native", copyPacket: false });
542961
+ const seen = /* @__PURE__ */ new Set();
542962
+ return attempts.filter((attempt) => {
542963
+ const key = `${attempt.inputFormat || ""}|${attempt.videoSize || ""}|${attempt.copyPacket ? "copy" : "encode"}`;
542964
+ if (seen.has(key))
542965
+ return false;
542966
+ seen.add(key);
542967
+ return true;
542968
+ });
542969
+ }
542970
+ function cameraFfmpegErrorNeedsPackageRepair(message2) {
542971
+ return /mjpeg|codec|decoder|encoder|unknown input format|not support|not supported|invalid pixel format|video4linux2|v4l2/i.test(message2);
542972
+ }
542973
+ function processErrorDetail(err) {
542974
+ if (err instanceof AsyncProcessError) {
542975
+ const stderr = err.stderr.toString("utf8").trim();
542976
+ const stdout = err.stdout.toString("utf8").trim();
542977
+ return [err.message, stderr, stdout].filter(Boolean).join("\n").slice(0, 1200);
542978
+ }
542979
+ return (err instanceof Error ? err.message : String(err)).slice(0, 1200);
542980
+ }
542809
542981
  function cameraProfileFor(rawProfile, expectedCameras) {
542810
542982
  const key = String(rawProfile || "generic").trim().toLowerCase();
542811
542983
  const base3 = CAMERA_PROFILES[key] ?? { ...DEFAULT_CAMERA_PROFILE, name: key || "generic" };
@@ -542857,15 +543029,17 @@ function formatRouterPlan(plan) {
542857
543029
  }
542858
543030
  return lines.join("\n");
542859
543031
  }
542860
- var KANDAO_USB_VENDORS, QOOCAM_WIFI_PASSWORD, QOOCAM_DEFAULT_IP, OSC_TIMEOUT, DEFAULT_CAMERA_PROFILE, CAMERA_PROFILES, CameraCaptureTool;
543032
+ var KANDAO_USB_VENDORS, QOOCAM_WIFI_PASSWORD, QOOCAM_DEFAULT_IP, OSC_TIMEOUT, DEFAULT_CAPTURE_SIZES, DEFAULT_CAMERA_PROFILE, CAMERA_PROFILES, CameraCaptureTool;
542861
543033
  var init_camera_capture = __esm({
542862
543034
  "packages/execution/dist/tools/camera-capture.js"() {
542863
543035
  "use strict";
542864
543036
  init_process_async();
543037
+ init_system_deps();
542865
543038
  KANDAO_USB_VENDORS = ["12d1", "2aad"];
542866
543039
  QOOCAM_WIFI_PASSWORD = "12345678";
542867
543040
  QOOCAM_DEFAULT_IP = "192.168.173.1";
542868
543041
  OSC_TIMEOUT = 15e3;
543042
+ DEFAULT_CAPTURE_SIZES = ["1280x720", "640x480", "1920x1080"];
542869
543043
  DEFAULT_CAMERA_PROFILE = {
542870
543044
  name: "generic",
542871
543045
  streamBasePort: 9e3,
@@ -543078,17 +543252,22 @@ ${formatRouterPlan(plan)}`,
543078
543252
  async discoverV4l2Devices() {
543079
543253
  const devices = [];
543080
543254
  const devs = sortedVideoDevicePaths();
543255
+ const canProbe = devs.length > 0 ? ensureCommand("v4l2-ctl").available : false;
543081
543256
  for (const dev of devs) {
543082
543257
  let name10 = "Unknown camera";
543083
543258
  let driver = "unknown";
543259
+ let captureCapable;
543084
543260
  try {
543085
- const info = await execFileText("v4l2-ctl", ["-d", dev, "--info"], { timeout: 3e3 });
543086
- const nameMatch = info.match(/Card type\s*:\s*(.+)/);
543087
- const driverMatch = info.match(/Driver name\s*:\s*(.+)/);
543088
- if (nameMatch)
543089
- name10 = nameMatch[1].trim();
543090
- if (driverMatch)
543091
- driver = driverMatch[1].trim();
543261
+ if (canProbe) {
543262
+ const info = await execFileText("v4l2-ctl", ["-d", dev, "--info"], { timeout: 3e3 });
543263
+ const nameMatch = info.match(/Card type\s*:\s*(.+)/);
543264
+ const driverMatch = info.match(/Driver name\s*:\s*(.+)/);
543265
+ if (nameMatch)
543266
+ name10 = nameMatch[1].trim();
543267
+ if (driverMatch)
543268
+ driver = driverMatch[1].trim();
543269
+ captureCapable = /(?:Device Caps|Capabilities)[\s\S]*Video Capture/i.test(info) && !/Metadata Capture/i.test(info);
543270
+ }
543092
543271
  } catch {
543093
543272
  try {
543094
543273
  const idx = dev.match(/video(\d+)/)?.[1];
@@ -543107,6 +543286,7 @@ ${formatRouterPlan(plan)}`,
543107
543286
  driver,
543108
543287
  backend: "v4l2",
543109
543288
  index,
543289
+ captureCapable,
543110
543290
  realsense,
543111
543291
  depthCapable: realsense || haystack.includes("depth")
543112
543292
  });
@@ -543115,14 +543295,15 @@ ${formatRouterPlan(plan)}`,
543115
543295
  }
543116
543296
  async resolveV4l2Device(rawDevice) {
543117
543297
  const devices = await this.discoverV4l2Devices();
543298
+ const captureDevices = devices.filter((device) => device.captureCapable !== false);
543118
543299
  if (!rawDevice)
543119
- return devices[0]?.device ?? null;
543300
+ return captureDevices[0]?.device ?? devices[0]?.device ?? null;
543120
543301
  if (rawDevice.startsWith("/dev/video"))
543121
543302
  return rawDevice;
543122
543303
  const slotMatch = rawDevice.match(/^(?:slot:|camera)?(\d+)$/i);
543123
543304
  if (slotMatch) {
543124
543305
  const idx = Number(slotMatch[1]);
543125
- return devices[idx]?.device ?? null;
543306
+ return captureDevices[idx]?.device ?? devices[idx]?.device ?? null;
543126
543307
  }
543127
543308
  return rawDevice;
543128
543309
  }
@@ -543134,48 +543315,155 @@ ${formatRouterPlan(plan)}`,
543134
543315
  if (!existsSync60(captureDir))
543135
543316
  mkdirSync34(captureDir, { recursive: true });
543136
543317
  const tempFile = outputPath3 || join75(captureDir, `capture-${Date.now()}.jpg`);
543318
+ const deps = await this.ensureV4l2CaptureStack();
543319
+ if (!deps.ok) {
543320
+ return { success: false, output: "", error: deps.error, durationMs: performance.now() - start2 };
543321
+ }
543322
+ const formats = await this.queryV4l2Formats(device);
543323
+ const attempts = buildV4l2CaptureAttempts(width, height, formats);
543324
+ const errors = [];
543325
+ let repaired = false;
543326
+ const captureState = {};
543327
+ const runAttempts = async () => {
543328
+ for (const attempt of attempts) {
543329
+ try {
543330
+ if (existsSync60(tempFile))
543331
+ unlinkSync11(tempFile);
543332
+ } catch {
543333
+ }
543334
+ const argv = this.ffmpegCaptureArgs(device, tempFile, attempt);
543335
+ try {
543336
+ await runProcessBuffer(ffmpegBin2(), argv, { timeout: 15e3 });
543337
+ if (existsSync60(tempFile) && readFileSync44(tempFile).length > 0) {
543338
+ captureState.successfulAttempt = attempt;
543339
+ return true;
543340
+ }
543341
+ errors.push(`${attempt.label}: ffmpeg produced no image bytes`);
543342
+ } catch (err) {
543343
+ const detail = processErrorDetail(err);
543344
+ errors.push(`${attempt.label}: ${detail}`);
543345
+ if (!repaired && cameraFfmpegErrorNeedsPackageRepair(detail)) {
543346
+ const repair = repairCommandPackage("ffmpeg");
543347
+ repaired = true;
543348
+ errors.push(repair.error ? `ffmpeg repair attempted: ${repair.error}` : "ffmpeg repair attempted: package install completed");
543349
+ if (repair.available)
543350
+ return runAttempts();
543351
+ }
543352
+ }
543353
+ }
543354
+ return false;
543355
+ };
543356
+ if (await runAttempts()) {
543357
+ const firstSuccess = errors.length > 0 ? `
543358
+ Recovered with capture profile: ${captureState.successfulAttempt?.label ?? "unknown"}. Last failed attempt: ${errors[errors.length - 1].slice(0, 260)}` : "";
543359
+ const result = this.returnCapturedFile(tempFile, captureState.successfulAttempt?.videoSize ?? `${width}x${height}`, device, outputPath3, start2);
543360
+ return result.success && firstSuccess ? { ...result, output: result.output + firstSuccess, llmContent: result.llmContent ? result.llmContent + firstSuccess : result.llmContent } : result;
543361
+ }
543362
+ const allErrors = errors.join("\n\n");
543363
+ if (/Device or resource busy/i.test(allErrors)) {
543364
+ return { success: false, output: "", error: `Camera ${device} is busy — close other camera apps.
543365
+ ${allErrors.slice(0, 900)}`, durationMs: performance.now() - start2 };
543366
+ }
543367
+ if (/Permission denied/i.test(allErrors)) {
543368
+ return { success: false, output: "", error: `Permission denied opening ${device}. Add the user to the video group or run the daemon with camera permissions.
543369
+ ${allErrors.slice(0, 900)}`, durationMs: performance.now() - start2 };
543370
+ }
543371
+ return {
543372
+ success: false,
543373
+ output: "",
543374
+ error: `Failed to capture from ${device} after ${attempts.length} ffmpeg profiles. Auto-install/repair was ${repaired ? "attempted" : "not triggered"}. On ARM Debian/Ubuntu, the camera stack package set is: sudo apt-get update && sudo apt-get install -y ffmpeg libavcodec-extra v4l-utils
543375
+
543376
+ ` + allErrors.slice(0, 1800),
543377
+ durationMs: performance.now() - start2
543378
+ };
543379
+ }
543380
+ async ensureV4l2CaptureStack() {
543381
+ if (ffmpegBin2() === "ffmpeg") {
543382
+ const ffmpeg = ensureCommand("ffmpeg");
543383
+ if (!ffmpeg.available) {
543384
+ return {
543385
+ ok: false,
543386
+ error: `ffmpeg is required for camera_capture and auto-install failed: ${ffmpeg.error ?? "unknown error"}. On ARM Debian/Ubuntu install: sudo apt-get update && sudo apt-get install -y ffmpeg libavcodec-extra v4l-utils`
543387
+ };
543388
+ }
543389
+ }
543390
+ ensureCommand("v4l2-ctl");
543137
543391
  try {
543138
- await execFileText("ffmpeg", [
543139
- "-hide_banner",
543140
- "-loglevel",
543141
- "error",
543142
- "-f",
543143
- "v4l2",
543144
- "-video_size",
543145
- `${width}x${height}`,
543146
- "-i",
543147
- device,
543148
- "-frames:v",
543149
- "1",
543150
- "-q:v",
543151
- "2",
543152
- "-y",
543153
- tempFile
543154
- ], { timeout: 1e4 });
543392
+ const codecs2 = await execFileText(ffmpegBin2(), ["-hide_banner", "-codecs"], { timeout: 6e3 });
543393
+ const formats = await execFileText(ffmpegBin2(), ["-hide_banner", "-formats"], { timeout: 6e3 });
543394
+ const hasMjpegDecode = /^\s*D\S*\s+mjpeg\b/im.test(codecs2);
543395
+ const hasMjpegEncode = /^\s*\S*E\S*\s+mjpeg\b/im.test(codecs2);
543396
+ const hasV4l2 = /video4linux2|v4l2/i.test(formats);
543397
+ if ((!hasMjpegDecode || !hasMjpegEncode || !hasV4l2) && ffmpegBin2() === "ffmpeg") {
543398
+ repairCommandPackage("ffmpeg");
543399
+ }
543155
543400
  } catch (err) {
543156
- const msg = err instanceof Error ? err.message : String(err);
543157
- if (msg.includes("Device or resource busy")) {
543158
- return { success: false, output: "", error: `Camera ${device} is busy — close other camera apps.`, durationMs: performance.now() - start2 };
543401
+ if (ffmpegBin2() === "ffmpeg")
543402
+ repairCommandPackage("ffmpeg");
543403
+ }
543404
+ return { ok: true, error: "" };
543405
+ }
543406
+ async queryV4l2Formats(device) {
543407
+ const dep = ensureCommand("v4l2-ctl");
543408
+ if (dep.available) {
543409
+ try {
543410
+ return parseV4l2Formats(await execFileText("v4l2-ctl", ["-d", device, "--list-formats-ext"], { timeout: 5e3 }));
543411
+ } catch {
543159
543412
  }
543160
- return { success: false, output: "", error: `Failed to capture from ${device}: ${msg.slice(0, 300)}`, durationMs: performance.now() - start2 };
543161
543413
  }
543162
- return this.returnCapturedFile(tempFile, `${width}x${height}`, device, outputPath3, start2);
543414
+ try {
543415
+ const probeResult = await runProcessBuffer(ffmpegBin2(), ["-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", device], { timeout: 5e3 }).catch((err) => {
543416
+ if (err instanceof AsyncProcessError)
543417
+ return err;
543418
+ throw err;
543419
+ });
543420
+ return parseV4l2Formats(Buffer.concat([probeResult.stdout, probeResult.stderr]).toString("utf8"));
543421
+ } catch {
543422
+ return [];
543423
+ }
543424
+ }
543425
+ ffmpegCaptureArgs(device, outputPath3, attempt) {
543426
+ const argv = ["-hide_banner", "-loglevel", "error", "-f", "v4l2", "-thread_queue_size", "4"];
543427
+ if (attempt.inputFormat)
543428
+ argv.push("-input_format", attempt.inputFormat);
543429
+ if (attempt.videoSize)
543430
+ argv.push("-video_size", attempt.videoSize);
543431
+ argv.push("-i", device, "-frames:v", "1");
543432
+ if (attempt.copyPacket) {
543433
+ argv.push("-c:v", "copy", "-f", "image2");
543434
+ } else {
543435
+ argv.push("-q:v", "2");
543436
+ }
543437
+ argv.push("-y", outputPath3);
543438
+ return argv;
543163
543439
  }
543164
543440
  async v4l2Info(device, start2) {
543165
543441
  let info = `Camera: ${device} [v4l2]
543166
543442
  `;
543443
+ const v4l2Ctl = ensureCommand("v4l2-ctl");
543444
+ const ffmpeg = ffmpegBin2() === "ffmpeg" ? ensureCommand("ffmpeg") : { available: true, installed: false };
543445
+ if (v4l2Ctl.installed || ffmpeg.installed) {
543446
+ info += `
543447
+ Auto-installed camera dependencies: ${[
543448
+ v4l2Ctl.installed ? "v4l2-ctl" : "",
543449
+ ffmpeg.installed ? "ffmpeg" : ""
543450
+ ].filter(Boolean).join(", ")}
543451
+ `;
543452
+ }
543167
543453
  try {
543454
+ if (!v4l2Ctl.available)
543455
+ throw new Error(v4l2Ctl.error || "v4l2-ctl unavailable");
543168
543456
  const formats = await execFileText("v4l2-ctl", ["-d", device, "--list-formats-ext"], { timeout: 5e3 });
543169
543457
  info += "\nSupported formats and resolutions:\n" + formats.trim();
543170
543458
  } catch {
543171
543459
  try {
543172
- const probeResult = await runProcessBuffer("ffmpeg", ["-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", device], { timeout: 5e3 }).catch((err) => {
543460
+ const probeResult = await runProcessBuffer(ffmpegBin2(), ["-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", device], { timeout: 5e3 }).catch((err) => {
543173
543461
  if (err instanceof AsyncProcessError)
543174
543462
  return err;
543175
543463
  throw err;
543176
543464
  });
543177
543465
  const probe = Buffer.concat([probeResult.stdout, probeResult.stderr]).toString("utf8");
543178
- const lines = probe.split("\n").filter((l2) => l2.includes("Raw") || l2.includes("Compressed") || l2.includes("video4linux2"));
543466
+ const lines = probe.split("\n").filter((l2) => l2.includes("Raw") || l2.includes("Compressed") || l2.includes("video4linux2") || l2.includes("MJPEG"));
543179
543467
  info += "\nFormats (ffmpeg probe):\n" + (lines.length > 0 ? lines.join("\n") : "Could not query formats");
543180
543468
  } catch {
543181
543469
  info += "\nCould not query camera capabilities";
@@ -700102,58 +700390,69 @@ function getWebUI() {
700102
700390
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
700103
700391
  <title>Omnius</title>
700104
700392
  <style>
700105
- /* ─── Open WebUI-shaped design tokens (OWUI-1) ────────────────────
700106
- * Replaces ~80 hardcoded color literals with CSS custom properties.
700107
- * Palette matches openwebui's neutral grayscale + functional blue
700108
- * accent. Brand gold preserved as --color-brand for Omnius-only marks.
700109
- * Typography flips body to Inter (UI) + JetBrains Mono (code only),
700110
- * leaving the terminal aesthetic only where we explicitly opt in.
700393
+ /* ─── Cold Tactical HUD design tokens ──────────────────────────────
700394
+ * Source of truth:
700395
+ * /home/roko/Desktop/hud-ui-style-kit(6)/hud-ui-style-kit
700111
700396
  *
700112
- * Source: /tmp/openwebui-ref/src/tailwind.css @theme + parity audit
700113
- * at .aiwg/owui-parity.md.
700397
+ * The UI remains a single daemon-served HTML document, so these tokens
700398
+ * intentionally map onto the older --color-* variable names as well as
700399
+ * the HUD-specific --hud-* names.
700114
700400
  */
700115
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
700116
-
700117
700401
  :root {
700118
- /* Surfaces — neutral grayscale, openwebui dark default */
700119
- --color-bg: oklch(0.16 0 0); /* gray-950, body background */
700120
- --color-bg-elevated: oklch(0.20 0 0); /* gray-900, panels + headers */
700121
- --color-bg-input: oklch(0.27 0 0); /* gray-850, inputs + chips + buttons */
700122
- --color-bg-hover: oklch(0.32 0 0); /* gray-800, hover surfaces */
700123
-
700124
- /* Foregrounds */
700125
- --color-fg: oklch(0.94 0 0); /* primary text */
700126
- --color-fg-muted: oklch(0.69 0 0); /* secondary text — gray-500 */
700127
- --color-fg-subtle: oklch(0.51 0 0); /* tertiary — gray-600 */
700128
- --color-fg-faint: oklch(0.42 0 0); /* faint hints — gray-700 */
700129
-
700130
- /* Borders */
700131
- --color-border: oklch(0.27 0 0); /* default — gray-850 */
700132
- --color-border-strong:oklch(0.32 0 0); /* emphasized gray-800 */
700133
-
700134
- /* Functional accents monochrome white accent for a clean, simple UI. */
700135
- --color-accent: #ffffff; /* white primary actions */
700136
- --color-accent-hover: #e5e7eb; /* gray-200 subtle hover */
700137
- --color-accent-fg: #0a0a0a; /* text/icon color ON an accent surface */
700138
- --color-success: #22c55e; /* green-500 */
700139
- --color-warning: #f59e0b; /* amber-500 */
700140
- --color-error: #ef4444; /* red-500 */
700141
- --color-info: #ffffff; /* white checkin/info */
700142
-
700143
- /* Omnius brand — kept narrow scope for Omnius-only marks (active tab strip,
700144
- * brand glyphs, status accents). Monochrome white to match the accent. */
700145
- --color-brand: #ffffff;
700146
- --color-brand-hover: #e5e7eb;
700402
+ --hud-void: #060a0c;
700403
+ --hud-black: #0a0f12;
700404
+ --hud-panel-solid: #121b1f;
700405
+ --hud-steel: #353f47;
700406
+ --hud-steel-2: #48545a;
700407
+ --hud-dim: #687577;
700408
+ --hud-muted: #829094;
700409
+ --hud-ink-soft: #c3ccce;
700410
+ --hud-ink: #d9dfdf;
700411
+ --hud-accent: #d29a3a;
700412
+ --hud-accent-strong: #f1bd55;
700413
+ --hud-cyan: #b7d3d8;
700414
+ --hud-danger: #d06c5f;
700415
+ --hud-success: #9fb594;
700416
+ --hud-panel-clip: polygon(0 0, calc(100% - 22px) 0, 100% 22px, 100% 100%, 18px 100%, 0 calc(100% - 18px));
700417
+ --hud-button-clip: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px));
700418
+ --hud-panel-shadow: 0 24px 80px rgba(0,0,0,0.62), inset 0 1px 0 rgba(255,255,255,0.09);
700419
+ --hud-cyan-glow: 0 0 18px rgba(183,211,216,0.22);
700420
+ --hud-amber-glow: 0 0 22px rgba(210,154,58,0.28);
700421
+
700422
+ /* Legacy color variable bridge */
700423
+ --color-bg: var(--hud-void);
700424
+ --color-bg-elevated: rgba(18, 27, 31, 0.88);
700425
+ --color-bg-input: rgba(10, 15, 18, 0.82);
700426
+ --color-bg-hover: rgba(53, 63, 71, 0.56);
700427
+
700428
+ --color-fg: var(--hud-ink);
700429
+ --color-fg-muted: var(--hud-ink-soft);
700430
+ --color-fg-subtle: var(--hud-muted);
700431
+ --color-fg-faint: var(--hud-dim);
700432
+
700433
+ --color-border: var(--hud-steel);
700434
+ --color-border-strong: var(--hud-steel-2);
700435
+
700436
+ --color-accent: var(--hud-accent);
700437
+ --color-accent-hover: var(--hud-accent-strong);
700438
+ --color-accent-fg: #081014;
700439
+ --color-success: var(--hud-success);
700440
+ --color-warning: var(--hud-accent-strong);
700441
+ --color-error: var(--hud-danger);
700442
+ --color-info: var(--hud-cyan);
700443
+ --color-brand: var(--hud-accent);
700444
+ --color-brand-hover: var(--hud-accent-strong);
700147
700445
 
700148
700446
  /* Typography */
700149
- --font-ui: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
700150
- --font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', ui-monospace, monospace;
700447
+ --font-display: 'DIN Condensed', 'Bahnschrift Condensed', 'Arial Narrow', 'Roboto Condensed', Impact, sans-serif;
700448
+ --font-ui: Inter, 'Segoe UI', Roboto, Arial, sans-serif;
700449
+ --font-mono: 'IBM Plex Mono', 'JetBrains Mono', 'Cascadia Mono', 'SF Mono', Consolas, ui-monospace, monospace;
700151
700450
 
700152
700451
  /* Radii */
700153
- --radius-sm: 6px;
700154
- --radius-md: 8px;
700155
- --radius-lg: 12px; /* openwebui's rounded-xl */
700156
- --radius-pill: 9999px;
700452
+ --radius-sm: 2px;
700453
+ --radius-md: 3px;
700454
+ --radius-lg: 4px;
700455
+ --radius-pill: 2px;
700157
700456
  }
700158
700457
 
700159
700458
  /* Light theme — opt in via [data-theme=light] on html.
@@ -701446,6 +701745,433 @@ body { display:flex; flex-direction:column; height:100vh; margin:0; overflow:hid
701446
701745
  text-transform: uppercase;
701447
701746
  letter-spacing: 0.05em;
701448
701747
  }
701748
+
701749
+ /* ════════════════════════════════════════════════════════════
701750
+ HUD foundation ported from hud-ui-style-kit
701751
+ ════════════════════════════════════════════════════════════ */
701752
+ body::before {
701753
+ content: "";
701754
+ position: fixed;
701755
+ inset: 0;
701756
+ pointer-events: none;
701757
+ z-index: -2;
701758
+ background:
701759
+ linear-gradient(rgba(183,211,216,0.045) 1px, transparent 1px),
701760
+ linear-gradient(90deg, rgba(183,211,216,0.045) 1px, transparent 1px),
701761
+ radial-gradient(circle at 22% 18%, rgba(183,211,216,0.09), transparent 30%),
701762
+ linear-gradient(135deg, #060a0c 0%, #0a1114 46%, #10191d 100%);
701763
+ background-size: 64px 64px, 64px 64px, auto, auto;
701764
+ }
701765
+ body::after {
701766
+ content: "";
701767
+ position: fixed;
701768
+ inset: 0;
701769
+ pointer-events: none;
701770
+ z-index: -1;
701771
+ background:
701772
+ repeating-linear-gradient(0deg, rgba(255,255,255,0.025) 0, rgba(255,255,255,0.025) 1px, transparent 1px, transparent 6px);
701773
+ mix-blend-mode: screen;
701774
+ opacity: 0.32;
701775
+ }
701776
+
701777
+ .hud-panel {
701778
+ position: relative;
701779
+ border: 1px solid var(--hud-steel);
701780
+ clip-path: var(--hud-panel-clip);
701781
+ background: rgba(10, 15, 18, 0.72);
701782
+ box-shadow: var(--hud-panel-shadow);
701783
+ overflow: hidden;
701784
+ }
701785
+ .hud-panel__chrome {
701786
+ position: absolute;
701787
+ inset: 0;
701788
+ pointer-events: none;
701789
+ z-index: 0;
701790
+ }
701791
+ .hud-panel__chrome::before {
701792
+ content: "";
701793
+ position: absolute;
701794
+ inset: 0;
701795
+ background:
701796
+ linear-gradient(rgba(183,211,216,0.055) 1px, transparent 1px),
701797
+ linear-gradient(90deg, rgba(183,211,216,0.045) 1px, transparent 1px);
701798
+ background-size: 18px 18px;
701799
+ opacity: 0.38;
701800
+ }
701801
+ .hud-panel__chrome::after {
701802
+ content: "";
701803
+ position: absolute;
701804
+ inset: 1px;
701805
+ border: 1px solid rgba(217,223,223,0.08);
701806
+ clip-path: var(--hud-panel-clip);
701807
+ }
701808
+ .hud-panel > :not(.hud-panel__chrome) {
701809
+ position: relative;
701810
+ z-index: 1;
701811
+ }
701812
+ .hud-panel--cut::before,
701813
+ .hud-panel--slant::before {
701814
+ content: "";
701815
+ position: absolute;
701816
+ top: 0;
701817
+ right: 0;
701818
+ width: 31px;
701819
+ border-top: 1px solid var(--hud-steel-2);
701820
+ transform: rotate(45deg);
701821
+ transform-origin: top right;
701822
+ z-index: 2;
701823
+ }
701824
+ .hud-panel--cut::after,
701825
+ .hud-panel--slant::after {
701826
+ content: "";
701827
+ position: absolute;
701828
+ left: 0;
701829
+ bottom: 0;
701830
+ width: 26px;
701831
+ border-top: 1px solid var(--hud-steel-2);
701832
+ transform: rotate(45deg);
701833
+ transform-origin: bottom left;
701834
+ z-index: 2;
701835
+ }
701836
+ .hud-panel__header {
701837
+ display: flex;
701838
+ align-items: center;
701839
+ justify-content: space-between;
701840
+ gap: 12px;
701841
+ padding: 12px 14px 8px;
701842
+ border-bottom: 1px solid rgba(72,84,90,0.64);
701843
+ }
701844
+ .hud-panel__body {
701845
+ padding: 12px 14px 14px;
701846
+ }
701847
+ .kicker,
701848
+ .hud-kicker {
701849
+ color: var(--hud-muted);
701850
+ font-family: var(--font-mono);
701851
+ font-size: 0.56rem;
701852
+ letter-spacing: 0.17em;
701853
+ text-transform: uppercase;
701854
+ }
701855
+ .hud-title {
701856
+ color: var(--hud-ink);
701857
+ font-family: var(--font-display);
701858
+ font-size: clamp(1.15rem, 2.2vw, 2rem);
701859
+ line-height: 0.95;
701860
+ letter-spacing: 0.08em;
701861
+ text-transform: uppercase;
701862
+ }
701863
+ .hud-subtitle {
701864
+ color: var(--hud-muted);
701865
+ font-family: var(--font-mono);
701866
+ font-size: 0.68rem;
701867
+ line-height: 1.45;
701868
+ }
701869
+ .hud-btn,
701870
+ .hud-tab {
701871
+ --hud-btn-border: var(--hud-steel);
701872
+ position: relative;
701873
+ display: inline-flex;
701874
+ align-items: center;
701875
+ justify-content: center;
701876
+ gap: 8px;
701877
+ min-height: 30px;
701878
+ border: 1px solid var(--hud-btn-border);
701879
+ clip-path: var(--hud-button-clip);
701880
+ background: rgba(18,27,31,0.74);
701881
+ color: var(--hud-ink-soft);
701882
+ font-family: var(--font-mono);
701883
+ font-size: 0.64rem;
701884
+ letter-spacing: 0.08em;
701885
+ text-transform: uppercase;
701886
+ cursor: pointer;
701887
+ overflow: hidden;
701888
+ }
701889
+ .hud-btn__chrome,
701890
+ .hud-tab__chrome {
701891
+ position: absolute;
701892
+ inset: 0;
701893
+ pointer-events: none;
701894
+ }
701895
+ .hud-btn__chrome::before,
701896
+ .hud-tab__chrome::before {
701897
+ content: "";
701898
+ position: absolute;
701899
+ top: 0;
701900
+ right: 0;
701901
+ width: 17px;
701902
+ border-top: 1px solid var(--hud-btn-border);
701903
+ transform: rotate(45deg);
701904
+ transform-origin: top right;
701905
+ }
701906
+ .hud-btn__chrome::after,
701907
+ .hud-tab__chrome::after {
701908
+ content: "";
701909
+ position: absolute;
701910
+ left: 0;
701911
+ bottom: 0;
701912
+ width: 17px;
701913
+ border-top: 1px solid var(--hud-btn-border);
701914
+ transform: rotate(45deg);
701915
+ transform-origin: bottom left;
701916
+ }
701917
+ .hud-btn__label,
701918
+ .hud-tab__label {
701919
+ position: relative;
701920
+ z-index: 1;
701921
+ white-space: nowrap;
701922
+ }
701923
+ .hud-btn:hover,
701924
+ .hud-tab:hover {
701925
+ --hud-btn-border: var(--hud-cyan);
701926
+ color: var(--hud-ink);
701927
+ box-shadow: var(--hud-cyan-glow);
701928
+ }
701929
+ .hud-btn--primary,
701930
+ .hud-tab[aria-selected="true"],
701931
+ .hud-tab.active {
701932
+ --hud-btn-border: var(--hud-accent);
701933
+ color: #081014;
701934
+ background: linear-gradient(180deg, var(--hud-accent-strong), var(--hud-accent));
701935
+ box-shadow: var(--hud-amber-glow);
701936
+ }
701937
+ .hud-btn--ghost {
701938
+ background: rgba(10,15,18,0.58);
701939
+ }
701940
+ .hud-btn--danger {
701941
+ --hud-btn-border: var(--hud-danger);
701942
+ color: var(--hud-danger);
701943
+ }
701944
+
701945
+ #omnius-shell {
701946
+ background: transparent !important;
701947
+ }
701948
+ #omnius-sidebar {
701949
+ background: rgba(8,13,16,0.88) !important;
701950
+ border-right: 1px solid var(--hud-steel-2) !important;
701951
+ box-shadow: 18px 0 48px rgba(0,0,0,0.38);
701952
+ }
701953
+ #omnius-sidebar::before {
701954
+ content: "";
701955
+ position: absolute;
701956
+ inset: 0;
701957
+ pointer-events: none;
701958
+ background:
701959
+ linear-gradient(rgba(183,211,216,0.04) 1px, transparent 1px),
701960
+ linear-gradient(90deg, rgba(183,211,216,0.035) 1px, transparent 1px);
701961
+ background-size: 18px 18px;
701962
+ opacity: 0.48;
701963
+ }
701964
+ #omnius-main {
701965
+ background: rgba(6,10,12,0.62) !important;
701966
+ }
701967
+ #header {
701968
+ min-height: 46px;
701969
+ background: rgba(10,15,18,0.84) !important;
701970
+ border-bottom: 1px solid var(--hud-steel-2) !important;
701971
+ box-shadow: inset 0 -1px 0 rgba(183,211,216,0.08);
701972
+ }
701973
+ #header .accent,
701974
+ #sidebar-brand {
701975
+ font-family: var(--font-display) !important;
701976
+ letter-spacing: 0.08em !important;
701977
+ text-transform: uppercase;
701978
+ }
701979
+ .sb-nav {
701980
+ border-radius: 0 !important;
701981
+ clip-path: var(--hud-button-clip);
701982
+ font-family: var(--font-mono) !important;
701983
+ letter-spacing: 0.08em;
701984
+ text-transform: uppercase;
701985
+ }
701986
+ .sb-nav.active {
701987
+ background: rgba(210,154,58,0.18) !important;
701988
+ border-color: var(--hud-accent) !important;
701989
+ color: var(--hud-accent-strong) !important;
701990
+ box-shadow: inset 2px 0 0 var(--hud-accent);
701991
+ }
701992
+ .sb-chat,
701993
+ .sb-folder-row,
701994
+ #sidebar-search,
701995
+ #input-area,
701996
+ textarea,
701997
+ input,
701998
+ select {
701999
+ border-radius: 0 !important;
702000
+ }
702001
+ input,
702002
+ textarea,
702003
+ select {
702004
+ background: rgba(6,10,12,0.72) !important;
702005
+ border-color: var(--hud-steel) !important;
702006
+ color: var(--hud-ink) !important;
702007
+ font-family: var(--font-mono);
702008
+ }
702009
+ input:focus,
702010
+ textarea:focus,
702011
+ select:focus {
702012
+ border-color: var(--hud-cyan) !important;
702013
+ box-shadow: var(--hud-cyan-glow);
702014
+ }
702015
+ button {
702016
+ border-radius: 0 !important;
702017
+ }
702018
+ #chat-container,
702019
+ #agent-panel,
702020
+ #jobs-panel,
702021
+ #config-panel,
702022
+ #activity-panel,
702023
+ #projects-panel,
702024
+ #voice-panel,
702025
+ #generate-panel {
702026
+ background:
702027
+ linear-gradient(rgba(183,211,216,0.032) 1px, transparent 1px),
702028
+ linear-gradient(90deg, rgba(183,211,216,0.028) 1px, transparent 1px) !important;
702029
+ background-size: 32px 32px !important;
702030
+ }
702031
+ .session-topbar,
702032
+ #footer,
702033
+ #input-toolbar,
702034
+ #input-row,
702035
+ #processes-row,
702036
+ #tasks-row {
702037
+ background: rgba(8,13,16,0.88) !important;
702038
+ border-color: var(--hud-steel) !important;
702039
+ }
702040
+ .msg.assistant::before {
702041
+ border-radius: 0 !important;
702042
+ clip-path: var(--hud-button-clip);
702043
+ background: rgba(210,154,58,0.16) !important;
702044
+ border: 1px solid var(--hud-accent);
702045
+ color: var(--hud-accent-strong) !important;
702046
+ }
702047
+ .msg.user > *,
702048
+ .msg.system > *,
702049
+ .msg .tool-card,
702050
+ .msg pre,
702051
+ .omnius-modal-window,
702052
+ .settings-provider-module,
702053
+ .settings-provider-setup {
702054
+ border-radius: 0 !important;
702055
+ border-color: var(--hud-steel) !important;
702056
+ background: rgba(10,15,18,0.72) !important;
702057
+ }
702058
+
702059
+ .hud-workbench {
702060
+ display: grid;
702061
+ grid-template-columns: minmax(190px, 240px) minmax(0, 1fr) minmax(260px, 360px);
702062
+ gap: 12px;
702063
+ min-height: 100%;
702064
+ }
702065
+ .hud-rail {
702066
+ display: flex;
702067
+ flex-direction: column;
702068
+ gap: 8px;
702069
+ }
702070
+ .hud-rail-group {
702071
+ display: grid;
702072
+ gap: 6px;
702073
+ }
702074
+ .hud-rail-label {
702075
+ padding: 0 2px;
702076
+ color: var(--hud-dim);
702077
+ font-family: var(--font-mono);
702078
+ font-size: 0.55rem;
702079
+ letter-spacing: 0.17em;
702080
+ text-transform: uppercase;
702081
+ }
702082
+ .hud-field {
702083
+ display: grid;
702084
+ gap: 4px;
702085
+ margin-bottom: 10px;
702086
+ }
702087
+ .hud-field label {
702088
+ color: var(--hud-muted);
702089
+ font-family: var(--font-mono);
702090
+ font-size: 0.58rem;
702091
+ letter-spacing: 0.12em;
702092
+ text-transform: uppercase;
702093
+ }
702094
+ .hud-form-grid {
702095
+ display: grid;
702096
+ grid-template-columns: repeat(3, minmax(90px, 1fr));
702097
+ gap: 8px;
702098
+ }
702099
+ .hud-status-line {
702100
+ min-height: 1.2em;
702101
+ color: var(--hud-muted);
702102
+ font-family: var(--font-mono);
702103
+ font-size: 0.64rem;
702104
+ line-height: 1.45;
702105
+ }
702106
+ .hud-meter {
702107
+ height: 8px;
702108
+ background: rgba(6,10,12,0.9);
702109
+ border: 1px solid var(--hud-steel);
702110
+ overflow: hidden;
702111
+ }
702112
+ .hud-meter > div {
702113
+ height: 100%;
702114
+ width: 0%;
702115
+ background: linear-gradient(90deg, var(--hud-accent), var(--hud-cyan));
702116
+ transition: width 0.2s;
702117
+ }
702118
+ .hud-result {
702119
+ display: grid;
702120
+ gap: 8px;
702121
+ color: var(--hud-ink-soft);
702122
+ font-family: var(--font-mono);
702123
+ font-size: 0.64rem;
702124
+ line-height: 1.45;
702125
+ }
702126
+ .hud-result-card {
702127
+ border: 1px solid rgba(72,84,90,0.78);
702128
+ background: rgba(6,10,12,0.68);
702129
+ padding: 8px;
702130
+ }
702131
+ .hud-result-card strong {
702132
+ display: block;
702133
+ margin-bottom: 4px;
702134
+ color: var(--hud-accent-strong);
702135
+ font-family: var(--font-display);
702136
+ font-size: 0.9rem;
702137
+ letter-spacing: 0.08em;
702138
+ text-transform: uppercase;
702139
+ }
702140
+ .hud-chip-row {
702141
+ display: flex;
702142
+ flex-wrap: wrap;
702143
+ gap: 5px;
702144
+ }
702145
+ .hud-chip {
702146
+ border: 1px solid var(--hud-steel);
702147
+ background: rgba(6,10,12,0.62);
702148
+ color: var(--hud-ink-soft);
702149
+ padding: 3px 6px;
702150
+ font-family: var(--font-mono);
702151
+ font-size: 0.56rem;
702152
+ letter-spacing: 0.08em;
702153
+ text-transform: uppercase;
702154
+ }
702155
+ .hud-chip.live { border-color: var(--hud-success); color: var(--hud-success); }
702156
+ .hud-chip.mock { border-color: var(--hud-danger); color: var(--hud-danger); }
702157
+ .hud-gallery-grid {
702158
+ display: grid;
702159
+ grid-template-columns: repeat(auto-fill,minmax(128px,1fr));
702160
+ gap: 8px;
702161
+ }
702162
+ .hud-scroll {
702163
+ max-height: 100%;
702164
+ overflow: auto;
702165
+ }
702166
+
702167
+ @media (max-width: 1180px) {
702168
+ .hud-workbench { grid-template-columns: 190px minmax(0, 1fr); }
702169
+ .hud-workbench > .hud-side-output { grid-column: 1 / -1; }
702170
+ }
702171
+ @media (max-width: 820px) {
702172
+ .hud-workbench { grid-template-columns: 1fr; }
702173
+ .hud-form-grid { grid-template-columns: 1fr; }
702174
+ }
701449
702175
  </style>
701450
702176
  </head>
701451
702177
  <body>
@@ -701771,94 +702497,162 @@ body { display:flex; flex-direction:column; height:100vh; margin:0; overflow:hid
701771
702497
  the unified ~/.omnius model store. Loads models from /v1/media/models,
701772
702498
  generates via /v1/media/<kind>, and browses the global gallery. -->
701773
702499
  <div id="generate-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
701774
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
701775
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin:0">Generate Media</h3>
701776
- <span id="gen-store-info" style="font-size:0.6rem;color:var(--color-fg-subtle)"></span>
701777
- </div>
701778
- <!-- Storage panel: where the heavy weights/venvs/gallery live + relocation -->
701779
- <div style="border:1px solid var(--color-border);border-radius:6px;padding:8px 10px;margin-bottom:12px;background:var(--color-bg-elevated)">
701780
- <div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
701781
- <div style="font-size:0.62rem;color:var(--color-fg-subtle);overflow:hidden">
701782
- <span style="color:var(--color-fg-muted)">Model storage:</span> <code id="gen-store-path" style="color:var(--color-brand);word-break:break-all">~/.omnius</code>
701783
- <span id="gen-store-disk" style="color:var(--color-fg-faint);margin-left:6px"></span>
701784
- </div>
701785
- <button onclick="toggleMediaLocationPanel()" style="flex:none;background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg-subtle);padding:3px 10px;border-radius:3px;font-size:0.6rem;cursor:pointer;font-family:inherit">Change location</button>
701786
- </div>
701787
- <div id="gen-relocate-panel" style="display:none;margin-top:8px;border-top:1px solid var(--color-border);padding-top:8px">
701788
- <div style="font-size:0.6rem;color:var(--color-fg-faint);margin-bottom:4px">Pick a folder to move the heavy model weights, venvs, and gallery to. DBs/sessions stay in <code>~/.omnius</code>.</div>
701789
- <div id="gen-folder-browser" style="max-height:180px;overflow:auto;border:1px solid var(--color-border);border-radius:4px;padding:4px;font-size:0.64rem;margin-bottom:6px;font-family:var(--font-mono)"></div>
701790
- <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
701791
- <input id="gen-relocate-target" type="text" placeholder="/absolute/folder" style="flex:1;min-width:160px;background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:4px 8px;border-radius:3px;font-size:0.64rem;font-family:var(--font-mono)">
701792
- <button onclick="previewMediaRelocate()" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg-subtle);padding:4px 10px;border-radius:3px;font-size:0.62rem;cursor:pointer;font-family:inherit">Preview</button>
701793
- <button onclick="startMediaRelocate()" style="background:var(--color-brand);border:none;color:var(--color-accent-fg);padding:4px 12px;border-radius:3px;font-size:0.62rem;font-weight:600;cursor:pointer;font-family:inherit">Migrate</button>
702500
+ <div class="hud-workbench">
702501
+ <aside class="hud-panel hud-panel--slant hud-rail" id="gen-mode-rail">
702502
+ <div class="hud-panel__chrome" aria-hidden="true"></div>
702503
+ <header class="hud-panel__header">
702504
+ <div>
702505
+ <div class="kicker">Media Operations</div>
702506
+ <h2 class="hud-title">Generate</h2>
702507
+ </div>
702508
+ </header>
702509
+ <div class="hud-panel__body hud-rail">
702510
+ <div class="hud-rail-group" id="gen-kind-row">
702511
+ <div class="hud-rail-label">Synthesis</div>
702512
+ <button class="hud-tab gen-kind gen-mode-tab active" aria-selected="true" data-kind="image" data-gen-mode="image" onclick="genSelectMode('image')"><span class="hud-tab__chrome" aria-hidden="true"></span><span class="hud-tab__label">Image</span></button>
702513
+ <button class="hud-tab gen-kind gen-mode-tab" aria-selected="false" data-kind="video" data-gen-mode="video" onclick="genSelectMode('video')"><span class="hud-tab__chrome" aria-hidden="true"></span><span class="hud-tab__label">Video</span></button>
702514
+ <button class="hud-tab gen-kind gen-mode-tab" aria-selected="false" data-kind="audio" data-gen-mode="audio" onclick="genSelectMode('audio')"><span class="hud-tab__chrome" aria-hidden="true"></span><span class="hud-tab__label">Audio</span></button>
702515
+ <button class="hud-tab gen-kind gen-mode-tab" aria-selected="false" data-kind="music" data-gen-mode="music" onclick="genSelectMode('music')"><span class="hud-tab__chrome" aria-hidden="true"></span><span class="hud-tab__label">Music</span></button>
702516
+ </div>
702517
+ <div class="hud-rail-group">
702518
+ <div class="hud-rail-label">Analysis</div>
702519
+ <button class="hud-tab gen-mode-tab" aria-selected="false" data-gen-mode="audio-analysis" onclick="genSelectMode('audio-analysis')"><span class="hud-tab__chrome" aria-hidden="true"></span><span class="hud-tab__label">Audio Analysis</span></button>
702520
+ <button class="hud-tab gen-mode-tab" aria-selected="false" data-gen-mode="video-analysis" onclick="genSelectMode('video-analysis')"><span class="hud-tab__chrome" aria-hidden="true"></span><span class="hud-tab__label">Video Analysis</span></button>
702521
+ <button class="hud-tab gen-mode-tab" aria-selected="false" data-gen-mode="live-analysis" onclick="genSelectMode('live-analysis')"><span class="hud-tab__chrome" aria-hidden="true"></span><span class="hud-tab__label">Live AV Analysis</span></button>
702522
+ </div>
702523
+ <div class="hud-result-card">
702524
+ <strong>Store</strong>
702525
+ <div class="hud-status-line"><code id="gen-store-path" style="color:var(--hud-cyan);word-break:break-all">~/.omnius</code></div>
702526
+ <div class="hud-status-line" id="gen-store-disk"></div>
702527
+ <div class="hud-status-line" id="gen-store-info"></div>
702528
+ <button class="hud-btn hud-btn--ghost" onclick="toggleMediaLocationPanel()" style="width:100%;margin-top:8px"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">Change Location</span></button>
702529
+ <div id="gen-relocate-panel" style="display:none;margin-top:10px;border-top:1px solid var(--hud-steel);padding-top:10px">
702530
+ <div class="hud-status-line">Move model weights, venvs, and gallery. DBs/sessions stay in ~/.omnius.</div>
702531
+ <div id="gen-folder-browser" style="max-height:180px;overflow:auto;border:1px solid var(--hud-steel);padding:5px;font-size:0.64rem;margin:6px 0;font-family:var(--font-mono)"></div>
702532
+ <div class="hud-field">
702533
+ <input id="gen-relocate-target" type="text" placeholder="/absolute/folder" style="width:100%;padding:6px 8px;font-size:0.64rem">
702534
+ </div>
702535
+ <div style="display:flex;gap:6px;flex-wrap:wrap">
702536
+ <button class="hud-btn hud-btn--ghost" onclick="previewMediaRelocate()"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">Preview</span></button>
702537
+ <button class="hud-btn hud-btn--primary" onclick="startMediaRelocate()"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">Migrate</span></button>
702538
+ </div>
702539
+ <div id="gen-relocate-status" class="hud-status-line" style="margin-top:6px"></div>
702540
+ <div id="gen-relocate-bar-wrap" class="hud-meter" style="display:none;margin-top:6px"><div id="gen-relocate-bar"></div></div>
702541
+ </div>
702542
+ </div>
701794
702543
  </div>
701795
- <div id="gen-relocate-status" style="font-size:0.62rem;color:var(--color-fg-subtle);margin-top:6px"></div>
701796
- <div id="gen-relocate-bar-wrap" style="display:none;margin-top:6px;height:8px;background:var(--color-bg);border:1px solid var(--color-border);border-radius:4px;overflow:hidden">
701797
- <div id="gen-relocate-bar" style="height:100%;width:0%;background:var(--color-brand);transition:width 0.2s"></div>
702544
+ </aside>
702545
+
702546
+ <main class="hud-rail">
702547
+ <section id="gen-create-panel" class="hud-panel hud-panel--cut">
702548
+ <div class="hud-panel__chrome" aria-hidden="true"></div>
702549
+ <header class="hud-panel__header">
702550
+ <div>
702551
+ <div class="kicker">Synthesis Console</div>
702552
+ <h2 class="hud-title" id="gen-mode-title">Image Generator</h2>
702553
+ </div>
702554
+ <button id="gen-run-btn" class="hud-btn hud-btn--primary" onclick="generateMedia()"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">Generate</span></button>
702555
+ </header>
702556
+ <div class="hud-panel__body">
702557
+ <div class="hud-field">
702558
+ <label for="gen-model">Model</label>
702559
+ <select id="gen-model" style="width:100%;padding:7px 9px;font-size:0.68rem">
702560
+ <option value="auto">auto (recommended)</option>
702561
+ </select>
702562
+ </div>
702563
+ <div class="hud-field">
702564
+ <label for="gen-prompt">Prompt</label>
702565
+ <textarea id="gen-prompt" rows="5" placeholder="Describe what to generate..." style="width:100%;padding:8px 10px;font-size:0.74rem;resize:vertical"></textarea>
702566
+ </div>
702567
+ <div class="hud-form-grid">
702568
+ <div class="hud-field"><label for="gen-seed">Seed</label><input id="gen-seed" type="number" placeholder="random" style="width:100%;padding:6px 8px;font-size:0.66rem"></div>
702569
+ <div id="gen-steps-wrap" class="hud-field"><label for="gen-steps">Steps</label><input id="gen-steps" type="number" placeholder="model default" style="width:100%;padding:6px 8px;font-size:0.66rem"></div>
702570
+ <div id="gen-duration-wrap" class="hud-field" style="display:none"><label for="gen-duration">Duration s</label><input id="gen-duration" type="number" placeholder="model default" style="width:100%;padding:6px 8px;font-size:0.66rem"></div>
702571
+ </div>
702572
+ <div id="gen-status" class="hud-status-line" style="margin-top:4px"></div>
702573
+ <div id="gen-progress-wrap" style="display:none;margin-top:8px">
702574
+ <div class="hud-status-line"><span id="gen-progress-stage">starting...</span> <span id="gen-progress-msg"></span></div>
702575
+ <div class="hud-meter" style="margin-top:4px"><div id="gen-progress-bar"></div></div>
702576
+ </div>
702577
+ </div>
702578
+ </section>
702579
+
702580
+ <section id="gen-analysis-panel" class="hud-panel hud-panel--cut" style="display:none">
702581
+ <div class="hud-panel__chrome" aria-hidden="true"></div>
702582
+ <header class="hud-panel__header">
702583
+ <div>
702584
+ <div class="kicker">Perception Console</div>
702585
+ <h2 class="hud-title" id="av-mode-title">Audio Analysis</h2>
702586
+ </div>
702587
+ <button id="av-run-btn" class="hud-btn hud-btn--primary" onclick="analyzeAvMedia()"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">Analyze</span></button>
702588
+ </header>
702589
+ <div class="hud-panel__body">
702590
+ <div class="hud-field">
702591
+ <label for="av-source">Source path or file URI</label>
702592
+ <input id="av-source" type="text" placeholder="/path/to/audio-or-video.mp4 or file:///path/to/media.wav" style="width:100%;padding:7px 9px;font-size:0.68rem">
702593
+ </div>
702594
+ <div class="hud-field">
702595
+ <label for="av-question">Question</label>
702596
+ <textarea id="av-question" rows="4" placeholder="What is happening? Who or what appears? What audio events matter?" style="width:100%;padding:8px 10px;font-size:0.72rem;resize:vertical"></textarea>
702597
+ </div>
702598
+ <div style="display:flex;gap:8px;flex-wrap:wrap">
702599
+ <button class="hud-btn hud-btn--ghost" onclick="analyzeAvMedia('audio-analysis')"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">Audio Pass</span></button>
702600
+ <button class="hud-btn hud-btn--ghost" onclick="analyzeAvMedia('video-analysis')"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">Video Pass</span></button>
702601
+ <button class="hud-btn hud-btn--ghost" onclick="analyzeAvMedia('live-analysis')"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">Full AV Pass</span></button>
702602
+ </div>
702603
+ <div id="av-status" class="hud-status-line" style="margin-top:8px"></div>
702604
+ <div id="av-result" class="hud-result" style="margin-top:10px"></div>
702605
+ </div>
702606
+ </section>
702607
+
702608
+ <section class="hud-panel hud-panel--cut">
702609
+ <div class="hud-panel__chrome" aria-hidden="true"></div>
702610
+ <header class="hud-panel__header">
702611
+ <div>
702612
+ <div class="kicker">Media Archive</div>
702613
+ <h2 class="hud-title">Gallery</h2>
702614
+ </div>
702615
+ <button class="hud-btn hud-btn--ghost" onclick="loadGenerateGallery()"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">Refresh</span></button>
702616
+ </header>
702617
+ <div class="hud-panel__body">
702618
+ <div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:8px;font-size:0.6rem">
702619
+ <div id="gen-filter-kinds" style="display:flex;gap:4px;flex-wrap:wrap">
702620
+ <button class="hud-btn hud-btn--primary gen-filter-kind active" data-fkind="" onclick="setGalleryKindFilter('')"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">All</span></button>
702621
+ <button class="hud-btn hud-btn--ghost gen-filter-kind" data-fkind="image" onclick="setGalleryKindFilter('image')"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">Image</span></button>
702622
+ <button class="hud-btn hud-btn--ghost gen-filter-kind" data-fkind="video" onclick="setGalleryKindFilter('video')"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">Video</span></button>
702623
+ <button class="hud-btn hud-btn--ghost gen-filter-kind" data-fkind="audio" onclick="setGalleryKindFilter('audio')"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">Audio</span></button>
702624
+ <button class="hud-btn hud-btn--ghost gen-filter-kind" data-fkind="music" onclick="setGalleryKindFilter('music')"><span class="hud-btn__chrome" aria-hidden="true"></span><span class="hud-btn__label">Music</span></button>
702625
+ </div>
702626
+ <input id="gen-filter-q" type="text" placeholder="search prompt..." oninput="scheduleGalleryReload()" style="flex:1;min-width:140px;padding:5px 8px;font-size:0.6rem">
702627
+ <input id="gen-filter-model" type="text" placeholder="model..." oninput="scheduleGalleryReload()" style="width:110px;padding:5px 8px;font-size:0.6rem">
702628
+ <label class="hud-subtitle">from <input id="gen-filter-from" type="date" onchange="scheduleGalleryReload()" style="padding:4px 6px;font-size:0.6rem"></label>
702629
+ <label class="hud-subtitle">to <input id="gen-filter-to" type="date" onchange="scheduleGalleryReload()" style="padding:4px 6px;font-size:0.6rem"></label>
702630
+ <select id="gen-filter-sort" onchange="scheduleGalleryReload()" style="padding:4px 6px;font-size:0.6rem">
702631
+ <option value="new">Newest</option><option value="old">Oldest</option><option value="big">Largest</option><option value="small">Smallest</option>
702632
+ </select>
702633
+ <span id="gen-gallery-count" class="hud-status-line"></span>
702634
+ </div>
702635
+ <div id="gen-gallery" class="hud-gallery-grid" style="font-size:0.62rem"></div>
702636
+ <div id="gen-gallery-more" style="text-align:center;margin-top:10px"></div>
702637
+ </div>
702638
+ </section>
702639
+ </main>
702640
+
702641
+ <aside class="hud-panel hud-panel--slant hud-side-output">
702642
+ <div class="hud-panel__chrome" aria-hidden="true"></div>
702643
+ <header class="hud-panel__header">
702644
+ <div>
702645
+ <div class="kicker">Evidence Output</div>
702646
+ <h2 class="hud-title">Signal</h2>
702647
+ </div>
702648
+ </header>
702649
+ <div class="hud-panel__body">
702650
+ <div id="gen-analysis-summary" class="hud-result">
702651
+ <div class="hud-result-card"><strong>Ready</strong><div>Generate new media or select Audio Analysis / Video Analysis from the left rail.</div></div>
702652
+ </div>
701798
702653
  </div>
701799
- </div>
701800
- </div>
701801
-
701802
- <!-- Kind selector -->
701803
- <div id="gen-kind-row" style="display:flex;gap:6px;margin-bottom:10px">
701804
- <button class="gen-kind active" data-kind="image" onclick="genSelectKind('image')" style="flex:1;background:var(--color-brand);border:1px solid var(--color-border);color:var(--color-accent-fg);padding:6px;border-radius:4px;font-size:0.66rem;cursor:pointer;font-family:inherit">Image</button>
701805
- <button class="gen-kind" data-kind="video" onclick="genSelectKind('video')" style="flex:1;background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:6px;border-radius:4px;font-size:0.66rem;cursor:pointer;font-family:inherit">Video</button>
701806
- <button class="gen-kind" data-kind="audio" onclick="genSelectKind('audio')" style="flex:1;background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:6px;border-radius:4px;font-size:0.66rem;cursor:pointer;font-family:inherit">Audio</button>
701807
- <button class="gen-kind" data-kind="music" onclick="genSelectKind('music')" style="flex:1;background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:6px;border-radius:4px;font-size:0.66rem;cursor:pointer;font-family:inherit">Music</button>
701808
- </div>
701809
-
701810
- <!-- Model selector -->
701811
- <label style="font-size:0.62rem;color:var(--color-fg-subtle);display:block;margin-bottom:3px">Model</label>
701812
- <select id="gen-model" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:5px 8px;border-radius:4px;font-size:0.66rem;width:100%;margin-bottom:10px;font-family:inherit">
701813
- <option value="auto">auto (recommended)</option>
701814
- </select>
701815
-
701816
- <!-- Prompt -->
701817
- <label style="font-size:0.62rem;color:var(--color-fg-subtle);display:block;margin-bottom:3px">Prompt</label>
701818
- <textarea id="gen-prompt" rows="3" placeholder="Describe what to generate…" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:6px 8px;border-radius:4px;font-size:0.7rem;width:100%;margin-bottom:10px;font-family:inherit;resize:vertical"></textarea>
701819
-
701820
- <!-- Optional params -->
701821
- <div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap">
701822
- <div style="flex:1;min-width:90px"><label style="font-size:0.6rem;color:var(--color-fg-subtle);display:block;margin-bottom:2px">Seed (optional)</label><input id="gen-seed" type="number" placeholder="random" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:4px 8px;border-radius:4px;font-size:0.64rem;width:100%;font-family:inherit"></div>
701823
- <div id="gen-steps-wrap" style="flex:1;min-width:90px"><label style="font-size:0.6rem;color:var(--color-fg-subtle);display:block;margin-bottom:2px">Steps (optional)</label><input id="gen-steps" type="number" placeholder="model default" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:4px 8px;border-radius:4px;font-size:0.64rem;width:100%;font-family:inherit"></div>
701824
- <div id="gen-duration-wrap" style="flex:1;min-width:90px;display:none"><label style="font-size:0.6rem;color:var(--color-fg-subtle);display:block;margin-bottom:2px">Duration s (optional)</label><input id="gen-duration" type="number" placeholder="model default" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:4px 8px;border-radius:4px;font-size:0.64rem;width:100%;font-family:inherit"></div>
702654
+ </aside>
701825
702655
  </div>
701826
-
701827
- <button id="gen-run-btn" onclick="generateMedia()" style="background:var(--color-brand);border:none;color:var(--color-accent-fg);padding:8px 16px;border-radius:4px;font-size:0.72rem;font-weight:600;cursor:pointer;font-family:inherit;width:100%">Generate</button>
701828
- <div id="gen-status" style="font-size:0.66rem;color:var(--color-fg-subtle);margin-top:8px;min-height:1em"></div>
701829
- <!-- Live generation progress (download / load / generate) via SSE -->
701830
- <div id="gen-progress-wrap" style="display:none;margin-top:8px">
701831
- <div style="font-size:0.62rem;color:var(--color-fg-muted)"><span id="gen-progress-stage">starting…</span> <span id="gen-progress-msg" style="color:var(--color-fg-faint)"></span></div>
701832
- <div style="margin-top:4px;height:8px;background:var(--color-bg);border:1px solid var(--color-border);border-radius:4px;overflow:hidden">
701833
- <div id="gen-progress-bar" style="height:100%;width:0%;background:var(--color-brand);transition:width 0.2s"></div>
701834
- </div>
701835
- </div>
701836
-
701837
- <!-- Gallery -->
701838
- <div style="display:flex;justify-content:space-between;align-items:center;margin:18px 0 8px">
701839
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin:0">Gallery</h3>
701840
- <button onclick="loadGenerateGallery()" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg-subtle);padding:3px 10px;border-radius:3px;font-size:0.62rem;cursor:pointer;font-family:inherit">↻ Refresh</button>
701841
- </div>
701842
- <!-- Gallery filter bar -->
701843
- <div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:8px;font-size:0.6rem">
701844
- <div id="gen-filter-kinds" style="display:flex;gap:4px">
701845
- <button class="gen-filter-kind active" data-fkind="" onclick="setGalleryKindFilter('')" style="background:var(--color-brand);border:1px solid var(--color-border);color:var(--color-accent-fg);padding:3px 8px;border-radius:3px;cursor:pointer;font-family:inherit">All</button>
701846
- <button class="gen-filter-kind" data-fkind="image" onclick="setGalleryKindFilter('image')" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:3px 8px;border-radius:3px;cursor:pointer;font-family:inherit">Image</button>
701847
- <button class="gen-filter-kind" data-fkind="video" onclick="setGalleryKindFilter('video')" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:3px 8px;border-radius:3px;cursor:pointer;font-family:inherit">Video</button>
701848
- <button class="gen-filter-kind" data-fkind="audio" onclick="setGalleryKindFilter('audio')" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:3px 8px;border-radius:3px;cursor:pointer;font-family:inherit">Audio</button>
701849
- <button class="gen-filter-kind" data-fkind="music" onclick="setGalleryKindFilter('music')" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:3px 8px;border-radius:3px;cursor:pointer;font-family:inherit">Music</button>
701850
- </div>
701851
- <input id="gen-filter-q" type="text" placeholder="search prompt…" oninput="scheduleGalleryReload()" style="flex:1;min-width:120px;background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:3px 8px;border-radius:3px;font-size:0.6rem;font-family:inherit">
701852
- <input id="gen-filter-model" type="text" placeholder="model…" oninput="scheduleGalleryReload()" style="width:100px;background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:3px 8px;border-radius:3px;font-size:0.6rem;font-family:inherit">
701853
- <label style="color:var(--color-fg-faint)">from <input id="gen-filter-from" type="date" onchange="scheduleGalleryReload()" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:2px 4px;border-radius:3px;font-size:0.6rem;font-family:inherit"></label>
701854
- <label style="color:var(--color-fg-faint)">to <input id="gen-filter-to" type="date" onchange="scheduleGalleryReload()" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:2px 4px;border-radius:3px;font-size:0.6rem;font-family:inherit"></label>
701855
- <select id="gen-filter-sort" onchange="scheduleGalleryReload()" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:2px 6px;border-radius:3px;font-size:0.6rem;font-family:inherit">
701856
- <option value="new">Newest</option><option value="old">Oldest</option><option value="big">Largest</option><option value="small">Smallest</option>
701857
- </select>
701858
- <span id="gen-gallery-count" style="color:var(--color-fg-faint)"></span>
701859
- </div>
701860
- <div id="gen-gallery" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px;font-size:0.62rem"></div>
701861
- <div id="gen-gallery-more" style="text-align:center;margin-top:10px"></div>
701862
702656
  </div>
701863
702657
  <!-- Media detail modal -->
701864
702658
  <div id="gen-detail-modal" class="omnius-modal" style="display:none" onclick="if(event.target===this)closeGenDetail()">
@@ -704667,6 +705461,7 @@ function routePathForTab(tab) {
704667
705461
  chat: '/chat',
704668
705462
  agent: '/agent',
704669
705463
  voice: '/voice',
705464
+ generate: '/generate',
704670
705465
  projects: '/projects',
704671
705466
  jobs: '/dashboard',
704672
705467
  activity: '/activity',
@@ -704681,6 +705476,7 @@ function tabForRoutePath(pathname) {
704681
705476
  '/chat': 'chat',
704682
705477
  '/agent': 'agent',
704683
705478
  '/voice': 'voice',
705479
+ '/generate': 'generate',
704684
705480
  '/projects': 'projects',
704685
705481
  '/dashboard': 'jobs',
704686
705482
  '/jobs': 'jobs',
@@ -704756,13 +705552,55 @@ window.switchTab = switchTab;
704756
705552
  // /v1/media/file as authenticated blobs.
704757
705553
  // ════════════════════════════════════════════════════════════
704758
705554
  let genKind = 'image';
705555
+ let genMode = 'image';
704759
705556
  let genModelsCache = null;
704760
705557
 
705558
+ function setHudControlLabel(el, text) {
705559
+ if (!el) return;
705560
+ const label = el.querySelector && el.querySelector('.hud-btn__label,.hud-tab__label');
705561
+ if (label) label.textContent = text;
705562
+ else el.textContent = text;
705563
+ }
705564
+
705565
+ function genSelectMode(mode) {
705566
+ genMode = mode || 'image';
705567
+ const analysis = genMode === 'audio-analysis' || genMode === 'video-analysis' || genMode === 'live-analysis';
705568
+ document.querySelectorAll('#gen-mode-rail .gen-mode-tab').forEach(b => {
705569
+ const active = b.getAttribute('data-gen-mode') === genMode;
705570
+ b.classList.toggle('active', active);
705571
+ b.setAttribute('aria-selected', active ? 'true' : 'false');
705572
+ });
705573
+ const createPanel = document.getElementById('gen-create-panel');
705574
+ const analysisPanel = document.getElementById('gen-analysis-panel');
705575
+ if (createPanel) createPanel.style.display = analysis ? 'none' : 'block';
705576
+ if (analysisPanel) analysisPanel.style.display = analysis ? 'block' : 'none';
705577
+ const modeTitle = document.getElementById('gen-mode-title');
705578
+ const avTitle = document.getElementById('av-mode-title');
705579
+ const avBtn = document.getElementById('av-run-btn');
705580
+ if (!analysis) {
705581
+ genSelectKind(genMode);
705582
+ if (modeTitle) modeTitle.textContent = genMode.charAt(0).toUpperCase() + genMode.slice(1) + ' Generator';
705583
+ return;
705584
+ }
705585
+ const label = genMode === 'audio-analysis'
705586
+ ? 'Audio Analysis'
705587
+ : genMode === 'video-analysis'
705588
+ ? 'Video Analysis'
705589
+ : 'Live AV Analysis';
705590
+ if (avTitle) avTitle.textContent = label;
705591
+ setHudControlLabel(avBtn, genMode === 'live-analysis' ? 'Analyze AV' : 'Analyze');
705592
+ const summary = document.getElementById('gen-analysis-summary');
705593
+ if (summary && !summary.getAttribute('data-has-result')) {
705594
+ summary.innerHTML = '<div class="hud-result-card"><strong>' + escapeHtml(label) + '</strong><div>Provide a local media path or file URI, then run analysis. Results will show adapter status, source metadata, answer, entities, events, relations, and shots.</div></div>';
705595
+ }
705596
+ }
705597
+
704761
705598
  function genSelectKind(kind) {
704762
705599
  genKind = kind;
704763
705600
  document.querySelectorAll('#gen-kind-row .gen-kind').forEach(b => {
704764
705601
  const active = b.getAttribute('data-kind') === kind;
704765
705602
  b.classList.toggle('active', active);
705603
+ b.setAttribute('aria-selected', active ? 'true' : 'false');
704766
705604
  b.style.background = active ? 'var(--color-brand)' : 'var(--color-bg)';
704767
705605
  b.style.color = active ? 'var(--color-accent-fg)' : 'var(--color-fg)';
704768
705606
  });
@@ -704938,7 +705776,7 @@ async function generateMedia() {
704938
705776
  if (seed !== '') body.seed = Number(seed);
704939
705777
  if (steps !== '' && (genKind === 'image' || genKind === 'video')) body.steps = Number(steps);
704940
705778
  if (duration !== '' && (genKind === 'audio' || genKind === 'music')) body.duration = Number(duration);
704941
- if (btn) { btn.disabled = true; btn.textContent = 'Generating'; }
705779
+ if (btn) { btn.disabled = true; setHudControlLabel(btn, 'Generating'); }
704942
705780
  if (statusEl) statusEl.textContent = 'Generating ' + genKind + ' — this can take a while on first run (model download + warmup).';
704943
705781
  // Reveal the live progress bar; SSE media.generation_progress fills it in.
704944
705782
  const pw = document.getElementById('gen-progress-wrap'); if (pw) pw.style.display = 'block';
@@ -704956,11 +705794,103 @@ async function generateMedia() {
704956
705794
  } catch (e) {
704957
705795
  if (statusEl) statusEl.textContent = 'Error: ' + (e && e.message || e);
704958
705796
  } finally {
704959
- if (btn) { btn.disabled = false; btn.textContent = 'Generate'; }
705797
+ if (btn) { btn.disabled = false; setHudControlLabel(btn, 'Generate'); }
704960
705798
  if (pw) setTimeout(() => { pw.style.display = 'none'; if (pb) pb.style.width = '0%'; }, 1000);
704961
705799
  }
704962
705800
  }
704963
705801
 
705802
+ function avArraySummary(items, label, limit) {
705803
+ const arr = Array.isArray(items) ? items.slice(0, limit || 8) : [];
705804
+ if (!arr.length) return '<div class="hud-result-card"><strong>' + label + '</strong><div>none reported</div></div>';
705805
+ return '<div class="hud-result-card"><strong>' + label + '</strong><pre style="white-space:pre-wrap;margin:0;background:transparent;border:0;padding:0;color:var(--hud-ink-soft)">' +
705806
+ escapeHtml(JSON.stringify(arr, null, 2)) +
705807
+ '</pre></div>';
705808
+ }
705809
+
705810
+ function renderAvAnalysis(data, mode) {
705811
+ const result = document.getElementById('av-result');
705812
+ const summary = document.getElementById('gen-analysis-summary');
705813
+ if (!result) return;
705814
+ const d = data || {};
705815
+ const source = d.source || {};
705816
+ const adapters = d.adapters || {};
705817
+ const adapterHtml = Object.keys(adapters).length
705818
+ ? '<div class="hud-chip-row">' + Object.keys(adapters).map(k => {
705819
+ const v = adapters[k];
705820
+ const cls = v === 'live' ? 'live' : 'mock';
705821
+ return '<span class="hud-chip ' + cls + '">' + escapeHtml(k + ': ' + v) + '</span>';
705822
+ }).join('') + '</div>'
705823
+ : '<div class="hud-status-line">adapter status unavailable</div>';
705824
+ const answerRaw = d.answer;
705825
+ const answer = typeof answerRaw === 'string'
705826
+ ? answerRaw
705827
+ : answerRaw && typeof answerRaw.answer === 'string'
705828
+ ? answerRaw.answer
705829
+ : answerRaw
705830
+ ? JSON.stringify(answerRaw, null, 2)
705831
+ : 'No composed answer returned.';
705832
+ const sourceBits = [
705833
+ source.path || source.uri || source.mediaUri || '',
705834
+ source.durationSec != null ? String(Math.round(source.durationSec * 10) / 10) + 's' : '',
705835
+ source.audio ? 'audio' : '',
705836
+ source.video ? 'video' : '',
705837
+ d.stack ? 'stack ' + d.stack : '',
705838
+ d.modelsLive ? 'live models' : 'mock/partial models',
705839
+ ].filter(Boolean).join(' | ');
705840
+ const cards =
705841
+ '<div class="hud-result-card"><strong>Answer</strong><div style="white-space:pre-wrap">' + escapeHtml(answer) + '</div></div>' +
705842
+ '<div class="hud-result-card"><strong>Source</strong><div>' + escapeHtml(sourceBits || 'unknown source') + '</div></div>' +
705843
+ '<div class="hud-result-card"><strong>Adapters</strong>' + adapterHtml + '</div>' +
705844
+ (d.note ? '<div class="hud-result-card"><strong>Quality Note</strong><div>' + escapeHtml(d.note) + '</div></div>' : '') +
705845
+ avArraySummary(d.entities, 'Entities', 10) +
705846
+ avArraySummary(d.events, 'Events', 10) +
705847
+ avArraySummary(d.relations, 'Relations', 10) +
705848
+ avArraySummary(d.shots, 'Shots', 8);
705849
+ result.innerHTML = cards;
705850
+ if (summary) {
705851
+ summary.setAttribute('data-has-result', '1');
705852
+ summary.innerHTML =
705853
+ '<div class="hud-result-card"><strong>' + escapeHtml((mode || genMode).replace(/-/g, ' ')) + '</strong>' +
705854
+ '<div>' + escapeHtml(sourceBits || 'analysis complete') + '</div></div>' +
705855
+ '<div class="hud-result-card"><strong>Answer</strong><div style="white-space:pre-wrap">' + escapeHtml(String(answer).slice(0, 700)) + '</div></div>' +
705856
+ '<div class="hud-result-card"><strong>Adapters</strong>' + adapterHtml + '</div>';
705857
+ }
705858
+ }
705859
+
705860
+ async function analyzeAvMedia(modeOverride) {
705861
+ if (modeOverride) genSelectMode(modeOverride);
705862
+ const sourceEl = document.getElementById('av-source');
705863
+ const questionEl = document.getElementById('av-question');
705864
+ const statusEl = document.getElementById('av-status');
705865
+ const btn = document.getElementById('av-run-btn');
705866
+ const source = (sourceEl && sourceEl.value || '').trim();
705867
+ const question = (questionEl && questionEl.value || '').trim();
705868
+ if (!source) {
705869
+ if (statusEl) statusEl.textContent = 'Provide a local media path or file URI first.';
705870
+ return;
705871
+ }
705872
+ if (statusEl) statusEl.textContent = 'Analyzing media through /v1/media/av/analyze...';
705873
+ if (btn) { btn.disabled = true; setHudControlLabel(btn, 'Analyzing'); }
705874
+ const body = source.indexOf('file://') === 0
705875
+ ? { mediaUri: source, question }
705876
+ : { path: source, question };
705877
+ try {
705878
+ const r = await fetch('/v1/media/av/analyze', { method: 'POST', headers: headers(), body: JSON.stringify(body) });
705879
+ const j = await r.json().catch(() => ({}));
705880
+ if (!r.ok) {
705881
+ if (statusEl) statusEl.textContent = 'Analysis failed: ' + (j.detail || j.title || ('HTTP ' + r.status));
705882
+ return;
705883
+ }
705884
+ if (statusEl) statusEl.textContent = 'Analysis complete: ' + ((j.data && j.data.episodeId) || 'episode captured');
705885
+ renderAvAnalysis(j.data || {}, genMode);
705886
+ } catch (e) {
705887
+ if (statusEl) statusEl.textContent = 'Analysis error: ' + (e && e.message || e);
705888
+ } finally {
705889
+ if (btn) { btn.disabled = false; setHudControlLabel(btn, genMode === 'live-analysis' ? 'Analyze AV' : 'Analyze'); }
705890
+ }
705891
+ }
705892
+ window.analyzeAvMedia = analyzeAvMedia;
705893
+
704964
705894
  // ─── Gallery: filters, metadata cards, pagination, detail modal ───────────────
704965
705895
  let genGallery = { kind: '', q: '', model: '', from: '', to: '', sort: 'new', offset: 0, items: [] };
704966
705896
  let _genGalleryTimer = null;
@@ -705083,7 +706013,7 @@ function copyGenPrompt(idx) { const it = _genGalleryDetail[idx]; if (it && it.pr
705083
706013
  function reuseGenPrompt(idx) {
705084
706014
  const it = _genGalleryDetail[idx]; if (!it) return;
705085
706015
  if (it.prompt) { const p = document.getElementById('gen-prompt'); if (p) p.value = it.prompt; }
705086
- if (it.kind) genSelectKind(it.kind);
706016
+ if (it.kind) genSelectMode(it.kind);
705087
706017
  closeGenDetail();
705088
706018
  document.getElementById('gen-prompt')?.scrollIntoView({ behavior: 'smooth' });
705089
706019
  }
@@ -705142,13 +706072,14 @@ function loadGenerateTab() {
705142
706072
  }
705143
706073
  }
705144
706074
  } catch {}
705145
- genSelectKind(genKind);
706075
+ genSelectMode(genKind || 'image');
705146
706076
  if (!genModelsCache) loadGenerateModels();
705147
706077
  genStoreInfo();
705148
706078
  loadGenerateGallery(true);
705149
706079
  _genTabLoaded = true;
705150
706080
  }
705151
706081
  window.genSelectKind = genSelectKind;
706082
+ window.genSelectMode = genSelectMode;
705152
706083
  window.generateMedia = generateMedia;
705153
706084
  window.loadGenerateGallery = loadGenerateGallery;
705154
706085
  window.addEventListener('popstate', () => {
@@ -717198,6 +718129,7 @@ async function handleRequest(req3, res, ollamaUrl, verbose, runtimeDefaults = {}
717198
718129
  "/chat",
717199
718130
  "/agent",
717200
718131
  "/voice",
718132
+ "/generate",
717201
718133
  "/projects",
717202
718134
  "/dashboard",
717203
718135
  "/jobs",