@camstack/addon-pipeline 1.1.59 → 1.1.60

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.
Files changed (30) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +41 -3
  4. package/dist/detection-pipeline/index.mjs +41 -3
  5. package/dist/{dist-DKeNH_5z.mjs → dist-CcqqznPf.mjs} +68 -2
  6. package/dist/{dist-B1u-QN05.js → dist-DynD1EC-.js} +73 -1
  7. package/dist/motion-wasm/index.js +1 -1
  8. package/dist/motion-wasm/index.mjs +1 -1
  9. package/dist/pipeline-runner/index.js +4 -4
  10. package/dist/pipeline-runner/index.mjs +4 -4
  11. package/dist/recorder/index.js +48 -20
  12. package/dist/recorder/index.mjs +48 -20
  13. package/dist/{sheet-geometry-BN80zpCa.js → sheet-geometry-BR-ip0CP.js} +2 -2
  14. package/dist/{sheet-geometry-Bk3fR4s7.mjs → sheet-geometry-DcpuGTCm.mjs} +2 -2
  15. package/dist/{step-definitions-DUB27V8Y.mjs → step-definitions-CvwB_Otk.mjs} +1 -1
  16. package/dist/{step-definitions-CPch37vh.js → step-definitions-DhhwklAL.js} +1 -1
  17. package/dist/stream-broker/_stub.js +1 -1
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DdkfKDgw.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B07QYb7Q.mjs} +3 -3
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DYQ6RHcb.mjs +26 -0
  20. package/dist/stream-broker/{hostInit-BLn2Pv8Q.mjs → hostInit-C02YjaoP.mjs} +3 -3
  21. package/dist/stream-broker/index.js +1 -1
  22. package/dist/stream-broker/index.mjs +1 -1
  23. package/dist/stream-broker/remoteEntry.js +1 -1
  24. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-B0_nx15B.js → MaskShapeCanvas-DI4BY7W2-ColkZGA1.js} +1 -1
  25. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-B8uM-VeO.js → MotionZonesSettings-NcxxQN8r-BIzNJmu0.js} +1 -1
  26. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DMOs4Hj9.js → PrivacyMaskSettings-APgPLF7p-BHuYOg7Q.js} +1 -1
  27. package/embed-dist/assets/{index-W9-H5LTj.js → index-n1_ZSmy0.js} +10 -10
  28. package/embed-dist/index.html +1 -1
  29. package/package.json +1 -1
  30. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D3h_W9uu.mjs +0 -26
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-B1u-QN05.js");
6
+ const require_dist = require("../dist-DynD1EC-.js");
7
7
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
8
8
  const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Cp_GTKTw.js");
9
9
  let node_fs = require("node:fs");
@@ -1,5 +1,5 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { C as mapAudioLabelToMacro, H as hydrateSchema, I as errMsg, L as BaseAddon, h as audioAnalyzerCapability, l as HF_BASE_URL, m as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG } from "../dist-DKeNH_5z.mjs";
2
+ import { C as mapAudioLabelToMacro, L as errMsg, R as BaseAddon, U as hydrateSchema, h as audioAnalyzerCapability, l as HF_BASE_URL, m as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG } from "../dist-CcqqznPf.mjs";
3
3
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
4
4
  import { r as downloadFile } from "../model-download-service-Cp9f4dk6-0wh7OK3s.mjs";
5
5
  import * as fs from "node:fs";
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-B1u-QN05.js");
7
- const require_step_definitions = require("../step-definitions-CPch37vh.js");
6
+ const require_dist = require("../dist-DynD1EC-.js");
7
+ const require_step_definitions = require("../step-definitions-DhhwklAL.js");
8
8
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
9
9
  const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Cp_GTKTw.js");
10
10
  let node_child_process = require("node:child_process");
@@ -3690,6 +3690,15 @@ var ONNX_FLOOR = {
3690
3690
  */
3691
3691
  var PROBE_READY_TIMEOUT_MS = 12e4;
3692
3692
  /**
3693
+ * Bound for the direct `platformProbe.getCapabilities` reconcile RPC issued
3694
+ * when the readiness gate times out (D8: readiness events are telemetry —
3695
+ * lossy — so a timed-out registry wait is reconciled against the provider
3696
+ * itself before conceding to the conservative floor). `getCapabilities`
3697
+ * resolves only once the probe is complete, so an answer IS readiness; the
3698
+ * race bound keeps a wedged transport from wedging `setApi`.
3699
+ */
3700
+ var PROBE_DIRECT_RPC_TIMEOUT_MS = 6e4;
3701
+ /**
3693
3702
  * Build the onnx-cpu floor pick using `pickBestRuntime` with a null hardware
3694
3703
  * env. Used wherever the old `detectBestEngine()` sync probe fell back — the
3695
3704
  * result is identical (onnx / cpu) but is now derived through the shared rules
@@ -3922,7 +3931,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3922
3931
  nodeId
3923
3932
  }, { timeoutMs: PROBE_READY_TIMEOUT_MS });
3924
3933
  } catch (err) {
3925
- this.log.warn("platform-probe readiness gate timed out conservative engine pick (floor is not persisted)", { meta: {
3934
+ if (await this.probePlatformReadinessDirect()) this.log.info("platform-probe readiness reconciled via direct probe RPC (ready event lost)", { meta: {
3935
+ nodeId,
3936
+ timeoutMs: PROBE_READY_TIMEOUT_MS
3937
+ } });
3938
+ else this.log.warn("platform-probe readiness gate timed out — conservative engine pick (floor is not persisted)", { meta: {
3926
3939
  nodeId,
3927
3940
  timeoutMs: PROBE_READY_TIMEOUT_MS,
3928
3941
  error: require_dist.errMsg(err)
@@ -3933,6 +3946,31 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3933
3946
  this.startProvisioningForCurrentEngine();
3934
3947
  }
3935
3948
  /**
3949
+ * Authoritative platform-probe reconcile: query the probe provider
3950
+ * DIRECTLY over the cap plane. The RPC resolves only once the async
3951
+ * probe has completed (`getCapabilities` awaits the probe promise), so
3952
+ * a successful answer proves readiness independent of the (lossy)
3953
+ * `system.ready-state` event stream. Mirrors the local-node branch of
3954
+ * `resolveProbeHardware` — an unpinned `getCapabilities` routes to this
3955
+ * node's own in-process provider (agent parent / hub). Bounded by
3956
+ * {@link PROBE_DIRECT_RPC_TIMEOUT_MS}; `false` on any failure.
3957
+ */
3958
+ async probePlatformReadinessDirect() {
3959
+ const api = this.addonCtx?.api;
3960
+ if (!api) return false;
3961
+ let timer;
3962
+ try {
3963
+ const bound = new Promise((resolve) => {
3964
+ timer = setTimeout(() => resolve(null), PROBE_DIRECT_RPC_TIMEOUT_MS);
3965
+ });
3966
+ return await Promise.race([api.platformProbe.getCapabilities.query(), bound]) !== null;
3967
+ } catch {
3968
+ return false;
3969
+ } finally {
3970
+ if (timer !== void 0) clearTimeout(timer);
3971
+ }
3972
+ }
3973
+ /**
3936
3974
  * Resolve the `'auto'` engine LIVE from this node's own kernel inference
3937
3975
  * resolver (`ctx.kernel.inferenceEngine`) — the runtime mirror of
3938
3976
  * `ctx.kernel.hwaccel`. Each node (hub + forked worker + remote agent)
@@ -1,6 +1,6 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { D as nodePin, H as hydrateSchema, I as errMsg, K as parseJsonUnknown, L as BaseAddon, O as pipelineExecutorCapability, P as supportedRuntimes$1, V as createEvent, Y as sleep, b as detectionPipelineCapability, f as YAMNET_TO_MACRO, j as runtimeDevices$1, st as EventCategory, t as APPLE_SA_TO_MACRO, v as defaultDeviceFor$1, x as evaluateZoneRules } from "../dist-DKeNH_5z.mjs";
3
- import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-DUB27V8Y.mjs";
2
+ import { D as nodePin, F as supportedRuntimes$1, H as createEvent, L as errMsg, M as runtimeDevices$1, O as pipelineExecutorCapability, R as BaseAddon, U as hydrateSchema, X as sleep, b as detectionPipelineCapability, ct as EventCategory, f as YAMNET_TO_MACRO, q as parseJsonUnknown, t as APPLE_SA_TO_MACRO, v as defaultDeviceFor$1, x as evaluateZoneRules } from "../dist-CcqqznPf.mjs";
3
+ import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-CvwB_Otk.mjs";
4
4
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
5
5
  import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-Cp9f4dk6-0wh7OK3s.mjs";
6
6
  import { spawn } from "node:child_process";
@@ -3682,6 +3682,15 @@ var ONNX_FLOOR = {
3682
3682
  */
3683
3683
  var PROBE_READY_TIMEOUT_MS = 12e4;
3684
3684
  /**
3685
+ * Bound for the direct `platformProbe.getCapabilities` reconcile RPC issued
3686
+ * when the readiness gate times out (D8: readiness events are telemetry —
3687
+ * lossy — so a timed-out registry wait is reconciled against the provider
3688
+ * itself before conceding to the conservative floor). `getCapabilities`
3689
+ * resolves only once the probe is complete, so an answer IS readiness; the
3690
+ * race bound keeps a wedged transport from wedging `setApi`.
3691
+ */
3692
+ var PROBE_DIRECT_RPC_TIMEOUT_MS = 6e4;
3693
+ /**
3685
3694
  * Build the onnx-cpu floor pick using `pickBestRuntime` with a null hardware
3686
3695
  * env. Used wherever the old `detectBestEngine()` sync probe fell back — the
3687
3696
  * result is identical (onnx / cpu) but is now derived through the shared rules
@@ -3914,7 +3923,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3914
3923
  nodeId
3915
3924
  }, { timeoutMs: PROBE_READY_TIMEOUT_MS });
3916
3925
  } catch (err) {
3917
- this.log.warn("platform-probe readiness gate timed out conservative engine pick (floor is not persisted)", { meta: {
3926
+ if (await this.probePlatformReadinessDirect()) this.log.info("platform-probe readiness reconciled via direct probe RPC (ready event lost)", { meta: {
3927
+ nodeId,
3928
+ timeoutMs: PROBE_READY_TIMEOUT_MS
3929
+ } });
3930
+ else this.log.warn("platform-probe readiness gate timed out — conservative engine pick (floor is not persisted)", { meta: {
3918
3931
  nodeId,
3919
3932
  timeoutMs: PROBE_READY_TIMEOUT_MS,
3920
3933
  error: errMsg(err)
@@ -3925,6 +3938,31 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3925
3938
  this.startProvisioningForCurrentEngine();
3926
3939
  }
3927
3940
  /**
3941
+ * Authoritative platform-probe reconcile: query the probe provider
3942
+ * DIRECTLY over the cap plane. The RPC resolves only once the async
3943
+ * probe has completed (`getCapabilities` awaits the probe promise), so
3944
+ * a successful answer proves readiness independent of the (lossy)
3945
+ * `system.ready-state` event stream. Mirrors the local-node branch of
3946
+ * `resolveProbeHardware` — an unpinned `getCapabilities` routes to this
3947
+ * node's own in-process provider (agent parent / hub). Bounded by
3948
+ * {@link PROBE_DIRECT_RPC_TIMEOUT_MS}; `false` on any failure.
3949
+ */
3950
+ async probePlatformReadinessDirect() {
3951
+ const api = this.addonCtx?.api;
3952
+ if (!api) return false;
3953
+ let timer;
3954
+ try {
3955
+ const bound = new Promise((resolve) => {
3956
+ timer = setTimeout(() => resolve(null), PROBE_DIRECT_RPC_TIMEOUT_MS);
3957
+ });
3958
+ return await Promise.race([api.platformProbe.getCapabilities.query(), bound]) !== null;
3959
+ } catch {
3960
+ return false;
3961
+ } finally {
3962
+ if (timer !== void 0) clearTimeout(timer);
3963
+ }
3964
+ }
3965
+ /**
3928
3966
  * Resolve the `'auto'` engine LIVE from this node's own kernel inference
3929
3967
  * resolver (`ctx.kernel.inferenceEngine`) — the runtime mirror of
3930
3968
  * `ctx.kernel.hwaccel`. Each node (hub + forked worker + remote agent)
@@ -7379,6 +7379,65 @@ var RecordingRetentionSchema = object({
7379
7379
  maxSizeGb: number().min(0).optional()
7380
7380
  });
7381
7381
  /**
7382
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
7383
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
7384
+ * previews at. Five graduated steps; absent on a config = `standard` (the
7385
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
7386
+ *
7387
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
7388
+ * Each window's index sidecar carries its own tile dims, so a camera whose
7389
+ * preset changed over time renders every historical window at the dims it was
7390
+ * written with.
7391
+ */
7392
+ var ScrubThumbnailPresetSchema = _enum([
7393
+ "minimal",
7394
+ "low",
7395
+ "standard",
7396
+ "high",
7397
+ "max"
7398
+ ]);
7399
+ /**
7400
+ * Preset → tile geometry + JPEG quality. Every step is 16:9; the graduated
7401
+ * ladder trades sheet size (and remote fetch cost) for scrub crispness.
7402
+ * `standard` matches the shipped `sheet-geometry`/`sheet-composer` defaults
7403
+ * (480×270 q72), so an unset config is byte-identical to today. The map is
7404
+ * strictly monotone in resolution AND quality across the ordered steps.
7405
+ */
7406
+ var SCRUB_THUMBNAIL_PRESETS = {
7407
+ minimal: {
7408
+ tileWidth: 240,
7409
+ tileHeight: 135,
7410
+ quality: 55
7411
+ },
7412
+ low: {
7413
+ tileWidth: 320,
7414
+ tileHeight: 180,
7415
+ quality: 60
7416
+ },
7417
+ standard: {
7418
+ tileWidth: 480,
7419
+ tileHeight: 270,
7420
+ quality: 72
7421
+ },
7422
+ high: {
7423
+ tileWidth: 640,
7424
+ tileHeight: 360,
7425
+ quality: 80
7426
+ },
7427
+ max: {
7428
+ tileWidth: 960,
7429
+ tileHeight: 540,
7430
+ quality: 85
7431
+ }
7432
+ };
7433
+ /**
7434
+ * Resolve the geometry for a recording config's scrub-thumbnail preset,
7435
+ * defaulting to `standard` when unset. Pure — no I/O.
7436
+ */
7437
+ function resolveScrubThumbnailGeometry(preset) {
7438
+ return SCRUB_THUMBNAIL_PRESETS[preset ?? "standard"];
7439
+ }
7440
+ /**
7382
7441
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7383
7442
  *
7384
7443
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -7415,7 +7474,14 @@ var RecordingConfigSchema = object({
7415
7474
  * derived into bands once via `migrateConfigToBands`.
7416
7475
  */
7417
7476
  bands: array(RecordingBandSchema).optional(),
7418
- retention: RecordingRetentionSchema.optional()
7477
+ retention: RecordingRetentionSchema.optional(),
7478
+ /**
7479
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
7480
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
7481
+ * windows only — existing sheets are immutable, and each window's index
7482
+ * carries its own tile dims so mixed-preset history renders correctly.
7483
+ */
7484
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7419
7485
  });
7420
7486
  /** All seven weekdays (0 = Sunday … 6 = Saturday), used when a schedule omits a
7421
7487
  * weekday filter. */
@@ -26156,4 +26222,4 @@ function defaultDeviceFor(id) {
26156
26222
  return def(id).defaultDevice;
26157
26223
  }
26158
26224
  //#endregion
26159
- export { discriminatedUnion as $, recordingCapability as A, DeviceType as B, mapAudioLabelToMacro as C, nodePin as D, motionDetectionCapability as E, webrtcSessionCapability as F, makeSourceBrokerId as G, hydrateSchema as H, errMsg as I, selectAssignedProfileSlots as J, parseJsonUnknown as K, BaseAddon as L, storageEvictableCapability as M, streamBrokerCapability as N, pipelineExecutorCapability as O, supportedRuntimes as P, boolean as Q, CAM_PROFILE_ORDER as R, hfModelUrl as S, migrateConfigToBands as T, isEvent as U, createEvent as V, makeProfileBrokerId as W, _enum as X, sleep as Y, array as Z, customAction as _, COCO_TO_MACRO as a, string as at, detectionPipelineCapability as b, EncodeProfileSchema as c, RingBuffer as d, lazy as et, YAMNET_TO_MACRO as f, cameraStreamsCapability as g, audioAnalyzerCapability as h, COCO_80_LABELS as i, record as it, runtimeDevices as j, pipelineRunnerCapability as k, HF_BASE_URL as l, audioAnalysisCapability as m, AUDIO_BACKEND_CHOICES as n, number as nt, DEFAULT_AUDIO_ANALYZER_CONFIG as o, union as ot, addonWidgetsSourceCapability as p, parseProfileBrokerId as q, AUDIO_MACRO_LABELS as r, object as rt, EVENT_PAD_MS as s, EventCategory as st, APPLE_SA_TO_MACRO as t, literal as tt, RecordingConfigSchema as u, defaultDeviceFor as v, maskUrlCredentials as w, evaluateZoneRules as x, defineCustomActions as y, DeviceFeature as z };
26225
+ export { boolean as $, recordingCapability as A, DeviceFeature as B, mapAudioLabelToMacro as C, nodePin as D, motionDetectionCapability as E, supportedRuntimes as F, makeProfileBrokerId as G, createEvent as H, webrtcSessionCapability as I, parseProfileBrokerId as J, makeSourceBrokerId as K, errMsg as L, runtimeDevices as M, storageEvictableCapability as N, pipelineExecutorCapability as O, streamBrokerCapability as P, array as Q, BaseAddon as R, hfModelUrl as S, migrateConfigToBands as T, hydrateSchema as U, DeviceType as V, isEvent as W, sleep as X, selectAssignedProfileSlots as Y, _enum as Z, customAction as _, COCO_TO_MACRO as a, record as at, detectionPipelineCapability as b, EncodeProfileSchema as c, EventCategory as ct, RingBuffer as d, discriminatedUnion as et, YAMNET_TO_MACRO as f, cameraStreamsCapability as g, audioAnalyzerCapability as h, COCO_80_LABELS as i, object as it, resolveScrubThumbnailGeometry as j, pipelineRunnerCapability as k, HF_BASE_URL as l, audioAnalysisCapability as m, AUDIO_BACKEND_CHOICES as n, literal as nt, DEFAULT_AUDIO_ANALYZER_CONFIG as o, string as ot, addonWidgetsSourceCapability as p, parseJsonUnknown as q, AUDIO_MACRO_LABELS as r, number as rt, EVENT_PAD_MS as s, union as st, APPLE_SA_TO_MACRO as t, lazy as tt, RecordingConfigSchema as u, defaultDeviceFor as v, maskUrlCredentials as w, evaluateZoneRules as x, defineCustomActions as y, CAM_PROFILE_ORDER as z };
@@ -7379,6 +7379,65 @@ var RecordingRetentionSchema = object({
7379
7379
  maxSizeGb: number().min(0).optional()
7380
7380
  });
7381
7381
  /**
7382
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
7383
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
7384
+ * previews at. Five graduated steps; absent on a config = `standard` (the
7385
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
7386
+ *
7387
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
7388
+ * Each window's index sidecar carries its own tile dims, so a camera whose
7389
+ * preset changed over time renders every historical window at the dims it was
7390
+ * written with.
7391
+ */
7392
+ var ScrubThumbnailPresetSchema = _enum([
7393
+ "minimal",
7394
+ "low",
7395
+ "standard",
7396
+ "high",
7397
+ "max"
7398
+ ]);
7399
+ /**
7400
+ * Preset → tile geometry + JPEG quality. Every step is 16:9; the graduated
7401
+ * ladder trades sheet size (and remote fetch cost) for scrub crispness.
7402
+ * `standard` matches the shipped `sheet-geometry`/`sheet-composer` defaults
7403
+ * (480×270 q72), so an unset config is byte-identical to today. The map is
7404
+ * strictly monotone in resolution AND quality across the ordered steps.
7405
+ */
7406
+ var SCRUB_THUMBNAIL_PRESETS = {
7407
+ minimal: {
7408
+ tileWidth: 240,
7409
+ tileHeight: 135,
7410
+ quality: 55
7411
+ },
7412
+ low: {
7413
+ tileWidth: 320,
7414
+ tileHeight: 180,
7415
+ quality: 60
7416
+ },
7417
+ standard: {
7418
+ tileWidth: 480,
7419
+ tileHeight: 270,
7420
+ quality: 72
7421
+ },
7422
+ high: {
7423
+ tileWidth: 640,
7424
+ tileHeight: 360,
7425
+ quality: 80
7426
+ },
7427
+ max: {
7428
+ tileWidth: 960,
7429
+ tileHeight: 540,
7430
+ quality: 85
7431
+ }
7432
+ };
7433
+ /**
7434
+ * Resolve the geometry for a recording config's scrub-thumbnail preset,
7435
+ * defaulting to `standard` when unset. Pure — no I/O.
7436
+ */
7437
+ function resolveScrubThumbnailGeometry(preset) {
7438
+ return SCRUB_THUMBNAIL_PRESETS[preset ?? "standard"];
7439
+ }
7440
+ /**
7382
7441
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7383
7442
  *
7384
7443
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -7415,7 +7474,14 @@ var RecordingConfigSchema = object({
7415
7474
  * derived into bands once via `migrateConfigToBands`.
7416
7475
  */
7417
7476
  bands: array(RecordingBandSchema).optional(),
7418
- retention: RecordingRetentionSchema.optional()
7477
+ retention: RecordingRetentionSchema.optional(),
7478
+ /**
7479
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
7480
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
7481
+ * windows only — existing sheets are immutable, and each window's index
7482
+ * carries its own tile dims so mixed-preset history renders correctly.
7483
+ */
7484
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7419
7485
  });
7420
7486
  /** All seven weekdays (0 = Sunday … 6 = Saturday), used when a schedule omits a
7421
7487
  * weekday filter. */
@@ -26468,6 +26534,12 @@ Object.defineProperty(exports, "recordingCapability", {
26468
26534
  return recordingCapability;
26469
26535
  }
26470
26536
  });
26537
+ Object.defineProperty(exports, "resolveScrubThumbnailGeometry", {
26538
+ enumerable: true,
26539
+ get: function() {
26540
+ return resolveScrubThumbnailGeometry;
26541
+ }
26542
+ });
26471
26543
  Object.defineProperty(exports, "runtimeDevices", {
26472
26544
  enumerable: true,
26473
26545
  get: function() {
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-B1u-QN05.js");
5
+ const require_dist = require("../dist-DynD1EC-.js");
6
6
  let node_fs = require("node:fs");
7
7
  let node_path = require("node:path");
8
8
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -1,4 +1,4 @@
1
- import { B as DeviceType, E as motionDetectionCapability, H as hydrateSchema, L as BaseAddon, x as evaluateZoneRules } from "../dist-DKeNH_5z.mjs";
1
+ import { E as motionDetectionCapability, R as BaseAddon, U as hydrateSchema, V as DeviceType, x as evaluateZoneRules } from "../dist-CcqqznPf.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -3,11 +3,11 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-B1u-QN05.js");
6
+ const require_dist = require("../dist-DynD1EC-.js");
7
7
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
8
- const require_sheet_geometry = require("../sheet-geometry-BN80zpCa.js");
8
+ const require_sheet_geometry = require("../sheet-geometry-BR-ip0CP.js");
9
9
  const require_worker_protocol = require("../worker-protocol-PP4jKHHJ.js");
10
- const require_step_definitions = require("../step-definitions-CPch37vh.js");
10
+ const require_step_definitions = require("../step-definitions-DhhwklAL.js");
11
11
  let node_child_process = require("node:child_process");
12
12
  let node_url = require("node:url");
13
13
  let sharp = require("sharp");
@@ -3087,7 +3087,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3087
3087
  const data = frame.data;
3088
3088
  (async () => {
3089
3089
  try {
3090
- const targetW = Math.min(320, width);
3090
+ const targetW = Math.min(480, width);
3091
3091
  const image = (0, sharp.default)(data, { raw: {
3092
3092
  width,
3093
3093
  height,
@@ -1,8 +1,8 @@
1
- import { D as nodePin, I as errMsg, L as BaseAddon, Q as boolean, V as createEvent, X as _enum, Z as array, _ as customAction, at as string, et as lazy, k as pipelineRunnerCapability, nt as number, rt as object, st as EventCategory, y as defineCustomActions } from "../dist-DKeNH_5z.mjs";
1
+ import { $ as boolean, D as nodePin, H as createEvent, L as errMsg, Q as array, R as BaseAddon, Z as _enum, _ as customAction, ct as EventCategory, it as object, k as pipelineRunnerCapability, ot as string, rt as number, tt as lazy, y as defineCustomActions } from "../dist-CcqqznPf.mjs";
2
2
  import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
3
- import { r as resolveHubHostname } from "../sheet-geometry-Bk3fR4s7.mjs";
3
+ import { r as resolveHubHostname } from "../sheet-geometry-DcpuGTCm.mjs";
4
4
  import { t as isWorkerReply } from "../worker-protocol-CyVJTZEO.mjs";
5
- import { a as getStepDefinition } from "../step-definitions-DUB27V8Y.mjs";
5
+ import { a as getStepDefinition } from "../step-definitions-CvwB_Otk.mjs";
6
6
  import { fork } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import sharp from "sharp";
@@ -3081,7 +3081,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
3081
3081
  const data = frame.data;
3082
3082
  (async () => {
3083
3083
  try {
3084
- const targetW = Math.min(320, width);
3084
+ const targetW = Math.min(480, width);
3085
3085
  const image = sharp(data, { raw: {
3086
3086
  width,
3087
3087
  height,
@@ -1,6 +1,6 @@
1
1
  const require_chunk = require("../chunk-D6vf50IK.js");
2
- const require_dist = require("../dist-B1u-QN05.js");
3
- const require_sheet_geometry = require("../sheet-geometry-BN80zpCa.js");
2
+ const require_dist = require("../dist-DynD1EC-.js");
3
+ const require_sheet_geometry = require("../sheet-geometry-BR-ip0CP.js");
4
4
  const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Cp_GTKTw.js");
5
5
  let node_child_process = require("node:child_process");
6
6
  let sharp = require("sharp");
@@ -2075,8 +2075,8 @@ var resolveRecordingHubHostname = require_sheet_geometry.resolveHubHostname;
2075
2075
  */
2076
2076
  function buildThumbnailIndex(input) {
2077
2077
  if (input.samples.length === 0) throw new Error("buildThumbnailIndex: no samples");
2078
- const tileWidth = input.tileWidth ?? 320;
2079
- const tileHeight = input.tileHeight ?? 180;
2078
+ const tileWidth = input.tileWidth ?? 480;
2079
+ const tileHeight = input.tileHeight ?? 270;
2080
2080
  const maxCols = input.maxCols ?? 10;
2081
2081
  const ordered = [...input.samples].sort((a, b) => a.t - b.t);
2082
2082
  const geometry = require_sheet_geometry.computeSheetGeometry(ordered.length, {
@@ -2229,6 +2229,7 @@ var ThumbnailSink = class {
2229
2229
  }
2230
2230
  async doFlush(deviceId, win) {
2231
2231
  if (win.samples.length === 0) return;
2232
+ const preset = await this.deps.resolvePreset(deviceId);
2232
2233
  const ordered = [...win.samples].sort((a, b) => a.capturedAt - b.capturedAt);
2233
2234
  const built = buildThumbnailIndex({
2234
2235
  deviceId,
@@ -2236,8 +2237,8 @@ var ThumbnailSink = class {
2236
2237
  windowMs: this.deps.windowMs,
2237
2238
  intervalMs: this.deps.intervalMs,
2238
2239
  source: this.deps.source,
2239
- tileWidth: this.deps.tileWidth,
2240
- tileHeight: this.deps.tileHeight,
2240
+ tileWidth: preset.tileWidth,
2241
+ tileHeight: preset.tileHeight,
2241
2242
  maxCols: this.deps.maxCols,
2242
2243
  samples: ordered.map((s) => ({ t: s.capturedAt }))
2243
2244
  });
@@ -2245,7 +2246,8 @@ var ThumbnailSink = class {
2245
2246
  const sheetBytes = await this.deps.compose({
2246
2247
  tiles: ordered.map((s) => s.jpeg),
2247
2248
  placements,
2248
- geometry: built.geometry
2249
+ geometry: built.geometry,
2250
+ quality: preset.quality
2249
2251
  });
2250
2252
  await this.deps.persist({
2251
2253
  deviceId,
@@ -2272,7 +2274,7 @@ async function composeSheet(sharp, input) {
2272
2274
  const entries = (await Promise.all(tiles.map((t) => sharp(t).resize(geometry.tileWidth, geometry.tileHeight, {
2273
2275
  fit: "cover",
2274
2276
  position: "centre"
2275
- }).jpeg({ quality: input.quality ?? 60 }).toBuffer()))).map((buf, i) => {
2277
+ }).jpeg({ quality: input.quality ?? 72 }).toBuffer()))).map((buf, i) => {
2276
2278
  const p = placements[i];
2277
2279
  return {
2278
2280
  input: buf,
@@ -2289,7 +2291,7 @@ async function composeSheet(sharp, input) {
2289
2291
  g: 0,
2290
2292
  b: 0
2291
2293
  }
2292
- } }).composite(entries).jpeg({ quality: input.quality ?? 60 }).toBuffer();
2294
+ } }).composite(entries).jpeg({ quality: input.quality ?? 72 }).toBuffer();
2293
2295
  }
2294
2296
  //#endregion
2295
2297
  //#region src/thumbnails/thumbnail-persister.ts
@@ -2483,6 +2485,22 @@ function planBackfill(input) {
2483
2485
  //#endregion
2484
2486
  //#region src/thumbnails/backfill-ffmpeg.ts
2485
2487
  /**
2488
+ * Map a JPEG quality (higher = better; the scrub presets span ~55..85) onto
2489
+ * ffmpeg's INVERSE `-q:v` scale (2 = best .. 31 = worst). Inverse-linear over
2490
+ * the full JPEG 1..100 domain, monotonic-decreasing (a higher JPEG quality
2491
+ * yields a lower/better ffmpeg q), clamped to [2, 31] for any input.
2492
+ *
2493
+ * These extracted keyframes are INTERMEDIATE — `composeSheet` re-encodes them
2494
+ * into the sheet at the preset's final JPEG quality — so this only governs the
2495
+ * extract's crispness. Anchoring the extract to the same preset keeps a
2496
+ * high-preset camera's backfill from being the bottleneck while a low-preset
2497
+ * camera spends fewer bytes on throwaway intermediates.
2498
+ */
2499
+ function jpegQualityToFfmpegQ(jpegQuality) {
2500
+ const q = Math.round(31 - (Math.min(100, Math.max(1, jpegQuality)) - 1) / 99 * 29);
2501
+ return Math.min(31, Math.max(2, q));
2502
+ }
2503
+ /**
2486
2504
  * ffmpeg args to extract keyframes from one segment as scaled JPEGs. `-skip_frame
2487
2505
  * nokey` + `-vsync 0` (a.k.a. passthrough) keeps only I-frames without
2488
2506
  * duplicating; `-an` drops audio. The caller resolves timestamps from the
@@ -2579,16 +2597,16 @@ var ThumbnailService = class {
2579
2597
  this.sink = new ThumbnailSink({
2580
2598
  windowMs: deps.config.windowMs,
2581
2599
  intervalMs: deps.config.intervalMs,
2582
- tileWidth: 320,
2583
- tileHeight: 180,
2584
2600
  maxCols: 10,
2601
+ resolvePreset: (deviceId) => deps.resolvePreset(deviceId),
2585
2602
  maxTilesPerWindow: MAX_TILES_PER_WINDOW,
2586
2603
  idleGraceMs: IDLE_GRACE_MS,
2587
2604
  source: "sampled",
2588
2605
  compose: (req) => composeSheet(SHARP, {
2589
2606
  tiles: [...req.tiles],
2590
2607
  placements: req.placements,
2591
- geometry: req.geometry
2608
+ geometry: req.geometry,
2609
+ quality: req.quality
2592
2610
  }),
2593
2611
  persist: (req) => this.persist(req),
2594
2612
  logger: deps.logger.child("thumbnail-sink")
@@ -2854,13 +2872,14 @@ var ThumbnailService = class {
2854
2872
  /** Fill ONE window: extract keyframes from its covering low segments, compose
2855
2873
  * a sheet, persist with `source:'backfill'`. */
2856
2874
  async backfillWindow(deviceId, windowStartMs, segments, root) {
2875
+ const preset = await this.deps.resolvePreset(deviceId);
2857
2876
  const workDir = await node_fs.promises.mkdtemp(node_path.default.join((0, node_os.tmpdir)(), `csthumb-${deviceId}-`));
2858
2877
  try {
2859
2878
  const samples = [];
2860
2879
  for (const seg of segments) {
2861
2880
  const absSeg = this.resolveSegmentAbs(deviceId, seg.path);
2862
2881
  if (!absSeg) continue;
2863
- const jpegs = await this.extractKeyframes(absSeg, workDir);
2882
+ const jpegs = await this.extractKeyframes(absSeg, workDir, preset);
2864
2883
  const n = jpegs.length;
2865
2884
  for (let i = 0; i < n; i += 1) {
2866
2885
  const t = seg.startMs + Math.round(seg.durMs * i / Math.max(1, n));
@@ -2879,21 +2898,22 @@ var ThumbnailService = class {
2879
2898
  windowMs: this.deps.config.windowMs,
2880
2899
  intervalMs: this.deps.config.intervalMs,
2881
2900
  source: "backfill",
2882
- tileWidth: 320,
2883
- tileHeight: 180,
2901
+ tileWidth: preset.tileWidth,
2902
+ tileHeight: preset.tileHeight,
2884
2903
  maxCols: 10,
2885
2904
  samples: ordered.map((s) => ({ t: s.t }))
2886
2905
  });
2887
2906
  const geometry = require_sheet_geometry.computeSheetGeometry(ordered.length, {
2888
- tileWidth: 320,
2889
- tileHeight: 180,
2907
+ tileWidth: preset.tileWidth,
2908
+ tileHeight: preset.tileHeight,
2890
2909
  maxCols: 10
2891
2910
  });
2892
2911
  const placements = require_sheet_geometry.allTilePlacements(ordered.length, geometry);
2893
2912
  const sheetBytes = await composeSheet(SHARP, {
2894
2913
  tiles: ordered.map((s) => s.jpeg),
2895
2914
  placements,
2896
- geometry
2915
+ geometry,
2916
+ quality: preset.quality
2897
2917
  });
2898
2918
  await persistThumbnailWindow(root, {
2899
2919
  deviceId,
@@ -2921,11 +2941,12 @@ var ThumbnailService = class {
2921
2941
  }
2922
2942
  /** Run ONE keyframe-only ffmpeg pass over a segment; return the extracted
2923
2943
  * JPEG buffers in order. */
2924
- async extractKeyframes(absSeg, workDir) {
2944
+ async extractKeyframes(absSeg, workDir, preset) {
2925
2945
  const args = buildKeyframeExtractArgs({
2926
2946
  inputPath: absSeg,
2927
2947
  outputPattern: node_path.default.join(workDir, "kf-%04d.jpg"),
2928
- tileWidth: 320
2948
+ tileWidth: preset.tileWidth,
2949
+ quality: jpegQualityToFfmpegQ(preset.quality)
2929
2950
  });
2930
2951
  await this.spawnFfmpeg(args);
2931
2952
  const files = (await node_fs.promises.readdir(workDir)).filter((f) => /^kf-\d+\.jpg$/.test(f)).sort();
@@ -3271,6 +3292,13 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
3271
3292
  locations: () => this.resolvedLocations,
3272
3293
  lowSegments: (deviceId) => this.index.segments(deviceId, "low"),
3273
3294
  recordingDeviceIds: async () => [...(await readDeviceConfigs(this.configStore())).keys()],
3295
+ resolvePreset: async (deviceId) => {
3296
+ try {
3297
+ return require_dist.resolveScrubThumbnailGeometry((await loadDeviceConfig(this.configStore(), deviceId)).scrubThumbnails);
3298
+ } catch {
3299
+ return require_dist.resolveScrubThumbnailGeometry(void 0);
3300
+ }
3301
+ },
3274
3302
  config: {
3275
3303
  enabled: this.config.thumbnailsEnabled,
3276
3304
  backfillEnabled: this.config.thumbnailBackfillEnabled,