@camstack/addon-post-analysis 1.2.298 → 1.2.299

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.
@@ -1,6 +1,6 @@
1
- import { $ as NcRuleSchema, $n as string, $t as isSystemEventSummaryRule, A as LockStateSchema, An as evaluateSensorEdge, At as audioMetricsCapability, B as NC_DEFAULT_SNOOZE_MINUTES, Bn as isDeviceScopedCap, Bt as embeddingRefusalOf, C as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Cn as systemEventSummaryScheduleOf, Ct as TimelapseRuleInputSchema, D as FULL_IMAGE_BBOX, Dn as zoneAnalyticsCapability, Dt as addonWidgetsSourceCapability, E as FIRST_LEVEL_MACRO_CLASSES, En as vectorDimFromBase64, Et as TrackSourceSchema, F as MediaFileKindEnum, Fn as CamProfileSchema, Ft as deriveRecordingMode, G as NC_TAXONOMY, Gn as array, Gt as faceGalleryCapability, I as MotionTriggeredBySchema, In as createEvent, It as describeSystemEventDeliveryProblem, J as NcAlarmSettingsPatchSchema, Jn as literal, Jt as isEventOwnerType, K as NC_TEMPLATE_VARS, Kn as boolean, Kt as failureContributionCapability, L as NC_ALARM_SYSTEM_EVENT_KINDS, Ln as customAction, Lt as describeSystemEventFamilyProblem, M as MAX_BIRTH_DECISION_RECORDS, Mn as DeviceType, Mt as buildEventKindDescriptor, Nn as errMsg, Nt as consumableLowPercentOf, O as FailureCounters, Ot as alarmPanelCapability, P as MOTION_CLOSE_AFTER_MS, Pn as BaseAddon, Pt as cosineSimilarity$1, Q as NcRulePatchSchema, Qn as record, Qt as isSystemDelivery, R as NC_CONDITION_CATALOG, Rn as defineCustomActions, S as EMBEDDING_REQUIREMENTS_FAILED, Sn as systemEventFilterApplies, St as TIMELAPSE_DENSE_FLOOR_SEC, T as EVENT_OWNER_TYPES, Tn as templateVarsFor, Tt as TimelapseRuleSchema, U as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, Un as sleep$1, Ut as encodeVectorBase64, V as NC_SENSOR_TRANSITION_KINDS, Vn as nodePin, W as NC_TANK_SYSTEM_EVENT_KINDS, Wn as _enum, Wt as evictionPolicyOfLocation, X as NcConditionsSchema, Xn as object, Xt as isMediaOwnerType, Y as NcConditionDescriptorSchema, Yn as number, Yt as isKnownTemplateVar, Z as NcRuleInputSchema, Zn as partialRecord, Zt as isScheduleActive, _n as servedTemplateVars, a as CLIP_EMBEDDING_STEP_ID, at as NcSnoozeSuppressedSchema, b as DeclaredDevices, bn as systemEventDeliveryProblems, bt as SCENE_DIVERGED, c as resolveClusterModelPin, cn as readDeviceStateFrom, ct as NcTemplatePreviewInputSchema, d as ArchivedDebugNoteSchema, dn as resolveClipSearchMinScore, dt as OpsLogEntrySchema, en as isVectorIndexDimConflict, er as unknown, et as NcRuleTargetSchema, f as BaseDevice, fn as resolveLocationMode, g as DEFAULT_EVENT_COLOR, gn as sensorRaisedOf, gt as RetrainStatusSchema, h as CoverStateSchema, hn as sceneMonitorCapability, ht as RebuildObjectEmbeddingsRefusalSchema, in as occupancyScope, it as NcSnoozeSchema, j as MAX_ARCHIVED_DEBUG_NOTES, jn as isSourceCap, jt as audioModeOf, k as LabelAttributionSchema, kn as canonicalHash, kt as assertTimelapseCadences, l as AUDIO_MACRO_LABELS, ln as readMotionTriggeredBy, lt as NcTemplatePreviewSchema, m as COCO_TO_MACRO, mn as ruleBelongsToAlarm$1, mt as RECORDING_SOURCE_CAMSTACK_ADDON, nn as mayWriteToLocation, nt as NcScheduleSchema, o as FACE_EMBEDDING_STEP_ID, on as pipelineAnalyticsCapability, ot as NcSystemEventKindSchema, p as BirthDecisionRecordSchema, pt as RECORDING_EXPORT_MAX_READ_BYTES, q as NcAlarmConfigSchema, qn as discriminatedUnion, qt as isDetectionMacroClass, rn as notificationRulesCapability, rt as NcSnoozeInputSchema, s as readClusterModelPinStrict, sn as plateGalleryCapability, st as NcTaxonomySchema, t as BUILTIN_CLIP_MODELS, tn as kebabToCamel, tr as EventCategory, tt as NcRuleUpdateInputSchema, u as AlarmArmModeSchema, un as readTimelapseGeneratedAt, ut as NcTemplateVarDescriptorSchema, v as DETECTION_MACRO_CLASSES, vn as storageOccupancyCapability, vt as SCENE_DEFAULT_ANCHOR_THRESHOLD, w as EVENT_KIND_BY_CAP, wn as templateCarriesSummary, wt as TimelapseRulePatchSchema, x as EMBEDDING_MODEL_FAILED, xn as systemEventFamilyProblems, xt as SceneMonitorSchema, yn as subKindsOf, yt as SCENE_DEFAULT_UNCOVERED_POLICY, z as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, zn as hydrateSchema } from "../clip-model-registry-D9WaswEs.mjs";
1
+ import { $ as NcRuleSchema, $n as partialRecord, $t as isScheduleActive, A as LockStateSchema, At as audioMetricsCapability, B as NC_DEFAULT_SNOOZE_MINUTES, Bn as defineCustomActions, Bt as embeddingRefusalOf, C as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Cn as systemEventFamilyProblems, Ct as TimelapseRuleInputSchema, D as FULL_IMAGE_BBOX, Dn as templateVarsFor, Dt as addonWidgetsSourceCapability, E as FIRST_LEVEL_MACRO_CLASSES, En as templateCarriesSummary, Et as TrackSourceSchema, F as MediaFileKindEnum, Fn as errMsg, Ft as deriveRecordingMode, G as NC_TAXONOMY, Gn as sleep$1, Gt as faceGalleryCapability, Hn as isDeviceScopedCap, I as MotionTriggeredBySchema, In as BaseAddon, It as describeSystemEventDeliveryProblem, J as NcAlarmSettingsPatchSchema, Jn as boolean, Jt as gifFromMp4Args, K as NC_TEMPLATE_VARS, Kn as _enum, Kt as failureContributionCapability, L as NC_ALARM_SYSTEM_EVENT_KINDS, Ln as CamProfileSchema, Lt as describeSystemEventFamilyProblem, M as MAX_BIRTH_DECISION_RECORDS, Mn as evaluateSensorEdge, Mt as buildEventKindDescriptor, Nn as isSourceCap, Nt as consumableLowPercentOf, O as FailureCounters, On as vectorDimFromBase64, Ot as alarmPanelCapability, P as MOTION_CLOSE_AFTER_MS, Pn as DeviceType, Pt as cosineSimilarity$1, Q as NcRulePatchSchema, Qn as object, Qt as isMediaOwnerType, R as NC_CONDITION_CATALOG, Rn as createEvent, S as EMBEDDING_REQUIREMENTS_FAILED, Sn as systemEventDeliveryProblems, St as TIMELAPSE_DENSE_FLOOR_SEC, T as EVENT_OWNER_TYPES, Tn as systemEventSummaryScheduleOf, Tt as TimelapseRuleSchema, U as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, Un as nodePin, Ut as encodeVectorBase64, V as NC_SENSOR_TRANSITION_KINDS, Vn as hydrateSchema, W as NC_TANK_SYSTEM_EVENT_KINDS, Wt as evictionPolicyOfLocation, X as NcConditionsSchema, Xn as literal, Xt as isEventOwnerType, Y as NcConditionDescriptorSchema, Yn as discriminatedUnion, Yt as isDetectionMacroClass, Z as NcRuleInputSchema, Zn as number, Zt as isKnownTemplateVar, _n as sceneMonitorCapability, a as CLIP_EMBEDDING_STEP_ID, an as notificationRulesCapability, at as NcSnoozeSuppressedSchema, b as DeclaredDevices, bn as storageOccupancyCapability, bt as SCENE_DIVERGED, c as resolveClusterModelPin, cn as pipelineAnalyticsCapability, ct as NcTemplatePreviewInputSchema, d as ArchivedDebugNoteSchema, dn as readMotionTriggeredBy, dt as OpsLogEntrySchema, en as isSystemDelivery, er as record, et as NcRuleTargetSchema, f as BaseDevice, fn as readTimelapseGeneratedAt, g as DEFAULT_EVENT_COLOR, gn as ruleBelongsToAlarm$1, gt as RetrainStatusSchema, h as CoverStateSchema, ht as RebuildObjectEmbeddingsRefusalSchema, in as mayWriteToLocation, it as NcSnoozeSchema, j as MAX_ARCHIVED_DEBUG_NOTES, jn as canonicalHash, jt as audioModeOf, k as LabelAttributionSchema, kn as zoneAnalyticsCapability, kt as assertTimelapseCadences, l as AUDIO_MACRO_LABELS, ln as plateGalleryCapability, lt as NcTemplatePreviewSchema, m as COCO_TO_MACRO, mn as resolveLocationMode, mt as RECORDING_SOURCE_CAMSTACK_ADDON, nn as isVectorIndexDimConflict, nr as unknown, nt as NcScheduleSchema, o as FACE_EMBEDDING_STEP_ID, on as occupancyScope, ot as NcSystemEventKindSchema, p as BirthDecisionRecordSchema, pn as resolveClipSearchMinScore, pt as RECORDING_EXPORT_MAX_READ_BYTES, q as NcAlarmConfigSchema, qn as array, qt as gifDeriveSpeed, rn as kebabToCamel, rr as EventCategory, rt as NcSnoozeInputSchema, s as readClusterModelPinStrict, st as NcTaxonomySchema, t as BUILTIN_CLIP_MODELS, tn as isSystemEventSummaryRule, tr as string, tt as NcRuleUpdateInputSchema, u as AlarmArmModeSchema, un as readDeviceStateFrom, ut as NcTemplateVarDescriptorSchema, v as DETECTION_MACRO_CLASSES, vn as sensorRaisedOf, vt as SCENE_DEFAULT_ANCHOR_THRESHOLD, w as EVENT_KIND_BY_CAP, wn as systemEventFilterApplies, wt as TimelapseRulePatchSchema, x as EMBEDDING_MODEL_FAILED, xn as subKindsOf, xt as SceneMonitorSchema, yn as servedTemplateVars, yt as SCENE_DEFAULT_UNCOVERED_POLICY, z as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, zn as customAction } from "../clip-model-registry-DCztUu1G.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
- import * as fs from "node:fs";
3
+ import * as fs$1 from "node:fs";
4
4
  import { promises } from "node:fs";
5
5
  import * as path$1 from "node:path";
6
6
  import path from "node:path";
@@ -8,6 +8,7 @@ import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from
8
8
  import { execFile } from "node:child_process";
9
9
  import { startEventLoopStallMonitor } from "@camstack/system/addon-utils";
10
10
  import { pipeline } from "node:stream/promises";
11
+ import fs from "node:fs/promises";
11
12
  import sharp from "sharp";
12
13
  import os from "node:os";
13
14
  import { isDeepStrictEqual } from "node:util";
@@ -3152,16 +3153,16 @@ function errnoCode(err) {
3152
3153
  async function writeFileAtomic(filePath, data) {
3153
3154
  const tmp = path$1.join(path$1.dirname(filePath), `.${path$1.basename(filePath)}.${randomUUID()}.tmp`);
3154
3155
  try {
3155
- await fs.promises.writeFile(tmp, data);
3156
+ await fs$1.promises.writeFile(tmp, data);
3156
3157
  await renameOver(tmp, filePath);
3157
3158
  } catch (err) {
3158
- await fs.promises.rm(tmp, { force: true }).catch(() => void 0);
3159
+ await fs$1.promises.rm(tmp, { force: true }).catch(() => void 0);
3159
3160
  throw err;
3160
3161
  }
3161
3162
  }
3162
3163
  async function renameOver(tmp, filePath) {
3163
3164
  for (let attempt = 0;; attempt += 1) try {
3164
- await fs.promises.rename(tmp, filePath);
3165
+ await fs$1.promises.rename(tmp, filePath);
3165
3166
  return;
3166
3167
  } catch (err) {
3167
3168
  const code = errnoCode(err);
@@ -3170,8 +3171,8 @@ async function renameOver(tmp, filePath) {
3170
3171
  await new Promise((resolve) => setTimeout(resolve, WINDOWS_RENAME_RETRY_MS));
3171
3172
  continue;
3172
3173
  }
3173
- await fs.promises.copyFile(tmp, filePath);
3174
- await fs.promises.rm(tmp, { force: true });
3174
+ await fs$1.promises.copyFile(tmp, filePath);
3175
+ await fs$1.promises.rm(tmp, { force: true });
3175
3176
  return;
3176
3177
  }
3177
3178
  }
@@ -3246,28 +3247,28 @@ var FilesystemStorageProvider = class {
3246
3247
  location,
3247
3248
  relativePath
3248
3249
  });
3249
- await fs.promises.mkdir(path$1.dirname(filePath), { recursive: true });
3250
+ await fs$1.promises.mkdir(path$1.dirname(filePath), { recursive: true });
3250
3251
  if (Buffer.isBuffer(data)) await writeFileAtomic(filePath, data);
3251
3252
  else {
3252
3253
  const tmp = path$1.join(path$1.dirname(filePath), `.${path$1.basename(filePath)}.${randomUUID()}.tmp`);
3253
3254
  try {
3254
- await pipeline(data, fs.createWriteStream(tmp));
3255
- await fs.promises.rename(tmp, filePath);
3255
+ await pipeline(data, fs$1.createWriteStream(tmp));
3256
+ await fs$1.promises.rename(tmp, filePath);
3256
3257
  } catch (err) {
3257
- await fs.promises.rm(tmp, { force: true }).catch(() => void 0);
3258
+ await fs$1.promises.rm(tmp, { force: true }).catch(() => void 0);
3258
3259
  throw err;
3259
3260
  }
3260
3261
  }
3261
3262
  }
3262
3263
  async read({ location, relativePath }) {
3263
- return fs.promises.readFile(await this.resolve({
3264
+ return fs$1.promises.readFile(await this.resolve({
3264
3265
  location,
3265
3266
  relativePath
3266
3267
  }));
3267
3268
  }
3268
3269
  async exists({ location, relativePath }) {
3269
3270
  try {
3270
- await fs.promises.access(await this.resolve({
3271
+ await fs$1.promises.access(await this.resolve({
3271
3272
  location,
3272
3273
  relativePath
3273
3274
  }));
@@ -3281,7 +3282,7 @@ var FilesystemStorageProvider = class {
3281
3282
  if (!base) return [];
3282
3283
  const dir = prefix ? path$1.join(base, prefix) : base;
3283
3284
  try {
3284
- return (await fs.promises.readdir(dir, { withFileTypes: true })).map((e) => prefix ? `${prefix}/${e.name}` : e.name);
3285
+ return (await fs$1.promises.readdir(dir, { withFileTypes: true })).map((e) => prefix ? `${prefix}/${e.name}` : e.name);
3285
3286
  } catch {
3286
3287
  return [];
3287
3288
  }
@@ -3291,19 +3292,19 @@ var FilesystemStorageProvider = class {
3291
3292
  location,
3292
3293
  relativePath
3293
3294
  });
3294
- await fs.promises.rm(filePath, { force: true });
3295
+ await fs$1.promises.rm(filePath, { force: true });
3295
3296
  }
3296
3297
  async getAvailableSpace({ location }) {
3297
3298
  const base = this.locationPaths.get(location);
3298
3299
  if (!base) return null;
3299
3300
  try {
3300
3301
  let target = base;
3301
- while (!fs.existsSync(target)) {
3302
+ while (!fs$1.existsSync(target)) {
3302
3303
  const parent = path$1.dirname(target);
3303
3304
  if (!parent || parent === target) return null;
3304
3305
  target = parent;
3305
3306
  }
3306
- const stats = await fs.promises.statfs(target);
3307
+ const stats = await fs$1.promises.statfs(target);
3307
3308
  return stats.bavail * stats.bsize;
3308
3309
  } catch {
3309
3310
  return null;
@@ -9973,14 +9974,25 @@ function occupancySubjectFromEdge(edge) {
9973
9974
  threshold: edge.threshold
9974
9975
  };
9975
9976
  }
9976
- /** Build the subject for a `package-event` evaluation (a persisted `package`
9977
- * object-event; `phase` derives from the event `state` at the call site). */
9977
+ /**
9978
+ * Build the subject for a `package-event` evaluation (a persisted `package`
9979
+ * object-event; `phase` derives from the event `state` at the call site).
9980
+ *
9981
+ * `observedAt` (D682, D684): a pick-up row's `timestamp` is when the departure
9982
+ * was CONCLUDED — five minutes of unseen observed time after the parcel went —
9983
+ * so the detector STORES the instant it happened on the row, and the footage is
9984
+ * cut around that (`media-anchor.ts`). Read from the row HERE, the one place
9985
+ * every path builds a package subject (live hook, boot reconcile, dry-run), so
9986
+ * none of them can anchor differently. `timestamp` keeps its meaning; a row
9987
+ * written before D684 has none, which the anchor reads as unknown, out loud.
9988
+ */
9978
9989
  function subjectFromPackageEvent(ev, phase) {
9979
9990
  return {
9980
9991
  kind: "package-event",
9981
9992
  recordId: String(ev.id),
9982
9993
  deviceId: ev.deviceId,
9983
9994
  timestamp: ev.timestamp,
9995
+ ...ev.observedAt !== void 0 ? { observedAt: ev.observedAt } : {},
9984
9996
  classNames: [ev.className],
9985
9997
  ...displayLabel(ev) !== void 0 ? { label: displayLabel(ev) } : {},
9986
9998
  ...recognitionPatch(ev.subLabelMeta),
@@ -10911,7 +10923,11 @@ function incomingFromAudioEvent(event, stillOwnerId) {
10911
10923
  })
10912
10924
  };
10913
10925
  }
10914
- /** Package object-event persist (delivered / picked-up). */
10926
+ /**
10927
+ * Package object-event persist (delivered / picked-up) — the live hook and the
10928
+ * boot reconcile alike. A pick-up's instant is read from the row
10929
+ * (`event.observedAt`, D684) inside `subjectFromPackageEvent`.
10930
+ */
10915
10931
  function incomingFromPackageEvent(event, phase) {
10916
10932
  return {
10917
10933
  subject: subjectFromPackageEvent(event, phase),
@@ -10921,7 +10937,8 @@ function incomingFromPackageEvent(event, phase) {
10921
10937
  tags: { deviceId: event.deviceId },
10922
10938
  meta: {
10923
10939
  eventId: event.id,
10924
- phase
10940
+ phase,
10941
+ ...event.observedAt !== void 0 ? { observedAt: event.observedAt } : {}
10925
10942
  }
10926
10943
  })
10927
10944
  };
@@ -19314,9 +19331,14 @@ function mediaAnchor(subject) {
19314
19331
  lagMs: subject.timestamp - observedAt
19315
19332
  };
19316
19333
  }
19334
+ function selfDatingKindOf(subject) {
19335
+ if (subject.occupancy !== void 0) return "occupancy";
19336
+ if (subject.packagePhase === "picked-up") return "package pick-up";
19337
+ return null;
19338
+ }
19317
19339
  /**
19318
19340
  * True when this anchor is the conclusion instant AND the subject was one that
19319
- * should have been able to date itself — today, an occupancy row.
19341
+ * should have been able to date itself — see {@link selfDatingKindOf}.
19320
19342
  *
19321
19343
  * Split from {@link mediaAnchor} because the anchor must stay pure of any
19322
19344
  * notion of subject KIND, while the log line that matters is exactly the one
@@ -20420,7 +20442,8 @@ var NcDispatcher = class {
20420
20442
  };
20421
20443
  const preRollSec = entry.payload.mediaClipPreRollSec ?? 4;
20422
20444
  const anchor = mediaAnchor(entry.payload.subject);
20423
- if (anchorFellBack(anchor, entry.payload.subject.occupancy !== void 0)) this.deps.logger.warn("occupancy footage anchored on the conclusion — moment unknown", {
20445
+ const selfDating = selfDatingKindOf(entry.payload.subject);
20446
+ if (anchorFellBack(anchor, selfDating !== null)) this.deps.logger.warn(`${selfDating ?? "occupancy"} footage anchored on the conclusion — moment unknown`, {
20424
20447
  tags: { deviceId: entry.payload.subject.deviceId },
20425
20448
  meta: {
20426
20449
  ruleId: entry.ruleId,
@@ -20448,7 +20471,7 @@ var NcDispatcher = class {
20448
20471
  preRollSec,
20449
20472
  retentionSec: this.ringRetentionSec(entry.payload.subject.deviceId),
20450
20473
  wanted: [...wantGif ? ["gif"] : [], ...wantMp4 ? ["video"] : []],
20451
- next: this.deps.cutRecordedClip !== void 0 ? "recorded-clip-follow-up" : "no-clip"
20474
+ next: this.recordedFollowUp(entry).kinds.length > 0 ? "recorded-clip-follow-up" : "no-clip"
20452
20475
  }
20453
20476
  });
20454
20477
  return {
@@ -20502,6 +20525,13 @@ var NcDispatcher = class {
20502
20525
  * alert for a complete one, which is precisely the wrong trade for the
20503
20526
  * notification that tells an operator someone is at the door.
20504
20527
  *
20528
+ * A GIF rule is served here too (D682): a package pick-up is concluded five
20529
+ * minutes after it happened, so a gif anchored on the pick-up is always
20530
+ * outside the ring. The gif is derived from THIS cut's bytes
20531
+ * (`deriveGifFromClip`) — one cut, never a second renderer — and a moment the
20532
+ * recordings cannot serve ships NO gif, by named reason, rather than a gif of
20533
+ * the wrong time.
20534
+ *
20505
20535
  * So the alert's latency is UNCHANGED, and **the clip is attached later**, as
20506
20536
  * a follow-up send to the same target carrying the same `tag` — a phone
20507
20537
  * collapses the two, so what the operator sees is the notification gaining
@@ -20524,7 +20554,9 @@ var NcDispatcher = class {
20524
20554
  */
20525
20555
  startRecordedFootage(entry, target, notification) {
20526
20556
  const cut = this.deps.cutRecordedClip;
20527
- if (cut === void 0 || entry.payload.mediaClip !== true) return;
20557
+ const plan = this.recordedFollowUp(entry);
20558
+ const wanted = plan.kinds;
20559
+ if (cut === void 0 || wanted.length === 0 && plan.gifRefused === void 0 && plan.videoRefused === void 0) return;
20528
20560
  const preRollSec = entry.payload.mediaClipPreRollSec ?? 4;
20529
20561
  const anchor = mediaAnchor(entry.payload.subject);
20530
20562
  if (footageSourceFor({
@@ -20534,34 +20566,70 @@ var NcDispatcher = class {
20534
20566
  ringRetentionSec: this.ringRetentionSec(entry.payload.subject.deviceId)
20535
20567
  }) !== "recordings") return;
20536
20568
  const deviceId = entry.payload.subject.deviceId;
20569
+ if (plan.gifRefused !== void 0) this.deps.logger.info("recorded gif follow-up: none — the gif is not cut from the recordings", {
20570
+ tags: { deviceId },
20571
+ meta: {
20572
+ ruleId: entry.ruleId,
20573
+ eventId: entry.recordId,
20574
+ targetId: target.id,
20575
+ reason: plan.gifRefused,
20576
+ anchoredOn: anchor.source,
20577
+ ...anchor.fallback !== void 0 ? { anchorFallback: anchor.fallback } : {},
20578
+ next: wanted
20579
+ }
20580
+ });
20581
+ if (plan.videoRefused !== void 0) this.deps.logger.info("recorded video follow-up: none — the video is not cut from the recordings", {
20582
+ tags: { deviceId },
20583
+ meta: {
20584
+ ruleId: entry.ruleId,
20585
+ eventId: entry.recordId,
20586
+ targetId: target.id,
20587
+ reason: plan.videoRefused,
20588
+ anchoredOn: anchor.source,
20589
+ ...anchor.fallback !== void 0 ? { anchorFallback: anchor.fallback } : {},
20590
+ next: wanted
20591
+ }
20592
+ });
20593
+ if (wanted.length === 0) return;
20594
+ const videoSpeed = entry.payload.mediaClipSpeed;
20537
20595
  const pending = (async () => {
20538
20596
  const result = await cut({
20539
20597
  deviceId,
20540
20598
  aroundMs: anchor.atMs,
20541
20599
  preRollSec,
20542
20600
  postRollSec: entry.payload.mediaClipPostRollSec ?? 6,
20543
- ...entry.payload.mediaClipSpeed !== void 0 ? { speed: entry.payload.mediaClipSpeed } : {},
20601
+ ...videoSpeed !== void 0 ? { speed: videoSpeed } : {},
20544
20602
  ruleId: entry.ruleId,
20545
20603
  recordId: entry.recordId
20546
20604
  });
20547
20605
  if (!result.ok) {
20548
- this.deps.logger.info("recorded clip follow-up: none — this delivery keeps no clip", {
20606
+ this.deps.logger.info("recorded footage follow-up: none — the recorded cut failed", {
20549
20607
  tags: { deviceId },
20550
20608
  meta: {
20551
20609
  ruleId: entry.ruleId,
20552
20610
  eventId: entry.recordId,
20553
20611
  targetId: target.id,
20554
- reason: result.reason
20612
+ reason: result.reason,
20613
+ wanted
20555
20614
  }
20556
20615
  });
20557
20616
  return;
20558
20617
  }
20559
- const withUrls = await this.withArtifactUrls(entry, [{
20560
- mediaType: "video",
20561
- bytes: result.mp4,
20562
- mime: "video/mp4",
20563
- name: "event.mp4"
20564
- }]);
20618
+ const attachments = await this.recordedAttachments(entry, wanted, result.mp4);
20619
+ if (attachments.length === 0) {
20620
+ this.deps.logger.info("recorded clip follow-up: none — the gif could not be derived", {
20621
+ tags: { deviceId },
20622
+ meta: {
20623
+ ruleId: entry.ruleId,
20624
+ eventId: entry.recordId,
20625
+ targetId: target.id,
20626
+ reason: "gif-derive-failed",
20627
+ wanted
20628
+ }
20629
+ });
20630
+ return;
20631
+ }
20632
+ const withUrls = await this.withArtifactUrls(entry, attachments);
20565
20633
  const sent = await this.deps.send({
20566
20634
  addonId: target.addonId,
20567
20635
  targetId: target.id,
@@ -20590,6 +20658,8 @@ var NcDispatcher = class {
20590
20658
  target: target.name,
20591
20659
  profile: result.profile,
20592
20660
  bytes: result.mp4.byteLength,
20661
+ attachments: attachments.map((a) => a.mediaType),
20662
+ anchoredOn: anchor.source,
20593
20663
  lagMs: this.now() - entry.updatedAt
20594
20664
  }
20595
20665
  });
@@ -20609,6 +20679,84 @@ var NcDispatcher = class {
20609
20679
  });
20610
20680
  }
20611
20681
  /**
20682
+ * What a recorded follow-up for this row would CARRY — the one answer both
20683
+ * the refusal line (`next`) and the follow-up read, so the log cannot promise
20684
+ * a follow-up the dispatcher will not send.
20685
+ *
20686
+ * A video when the rule asked for one. A gif only for a package PICK-UP
20687
+ * whose moment is KNOWN (D682): the gif is derived from the recorded cut's
20688
+ * own bytes, the same way the broker derives it from a ring cut — one cut,
20689
+ * two containers, never a second renderer.
20690
+ *
20691
+ * Narrow on purpose. A pick-up is the one kind whose moment is ALWAYS outside
20692
+ * the ring; every other kind keeps the gif behaviour it had (the ring's, or
20693
+ * none) rather than silently gaining a recorder export per notification. And
20694
+ * a pick-up that fell back to its conclusion gets no recorded footage at all,
20695
+ * gif OR video — cutting the recordings there is exactly the doormat D682
20696
+ * removed (see {@link NcRecordedGifRefusal}, {@link NcRecordedVideoRefusal}).
20697
+ */
20698
+ recordedFollowUp(entry) {
20699
+ if (this.deps.cutRecordedClip === void 0) return { kinds: [] };
20700
+ const gifRefused = this.recordedGifRefusal(entry);
20701
+ const videoRefused = this.recordedVideoRefusal(entry);
20702
+ const kinds = [];
20703
+ if (entry.payload.mediaGif === true && gifRefused === void 0) kinds.push("gif");
20704
+ if (entry.payload.mediaClip === true && videoRefused === void 0) kinds.push("video");
20705
+ return {
20706
+ kinds,
20707
+ ...gifRefused !== void 0 ? { gifRefused } : {},
20708
+ ...videoRefused !== void 0 ? { videoRefused } : {}
20709
+ };
20710
+ }
20711
+ /** Why this row's video is not cut from the recordings, or `undefined` when
20712
+ * it is (or when the rule asked for no video). */
20713
+ recordedVideoRefusal(entry) {
20714
+ if (entry.payload.mediaClip !== true) return void 0;
20715
+ const subject = entry.payload.subject;
20716
+ if (selfDatingKindOf(subject) !== "package pick-up") return void 0;
20717
+ if (anchorFellBack(mediaAnchor(subject), true)) return "moment-unknown";
20718
+ }
20719
+ /** Why this row's gif is not cut from the recordings, or `undefined` when it
20720
+ * is (or when the rule asked for no gif). */
20721
+ recordedGifRefusal(entry) {
20722
+ if (entry.payload.mediaGif !== true) return void 0;
20723
+ if (this.deps.deriveGifFromClip === void 0) return "no-derive";
20724
+ const subject = entry.payload.subject;
20725
+ if (selfDatingKindOf(subject) !== "package pick-up") return "not-a-pick-up";
20726
+ if (anchorFellBack(mediaAnchor(subject), true)) return "moment-unknown";
20727
+ }
20728
+ /**
20729
+ * The follow-up's attachments, gif first and video second — the order the
20730
+ * ring path ships them in. A gif that could not be derived is left out (its
20731
+ * reason already logged); the video, when asked for, still ships.
20732
+ */
20733
+ async recordedAttachments(entry, wanted, mp4) {
20734
+ const out = [];
20735
+ const derive = this.deps.deriveGifFromClip;
20736
+ if (wanted.includes("gif") && derive !== void 0) {
20737
+ const gif = await derive({
20738
+ deviceId: entry.payload.subject.deviceId,
20739
+ mp4,
20740
+ ...entry.payload.mediaClipSpeed !== void 0 ? { videoSpeed: entry.payload.mediaClipSpeed } : {},
20741
+ ruleId: entry.ruleId,
20742
+ recordId: entry.recordId
20743
+ });
20744
+ if (gif.ok) out.push({
20745
+ mediaType: "gif",
20746
+ bytes: gif.gif,
20747
+ mime: "image/gif",
20748
+ name: "event.gif"
20749
+ });
20750
+ }
20751
+ if (wanted.includes("video")) out.push({
20752
+ mediaType: "video",
20753
+ bytes: mp4,
20754
+ mime: "video/mp4",
20755
+ name: "event.mp4"
20756
+ });
20757
+ return out;
20758
+ }
20759
+ /**
20612
20760
  * Settle every recorded-clip follow-up this dispatcher has in flight.
20613
20761
  *
20614
20762
  * A TEST SEAM, and the honest kind: the follow-up is detached by design, so
@@ -32536,6 +32684,9 @@ var NotificationCenter = class NotificationCenter {
32536
32684
  * delivered/picked-up), in the SAME moment as the durable insert. Feeds the
32537
32685
  * `package-event` trigger. Package events ARE object-event rows, so the boot
32538
32686
  * crash-gap reconcile re-covers them (routed by className in {@link reconcile}).
32687
+ *
32688
+ * When a pick-up HAPPENED is on the row (`event.observedAt`, D684), so this
32689
+ * hook and the reconcile read it from the same place.
32539
32690
  */
32540
32691
  onPackageEventPersisted(event, phase) {
32541
32692
  this.consumeEvent(incomingFromPackageEvent(event, phase));
@@ -53030,7 +53181,9 @@ var MOTION_EVENTS_COLLECTION = "pipeline-analytics:motion-events";
53030
53181
  * @durable class=ledger owner=pipeline-analytics
53031
53182
  * write="one row per detected object per processed frame — every tracked
53032
53183
  * detection the frame processor emits, plus the appearance events and the
53033
- * synthetic package-drop events. Every row carries the trackId it belongs to."
53184
+ * synthetic package-drop events. Every row carries the trackId it belongs to.
53185
+ * A package PICK-UP row also carries observedAt, when the parcel went,
53186
+ * beside its timestamp, when the departure was concluded (D684)."
53034
53187
  * retention="TRACK-OWNED: it has no clock of its own. Rows go only when their
53035
53188
  * track goes, through EventStore.deleteByTracks — which folds in the deletion
53036
53189
  * of the event's child crops so a caller cannot take the events and leave the
@@ -53237,7 +53390,21 @@ var OBJECT_COLUMNS = [
53237
53390
  {
53238
53391
  name: "importance",
53239
53392
  type: "REAL"
53240
- }
53393
+ },
53394
+ (
53395
+ /**
53396
+ * When a package PICK-UP happened, beside `timestamp` (when it was
53397
+ * CONCLUDED, five minutes later) — see `ObjectEvent.observedAt` (D684).
53398
+ *
53399
+ * DECLARED, or the backend drops it on write (see `frameId`). Additive: the
53400
+ * collection gains the column in place (`ALTER TABLE … ADD COLUMN`), and every
53401
+ * row written before it reads SQL NULL → absent after `stripNulls`, which is
53402
+ * the truth about it: the moment is unknown. Only the package detector sets it.
53403
+ */
53404
+ {
53405
+ name: "observedAt",
53406
+ type: "INTEGER"
53407
+ })
53241
53408
  ];
53242
53409
  var AUDIO_COLUMNS = [
53243
53410
  ...COMMON_BASE_COLUMNS,
@@ -59025,6 +59192,180 @@ function decodeBase64(base64) {
59025
59192
  }
59026
59193
  }
59027
59194
  //#endregion
59195
+ //#region src/notification-center/recorded-gif.ts
59196
+ /**
59197
+ * The notification GIF for a moment only the RECORDINGS still hold — derived
59198
+ * from the recorded clip's own bytes (D682).
59199
+ *
59200
+ * ## Why this exists
59201
+ *
59202
+ * A package pick-up is concluded five minutes after the parcel went
59203
+ * (`PACKAGE_STILLNESS_TTL_MS`). Once its footage is anchored on the pick-up
59204
+ * itself rather than on the conclusion (`media-anchor.ts`), the window is far
59205
+ * older than any clip ring (≤ 60 s, `NC_CLIP_RETENTION_MAX_SEC`), so the ring
59206
+ * refuses it. A gif-only rule then had NOTHING: the recorded follow-up served
59207
+ * only a rule that asked for a video. The honest fix for "the gif shows the
59208
+ * empty doormat" is not "no gif" — it is the gif of the pick-up, from the
59209
+ * recordings.
59210
+ *
59211
+ * ## One cut, and the gif is DERIVED from it
59212
+ *
59213
+ * The recorded clip (`recorded-clip.ts`, one `recordingExport` render) is the
59214
+ * only cut. The gif is derived from ITS bytes with the SAME filter chain the
59215
+ * broker uses on a ring cut (`gifFromMp4Args` in `@camstack/types`), so a gif
59216
+ * of an old moment and a gif of a live one look the same, and a rule that asks
59217
+ * for both a gif and a video gets two containers of ONE window — the guarantee
59218
+ * `NcEventMediaRequest` exists for.
59219
+ *
59220
+ * ## Never throws, every miss named
59221
+ *
59222
+ * `derive-failed` (ffmpeg could not produce it) and `stage-failed` (the clip
59223
+ * could not be written where ffmpeg can read it) are logged with
59224
+ * `tags: { deviceId }` (D400: a missing attachment names its reason).
59225
+ */
59226
+ /** Bound on one derive: a ~10 s window encodes in well under a second; a
59227
+ * derive that has not finished in 20 s is wedged. Same bound as the broker's. */
59228
+ var NC_RECORDED_GIF_TIMEOUT_MS = 2e4;
59229
+ /** A notification gif is a few hundred KB; this is headroom, not a budget. */
59230
+ var RECORDED_GIF_MAX_BUFFER = 32 * 1024 * 1024;
59231
+ /**
59232
+ * Derive the gif. Never throws; every `ok: false` has logged its reason.
59233
+ */
59234
+ async function deriveRecordedGif(ports, req) {
59235
+ const tags = { deviceId: req.deviceId };
59236
+ const common = {
59237
+ ruleId: req.ruleId,
59238
+ eventId: req.recordId,
59239
+ sourceBytes: req.mp4.byteLength
59240
+ };
59241
+ let file;
59242
+ try {
59243
+ file = await ports.writeTemp(req.mp4);
59244
+ } catch (err) {
59245
+ ports.logger.warn("recorded gif: could not stage the recorded clip — no gif", {
59246
+ tags,
59247
+ meta: {
59248
+ ...common,
59249
+ reason: "stage-failed",
59250
+ error: String(err)
59251
+ }
59252
+ });
59253
+ return {
59254
+ ok: false,
59255
+ reason: "stage-failed"
59256
+ };
59257
+ }
59258
+ try {
59259
+ const speed = gifDeriveSpeed(4, req.videoSpeed ?? 1);
59260
+ const args = gifFromMp4Args({
59261
+ maxWidth: 640,
59262
+ fps: 12,
59263
+ speed,
59264
+ input: file
59265
+ });
59266
+ const startedAt = Date.now();
59267
+ let ran;
59268
+ try {
59269
+ ran = await ports.run(await ports.resolveFfmpeg(), args);
59270
+ } catch (err) {
59271
+ ran = {
59272
+ ok: false,
59273
+ error: String(err)
59274
+ };
59275
+ }
59276
+ const out = ran.ok ? ran.stdout : null;
59277
+ if (out === null || out.byteLength === 0) {
59278
+ ports.logger.warn("recorded gif: the derive failed — the follow-up carries no gif", {
59279
+ tags,
59280
+ meta: {
59281
+ ...common,
59282
+ reason: "derive-failed",
59283
+ error: ran.ok ? "ffmpeg produced no bytes" : ran.error.slice(-400)
59284
+ }
59285
+ });
59286
+ return {
59287
+ ok: false,
59288
+ reason: "derive-failed"
59289
+ };
59290
+ }
59291
+ const gif = new Uint8Array(new ArrayBuffer(out.byteLength));
59292
+ gif.set(out);
59293
+ ports.logger.info("recorded gif: derived from the recorded clip", {
59294
+ tags,
59295
+ meta: {
59296
+ ...common,
59297
+ gifBytes: gif.byteLength,
59298
+ width: 640,
59299
+ fps: 12,
59300
+ speed,
59301
+ encodeMs: Date.now() - startedAt
59302
+ }
59303
+ });
59304
+ return {
59305
+ ok: true,
59306
+ gif
59307
+ };
59308
+ } finally {
59309
+ await ports.removeTemp(file).catch((err) => {
59310
+ ports.logger.warn("recorded gif: could not remove the staged clip", {
59311
+ tags,
59312
+ meta: {
59313
+ ...common,
59314
+ file,
59315
+ error: String(err)
59316
+ }
59317
+ });
59318
+ });
59319
+ }
59320
+ }
59321
+ /** The production ports: the pinned ffmpeg, a temp file, a bounded child. */
59322
+ function buildRecordedGifPorts(input) {
59323
+ return {
59324
+ logger: input.logger,
59325
+ resolveFfmpeg: input.resolveFfmpeg,
59326
+ run: (cmd, args) => new Promise((resolve) => {
59327
+ execFile(cmd, [...args], {
59328
+ encoding: "buffer",
59329
+ maxBuffer: RECORDED_GIF_MAX_BUFFER,
59330
+ timeout: NC_RECORDED_GIF_TIMEOUT_MS,
59331
+ killSignal: "SIGKILL"
59332
+ }, (err, stdout) => resolve(err !== null ? {
59333
+ ok: false,
59334
+ error: String(err)
59335
+ } : {
59336
+ ok: true,
59337
+ stdout
59338
+ }));
59339
+ }),
59340
+ writeTemp: (bytes) => stageTempClip(path.join(os.tmpdir(), `camstack-nc-gif-${randomUUID()}.mp4`), bytes, {
59341
+ write: (file, data) => fs.writeFile(file, data),
59342
+ remove: (file) => fs.rm(file, { force: true })
59343
+ }),
59344
+ removeTemp: (file) => fs.rm(file, { force: true })
59345
+ };
59346
+ }
59347
+ /**
59348
+ * Write the clip to `file`, and on a failed write remove whatever landed.
59349
+ *
59350
+ * The path is named BEFORE the write, because a write can create the file and
59351
+ * then fail (ENOSPC half-way): the caller's `finally` only knows a path the
59352
+ * write RETURNED, so a partial `camstack-nc-gif-*.mp4` used to stay in the temp
59353
+ * directory for good. The write's error is re-thrown — the caller names it
59354
+ * (`stage-failed`, tagged per camera). A removal that ALSO fails is a real leak
59355
+ * and rides in that same error's message, so it reaches that one line rather
59356
+ * than a second, untagged one.
59357
+ */
59358
+ async function stageTempClip(file, bytes, io) {
59359
+ try {
59360
+ await io.write(file, bytes);
59361
+ return file;
59362
+ } catch (err) {
59363
+ const leaked = await io.remove(file).then(() => null, (removeErr) => String(removeErr));
59364
+ if (leaked === null) throw err;
59365
+ throw new Error(`${String(err)}; the partial file ${file} was NOT removed: ${leaked}`);
59366
+ }
59367
+ }
59368
+ //#endregion
59028
59369
  //#region src/shared/frame/crop-extractor.ts
59029
59370
  /**
59030
59371
  * The ONE reading of a normalized box against a frame: round, then clamp so
@@ -70498,10 +70839,12 @@ var PackageDropDetector = class {
70498
70839
  pickupAt: entry.lastConfirmedAt
70499
70840
  }
70500
70841
  });
70842
+ const observedAt = collector?.observedAt ?? pickupInstantOf(entry);
70501
70843
  const draft = {
70502
70844
  kind: "object",
70503
70845
  deviceId: entry.deviceId,
70504
70846
  timestamp,
70847
+ observedAt,
70505
70848
  source: "pipeline",
70506
70849
  idempotencyKey: pickedUpKey,
70507
70850
  trackId: mediaTrackId,
@@ -70553,6 +70896,8 @@ var PackageDropDetector = class {
70553
70896
  idempotencyKey: pickedUpKey,
70554
70897
  deliveredEventId: delivered.id,
70555
70898
  mediaTrackId,
70899
+ observedAt,
70900
+ concludedLagMs: timestamp - observedAt,
70556
70901
  ...collector !== void 0 ? {
70557
70902
  collectorTrackId: collector.trackId,
70558
70903
  collectorClassName: collector.className,
@@ -89067,6 +89412,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89067
89412
  readExportBytes: (input) => api.recordingExport.readExportBytes.query(input),
89068
89413
  deleteExport: (input) => api.recordingExport.deleteExport.mutate(input)
89069
89414
  }, req),
89415
+ deriveGifFromClip: (req) => deriveRecordedGif(buildRecordedGifPorts({
89416
+ logger: logger.child("RecordedGif"),
89417
+ resolveFfmpeg: () => this.ctx.deps.ensureFfmpeg()
89418
+ }), req),
89070
89419
  publishArtifact: async (bytes, mime, deviceId) => await this.ncArtifactPlane?.publish(bytes, mime, deviceId) ?? null,
89071
89420
  viewerUrlBase: async () => pickArtifactBaseUrl(await this.ncEndpoints()),
89072
89421
  indexArtifact: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? {
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Bk7ACano.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-thT_ff8Z.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.298",
3
+ "version": "1.2.299",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",