@camstack/addon-post-analysis 1.2.3 → 1.2.4
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/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C4dWm6ZL.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-ChHurWQz.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-3T15bdHA.mjs → hostInit-DQbbiJYs.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +258 -37
- package/dist/pipeline-analytics/index.mjs +258 -37
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
|
|
|
3
3
|
var e = {
|
|
4
4
|
"@camstack/sdk": {
|
|
5
5
|
name: "@camstack/sdk",
|
|
6
|
-
version: "1.2.
|
|
6
|
+
version: "1.2.4",
|
|
7
7
|
scope: ["default"],
|
|
8
8
|
loaded: !1,
|
|
9
9
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -18,7 +18,7 @@ var e = {
|
|
|
18
18
|
},
|
|
19
19
|
"@camstack/types": {
|
|
20
20
|
name: "@camstack/types",
|
|
21
|
-
version: "1.2.
|
|
21
|
+
version: "1.2.4",
|
|
22
22
|
scope: ["default"],
|
|
23
23
|
loaded: !1,
|
|
24
24
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -33,7 +33,7 @@ var e = {
|
|
|
33
33
|
},
|
|
34
34
|
"@camstack/ui-library": {
|
|
35
35
|
name: "@camstack/ui-library",
|
|
36
|
-
version: "1.2.
|
|
36
|
+
version: "1.2.4",
|
|
37
37
|
scope: ["default"],
|
|
38
38
|
loaded: !1,
|
|
39
39
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -36,7 +36,7 @@ async function r() {
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"@camstack/types": {
|
|
39
|
-
version: "1.2.
|
|
39
|
+
version: "1.2.4",
|
|
40
40
|
scope: "default",
|
|
41
41
|
shareConfig: {
|
|
42
42
|
singleton: !0,
|
|
@@ -45,7 +45,7 @@ async function r() {
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"@camstack/sdk": {
|
|
48
|
-
version: "1.2.
|
|
48
|
+
version: "1.2.4",
|
|
49
49
|
scope: "default",
|
|
50
50
|
shareConfig: {
|
|
51
51
|
singleton: !0,
|
|
@@ -81,7 +81,7 @@ async function r() {
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"@camstack/ui-library": {
|
|
84
|
-
version: "1.2.
|
|
84
|
+
version: "1.2.4",
|
|
85
85
|
scope: "default",
|
|
86
86
|
shareConfig: {
|
|
87
87
|
singleton: !0,
|
|
@@ -3360,6 +3360,24 @@ async function cascadeDeleteTracks(registry, trackStore, trackIds) {
|
|
|
3360
3360
|
for (const trackId of trackIds) await trackStore.deletePersisted(trackId);
|
|
3361
3361
|
}
|
|
3362
3362
|
/**
|
|
3363
|
+
* Full whole-track cascade for ONE track: events → their media + track media →
|
|
3364
|
+
* track row. Delegates to the generalized `cascadeDeleteTracks` so the shipped
|
|
3365
|
+
* `deleteTracks` provider path flows through the same engine while preserving
|
|
3366
|
+
* the exact legacy call order (event ids stay local to the delete). Adapts the
|
|
3367
|
+
* narrow legacy store shapes onto `TrackScopedStore` — a superset store (faces /
|
|
3368
|
+
* embeddings) is opted in by the CALLER's registry, not by this legacy shim.
|
|
3369
|
+
*/
|
|
3370
|
+
async function cascadeDeleteTrack(stores, trackId) {
|
|
3371
|
+
await cascadeDeleteTracks([{ deleteByTracks: async (ids) => {
|
|
3372
|
+
for (const id of ids) {
|
|
3373
|
+
const eventIds = await stores.eventStore.deleteByTrack(id);
|
|
3374
|
+
if (eventIds.length > 0) await stores.mediaStore.deleteForEvents([...eventIds]);
|
|
3375
|
+
}
|
|
3376
|
+
} }, { deleteByTracks: async (ids) => {
|
|
3377
|
+
await stores.mediaStore.deleteForTracks([...ids]);
|
|
3378
|
+
} }], stores.trackStore, [trackId]);
|
|
3379
|
+
}
|
|
3380
|
+
/**
|
|
3363
3381
|
* Widened batch whole-track deletion — the ONE engine behind the three §5 entry
|
|
3364
3382
|
* points. Runs the FULL registry cascade per track (via `cascadeDeleteTracks`
|
|
3365
3383
|
* with a single-id list so the track root is deleted last), fires cleanup on
|
|
@@ -6478,6 +6496,36 @@ function squareSafeCropRegionNormalized(bbox, frame, padding) {
|
|
|
6478
6496
|
};
|
|
6479
6497
|
}
|
|
6480
6498
|
//#endregion
|
|
6499
|
+
//#region src/shared/frame/crop-extractor.ts
|
|
6500
|
+
/**
|
|
6501
|
+
* Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
|
|
6502
|
+
* Coordinates are clamped to frame bounds to avoid out-of-range errors.
|
|
6503
|
+
*/
|
|
6504
|
+
async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
|
|
6505
|
+
const rawLeft = Math.round(bbox.x * frameWidth);
|
|
6506
|
+
const rawTop = Math.round(bbox.y * frameHeight);
|
|
6507
|
+
const rawWidth = Math.round(bbox.w * frameWidth);
|
|
6508
|
+
const rawHeight = Math.round(bbox.h * frameHeight);
|
|
6509
|
+
const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
|
|
6510
|
+
const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
|
|
6511
|
+
const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
|
|
6512
|
+
const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
|
|
6513
|
+
return {
|
|
6514
|
+
crop: await (0, sharp.default)(frameData, { raw: {
|
|
6515
|
+
width: frameWidth,
|
|
6516
|
+
height: frameHeight,
|
|
6517
|
+
channels: 3
|
|
6518
|
+
} }).extract({
|
|
6519
|
+
left,
|
|
6520
|
+
top,
|
|
6521
|
+
width,
|
|
6522
|
+
height
|
|
6523
|
+
}).jpeg({ quality: 90 }).toBuffer(),
|
|
6524
|
+
width,
|
|
6525
|
+
height
|
|
6526
|
+
};
|
|
6527
|
+
}
|
|
6528
|
+
//#endregion
|
|
6481
6529
|
//#region src/shared/frame/square-subject-crop.ts
|
|
6482
6530
|
/** Expansion factor applied to the bbox long side to frame the subject with a
|
|
6483
6531
|
* little breathing room (operator choice: ×1.2). */
|
|
@@ -6716,7 +6764,8 @@ var EventMediaDispatcher = class {
|
|
|
6716
6764
|
const snapshots = input.snapshots ?? [];
|
|
6717
6765
|
const empty = {
|
|
6718
6766
|
storedSnapshots: [],
|
|
6719
|
-
thumbnailTrackIds: []
|
|
6767
|
+
thumbnailTrackIds: [],
|
|
6768
|
+
rasterFallbacks: []
|
|
6720
6769
|
};
|
|
6721
6770
|
if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
|
|
6722
6771
|
let decoded;
|
|
@@ -6768,6 +6817,7 @@ var EventMediaDispatcher = class {
|
|
|
6768
6817
|
});
|
|
6769
6818
|
return empty;
|
|
6770
6819
|
}
|
|
6820
|
+
const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds);
|
|
6771
6821
|
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
|
|
6772
6822
|
for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf);
|
|
6773
6823
|
const storedSnapshots = [];
|
|
@@ -6779,10 +6829,55 @@ var EventMediaDispatcher = class {
|
|
|
6779
6829
|
}
|
|
6780
6830
|
return {
|
|
6781
6831
|
storedSnapshots,
|
|
6782
|
-
thumbnailTrackIds
|
|
6832
|
+
thumbnailTrackIds,
|
|
6833
|
+
rasterFallbacks
|
|
6783
6834
|
};
|
|
6784
6835
|
}
|
|
6785
6836
|
/**
|
|
6837
|
+
* Cut ONE clean detection-raster subject crop per WANTED track that has a
|
|
6838
|
+
* target (firstFrame or snapshot) in this frame — the "first available
|
|
6839
|
+
* detection-raster frame" of the zero-media fallback. Cropped from the
|
|
6840
|
+
* already-resolved `frameData` at its REAL resolution via {@link extractCrop}
|
|
6841
|
+
* (extract-only — NEVER upscaled). Deduped per trackId (first target wins).
|
|
6842
|
+
* A per-track encode failure is skipped (logged) — a missing fallback simply
|
|
6843
|
+
* leaves the track with no last-resort preview, never an error.
|
|
6844
|
+
*/
|
|
6845
|
+
async collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, cropPadding, wanted) {
|
|
6846
|
+
if (!wanted || wanted.size === 0) return [];
|
|
6847
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6848
|
+
const out = [];
|
|
6849
|
+
const candidates = [...trackFrames.map((t) => ({
|
|
6850
|
+
trackId: t.trackId,
|
|
6851
|
+
timestamp: t.timestamp,
|
|
6852
|
+
bbox: t.bbox
|
|
6853
|
+
})), ...snapshots.map((s) => ({
|
|
6854
|
+
trackId: s.trackId,
|
|
6855
|
+
timestamp: s.timestamp,
|
|
6856
|
+
bbox: s.bbox
|
|
6857
|
+
}))];
|
|
6858
|
+
for (const c of candidates) {
|
|
6859
|
+
if (!wanted.has(c.trackId) || seen.has(c.trackId)) continue;
|
|
6860
|
+
seen.add(c.trackId);
|
|
6861
|
+
try {
|
|
6862
|
+
const { crop } = await extractCrop(frameData, fw, fh, squareSafeCropRegionNormalized(c.bbox, {
|
|
6863
|
+
W: fw,
|
|
6864
|
+
H: fh
|
|
6865
|
+
}, cropPadding));
|
|
6866
|
+
out.push({
|
|
6867
|
+
trackId: c.trackId,
|
|
6868
|
+
timestamp: c.timestamp,
|
|
6869
|
+
jpeg: crop
|
|
6870
|
+
});
|
|
6871
|
+
} catch (err) {
|
|
6872
|
+
this.deps.logger.debug("event media: raster fallback crop failed", { meta: {
|
|
6873
|
+
trackId: c.trackId,
|
|
6874
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6875
|
+
} });
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6878
|
+
return out;
|
|
6879
|
+
}
|
|
6880
|
+
/**
|
|
6786
6881
|
* Periodic per-track media (§5). The boxed FULL frame is encoded once and
|
|
6787
6882
|
* shared by the appended `snapshot` (timeline filmstrip) and the rolling
|
|
6788
6883
|
* `lastFrame` (overwrite). The best `thumbnail` is DIFFERENT: a clean
|
|
@@ -7049,36 +7144,6 @@ var EventMediaDispatcher = class {
|
|
|
7049
7144
|
}
|
|
7050
7145
|
};
|
|
7051
7146
|
//#endregion
|
|
7052
|
-
//#region src/shared/frame/crop-extractor.ts
|
|
7053
|
-
/**
|
|
7054
|
-
* Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
|
|
7055
|
-
* Coordinates are clamped to frame bounds to avoid out-of-range errors.
|
|
7056
|
-
*/
|
|
7057
|
-
async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
|
|
7058
|
-
const rawLeft = Math.round(bbox.x * frameWidth);
|
|
7059
|
-
const rawTop = Math.round(bbox.y * frameHeight);
|
|
7060
|
-
const rawWidth = Math.round(bbox.w * frameWidth);
|
|
7061
|
-
const rawHeight = Math.round(bbox.h * frameHeight);
|
|
7062
|
-
const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
|
|
7063
|
-
const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
|
|
7064
|
-
const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
|
|
7065
|
-
const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
|
|
7066
|
-
return {
|
|
7067
|
-
crop: await (0, sharp.default)(frameData, { raw: {
|
|
7068
|
-
width: frameWidth,
|
|
7069
|
-
height: frameHeight,
|
|
7070
|
-
channels: 3
|
|
7071
|
-
} }).extract({
|
|
7072
|
-
left,
|
|
7073
|
-
top,
|
|
7074
|
-
width,
|
|
7075
|
-
height
|
|
7076
|
-
}).jpeg({ quality: 90 }).toBuffer(),
|
|
7077
|
-
width,
|
|
7078
|
-
height
|
|
7079
|
-
};
|
|
7080
|
-
}
|
|
7081
|
-
//#endregion
|
|
7082
7147
|
//#region src/pipeline-analytics/embedding/embedding-dispatcher.ts
|
|
7083
7148
|
function selectCropBbox(detection) {
|
|
7084
7149
|
return detection.refinedBbox ?? detection.bbox;
|
|
@@ -8867,6 +8932,16 @@ function planPeriodicMedia(input) {
|
|
|
8867
8932
|
bestThumbnail: input.isNewBest || !thumbnailLanded
|
|
8868
8933
|
};
|
|
8869
8934
|
}
|
|
8935
|
+
var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = { suppressMaxDurationMs: 1e3 };
|
|
8936
|
+
/**
|
|
8937
|
+
* Classify a closing track's persistence outcome. Pure — see the module header
|
|
8938
|
+
* for the full contract.
|
|
8939
|
+
*/
|
|
8940
|
+
function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG) {
|
|
8941
|
+
if (input.hasMedia) return "persist";
|
|
8942
|
+
if (input.durationMs < config.suppressMaxDurationMs && !input.confirmed) return "suppress";
|
|
8943
|
+
return input.hasRasterFallback ? "raster-fallback" : "persist";
|
|
8944
|
+
}
|
|
8870
8945
|
//#endregion
|
|
8871
8946
|
//#region src/pipeline-analytics/best-thumbnail-guard.ts
|
|
8872
8947
|
/**
|
|
@@ -12126,6 +12201,23 @@ function pickTrackFallbackMedia(files) {
|
|
|
12126
12201
|
return files.find((f) => f.kind === "thumbnailSmall") ?? files.find((f) => f.kind === "thumbnail") ?? files.find((f) => f.kind === "lastFrame") ?? files.find((f) => f.kind === "firstFrame") ?? [...files].reverse().find((f) => f.kind === "snapshot") ?? files[files.length - 1];
|
|
12127
12202
|
}
|
|
12128
12203
|
/**
|
|
12204
|
+
* Crop-forced resolution (the `?kind=crop` reel/timeline path). Prefer a CLEAN
|
|
12205
|
+
* (never-boxed) frame via the injected `pickClean`; when the track owns NO clean
|
|
12206
|
+
* frame at all — the regression class created by retiring `crop`/`fullFrame`
|
|
12207
|
+
* production (2026-07-21), leaving degenerate tracks with only boxed
|
|
12208
|
+
* `lastFrame`/`snapshot`/`firstFrame` — degrade to ANY track media rather than
|
|
12209
|
+
* returning undefined. A boxed frame (with the target's box drawn) is a real
|
|
12210
|
+
* preview; an empty tile is not. Clean is always tried FIRST so the common case
|
|
12211
|
+
* still serves an unboxed tile. Undefined only when the owner has NO media.
|
|
12212
|
+
*
|
|
12213
|
+
* `pickClean` is injected because the clean-kind picker (`pickCleanMedia`) lives
|
|
12214
|
+
* in the addon `index.ts` over the concrete `MediaFileKind` set; this keeps the
|
|
12215
|
+
* composition pure + unit-testable here.
|
|
12216
|
+
*/
|
|
12217
|
+
function resolveCropForcedMedia(files, pickClean) {
|
|
12218
|
+
return pickClean(files) ?? pickTrackFallbackMedia(files);
|
|
12219
|
+
}
|
|
12220
|
+
/**
|
|
12129
12221
|
* Resolve the default-path media for an event id: the event's own media when it
|
|
12130
12222
|
* has any, else the owning track's fallback media. Returns `null` when neither
|
|
12131
12223
|
* yields a blob (caller 404s → the surface shows an icon).
|
|
@@ -12481,6 +12573,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12481
12573
|
* area) + emit time, so a material improvement is measured against the
|
|
12482
12574
|
* last emit and debounced. Seeded at `start`, dropped at `end`. */
|
|
12483
12575
|
trackLifecycleUpdateMem = /* @__PURE__ */ new Map();
|
|
12576
|
+
/** Per-active-track close-policy state (spec §"Zero-media track policy"):
|
|
12577
|
+
* whether the track ever reached the tracker's hit-confirmation
|
|
12578
|
+
* (`trackAge >= minHits`) and the FIRST retained detection-raster subject
|
|
12579
|
+
* crop. Both are read ONLY at close (`sweepExpiredTracks`) to decide
|
|
12580
|
+
* suppress / raster-fallback / persist for a track that ends with no media.
|
|
12581
|
+
* `deviceId` lets `clearDevice` prune without an active-track lookup. Dropped
|
|
12582
|
+
* on every teardown path. */
|
|
12583
|
+
trackClosureState = /* @__PURE__ */ new Map();
|
|
12484
12584
|
/** The shared crop extractor (native-res first, detection-frame fallback),
|
|
12485
12585
|
* captured in the constructor so `processFrame` can crop object thumbnails in
|
|
12486
12586
|
* the same live-frame window as the face/plate/event-media captures. The
|
|
@@ -12972,6 +13072,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12972
13072
|
this.bindingCache?.onBindingsChanged(data);
|
|
12973
13073
|
if (data.capName === "pipeline-analytics" && data.reason === "wrapper-deactivated") {
|
|
12974
13074
|
this.trackStore?.clearDevice(data.deviceId);
|
|
13075
|
+
this.clearClosureStateForDevice(data.deviceId);
|
|
12975
13076
|
this.stationaryRegistry?.forgetDevice(data.deviceId);
|
|
12976
13077
|
this.overlayState.clearDevice(data.deviceId);
|
|
12977
13078
|
this.overlaySynthesisWarnAt.delete(data.deviceId);
|
|
@@ -12990,6 +13091,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12990
13091
|
this.unsubDeviceUnreg = this.ctx.eventBus.subscribe({ category: require_dist.EventCategory.DeviceUnregistered }, (ev) => {
|
|
12991
13092
|
const { deviceId } = ev.data;
|
|
12992
13093
|
this.trackStore?.clearDevice(deviceId);
|
|
13094
|
+
this.clearClosureStateForDevice(deviceId);
|
|
12993
13095
|
this.stationaryRegistry?.forgetDevice(deviceId);
|
|
12994
13096
|
this.overlayState.clearDevice(deviceId);
|
|
12995
13097
|
this.overlaySynthesisWarnAt.delete(deviceId);
|
|
@@ -13345,6 +13447,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13345
13447
|
this.thumbnailInFlight.clear();
|
|
13346
13448
|
this.keyFrameInFlight.clear();
|
|
13347
13449
|
this.trackLifecycleUpdateMem.clear();
|
|
13450
|
+
this.trackClosureState.clear();
|
|
13348
13451
|
this.objectEmbeddingBestSelector.clear();
|
|
13349
13452
|
this.levelStateByDevice.clear();
|
|
13350
13453
|
this.settingsCacheByDevice.clear();
|
|
@@ -13463,6 +13566,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13463
13566
|
});
|
|
13464
13567
|
positionsCountById.set(t.trackId, upserted.positions.length);
|
|
13465
13568
|
}
|
|
13569
|
+
if (result.rawTrackedDetections.length > 0) {
|
|
13570
|
+
const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
|
|
13571
|
+
for (const rt of result.rawTrackedDetections) if (rt.trackAge >= minHits) this.markTrackConfirmed(deviceId, rt.trackId);
|
|
13572
|
+
}
|
|
13466
13573
|
const log = this.ctx.logger.withTags({ deviceId });
|
|
13467
13574
|
const prevIds = this.lastActiveTrackIds.get(key) ?? /* @__PURE__ */ new Set();
|
|
13468
13575
|
const firstFrameTargets = [];
|
|
@@ -13515,6 +13622,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13515
13622
|
...t.label ? { label: t.label } : {}
|
|
13516
13623
|
});
|
|
13517
13624
|
this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
|
|
13625
|
+
this.lastFrameAtByTrack.set(id, result.timestamp);
|
|
13518
13626
|
}
|
|
13519
13627
|
this.ctx.eventBus.emit({
|
|
13520
13628
|
id: `pa-${(0, node_crypto.randomUUID)()}`,
|
|
@@ -13686,14 +13794,25 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13686
13794
|
} });
|
|
13687
13795
|
const thumbInFlightTrackIds = snapshotTargets.filter((t) => t.bestThumbnail).map((t) => t.trackId);
|
|
13688
13796
|
for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.add(trackId);
|
|
13797
|
+
const rasterFallbackWantedTrackIds = /* @__PURE__ */ new Set();
|
|
13798
|
+
for (const t of firstFrameTargets) if (!this.trackClosureState.get(t.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(t.trackId);
|
|
13799
|
+
for (const s of snapshotTargets) if (!this.trackClosureState.get(s.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(s.trackId);
|
|
13689
13800
|
this.eventMediaDispatcher.captureForFrame({
|
|
13690
13801
|
deviceId,
|
|
13691
13802
|
frameHandle,
|
|
13692
13803
|
events: eventTargets,
|
|
13693
13804
|
trackFrames: firstFrameTargets,
|
|
13694
13805
|
snapshots: snapshotTargets,
|
|
13695
|
-
cropPadding: mediaSettings.cropPadding
|
|
13806
|
+
cropPadding: mediaSettings.cropPadding,
|
|
13807
|
+
rasterFallbackWantedTrackIds
|
|
13696
13808
|
}).then((res) => {
|
|
13809
|
+
for (const rf of res.rasterFallbacks) {
|
|
13810
|
+
const st = this.ensureTrackClosureState(deviceId, rf.trackId);
|
|
13811
|
+
if (!st.rasterFallback) st.rasterFallback = {
|
|
13812
|
+
jpeg: rf.jpeg,
|
|
13813
|
+
timestamp: rf.timestamp
|
|
13814
|
+
};
|
|
13815
|
+
}
|
|
13697
13816
|
for (const s of res.storedSnapshots) this.trackStore?.addSnapshot(s.trackId, {
|
|
13698
13817
|
timestamp: s.timestamp,
|
|
13699
13818
|
position: {
|
|
@@ -14704,6 +14823,44 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14704
14823
|
const expired = await this.trackStore.expireStale(Date.now());
|
|
14705
14824
|
for (const t of expired) {
|
|
14706
14825
|
const duration = t.lastSeen - t.firstSeen;
|
|
14826
|
+
const closure = this.trackClosureState.get(t.trackId);
|
|
14827
|
+
const outcome = decideZeroMediaPolicy({
|
|
14828
|
+
durationMs: duration,
|
|
14829
|
+
hasMedia: (await this.mediaStore?.listByOwner("track", t.trackId) ?? []).length > 0,
|
|
14830
|
+
confirmed: closure?.confirmed ?? false,
|
|
14831
|
+
hasRasterFallback: closure?.rasterFallback !== void 0
|
|
14832
|
+
});
|
|
14833
|
+
if (outcome === "suppress") {
|
|
14834
|
+
this.ctx.logger.info("track suppressed (zero-media false birth)", {
|
|
14835
|
+
tags: { deviceId: t.deviceId },
|
|
14836
|
+
meta: {
|
|
14837
|
+
trackId: t.trackId,
|
|
14838
|
+
className: t.className,
|
|
14839
|
+
durationMs: duration,
|
|
14840
|
+
positions: t.positions.length
|
|
14841
|
+
}
|
|
14842
|
+
});
|
|
14843
|
+
await this.suppressZeroMediaTrack(t.deviceId, t.trackId);
|
|
14844
|
+
continue;
|
|
14845
|
+
}
|
|
14846
|
+
if (outcome === "raster-fallback" && closure?.rasterFallback) try {
|
|
14847
|
+
await this.mediaStore?.put({
|
|
14848
|
+
deviceId: t.deviceId,
|
|
14849
|
+
ownerKind: "track",
|
|
14850
|
+
ownerId: t.trackId,
|
|
14851
|
+
kind: "thumbnail",
|
|
14852
|
+
timestamp: closure.rasterFallback.timestamp,
|
|
14853
|
+
data: closure.rasterFallback.jpeg
|
|
14854
|
+
});
|
|
14855
|
+
} catch (err) {
|
|
14856
|
+
this.ctx.logger.debug("zero-media raster fallback put failed", {
|
|
14857
|
+
tags: { deviceId: t.deviceId },
|
|
14858
|
+
meta: {
|
|
14859
|
+
trackId: t.trackId,
|
|
14860
|
+
error: String(err)
|
|
14861
|
+
}
|
|
14862
|
+
});
|
|
14863
|
+
}
|
|
14707
14864
|
this.ctx.logger.info("track ended", {
|
|
14708
14865
|
tags: { deviceId: t.deviceId },
|
|
14709
14866
|
meta: {
|
|
@@ -14814,12 +14971,53 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14814
14971
|
});
|
|
14815
14972
|
this.emitTrackLifecycle(endPayload, t.lastSeen);
|
|
14816
14973
|
this.trackLifecycleUpdateMem.delete(t.trackId);
|
|
14974
|
+
this.trackClosureState.delete(t.trackId);
|
|
14817
14975
|
}
|
|
14818
14976
|
} catch (err) {
|
|
14819
14977
|
if (this.shuttingDown) return;
|
|
14820
14978
|
this.ctx.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
|
|
14821
14979
|
}
|
|
14822
14980
|
}
|
|
14981
|
+
/**
|
|
14982
|
+
* Undo a zero-media false-birth track (spec §"Zero-media track policy"): tear
|
|
14983
|
+
* down its live per-track state (mirroring the natural end MINUS importance
|
|
14984
|
+
* scoring + the lifecycle `end`/`TrackEnded` events) and cascade-delete the
|
|
14985
|
+
* just-persisted row + any events/media so nothing dangles. Best-effort.
|
|
14986
|
+
*/
|
|
14987
|
+
async suppressZeroMediaTrack(deviceId, trackId) {
|
|
14988
|
+
const faceEnd = this.faceRecognizer?.onTrackEnd(deviceId, trackId);
|
|
14989
|
+
const dropKeyFrame = () => {
|
|
14990
|
+
this.keyFrameKeyByTrackId.delete(trackId);
|
|
14991
|
+
};
|
|
14992
|
+
if (faceEnd) faceEnd.finally(dropKeyFrame);
|
|
14993
|
+
else dropKeyFrame();
|
|
14994
|
+
this.plateRecognizer?.onTrackEnd(deviceId, trackId);
|
|
14995
|
+
this.bestFrameTracker.delete(trackId);
|
|
14996
|
+
this.objectEmbeddingBestSelector.delete(trackId);
|
|
14997
|
+
this.lastFrameAtByTrack.delete(trackId);
|
|
14998
|
+
this.thumbnailLandedTracks.delete(trackId);
|
|
14999
|
+
this.thumbnailInFlight.delete(trackId);
|
|
15000
|
+
this.keyFrameInFlight.delete(trackId);
|
|
15001
|
+
this.detailDispatcher?.onTrackEnded(deviceId, trackId);
|
|
15002
|
+
this.overlayState.onTrackEnded(deviceId, trackId);
|
|
15003
|
+
this.trackLifecycleUpdateMem.delete(trackId);
|
|
15004
|
+
this.trackClosureState.delete(trackId);
|
|
15005
|
+
if (this.eventStore && this.mediaStore && this.trackStore) try {
|
|
15006
|
+
await cascadeDeleteTrack({
|
|
15007
|
+
eventStore: this.eventStore,
|
|
15008
|
+
mediaStore: this.mediaStore,
|
|
15009
|
+
trackStore: this.trackStore
|
|
15010
|
+
}, trackId);
|
|
15011
|
+
} catch (err) {
|
|
15012
|
+
this.ctx.logger.debug("zero-media suppression cascade failed", {
|
|
15013
|
+
tags: { deviceId },
|
|
15014
|
+
meta: {
|
|
15015
|
+
trackId,
|
|
15016
|
+
error: String(err)
|
|
15017
|
+
}
|
|
15018
|
+
});
|
|
15019
|
+
}
|
|
15020
|
+
}
|
|
14823
15021
|
async sweepRetention() {
|
|
14824
15022
|
if (this.shuttingDown || !this.eventStore || !this.mediaStore) return;
|
|
14825
15023
|
const now = Date.now();
|
|
@@ -15029,6 +15227,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
15029
15227
|
this.thumbnailInFlight.delete(track.trackId);
|
|
15030
15228
|
this.keyFrameInFlight.delete(track.trackId);
|
|
15031
15229
|
this.trackLifecycleUpdateMem.delete(track.trackId);
|
|
15230
|
+
this.trackClosureState.delete(track.trackId);
|
|
15032
15231
|
this.detailDispatcher?.onTrackEnded(deviceId, track.trackId);
|
|
15033
15232
|
this.overlayState.onTrackEnded(deviceId, track.trackId);
|
|
15034
15233
|
this.lastActiveTrackIds.get(key)?.delete(track.trackId);
|
|
@@ -15327,6 +15526,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
15327
15526
|
}
|
|
15328
15527
|
async clearTracks(input) {
|
|
15329
15528
|
this.trackStore?.clearDevice(input.deviceId);
|
|
15529
|
+
this.clearClosureStateForDevice(input.deviceId);
|
|
15330
15530
|
this.stationaryRegistry?.clearDevice(input.deviceId);
|
|
15331
15531
|
this.overlayState.clearDevice(input.deviceId);
|
|
15332
15532
|
this.overlaySynthesisWarnAt.delete(input.deviceId);
|
|
@@ -15539,6 +15739,27 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
15539
15739
|
clearLiveTrackState(deviceId, trackId) {
|
|
15540
15740
|
this.detailDispatcher?.onTrackEnded(deviceId, trackId);
|
|
15541
15741
|
this.overlayState.onTrackEnded(deviceId, trackId);
|
|
15742
|
+
this.trackClosureState.delete(trackId);
|
|
15743
|
+
}
|
|
15744
|
+
/** Lazily create + return a track's close-policy state (zero-media policy). */
|
|
15745
|
+
ensureTrackClosureState(deviceId, trackId) {
|
|
15746
|
+
let st = this.trackClosureState.get(trackId);
|
|
15747
|
+
if (!st) {
|
|
15748
|
+
st = {
|
|
15749
|
+
deviceId,
|
|
15750
|
+
confirmed: false
|
|
15751
|
+
};
|
|
15752
|
+
this.trackClosureState.set(trackId, st);
|
|
15753
|
+
}
|
|
15754
|
+
return st;
|
|
15755
|
+
}
|
|
15756
|
+
/** Sticky-mark a track as confirmed once it reaches the tracker's hit gate. */
|
|
15757
|
+
markTrackConfirmed(deviceId, trackId) {
|
|
15758
|
+
this.ensureTrackClosureState(deviceId, trackId).confirmed = true;
|
|
15759
|
+
}
|
|
15760
|
+
/** Drop every close-policy entry for a device (mirrors `trackStore.clearDevice`). */
|
|
15761
|
+
clearClosureStateForDevice(deviceId) {
|
|
15762
|
+
for (const [trackId, st] of this.trackClosureState) if (st.deviceId === deviceId) this.trackClosureState.delete(trackId);
|
|
15542
15763
|
}
|
|
15543
15764
|
async deleteTracks(input) {
|
|
15544
15765
|
const cascade = this.buildTrackCascadeRegistry();
|
|
@@ -15833,11 +16054,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
15833
16054
|
const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
|
|
15834
16055
|
if (preferKind !== void 0 && preferKind.length > 0) {
|
|
15835
16056
|
const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
|
|
15836
|
-
const
|
|
15837
|
-
if (!
|
|
16057
|
+
const chosen = resolveCropForcedMedia([...eventFiles, ...trackFiles], (files) => pickCleanMedia(files, preferKind));
|
|
16058
|
+
if (!chosen) return null;
|
|
15838
16059
|
return {
|
|
15839
|
-
bytes: Buffer.from(
|
|
15840
|
-
key:
|
|
16060
|
+
bytes: Buffer.from(chosen.base64, "base64"),
|
|
16061
|
+
key: chosen.key
|
|
15841
16062
|
};
|
|
15842
16063
|
}
|
|
15843
16064
|
const chosen = await resolveDefaultEventMedia({
|
|
@@ -3355,6 +3355,24 @@ async function cascadeDeleteTracks(registry, trackStore, trackIds) {
|
|
|
3355
3355
|
for (const trackId of trackIds) await trackStore.deletePersisted(trackId);
|
|
3356
3356
|
}
|
|
3357
3357
|
/**
|
|
3358
|
+
* Full whole-track cascade for ONE track: events → their media + track media →
|
|
3359
|
+
* track row. Delegates to the generalized `cascadeDeleteTracks` so the shipped
|
|
3360
|
+
* `deleteTracks` provider path flows through the same engine while preserving
|
|
3361
|
+
* the exact legacy call order (event ids stay local to the delete). Adapts the
|
|
3362
|
+
* narrow legacy store shapes onto `TrackScopedStore` — a superset store (faces /
|
|
3363
|
+
* embeddings) is opted in by the CALLER's registry, not by this legacy shim.
|
|
3364
|
+
*/
|
|
3365
|
+
async function cascadeDeleteTrack(stores, trackId) {
|
|
3366
|
+
await cascadeDeleteTracks([{ deleteByTracks: async (ids) => {
|
|
3367
|
+
for (const id of ids) {
|
|
3368
|
+
const eventIds = await stores.eventStore.deleteByTrack(id);
|
|
3369
|
+
if (eventIds.length > 0) await stores.mediaStore.deleteForEvents([...eventIds]);
|
|
3370
|
+
}
|
|
3371
|
+
} }, { deleteByTracks: async (ids) => {
|
|
3372
|
+
await stores.mediaStore.deleteForTracks([...ids]);
|
|
3373
|
+
} }], stores.trackStore, [trackId]);
|
|
3374
|
+
}
|
|
3375
|
+
/**
|
|
3358
3376
|
* Widened batch whole-track deletion — the ONE engine behind the three §5 entry
|
|
3359
3377
|
* points. Runs the FULL registry cascade per track (via `cascadeDeleteTracks`
|
|
3360
3378
|
* with a single-id list so the track root is deleted last), fires cleanup on
|
|
@@ -6473,6 +6491,36 @@ function squareSafeCropRegionNormalized(bbox, frame, padding) {
|
|
|
6473
6491
|
};
|
|
6474
6492
|
}
|
|
6475
6493
|
//#endregion
|
|
6494
|
+
//#region src/shared/frame/crop-extractor.ts
|
|
6495
|
+
/**
|
|
6496
|
+
* Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
|
|
6497
|
+
* Coordinates are clamped to frame bounds to avoid out-of-range errors.
|
|
6498
|
+
*/
|
|
6499
|
+
async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
|
|
6500
|
+
const rawLeft = Math.round(bbox.x * frameWidth);
|
|
6501
|
+
const rawTop = Math.round(bbox.y * frameHeight);
|
|
6502
|
+
const rawWidth = Math.round(bbox.w * frameWidth);
|
|
6503
|
+
const rawHeight = Math.round(bbox.h * frameHeight);
|
|
6504
|
+
const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
|
|
6505
|
+
const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
|
|
6506
|
+
const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
|
|
6507
|
+
const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
|
|
6508
|
+
return {
|
|
6509
|
+
crop: await sharp(frameData, { raw: {
|
|
6510
|
+
width: frameWidth,
|
|
6511
|
+
height: frameHeight,
|
|
6512
|
+
channels: 3
|
|
6513
|
+
} }).extract({
|
|
6514
|
+
left,
|
|
6515
|
+
top,
|
|
6516
|
+
width,
|
|
6517
|
+
height
|
|
6518
|
+
}).jpeg({ quality: 90 }).toBuffer(),
|
|
6519
|
+
width,
|
|
6520
|
+
height
|
|
6521
|
+
};
|
|
6522
|
+
}
|
|
6523
|
+
//#endregion
|
|
6476
6524
|
//#region src/shared/frame/square-subject-crop.ts
|
|
6477
6525
|
/** Expansion factor applied to the bbox long side to frame the subject with a
|
|
6478
6526
|
* little breathing room (operator choice: ×1.2). */
|
|
@@ -6711,7 +6759,8 @@ var EventMediaDispatcher = class {
|
|
|
6711
6759
|
const snapshots = input.snapshots ?? [];
|
|
6712
6760
|
const empty = {
|
|
6713
6761
|
storedSnapshots: [],
|
|
6714
|
-
thumbnailTrackIds: []
|
|
6762
|
+
thumbnailTrackIds: [],
|
|
6763
|
+
rasterFallbacks: []
|
|
6715
6764
|
};
|
|
6716
6765
|
if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
|
|
6717
6766
|
let decoded;
|
|
@@ -6763,6 +6812,7 @@ var EventMediaDispatcher = class {
|
|
|
6763
6812
|
});
|
|
6764
6813
|
return empty;
|
|
6765
6814
|
}
|
|
6815
|
+
const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds);
|
|
6766
6816
|
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
|
|
6767
6817
|
for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf);
|
|
6768
6818
|
const storedSnapshots = [];
|
|
@@ -6774,10 +6824,55 @@ var EventMediaDispatcher = class {
|
|
|
6774
6824
|
}
|
|
6775
6825
|
return {
|
|
6776
6826
|
storedSnapshots,
|
|
6777
|
-
thumbnailTrackIds
|
|
6827
|
+
thumbnailTrackIds,
|
|
6828
|
+
rasterFallbacks
|
|
6778
6829
|
};
|
|
6779
6830
|
}
|
|
6780
6831
|
/**
|
|
6832
|
+
* Cut ONE clean detection-raster subject crop per WANTED track that has a
|
|
6833
|
+
* target (firstFrame or snapshot) in this frame — the "first available
|
|
6834
|
+
* detection-raster frame" of the zero-media fallback. Cropped from the
|
|
6835
|
+
* already-resolved `frameData` at its REAL resolution via {@link extractCrop}
|
|
6836
|
+
* (extract-only — NEVER upscaled). Deduped per trackId (first target wins).
|
|
6837
|
+
* A per-track encode failure is skipped (logged) — a missing fallback simply
|
|
6838
|
+
* leaves the track with no last-resort preview, never an error.
|
|
6839
|
+
*/
|
|
6840
|
+
async collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, cropPadding, wanted) {
|
|
6841
|
+
if (!wanted || wanted.size === 0) return [];
|
|
6842
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6843
|
+
const out = [];
|
|
6844
|
+
const candidates = [...trackFrames.map((t) => ({
|
|
6845
|
+
trackId: t.trackId,
|
|
6846
|
+
timestamp: t.timestamp,
|
|
6847
|
+
bbox: t.bbox
|
|
6848
|
+
})), ...snapshots.map((s) => ({
|
|
6849
|
+
trackId: s.trackId,
|
|
6850
|
+
timestamp: s.timestamp,
|
|
6851
|
+
bbox: s.bbox
|
|
6852
|
+
}))];
|
|
6853
|
+
for (const c of candidates) {
|
|
6854
|
+
if (!wanted.has(c.trackId) || seen.has(c.trackId)) continue;
|
|
6855
|
+
seen.add(c.trackId);
|
|
6856
|
+
try {
|
|
6857
|
+
const { crop } = await extractCrop(frameData, fw, fh, squareSafeCropRegionNormalized(c.bbox, {
|
|
6858
|
+
W: fw,
|
|
6859
|
+
H: fh
|
|
6860
|
+
}, cropPadding));
|
|
6861
|
+
out.push({
|
|
6862
|
+
trackId: c.trackId,
|
|
6863
|
+
timestamp: c.timestamp,
|
|
6864
|
+
jpeg: crop
|
|
6865
|
+
});
|
|
6866
|
+
} catch (err) {
|
|
6867
|
+
this.deps.logger.debug("event media: raster fallback crop failed", { meta: {
|
|
6868
|
+
trackId: c.trackId,
|
|
6869
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6870
|
+
} });
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
return out;
|
|
6874
|
+
}
|
|
6875
|
+
/**
|
|
6781
6876
|
* Periodic per-track media (§5). The boxed FULL frame is encoded once and
|
|
6782
6877
|
* shared by the appended `snapshot` (timeline filmstrip) and the rolling
|
|
6783
6878
|
* `lastFrame` (overwrite). The best `thumbnail` is DIFFERENT: a clean
|
|
@@ -7044,36 +7139,6 @@ var EventMediaDispatcher = class {
|
|
|
7044
7139
|
}
|
|
7045
7140
|
};
|
|
7046
7141
|
//#endregion
|
|
7047
|
-
//#region src/shared/frame/crop-extractor.ts
|
|
7048
|
-
/**
|
|
7049
|
-
* Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
|
|
7050
|
-
* Coordinates are clamped to frame bounds to avoid out-of-range errors.
|
|
7051
|
-
*/
|
|
7052
|
-
async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
|
|
7053
|
-
const rawLeft = Math.round(bbox.x * frameWidth);
|
|
7054
|
-
const rawTop = Math.round(bbox.y * frameHeight);
|
|
7055
|
-
const rawWidth = Math.round(bbox.w * frameWidth);
|
|
7056
|
-
const rawHeight = Math.round(bbox.h * frameHeight);
|
|
7057
|
-
const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
|
|
7058
|
-
const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
|
|
7059
|
-
const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
|
|
7060
|
-
const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
|
|
7061
|
-
return {
|
|
7062
|
-
crop: await sharp(frameData, { raw: {
|
|
7063
|
-
width: frameWidth,
|
|
7064
|
-
height: frameHeight,
|
|
7065
|
-
channels: 3
|
|
7066
|
-
} }).extract({
|
|
7067
|
-
left,
|
|
7068
|
-
top,
|
|
7069
|
-
width,
|
|
7070
|
-
height
|
|
7071
|
-
}).jpeg({ quality: 90 }).toBuffer(),
|
|
7072
|
-
width,
|
|
7073
|
-
height
|
|
7074
|
-
};
|
|
7075
|
-
}
|
|
7076
|
-
//#endregion
|
|
7077
7142
|
//#region src/pipeline-analytics/embedding/embedding-dispatcher.ts
|
|
7078
7143
|
function selectCropBbox(detection) {
|
|
7079
7144
|
return detection.refinedBbox ?? detection.bbox;
|
|
@@ -8862,6 +8927,16 @@ function planPeriodicMedia(input) {
|
|
|
8862
8927
|
bestThumbnail: input.isNewBest || !thumbnailLanded
|
|
8863
8928
|
};
|
|
8864
8929
|
}
|
|
8930
|
+
var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = { suppressMaxDurationMs: 1e3 };
|
|
8931
|
+
/**
|
|
8932
|
+
* Classify a closing track's persistence outcome. Pure — see the module header
|
|
8933
|
+
* for the full contract.
|
|
8934
|
+
*/
|
|
8935
|
+
function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG) {
|
|
8936
|
+
if (input.hasMedia) return "persist";
|
|
8937
|
+
if (input.durationMs < config.suppressMaxDurationMs && !input.confirmed) return "suppress";
|
|
8938
|
+
return input.hasRasterFallback ? "raster-fallback" : "persist";
|
|
8939
|
+
}
|
|
8865
8940
|
//#endregion
|
|
8866
8941
|
//#region src/pipeline-analytics/best-thumbnail-guard.ts
|
|
8867
8942
|
/**
|
|
@@ -12121,6 +12196,23 @@ function pickTrackFallbackMedia(files) {
|
|
|
12121
12196
|
return files.find((f) => f.kind === "thumbnailSmall") ?? files.find((f) => f.kind === "thumbnail") ?? files.find((f) => f.kind === "lastFrame") ?? files.find((f) => f.kind === "firstFrame") ?? [...files].reverse().find((f) => f.kind === "snapshot") ?? files[files.length - 1];
|
|
12122
12197
|
}
|
|
12123
12198
|
/**
|
|
12199
|
+
* Crop-forced resolution (the `?kind=crop` reel/timeline path). Prefer a CLEAN
|
|
12200
|
+
* (never-boxed) frame via the injected `pickClean`; when the track owns NO clean
|
|
12201
|
+
* frame at all — the regression class created by retiring `crop`/`fullFrame`
|
|
12202
|
+
* production (2026-07-21), leaving degenerate tracks with only boxed
|
|
12203
|
+
* `lastFrame`/`snapshot`/`firstFrame` — degrade to ANY track media rather than
|
|
12204
|
+
* returning undefined. A boxed frame (with the target's box drawn) is a real
|
|
12205
|
+
* preview; an empty tile is not. Clean is always tried FIRST so the common case
|
|
12206
|
+
* still serves an unboxed tile. Undefined only when the owner has NO media.
|
|
12207
|
+
*
|
|
12208
|
+
* `pickClean` is injected because the clean-kind picker (`pickCleanMedia`) lives
|
|
12209
|
+
* in the addon `index.ts` over the concrete `MediaFileKind` set; this keeps the
|
|
12210
|
+
* composition pure + unit-testable here.
|
|
12211
|
+
*/
|
|
12212
|
+
function resolveCropForcedMedia(files, pickClean) {
|
|
12213
|
+
return pickClean(files) ?? pickTrackFallbackMedia(files);
|
|
12214
|
+
}
|
|
12215
|
+
/**
|
|
12124
12216
|
* Resolve the default-path media for an event id: the event's own media when it
|
|
12125
12217
|
* has any, else the owning track's fallback media. Returns `null` when neither
|
|
12126
12218
|
* yields a blob (caller 404s → the surface shows an icon).
|
|
@@ -12476,6 +12568,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12476
12568
|
* area) + emit time, so a material improvement is measured against the
|
|
12477
12569
|
* last emit and debounced. Seeded at `start`, dropped at `end`. */
|
|
12478
12570
|
trackLifecycleUpdateMem = /* @__PURE__ */ new Map();
|
|
12571
|
+
/** Per-active-track close-policy state (spec §"Zero-media track policy"):
|
|
12572
|
+
* whether the track ever reached the tracker's hit-confirmation
|
|
12573
|
+
* (`trackAge >= minHits`) and the FIRST retained detection-raster subject
|
|
12574
|
+
* crop. Both are read ONLY at close (`sweepExpiredTracks`) to decide
|
|
12575
|
+
* suppress / raster-fallback / persist for a track that ends with no media.
|
|
12576
|
+
* `deviceId` lets `clearDevice` prune without an active-track lookup. Dropped
|
|
12577
|
+
* on every teardown path. */
|
|
12578
|
+
trackClosureState = /* @__PURE__ */ new Map();
|
|
12479
12579
|
/** The shared crop extractor (native-res first, detection-frame fallback),
|
|
12480
12580
|
* captured in the constructor so `processFrame` can crop object thumbnails in
|
|
12481
12581
|
* the same live-frame window as the face/plate/event-media captures. The
|
|
@@ -12967,6 +13067,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12967
13067
|
this.bindingCache?.onBindingsChanged(data);
|
|
12968
13068
|
if (data.capName === "pipeline-analytics" && data.reason === "wrapper-deactivated") {
|
|
12969
13069
|
this.trackStore?.clearDevice(data.deviceId);
|
|
13070
|
+
this.clearClosureStateForDevice(data.deviceId);
|
|
12970
13071
|
this.stationaryRegistry?.forgetDevice(data.deviceId);
|
|
12971
13072
|
this.overlayState.clearDevice(data.deviceId);
|
|
12972
13073
|
this.overlaySynthesisWarnAt.delete(data.deviceId);
|
|
@@ -12985,6 +13086,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12985
13086
|
this.unsubDeviceUnreg = this.ctx.eventBus.subscribe({ category: EventCategory.DeviceUnregistered }, (ev) => {
|
|
12986
13087
|
const { deviceId } = ev.data;
|
|
12987
13088
|
this.trackStore?.clearDevice(deviceId);
|
|
13089
|
+
this.clearClosureStateForDevice(deviceId);
|
|
12988
13090
|
this.stationaryRegistry?.forgetDevice(deviceId);
|
|
12989
13091
|
this.overlayState.clearDevice(deviceId);
|
|
12990
13092
|
this.overlaySynthesisWarnAt.delete(deviceId);
|
|
@@ -13340,6 +13442,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13340
13442
|
this.thumbnailInFlight.clear();
|
|
13341
13443
|
this.keyFrameInFlight.clear();
|
|
13342
13444
|
this.trackLifecycleUpdateMem.clear();
|
|
13445
|
+
this.trackClosureState.clear();
|
|
13343
13446
|
this.objectEmbeddingBestSelector.clear();
|
|
13344
13447
|
this.levelStateByDevice.clear();
|
|
13345
13448
|
this.settingsCacheByDevice.clear();
|
|
@@ -13458,6 +13561,10 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13458
13561
|
});
|
|
13459
13562
|
positionsCountById.set(t.trackId, upserted.positions.length);
|
|
13460
13563
|
}
|
|
13564
|
+
if (result.rawTrackedDetections.length > 0) {
|
|
13565
|
+
const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
|
|
13566
|
+
for (const rt of result.rawTrackedDetections) if (rt.trackAge >= minHits) this.markTrackConfirmed(deviceId, rt.trackId);
|
|
13567
|
+
}
|
|
13461
13568
|
const log = this.ctx.logger.withTags({ deviceId });
|
|
13462
13569
|
const prevIds = this.lastActiveTrackIds.get(key) ?? /* @__PURE__ */ new Set();
|
|
13463
13570
|
const firstFrameTargets = [];
|
|
@@ -13510,6 +13617,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13510
13617
|
...t.label ? { label: t.label } : {}
|
|
13511
13618
|
});
|
|
13512
13619
|
this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
|
|
13620
|
+
this.lastFrameAtByTrack.set(id, result.timestamp);
|
|
13513
13621
|
}
|
|
13514
13622
|
this.ctx.eventBus.emit({
|
|
13515
13623
|
id: `pa-${randomUUID()}`,
|
|
@@ -13681,14 +13789,25 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13681
13789
|
} });
|
|
13682
13790
|
const thumbInFlightTrackIds = snapshotTargets.filter((t) => t.bestThumbnail).map((t) => t.trackId);
|
|
13683
13791
|
for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.add(trackId);
|
|
13792
|
+
const rasterFallbackWantedTrackIds = /* @__PURE__ */ new Set();
|
|
13793
|
+
for (const t of firstFrameTargets) if (!this.trackClosureState.get(t.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(t.trackId);
|
|
13794
|
+
for (const s of snapshotTargets) if (!this.trackClosureState.get(s.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(s.trackId);
|
|
13684
13795
|
this.eventMediaDispatcher.captureForFrame({
|
|
13685
13796
|
deviceId,
|
|
13686
13797
|
frameHandle,
|
|
13687
13798
|
events: eventTargets,
|
|
13688
13799
|
trackFrames: firstFrameTargets,
|
|
13689
13800
|
snapshots: snapshotTargets,
|
|
13690
|
-
cropPadding: mediaSettings.cropPadding
|
|
13801
|
+
cropPadding: mediaSettings.cropPadding,
|
|
13802
|
+
rasterFallbackWantedTrackIds
|
|
13691
13803
|
}).then((res) => {
|
|
13804
|
+
for (const rf of res.rasterFallbacks) {
|
|
13805
|
+
const st = this.ensureTrackClosureState(deviceId, rf.trackId);
|
|
13806
|
+
if (!st.rasterFallback) st.rasterFallback = {
|
|
13807
|
+
jpeg: rf.jpeg,
|
|
13808
|
+
timestamp: rf.timestamp
|
|
13809
|
+
};
|
|
13810
|
+
}
|
|
13692
13811
|
for (const s of res.storedSnapshots) this.trackStore?.addSnapshot(s.trackId, {
|
|
13693
13812
|
timestamp: s.timestamp,
|
|
13694
13813
|
position: {
|
|
@@ -14699,6 +14818,44 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14699
14818
|
const expired = await this.trackStore.expireStale(Date.now());
|
|
14700
14819
|
for (const t of expired) {
|
|
14701
14820
|
const duration = t.lastSeen - t.firstSeen;
|
|
14821
|
+
const closure = this.trackClosureState.get(t.trackId);
|
|
14822
|
+
const outcome = decideZeroMediaPolicy({
|
|
14823
|
+
durationMs: duration,
|
|
14824
|
+
hasMedia: (await this.mediaStore?.listByOwner("track", t.trackId) ?? []).length > 0,
|
|
14825
|
+
confirmed: closure?.confirmed ?? false,
|
|
14826
|
+
hasRasterFallback: closure?.rasterFallback !== void 0
|
|
14827
|
+
});
|
|
14828
|
+
if (outcome === "suppress") {
|
|
14829
|
+
this.ctx.logger.info("track suppressed (zero-media false birth)", {
|
|
14830
|
+
tags: { deviceId: t.deviceId },
|
|
14831
|
+
meta: {
|
|
14832
|
+
trackId: t.trackId,
|
|
14833
|
+
className: t.className,
|
|
14834
|
+
durationMs: duration,
|
|
14835
|
+
positions: t.positions.length
|
|
14836
|
+
}
|
|
14837
|
+
});
|
|
14838
|
+
await this.suppressZeroMediaTrack(t.deviceId, t.trackId);
|
|
14839
|
+
continue;
|
|
14840
|
+
}
|
|
14841
|
+
if (outcome === "raster-fallback" && closure?.rasterFallback) try {
|
|
14842
|
+
await this.mediaStore?.put({
|
|
14843
|
+
deviceId: t.deviceId,
|
|
14844
|
+
ownerKind: "track",
|
|
14845
|
+
ownerId: t.trackId,
|
|
14846
|
+
kind: "thumbnail",
|
|
14847
|
+
timestamp: closure.rasterFallback.timestamp,
|
|
14848
|
+
data: closure.rasterFallback.jpeg
|
|
14849
|
+
});
|
|
14850
|
+
} catch (err) {
|
|
14851
|
+
this.ctx.logger.debug("zero-media raster fallback put failed", {
|
|
14852
|
+
tags: { deviceId: t.deviceId },
|
|
14853
|
+
meta: {
|
|
14854
|
+
trackId: t.trackId,
|
|
14855
|
+
error: String(err)
|
|
14856
|
+
}
|
|
14857
|
+
});
|
|
14858
|
+
}
|
|
14702
14859
|
this.ctx.logger.info("track ended", {
|
|
14703
14860
|
tags: { deviceId: t.deviceId },
|
|
14704
14861
|
meta: {
|
|
@@ -14809,12 +14966,53 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14809
14966
|
});
|
|
14810
14967
|
this.emitTrackLifecycle(endPayload, t.lastSeen);
|
|
14811
14968
|
this.trackLifecycleUpdateMem.delete(t.trackId);
|
|
14969
|
+
this.trackClosureState.delete(t.trackId);
|
|
14812
14970
|
}
|
|
14813
14971
|
} catch (err) {
|
|
14814
14972
|
if (this.shuttingDown) return;
|
|
14815
14973
|
this.ctx.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
|
|
14816
14974
|
}
|
|
14817
14975
|
}
|
|
14976
|
+
/**
|
|
14977
|
+
* Undo a zero-media false-birth track (spec §"Zero-media track policy"): tear
|
|
14978
|
+
* down its live per-track state (mirroring the natural end MINUS importance
|
|
14979
|
+
* scoring + the lifecycle `end`/`TrackEnded` events) and cascade-delete the
|
|
14980
|
+
* just-persisted row + any events/media so nothing dangles. Best-effort.
|
|
14981
|
+
*/
|
|
14982
|
+
async suppressZeroMediaTrack(deviceId, trackId) {
|
|
14983
|
+
const faceEnd = this.faceRecognizer?.onTrackEnd(deviceId, trackId);
|
|
14984
|
+
const dropKeyFrame = () => {
|
|
14985
|
+
this.keyFrameKeyByTrackId.delete(trackId);
|
|
14986
|
+
};
|
|
14987
|
+
if (faceEnd) faceEnd.finally(dropKeyFrame);
|
|
14988
|
+
else dropKeyFrame();
|
|
14989
|
+
this.plateRecognizer?.onTrackEnd(deviceId, trackId);
|
|
14990
|
+
this.bestFrameTracker.delete(trackId);
|
|
14991
|
+
this.objectEmbeddingBestSelector.delete(trackId);
|
|
14992
|
+
this.lastFrameAtByTrack.delete(trackId);
|
|
14993
|
+
this.thumbnailLandedTracks.delete(trackId);
|
|
14994
|
+
this.thumbnailInFlight.delete(trackId);
|
|
14995
|
+
this.keyFrameInFlight.delete(trackId);
|
|
14996
|
+
this.detailDispatcher?.onTrackEnded(deviceId, trackId);
|
|
14997
|
+
this.overlayState.onTrackEnded(deviceId, trackId);
|
|
14998
|
+
this.trackLifecycleUpdateMem.delete(trackId);
|
|
14999
|
+
this.trackClosureState.delete(trackId);
|
|
15000
|
+
if (this.eventStore && this.mediaStore && this.trackStore) try {
|
|
15001
|
+
await cascadeDeleteTrack({
|
|
15002
|
+
eventStore: this.eventStore,
|
|
15003
|
+
mediaStore: this.mediaStore,
|
|
15004
|
+
trackStore: this.trackStore
|
|
15005
|
+
}, trackId);
|
|
15006
|
+
} catch (err) {
|
|
15007
|
+
this.ctx.logger.debug("zero-media suppression cascade failed", {
|
|
15008
|
+
tags: { deviceId },
|
|
15009
|
+
meta: {
|
|
15010
|
+
trackId,
|
|
15011
|
+
error: String(err)
|
|
15012
|
+
}
|
|
15013
|
+
});
|
|
15014
|
+
}
|
|
15015
|
+
}
|
|
14818
15016
|
async sweepRetention() {
|
|
14819
15017
|
if (this.shuttingDown || !this.eventStore || !this.mediaStore) return;
|
|
14820
15018
|
const now = Date.now();
|
|
@@ -15024,6 +15222,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15024
15222
|
this.thumbnailInFlight.delete(track.trackId);
|
|
15025
15223
|
this.keyFrameInFlight.delete(track.trackId);
|
|
15026
15224
|
this.trackLifecycleUpdateMem.delete(track.trackId);
|
|
15225
|
+
this.trackClosureState.delete(track.trackId);
|
|
15027
15226
|
this.detailDispatcher?.onTrackEnded(deviceId, track.trackId);
|
|
15028
15227
|
this.overlayState.onTrackEnded(deviceId, track.trackId);
|
|
15029
15228
|
this.lastActiveTrackIds.get(key)?.delete(track.trackId);
|
|
@@ -15322,6 +15521,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15322
15521
|
}
|
|
15323
15522
|
async clearTracks(input) {
|
|
15324
15523
|
this.trackStore?.clearDevice(input.deviceId);
|
|
15524
|
+
this.clearClosureStateForDevice(input.deviceId);
|
|
15325
15525
|
this.stationaryRegistry?.clearDevice(input.deviceId);
|
|
15326
15526
|
this.overlayState.clearDevice(input.deviceId);
|
|
15327
15527
|
this.overlaySynthesisWarnAt.delete(input.deviceId);
|
|
@@ -15534,6 +15734,27 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15534
15734
|
clearLiveTrackState(deviceId, trackId) {
|
|
15535
15735
|
this.detailDispatcher?.onTrackEnded(deviceId, trackId);
|
|
15536
15736
|
this.overlayState.onTrackEnded(deviceId, trackId);
|
|
15737
|
+
this.trackClosureState.delete(trackId);
|
|
15738
|
+
}
|
|
15739
|
+
/** Lazily create + return a track's close-policy state (zero-media policy). */
|
|
15740
|
+
ensureTrackClosureState(deviceId, trackId) {
|
|
15741
|
+
let st = this.trackClosureState.get(trackId);
|
|
15742
|
+
if (!st) {
|
|
15743
|
+
st = {
|
|
15744
|
+
deviceId,
|
|
15745
|
+
confirmed: false
|
|
15746
|
+
};
|
|
15747
|
+
this.trackClosureState.set(trackId, st);
|
|
15748
|
+
}
|
|
15749
|
+
return st;
|
|
15750
|
+
}
|
|
15751
|
+
/** Sticky-mark a track as confirmed once it reaches the tracker's hit gate. */
|
|
15752
|
+
markTrackConfirmed(deviceId, trackId) {
|
|
15753
|
+
this.ensureTrackClosureState(deviceId, trackId).confirmed = true;
|
|
15754
|
+
}
|
|
15755
|
+
/** Drop every close-policy entry for a device (mirrors `trackStore.clearDevice`). */
|
|
15756
|
+
clearClosureStateForDevice(deviceId) {
|
|
15757
|
+
for (const [trackId, st] of this.trackClosureState) if (st.deviceId === deviceId) this.trackClosureState.delete(trackId);
|
|
15537
15758
|
}
|
|
15538
15759
|
async deleteTracks(input) {
|
|
15539
15760
|
const cascade = this.buildTrackCascadeRegistry();
|
|
@@ -15828,11 +16049,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15828
16049
|
const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
|
|
15829
16050
|
if (preferKind !== void 0 && preferKind.length > 0) {
|
|
15830
16051
|
const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
|
|
15831
|
-
const
|
|
15832
|
-
if (!
|
|
16052
|
+
const chosen = resolveCropForcedMedia([...eventFiles, ...trackFiles], (files) => pickCleanMedia(files, preferKind));
|
|
16053
|
+
if (!chosen) return null;
|
|
15833
16054
|
return {
|
|
15834
|
-
bytes: Buffer.from(
|
|
15835
|
-
key:
|
|
16055
|
+
bytes: Buffer.from(chosen.base64, "base64"),
|
|
16056
|
+
key: chosen.key
|
|
15836
16057
|
};
|
|
15837
16058
|
}
|
|
15838
16059
|
const chosen = await resolveDefaultEventMedia({
|
|
@@ -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-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-ChHurWQz.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.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "CamStack Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|