@camstack/addon-post-analysis 1.2.2 → 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/{dist-DSP-cVM0.js → dist-Ck79MtSp.js} +2 -1
- package/dist/{dist-Bu9eXObI.mjs → dist-gQ5DHTYd.mjs} +2 -1
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-DtSEvIXU.js → node-C4bKtLou.js} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-2w2j7Rm4.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-ChHurWQz.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-CTQ8-sg-.mjs → hostInit-DQbbiJYs.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +390 -46
- package/dist/pipeline-analytics/index.mjs +389 -45
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-
|
|
1
|
+
import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-gQ5DHTYd.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import sharp from "sharp";
|
|
4
4
|
//#region src/pipeline-analytics/videoclips-provider.ts
|
|
@@ -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
|
|
@@ -4492,6 +4510,7 @@ var SINGLE_INSTANCE_KINDS = new Set([
|
|
|
4492
4510
|
"keyFrame",
|
|
4493
4511
|
"keyFrameSmall",
|
|
4494
4512
|
"thumbnail",
|
|
4513
|
+
"thumbnailSmall",
|
|
4495
4514
|
"firstFrame",
|
|
4496
4515
|
"lastFrame"
|
|
4497
4516
|
]);
|
|
@@ -6472,6 +6491,106 @@ function squareSafeCropRegionNormalized(bbox, frame, padding) {
|
|
|
6472
6491
|
};
|
|
6473
6492
|
}
|
|
6474
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
|
|
6524
|
+
//#region src/shared/frame/square-subject-crop.ts
|
|
6525
|
+
/** Expansion factor applied to the bbox long side to frame the subject with a
|
|
6526
|
+
* little breathing room (operator choice: ×1.2). */
|
|
6527
|
+
var SQUARE_SUBJECT_CROP_EXPANSION = 1.2;
|
|
6528
|
+
/**
|
|
6529
|
+
* Compute a SQUARE crop region in PIXEL space around the subject bbox.
|
|
6530
|
+
*
|
|
6531
|
+
* Algorithm:
|
|
6532
|
+
* 1. center: cx = x + w/2, cy = y + h/2
|
|
6533
|
+
* 2. side = max(w, h) × SQUARE_SUBJECT_CROP_EXPANSION
|
|
6534
|
+
* 3. clamp side to fit the frame: side = min(side, W, H) — a square can never
|
|
6535
|
+
* exceed the frame's SHORT edge (this is the "subject bigger than the
|
|
6536
|
+
* frame's short side" case)
|
|
6537
|
+
* 4. center-and-clamp the origin so the square stays fully inside the frame
|
|
6538
|
+
* 5. round to integer pixels
|
|
6539
|
+
*
|
|
6540
|
+
* The returned region fully contains the bbox whenever the bbox itself fits in a
|
|
6541
|
+
* square of the frame's short side (always true for real detections).
|
|
6542
|
+
*/
|
|
6543
|
+
function squareSubjectCropRegion(bbox, frame) {
|
|
6544
|
+
const { W, H } = frame;
|
|
6545
|
+
const cx = bbox.x + bbox.w / 2;
|
|
6546
|
+
const cy = bbox.y + bbox.h / 2;
|
|
6547
|
+
const longerSide = Math.max(bbox.w, bbox.h);
|
|
6548
|
+
const side = Math.min(longerSide * SQUARE_SUBJECT_CROP_EXPANSION, W, H);
|
|
6549
|
+
const rawX0 = cx - side / 2;
|
|
6550
|
+
const rawY0 = cy - side / 2;
|
|
6551
|
+
const x0 = Math.max(0, Math.min(rawX0, W - side));
|
|
6552
|
+
const y0 = Math.max(0, Math.min(rawY0, H - side));
|
|
6553
|
+
return {
|
|
6554
|
+
x: Math.round(x0),
|
|
6555
|
+
y: Math.round(y0),
|
|
6556
|
+
w: Math.round(side),
|
|
6557
|
+
h: Math.round(side)
|
|
6558
|
+
};
|
|
6559
|
+
}
|
|
6560
|
+
/**
|
|
6561
|
+
* The same SQUARE region as {@link squareSubjectCropRegion}, expressed in
|
|
6562
|
+
* NORMALIZED [0,1]×[0,1] coordinates. A normalized box maps DIRECTLY onto a
|
|
6563
|
+
* native-resolution surface of the SAME aspect ratio, so the region computed
|
|
6564
|
+
* from the detection-frame dimensions addresses the exact same ROI on the
|
|
6565
|
+
* runner's retained native frame. Reuses the pixel geometry verbatim (single
|
|
6566
|
+
* source of truth) and divides by the frame dimensions.
|
|
6567
|
+
*/
|
|
6568
|
+
function squareSubjectCropRegionNormalized(bbox, frame) {
|
|
6569
|
+
const region = squareSubjectCropRegion(bbox, frame);
|
|
6570
|
+
return {
|
|
6571
|
+
x: region.x / frame.W,
|
|
6572
|
+
y: region.y / frame.H,
|
|
6573
|
+
w: region.w / frame.W,
|
|
6574
|
+
h: region.h / frame.H
|
|
6575
|
+
};
|
|
6576
|
+
}
|
|
6577
|
+
/**
|
|
6578
|
+
* Derive the `thumbnailSmall` JPEG from an already-encoded native subject-crop
|
|
6579
|
+
* JPEG. Downscales to at most {@link THUMBNAIL_SMALL_MAX_WIDTH} on the long side
|
|
6580
|
+
* (aspect preserved). If the native crop's long side is already ≤ the cap, the
|
|
6581
|
+
* ORIGINAL buffer is returned unchanged (store as-is, never upscale, no wasted
|
|
6582
|
+
* re-encode).
|
|
6583
|
+
*/
|
|
6584
|
+
async function deriveThumbnailSmall(nativeJpeg) {
|
|
6585
|
+
const meta = await sharp(nativeJpeg).metadata();
|
|
6586
|
+
const longSide = Math.max(meta.width ?? 0, meta.height ?? 0);
|
|
6587
|
+
if (longSide > 0 && longSide <= 480) return nativeJpeg;
|
|
6588
|
+
return sharp(nativeJpeg).resize(480, 480, {
|
|
6589
|
+
fit: "inside",
|
|
6590
|
+
withoutEnlargement: true
|
|
6591
|
+
}).jpeg({ quality: 88 }).toBuffer();
|
|
6592
|
+
}
|
|
6593
|
+
//#endregion
|
|
6475
6594
|
//#region src/shared/frame/box-drawer.ts
|
|
6476
6595
|
var DEFAULT_COLOR = DEFAULT_EVENT_COLOR;
|
|
6477
6596
|
var DEFAULT_QUALITY = 80;
|
|
@@ -6640,7 +6759,8 @@ var EventMediaDispatcher = class {
|
|
|
6640
6759
|
const snapshots = input.snapshots ?? [];
|
|
6641
6760
|
const empty = {
|
|
6642
6761
|
storedSnapshots: [],
|
|
6643
|
-
thumbnailTrackIds: []
|
|
6762
|
+
thumbnailTrackIds: [],
|
|
6763
|
+
rasterFallbacks: []
|
|
6644
6764
|
};
|
|
6645
6765
|
if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
|
|
6646
6766
|
let decoded;
|
|
@@ -6692,21 +6812,67 @@ var EventMediaDispatcher = class {
|
|
|
6692
6812
|
});
|
|
6693
6813
|
return empty;
|
|
6694
6814
|
}
|
|
6815
|
+
const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds);
|
|
6695
6816
|
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
|
|
6696
6817
|
for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf);
|
|
6697
6818
|
const storedSnapshots = [];
|
|
6698
6819
|
const thumbnailTrackIds = [];
|
|
6699
6820
|
for (const sn of snapshots) {
|
|
6700
|
-
const res = await this.writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn
|
|
6821
|
+
const res = await this.writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn);
|
|
6701
6822
|
if (res.storedSnapshot) storedSnapshots.push(res.storedSnapshot);
|
|
6702
6823
|
if (res.thumbnailWritten) thumbnailTrackIds.push(sn.trackId);
|
|
6703
6824
|
}
|
|
6704
6825
|
return {
|
|
6705
6826
|
storedSnapshots,
|
|
6706
|
-
thumbnailTrackIds
|
|
6827
|
+
thumbnailTrackIds,
|
|
6828
|
+
rasterFallbacks
|
|
6707
6829
|
};
|
|
6708
6830
|
}
|
|
6709
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
|
+
/**
|
|
6710
6876
|
* Periodic per-track media (§5). The boxed FULL frame is encoded once and
|
|
6711
6877
|
* shared by the appended `snapshot` (timeline filmstrip) and the rolling
|
|
6712
6878
|
* `lastFrame` (overwrite). The best `thumbnail` is DIFFERENT: a clean
|
|
@@ -6718,7 +6884,7 @@ var EventMediaDispatcher = class {
|
|
|
6718
6884
|
* failed). `thumbnailWritten` reports whether a best `thumbnail` actually
|
|
6719
6885
|
* landed this frame (#27-A) so the caller can stop forcing retries.
|
|
6720
6886
|
*/
|
|
6721
|
-
async writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn
|
|
6887
|
+
async writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn) {
|
|
6722
6888
|
if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return {
|
|
6723
6889
|
storedSnapshot: null,
|
|
6724
6890
|
thumbnailWritten: false
|
|
@@ -6745,8 +6911,11 @@ var EventMediaDispatcher = class {
|
|
|
6745
6911
|
if (sn.rollingLastFrame && boxed) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
|
|
6746
6912
|
let thumbnailWritten = false;
|
|
6747
6913
|
if (sn.bestThumbnail) {
|
|
6748
|
-
const
|
|
6749
|
-
if (
|
|
6914
|
+
const variants = await this.cropSubjectVariants(frameHandle, fw, fh, sn.bbox);
|
|
6915
|
+
if (variants) {
|
|
6916
|
+
thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, variants.thumbnail);
|
|
6917
|
+
await this.replaceKind(deviceId, sn.trackId, "thumbnailSmall", sn.timestamp, variants.thumbnailSmall);
|
|
6918
|
+
}
|
|
6750
6919
|
}
|
|
6751
6920
|
return {
|
|
6752
6921
|
storedSnapshot: stored,
|
|
@@ -6796,6 +6965,54 @@ var EventMediaDispatcher = class {
|
|
|
6796
6965
|
}
|
|
6797
6966
|
}
|
|
6798
6967
|
/**
|
|
6968
|
+
* The best-shot subject crop as its TWO persisted variants (best-crop
|
|
6969
|
+
* fast-load, 2026-07-21). ONE native fetch: the SQUARE-framed subject ROI
|
|
6970
|
+
* (side = max(w,h)×1.2, clamped to the frame — {@link squareSubjectCropRegionNormalized})
|
|
6971
|
+
* is requested from the runner's retained native surface with NO `maxWidth`
|
|
6972
|
+
* (uncapped TRUE native, decision #3) → the `thumbnail`. The `thumbnailSmall`
|
|
6973
|
+
* is DERIVED by downscaling that SAME JPEG to the 480 long-side cap — never a
|
|
6974
|
+
* second `getNativeCropJpeg`, never upscaled ({@link deriveThumbnailSmall}
|
|
6975
|
+
* returns the native buffer as-is when it is already ≤ 480).
|
|
6976
|
+
*
|
|
6977
|
+
* NATIVE-OR-NOTHING: any miss/error (or a runner without the method) returns
|
|
6978
|
+
* `null` after a loud `logger.warn`; the caller SKIPS the write and the
|
|
6979
|
+
* per-frame retry lands a real native crop later. Never a local resize
|
|
6980
|
+
* upscale of a ≤640 tile (a blurred lie).
|
|
6981
|
+
*/
|
|
6982
|
+
async cropSubjectVariants(frameHandle, fw, fh, bbox) {
|
|
6983
|
+
if (!this.deps.getNativeCropJpeg) {
|
|
6984
|
+
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
6985
|
+
shmId: frameHandle.shmId,
|
|
6986
|
+
reason: "no-native-cap"
|
|
6987
|
+
} });
|
|
6988
|
+
return null;
|
|
6989
|
+
}
|
|
6990
|
+
try {
|
|
6991
|
+
const norm = squareSubjectCropRegionNormalized(bbox, {
|
|
6992
|
+
W: fw,
|
|
6993
|
+
H: fh
|
|
6994
|
+
});
|
|
6995
|
+
const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
|
|
6996
|
+
if (!native) {
|
|
6997
|
+
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
6998
|
+
shmId: frameHandle.shmId,
|
|
6999
|
+
reason: "native-miss"
|
|
7000
|
+
} });
|
|
7001
|
+
return null;
|
|
7002
|
+
}
|
|
7003
|
+
return {
|
|
7004
|
+
thumbnail: native,
|
|
7005
|
+
thumbnailSmall: await deriveThumbnailSmall(native)
|
|
7006
|
+
};
|
|
7007
|
+
} catch (err) {
|
|
7008
|
+
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
7009
|
+
shmId: frameHandle.shmId,
|
|
7010
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7011
|
+
} });
|
|
7012
|
+
return null;
|
|
7013
|
+
}
|
|
7014
|
+
}
|
|
7015
|
+
/**
|
|
6799
7016
|
* The boxed timeline tile (`firstFrame`/`snapshot`/`lastFrame`): the track's
|
|
6800
7017
|
* OWN box burned onto the NATIVE full frame downscaled to
|
|
6801
7018
|
* {@link SNAPSHOT_MAX_WIDTH}. On a native miss it falls back to the ≤640
|
|
@@ -6922,36 +7139,6 @@ var EventMediaDispatcher = class {
|
|
|
6922
7139
|
}
|
|
6923
7140
|
};
|
|
6924
7141
|
//#endregion
|
|
6925
|
-
//#region src/shared/frame/crop-extractor.ts
|
|
6926
|
-
/**
|
|
6927
|
-
* Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
|
|
6928
|
-
* Coordinates are clamped to frame bounds to avoid out-of-range errors.
|
|
6929
|
-
*/
|
|
6930
|
-
async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
|
|
6931
|
-
const rawLeft = Math.round(bbox.x * frameWidth);
|
|
6932
|
-
const rawTop = Math.round(bbox.y * frameHeight);
|
|
6933
|
-
const rawWidth = Math.round(bbox.w * frameWidth);
|
|
6934
|
-
const rawHeight = Math.round(bbox.h * frameHeight);
|
|
6935
|
-
const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
|
|
6936
|
-
const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
|
|
6937
|
-
const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
|
|
6938
|
-
const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
|
|
6939
|
-
return {
|
|
6940
|
-
crop: await sharp(frameData, { raw: {
|
|
6941
|
-
width: frameWidth,
|
|
6942
|
-
height: frameHeight,
|
|
6943
|
-
channels: 3
|
|
6944
|
-
} }).extract({
|
|
6945
|
-
left,
|
|
6946
|
-
top,
|
|
6947
|
-
width,
|
|
6948
|
-
height
|
|
6949
|
-
}).jpeg({ quality: 90 }).toBuffer(),
|
|
6950
|
-
width,
|
|
6951
|
-
height
|
|
6952
|
-
};
|
|
6953
|
-
}
|
|
6954
|
-
//#endregion
|
|
6955
7142
|
//#region src/pipeline-analytics/embedding/embedding-dispatcher.ts
|
|
6956
7143
|
function selectCropBbox(detection) {
|
|
6957
7144
|
return detection.refinedBbox ?? detection.bbox;
|
|
@@ -8740,6 +8927,16 @@ function planPeriodicMedia(input) {
|
|
|
8740
8927
|
bestThumbnail: input.isNewBest || !thumbnailLanded
|
|
8741
8928
|
};
|
|
8742
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
|
+
}
|
|
8743
8940
|
//#endregion
|
|
8744
8941
|
//#region src/pipeline-analytics/best-thumbnail-guard.ts
|
|
8745
8942
|
/**
|
|
@@ -11992,10 +12189,28 @@ function pickEventOwnedMedia(files) {
|
|
|
11992
12189
|
return files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
|
|
11993
12190
|
}
|
|
11994
12191
|
/** Pick a track's fallback media in the shared cadence-preference order the
|
|
11995
|
-
* KeyEvent path uses:
|
|
11996
|
-
*
|
|
12192
|
+
* KeyEvent path uses: `thumbnailSmall` (480 fast-load) → best `thumbnail` →
|
|
12193
|
+
* rolling `lastFrame` → `firstFrame` → newest `snapshot` → any track blob.
|
|
12194
|
+
* Undefined when the track owns no media. */
|
|
11997
12195
|
function pickTrackFallbackMedia(files) {
|
|
11998
|
-
return 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];
|
|
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];
|
|
12197
|
+
}
|
|
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);
|
|
11999
12214
|
}
|
|
12000
12215
|
/**
|
|
12001
12216
|
* Resolve the default-path media for an event id: the event's own media when it
|
|
@@ -12353,6 +12568,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12353
12568
|
* area) + emit time, so a material improvement is measured against the
|
|
12354
12569
|
* last emit and debounced. Seeded at `start`, dropped at `end`. */
|
|
12355
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();
|
|
12356
12579
|
/** The shared crop extractor (native-res first, detection-frame fallback),
|
|
12357
12580
|
* captured in the constructor so `processFrame` can crop object thumbnails in
|
|
12358
12581
|
* the same live-frame window as the face/plate/event-media captures. The
|
|
@@ -12844,6 +13067,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12844
13067
|
this.bindingCache?.onBindingsChanged(data);
|
|
12845
13068
|
if (data.capName === "pipeline-analytics" && data.reason === "wrapper-deactivated") {
|
|
12846
13069
|
this.trackStore?.clearDevice(data.deviceId);
|
|
13070
|
+
this.clearClosureStateForDevice(data.deviceId);
|
|
12847
13071
|
this.stationaryRegistry?.forgetDevice(data.deviceId);
|
|
12848
13072
|
this.overlayState.clearDevice(data.deviceId);
|
|
12849
13073
|
this.overlaySynthesisWarnAt.delete(data.deviceId);
|
|
@@ -12862,6 +13086,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12862
13086
|
this.unsubDeviceUnreg = this.ctx.eventBus.subscribe({ category: EventCategory.DeviceUnregistered }, (ev) => {
|
|
12863
13087
|
const { deviceId } = ev.data;
|
|
12864
13088
|
this.trackStore?.clearDevice(deviceId);
|
|
13089
|
+
this.clearClosureStateForDevice(deviceId);
|
|
12865
13090
|
this.stationaryRegistry?.forgetDevice(deviceId);
|
|
12866
13091
|
this.overlayState.clearDevice(deviceId);
|
|
12867
13092
|
this.overlaySynthesisWarnAt.delete(deviceId);
|
|
@@ -13217,6 +13442,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13217
13442
|
this.thumbnailInFlight.clear();
|
|
13218
13443
|
this.keyFrameInFlight.clear();
|
|
13219
13444
|
this.trackLifecycleUpdateMem.clear();
|
|
13445
|
+
this.trackClosureState.clear();
|
|
13220
13446
|
this.objectEmbeddingBestSelector.clear();
|
|
13221
13447
|
this.levelStateByDevice.clear();
|
|
13222
13448
|
this.settingsCacheByDevice.clear();
|
|
@@ -13335,6 +13561,10 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13335
13561
|
});
|
|
13336
13562
|
positionsCountById.set(t.trackId, upserted.positions.length);
|
|
13337
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
|
+
}
|
|
13338
13568
|
const log = this.ctx.logger.withTags({ deviceId });
|
|
13339
13569
|
const prevIds = this.lastActiveTrackIds.get(key) ?? /* @__PURE__ */ new Set();
|
|
13340
13570
|
const firstFrameTargets = [];
|
|
@@ -13387,6 +13617,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13387
13617
|
...t.label ? { label: t.label } : {}
|
|
13388
13618
|
});
|
|
13389
13619
|
this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
|
|
13620
|
+
this.lastFrameAtByTrack.set(id, result.timestamp);
|
|
13390
13621
|
}
|
|
13391
13622
|
this.ctx.eventBus.emit({
|
|
13392
13623
|
id: `pa-${randomUUID()}`,
|
|
@@ -13558,14 +13789,25 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13558
13789
|
} });
|
|
13559
13790
|
const thumbInFlightTrackIds = snapshotTargets.filter((t) => t.bestThumbnail).map((t) => t.trackId);
|
|
13560
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);
|
|
13561
13795
|
this.eventMediaDispatcher.captureForFrame({
|
|
13562
13796
|
deviceId,
|
|
13563
13797
|
frameHandle,
|
|
13564
13798
|
events: eventTargets,
|
|
13565
13799
|
trackFrames: firstFrameTargets,
|
|
13566
13800
|
snapshots: snapshotTargets,
|
|
13567
|
-
cropPadding: mediaSettings.cropPadding
|
|
13801
|
+
cropPadding: mediaSettings.cropPadding,
|
|
13802
|
+
rasterFallbackWantedTrackIds
|
|
13568
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
|
+
}
|
|
13569
13811
|
for (const s of res.storedSnapshots) this.trackStore?.addSnapshot(s.trackId, {
|
|
13570
13812
|
timestamp: s.timestamp,
|
|
13571
13813
|
position: {
|
|
@@ -14576,6 +14818,44 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14576
14818
|
const expired = await this.trackStore.expireStale(Date.now());
|
|
14577
14819
|
for (const t of expired) {
|
|
14578
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
|
+
}
|
|
14579
14859
|
this.ctx.logger.info("track ended", {
|
|
14580
14860
|
tags: { deviceId: t.deviceId },
|
|
14581
14861
|
meta: {
|
|
@@ -14686,12 +14966,53 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14686
14966
|
});
|
|
14687
14967
|
this.emitTrackLifecycle(endPayload, t.lastSeen);
|
|
14688
14968
|
this.trackLifecycleUpdateMem.delete(t.trackId);
|
|
14969
|
+
this.trackClosureState.delete(t.trackId);
|
|
14689
14970
|
}
|
|
14690
14971
|
} catch (err) {
|
|
14691
14972
|
if (this.shuttingDown) return;
|
|
14692
14973
|
this.ctx.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
|
|
14693
14974
|
}
|
|
14694
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
|
+
}
|
|
14695
15016
|
async sweepRetention() {
|
|
14696
15017
|
if (this.shuttingDown || !this.eventStore || !this.mediaStore) return;
|
|
14697
15018
|
const now = Date.now();
|
|
@@ -14901,6 +15222,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14901
15222
|
this.thumbnailInFlight.delete(track.trackId);
|
|
14902
15223
|
this.keyFrameInFlight.delete(track.trackId);
|
|
14903
15224
|
this.trackLifecycleUpdateMem.delete(track.trackId);
|
|
15225
|
+
this.trackClosureState.delete(track.trackId);
|
|
14904
15226
|
this.detailDispatcher?.onTrackEnded(deviceId, track.trackId);
|
|
14905
15227
|
this.overlayState.onTrackEnded(deviceId, track.trackId);
|
|
14906
15228
|
this.lastActiveTrackIds.get(key)?.delete(track.trackId);
|
|
@@ -15199,6 +15521,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15199
15521
|
}
|
|
15200
15522
|
async clearTracks(input) {
|
|
15201
15523
|
this.trackStore?.clearDevice(input.deviceId);
|
|
15524
|
+
this.clearClosureStateForDevice(input.deviceId);
|
|
15202
15525
|
this.stationaryRegistry?.clearDevice(input.deviceId);
|
|
15203
15526
|
this.overlayState.clearDevice(input.deviceId);
|
|
15204
15527
|
this.overlaySynthesisWarnAt.delete(input.deviceId);
|
|
@@ -15411,6 +15734,27 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15411
15734
|
clearLiveTrackState(deviceId, trackId) {
|
|
15412
15735
|
this.detailDispatcher?.onTrackEnded(deviceId, trackId);
|
|
15413
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);
|
|
15414
15758
|
}
|
|
15415
15759
|
async deleteTracks(input) {
|
|
15416
15760
|
const cascade = this.buildTrackCascadeRegistry();
|
|
@@ -15705,11 +16049,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15705
16049
|
const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
|
|
15706
16050
|
if (preferKind !== void 0 && preferKind.length > 0) {
|
|
15707
16051
|
const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
|
|
15708
|
-
const
|
|
15709
|
-
if (!
|
|
16052
|
+
const chosen = resolveCropForcedMedia([...eventFiles, ...trackFiles], (files) => pickCleanMedia(files, preferKind));
|
|
16053
|
+
if (!chosen) return null;
|
|
15710
16054
|
return {
|
|
15711
|
-
bytes: Buffer.from(
|
|
15712
|
-
key:
|
|
16055
|
+
bytes: Buffer.from(chosen.base64, "base64"),
|
|
16056
|
+
key: chosen.key
|
|
15713
16057
|
};
|
|
15714
16058
|
}
|
|
15715
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",
|