@camstack/addon-post-analysis 1.2.2 → 1.2.3
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-C4dWm6ZL.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-CTQ8-sg-.mjs → hostInit-3T15bdHA.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +132 -9
- package/dist/pipeline-analytics/index.mjs +131 -8
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -16708,7 +16708,8 @@ var MediaFileKindEnum = _enum([
|
|
|
16708
16708
|
"faceCrop",
|
|
16709
16709
|
"plateCrop",
|
|
16710
16710
|
"keyFrame",
|
|
16711
|
-
"keyFrameSmall"
|
|
16711
|
+
"keyFrameSmall",
|
|
16712
|
+
"thumbnailSmall"
|
|
16712
16713
|
]);
|
|
16713
16714
|
var MediaFileSchema = object({
|
|
16714
16715
|
key: string(),
|
|
@@ -16686,7 +16686,8 @@ var MediaFileKindEnum = _enum([
|
|
|
16686
16686
|
"faceCrop",
|
|
16687
16687
|
"plateCrop",
|
|
16688
16688
|
"keyFrame",
|
|
16689
|
-
"keyFrameSmall"
|
|
16689
|
+
"keyFrameSmall",
|
|
16690
|
+
"thumbnailSmall"
|
|
16690
16691
|
]);
|
|
16691
16692
|
var MediaFileSchema = object({
|
|
16692
16693
|
key: string(),
|
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-Ck79MtSp.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_fs$1 = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
node_fs = require_dist.__toESM(node_fs);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { b as BaseAddon, f as hfModelUrl, u as embeddingEncoderCapability } from "../dist-
|
|
1
|
+
import { b as BaseAddon, f as hfModelUrl, u as embeddingEncoderCapability } from "../dist-gQ5DHTYd.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -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.3",
|
|
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.3",
|
|
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.3",
|
|
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.3",
|
|
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.3",
|
|
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.3",
|
|
85
85
|
scope: "default",
|
|
86
86
|
shareConfig: {
|
|
87
87
|
singleton: !0,
|
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-Ck79MtSp.js");
|
|
6
6
|
let node_crypto = require("node:crypto");
|
|
7
7
|
let sharp = require("sharp");
|
|
8
8
|
sharp = require_dist.__toESM(sharp);
|
|
@@ -4497,6 +4497,7 @@ var SINGLE_INSTANCE_KINDS = new Set([
|
|
|
4497
4497
|
"keyFrame",
|
|
4498
4498
|
"keyFrameSmall",
|
|
4499
4499
|
"thumbnail",
|
|
4500
|
+
"thumbnailSmall",
|
|
4500
4501
|
"firstFrame",
|
|
4501
4502
|
"lastFrame"
|
|
4502
4503
|
]);
|
|
@@ -6477,6 +6478,76 @@ function squareSafeCropRegionNormalized(bbox, frame, padding) {
|
|
|
6477
6478
|
};
|
|
6478
6479
|
}
|
|
6479
6480
|
//#endregion
|
|
6481
|
+
//#region src/shared/frame/square-subject-crop.ts
|
|
6482
|
+
/** Expansion factor applied to the bbox long side to frame the subject with a
|
|
6483
|
+
* little breathing room (operator choice: ×1.2). */
|
|
6484
|
+
var SQUARE_SUBJECT_CROP_EXPANSION = 1.2;
|
|
6485
|
+
/**
|
|
6486
|
+
* Compute a SQUARE crop region in PIXEL space around the subject bbox.
|
|
6487
|
+
*
|
|
6488
|
+
* Algorithm:
|
|
6489
|
+
* 1. center: cx = x + w/2, cy = y + h/2
|
|
6490
|
+
* 2. side = max(w, h) × SQUARE_SUBJECT_CROP_EXPANSION
|
|
6491
|
+
* 3. clamp side to fit the frame: side = min(side, W, H) — a square can never
|
|
6492
|
+
* exceed the frame's SHORT edge (this is the "subject bigger than the
|
|
6493
|
+
* frame's short side" case)
|
|
6494
|
+
* 4. center-and-clamp the origin so the square stays fully inside the frame
|
|
6495
|
+
* 5. round to integer pixels
|
|
6496
|
+
*
|
|
6497
|
+
* The returned region fully contains the bbox whenever the bbox itself fits in a
|
|
6498
|
+
* square of the frame's short side (always true for real detections).
|
|
6499
|
+
*/
|
|
6500
|
+
function squareSubjectCropRegion(bbox, frame) {
|
|
6501
|
+
const { W, H } = frame;
|
|
6502
|
+
const cx = bbox.x + bbox.w / 2;
|
|
6503
|
+
const cy = bbox.y + bbox.h / 2;
|
|
6504
|
+
const longerSide = Math.max(bbox.w, bbox.h);
|
|
6505
|
+
const side = Math.min(longerSide * SQUARE_SUBJECT_CROP_EXPANSION, W, H);
|
|
6506
|
+
const rawX0 = cx - side / 2;
|
|
6507
|
+
const rawY0 = cy - side / 2;
|
|
6508
|
+
const x0 = Math.max(0, Math.min(rawX0, W - side));
|
|
6509
|
+
const y0 = Math.max(0, Math.min(rawY0, H - side));
|
|
6510
|
+
return {
|
|
6511
|
+
x: Math.round(x0),
|
|
6512
|
+
y: Math.round(y0),
|
|
6513
|
+
w: Math.round(side),
|
|
6514
|
+
h: Math.round(side)
|
|
6515
|
+
};
|
|
6516
|
+
}
|
|
6517
|
+
/**
|
|
6518
|
+
* The same SQUARE region as {@link squareSubjectCropRegion}, expressed in
|
|
6519
|
+
* NORMALIZED [0,1]×[0,1] coordinates. A normalized box maps DIRECTLY onto a
|
|
6520
|
+
* native-resolution surface of the SAME aspect ratio, so the region computed
|
|
6521
|
+
* from the detection-frame dimensions addresses the exact same ROI on the
|
|
6522
|
+
* runner's retained native frame. Reuses the pixel geometry verbatim (single
|
|
6523
|
+
* source of truth) and divides by the frame dimensions.
|
|
6524
|
+
*/
|
|
6525
|
+
function squareSubjectCropRegionNormalized(bbox, frame) {
|
|
6526
|
+
const region = squareSubjectCropRegion(bbox, frame);
|
|
6527
|
+
return {
|
|
6528
|
+
x: region.x / frame.W,
|
|
6529
|
+
y: region.y / frame.H,
|
|
6530
|
+
w: region.w / frame.W,
|
|
6531
|
+
h: region.h / frame.H
|
|
6532
|
+
};
|
|
6533
|
+
}
|
|
6534
|
+
/**
|
|
6535
|
+
* Derive the `thumbnailSmall` JPEG from an already-encoded native subject-crop
|
|
6536
|
+
* JPEG. Downscales to at most {@link THUMBNAIL_SMALL_MAX_WIDTH} on the long side
|
|
6537
|
+
* (aspect preserved). If the native crop's long side is already ≤ the cap, the
|
|
6538
|
+
* ORIGINAL buffer is returned unchanged (store as-is, never upscale, no wasted
|
|
6539
|
+
* re-encode).
|
|
6540
|
+
*/
|
|
6541
|
+
async function deriveThumbnailSmall(nativeJpeg) {
|
|
6542
|
+
const meta = await (0, sharp.default)(nativeJpeg).metadata();
|
|
6543
|
+
const longSide = Math.max(meta.width ?? 0, meta.height ?? 0);
|
|
6544
|
+
if (longSide > 0 && longSide <= 480) return nativeJpeg;
|
|
6545
|
+
return (0, sharp.default)(nativeJpeg).resize(480, 480, {
|
|
6546
|
+
fit: "inside",
|
|
6547
|
+
withoutEnlargement: true
|
|
6548
|
+
}).jpeg({ quality: 88 }).toBuffer();
|
|
6549
|
+
}
|
|
6550
|
+
//#endregion
|
|
6480
6551
|
//#region src/shared/frame/box-drawer.ts
|
|
6481
6552
|
var DEFAULT_COLOR = require_dist.DEFAULT_EVENT_COLOR;
|
|
6482
6553
|
var DEFAULT_QUALITY = 80;
|
|
@@ -6702,7 +6773,7 @@ var EventMediaDispatcher = class {
|
|
|
6702
6773
|
const storedSnapshots = [];
|
|
6703
6774
|
const thumbnailTrackIds = [];
|
|
6704
6775
|
for (const sn of snapshots) {
|
|
6705
|
-
const res = await this.writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn
|
|
6776
|
+
const res = await this.writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn);
|
|
6706
6777
|
if (res.storedSnapshot) storedSnapshots.push(res.storedSnapshot);
|
|
6707
6778
|
if (res.thumbnailWritten) thumbnailTrackIds.push(sn.trackId);
|
|
6708
6779
|
}
|
|
@@ -6723,7 +6794,7 @@ var EventMediaDispatcher = class {
|
|
|
6723
6794
|
* failed). `thumbnailWritten` reports whether a best `thumbnail` actually
|
|
6724
6795
|
* landed this frame (#27-A) so the caller can stop forcing retries.
|
|
6725
6796
|
*/
|
|
6726
|
-
async writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn
|
|
6797
|
+
async writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn) {
|
|
6727
6798
|
if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return {
|
|
6728
6799
|
storedSnapshot: null,
|
|
6729
6800
|
thumbnailWritten: false
|
|
@@ -6750,8 +6821,11 @@ var EventMediaDispatcher = class {
|
|
|
6750
6821
|
if (sn.rollingLastFrame && boxed) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
|
|
6751
6822
|
let thumbnailWritten = false;
|
|
6752
6823
|
if (sn.bestThumbnail) {
|
|
6753
|
-
const
|
|
6754
|
-
if (
|
|
6824
|
+
const variants = await this.cropSubjectVariants(frameHandle, fw, fh, sn.bbox);
|
|
6825
|
+
if (variants) {
|
|
6826
|
+
thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, variants.thumbnail);
|
|
6827
|
+
await this.replaceKind(deviceId, sn.trackId, "thumbnailSmall", sn.timestamp, variants.thumbnailSmall);
|
|
6828
|
+
}
|
|
6755
6829
|
}
|
|
6756
6830
|
return {
|
|
6757
6831
|
storedSnapshot: stored,
|
|
@@ -6801,6 +6875,54 @@ var EventMediaDispatcher = class {
|
|
|
6801
6875
|
}
|
|
6802
6876
|
}
|
|
6803
6877
|
/**
|
|
6878
|
+
* The best-shot subject crop as its TWO persisted variants (best-crop
|
|
6879
|
+
* fast-load, 2026-07-21). ONE native fetch: the SQUARE-framed subject ROI
|
|
6880
|
+
* (side = max(w,h)×1.2, clamped to the frame — {@link squareSubjectCropRegionNormalized})
|
|
6881
|
+
* is requested from the runner's retained native surface with NO `maxWidth`
|
|
6882
|
+
* (uncapped TRUE native, decision #3) → the `thumbnail`. The `thumbnailSmall`
|
|
6883
|
+
* is DERIVED by downscaling that SAME JPEG to the 480 long-side cap — never a
|
|
6884
|
+
* second `getNativeCropJpeg`, never upscaled ({@link deriveThumbnailSmall}
|
|
6885
|
+
* returns the native buffer as-is when it is already ≤ 480).
|
|
6886
|
+
*
|
|
6887
|
+
* NATIVE-OR-NOTHING: any miss/error (or a runner without the method) returns
|
|
6888
|
+
* `null` after a loud `logger.warn`; the caller SKIPS the write and the
|
|
6889
|
+
* per-frame retry lands a real native crop later. Never a local resize
|
|
6890
|
+
* upscale of a ≤640 tile (a blurred lie).
|
|
6891
|
+
*/
|
|
6892
|
+
async cropSubjectVariants(frameHandle, fw, fh, bbox) {
|
|
6893
|
+
if (!this.deps.getNativeCropJpeg) {
|
|
6894
|
+
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
6895
|
+
shmId: frameHandle.shmId,
|
|
6896
|
+
reason: "no-native-cap"
|
|
6897
|
+
} });
|
|
6898
|
+
return null;
|
|
6899
|
+
}
|
|
6900
|
+
try {
|
|
6901
|
+
const norm = squareSubjectCropRegionNormalized(bbox, {
|
|
6902
|
+
W: fw,
|
|
6903
|
+
H: fh
|
|
6904
|
+
});
|
|
6905
|
+
const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
|
|
6906
|
+
if (!native) {
|
|
6907
|
+
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
6908
|
+
shmId: frameHandle.shmId,
|
|
6909
|
+
reason: "native-miss"
|
|
6910
|
+
} });
|
|
6911
|
+
return null;
|
|
6912
|
+
}
|
|
6913
|
+
return {
|
|
6914
|
+
thumbnail: native,
|
|
6915
|
+
thumbnailSmall: await deriveThumbnailSmall(native)
|
|
6916
|
+
};
|
|
6917
|
+
} catch (err) {
|
|
6918
|
+
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
6919
|
+
shmId: frameHandle.shmId,
|
|
6920
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6921
|
+
} });
|
|
6922
|
+
return null;
|
|
6923
|
+
}
|
|
6924
|
+
}
|
|
6925
|
+
/**
|
|
6804
6926
|
* The boxed timeline tile (`firstFrame`/`snapshot`/`lastFrame`): the track's
|
|
6805
6927
|
* OWN box burned onto the NATIVE full frame downscaled to
|
|
6806
6928
|
* {@link SNAPSHOT_MAX_WIDTH}. On a native miss it falls back to the ≤640
|
|
@@ -11997,10 +12119,11 @@ function pickEventOwnedMedia(files) {
|
|
|
11997
12119
|
return files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
|
|
11998
12120
|
}
|
|
11999
12121
|
/** Pick a track's fallback media in the shared cadence-preference order the
|
|
12000
|
-
* KeyEvent path uses:
|
|
12001
|
-
*
|
|
12122
|
+
* KeyEvent path uses: `thumbnailSmall` (480 fast-load) → best `thumbnail` →
|
|
12123
|
+
* rolling `lastFrame` → `firstFrame` → newest `snapshot` → any track blob.
|
|
12124
|
+
* Undefined when the track owns no media. */
|
|
12002
12125
|
function pickTrackFallbackMedia(files) {
|
|
12003
|
-
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];
|
|
12126
|
+
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];
|
|
12004
12127
|
}
|
|
12005
12128
|
/**
|
|
12006
12129
|
* Resolve the default-path media for an event id: the event's own media when it
|
|
@@ -12416,7 +12539,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12416
12539
|
let storage = this.ctx.kernel.storage;
|
|
12417
12540
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
12418
12541
|
if (mediaRoot) {
|
|
12419
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
12542
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-C4bKtLou.js"));
|
|
12420
12543
|
storage = new FilesystemStorageProvider(mediaRoot);
|
|
12421
12544
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
12422
12545
|
}
|
|
@@ -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
|
|
@@ -4492,6 +4492,7 @@ var SINGLE_INSTANCE_KINDS = new Set([
|
|
|
4492
4492
|
"keyFrame",
|
|
4493
4493
|
"keyFrameSmall",
|
|
4494
4494
|
"thumbnail",
|
|
4495
|
+
"thumbnailSmall",
|
|
4495
4496
|
"firstFrame",
|
|
4496
4497
|
"lastFrame"
|
|
4497
4498
|
]);
|
|
@@ -6472,6 +6473,76 @@ function squareSafeCropRegionNormalized(bbox, frame, padding) {
|
|
|
6472
6473
|
};
|
|
6473
6474
|
}
|
|
6474
6475
|
//#endregion
|
|
6476
|
+
//#region src/shared/frame/square-subject-crop.ts
|
|
6477
|
+
/** Expansion factor applied to the bbox long side to frame the subject with a
|
|
6478
|
+
* little breathing room (operator choice: ×1.2). */
|
|
6479
|
+
var SQUARE_SUBJECT_CROP_EXPANSION = 1.2;
|
|
6480
|
+
/**
|
|
6481
|
+
* Compute a SQUARE crop region in PIXEL space around the subject bbox.
|
|
6482
|
+
*
|
|
6483
|
+
* Algorithm:
|
|
6484
|
+
* 1. center: cx = x + w/2, cy = y + h/2
|
|
6485
|
+
* 2. side = max(w, h) × SQUARE_SUBJECT_CROP_EXPANSION
|
|
6486
|
+
* 3. clamp side to fit the frame: side = min(side, W, H) — a square can never
|
|
6487
|
+
* exceed the frame's SHORT edge (this is the "subject bigger than the
|
|
6488
|
+
* frame's short side" case)
|
|
6489
|
+
* 4. center-and-clamp the origin so the square stays fully inside the frame
|
|
6490
|
+
* 5. round to integer pixels
|
|
6491
|
+
*
|
|
6492
|
+
* The returned region fully contains the bbox whenever the bbox itself fits in a
|
|
6493
|
+
* square of the frame's short side (always true for real detections).
|
|
6494
|
+
*/
|
|
6495
|
+
function squareSubjectCropRegion(bbox, frame) {
|
|
6496
|
+
const { W, H } = frame;
|
|
6497
|
+
const cx = bbox.x + bbox.w / 2;
|
|
6498
|
+
const cy = bbox.y + bbox.h / 2;
|
|
6499
|
+
const longerSide = Math.max(bbox.w, bbox.h);
|
|
6500
|
+
const side = Math.min(longerSide * SQUARE_SUBJECT_CROP_EXPANSION, W, H);
|
|
6501
|
+
const rawX0 = cx - side / 2;
|
|
6502
|
+
const rawY0 = cy - side / 2;
|
|
6503
|
+
const x0 = Math.max(0, Math.min(rawX0, W - side));
|
|
6504
|
+
const y0 = Math.max(0, Math.min(rawY0, H - side));
|
|
6505
|
+
return {
|
|
6506
|
+
x: Math.round(x0),
|
|
6507
|
+
y: Math.round(y0),
|
|
6508
|
+
w: Math.round(side),
|
|
6509
|
+
h: Math.round(side)
|
|
6510
|
+
};
|
|
6511
|
+
}
|
|
6512
|
+
/**
|
|
6513
|
+
* The same SQUARE region as {@link squareSubjectCropRegion}, expressed in
|
|
6514
|
+
* NORMALIZED [0,1]×[0,1] coordinates. A normalized box maps DIRECTLY onto a
|
|
6515
|
+
* native-resolution surface of the SAME aspect ratio, so the region computed
|
|
6516
|
+
* from the detection-frame dimensions addresses the exact same ROI on the
|
|
6517
|
+
* runner's retained native frame. Reuses the pixel geometry verbatim (single
|
|
6518
|
+
* source of truth) and divides by the frame dimensions.
|
|
6519
|
+
*/
|
|
6520
|
+
function squareSubjectCropRegionNormalized(bbox, frame) {
|
|
6521
|
+
const region = squareSubjectCropRegion(bbox, frame);
|
|
6522
|
+
return {
|
|
6523
|
+
x: region.x / frame.W,
|
|
6524
|
+
y: region.y / frame.H,
|
|
6525
|
+
w: region.w / frame.W,
|
|
6526
|
+
h: region.h / frame.H
|
|
6527
|
+
};
|
|
6528
|
+
}
|
|
6529
|
+
/**
|
|
6530
|
+
* Derive the `thumbnailSmall` JPEG from an already-encoded native subject-crop
|
|
6531
|
+
* JPEG. Downscales to at most {@link THUMBNAIL_SMALL_MAX_WIDTH} on the long side
|
|
6532
|
+
* (aspect preserved). If the native crop's long side is already ≤ the cap, the
|
|
6533
|
+
* ORIGINAL buffer is returned unchanged (store as-is, never upscale, no wasted
|
|
6534
|
+
* re-encode).
|
|
6535
|
+
*/
|
|
6536
|
+
async function deriveThumbnailSmall(nativeJpeg) {
|
|
6537
|
+
const meta = await sharp(nativeJpeg).metadata();
|
|
6538
|
+
const longSide = Math.max(meta.width ?? 0, meta.height ?? 0);
|
|
6539
|
+
if (longSide > 0 && longSide <= 480) return nativeJpeg;
|
|
6540
|
+
return sharp(nativeJpeg).resize(480, 480, {
|
|
6541
|
+
fit: "inside",
|
|
6542
|
+
withoutEnlargement: true
|
|
6543
|
+
}).jpeg({ quality: 88 }).toBuffer();
|
|
6544
|
+
}
|
|
6545
|
+
//#endregion
|
|
6475
6546
|
//#region src/shared/frame/box-drawer.ts
|
|
6476
6547
|
var DEFAULT_COLOR = DEFAULT_EVENT_COLOR;
|
|
6477
6548
|
var DEFAULT_QUALITY = 80;
|
|
@@ -6697,7 +6768,7 @@ var EventMediaDispatcher = class {
|
|
|
6697
6768
|
const storedSnapshots = [];
|
|
6698
6769
|
const thumbnailTrackIds = [];
|
|
6699
6770
|
for (const sn of snapshots) {
|
|
6700
|
-
const res = await this.writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn
|
|
6771
|
+
const res = await this.writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn);
|
|
6701
6772
|
if (res.storedSnapshot) storedSnapshots.push(res.storedSnapshot);
|
|
6702
6773
|
if (res.thumbnailWritten) thumbnailTrackIds.push(sn.trackId);
|
|
6703
6774
|
}
|
|
@@ -6718,7 +6789,7 @@ var EventMediaDispatcher = class {
|
|
|
6718
6789
|
* failed). `thumbnailWritten` reports whether a best `thumbnail` actually
|
|
6719
6790
|
* landed this frame (#27-A) so the caller can stop forcing retries.
|
|
6720
6791
|
*/
|
|
6721
|
-
async writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn
|
|
6792
|
+
async writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn) {
|
|
6722
6793
|
if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return {
|
|
6723
6794
|
storedSnapshot: null,
|
|
6724
6795
|
thumbnailWritten: false
|
|
@@ -6745,8 +6816,11 @@ var EventMediaDispatcher = class {
|
|
|
6745
6816
|
if (sn.rollingLastFrame && boxed) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
|
|
6746
6817
|
let thumbnailWritten = false;
|
|
6747
6818
|
if (sn.bestThumbnail) {
|
|
6748
|
-
const
|
|
6749
|
-
if (
|
|
6819
|
+
const variants = await this.cropSubjectVariants(frameHandle, fw, fh, sn.bbox);
|
|
6820
|
+
if (variants) {
|
|
6821
|
+
thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, variants.thumbnail);
|
|
6822
|
+
await this.replaceKind(deviceId, sn.trackId, "thumbnailSmall", sn.timestamp, variants.thumbnailSmall);
|
|
6823
|
+
}
|
|
6750
6824
|
}
|
|
6751
6825
|
return {
|
|
6752
6826
|
storedSnapshot: stored,
|
|
@@ -6796,6 +6870,54 @@ var EventMediaDispatcher = class {
|
|
|
6796
6870
|
}
|
|
6797
6871
|
}
|
|
6798
6872
|
/**
|
|
6873
|
+
* The best-shot subject crop as its TWO persisted variants (best-crop
|
|
6874
|
+
* fast-load, 2026-07-21). ONE native fetch: the SQUARE-framed subject ROI
|
|
6875
|
+
* (side = max(w,h)×1.2, clamped to the frame — {@link squareSubjectCropRegionNormalized})
|
|
6876
|
+
* is requested from the runner's retained native surface with NO `maxWidth`
|
|
6877
|
+
* (uncapped TRUE native, decision #3) → the `thumbnail`. The `thumbnailSmall`
|
|
6878
|
+
* is DERIVED by downscaling that SAME JPEG to the 480 long-side cap — never a
|
|
6879
|
+
* second `getNativeCropJpeg`, never upscaled ({@link deriveThumbnailSmall}
|
|
6880
|
+
* returns the native buffer as-is when it is already ≤ 480).
|
|
6881
|
+
*
|
|
6882
|
+
* NATIVE-OR-NOTHING: any miss/error (or a runner without the method) returns
|
|
6883
|
+
* `null` after a loud `logger.warn`; the caller SKIPS the write and the
|
|
6884
|
+
* per-frame retry lands a real native crop later. Never a local resize
|
|
6885
|
+
* upscale of a ≤640 tile (a blurred lie).
|
|
6886
|
+
*/
|
|
6887
|
+
async cropSubjectVariants(frameHandle, fw, fh, bbox) {
|
|
6888
|
+
if (!this.deps.getNativeCropJpeg) {
|
|
6889
|
+
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
6890
|
+
shmId: frameHandle.shmId,
|
|
6891
|
+
reason: "no-native-cap"
|
|
6892
|
+
} });
|
|
6893
|
+
return null;
|
|
6894
|
+
}
|
|
6895
|
+
try {
|
|
6896
|
+
const norm = squareSubjectCropRegionNormalized(bbox, {
|
|
6897
|
+
W: fw,
|
|
6898
|
+
H: fh
|
|
6899
|
+
});
|
|
6900
|
+
const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
|
|
6901
|
+
if (!native) {
|
|
6902
|
+
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
6903
|
+
shmId: frameHandle.shmId,
|
|
6904
|
+
reason: "native-miss"
|
|
6905
|
+
} });
|
|
6906
|
+
return null;
|
|
6907
|
+
}
|
|
6908
|
+
return {
|
|
6909
|
+
thumbnail: native,
|
|
6910
|
+
thumbnailSmall: await deriveThumbnailSmall(native)
|
|
6911
|
+
};
|
|
6912
|
+
} catch (err) {
|
|
6913
|
+
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
6914
|
+
shmId: frameHandle.shmId,
|
|
6915
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6916
|
+
} });
|
|
6917
|
+
return null;
|
|
6918
|
+
}
|
|
6919
|
+
}
|
|
6920
|
+
/**
|
|
6799
6921
|
* The boxed timeline tile (`firstFrame`/`snapshot`/`lastFrame`): the track's
|
|
6800
6922
|
* OWN box burned onto the NATIVE full frame downscaled to
|
|
6801
6923
|
* {@link SNAPSHOT_MAX_WIDTH}. On a native miss it falls back to the ≤640
|
|
@@ -11992,10 +12114,11 @@ function pickEventOwnedMedia(files) {
|
|
|
11992
12114
|
return files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
|
|
11993
12115
|
}
|
|
11994
12116
|
/** Pick a track's fallback media in the shared cadence-preference order the
|
|
11995
|
-
* KeyEvent path uses:
|
|
11996
|
-
*
|
|
12117
|
+
* KeyEvent path uses: `thumbnailSmall` (480 fast-load) → best `thumbnail` →
|
|
12118
|
+
* rolling `lastFrame` → `firstFrame` → newest `snapshot` → any track blob.
|
|
12119
|
+
* Undefined when the track owns no media. */
|
|
11997
12120
|
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];
|
|
12121
|
+
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];
|
|
11999
12122
|
}
|
|
12000
12123
|
/**
|
|
12001
12124
|
* Resolve the default-path media for an event id: the event's own media when it
|
|
@@ -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-C4dWm6ZL.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.3",
|
|
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",
|