@camstack/addon-post-analysis 1.2.622 → 1.2.623

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 (15) hide show
  1. package/THIRD_PARTY_MODELS.md +2 -2
  2. package/dist/{clip-model-registry-EDTOCKN9.mjs → clip-model-registry-C58fyELd.mjs} +181 -65
  3. package/dist/{clip-model-registry-MHJ9BSUV.js → clip-model-registry-KKT83z9U.js} +192 -64
  4. package/dist/embedding-encoder/index.js +1 -1
  5. package/dist/embedding-encoder/index.mjs +1 -1
  6. package/dist/pipeline-analytics/_stub.js +2 -2
  7. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DAyVukQp.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-_ahOxwNA.mjs} +3 -3
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-V5-s2hmZ.mjs +26 -0
  9. package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CssYIiFD.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B1HZu0i3.mjs} +1 -1
  10. package/dist/pipeline-analytics/{hostInit-C4-dCKYR.mjs → hostInit-CdRVx4vF.mjs} +3 -3
  11. package/dist/pipeline-analytics/index.js +335 -86
  12. package/dist/pipeline-analytics/index.mjs +335 -86
  13. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  14. package/package.json +1 -1
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CTvx3PDS.mjs +0 -26
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_clip_model_registry = require("../clip-model-registry-MHJ9BSUV.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-KKT83z9U.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -5414,41 +5414,106 @@ var NC_TRACKS_AT_INSTANT_LIMIT = 25;
5414
5414
  */
5415
5415
  var NC_TRACK_LIFE_SLACK_MS = 200;
5416
5416
  /**
5417
+ * A position this close to the instant is drawn as it is: one detection tick,
5418
+ * the same notion as {@link NC_TRACK_LIFE_SLACK_MS}. Anything farther is a
5419
+ * position of a MOVED subject, so it is either interpolated (a position on
5420
+ * each side inside the tolerance) or the track is left out — never drawn at a
5421
+ * later place than it was at the picture's instant.
5422
+ */
5423
+ var NC_BOX_EXACT_MS = NC_TRACK_LIFE_SLACK_MS;
5424
+ /** The box at `timestamp`: a near position as is, else the bracketing pair interpolated. */
5425
+ function boxAt(positions, timestamp, toleranceMs) {
5426
+ let nearest = null;
5427
+ let before = null;
5428
+ let after = null;
5429
+ for (const p of positions) {
5430
+ const d = p.timestamp - timestamp;
5431
+ if (nearest === null || Math.abs(d) < Math.abs(nearest.timestamp - timestamp)) nearest = p;
5432
+ if (d <= 0 && (before === null || p.timestamp > before.timestamp)) before = p;
5433
+ if (d >= 0 && (after === null || p.timestamp < after.timestamp)) after = p;
5434
+ }
5435
+ if (nearest === null) return null;
5436
+ const dt = nearest.timestamp - timestamp;
5437
+ if (Math.abs(dt) <= NC_BOX_EXACT_MS) return {
5438
+ bbox: nearest.bbox,
5439
+ score: nearest.score,
5440
+ dtMs: dt,
5441
+ interpolated: false
5442
+ };
5443
+ if (before === null || after === null || timestamp - before.timestamp > toleranceMs || after.timestamp - timestamp > toleranceMs) return null;
5444
+ const span = after.timestamp - before.timestamp;
5445
+ const f = span === 0 ? 0 : (timestamp - before.timestamp) / span;
5446
+ const lerp = (a, b) => a + (b - a) * f;
5447
+ return {
5448
+ bbox: {
5449
+ x: lerp(before.bbox.x, after.bbox.x),
5450
+ y: lerp(before.bbox.y, after.bbox.y),
5451
+ w: lerp(before.bbox.w, after.bbox.w),
5452
+ h: lerp(before.bbox.h, after.bbox.h)
5453
+ },
5454
+ score: nearest.score,
5455
+ dtMs: dt,
5456
+ interpolated: true
5457
+ };
5458
+ }
5459
+ /**
5417
5460
  * Every source track ALIVE at `timestamp` (± {@link NC_TRACK_LIFE_SLACK_MS})
5418
- * with a position within `toleranceMs` of it, its nearest box normalised,
5419
- * deduplicated by track id (a live copy and a persisted copy of one track are
5420
- * one track). Pure.
5461
+ * with a box AT that instant: a trail position within {@link NC_BOX_EXACT_MS},
5462
+ * else the linear interpolation of the positions bracketing it (each within
5463
+ * `toleranceMs`). A track with neither is not drawn — `onDrop` names it.
5464
+ * Deduplicated by track id (a live copy and a persisted copy are one track).
5465
+ * Pure.
5421
5466
  */
5422
- function tracksAtInstant(sources, timestamp, toleranceMs, fallbackDims) {
5467
+ function tracksAtInstant(sources, timestamp, toleranceMs, fallbackDims, onDrop) {
5423
5468
  const out = /* @__PURE__ */ new Map();
5424
5469
  for (const track of sources) {
5425
5470
  if (out.has(track.trackId)) continue;
5426
- if (track.firstSeen > timestamp + NC_TRACK_LIFE_SLACK_MS || track.lastSeen < timestamp - NC_TRACK_LIFE_SLACK_MS) continue;
5471
+ if (track.firstSeen > timestamp + NC_TRACK_LIFE_SLACK_MS || track.lastSeen < timestamp - NC_TRACK_LIFE_SLACK_MS) {
5472
+ onDrop?.({
5473
+ trackId: track.trackId,
5474
+ className: track.className,
5475
+ reason: "outside-life"
5476
+ });
5477
+ continue;
5478
+ }
5427
5479
  const w = track.frameWidth ?? fallbackDims?.w ?? 0;
5428
5480
  const h = track.frameHeight ?? fallbackDims?.h ?? 0;
5429
- if (w <= 0 || h <= 0) continue;
5430
- let best = null;
5431
- let bestDelta = Number.POSITIVE_INFINITY;
5432
- for (const p of track.positions) {
5433
- const delta = Math.abs(p.timestamp - timestamp);
5434
- if (delta < bestDelta) {
5435
- bestDelta = delta;
5436
- best = p;
5437
- }
5481
+ if (w <= 0 || h <= 0) {
5482
+ onDrop?.({
5483
+ trackId: track.trackId,
5484
+ className: track.className,
5485
+ reason: "no-frame-dims"
5486
+ });
5487
+ continue;
5488
+ }
5489
+ const at = boxAt(track.positions, timestamp, toleranceMs);
5490
+ if (at === null) {
5491
+ const nearestDtMs = track.positions.reduce((best, p) => {
5492
+ const d = p.timestamp - timestamp;
5493
+ return best === void 0 || Math.abs(d) < Math.abs(best) ? d : best;
5494
+ }, void 0);
5495
+ onDrop?.({
5496
+ trackId: track.trackId,
5497
+ className: track.className,
5498
+ reason: "no-position-near-instant",
5499
+ ...nearestDtMs !== void 0 ? { nearestDtMs } : {}
5500
+ });
5501
+ continue;
5438
5502
  }
5439
- if (best === null || bestDelta > toleranceMs) continue;
5440
5503
  const label = track.subLabel ?? track.label;
5441
5504
  out.set(track.trackId, {
5442
5505
  trackId: track.trackId,
5443
5506
  className: track.className,
5444
5507
  ...label !== void 0 && label !== track.className ? { label } : {},
5445
- ...best.score !== void 0 ? { confidence: best.score } : {},
5508
+ ...at.score !== void 0 ? { confidence: at.score } : {},
5446
5509
  bbox: {
5447
- x: best.bbox.x / w,
5448
- y: best.bbox.y / h,
5449
- w: best.bbox.w / w,
5450
- h: best.bbox.h / h
5451
- }
5510
+ x: at.bbox.x / w,
5511
+ y: at.bbox.y / h,
5512
+ w: at.bbox.w / w,
5513
+ h: at.bbox.h / h
5514
+ },
5515
+ dtMs: at.dtMs,
5516
+ interpolated: at.interpolated
5452
5517
  });
5453
5518
  }
5454
5519
  return [...out.values()];
@@ -19350,11 +19415,16 @@ var DebugNoteArchiveStore = class {
19350
19415
  };
19351
19416
  //#endregion
19352
19417
  //#region src/pipeline-analytics/store/event-ledger-collection.ts
19418
+ /**
19419
+ * One event ledger (motion, audio, sensor, faces, plates …) over its
19420
+ * `ctx.records` kind. Every node has `ctx.records`, so there is no other path.
19421
+ */
19353
19422
  /** Matches {@link EventStore.LEDGER_PAGE_SIZE} — kept local to avoid a circular import. */
19354
19423
  var LEDGER_PAGE_SIZE = 500;
19355
19424
  var EventLedgerCollection = class EventLedgerCollection {
19356
19425
  collection;
19357
19426
  kind;
19427
+ records;
19358
19428
  autoIntegerId;
19359
19429
  legacyAtField;
19360
19430
  deviceIdMode;
@@ -19370,6 +19440,7 @@ var EventLedgerCollection = class EventLedgerCollection {
19370
19440
  this.logger = deps.logger;
19371
19441
  this.deriveBody = deps.deriveBody;
19372
19442
  this.indexFields = deps.definition.declaration.indexes.map((i) => i.fields);
19443
+ this.records = deps.records;
19373
19444
  const handle = deps.records.kind(deps.definition);
19374
19445
  this.kind = handle;
19375
19446
  }
@@ -19388,6 +19459,31 @@ var EventLedgerCollection = class EventLedgerCollection {
19388
19459
  }
19389
19460
  await this.kind.put(put);
19390
19461
  }
19462
+ /**
19463
+ * `upsertRow` as an op of {@link EventLedgerCollection.transact}. A row that
19464
+ * cannot be mapped THROWS here: inside a transaction a silently skipped write
19465
+ * would let the rest of the call commit without it.
19466
+ */
19467
+ async txUpsert(id, value) {
19468
+ const put = this.buildRecordPut(id, value);
19469
+ if (put === null) throw new require_clip_model_registry.InvalidRecordError(`${this.collection}/${id}: the record could not be mapped`);
19470
+ return this.kind.txPut(put);
19471
+ }
19472
+ /** `update` as an op of {@link EventLedgerCollection.transact}. */
19473
+ txUpdate(id, patch) {
19474
+ return this.kind.txPatch(id, this.derived(patch, "patch"));
19475
+ }
19476
+ /** `deleteKey` as an op of {@link EventLedgerCollection.transact}. */
19477
+ txDelete(id) {
19478
+ return this.kind.txDelete(id);
19479
+ }
19480
+ /**
19481
+ * Run ops (built by the `tx*` methods of ledgers whose kinds share a
19482
+ * `transactFamily`) in ONE transaction: all land or none does.
19483
+ */
19484
+ transact(ops) {
19485
+ return this.records.transact(ops);
19486
+ }
19391
19487
  async insert(record) {
19392
19488
  const idNum = this.autoIntegerId ? await this.kind.allocateId() : null;
19393
19489
  const id = idNum !== null ? String(idNum) : typeof record["id"] === "string" ? record["id"] : null;
@@ -22329,8 +22425,9 @@ var RetrainAnnotationStore = class {
22329
22425
  * The old rows' deletion, the new rows, the frame's `dismissed` set and its
22330
22426
  * bumped `revision` run under a per-frame commit lock, guarded by
22331
22427
  * `commit.fromRevision`: a concurrent save in this process leaves one winner
22332
- * and one `conflict` — never a blend. The steps are separate records writes,
22333
- * so a crash between them is NOT rolled back.
22428
+ * and one `conflict` — never a blend. The deletion, the new rows and the frame
22429
+ * row are ONE records `transact` (the four retrain kinds share a family): a
22430
+ * failure leaves the frame exactly as it was.
22334
22431
  *
22335
22432
  * Returns the persisted rows so the caller renders ids it can address.
22336
22433
  */
@@ -22447,21 +22544,14 @@ var RetrainAnnotationStore = class {
22447
22544
  }
22448
22545
  return stamped;
22449
22546
  }
22450
- /** Remove every annotation of a frame — the operator un-selecting it. */
22451
- async deleteForFrame(mediaKey) {
22452
- const rows = await this.listByFrame(mediaKey);
22453
- for (const row of rows) try {
22454
- await this.annotations.deleteKey(row.id);
22455
- } catch (err) {
22456
- this.logger.warn("retrain annotation delete failed", {
22457
- tags: { deviceId: row.deviceId },
22458
- meta: {
22459
- annotationId: row.id,
22460
- error: String(err)
22461
- }
22462
- });
22463
- }
22464
- return rows.length;
22547
+ /**
22548
+ * The delete ops of every annotation of a frame, for ONE transaction with
22549
+ * the frame row and its memberships (RetrainService.removeFrame). A failed
22550
+ * read THROWS: "no annotations" must never be the answer to an unreadable
22551
+ * store, because the frame row would then be deleted over live boxes.
22552
+ */
22553
+ async deleteOpsForFrame(mediaKey) {
22554
+ return (await this.annotations.query({ where: { mediaKey } })).map((row) => this.annotations.txDelete(row.id));
22465
22555
  }
22466
22556
  async replaceForFrameOnRecords(mediaKey, rows, commit, nextRevision) {
22467
22557
  const frame = await this.frames.get(mediaKey);
@@ -22474,12 +22564,14 @@ var RetrainAnnotationStore = class {
22474
22564
  kind: "conflict",
22475
22565
  currentRevision
22476
22566
  };
22477
- await this.annotations.deleteWhere({ where: { mediaKey } });
22478
- for (const row of rows) await this.annotations.upsertRow(row.id, this.toRecord(row));
22479
- await this.frames.update(mediaKey, {
22567
+ const ops = [];
22568
+ for (const existing of await this.annotations.query({ where: { mediaKey } })) ops.push(this.annotations.txDelete(existing.id));
22569
+ for (const row of rows) ops.push(await this.annotations.txUpsert(row.id, this.toRecord(row)));
22570
+ ops.push(await this.frames.txUpdate(mediaKey, {
22480
22571
  revision: nextRevision,
22481
22572
  ...commit.dismissed !== void 0 ? { dismissed: [...commit.dismissed] } : {}
22482
- });
22573
+ }));
22574
+ await this.frames.transact(ops);
22483
22575
  return {
22484
22576
  kind: "saved",
22485
22577
  rows,
@@ -22497,8 +22589,10 @@ var RetrainAnnotationStore = class {
22497
22589
  kind: "conflict",
22498
22590
  currentRevision
22499
22591
  };
22500
- for (const row of eligible) await this.annotations.update(row.id, { origin: origins.get(row.id) ?? null });
22501
- await this.frames.update(mediaKey, { revision: nextRevision });
22592
+ const ops = [];
22593
+ for (const row of eligible) ops.push(await this.annotations.txUpdate(row.id, { origin: origins.get(row.id) ?? null }));
22594
+ ops.push(await this.frames.txUpdate(mediaKey, { revision: nextRevision }));
22595
+ await this.frames.transact(ops);
22502
22596
  return {
22503
22597
  kind: "updated",
22504
22598
  updated: eligible.length,
@@ -22669,16 +22763,16 @@ var RetrainCollectionStore = class {
22669
22763
  }, counts.get(input.id) ?? 0)
22670
22764
  };
22671
22765
  }
22672
- /** Remove the collection and its memberships — members first, so a crash
22673
- * never leaves memberships pointing at a name that no longer exists. */
22766
+ /** Remove the collection and its memberships in ONE transaction: a failure
22767
+ * leaves the collection with all its members, never memberships pointing at
22768
+ * a name that no longer exists, nor a collection that silently lost some. */
22674
22769
  async delete(id) {
22675
22770
  if (await this.readRow(id) === null) return {
22676
22771
  deleted: false,
22677
22772
  memberFrameIds: []
22678
22773
  };
22679
22774
  const members = await this.memberships.query({ where: { collectionId: id } });
22680
- for (const m of members) await this.memberships.deleteKey(m.id);
22681
- await this.collections.deleteKey(id);
22775
+ await this.collections.transact([...members.map((m) => this.memberships.txDelete(m.id)), this.collections.txDelete(id)]);
22682
22776
  this.logger.info("retrain collection deleted", { meta: {
22683
22777
  collectionId: id,
22684
22778
  members: members.length
@@ -22707,11 +22801,29 @@ var RetrainCollectionStore = class {
22707
22801
  await this.memberships.deleteKey(key);
22708
22802
  return true;
22709
22803
  }
22710
- /** Remove a frame from EVERY collection — the frame itself is leaving. */
22711
- async forgetFrame(frameId) {
22712
- const rows = await this.membershipRows({ frameId });
22713
- for (const r of rows) await this.memberships.deleteKey(r.id);
22714
- return rows.length;
22804
+ /**
22805
+ * The delete ops of every membership of a frame, for ONE transaction with
22806
+ * the frame row and its annotations (RetrainService.removeFrame). A failed
22807
+ * read THROWS: the frame row must not be deleted over memberships nobody
22808
+ * could list.
22809
+ */
22810
+ async forgetFrameOps(frameId) {
22811
+ return (await this.memberships.query({ where: { frameId } })).map((r) => this.memberships.txDelete(r.id));
22812
+ }
22813
+ /**
22814
+ * The op that adds a frame to a collection, for ONE transaction with the
22815
+ * frame's own row (a still stored from the recording). `null` when it is
22816
+ * already a member: there is nothing to write.
22817
+ */
22818
+ async addFrameOp(input) {
22819
+ const key = membershipKey(input.collectionId, input.frameId);
22820
+ if (await this.memberships.get(key) !== null) return null;
22821
+ return this.memberships.txUpsert(key, {
22822
+ collectionId: input.collectionId,
22823
+ frameId: input.frameId,
22824
+ deviceId: input.deviceId,
22825
+ addedAt: this.now()
22826
+ });
22715
22827
  }
22716
22828
  /** The frames of a collection, in the order they were added. */
22717
22829
  async listFrameIds(collectionId) {
@@ -23303,7 +23415,7 @@ var RetrainFrameStore = class {
23303
23415
  * returned untouched; the blob lands before the row; a refusal is named.
23304
23416
  * `capturedAt` IS the instant — the camera saw this frame then.
23305
23417
  */
23306
- async storeRecordingStill(input) {
23418
+ async storeRecordingStill(input, withRow = []) {
23307
23419
  const frameId = recordingFrameId(input.deviceId, input.epochMs);
23308
23420
  const existing = await this.get(frameId);
23309
23421
  if (existing !== null) return {
@@ -23350,7 +23462,7 @@ var RetrainFrameStore = class {
23350
23462
  relativePath: path,
23351
23463
  data: input.bytes
23352
23464
  });
23353
- await this.ledger.upsertRow(frameId, {
23465
+ const row = await this.ledger.txUpsert(frameId, {
23354
23466
  deviceId: frame.deviceId,
23355
23467
  trackId: "",
23356
23468
  sourceMediaId: 0,
@@ -23367,6 +23479,7 @@ var RetrainFrameStore = class {
23367
23479
  origin: "recording",
23368
23480
  epochMs: input.epochMs
23369
23481
  });
23482
+ await this.ledger.transact([row, ...withRow]);
23370
23483
  };
23371
23484
  try {
23372
23485
  if (this.writeGate) await this.writeGate.run(persist);
@@ -23420,22 +23533,33 @@ var RetrainFrameStore = class {
23420
23533
  /**
23421
23534
  * Drop a copy and its blob — the operator un-selecting a frame.
23422
23535
  *
23423
- * The caller must have removed the frame's annotations first; this store does
23424
- * not cascade, because a store that deletes rows in another table on a
23425
- * best-effort write is how a half-deleted annotation set happens.
23536
+ * The ROW goes first, in one transaction with `withRow` (the frame's
23537
+ * annotations and collection memberships, which the caller builds): a failure
23538
+ * leaves the frame, its boxes and its memberships all in place, and throws.
23539
+ * The blob goes after: if that fails the row is already gone and the bytes
23540
+ * are reclaimable junk, logged, never a row pointing at missing media.
23426
23541
  */
23427
- async deleteFrame(frame) {
23542
+ async deleteFrame(frame, withRow = []) {
23428
23543
  const remove = async () => {
23544
+ try {
23545
+ await this.ledger.transact([...withRow, this.ledger.txDelete(frame.frameId)]);
23546
+ } catch (err) {
23547
+ this.logger.error("retrain frame delete failed; nothing was removed", {
23548
+ tags: { deviceId: frame.deviceId },
23549
+ meta: {
23550
+ frameId: frame.frameId,
23551
+ error: String(err)
23552
+ }
23553
+ });
23554
+ throw err;
23555
+ }
23429
23556
  try {
23430
23557
  await this.storage.delete({
23431
23558
  location: frame.locationId ?? LEGACY_RETRAIN_MEDIA_LOCATION,
23432
23559
  relativePath: framePath(frame)
23433
23560
  });
23434
- } catch {}
23435
- try {
23436
- await this.ledger.deleteKey(frame.frameId);
23437
23561
  } catch (err) {
23438
- this.logger.warn("RetrainFrameStore.deleteFrame row delete failed", {
23562
+ this.logger.warn("retrain frame removed but its blob could not be deleted", {
23439
23563
  tags: { deviceId: frame.deviceId },
23440
23564
  meta: {
23441
23565
  frameId: frame.frameId,
@@ -28431,7 +28555,9 @@ function incomingFromSystemEvent(event, lookup) {
28431
28555
  const blindForMs = numberField(data, "blindForMs");
28432
28556
  if (deviceId === void 0 || blindForMs === void 0) return null;
28433
28557
  const identity = lookup(deviceId);
28434
- const minutes = Math.max(1, Math.round(blindForMs / 6e4));
28558
+ const motionMs = numberField(data, "motionMs") ?? blindForMs;
28559
+ const sessions = numberField(data, "sessions") ?? 1;
28560
+ const minutes = Math.max(1, Math.round(motionMs / 6e4));
28435
28561
  return systemSubject({
28436
28562
  facts,
28437
28563
  kind: "detection-blind",
@@ -28440,13 +28566,39 @@ function incomingFromSystemEvent(event, lookup) {
28440
28566
  bodyKey: "system.detection-blind.body",
28441
28567
  textParams: {
28442
28568
  ...deviceTextParams(deviceId, identity),
28443
- minutes: String(minutes)
28569
+ minutes: String(minutes),
28570
+ sessions: String(sessions)
28444
28571
  },
28445
28572
  textCount: minutes,
28446
28573
  deviceId,
28447
28574
  ...identity !== void 0 ? { deviceType: identity.type } : {}
28448
28575
  });
28449
28576
  }
28577
+ case "pipeline.motion-noisy":
28578
+ case "pipeline.motion-calm": {
28579
+ const deviceId = numberField(data, "deviceId");
28580
+ const dutyPct = numberField(data, "dutyPct");
28581
+ const sessions = numberField(data, "sessions");
28582
+ const windowMs = numberField(data, "windowMs");
28583
+ if (deviceId === void 0 || dutyPct === void 0 || sessions === void 0 || windowMs === void 0) return null;
28584
+ const noisy = event.category === "pipeline.motion-noisy";
28585
+ const identity = lookup(deviceId);
28586
+ return systemSubject({
28587
+ facts,
28588
+ kind: noisy ? "motion-noisy" : "motion-calm",
28589
+ subject: `motion-noise:${deviceId}`,
28590
+ titleKey: noisy ? "system.motion-noisy.title" : "system.motion-calm.title",
28591
+ bodyKey: noisy ? "system.motion-noisy.body" : "system.motion-calm.body",
28592
+ textParams: {
28593
+ ...deviceTextParams(deviceId, identity),
28594
+ minutes: String(Math.max(1, Math.round(windowMs / 6e4))),
28595
+ duty: String(dutyPct),
28596
+ sessions: String(sessions)
28597
+ },
28598
+ deviceId,
28599
+ ...identity !== void 0 ? { deviceType: identity.type } : {}
28600
+ });
28601
+ }
28450
28602
  case "recording.health.stalled": {
28451
28603
  const deviceId = numberField(data, "deviceId");
28452
28604
  const idleMs = numberField(data, "idleMs");
@@ -28564,7 +28716,7 @@ function incomingFromSystemEvent(event, lookup) {
28564
28716
  case "system.restarting": return incomingFromNodeRestarting(facts, data);
28565
28717
  case "recording.export.completed": return incomingFromExportCompleted(facts, data);
28566
28718
  case "backup.run-completed": return incomingFromBackupCompleted(facts, data);
28567
- case "alert.episode": return incomingFromAlertEpisode(facts);
28719
+ case "alert.episode": return incomingFromAlertEpisode(facts, lookup);
28568
28720
  default: return null;
28569
28721
  }
28570
28722
  }
@@ -28580,9 +28732,11 @@ function incomingFromSystemEvent(event, lookup) {
28580
28732
  * Whether an edge is notified at all (once per episode, reconciled from the
28581
28733
  * alerts store) is decided before this point, by `NcAlertEpisodeLedger`.
28582
28734
  */
28583
- function incomingFromAlertEpisode(facts) {
28735
+ function incomingFromAlertEpisode(facts, lookup) {
28584
28736
  const payload = require_clip_model_registry.parseAlertEpisodePayload(facts.data);
28585
28737
  if (payload === null) return null;
28738
+ const attention = incomingFromDeviceAttention(facts, payload, lookup);
28739
+ if (attention !== null) return attention;
28586
28740
  const kind = require_clip_model_registry.alertEpisodeKind(payload.severity, payload.phase);
28587
28741
  if (kind === null) return null;
28588
28742
  return systemSubject({
@@ -28603,6 +28757,38 @@ function incomingFromAlertEpisode(facts) {
28603
28757
  ...payload.nodeId !== void 0 ? { nodeId: payload.nodeId } : {}
28604
28758
  });
28605
28759
  }
28760
+ /**
28761
+ * A CRITICAL alert whose source is a DEVICE (D911) is announced in the devices
28762
+ * family — `device-attention-required` / `-resolved`, carrying the device id so
28763
+ * a rule's device filters apply — and NOT as `alert-critical`, so one alert
28764
+ * never notifies twice through two families. A device source whose id is not a
28765
+ * device id falls through to the generic alert kinds.
28766
+ */
28767
+ function incomingFromDeviceAttention(facts, payload, lookup) {
28768
+ if (payload.sourceType !== "device" || payload.sourceId === void 0) return null;
28769
+ const kind = require_clip_model_registry.deviceAttentionKind(payload.severity, payload.phase);
28770
+ if (kind === null) return null;
28771
+ const deviceId = Number(payload.sourceId);
28772
+ if (!Number.isInteger(deviceId)) return null;
28773
+ const identity = lookup?.(deviceId);
28774
+ return systemSubject({
28775
+ facts,
28776
+ kind,
28777
+ subject: `alert:${payload.episodeId}`,
28778
+ titleKey: `system.${kind}.title`,
28779
+ bodyKey: `system.${kind}.body`,
28780
+ textParams: {
28781
+ ...deviceTextParams(deviceId, identity),
28782
+ title: payload.title,
28783
+ message: payload.message,
28784
+ category: payload.category,
28785
+ actionUrl: payload.actionUrl ?? ""
28786
+ },
28787
+ deviceId,
28788
+ ...identity !== void 0 ? { deviceType: identity.type } : {},
28789
+ ...payload.nodeId !== void 0 ? { nodeId: payload.nodeId } : {}
28790
+ });
28791
+ }
28606
28792
  /** The shells whose update has its own sentence. Anything else gets the plain
28607
28793
  * one: a body that names a shell nobody observed is worse than a vague body. */
28608
28794
  var WRAPPER_BODY_KINDS = new Set([
@@ -30593,9 +30779,13 @@ var en_default = {
30593
30779
  "system.addon-crash-loop.body": "The {{addons}} addon on node {{nodeId}} crashed {{count}} times in a row and will NOT be restarted. Everything it provides is offline until it is repaired.",
30594
30780
  "system.detection-blind.title": "Detecting nothing — {{deviceName}}",
30595
30781
  "system.detection-blind.body": {
30596
- "one": "Camera {{deviceName}} has been analysed for {{minutes}} minute with no detection at all",
30597
- "other": "Camera {{deviceName}} has been analysed for {{minutes}} minutes with no detection at all"
30782
+ "one": "Camera {{deviceName}} moved for {{minutes}} minute over {{sessions}} sessions and the detector produced nothing at all, not even a weak candidate",
30783
+ "other": "Camera {{deviceName}} moved for {{minutes}} minutes over {{sessions}} sessions and the detector produced nothing at all, not even a weak candidate"
30598
30784
  },
30785
+ "system.motion-noisy.title": "Constant motion, no subject — {{deviceName}}",
30786
+ "system.motion-noisy.body": "Camera {{deviceName}} has been in motion {{duty}}% of the last {{minutes}} minutes ({{sessions}} sessions) without a single detection. The detector is fine — tune this camera's motion zones or sensitivity.",
30787
+ "system.motion-calm.title": "Motion calm again — {{deviceName}}",
30788
+ "system.motion-calm.body": "Camera {{deviceName}} is no longer in constant motion ({{duty}}% of the last {{minutes}} minutes, {{sessions}} sessions)",
30599
30789
  "system.recording-stalled.title": "Recording stalled — {{deviceName}}",
30600
30790
  "system.recording-stalled.body": {
30601
30791
  "one": "Camera {{deviceName}} has not written a recording segment for {{minutes}} minute",
@@ -30605,6 +30795,10 @@ var en_default = {
30605
30795
  "system.recording-healthy.body": "Camera {{deviceName}} is writing recording segments again",
30606
30796
  "system.recording-degraded.title": "Recording degraded — {{deviceName}}",
30607
30797
  "system.recording-degraded.body": "Camera {{deviceName}} exhausted its writer restart budget ({{message}})",
30798
+ "system.device-attention-required.title": "{{title}}",
30799
+ "system.device-attention-required.body": "{{message}}",
30800
+ "system.device-attention-resolved.title": "Resolved: {{title}}",
30801
+ "system.device-attention-resolved.body": "{{deviceName}} no longer needs attention",
30608
30802
  "system.storage-location-low.title": "Storage low — {{locationName}}",
30609
30803
  "system.storage-location-low.body": "Location {{locationName}} is down to {{freePercent}}% free ({{reason}})",
30610
30804
  "system.storage-location-critical.title": "Storage critical — {{locationName}}",
@@ -30993,6 +31187,10 @@ var it_default = {
30993
31187
  "system.recording-healthy.body": "La camera {{deviceName}} scrive di nuovo i segmenti",
30994
31188
  "system.recording-degraded.title": "Registrazione degradata — {{deviceName}}",
30995
31189
  "system.recording-degraded.body": "La camera {{deviceName}} ha esaurito i riavvii del writer ({{message}})",
31190
+ "system.device-attention-required.title": "{{title}}",
31191
+ "system.device-attention-required.body": "{{message}}",
31192
+ "system.device-attention-resolved.title": "Risolto: {{title}}",
31193
+ "system.device-attention-resolved.body": "{{deviceName}} non richiede più intervento",
30996
31194
  "system.storage-location-low.title": "Storage basso — {{locationName}}",
30997
31195
  "system.storage-location-low.body": "La location {{locationName}} è al {{freePercent}}% libero ({{reason}})",
30998
31196
  "system.storage-location-critical.title": "Storage critico — {{locationName}}",
@@ -31001,9 +31199,13 @@ var it_default = {
31001
31199
  "system.storage-location-ok.body": "La location {{locationName}} ha recuperato spazio libero",
31002
31200
  "system.detection-blind.title": "Senza detection — {{deviceName}}",
31003
31201
  "system.detection-blind.body": {
31004
- "one": "La camera {{deviceName}} è analizzata da {{minutes}} minuto senza alcun rilevamento",
31005
- "other": "La camera {{deviceName}} è analizzata da {{minutes}} minuti senza alcun rilevamento"
31202
+ "one": "La camera {{deviceName}} ha avuto movimento per {{minutes}} minuto in {{sessions}} sessioni e il detector non ha prodotto nulla, nemmeno un candidato debole",
31203
+ "other": "La camera {{deviceName}} ha avuto movimento per {{minutes}} minuti in {{sessions}} sessioni e il detector non ha prodotto nulla, nemmeno un candidato debole"
31006
31204
  },
31205
+ "system.motion-noisy.title": "Movimento continuo senza soggetto — {{deviceName}}",
31206
+ "system.motion-noisy.body": "La camera {{deviceName}} è stata in movimento il {{duty}}% degli ultimi {{minutes}} minuti ({{sessions}} sessioni) senza alcun rilevamento. Il detector funziona — regola le zone di movimento o la sensibilità di questa camera.",
31207
+ "system.motion-calm.title": "Movimento di nuovo calmo — {{deviceName}}",
31208
+ "system.motion-calm.body": "La camera {{deviceName}} non è più in movimento continuo ({{duty}}% degli ultimi {{minutes}} minuti, {{sessions}} sessioni)",
31007
31209
  "system.addon-update-available.title": {
31008
31210
  "one": "{{count}} aggiornamento addon disponibile",
31009
31211
  "other": "{{count}} aggiornamenti addon disponibili"
@@ -33563,6 +33765,7 @@ function systemEventSubject(kind, scope) {
33563
33765
  if (kind === "node-online" || kind === "node-offline") return `node:${scope.nodeId ?? "hub"}`;
33564
33766
  if (kind === "node-inference-unavailable") return `node-inference:${scope.nodeId ?? "hub"}`;
33565
33767
  if (kind === "detection-blind") return `detection-blind:${scope.deviceId ?? 0}`;
33768
+ if (kind === "motion-noisy" || kind === "motion-calm") return `motion-noise:${scope.deviceId ?? 0}`;
33566
33769
  if (kind === "addon-crash-loop") return `addon-crash:${scope.nodeId ?? "hub"}:${TEST_CRASHED_RUNNER}`;
33567
33770
  if (kind === "addon-update-available" || kind === "server-update-available") return `package:${scope.packageName ?? "@camstack/server"}`;
33568
33771
  if (kind === "wrapper-update-available") return `wrapper:${scope.nodeId ?? "hub"}`;
@@ -33708,6 +33911,8 @@ function planRuleTest(input) {
33708
33911
  seconds: "30",
33709
33912
  devices: "cuda:0",
33710
33913
  minutes: "15",
33914
+ duty: "55",
33915
+ sessions: "96",
33711
33916
  addons: TEST_CRASHED_RUNNER,
33712
33917
  list: "@camstack/addon-pipeline 1.0.1 (current 1.0.0)",
33713
33918
  nodes: "hub",
@@ -40365,7 +40570,17 @@ var NcNotificationImageStage = class {
40365
40570
  ...subjectBox,
40366
40571
  label: imageLabelText(payload.subject.label ?? payload.subject.className, payload.subject.confidence)
40367
40572
  } : null;
40368
- const others = mode === "scene-boxes" ? otherTracks(await this.tracksAt(camera, source.timestamp), subjectTrackId, subjectBox, payload.subject.className).map((t) => ({
40573
+ const othersDrawn = mode === "scene-boxes" ? otherTracks(await this.tracksAt(camera, source.timestamp), subjectTrackId, subjectBox, payload.subject.className) : [];
40574
+ if (mode === "scene-boxes") this.deps.logger.info("scene-boxes: other boxes drawn", {
40575
+ tags: { deviceId: camera },
40576
+ meta: {
40577
+ ruleId: entry.ruleId,
40578
+ recordId: entry.recordId,
40579
+ instant: source.timestamp,
40580
+ drawn: othersDrawn.map((t) => `${t.trackId}/${t.className}/dt=${t.dtMs ?? "?"}ms/${t.interpolated === true ? "interpolated" : "exact"}`).join(",")
40581
+ }
40582
+ });
40583
+ const others = mode === "scene-boxes" ? othersDrawn.map((t) => ({
40369
40584
  ...t.bbox,
40370
40585
  label: imageLabelText(t.label ?? t.className, t.confidence),
40371
40586
  ...t.confidence !== void 0 ? { confidence: t.confidence } : {}
@@ -87732,7 +87947,12 @@ var RetrainCollectionService = class {
87732
87947
  reason: "unknown-collection"
87733
87948
  };
87734
87949
  }
87735
- const stored = await this.storeStill(input.deviceId, input.epochMs);
87950
+ const membershipOp = await this.deps.collections.addFrameOp({
87951
+ collectionId: input.collectionId,
87952
+ frameId: recordingFrameId(input.deviceId, input.epochMs),
87953
+ deviceId: input.deviceId
87954
+ });
87955
+ const stored = await this.storeStill(input.deviceId, input.epochMs, membershipOp === null ? [] : [membershipOp]);
87736
87956
  if (stored.kind === "refused") {
87737
87957
  this.deps.logger.info("retrain collection add refused", {
87738
87958
  tags,
@@ -87744,7 +87964,7 @@ var RetrainCollectionService = class {
87744
87964
  });
87745
87965
  return stored;
87746
87966
  }
87747
- await this.deps.collections.addFrame({
87967
+ if (stored.reused) await this.deps.collections.addFrame({
87748
87968
  collectionId: input.collectionId,
87749
87969
  frameId: stored.frame.frameId,
87750
87970
  deviceId: input.deviceId
@@ -87809,7 +88029,7 @@ var RetrainCollectionService = class {
87809
88029
  const ids = await this.deps.collections.listFrameIds(input.collectionId);
87810
88030
  return this.deps.retrain.toWireFrames(await this.deps.frames.getMany(ids));
87811
88031
  }
87812
- async storeStill(deviceId, epochMs) {
88032
+ async storeStill(deviceId, epochMs, withRow) {
87813
88033
  const existing = await this.deps.frames.get(recordingFrameId(deviceId, epochMs));
87814
88034
  if (existing !== null) return {
87815
88035
  kind: "stored",
@@ -87829,7 +88049,7 @@ var RetrainCollectionService = class {
87829
88049
  deviceId,
87830
88050
  epochMs,
87831
88051
  bytes: still.jpeg
87832
- });
88052
+ }, withRow);
87833
88053
  return stored.kind === "copied" ? {
87834
88054
  kind: "stored",
87835
88055
  frame: stored.frame,
@@ -90152,9 +90372,9 @@ var RetrainService = class {
90152
90372
  ...this.frameUrls(updated.frameId)
90153
90373
  };
90154
90374
  }
90155
- /** Un-select: annotations first, then the copy. Never the other way round —
90156
- * an annotation whose frame is gone is unopenable and unfixable. The frame's
90157
- * collection memberships go with it. */
90375
+ /** Un-select: the frame row, its annotations and its collection memberships
90376
+ * leave in one transaction (an annotation whose frame is gone is unopenable
90377
+ * and unfixable, so they never part); the blob is deleted after the row. */
90158
90378
  async deselectFrame(input) {
90159
90379
  const frame = await this.deps.frames.get(input.frameId);
90160
90380
  if (frame === null) {
@@ -90173,11 +90393,16 @@ var RetrainService = class {
90173
90393
  removedAnnotations: await this.removeFrame(frame)
90174
90394
  };
90175
90395
  }
90176
- /** Remove a frame from the dataset: annotations, memberships, then the copy. */
90396
+ /**
90397
+ * Remove a frame from the dataset. The frame row, its annotations and its
90398
+ * collection memberships leave in ONE transaction (a failure removes nothing
90399
+ * and throws); the blob is deleted after the row, never before.
90400
+ */
90177
90401
  async removeFrame(frame) {
90178
- const removedAnnotations = await this.deps.annotations.deleteForFrame(frame.frameId);
90179
- await this.deps.memberships?.forgetFrame(frame.frameId);
90180
- await this.deps.frames.deleteFrame(frame);
90402
+ const annotationOps = await this.deps.annotations.deleteOpsForFrame(frame.frameId);
90403
+ const membershipOps = await this.deps.memberships?.forgetFrameOps(frame.frameId) ?? [];
90404
+ await this.deps.frames.deleteFrame(frame, [...annotationOps, ...membershipOps]);
90405
+ const removedAnnotations = annotationOps.length;
90181
90406
  this.deps.logger.info("retrain frame removed from the dataset", {
90182
90407
  tags: { deviceId: frame.deviceId },
90183
90408
  meta: {
@@ -90371,7 +90596,8 @@ var RetrainService = class {
90371
90596
  * The guarded replace. A caller that names the revision it saw gets exactly
90372
90597
  * one attempt: a different current revision is a conflict, never a retry —
90373
90598
  * the point is that it must look again. A caller that names none asked for
90374
- * last-write-wins, so a race lost between our read and the transaction is
90599
+ * last-write-wins, so a race lost between our revision read and the records
90600
+ * `transact` that commits the save (a per-frame lock serialises the two) is
90375
90601
  * retried on the fresh revision, a bounded number of times.
90376
90602
  */
90377
90603
  async replaceGuarded(frame, input, drafts, dismissed) {
@@ -103998,6 +104224,8 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
103998
104224
  require_clip_model_registry.EventCategory.WorkerOffline,
103999
104225
  require_clip_model_registry.EventCategory.PipelineNodeInferenceUnavailable,
104000
104226
  require_clip_model_registry.EventCategory.PipelineDetectionBlind,
104227
+ require_clip_model_registry.EventCategory.PipelineMotionNoisy,
104228
+ require_clip_model_registry.EventCategory.PipelineMotionCalm,
104001
104229
  require_clip_model_registry.EventCategory.RecordingHealthStalled,
104002
104230
  require_clip_model_registry.EventCategory.RecordingHealthOk,
104003
104231
  require_clip_model_registry.EventCategory.RecordingHealthDegraded,
@@ -107016,6 +107244,18 @@ Object.freeze({
107016
107244
  "empty"
107017
107245
  ]
107018
107246
  },
107247
+ {
107248
+ "path": "filterCleaningDue",
107249
+ "valueType": "boolean"
107250
+ },
107251
+ {
107252
+ "path": "machineCareDue",
107253
+ "valueType": "boolean"
107254
+ },
107255
+ {
107256
+ "path": "machineCareRemainingRuns",
107257
+ "valueType": "number"
107258
+ },
107019
107259
  {
107020
107260
  "path": "halfLoad",
107021
107261
  "valueType": "boolean"
@@ -116310,7 +116550,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
116310
116550
  since: timestamp - toleranceMs,
116311
116551
  limit: NC_TRACKS_AT_INSTANT_LIMIT
116312
116552
  });
116313
- return tracksAtInstant([...live, ...persisted.tracks], timestamp, toleranceMs, this.lastFrameDimsByDevice.get(deviceId));
116553
+ return tracksAtInstant([...live, ...persisted.tracks], timestamp, toleranceMs, this.lastFrameDimsByDevice.get(deviceId), (drop) => this.ctx.logger.info("scene-boxes: other track not drawn", {
116554
+ tags: { deviceId },
116555
+ meta: {
116556
+ trackId: drop.trackId,
116557
+ className: drop.className,
116558
+ reason: drop.reason,
116559
+ instant: timestamp,
116560
+ ...drop.nearestDtMs !== void 0 ? { nearestDtMs: drop.nearestDtMs } : {}
116561
+ }
116562
+ }));
116314
116563
  },
116315
116564
  getDeviceName,
116316
116565
  getDeviceLocation,