@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.
- package/dist/{clip-model-registry-D9WaswEs.mjs → clip-model-registry-DCztUu1G.mjs} +100 -1
- package/dist/{clip-model-registry-olxNKN7T.js → clip-model-registry-hTbsQ-z7.js} +111 -0
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Bk7ACano.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-thT_ff8Z.mjs} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D1CfMkm2.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C3QGV7zb.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-DtPqVNsM.mjs → hostInit-D9z3Da9D.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +370 -20
- package/dist/pipeline-analytics/index.mjs +385 -36
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -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-
|
|
5
|
+
const require_clip_model_registry = require("../clip-model-registry-hTbsQ-z7.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");
|
|
@@ -12,6 +12,8 @@ let node_crypto = require("node:crypto");
|
|
|
12
12
|
let node_child_process = require("node:child_process");
|
|
13
13
|
let _camstack_system_addon_utils = require("@camstack/system/addon-utils");
|
|
14
14
|
let node_stream_promises = require("node:stream/promises");
|
|
15
|
+
let node_fs_promises = require("node:fs/promises");
|
|
16
|
+
node_fs_promises = require_clip_model_registry.__toESM(node_fs_promises, 1);
|
|
15
17
|
let sharp = require("sharp");
|
|
16
18
|
sharp = require_clip_model_registry.__toESM(sharp);
|
|
17
19
|
let node_os = require("node:os");
|
|
@@ -9980,14 +9982,25 @@ function occupancySubjectFromEdge(edge) {
|
|
|
9980
9982
|
threshold: edge.threshold
|
|
9981
9983
|
};
|
|
9982
9984
|
}
|
|
9983
|
-
/**
|
|
9984
|
-
*
|
|
9985
|
+
/**
|
|
9986
|
+
* Build the subject for a `package-event` evaluation (a persisted `package`
|
|
9987
|
+
* object-event; `phase` derives from the event `state` at the call site).
|
|
9988
|
+
*
|
|
9989
|
+
* `observedAt` (D682, D684): a pick-up row's `timestamp` is when the departure
|
|
9990
|
+
* was CONCLUDED — five minutes of unseen observed time after the parcel went —
|
|
9991
|
+
* so the detector STORES the instant it happened on the row, and the footage is
|
|
9992
|
+
* cut around that (`media-anchor.ts`). Read from the row HERE, the one place
|
|
9993
|
+
* every path builds a package subject (live hook, boot reconcile, dry-run), so
|
|
9994
|
+
* none of them can anchor differently. `timestamp` keeps its meaning; a row
|
|
9995
|
+
* written before D684 has none, which the anchor reads as unknown, out loud.
|
|
9996
|
+
*/
|
|
9985
9997
|
function subjectFromPackageEvent(ev, phase) {
|
|
9986
9998
|
return {
|
|
9987
9999
|
kind: "package-event",
|
|
9988
10000
|
recordId: String(ev.id),
|
|
9989
10001
|
deviceId: ev.deviceId,
|
|
9990
10002
|
timestamp: ev.timestamp,
|
|
10003
|
+
...ev.observedAt !== void 0 ? { observedAt: ev.observedAt } : {},
|
|
9991
10004
|
classNames: [ev.className],
|
|
9992
10005
|
...displayLabel(ev) !== void 0 ? { label: displayLabel(ev) } : {},
|
|
9993
10006
|
...recognitionPatch(ev.subLabelMeta),
|
|
@@ -10918,7 +10931,11 @@ function incomingFromAudioEvent(event, stillOwnerId) {
|
|
|
10918
10931
|
})
|
|
10919
10932
|
};
|
|
10920
10933
|
}
|
|
10921
|
-
/**
|
|
10934
|
+
/**
|
|
10935
|
+
* Package object-event persist (delivered / picked-up) — the live hook and the
|
|
10936
|
+
* boot reconcile alike. A pick-up's instant is read from the row
|
|
10937
|
+
* (`event.observedAt`, D684) inside `subjectFromPackageEvent`.
|
|
10938
|
+
*/
|
|
10922
10939
|
function incomingFromPackageEvent(event, phase) {
|
|
10923
10940
|
return {
|
|
10924
10941
|
subject: subjectFromPackageEvent(event, phase),
|
|
@@ -10928,7 +10945,8 @@ function incomingFromPackageEvent(event, phase) {
|
|
|
10928
10945
|
tags: { deviceId: event.deviceId },
|
|
10929
10946
|
meta: {
|
|
10930
10947
|
eventId: event.id,
|
|
10931
|
-
phase
|
|
10948
|
+
phase,
|
|
10949
|
+
...event.observedAt !== void 0 ? { observedAt: event.observedAt } : {}
|
|
10932
10950
|
}
|
|
10933
10951
|
})
|
|
10934
10952
|
};
|
|
@@ -19336,9 +19354,14 @@ function mediaAnchor(subject) {
|
|
|
19336
19354
|
lagMs: subject.timestamp - observedAt
|
|
19337
19355
|
};
|
|
19338
19356
|
}
|
|
19357
|
+
function selfDatingKindOf(subject) {
|
|
19358
|
+
if (subject.occupancy !== void 0) return "occupancy";
|
|
19359
|
+
if (subject.packagePhase === "picked-up") return "package pick-up";
|
|
19360
|
+
return null;
|
|
19361
|
+
}
|
|
19339
19362
|
/**
|
|
19340
19363
|
* True when this anchor is the conclusion instant AND the subject was one that
|
|
19341
|
-
* should have been able to date itself —
|
|
19364
|
+
* should have been able to date itself — see {@link selfDatingKindOf}.
|
|
19342
19365
|
*
|
|
19343
19366
|
* Split from {@link mediaAnchor} because the anchor must stay pure of any
|
|
19344
19367
|
* notion of subject KIND, while the log line that matters is exactly the one
|
|
@@ -20442,7 +20465,8 @@ var NcDispatcher = class {
|
|
|
20442
20465
|
};
|
|
20443
20466
|
const preRollSec = entry.payload.mediaClipPreRollSec ?? 4;
|
|
20444
20467
|
const anchor = mediaAnchor(entry.payload.subject);
|
|
20445
|
-
|
|
20468
|
+
const selfDating = selfDatingKindOf(entry.payload.subject);
|
|
20469
|
+
if (anchorFellBack(anchor, selfDating !== null)) this.deps.logger.warn(`${selfDating ?? "occupancy"} footage anchored on the conclusion — moment unknown`, {
|
|
20446
20470
|
tags: { deviceId: entry.payload.subject.deviceId },
|
|
20447
20471
|
meta: {
|
|
20448
20472
|
ruleId: entry.ruleId,
|
|
@@ -20470,7 +20494,7 @@ var NcDispatcher = class {
|
|
|
20470
20494
|
preRollSec,
|
|
20471
20495
|
retentionSec: this.ringRetentionSec(entry.payload.subject.deviceId),
|
|
20472
20496
|
wanted: [...wantGif ? ["gif"] : [], ...wantMp4 ? ["video"] : []],
|
|
20473
|
-
next: this.
|
|
20497
|
+
next: this.recordedFollowUp(entry).kinds.length > 0 ? "recorded-clip-follow-up" : "no-clip"
|
|
20474
20498
|
}
|
|
20475
20499
|
});
|
|
20476
20500
|
return {
|
|
@@ -20524,6 +20548,13 @@ var NcDispatcher = class {
|
|
|
20524
20548
|
* alert for a complete one, which is precisely the wrong trade for the
|
|
20525
20549
|
* notification that tells an operator someone is at the door.
|
|
20526
20550
|
*
|
|
20551
|
+
* A GIF rule is served here too (D682): a package pick-up is concluded five
|
|
20552
|
+
* minutes after it happened, so a gif anchored on the pick-up is always
|
|
20553
|
+
* outside the ring. The gif is derived from THIS cut's bytes
|
|
20554
|
+
* (`deriveGifFromClip`) — one cut, never a second renderer — and a moment the
|
|
20555
|
+
* recordings cannot serve ships NO gif, by named reason, rather than a gif of
|
|
20556
|
+
* the wrong time.
|
|
20557
|
+
*
|
|
20527
20558
|
* So the alert's latency is UNCHANGED, and **the clip is attached later**, as
|
|
20528
20559
|
* a follow-up send to the same target carrying the same `tag` — a phone
|
|
20529
20560
|
* collapses the two, so what the operator sees is the notification gaining
|
|
@@ -20546,7 +20577,9 @@ var NcDispatcher = class {
|
|
|
20546
20577
|
*/
|
|
20547
20578
|
startRecordedFootage(entry, target, notification) {
|
|
20548
20579
|
const cut = this.deps.cutRecordedClip;
|
|
20549
|
-
|
|
20580
|
+
const plan = this.recordedFollowUp(entry);
|
|
20581
|
+
const wanted = plan.kinds;
|
|
20582
|
+
if (cut === void 0 || wanted.length === 0 && plan.gifRefused === void 0 && plan.videoRefused === void 0) return;
|
|
20550
20583
|
const preRollSec = entry.payload.mediaClipPreRollSec ?? 4;
|
|
20551
20584
|
const anchor = mediaAnchor(entry.payload.subject);
|
|
20552
20585
|
if (footageSourceFor({
|
|
@@ -20556,34 +20589,70 @@ var NcDispatcher = class {
|
|
|
20556
20589
|
ringRetentionSec: this.ringRetentionSec(entry.payload.subject.deviceId)
|
|
20557
20590
|
}) !== "recordings") return;
|
|
20558
20591
|
const deviceId = entry.payload.subject.deviceId;
|
|
20592
|
+
if (plan.gifRefused !== void 0) this.deps.logger.info("recorded gif follow-up: none — the gif is not cut from the recordings", {
|
|
20593
|
+
tags: { deviceId },
|
|
20594
|
+
meta: {
|
|
20595
|
+
ruleId: entry.ruleId,
|
|
20596
|
+
eventId: entry.recordId,
|
|
20597
|
+
targetId: target.id,
|
|
20598
|
+
reason: plan.gifRefused,
|
|
20599
|
+
anchoredOn: anchor.source,
|
|
20600
|
+
...anchor.fallback !== void 0 ? { anchorFallback: anchor.fallback } : {},
|
|
20601
|
+
next: wanted
|
|
20602
|
+
}
|
|
20603
|
+
});
|
|
20604
|
+
if (plan.videoRefused !== void 0) this.deps.logger.info("recorded video follow-up: none — the video is not cut from the recordings", {
|
|
20605
|
+
tags: { deviceId },
|
|
20606
|
+
meta: {
|
|
20607
|
+
ruleId: entry.ruleId,
|
|
20608
|
+
eventId: entry.recordId,
|
|
20609
|
+
targetId: target.id,
|
|
20610
|
+
reason: plan.videoRefused,
|
|
20611
|
+
anchoredOn: anchor.source,
|
|
20612
|
+
...anchor.fallback !== void 0 ? { anchorFallback: anchor.fallback } : {},
|
|
20613
|
+
next: wanted
|
|
20614
|
+
}
|
|
20615
|
+
});
|
|
20616
|
+
if (wanted.length === 0) return;
|
|
20617
|
+
const videoSpeed = entry.payload.mediaClipSpeed;
|
|
20559
20618
|
const pending = (async () => {
|
|
20560
20619
|
const result = await cut({
|
|
20561
20620
|
deviceId,
|
|
20562
20621
|
aroundMs: anchor.atMs,
|
|
20563
20622
|
preRollSec,
|
|
20564
20623
|
postRollSec: entry.payload.mediaClipPostRollSec ?? 6,
|
|
20565
|
-
...
|
|
20624
|
+
...videoSpeed !== void 0 ? { speed: videoSpeed } : {},
|
|
20566
20625
|
ruleId: entry.ruleId,
|
|
20567
20626
|
recordId: entry.recordId
|
|
20568
20627
|
});
|
|
20569
20628
|
if (!result.ok) {
|
|
20570
|
-
this.deps.logger.info("recorded
|
|
20629
|
+
this.deps.logger.info("recorded footage follow-up: none — the recorded cut failed", {
|
|
20571
20630
|
tags: { deviceId },
|
|
20572
20631
|
meta: {
|
|
20573
20632
|
ruleId: entry.ruleId,
|
|
20574
20633
|
eventId: entry.recordId,
|
|
20575
20634
|
targetId: target.id,
|
|
20576
|
-
reason: result.reason
|
|
20635
|
+
reason: result.reason,
|
|
20636
|
+
wanted
|
|
20577
20637
|
}
|
|
20578
20638
|
});
|
|
20579
20639
|
return;
|
|
20580
20640
|
}
|
|
20581
|
-
const
|
|
20582
|
-
|
|
20583
|
-
|
|
20584
|
-
|
|
20585
|
-
|
|
20586
|
-
|
|
20641
|
+
const attachments = await this.recordedAttachments(entry, wanted, result.mp4);
|
|
20642
|
+
if (attachments.length === 0) {
|
|
20643
|
+
this.deps.logger.info("recorded clip follow-up: none — the gif could not be derived", {
|
|
20644
|
+
tags: { deviceId },
|
|
20645
|
+
meta: {
|
|
20646
|
+
ruleId: entry.ruleId,
|
|
20647
|
+
eventId: entry.recordId,
|
|
20648
|
+
targetId: target.id,
|
|
20649
|
+
reason: "gif-derive-failed",
|
|
20650
|
+
wanted
|
|
20651
|
+
}
|
|
20652
|
+
});
|
|
20653
|
+
return;
|
|
20654
|
+
}
|
|
20655
|
+
const withUrls = await this.withArtifactUrls(entry, attachments);
|
|
20587
20656
|
const sent = await this.deps.send({
|
|
20588
20657
|
addonId: target.addonId,
|
|
20589
20658
|
targetId: target.id,
|
|
@@ -20612,6 +20681,8 @@ var NcDispatcher = class {
|
|
|
20612
20681
|
target: target.name,
|
|
20613
20682
|
profile: result.profile,
|
|
20614
20683
|
bytes: result.mp4.byteLength,
|
|
20684
|
+
attachments: attachments.map((a) => a.mediaType),
|
|
20685
|
+
anchoredOn: anchor.source,
|
|
20615
20686
|
lagMs: this.now() - entry.updatedAt
|
|
20616
20687
|
}
|
|
20617
20688
|
});
|
|
@@ -20631,6 +20702,84 @@ var NcDispatcher = class {
|
|
|
20631
20702
|
});
|
|
20632
20703
|
}
|
|
20633
20704
|
/**
|
|
20705
|
+
* What a recorded follow-up for this row would CARRY — the one answer both
|
|
20706
|
+
* the refusal line (`next`) and the follow-up read, so the log cannot promise
|
|
20707
|
+
* a follow-up the dispatcher will not send.
|
|
20708
|
+
*
|
|
20709
|
+
* A video when the rule asked for one. A gif only for a package PICK-UP
|
|
20710
|
+
* whose moment is KNOWN (D682): the gif is derived from the recorded cut's
|
|
20711
|
+
* own bytes, the same way the broker derives it from a ring cut — one cut,
|
|
20712
|
+
* two containers, never a second renderer.
|
|
20713
|
+
*
|
|
20714
|
+
* Narrow on purpose. A pick-up is the one kind whose moment is ALWAYS outside
|
|
20715
|
+
* the ring; every other kind keeps the gif behaviour it had (the ring's, or
|
|
20716
|
+
* none) rather than silently gaining a recorder export per notification. And
|
|
20717
|
+
* a pick-up that fell back to its conclusion gets no recorded footage at all,
|
|
20718
|
+
* gif OR video — cutting the recordings there is exactly the doormat D682
|
|
20719
|
+
* removed (see {@link NcRecordedGifRefusal}, {@link NcRecordedVideoRefusal}).
|
|
20720
|
+
*/
|
|
20721
|
+
recordedFollowUp(entry) {
|
|
20722
|
+
if (this.deps.cutRecordedClip === void 0) return { kinds: [] };
|
|
20723
|
+
const gifRefused = this.recordedGifRefusal(entry);
|
|
20724
|
+
const videoRefused = this.recordedVideoRefusal(entry);
|
|
20725
|
+
const kinds = [];
|
|
20726
|
+
if (entry.payload.mediaGif === true && gifRefused === void 0) kinds.push("gif");
|
|
20727
|
+
if (entry.payload.mediaClip === true && videoRefused === void 0) kinds.push("video");
|
|
20728
|
+
return {
|
|
20729
|
+
kinds,
|
|
20730
|
+
...gifRefused !== void 0 ? { gifRefused } : {},
|
|
20731
|
+
...videoRefused !== void 0 ? { videoRefused } : {}
|
|
20732
|
+
};
|
|
20733
|
+
}
|
|
20734
|
+
/** Why this row's video is not cut from the recordings, or `undefined` when
|
|
20735
|
+
* it is (or when the rule asked for no video). */
|
|
20736
|
+
recordedVideoRefusal(entry) {
|
|
20737
|
+
if (entry.payload.mediaClip !== true) return void 0;
|
|
20738
|
+
const subject = entry.payload.subject;
|
|
20739
|
+
if (selfDatingKindOf(subject) !== "package pick-up") return void 0;
|
|
20740
|
+
if (anchorFellBack(mediaAnchor(subject), true)) return "moment-unknown";
|
|
20741
|
+
}
|
|
20742
|
+
/** Why this row's gif is not cut from the recordings, or `undefined` when it
|
|
20743
|
+
* is (or when the rule asked for no gif). */
|
|
20744
|
+
recordedGifRefusal(entry) {
|
|
20745
|
+
if (entry.payload.mediaGif !== true) return void 0;
|
|
20746
|
+
if (this.deps.deriveGifFromClip === void 0) return "no-derive";
|
|
20747
|
+
const subject = entry.payload.subject;
|
|
20748
|
+
if (selfDatingKindOf(subject) !== "package pick-up") return "not-a-pick-up";
|
|
20749
|
+
if (anchorFellBack(mediaAnchor(subject), true)) return "moment-unknown";
|
|
20750
|
+
}
|
|
20751
|
+
/**
|
|
20752
|
+
* The follow-up's attachments, gif first and video second — the order the
|
|
20753
|
+
* ring path ships them in. A gif that could not be derived is left out (its
|
|
20754
|
+
* reason already logged); the video, when asked for, still ships.
|
|
20755
|
+
*/
|
|
20756
|
+
async recordedAttachments(entry, wanted, mp4) {
|
|
20757
|
+
const out = [];
|
|
20758
|
+
const derive = this.deps.deriveGifFromClip;
|
|
20759
|
+
if (wanted.includes("gif") && derive !== void 0) {
|
|
20760
|
+
const gif = await derive({
|
|
20761
|
+
deviceId: entry.payload.subject.deviceId,
|
|
20762
|
+
mp4,
|
|
20763
|
+
...entry.payload.mediaClipSpeed !== void 0 ? { videoSpeed: entry.payload.mediaClipSpeed } : {},
|
|
20764
|
+
ruleId: entry.ruleId,
|
|
20765
|
+
recordId: entry.recordId
|
|
20766
|
+
});
|
|
20767
|
+
if (gif.ok) out.push({
|
|
20768
|
+
mediaType: "gif",
|
|
20769
|
+
bytes: gif.gif,
|
|
20770
|
+
mime: "image/gif",
|
|
20771
|
+
name: "event.gif"
|
|
20772
|
+
});
|
|
20773
|
+
}
|
|
20774
|
+
if (wanted.includes("video")) out.push({
|
|
20775
|
+
mediaType: "video",
|
|
20776
|
+
bytes: mp4,
|
|
20777
|
+
mime: "video/mp4",
|
|
20778
|
+
name: "event.mp4"
|
|
20779
|
+
});
|
|
20780
|
+
return out;
|
|
20781
|
+
}
|
|
20782
|
+
/**
|
|
20634
20783
|
* Settle every recorded-clip follow-up this dispatcher has in flight.
|
|
20635
20784
|
*
|
|
20636
20785
|
* A TEST SEAM, and the honest kind: the follow-up is detached by design, so
|
|
@@ -32558,6 +32707,9 @@ var NotificationCenter = class NotificationCenter {
|
|
|
32558
32707
|
* delivered/picked-up), in the SAME moment as the durable insert. Feeds the
|
|
32559
32708
|
* `package-event` trigger. Package events ARE object-event rows, so the boot
|
|
32560
32709
|
* crash-gap reconcile re-covers them (routed by className in {@link reconcile}).
|
|
32710
|
+
*
|
|
32711
|
+
* When a pick-up HAPPENED is on the row (`event.observedAt`, D684), so this
|
|
32712
|
+
* hook and the reconcile read it from the same place.
|
|
32561
32713
|
*/
|
|
32562
32714
|
onPackageEventPersisted(event, phase) {
|
|
32563
32715
|
this.consumeEvent(incomingFromPackageEvent(event, phase));
|
|
@@ -53055,7 +53207,9 @@ var MOTION_EVENTS_COLLECTION = "pipeline-analytics:motion-events";
|
|
|
53055
53207
|
* @durable class=ledger owner=pipeline-analytics
|
|
53056
53208
|
* write="one row per detected object per processed frame — every tracked
|
|
53057
53209
|
* detection the frame processor emits, plus the appearance events and the
|
|
53058
|
-
* synthetic package-drop events. Every row carries the trackId it belongs to.
|
|
53210
|
+
* synthetic package-drop events. Every row carries the trackId it belongs to.
|
|
53211
|
+
* A package PICK-UP row also carries observedAt, when the parcel went,
|
|
53212
|
+
* beside its timestamp, when the departure was concluded (D684)."
|
|
53059
53213
|
* retention="TRACK-OWNED: it has no clock of its own. Rows go only when their
|
|
53060
53214
|
* track goes, through EventStore.deleteByTracks — which folds in the deletion
|
|
53061
53215
|
* of the event's child crops so a caller cannot take the events and leave the
|
|
@@ -53262,7 +53416,21 @@ var OBJECT_COLUMNS = [
|
|
|
53262
53416
|
{
|
|
53263
53417
|
name: "importance",
|
|
53264
53418
|
type: "REAL"
|
|
53265
|
-
}
|
|
53419
|
+
},
|
|
53420
|
+
(
|
|
53421
|
+
/**
|
|
53422
|
+
* When a package PICK-UP happened, beside `timestamp` (when it was
|
|
53423
|
+
* CONCLUDED, five minutes later) — see `ObjectEvent.observedAt` (D684).
|
|
53424
|
+
*
|
|
53425
|
+
* DECLARED, or the backend drops it on write (see `frameId`). Additive: the
|
|
53426
|
+
* collection gains the column in place (`ALTER TABLE … ADD COLUMN`), and every
|
|
53427
|
+
* row written before it reads SQL NULL → absent after `stripNulls`, which is
|
|
53428
|
+
* the truth about it: the moment is unknown. Only the package detector sets it.
|
|
53429
|
+
*/
|
|
53430
|
+
{
|
|
53431
|
+
name: "observedAt",
|
|
53432
|
+
type: "INTEGER"
|
|
53433
|
+
})
|
|
53266
53434
|
];
|
|
53267
53435
|
var AUDIO_COLUMNS = [
|
|
53268
53436
|
...COMMON_BASE_COLUMNS,
|
|
@@ -59101,6 +59269,180 @@ function decodeBase64(base64) {
|
|
|
59101
59269
|
}
|
|
59102
59270
|
}
|
|
59103
59271
|
//#endregion
|
|
59272
|
+
//#region src/notification-center/recorded-gif.ts
|
|
59273
|
+
/**
|
|
59274
|
+
* The notification GIF for a moment only the RECORDINGS still hold — derived
|
|
59275
|
+
* from the recorded clip's own bytes (D682).
|
|
59276
|
+
*
|
|
59277
|
+
* ## Why this exists
|
|
59278
|
+
*
|
|
59279
|
+
* A package pick-up is concluded five minutes after the parcel went
|
|
59280
|
+
* (`PACKAGE_STILLNESS_TTL_MS`). Once its footage is anchored on the pick-up
|
|
59281
|
+
* itself rather than on the conclusion (`media-anchor.ts`), the window is far
|
|
59282
|
+
* older than any clip ring (≤ 60 s, `NC_CLIP_RETENTION_MAX_SEC`), so the ring
|
|
59283
|
+
* refuses it. A gif-only rule then had NOTHING: the recorded follow-up served
|
|
59284
|
+
* only a rule that asked for a video. The honest fix for "the gif shows the
|
|
59285
|
+
* empty doormat" is not "no gif" — it is the gif of the pick-up, from the
|
|
59286
|
+
* recordings.
|
|
59287
|
+
*
|
|
59288
|
+
* ## One cut, and the gif is DERIVED from it
|
|
59289
|
+
*
|
|
59290
|
+
* The recorded clip (`recorded-clip.ts`, one `recordingExport` render) is the
|
|
59291
|
+
* only cut. The gif is derived from ITS bytes with the SAME filter chain the
|
|
59292
|
+
* broker uses on a ring cut (`gifFromMp4Args` in `@camstack/types`), so a gif
|
|
59293
|
+
* of an old moment and a gif of a live one look the same, and a rule that asks
|
|
59294
|
+
* for both a gif and a video gets two containers of ONE window — the guarantee
|
|
59295
|
+
* `NcEventMediaRequest` exists for.
|
|
59296
|
+
*
|
|
59297
|
+
* ## Never throws, every miss named
|
|
59298
|
+
*
|
|
59299
|
+
* `derive-failed` (ffmpeg could not produce it) and `stage-failed` (the clip
|
|
59300
|
+
* could not be written where ffmpeg can read it) are logged with
|
|
59301
|
+
* `tags: { deviceId }` (D400: a missing attachment names its reason).
|
|
59302
|
+
*/
|
|
59303
|
+
/** Bound on one derive: a ~10 s window encodes in well under a second; a
|
|
59304
|
+
* derive that has not finished in 20 s is wedged. Same bound as the broker's. */
|
|
59305
|
+
var NC_RECORDED_GIF_TIMEOUT_MS = 2e4;
|
|
59306
|
+
/** A notification gif is a few hundred KB; this is headroom, not a budget. */
|
|
59307
|
+
var RECORDED_GIF_MAX_BUFFER = 32 * 1024 * 1024;
|
|
59308
|
+
/**
|
|
59309
|
+
* Derive the gif. Never throws; every `ok: false` has logged its reason.
|
|
59310
|
+
*/
|
|
59311
|
+
async function deriveRecordedGif(ports, req) {
|
|
59312
|
+
const tags = { deviceId: req.deviceId };
|
|
59313
|
+
const common = {
|
|
59314
|
+
ruleId: req.ruleId,
|
|
59315
|
+
eventId: req.recordId,
|
|
59316
|
+
sourceBytes: req.mp4.byteLength
|
|
59317
|
+
};
|
|
59318
|
+
let file;
|
|
59319
|
+
try {
|
|
59320
|
+
file = await ports.writeTemp(req.mp4);
|
|
59321
|
+
} catch (err) {
|
|
59322
|
+
ports.logger.warn("recorded gif: could not stage the recorded clip — no gif", {
|
|
59323
|
+
tags,
|
|
59324
|
+
meta: {
|
|
59325
|
+
...common,
|
|
59326
|
+
reason: "stage-failed",
|
|
59327
|
+
error: String(err)
|
|
59328
|
+
}
|
|
59329
|
+
});
|
|
59330
|
+
return {
|
|
59331
|
+
ok: false,
|
|
59332
|
+
reason: "stage-failed"
|
|
59333
|
+
};
|
|
59334
|
+
}
|
|
59335
|
+
try {
|
|
59336
|
+
const speed = require_clip_model_registry.gifDeriveSpeed(4, req.videoSpeed ?? 1);
|
|
59337
|
+
const args = require_clip_model_registry.gifFromMp4Args({
|
|
59338
|
+
maxWidth: 640,
|
|
59339
|
+
fps: 12,
|
|
59340
|
+
speed,
|
|
59341
|
+
input: file
|
|
59342
|
+
});
|
|
59343
|
+
const startedAt = Date.now();
|
|
59344
|
+
let ran;
|
|
59345
|
+
try {
|
|
59346
|
+
ran = await ports.run(await ports.resolveFfmpeg(), args);
|
|
59347
|
+
} catch (err) {
|
|
59348
|
+
ran = {
|
|
59349
|
+
ok: false,
|
|
59350
|
+
error: String(err)
|
|
59351
|
+
};
|
|
59352
|
+
}
|
|
59353
|
+
const out = ran.ok ? ran.stdout : null;
|
|
59354
|
+
if (out === null || out.byteLength === 0) {
|
|
59355
|
+
ports.logger.warn("recorded gif: the derive failed — the follow-up carries no gif", {
|
|
59356
|
+
tags,
|
|
59357
|
+
meta: {
|
|
59358
|
+
...common,
|
|
59359
|
+
reason: "derive-failed",
|
|
59360
|
+
error: ran.ok ? "ffmpeg produced no bytes" : ran.error.slice(-400)
|
|
59361
|
+
}
|
|
59362
|
+
});
|
|
59363
|
+
return {
|
|
59364
|
+
ok: false,
|
|
59365
|
+
reason: "derive-failed"
|
|
59366
|
+
};
|
|
59367
|
+
}
|
|
59368
|
+
const gif = new Uint8Array(new ArrayBuffer(out.byteLength));
|
|
59369
|
+
gif.set(out);
|
|
59370
|
+
ports.logger.info("recorded gif: derived from the recorded clip", {
|
|
59371
|
+
tags,
|
|
59372
|
+
meta: {
|
|
59373
|
+
...common,
|
|
59374
|
+
gifBytes: gif.byteLength,
|
|
59375
|
+
width: 640,
|
|
59376
|
+
fps: 12,
|
|
59377
|
+
speed,
|
|
59378
|
+
encodeMs: Date.now() - startedAt
|
|
59379
|
+
}
|
|
59380
|
+
});
|
|
59381
|
+
return {
|
|
59382
|
+
ok: true,
|
|
59383
|
+
gif
|
|
59384
|
+
};
|
|
59385
|
+
} finally {
|
|
59386
|
+
await ports.removeTemp(file).catch((err) => {
|
|
59387
|
+
ports.logger.warn("recorded gif: could not remove the staged clip", {
|
|
59388
|
+
tags,
|
|
59389
|
+
meta: {
|
|
59390
|
+
...common,
|
|
59391
|
+
file,
|
|
59392
|
+
error: String(err)
|
|
59393
|
+
}
|
|
59394
|
+
});
|
|
59395
|
+
});
|
|
59396
|
+
}
|
|
59397
|
+
}
|
|
59398
|
+
/** The production ports: the pinned ffmpeg, a temp file, a bounded child. */
|
|
59399
|
+
function buildRecordedGifPorts(input) {
|
|
59400
|
+
return {
|
|
59401
|
+
logger: input.logger,
|
|
59402
|
+
resolveFfmpeg: input.resolveFfmpeg,
|
|
59403
|
+
run: (cmd, args) => new Promise((resolve) => {
|
|
59404
|
+
(0, node_child_process.execFile)(cmd, [...args], {
|
|
59405
|
+
encoding: "buffer",
|
|
59406
|
+
maxBuffer: RECORDED_GIF_MAX_BUFFER,
|
|
59407
|
+
timeout: NC_RECORDED_GIF_TIMEOUT_MS,
|
|
59408
|
+
killSignal: "SIGKILL"
|
|
59409
|
+
}, (err, stdout) => resolve(err !== null ? {
|
|
59410
|
+
ok: false,
|
|
59411
|
+
error: String(err)
|
|
59412
|
+
} : {
|
|
59413
|
+
ok: true,
|
|
59414
|
+
stdout
|
|
59415
|
+
}));
|
|
59416
|
+
}),
|
|
59417
|
+
writeTemp: (bytes) => stageTempClip(node_path.default.join(node_os.default.tmpdir(), `camstack-nc-gif-${(0, node_crypto.randomUUID)()}.mp4`), bytes, {
|
|
59418
|
+
write: (file, data) => node_fs_promises.default.writeFile(file, data),
|
|
59419
|
+
remove: (file) => node_fs_promises.default.rm(file, { force: true })
|
|
59420
|
+
}),
|
|
59421
|
+
removeTemp: (file) => node_fs_promises.default.rm(file, { force: true })
|
|
59422
|
+
};
|
|
59423
|
+
}
|
|
59424
|
+
/**
|
|
59425
|
+
* Write the clip to `file`, and on a failed write remove whatever landed.
|
|
59426
|
+
*
|
|
59427
|
+
* The path is named BEFORE the write, because a write can create the file and
|
|
59428
|
+
* then fail (ENOSPC half-way): the caller's `finally` only knows a path the
|
|
59429
|
+
* write RETURNED, so a partial `camstack-nc-gif-*.mp4` used to stay in the temp
|
|
59430
|
+
* directory for good. The write's error is re-thrown — the caller names it
|
|
59431
|
+
* (`stage-failed`, tagged per camera). A removal that ALSO fails is a real leak
|
|
59432
|
+
* and rides in that same error's message, so it reaches that one line rather
|
|
59433
|
+
* than a second, untagged one.
|
|
59434
|
+
*/
|
|
59435
|
+
async function stageTempClip(file, bytes, io) {
|
|
59436
|
+
try {
|
|
59437
|
+
await io.write(file, bytes);
|
|
59438
|
+
return file;
|
|
59439
|
+
} catch (err) {
|
|
59440
|
+
const leaked = await io.remove(file).then(() => null, (removeErr) => String(removeErr));
|
|
59441
|
+
if (leaked === null) throw err;
|
|
59442
|
+
throw new Error(`${String(err)}; the partial file ${file} was NOT removed: ${leaked}`);
|
|
59443
|
+
}
|
|
59444
|
+
}
|
|
59445
|
+
//#endregion
|
|
59104
59446
|
//#region src/shared/frame/crop-extractor.ts
|
|
59105
59447
|
/**
|
|
59106
59448
|
* The ONE reading of a normalized box against a frame: round, then clamp so
|
|
@@ -70574,10 +70916,12 @@ var PackageDropDetector = class {
|
|
|
70574
70916
|
pickupAt: entry.lastConfirmedAt
|
|
70575
70917
|
}
|
|
70576
70918
|
});
|
|
70919
|
+
const observedAt = collector?.observedAt ?? pickupInstantOf(entry);
|
|
70577
70920
|
const draft = {
|
|
70578
70921
|
kind: "object",
|
|
70579
70922
|
deviceId: entry.deviceId,
|
|
70580
70923
|
timestamp,
|
|
70924
|
+
observedAt,
|
|
70581
70925
|
source: "pipeline",
|
|
70582
70926
|
idempotencyKey: pickedUpKey,
|
|
70583
70927
|
trackId: mediaTrackId,
|
|
@@ -70629,6 +70973,8 @@ var PackageDropDetector = class {
|
|
|
70629
70973
|
idempotencyKey: pickedUpKey,
|
|
70630
70974
|
deliveredEventId: delivered.id,
|
|
70631
70975
|
mediaTrackId,
|
|
70976
|
+
observedAt,
|
|
70977
|
+
concludedLagMs: timestamp - observedAt,
|
|
70632
70978
|
...collector !== void 0 ? {
|
|
70633
70979
|
collectorTrackId: collector.trackId,
|
|
70634
70980
|
collectorClassName: collector.className,
|
|
@@ -89143,6 +89489,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
89143
89489
|
readExportBytes: (input) => api.recordingExport.readExportBytes.query(input),
|
|
89144
89490
|
deleteExport: (input) => api.recordingExport.deleteExport.mutate(input)
|
|
89145
89491
|
}, req),
|
|
89492
|
+
deriveGifFromClip: (req) => deriveRecordedGif(buildRecordedGifPorts({
|
|
89493
|
+
logger: logger.child("RecordedGif"),
|
|
89494
|
+
resolveFfmpeg: () => this.ctx.deps.ensureFfmpeg()
|
|
89495
|
+
}), req),
|
|
89146
89496
|
publishArtifact: async (bytes, mime, deviceId) => await this.ncArtifactPlane?.publish(bytes, mime, deviceId) ?? null,
|
|
89147
89497
|
viewerUrlBase: async () => pickArtifactBaseUrl(await this.ncEndpoints()),
|
|
89148
89498
|
indexArtifact: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? {
|