@camstack/addon-post-analysis 1.1.26 → 1.1.27
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/{dist-DytVmDZg.mjs → dist-BqOBYSWs.mjs} +45 -3
- package/dist/{dist-BEx5ST1W.js → dist-CtnFKuWh.js} +45 -3
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/enrichment-engine/index.js +2 -2
- package/dist/enrichment-engine/index.mjs +1 -1
- package/dist/{node-Cg_cGqs0.js → node-BN31iDiA.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-p-r6oXq8.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Dvz_QBOD.mjs} +3 -3
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C6LbOCa9.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BXQHc7Tq.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-2UZIpU0W.mjs → hostInit-C7nUOwbi.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +588 -6
- package/dist/pipeline-analytics/index.mjs +586 -4
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/dist/{resolve-frame-Cbm_NFuq.js → resolve-frame-BAdpVnlx.js} +1 -1
- package/package.json +1 -1
|
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_resolve_frame = require("../resolve-frame-
|
|
5
|
+
const require_dist = require("../dist-CtnFKuWh.js");
|
|
6
|
+
const require_resolve_frame = require("../resolve-frame-BAdpVnlx.js");
|
|
7
7
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
8
8
|
let sharp = require("sharp");
|
|
9
9
|
sharp = require_dist.__toESM(sharp);
|
|
@@ -1527,6 +1527,7 @@ var FrameProcessor = class {
|
|
|
1527
1527
|
const embeddingByBbox = /* @__PURE__ */ new Map();
|
|
1528
1528
|
const firstLevelBboxById = /* @__PURE__ */ new Map();
|
|
1529
1529
|
const faceBboxByBbox = /* @__PURE__ */ new Map();
|
|
1530
|
+
const faceAlignedCropByBbox = /* @__PURE__ */ new Map();
|
|
1530
1531
|
const plateByBbox = /* @__PURE__ */ new Map();
|
|
1531
1532
|
const maskByBbox = /* @__PURE__ */ new Map();
|
|
1532
1533
|
const flatDetections = frame.detections.filter((d) => d.kind === "first-level").map((det) => {
|
|
@@ -1569,6 +1570,7 @@ var FrameProcessor = class {
|
|
|
1569
1570
|
w: det.bbox.width,
|
|
1570
1571
|
h: det.bbox.height
|
|
1571
1572
|
});
|
|
1573
|
+
if (det.faceAlignedCrop !== void 0) faceAlignedCropByBbox.set(parentBbox, det.faceAlignedCrop);
|
|
1572
1574
|
if (det.embedding !== void 0 && !embeddingByBbox.has(parentBbox)) embeddingByBbox.set(parentBbox, {
|
|
1573
1575
|
embedding: det.embedding,
|
|
1574
1576
|
...det.embeddingModelId !== void 0 ? { embeddingModelId: det.embeddingModelId } : {}
|
|
@@ -1611,6 +1613,7 @@ var FrameProcessor = class {
|
|
|
1611
1613
|
});
|
|
1612
1614
|
const emb = embeddingByBbox.get(td.bbox);
|
|
1613
1615
|
const faceBbox = faceBboxByBbox.get(td.bbox);
|
|
1616
|
+
const faceAlignedCrop = faceAlignedCropByBbox.get(td.bbox);
|
|
1614
1617
|
const plate = plateByBbox.get(td.bbox);
|
|
1615
1618
|
return {
|
|
1616
1619
|
trackId: td.trackId,
|
|
@@ -1625,6 +1628,7 @@ var FrameProcessor = class {
|
|
|
1625
1628
|
...emb.embeddingModelId !== void 0 ? { embeddingModelId: emb.embeddingModelId } : {}
|
|
1626
1629
|
} : {},
|
|
1627
1630
|
...faceBbox !== void 0 ? { faceBbox } : {},
|
|
1631
|
+
...faceAlignedCrop !== void 0 ? { faceAlignedCrop } : {},
|
|
1628
1632
|
...plate !== void 0 ? {
|
|
1629
1633
|
plateText: plate.text,
|
|
1630
1634
|
plateScore: plate.score,
|
|
@@ -5500,6 +5504,14 @@ function updateTrackAggregate(prev, match, opts) {
|
|
|
5500
5504
|
//#region src/pipeline-analytics/face-recognizer.ts
|
|
5501
5505
|
/** At most one "dropping imageless track" log per this interval, per recognizer. */
|
|
5502
5506
|
var FACE_IMAGELESS_LOG_THROTTLE_MS = 6e4;
|
|
5507
|
+
/**
|
|
5508
|
+
* arcface model id stamped on a detail-plane face candidate when the gallery is
|
|
5509
|
+
* empty (collect-only). The detail-subtree result carries no `embeddingModelId`
|
|
5510
|
+
* (the two-plane `DetailResult` schema omits it), so recognition uses the
|
|
5511
|
+
* gallery's own model id (all enrolled samples share one) and this constant is
|
|
5512
|
+
* only a placeholder for the collect-only case where the id is never compared.
|
|
5513
|
+
*/
|
|
5514
|
+
var FALLBACK_FACE_MODEL_ID = "arcface";
|
|
5503
5515
|
var FaceRecognizer = class {
|
|
5504
5516
|
deps;
|
|
5505
5517
|
gallery = [];
|
|
@@ -5527,6 +5539,38 @@ var FaceRecognizer = class {
|
|
|
5527
5539
|
this.deps.logger.warn("FaceRecognizer.refreshGallery failed", { meta: { error: String(err) } });
|
|
5528
5540
|
}
|
|
5529
5541
|
}
|
|
5542
|
+
/**
|
|
5543
|
+
* Two-plane detail feed: ingest ONE `runDetailSubtree` face result for a
|
|
5544
|
+
* track and run it through the SAME `processFrame` logic (candidate → best
|
|
5545
|
+
* face → gallery match → crop hold). The result is synthesized into a single
|
|
5546
|
+
* `TrackedDetectionOut` candidate so no recognizer logic changes — only the
|
|
5547
|
+
* input source moves from the per-frame plane to this per-track call.
|
|
5548
|
+
*/
|
|
5549
|
+
async ingestFaceDetail(input) {
|
|
5550
|
+
const modelId = this.gallery[0]?.modelId ?? FALLBACK_FACE_MODEL_ID;
|
|
5551
|
+
const candidate = {
|
|
5552
|
+
trackId: input.trackId,
|
|
5553
|
+
className: "face",
|
|
5554
|
+
confidence: input.score,
|
|
5555
|
+
bbox: input.parentBbox,
|
|
5556
|
+
zones: [],
|
|
5557
|
+
state: "moving",
|
|
5558
|
+
embedding: input.embedding,
|
|
5559
|
+
embeddingModelId: modelId,
|
|
5560
|
+
...input.faceBbox !== void 0 ? { faceBbox: input.faceBbox } : {},
|
|
5561
|
+
...input.alignedCropJpeg !== void 0 ? { faceAlignedCrop: input.alignedCropJpeg } : {}
|
|
5562
|
+
};
|
|
5563
|
+
await this.processFrame({
|
|
5564
|
+
deviceId: input.deviceId,
|
|
5565
|
+
timestamp: input.timestamp,
|
|
5566
|
+
frameWidth: input.frameWidth,
|
|
5567
|
+
frameHeight: input.frameHeight,
|
|
5568
|
+
tracked: [candidate],
|
|
5569
|
+
settings: input.settings,
|
|
5570
|
+
cropPadding: input.cropPadding,
|
|
5571
|
+
...input.frameHandle !== void 0 ? { frameHandle: input.frameHandle } : {}
|
|
5572
|
+
});
|
|
5573
|
+
}
|
|
5530
5574
|
async processFrame(input) {
|
|
5531
5575
|
const { settings } = input;
|
|
5532
5576
|
const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.confidence >= settings.minFaceConfidence);
|
|
@@ -5564,7 +5608,8 @@ var FaceRecognizer = class {
|
|
|
5564
5608
|
if (isNewBest || needsCrop) {
|
|
5565
5609
|
const cropBbox = c.faceBbox ?? c.bbox;
|
|
5566
5610
|
let crop;
|
|
5567
|
-
if (c.
|
|
5611
|
+
if (c.faceAlignedCrop !== void 0) crop = Buffer.from(c.faceAlignedCrop, "base64");
|
|
5612
|
+
else if (c.faceBbox !== void 0 && input.frameHandle !== void 0) try {
|
|
5568
5613
|
crop = await this.deps.captureCrop(input.frameHandle, c.faceBbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
|
|
5569
5614
|
} catch (err) {
|
|
5570
5615
|
this.deps.logger.debug("FaceRecognizer crop capture failed", {
|
|
@@ -5755,6 +5800,346 @@ var FaceRecognizer = class {
|
|
|
5755
5800
|
}
|
|
5756
5801
|
};
|
|
5757
5802
|
//#endregion
|
|
5803
|
+
//#region src/pipeline-analytics/detail-scheduler.ts
|
|
5804
|
+
/** Default backoff/period when a step's cadence omits `minIntervalMs`. */
|
|
5805
|
+
var DEFAULT_MIN_INTERVAL_MS = 1e3;
|
|
5806
|
+
/** Default fire cap for a `once` step when its announce omits `maxPerTrack`. */
|
|
5807
|
+
var DEFAULT_ONCE_MAX_PER_TRACK = 3;
|
|
5808
|
+
/**
|
|
5809
|
+
* Pure per-(track, step) scheduling state machine for detail-subtree
|
|
5810
|
+
* dispatch. Given a camera's announced child steps (`DetailStepAnnounce[]`,
|
|
5811
|
+
* read off `PipelineInferenceResultPayload.detailSteps`), decides WHEN each
|
|
5812
|
+
* step should run for a given track — independent of transport, I/O, or
|
|
5813
|
+
* timers. The caller drives it with wall-clock `nowMs` and dispatches the
|
|
5814
|
+
* returned `DetailRequest[]`.
|
|
5815
|
+
*/
|
|
5816
|
+
var DetailScheduler = class {
|
|
5817
|
+
tracks = /* @__PURE__ */ new Map();
|
|
5818
|
+
/** Track appeared with class + announce; returns immediate requests. */
|
|
5819
|
+
onTrackStarted(trackId, className, announce, nowMs) {
|
|
5820
|
+
const steps = /* @__PURE__ */ new Map();
|
|
5821
|
+
const requests = [];
|
|
5822
|
+
for (const stepAnnounce of announce) {
|
|
5823
|
+
if (!stepAnnounce.inputClasses.includes(className)) continue;
|
|
5824
|
+
const state = {
|
|
5825
|
+
announce: stepAnnounce,
|
|
5826
|
+
firedCount: 1,
|
|
5827
|
+
lastFiredAt: nowMs,
|
|
5828
|
+
sticky: false,
|
|
5829
|
+
retryPending: false
|
|
5830
|
+
};
|
|
5831
|
+
steps.set(stepAnnounce.stepId, state);
|
|
5832
|
+
requests.push({
|
|
5833
|
+
trackId,
|
|
5834
|
+
stepId: stepAnnounce.stepId,
|
|
5835
|
+
reason: "new-track"
|
|
5836
|
+
});
|
|
5837
|
+
}
|
|
5838
|
+
this.tracks.set(trackId, steps);
|
|
5839
|
+
return requests;
|
|
5840
|
+
}
|
|
5841
|
+
/** Better candidate crop observed for the track. */
|
|
5842
|
+
onCandidateImproved(trackId, nowMs) {
|
|
5843
|
+
const steps = this.tracks.get(trackId);
|
|
5844
|
+
if (!steps) return [];
|
|
5845
|
+
const requests = [];
|
|
5846
|
+
for (const state of steps.values()) {
|
|
5847
|
+
if (state.announce.cadence.trigger !== "improve") continue;
|
|
5848
|
+
if (!this.canFire(state, nowMs)) continue;
|
|
5849
|
+
this.markFired(state, nowMs);
|
|
5850
|
+
requests.push({
|
|
5851
|
+
trackId,
|
|
5852
|
+
stepId: state.announce.stepId,
|
|
5853
|
+
reason: "improve"
|
|
5854
|
+
});
|
|
5855
|
+
}
|
|
5856
|
+
return requests;
|
|
5857
|
+
}
|
|
5858
|
+
/** Periodic tick (call ~1/s). Also carries pending retries for any trigger kind. */
|
|
5859
|
+
tick(nowMs) {
|
|
5860
|
+
const requests = [];
|
|
5861
|
+
for (const [trackId, steps] of this.tracks) for (const state of steps.values()) {
|
|
5862
|
+
if (state.sticky) continue;
|
|
5863
|
+
if (state.retryPending) {
|
|
5864
|
+
if (!this.intervalElapsed(state, nowMs)) continue;
|
|
5865
|
+
if (!this.underMaxPerTrack(state)) {
|
|
5866
|
+
state.retryPending = false;
|
|
5867
|
+
continue;
|
|
5868
|
+
}
|
|
5869
|
+
this.markFired(state, nowMs);
|
|
5870
|
+
requests.push({
|
|
5871
|
+
trackId,
|
|
5872
|
+
stepId: state.announce.stepId,
|
|
5873
|
+
reason: "retry"
|
|
5874
|
+
});
|
|
5875
|
+
continue;
|
|
5876
|
+
}
|
|
5877
|
+
if (state.announce.cadence.trigger !== "periodic") continue;
|
|
5878
|
+
if (!this.canFire(state, nowMs)) continue;
|
|
5879
|
+
this.markFired(state, nowMs);
|
|
5880
|
+
requests.push({
|
|
5881
|
+
trackId,
|
|
5882
|
+
stepId: state.announce.stepId,
|
|
5883
|
+
reason: "periodic"
|
|
5884
|
+
});
|
|
5885
|
+
}
|
|
5886
|
+
return requests;
|
|
5887
|
+
}
|
|
5888
|
+
/**
|
|
5889
|
+
* Result arrived; confidence drives sticky/retry. null = failed (retry per
|
|
5890
|
+
* policy). `_nowMs` is part of the public signature for symmetry with the
|
|
5891
|
+
* other methods but isn't needed here — retry backoff is anchored to
|
|
5892
|
+
* `lastFiredAt` (set when the step was actually dispatched), not to when
|
|
5893
|
+
* its result came back.
|
|
5894
|
+
*/
|
|
5895
|
+
onResult(trackId, stepId, confidence, _nowMs) {
|
|
5896
|
+
const steps = this.tracks.get(trackId);
|
|
5897
|
+
if (!steps) return;
|
|
5898
|
+
const state = steps.get(stepId);
|
|
5899
|
+
if (!state) return;
|
|
5900
|
+
if (state.sticky) return;
|
|
5901
|
+
const { stickyOnConfidence } = state.announce.cadence;
|
|
5902
|
+
if (confidence !== null && stickyOnConfidence !== void 0 && confidence >= stickyOnConfidence) {
|
|
5903
|
+
state.sticky = true;
|
|
5904
|
+
state.retryPending = false;
|
|
5905
|
+
return;
|
|
5906
|
+
}
|
|
5907
|
+
if (confidence === null) {
|
|
5908
|
+
if (this.underMaxPerTrack(state)) state.retryPending = true;
|
|
5909
|
+
return;
|
|
5910
|
+
}
|
|
5911
|
+
if (state.announce.cadence.trigger === "once" && stickyOnConfidence !== void 0) {
|
|
5912
|
+
if (this.underMaxPerTrack(state)) state.retryPending = true;
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
onTrackEnded(trackId) {
|
|
5916
|
+
this.tracks.delete(trackId);
|
|
5917
|
+
}
|
|
5918
|
+
canFire(state, nowMs) {
|
|
5919
|
+
if (state.sticky) return false;
|
|
5920
|
+
if (!this.underMaxPerTrack(state)) return false;
|
|
5921
|
+
return this.intervalElapsed(state, nowMs);
|
|
5922
|
+
}
|
|
5923
|
+
intervalElapsed(state, nowMs) {
|
|
5924
|
+
if (state.lastFiredAt === null) return true;
|
|
5925
|
+
const minIntervalMs = state.announce.cadence.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
5926
|
+
return nowMs - state.lastFiredAt >= minIntervalMs;
|
|
5927
|
+
}
|
|
5928
|
+
underMaxPerTrack(state) {
|
|
5929
|
+
const maxPerTrack = state.announce.cadence.maxPerTrack ?? (state.announce.cadence.trigger === "once" ? DEFAULT_ONCE_MAX_PER_TRACK : Infinity);
|
|
5930
|
+
return state.firedCount < maxPerTrack;
|
|
5931
|
+
}
|
|
5932
|
+
markFired(state, nowMs) {
|
|
5933
|
+
state.firedCount += 1;
|
|
5934
|
+
state.lastFiredAt = nowMs;
|
|
5935
|
+
state.retryPending = false;
|
|
5936
|
+
}
|
|
5937
|
+
};
|
|
5938
|
+
//#endregion
|
|
5939
|
+
//#region src/pipeline-analytics/detail-dispatcher.ts
|
|
5940
|
+
/** Throttle for the per-device "detail call failed" warn — one line / minute. */
|
|
5941
|
+
var FAIL_WARN_THROTTLE_MS = 6e4;
|
|
5942
|
+
var TrackDetailDispatcher = class {
|
|
5943
|
+
deps;
|
|
5944
|
+
devices = /* @__PURE__ */ new Map();
|
|
5945
|
+
maxInFlight;
|
|
5946
|
+
tickIntervalMs;
|
|
5947
|
+
disposed = false;
|
|
5948
|
+
constructor(deps) {
|
|
5949
|
+
this.deps = deps;
|
|
5950
|
+
this.maxInFlight = deps.maxInFlightPerDevice ?? 2;
|
|
5951
|
+
this.tickIntervalMs = deps.tickIntervalMs ?? 1e3;
|
|
5952
|
+
}
|
|
5953
|
+
/** A track appeared: record its frame, seed its best-confidence, and dispatch
|
|
5954
|
+
* the scheduler's immediate (new-track) requests. */
|
|
5955
|
+
onTrackStarted(deviceId, trackId, className, announce, frame, nowMs) {
|
|
5956
|
+
if (this.disposed) return;
|
|
5957
|
+
const dev = this.ensureDevice(deviceId);
|
|
5958
|
+
dev.tracks.set(trackId, frame);
|
|
5959
|
+
dev.candidateBest.observe(trackId, frame.confidence, frame.timestamp);
|
|
5960
|
+
const requests = dev.scheduler.onTrackStarted(trackId, className, announce, nowMs);
|
|
5961
|
+
this.enqueue(deviceId, dev, requests);
|
|
5962
|
+
this.ensureTimer(deviceId, dev);
|
|
5963
|
+
}
|
|
5964
|
+
/** A subsequent frame for a live track: refresh its frame + fire
|
|
5965
|
+
* `improve`-cadence steps when the detector confidence strictly improves.
|
|
5966
|
+
*
|
|
5967
|
+
* `announce` is the frame's currently-announced detail chain. When a track
|
|
5968
|
+
* is alive but has NO dispatcher state yet — it existed before `detailSteps`
|
|
5969
|
+
* first appeared (a mid-track redeploy / config change) — this adopts it as a
|
|
5970
|
+
* new track so it starts getting scheduled instead of starving for its whole
|
|
5971
|
+
* life. Idempotent: guarded by the per-track state check, so a track that
|
|
5972
|
+
* already has state is never reset. */
|
|
5973
|
+
onFrame(deviceId, trackId, announce, frame, nowMs) {
|
|
5974
|
+
if (this.disposed) return;
|
|
5975
|
+
const dev = this.devices.get(deviceId);
|
|
5976
|
+
if (!dev || !dev.tracks.has(trackId)) {
|
|
5977
|
+
if (announce.length > 0) this.onTrackStarted(deviceId, trackId, frame.className, announce, frame, nowMs);
|
|
5978
|
+
return;
|
|
5979
|
+
}
|
|
5980
|
+
dev.tracks.set(trackId, frame);
|
|
5981
|
+
if (!dev.candidateBest.observe(trackId, frame.confidence, frame.timestamp)) return;
|
|
5982
|
+
const requests = dev.scheduler.onCandidateImproved(trackId, nowMs);
|
|
5983
|
+
this.enqueue(deviceId, dev, requests);
|
|
5984
|
+
}
|
|
5985
|
+
/** A track ended (durable TTL expiry): drop its scheduler + frame state. Any
|
|
5986
|
+
* queued request for it is discarded at dequeue. */
|
|
5987
|
+
onTrackEnded(deviceId, trackId) {
|
|
5988
|
+
const dev = this.devices.get(deviceId);
|
|
5989
|
+
if (!dev) return;
|
|
5990
|
+
dev.scheduler.onTrackEnded(trackId);
|
|
5991
|
+
dev.tracks.delete(trackId);
|
|
5992
|
+
dev.candidateBest.delete(trackId);
|
|
5993
|
+
if (dev.tracks.size === 0) this.clearTimer(dev);
|
|
5994
|
+
}
|
|
5995
|
+
dispose() {
|
|
5996
|
+
this.disposed = true;
|
|
5997
|
+
for (const dev of this.devices.values()) {
|
|
5998
|
+
this.clearTimer(dev);
|
|
5999
|
+
dev.queue.length = 0;
|
|
6000
|
+
dev.tracks.clear();
|
|
6001
|
+
dev.candidateBest.clear();
|
|
6002
|
+
}
|
|
6003
|
+
this.devices.clear();
|
|
6004
|
+
}
|
|
6005
|
+
ensureDevice(deviceId) {
|
|
6006
|
+
let dev = this.devices.get(deviceId);
|
|
6007
|
+
if (!dev) {
|
|
6008
|
+
dev = {
|
|
6009
|
+
scheduler: new DetailScheduler(),
|
|
6010
|
+
tracks: /* @__PURE__ */ new Map(),
|
|
6011
|
+
candidateBest: new BestDetectionTracker(),
|
|
6012
|
+
queue: [],
|
|
6013
|
+
inFlight: 0,
|
|
6014
|
+
timer: null,
|
|
6015
|
+
lastFailWarnAt: 0
|
|
6016
|
+
};
|
|
6017
|
+
this.devices.set(deviceId, dev);
|
|
6018
|
+
}
|
|
6019
|
+
return dev;
|
|
6020
|
+
}
|
|
6021
|
+
ensureTimer(deviceId, dev) {
|
|
6022
|
+
if (dev.timer) return;
|
|
6023
|
+
const timer = setInterval(() => this.tick(deviceId, dev), this.tickIntervalMs);
|
|
6024
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
6025
|
+
dev.timer = timer;
|
|
6026
|
+
}
|
|
6027
|
+
clearTimer(dev) {
|
|
6028
|
+
if (dev.timer) {
|
|
6029
|
+
clearInterval(dev.timer);
|
|
6030
|
+
dev.timer = null;
|
|
6031
|
+
}
|
|
6032
|
+
}
|
|
6033
|
+
tick(deviceId, dev) {
|
|
6034
|
+
if (this.disposed) return;
|
|
6035
|
+
const requests = dev.scheduler.tick(Date.now());
|
|
6036
|
+
this.enqueue(deviceId, dev, requests);
|
|
6037
|
+
}
|
|
6038
|
+
enqueue(deviceId, dev, requests) {
|
|
6039
|
+
if (requests.length === 0) return;
|
|
6040
|
+
for (const r of requests) dev.queue.push(r);
|
|
6041
|
+
this.pump(deviceId, dev);
|
|
6042
|
+
}
|
|
6043
|
+
pump(deviceId, dev) {
|
|
6044
|
+
while (dev.inFlight < this.maxInFlight && dev.queue.length > 0) {
|
|
6045
|
+
const req = dev.queue.shift();
|
|
6046
|
+
if (req === void 0) break;
|
|
6047
|
+
const frame = dev.tracks.get(req.trackId);
|
|
6048
|
+
if (frame === void 0) continue;
|
|
6049
|
+
dev.inFlight += 1;
|
|
6050
|
+
this.dispatch(deviceId, dev, req, frame).finally(() => {
|
|
6051
|
+
dev.inFlight -= 1;
|
|
6052
|
+
this.pump(deviceId, dev);
|
|
6053
|
+
});
|
|
6054
|
+
}
|
|
6055
|
+
}
|
|
6056
|
+
async dispatch(deviceId, dev, req, frame) {
|
|
6057
|
+
const details = await this.runOnce(deviceId, dev, req, frame);
|
|
6058
|
+
let topScore = null;
|
|
6059
|
+
if (details !== null && details.length > 0) {
|
|
6060
|
+
topScore = details.reduce((max, d) => d.score > max ? d.score : max, Number.NEGATIVE_INFINITY);
|
|
6061
|
+
try {
|
|
6062
|
+
await this.deps.routeResults(deviceId, req.trackId, details, frame);
|
|
6063
|
+
} catch (err) {
|
|
6064
|
+
this.deps.logger.warn("detail result routing failed", {
|
|
6065
|
+
tags: { deviceId },
|
|
6066
|
+
meta: {
|
|
6067
|
+
trackId: req.trackId,
|
|
6068
|
+
stepId: req.stepId,
|
|
6069
|
+
error: String(err)
|
|
6070
|
+
}
|
|
6071
|
+
});
|
|
6072
|
+
}
|
|
6073
|
+
}
|
|
6074
|
+
dev.scheduler.onResult(req.trackId, req.stepId, topScore, Date.now());
|
|
6075
|
+
}
|
|
6076
|
+
/**
|
|
6077
|
+
* Run the request once via the frameHandle, and — on a miss (null OR throw)
|
|
6078
|
+
* — retry ONCE with a `cropJpeg` fallback when one can be captured. Returns
|
|
6079
|
+
* the detail list, or `null` when both attempts fail to produce a result.
|
|
6080
|
+
*/
|
|
6081
|
+
async runOnce(deviceId, dev, req, frame) {
|
|
6082
|
+
const parent = {
|
|
6083
|
+
bbox: { ...frame.bbox },
|
|
6084
|
+
className: frame.className
|
|
6085
|
+
};
|
|
6086
|
+
if (frame.frameHandle !== void 0) try {
|
|
6087
|
+
const primary = await this.deps.runDetailSubtree({
|
|
6088
|
+
deviceId,
|
|
6089
|
+
frameHandle: frame.frameHandle,
|
|
6090
|
+
parent,
|
|
6091
|
+
steps: [req.stepId]
|
|
6092
|
+
}, frame.nodeId);
|
|
6093
|
+
if (primary !== null) return primary.details;
|
|
6094
|
+
} catch (err) {
|
|
6095
|
+
this.deps.logger.debug("detail primary call failed — trying crop fallback", {
|
|
6096
|
+
tags: { deviceId },
|
|
6097
|
+
meta: {
|
|
6098
|
+
trackId: req.trackId,
|
|
6099
|
+
stepId: req.stepId,
|
|
6100
|
+
error: String(err)
|
|
6101
|
+
}
|
|
6102
|
+
});
|
|
6103
|
+
}
|
|
6104
|
+
if (this.deps.captureCropBase64 !== void 0) try {
|
|
6105
|
+
const cropJpeg = await this.deps.captureCropBase64(frame);
|
|
6106
|
+
if (cropJpeg !== null) {
|
|
6107
|
+
const retry = await this.deps.runDetailSubtree({
|
|
6108
|
+
deviceId,
|
|
6109
|
+
cropJpeg,
|
|
6110
|
+
parent,
|
|
6111
|
+
steps: [req.stepId]
|
|
6112
|
+
}, frame.nodeId);
|
|
6113
|
+
if (retry !== null) return retry.details;
|
|
6114
|
+
}
|
|
6115
|
+
} catch (err) {
|
|
6116
|
+
this.deps.logger.debug("detail crop-fallback call failed", {
|
|
6117
|
+
tags: { deviceId },
|
|
6118
|
+
meta: {
|
|
6119
|
+
trackId: req.trackId,
|
|
6120
|
+
stepId: req.stepId,
|
|
6121
|
+
error: String(err)
|
|
6122
|
+
}
|
|
6123
|
+
});
|
|
6124
|
+
}
|
|
6125
|
+
this.warnFailThrottled(deviceId, dev, req);
|
|
6126
|
+
return null;
|
|
6127
|
+
}
|
|
6128
|
+
warnFailThrottled(deviceId, dev, req) {
|
|
6129
|
+
const now = Date.now();
|
|
6130
|
+
if (now - dev.lastFailWarnAt < FAIL_WARN_THROTTLE_MS) return;
|
|
6131
|
+
dev.lastFailWarnAt = now;
|
|
6132
|
+
this.deps.logger.warn("runDetailSubtree produced no result (frame + crop both missed)", {
|
|
6133
|
+
tags: { deviceId },
|
|
6134
|
+
meta: {
|
|
6135
|
+
trackId: req.trackId,
|
|
6136
|
+
stepId: req.stepId,
|
|
6137
|
+
reason: req.reason
|
|
6138
|
+
}
|
|
6139
|
+
});
|
|
6140
|
+
}
|
|
6141
|
+
};
|
|
6142
|
+
//#endregion
|
|
5758
6143
|
//#region src/pipeline-analytics/store/plate-store.ts
|
|
5759
6144
|
var PLATES_COLLECTION = "pipeline-analytics:plates";
|
|
5760
6145
|
var PLATE_COLUMNS = [
|
|
@@ -6399,6 +6784,12 @@ function createEventMediaHandler(deps) {
|
|
|
6399
6784
|
* surface to turn the refinement pipeline on/off for a camera.
|
|
6400
6785
|
*/
|
|
6401
6786
|
var TTL_SWEEP_INTERVAL_MS = 5e3;
|
|
6787
|
+
/** Padding applied to the parent bbox for the `cropJpeg` retry fallback when a
|
|
6788
|
+
* scheduled detail call's frameHandle lease is already gone. */
|
|
6789
|
+
var DETAIL_FALLBACK_CROP_PADDING = .15;
|
|
6790
|
+
/** How long the active CLIP model id (from the embedding-encoder) is cached
|
|
6791
|
+
* before re-reading. */
|
|
6792
|
+
var CLIP_MODEL_ID_CACHE_TTL_MS = 6e4;
|
|
6402
6793
|
var SETTINGS_CACHE_TTL_MS = 5e3;
|
|
6403
6794
|
/** §5 best-frame: a track's `thumbnail` is overwritten only when the current
|
|
6404
6795
|
* detection confidence beats the held best by at least this margin (hysteresis
|
|
@@ -6431,6 +6822,15 @@ var RETENTION_SWEEP_INTERVAL_MS = 5 * 6e4;
|
|
|
6431
6822
|
var AUDIO_EVENT_HEARTBEAT_MS = 5e3;
|
|
6432
6823
|
var MOTION_EVENT_HEARTBEAT_MS = 5e3;
|
|
6433
6824
|
/**
|
|
6825
|
+
* Decode a base64 little-endian float32 vector (the `DetailResult.embedding`
|
|
6826
|
+
* wire encoding produced by `runDetailSubtree`) back into a plain number[].
|
|
6827
|
+
*/
|
|
6828
|
+
function decodeEmbeddingBase64(base64) {
|
|
6829
|
+
const bytes = Buffer.from(base64, "base64");
|
|
6830
|
+
const view = new Float32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
|
|
6831
|
+
return Array.from(view);
|
|
6832
|
+
}
|
|
6833
|
+
/**
|
|
6434
6834
|
* Re-home the global analytics sections into the per-device `Analytics`
|
|
6435
6835
|
* top-tab. Every section defaults to the `Analytics` tab (so the
|
|
6436
6836
|
* device-manager aggregator groups them) AND is marked
|
|
@@ -6480,6 +6880,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6480
6880
|
plateStore = null;
|
|
6481
6881
|
plateRecognizer = null;
|
|
6482
6882
|
objectEmbeddingStore = null;
|
|
6883
|
+
/** Two-plane detail scheduler/dispatcher: per-track on-demand face/clip/
|
|
6884
|
+
* classifier/plate enrichment via `pipelineRunner.runDetailSubtree`. Replaces
|
|
6885
|
+
* the per-frame child consumption the executor no longer emits. */
|
|
6886
|
+
detailDispatcher = null;
|
|
6887
|
+
/** Active CLIP image-embedding model id (== the text encoder's), TTL-cached.
|
|
6888
|
+
* Stamped on object-embedding rows from the detail plane so semantic search's
|
|
6889
|
+
* same-model gate keeps matching. */
|
|
6890
|
+
clipModelIdCache = null;
|
|
6483
6891
|
/** Frame-based event/track media (crop + boxed full-frame) from the
|
|
6484
6892
|
* detection-pipeline DECODED frame — the ONLY image source (never the
|
|
6485
6893
|
* snapshot cap). Null when shm frame access is unavailable. */
|
|
@@ -6587,7 +6995,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6587
6995
|
let storage = this.ctx.kernel.storage;
|
|
6588
6996
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
6589
6997
|
if (mediaRoot) {
|
|
6590
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
6998
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-BN31iDiA.js"));
|
|
6591
6999
|
storage = new FilesystemStorageProvider(mediaRoot);
|
|
6592
7000
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
6593
7001
|
}
|
|
@@ -6739,6 +7147,21 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6739
7147
|
store: api.settingsStore,
|
|
6740
7148
|
logger: logger.child("ObjectEmbeddingStore")
|
|
6741
7149
|
});
|
|
7150
|
+
const runnerApi = api.pipelineRunner;
|
|
7151
|
+
this.detailDispatcher = new TrackDetailDispatcher({
|
|
7152
|
+
logger: logger.child("DetailDispatcher"),
|
|
7153
|
+
runDetailSubtree: async (input, nodeId) => {
|
|
7154
|
+
if (!runnerApi?.runDetailSubtree) return null;
|
|
7155
|
+
if (nodeId !== void 0) return runnerApi.runDetailSubtree.mutate(input, require_dist.nodePin(nodeId));
|
|
7156
|
+
return runnerApi.runDetailSubtree.mutate(input);
|
|
7157
|
+
},
|
|
7158
|
+
routeResults: (deviceId, trackId, details, frame) => this.routeDetailResults(deviceId, trackId, details, frame),
|
|
7159
|
+
captureCropBase64: async (frame) => {
|
|
7160
|
+
if (frame.frameHandle === void 0 || !this.captureCrop) return null;
|
|
7161
|
+
const buf = await this.captureCrop(frame.frameHandle, { ...frame.bbox }, frame.frameWidth, frame.frameHeight, DETAIL_FALLBACK_CROP_PADDING);
|
|
7162
|
+
return buf ? buf.toString("base64") : null;
|
|
7163
|
+
}
|
|
7164
|
+
});
|
|
6742
7165
|
this.bindingCache = new BindingCache({
|
|
6743
7166
|
api,
|
|
6744
7167
|
logger: logger.child("BindingCache")
|
|
@@ -7143,6 +7566,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7143
7566
|
}
|
|
7144
7567
|
this.zoneAnalytics?.destroy();
|
|
7145
7568
|
this.audioMetrics?.destroy();
|
|
7569
|
+
this.detailDispatcher?.dispose();
|
|
7570
|
+
this.detailDispatcher = null;
|
|
7146
7571
|
this.processors.clear();
|
|
7147
7572
|
this.lastActiveTrackIds.clear();
|
|
7148
7573
|
this.dropoutSkipsByKey.clear();
|
|
@@ -7167,7 +7592,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7167
7592
|
async handleInferenceResult(data) {
|
|
7168
7593
|
if (this.shuttingDown) return;
|
|
7169
7594
|
const { deviceId, frame } = data;
|
|
7170
|
-
await this.processFrame(deviceId, frame, "pipeline", data.frameHandle);
|
|
7595
|
+
await this.processFrame(deviceId, frame, "pipeline", data.frameHandle, data.detailSteps);
|
|
7171
7596
|
}
|
|
7172
7597
|
/**
|
|
7173
7598
|
* Run one detection frame through the analysis layers for a given
|
|
@@ -7177,7 +7602,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7177
7602
|
* tracking/zone/event state never crosses between sources. Emits the
|
|
7178
7603
|
* SAME canonical events, distinguished only by `source`.
|
|
7179
7604
|
*/
|
|
7180
|
-
async processFrame(deviceId, frame, source, frameHandle) {
|
|
7605
|
+
async processFrame(deviceId, frame, source, frameHandle, detailSteps) {
|
|
7181
7606
|
if (this.shuttingDown) return;
|
|
7182
7607
|
if (!await this.bindingCache.isActive(deviceId)) return;
|
|
7183
7608
|
const key = this.procKey(deviceId, source);
|
|
@@ -7289,6 +7714,26 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7289
7714
|
} });
|
|
7290
7715
|
}
|
|
7291
7716
|
this.lastActiveTrackIds.set(key, currentTrackIds);
|
|
7717
|
+
if (this.detailDispatcher && detailSteps && detailSteps.length > 0) {
|
|
7718
|
+
const dispatcher = this.detailDispatcher;
|
|
7719
|
+
const steps = detailSteps;
|
|
7720
|
+
for (const t of result.tracked) {
|
|
7721
|
+
const detailFrame = {
|
|
7722
|
+
bbox: { ...t.bbox },
|
|
7723
|
+
frameWidth: result.frameWidth,
|
|
7724
|
+
frameHeight: result.frameHeight,
|
|
7725
|
+
className: t.className,
|
|
7726
|
+
confidence: t.confidence,
|
|
7727
|
+
timestamp: result.timestamp,
|
|
7728
|
+
...frameHandle !== void 0 ? {
|
|
7729
|
+
frameHandle,
|
|
7730
|
+
nodeId: frameHandle.nodeId
|
|
7731
|
+
} : {}
|
|
7732
|
+
};
|
|
7733
|
+
if (prevIds.has(t.trackId)) dispatcher.onFrame(deviceId, t.trackId, steps, detailFrame, result.timestamp);
|
|
7734
|
+
else dispatcher.onTrackStarted(deviceId, t.trackId, t.className, steps, detailFrame, result.timestamp);
|
|
7735
|
+
}
|
|
7736
|
+
}
|
|
7292
7737
|
if (newTrackCount > 0 || lostTrackCount > 0 || result.objectEvents.length > 0) {
|
|
7293
7738
|
const byState = {};
|
|
7294
7739
|
for (const t of result.tracked) byState[t.state] = (byState[t.state] ?? 0) + 1;
|
|
@@ -7500,6 +7945,142 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7500
7945
|
return settings;
|
|
7501
7946
|
}
|
|
7502
7947
|
/**
|
|
7948
|
+
* Route one track's `runDetailSubtree` results (two-plane detail dispatch)
|
|
7949
|
+
* into the EXISTING per-track consumers, discriminated by payload SHAPE:
|
|
7950
|
+
* • embedding + alignedCropJpeg → FACE (arcface): the FaceRecognizer's
|
|
7951
|
+
* candidate/best-face/gallery/keyFrame path (input source cut-over);
|
|
7952
|
+
* • embedding only → CLIP object embedding → the object-embedding store
|
|
7953
|
+
* (semantic search);
|
|
7954
|
+
* • label only → classifier answer / plate OCR text → the track's
|
|
7955
|
+
* enrichment label the notifier/UI already read.
|
|
7956
|
+
* Best-effort (D8): a per-detail failure is logged and never propagated.
|
|
7957
|
+
*/
|
|
7958
|
+
async routeDetailResults(deviceId, trackId, details, frame) {
|
|
7959
|
+
for (const d of details) try {
|
|
7960
|
+
if (d.className === "face" && d.embedding !== void 0) await this.routeFaceDetail(deviceId, trackId, d, frame);
|
|
7961
|
+
else if (d.embedding !== void 0) await this.routeClipDetail(deviceId, trackId, d, frame.timestamp);
|
|
7962
|
+
else if (d.label !== void 0 && d.label.length > 0) await this.applyTrackEnrichmentLabel(deviceId, trackId, d.label);
|
|
7963
|
+
} catch (err) {
|
|
7964
|
+
this.ctx.logger.warn("detail result route failed", {
|
|
7965
|
+
tags: { deviceId },
|
|
7966
|
+
meta: {
|
|
7967
|
+
trackId,
|
|
7968
|
+
stepId: d.stepId,
|
|
7969
|
+
error: require_dist.errMsg(err)
|
|
7970
|
+
}
|
|
7971
|
+
});
|
|
7972
|
+
}
|
|
7973
|
+
}
|
|
7974
|
+
/** Face-embedding detail → the FaceRecognizer (same gate + logic as the
|
|
7975
|
+
* former per-frame face path; only the input source moved). */
|
|
7976
|
+
async routeFaceDetail(deviceId, trackId, detail, frame) {
|
|
7977
|
+
if (!this.faceRecognizer || detail.embedding === void 0) return;
|
|
7978
|
+
if (!await this.resolveGlobalFaceEnabled()) return;
|
|
7979
|
+
const [settings, media] = await Promise.all([this.resolveDeviceFaceSettings(deviceId), this.resolveDeviceMediaSettings(deviceId)]);
|
|
7980
|
+
await this.faceRecognizer.ingestFaceDetail({
|
|
7981
|
+
deviceId,
|
|
7982
|
+
trackId,
|
|
7983
|
+
timestamp: frame.timestamp,
|
|
7984
|
+
frameWidth: frame.frameWidth,
|
|
7985
|
+
frameHeight: frame.frameHeight,
|
|
7986
|
+
score: detail.score,
|
|
7987
|
+
embedding: decodeEmbeddingBase64(detail.embedding),
|
|
7988
|
+
parentBbox: { ...frame.bbox },
|
|
7989
|
+
...detail.bbox !== void 0 ? { faceBbox: { ...detail.bbox } } : {},
|
|
7990
|
+
...detail.alignedCropJpeg !== void 0 ? { alignedCropJpeg: detail.alignedCropJpeg } : {},
|
|
7991
|
+
settings,
|
|
7992
|
+
cropPadding: media.cropPadding,
|
|
7993
|
+
...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {}
|
|
7994
|
+
});
|
|
7995
|
+
}
|
|
7996
|
+
/** CLIP object-embedding detail → the object-embedding store (semantic
|
|
7997
|
+
* search). Stamps the active encoder's model id so the same-model search
|
|
7998
|
+
* gate keeps matching. */
|
|
7999
|
+
async routeClipDetail(deviceId, trackId, detail, timestamp) {
|
|
8000
|
+
const store = this.objectEmbeddingStore;
|
|
8001
|
+
if (!store || detail.embedding === void 0) return;
|
|
8002
|
+
const modelId = await this.resolveClipModelId();
|
|
8003
|
+
if (modelId === null) {
|
|
8004
|
+
this.ctx.logger.debug("clip detail dropped — no active embedding model id", {
|
|
8005
|
+
tags: { deviceId },
|
|
8006
|
+
meta: {
|
|
8007
|
+
trackId,
|
|
8008
|
+
stepId: detail.stepId
|
|
8009
|
+
}
|
|
8010
|
+
});
|
|
8011
|
+
return;
|
|
8012
|
+
}
|
|
8013
|
+
await store.upsertIfBetter({
|
|
8014
|
+
trackId,
|
|
8015
|
+
deviceId,
|
|
8016
|
+
timestamp,
|
|
8017
|
+
className: detail.className,
|
|
8018
|
+
embedding: decodeEmbeddingBase64(detail.embedding),
|
|
8019
|
+
modelId,
|
|
8020
|
+
confidence: detail.score
|
|
8021
|
+
});
|
|
8022
|
+
}
|
|
8023
|
+
/** Classifier answer / plate OCR text → the track's enrichment label
|
|
8024
|
+
* (TrackStore + persisted events + importance), mirroring the FaceRecognizer
|
|
8025
|
+
* label-propagation path. */
|
|
8026
|
+
async applyTrackEnrichmentLabel(deviceId, trackId, label) {
|
|
8027
|
+
try {
|
|
8028
|
+
await this.trackStore?.setLabel(trackId, label);
|
|
8029
|
+
} catch (err) {
|
|
8030
|
+
this.ctx.logger.warn("detail label setLabel failed", {
|
|
8031
|
+
tags: { deviceId },
|
|
8032
|
+
meta: {
|
|
8033
|
+
trackId,
|
|
8034
|
+
error: require_dist.errMsg(err)
|
|
8035
|
+
}
|
|
8036
|
+
});
|
|
8037
|
+
}
|
|
8038
|
+
try {
|
|
8039
|
+
await this.eventStore?.setLabelForTrack(trackId, label);
|
|
8040
|
+
} catch (err) {
|
|
8041
|
+
this.ctx.logger.warn("detail label setLabelForTrack failed", {
|
|
8042
|
+
tags: { deviceId },
|
|
8043
|
+
meta: {
|
|
8044
|
+
trackId,
|
|
8045
|
+
error: require_dist.errMsg(err)
|
|
8046
|
+
}
|
|
8047
|
+
});
|
|
8048
|
+
}
|
|
8049
|
+
try {
|
|
8050
|
+
const trackStore = this.trackStore;
|
|
8051
|
+
const eventStore = this.eventStore;
|
|
8052
|
+
if (trackStore && eventStore) await recomputeTrackImportance({
|
|
8053
|
+
trackStore,
|
|
8054
|
+
eventStore
|
|
8055
|
+
}, trackId);
|
|
8056
|
+
} catch (err) {
|
|
8057
|
+
this.ctx.logger.debug("detail label recomputeImportance failed", {
|
|
8058
|
+
tags: { deviceId },
|
|
8059
|
+
meta: {
|
|
8060
|
+
trackId,
|
|
8061
|
+
error: require_dist.errMsg(err)
|
|
8062
|
+
}
|
|
8063
|
+
});
|
|
8064
|
+
}
|
|
8065
|
+
}
|
|
8066
|
+
/** Active CLIP image-embedding model id (== the text encoder's), TTL-cached.
|
|
8067
|
+
* Returns null when the embedding-encoder cap is unavailable. */
|
|
8068
|
+
async resolveClipModelId() {
|
|
8069
|
+
const now = Date.now();
|
|
8070
|
+
if (this.clipModelIdCache && now < this.clipModelIdCache.expiresAt) return this.clipModelIdCache.value;
|
|
8071
|
+
let value = null;
|
|
8072
|
+
try {
|
|
8073
|
+
value = (await this.ctx.api.embeddingEncoder.getInfo.query())?.modelId ?? null;
|
|
8074
|
+
} catch (err) {
|
|
8075
|
+
this.ctx.logger.debug("resolveClipModelId: getInfo failed", { meta: { error: require_dist.errMsg(err) } });
|
|
8076
|
+
}
|
|
8077
|
+
this.clipModelIdCache = {
|
|
8078
|
+
value,
|
|
8079
|
+
expiresAt: now + CLIP_MODEL_ID_CACHE_TTL_MS
|
|
8080
|
+
};
|
|
8081
|
+
return value;
|
|
8082
|
+
}
|
|
8083
|
+
/**
|
|
7503
8084
|
* §5 — decide which active tracks need periodic media THIS frame. Pure over
|
|
7504
8085
|
* TrackStore.lastSnapshotAt + the per-track best-confidence map:
|
|
7505
8086
|
* • `snapshot` (append) + `lastFrame` (rolling overwrite) fire together on
|
|
@@ -7887,6 +8468,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7887
8468
|
}
|
|
7888
8469
|
this.bestFrameTracker.delete(t.trackId);
|
|
7889
8470
|
this.objectEmbeddingBestSelector.delete(t.trackId);
|
|
8471
|
+
this.detailDispatcher?.onTrackEnded(t.deviceId, t.trackId);
|
|
7890
8472
|
this.ctx.eventBus.emit({
|
|
7891
8473
|
id: `pa-end-${t.trackId}`,
|
|
7892
8474
|
timestamp: new Date(t.lastSeen),
|