@camstack/addon-pipeline 1.2.276 → 1.2.279

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.
@@ -13763,6 +13763,35 @@ var PlacementService = class {
13763
13763
  });
13764
13764
  }
13765
13765
  };
13766
+ /** How long each kind stays quiet after it has been listed. Absent = no bound. */
13767
+ var QUIET_BY_KIND = {
13768
+ audio: 5 * 6e4,
13769
+ motion: 2 * 6e4
13770
+ };
13771
+ /**
13772
+ * Thin a run of same-kind rows to one per cooldown.
13773
+ *
13774
+ * The DECISION is made oldest-first, because a cooldown read off an unsorted
13775
+ * list keeps an arbitrary subset. The ANSWER keeps the caller's own order:
13776
+ * this provider lists newest-first, and quietly reversing it here would have
13777
+ * turned a density filter into a reordering — caught by the spec that pins
13778
+ * `clips[0].kind`, which is the only reason it is not shipping.
13779
+ */
13780
+ function applyQuietKinds(clips) {
13781
+ const lastEmitted = /* @__PURE__ */ new Map();
13782
+ const dropped = /* @__PURE__ */ new Set();
13783
+ for (const clip of [...clips].toSorted((a, b) => a.timeRange.startMs - b.timeRange.startMs)) {
13784
+ const quietMs = QUIET_BY_KIND[clip.kind];
13785
+ if (quietMs === void 0) continue;
13786
+ const previous = lastEmitted.get(clip.kind);
13787
+ if (previous !== void 0 && clip.timeRange.startMs - previous < quietMs) {
13788
+ dropped.add(clip.id);
13789
+ continue;
13790
+ }
13791
+ lastEmitted.set(clip.kind, clip.timeRange.startMs);
13792
+ }
13793
+ return clips.filter((clip) => !dropped.has(clip.id));
13794
+ }
13766
13795
  //#endregion
13767
13796
  //#region src/recorder/addon/videoclip-visits.ts
13768
13797
  /**
@@ -13811,10 +13840,22 @@ function clusterFootageVisits(ranges, gapMs = require_dist.VISIT_MERGE_GAP_MS) {
13811
13840
  * so the visit became the recording session.
13812
13841
  *
13813
13842
  * Measured on 592, 2026-09-21, 24 h: seven rows covering the whole day, one of
13814
- * them 7 h 58 m. Clustering the same day's EVENTS at the same 30 s gives 46
13815
- * visits overnight and 36 in the day, median 17 s, longest 99 s. The constant
13816
- * was right; the input was not. No duration cap: it would split by the clock
13817
- * rather than by what happened, and nothing here needs splitting that way.
13843
+ * them 7 h 58 m.
13844
+ *
13845
+ * ## And the grain is the PADDING, not a constant
13846
+ *
13847
+ * `VISIT_MERGE_GAP_MS` (30 s) was arbitrary here and sat on a cliff. 592's
13848
+ * worst hour holds 492 events — 150 object, 98 motion, **244 audio** — whose
13849
+ * LARGEST gap is 25 s, so nothing ever reached 30 and the hour was one row.
13850
+ * Measured on that hour:
13851
+ *
13852
+ * | rule | clips | longest |
13853
+ * | --- | --- | --- |
13854
+ * | 30 s | 1 | 66 min |
13855
+ * | `preMs + postMs` (15 s) | 105 | 3.5 min |
13856
+ *
13857
+ * So two clips merge exactly when they would otherwise OVERLAP. Nothing to
13858
+ * tune, nothing to disagree with the pad.
13818
13859
  *
13819
13860
  * ## What the footage is still for
13820
13861
  *
@@ -13825,7 +13866,8 @@ function clusterFootageVisits(ranges, gapMs = require_dist.VISIT_MERGE_GAP_MS) {
13825
13866
  * recorder never kept is not a clip, and listing it is a row that opens onto
13826
13867
  * nothing.
13827
13868
  */
13828
- function clusterEventVisits(events, ranges, gapMs, pad) {
13869
+ function clusterEventVisits(events, ranges, pad) {
13870
+ const gapMs = pad.preMs + pad.postMs;
13829
13871
  if (events.length === 0 || ranges.length === 0) return [];
13830
13872
  const times = [...events].map((e) => e.timestamp).toSorted((a, b) => a - b);
13831
13873
  const sortedRanges = [...ranges].toSorted((a, b) => a.startMs - b.startMs);
@@ -14111,7 +14153,7 @@ function intersectsWindow(clip, since, until) {
14111
14153
  * scan on an indexed column buys a list that stops changing under the cursor.
14112
14154
  */
14113
14155
  function leftOverhangMs(pad) {
14114
- return require_dist.VISIT_MERGE_GAP_MS + pad.preMs;
14156
+ return pad.preMs + pad.postMs + pad.preMs;
14115
14157
  }
14116
14158
  /** No events at all — what the degraded branch annotates its visits with. */
14117
14159
  var NO_EVENTS = {
@@ -14171,14 +14213,14 @@ function createVideoclipsProvider(deps) {
14171
14213
  since: readSince,
14172
14214
  until
14173
14215
  });
14174
- clips = visitsToClips(clusterEventVisits(allEvents(events), ranges, require_dist.VISIT_MERGE_GAP_MS, deps.defaultPad), events, deps.defaultPad, vouched);
14216
+ clips = visitsToClips(clusterEventVisits(allEvents(events), ranges, deps.defaultPad), events, deps.defaultPad, vouched);
14175
14217
  } catch (err) {
14176
14218
  deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
14177
14219
  deviceId,
14178
14220
  error: err instanceof Error ? err.message : String(err)
14179
14221
  });
14180
14222
  }
14181
- const inWindow = clips.filter((c) => intersectsWindow(c, since, until));
14223
+ const inWindow = applyQuietKinds(clips).filter((c) => intersectsWindow(c, since, until));
14182
14224
  const answer = limit != null ? inWindow.slice(0, limit) : inWindow;
14183
14225
  reportUnvouchedClips(deps, deviceId, answer);
14184
14226
  return answer;
@@ -13756,6 +13756,35 @@ var PlacementService = class {
13756
13756
  });
13757
13757
  }
13758
13758
  };
13759
+ /** How long each kind stays quiet after it has been listed. Absent = no bound. */
13760
+ var QUIET_BY_KIND = {
13761
+ audio: 5 * 6e4,
13762
+ motion: 2 * 6e4
13763
+ };
13764
+ /**
13765
+ * Thin a run of same-kind rows to one per cooldown.
13766
+ *
13767
+ * The DECISION is made oldest-first, because a cooldown read off an unsorted
13768
+ * list keeps an arbitrary subset. The ANSWER keeps the caller's own order:
13769
+ * this provider lists newest-first, and quietly reversing it here would have
13770
+ * turned a density filter into a reordering — caught by the spec that pins
13771
+ * `clips[0].kind`, which is the only reason it is not shipping.
13772
+ */
13773
+ function applyQuietKinds(clips) {
13774
+ const lastEmitted = /* @__PURE__ */ new Map();
13775
+ const dropped = /* @__PURE__ */ new Set();
13776
+ for (const clip of [...clips].toSorted((a, b) => a.timeRange.startMs - b.timeRange.startMs)) {
13777
+ const quietMs = QUIET_BY_KIND[clip.kind];
13778
+ if (quietMs === void 0) continue;
13779
+ const previous = lastEmitted.get(clip.kind);
13780
+ if (previous !== void 0 && clip.timeRange.startMs - previous < quietMs) {
13781
+ dropped.add(clip.id);
13782
+ continue;
13783
+ }
13784
+ lastEmitted.set(clip.kind, clip.timeRange.startMs);
13785
+ }
13786
+ return clips.filter((clip) => !dropped.has(clip.id));
13787
+ }
13759
13788
  //#endregion
13760
13789
  //#region src/recorder/addon/videoclip-visits.ts
13761
13790
  /**
@@ -13804,10 +13833,22 @@ function clusterFootageVisits(ranges, gapMs = VISIT_MERGE_GAP_MS) {
13804
13833
  * so the visit became the recording session.
13805
13834
  *
13806
13835
  * Measured on 592, 2026-09-21, 24 h: seven rows covering the whole day, one of
13807
- * them 7 h 58 m. Clustering the same day's EVENTS at the same 30 s gives 46
13808
- * visits overnight and 36 in the day, median 17 s, longest 99 s. The constant
13809
- * was right; the input was not. No duration cap: it would split by the clock
13810
- * rather than by what happened, and nothing here needs splitting that way.
13836
+ * them 7 h 58 m.
13837
+ *
13838
+ * ## And the grain is the PADDING, not a constant
13839
+ *
13840
+ * `VISIT_MERGE_GAP_MS` (30 s) was arbitrary here and sat on a cliff. 592's
13841
+ * worst hour holds 492 events — 150 object, 98 motion, **244 audio** — whose
13842
+ * LARGEST gap is 25 s, so nothing ever reached 30 and the hour was one row.
13843
+ * Measured on that hour:
13844
+ *
13845
+ * | rule | clips | longest |
13846
+ * | --- | --- | --- |
13847
+ * | 30 s | 1 | 66 min |
13848
+ * | `preMs + postMs` (15 s) | 105 | 3.5 min |
13849
+ *
13850
+ * So two clips merge exactly when they would otherwise OVERLAP. Nothing to
13851
+ * tune, nothing to disagree with the pad.
13811
13852
  *
13812
13853
  * ## What the footage is still for
13813
13854
  *
@@ -13818,7 +13859,8 @@ function clusterFootageVisits(ranges, gapMs = VISIT_MERGE_GAP_MS) {
13818
13859
  * recorder never kept is not a clip, and listing it is a row that opens onto
13819
13860
  * nothing.
13820
13861
  */
13821
- function clusterEventVisits(events, ranges, gapMs, pad) {
13862
+ function clusterEventVisits(events, ranges, pad) {
13863
+ const gapMs = pad.preMs + pad.postMs;
13822
13864
  if (events.length === 0 || ranges.length === 0) return [];
13823
13865
  const times = [...events].map((e) => e.timestamp).toSorted((a, b) => a - b);
13824
13866
  const sortedRanges = [...ranges].toSorted((a, b) => a.startMs - b.startMs);
@@ -14104,7 +14146,7 @@ function intersectsWindow(clip, since, until) {
14104
14146
  * scan on an indexed column buys a list that stops changing under the cursor.
14105
14147
  */
14106
14148
  function leftOverhangMs(pad) {
14107
- return VISIT_MERGE_GAP_MS + pad.preMs;
14149
+ return pad.preMs + pad.postMs + pad.preMs;
14108
14150
  }
14109
14151
  /** No events at all — what the degraded branch annotates its visits with. */
14110
14152
  var NO_EVENTS = {
@@ -14164,14 +14206,14 @@ function createVideoclipsProvider(deps) {
14164
14206
  since: readSince,
14165
14207
  until
14166
14208
  });
14167
- clips = visitsToClips(clusterEventVisits(allEvents(events), ranges, VISIT_MERGE_GAP_MS, deps.defaultPad), events, deps.defaultPad, vouched);
14209
+ clips = visitsToClips(clusterEventVisits(allEvents(events), ranges, deps.defaultPad), events, deps.defaultPad, vouched);
14168
14210
  } catch (err) {
14169
14211
  deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
14170
14212
  deviceId,
14171
14213
  error: err instanceof Error ? err.message : String(err)
14172
14214
  });
14173
14215
  }
14174
- const inWindow = clips.filter((c) => intersectsWindow(c, since, until));
14216
+ const inWindow = applyQuietKinds(clips).filter((c) => intersectsWindow(c, since, until));
14175
14217
  const answer = limit != null ? inWindow.slice(0, limit) : inWindow;
14176
14218
  reportUnvouchedClips(deps, deviceId, answer);
14177
14219
  return answer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.276",
3
+ "version": "1.2.279",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",