@camstack/addon-post-analysis 1.2.123 → 1.2.125
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.
|
@@ -25937,6 +25937,11 @@ var embeddingActions = require_dist.defineCustomActions({
|
|
|
25937
25937
|
"embedding.reembedFacesStatus": require_dist.customAction(NoInputSchema$1, ReembedFacesStatusSchema)
|
|
25938
25938
|
});
|
|
25939
25939
|
//#endregion
|
|
25940
|
+
//#region src/pipeline-analytics/include-crops-default.ts
|
|
25941
|
+
function resolveIncludeCrops(input) {
|
|
25942
|
+
return input?.includeCrops === true;
|
|
25943
|
+
}
|
|
25944
|
+
//#endregion
|
|
25940
25945
|
//#region src/pipeline-analytics/map-with-concurrency.ts
|
|
25941
25946
|
/**
|
|
25942
25947
|
* Apply `mapper` to every item with at most `limit` in flight at once.
|
|
@@ -26595,7 +26600,7 @@ var FaceGalleryProvider = class {
|
|
|
26595
26600
|
});
|
|
26596
26601
|
const identities = await this.identityStore.listIdentities();
|
|
26597
26602
|
const nameMap = new Map(identities.map((i) => [i.id, i.name]));
|
|
26598
|
-
const includeCrops = input
|
|
26603
|
+
const includeCrops = resolveIncludeCrops(input);
|
|
26599
26604
|
return mapWithConcurrency(faces, FACE_CROP_FETCH_CONCURRENCY, async (face) => {
|
|
26600
26605
|
const base64 = includeCrops ? await this.faceCropBase64(face) : void 0;
|
|
26601
26606
|
const cropUrl = this.cropUrlFor(face);
|
|
@@ -30867,7 +30872,7 @@ var PlateGalleryProvider = class {
|
|
|
30867
30872
|
return media.length > 0 ? media[0].base64 : void 0;
|
|
30868
30873
|
}
|
|
30869
30874
|
async toPlateInfo(plate, nameMap, opts) {
|
|
30870
|
-
const base64 = opts
|
|
30875
|
+
const base64 = resolveIncludeCrops(opts) ? await this.plateCropBase64(plate) : void 0;
|
|
30871
30876
|
const cropUrl = this.cropUrlFor(plate);
|
|
30872
30877
|
const vehicleName = plate.recognizedVehicleId != null ? nameMap?.get(plate.recognizedVehicleId) : void 0;
|
|
30873
30878
|
return {
|
|
@@ -30905,7 +30910,7 @@ var PlateGalleryProvider = class {
|
|
|
30905
30910
|
...input?.limit !== void 0 ? { limit: input.limit } : {}
|
|
30906
30911
|
});
|
|
30907
30912
|
const nameMap = await this.vehicleNameMap();
|
|
30908
|
-
const includeCrops = input
|
|
30913
|
+
const includeCrops = resolveIncludeCrops(input);
|
|
30909
30914
|
return Promise.all(rows.map((p) => this.toPlateInfo(p, nameMap, { includeCrops })));
|
|
30910
30915
|
}
|
|
30911
30916
|
async getPlateByTrack(input) {
|
|
@@ -49922,44 +49927,65 @@ var TrackStore = class {
|
|
|
49922
49927
|
async listDeviceIdsSince(since) {
|
|
49923
49928
|
return this.collectDeviceIds(Math.max(0, Math.floor(since)));
|
|
49924
49929
|
}
|
|
49930
|
+
/**
|
|
49931
|
+
* The distinct `deviceId`s with a track at or after `fromLastSeen`.
|
|
49932
|
+
*
|
|
49933
|
+
* ## Why this is a histogram and not a paged read
|
|
49934
|
+
*
|
|
49935
|
+
* It used to drain the table in 501-row pages on a `lastSeen` range with no
|
|
49936
|
+
* `deviceId`, ordered by `lastSeen`. The indexes on this table are
|
|
49937
|
+
* `(deviceId, lastSeen)` and `(deviceId, firstSeen)` — neither can serve a
|
|
49938
|
+
* `lastSeen` range from the left — so `EXPLAIN QUERY PLAN` on the live
|
|
49939
|
+
* database answered:
|
|
49940
|
+
*
|
|
49941
|
+
* ```
|
|
49942
|
+
* SCAN pipeline-analytics:tracks
|
|
49943
|
+
* USE TEMP B-TREE FOR ORDER BY
|
|
49944
|
+
* ```
|
|
49945
|
+
*
|
|
49946
|
+
* A full scan of a 155 MB table, plus a sorter holding every matching row —
|
|
49947
|
+
* and the store has no column projection, so each row carries its ~8.5 KB
|
|
49948
|
+
* `positions`/`snapshots` JSON through the sort. PER PAGE. Draining 17 940
|
|
49949
|
+
* rows took 36 such pages: measured on a copy of the live database, 36 full
|
|
49950
|
+
* scans, and on the hub itself single statements of 1 196–1 615 ms each,
|
|
49951
|
+
* every one of them blocking the one synchronous thread the whole cluster
|
|
49952
|
+
* reads configuration through. `listDeviceIds` has ~10 callers.
|
|
49953
|
+
*
|
|
49954
|
+
* The answer being asked for is at most a couple of dozen integers. A
|
|
49955
|
+
* histogram on `deviceId` with `bucketSize: 1` is `GROUP BY deviceId`, and
|
|
49956
|
+
* the planner serves it from an index that already exists:
|
|
49957
|
+
*
|
|
49958
|
+
* ```
|
|
49959
|
+
* SCAN pipeline-analytics:tracks USING COVERING INDEX idx_tracks_device_lastSeen
|
|
49960
|
+
* USE TEMP B-TREE FOR GROUP BY
|
|
49961
|
+
* ```
|
|
49962
|
+
*
|
|
49963
|
+
* 384 KB of index instead of 155 MB of table, ONE statement instead of 36:
|
|
49964
|
+
* **6.0 ms against 678 ms** on the same copy, warm cache both times. No new
|
|
49965
|
+
* index, so no write amplification and no extra bytes — the cheap question
|
|
49966
|
+
* came first, it just was not being asked (D56).
|
|
49967
|
+
*
|
|
49968
|
+
* `bucket` IS the deviceId: the column is INTEGER and the bucket expression
|
|
49969
|
+
* is `CAST((deviceId - 0) / 1 AS INTEGER)`. Best-effort, as before — a
|
|
49970
|
+
* failure yields [] and the caller treats it as "no devices", which is the
|
|
49971
|
+
* same answer it got from a failed page.
|
|
49972
|
+
*/
|
|
49925
49973
|
async collectDeviceIds(fromLastSeen) {
|
|
49926
|
-
const seenDevices = /* @__PURE__ */ new Set();
|
|
49927
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
49928
|
-
const PAGE = 500;
|
|
49929
|
-
let cursor = fromLastSeen;
|
|
49930
49974
|
try {
|
|
49931
|
-
|
|
49932
|
-
|
|
49933
|
-
|
|
49934
|
-
|
|
49935
|
-
|
|
49936
|
-
|
|
49937
|
-
|
|
49938
|
-
|
|
49939
|
-
|
|
49940
|
-
|
|
49941
|
-
}
|
|
49942
|
-
});
|
|
49943
|
-
if (rows.length === 0) break;
|
|
49944
|
-
let newInPage = 0;
|
|
49945
|
-
let maxLastSeen = cursor;
|
|
49946
|
-
for (const r of rows) {
|
|
49947
|
-
if (typeof r.id === "string" && !seenIds.has(r.id)) {
|
|
49948
|
-
seenIds.add(r.id);
|
|
49949
|
-
newInPage++;
|
|
49950
|
-
}
|
|
49951
|
-
const ls = Number(r.data["lastSeen"]);
|
|
49952
|
-
if (Number.isFinite(ls) && ls > maxLastSeen) maxLastSeen = ls;
|
|
49953
|
-
const deviceId = Number(r.data["deviceId"]);
|
|
49954
|
-
if (Number.isFinite(deviceId)) seenDevices.add(deviceId);
|
|
49955
|
-
}
|
|
49956
|
-
if (rows.length < PAGE || newInPage === 0) break;
|
|
49957
|
-
cursor = maxLastSeen;
|
|
49958
|
-
}
|
|
49975
|
+
const buckets = await this.store.histogram.query({
|
|
49976
|
+
collection: TRACKS_COLLECTION,
|
|
49977
|
+
field: "deviceId",
|
|
49978
|
+
bucketSize: 1,
|
|
49979
|
+
origin: 0,
|
|
49980
|
+
...fromLastSeen > 0 ? { filter: { whereBetween: { lastSeen: [fromLastSeen, Number.MAX_SAFE_INTEGER] } } } : {}
|
|
49981
|
+
});
|
|
49982
|
+
const ids = [];
|
|
49983
|
+
for (const b of buckets) if (b.count > 0 && Number.isFinite(b.bucket)) ids.push(b.bucket);
|
|
49984
|
+
return ids;
|
|
49959
49985
|
} catch (err) {
|
|
49960
49986
|
this.logger.warn("TrackStore.listDeviceIds failed", { meta: { error: String(err) } });
|
|
49987
|
+
return [];
|
|
49961
49988
|
}
|
|
49962
|
-
return [...seenDevices];
|
|
49963
49989
|
}
|
|
49964
49990
|
/** Historical query — hits the persisted collection. With `zone` set,
|
|
49965
49991
|
* candidates are SQL-prefiltered on the envelope columns (overlap test via
|
|
@@ -25915,6 +25915,11 @@ var embeddingActions = defineCustomActions({
|
|
|
25915
25915
|
"embedding.reembedFacesStatus": customAction(NoInputSchema$1, ReembedFacesStatusSchema)
|
|
25916
25916
|
});
|
|
25917
25917
|
//#endregion
|
|
25918
|
+
//#region src/pipeline-analytics/include-crops-default.ts
|
|
25919
|
+
function resolveIncludeCrops(input) {
|
|
25920
|
+
return input?.includeCrops === true;
|
|
25921
|
+
}
|
|
25922
|
+
//#endregion
|
|
25918
25923
|
//#region src/pipeline-analytics/map-with-concurrency.ts
|
|
25919
25924
|
/**
|
|
25920
25925
|
* Apply `mapper` to every item with at most `limit` in flight at once.
|
|
@@ -26573,7 +26578,7 @@ var FaceGalleryProvider = class {
|
|
|
26573
26578
|
});
|
|
26574
26579
|
const identities = await this.identityStore.listIdentities();
|
|
26575
26580
|
const nameMap = new Map(identities.map((i) => [i.id, i.name]));
|
|
26576
|
-
const includeCrops = input
|
|
26581
|
+
const includeCrops = resolveIncludeCrops(input);
|
|
26577
26582
|
return mapWithConcurrency(faces, FACE_CROP_FETCH_CONCURRENCY, async (face) => {
|
|
26578
26583
|
const base64 = includeCrops ? await this.faceCropBase64(face) : void 0;
|
|
26579
26584
|
const cropUrl = this.cropUrlFor(face);
|
|
@@ -30843,7 +30848,7 @@ var PlateGalleryProvider = class {
|
|
|
30843
30848
|
return media.length > 0 ? media[0].base64 : void 0;
|
|
30844
30849
|
}
|
|
30845
30850
|
async toPlateInfo(plate, nameMap, opts) {
|
|
30846
|
-
const base64 = opts
|
|
30851
|
+
const base64 = resolveIncludeCrops(opts) ? await this.plateCropBase64(plate) : void 0;
|
|
30847
30852
|
const cropUrl = this.cropUrlFor(plate);
|
|
30848
30853
|
const vehicleName = plate.recognizedVehicleId != null ? nameMap?.get(plate.recognizedVehicleId) : void 0;
|
|
30849
30854
|
return {
|
|
@@ -30881,7 +30886,7 @@ var PlateGalleryProvider = class {
|
|
|
30881
30886
|
...input?.limit !== void 0 ? { limit: input.limit } : {}
|
|
30882
30887
|
});
|
|
30883
30888
|
const nameMap = await this.vehicleNameMap();
|
|
30884
|
-
const includeCrops = input
|
|
30889
|
+
const includeCrops = resolveIncludeCrops(input);
|
|
30885
30890
|
return Promise.all(rows.map((p) => this.toPlateInfo(p, nameMap, { includeCrops })));
|
|
30886
30891
|
}
|
|
30887
30892
|
async getPlateByTrack(input) {
|
|
@@ -49859,44 +49864,65 @@ var TrackStore = class {
|
|
|
49859
49864
|
async listDeviceIdsSince(since) {
|
|
49860
49865
|
return this.collectDeviceIds(Math.max(0, Math.floor(since)));
|
|
49861
49866
|
}
|
|
49867
|
+
/**
|
|
49868
|
+
* The distinct `deviceId`s with a track at or after `fromLastSeen`.
|
|
49869
|
+
*
|
|
49870
|
+
* ## Why this is a histogram and not a paged read
|
|
49871
|
+
*
|
|
49872
|
+
* It used to drain the table in 501-row pages on a `lastSeen` range with no
|
|
49873
|
+
* `deviceId`, ordered by `lastSeen`. The indexes on this table are
|
|
49874
|
+
* `(deviceId, lastSeen)` and `(deviceId, firstSeen)` — neither can serve a
|
|
49875
|
+
* `lastSeen` range from the left — so `EXPLAIN QUERY PLAN` on the live
|
|
49876
|
+
* database answered:
|
|
49877
|
+
*
|
|
49878
|
+
* ```
|
|
49879
|
+
* SCAN pipeline-analytics:tracks
|
|
49880
|
+
* USE TEMP B-TREE FOR ORDER BY
|
|
49881
|
+
* ```
|
|
49882
|
+
*
|
|
49883
|
+
* A full scan of a 155 MB table, plus a sorter holding every matching row —
|
|
49884
|
+
* and the store has no column projection, so each row carries its ~8.5 KB
|
|
49885
|
+
* `positions`/`snapshots` JSON through the sort. PER PAGE. Draining 17 940
|
|
49886
|
+
* rows took 36 such pages: measured on a copy of the live database, 36 full
|
|
49887
|
+
* scans, and on the hub itself single statements of 1 196–1 615 ms each,
|
|
49888
|
+
* every one of them blocking the one synchronous thread the whole cluster
|
|
49889
|
+
* reads configuration through. `listDeviceIds` has ~10 callers.
|
|
49890
|
+
*
|
|
49891
|
+
* The answer being asked for is at most a couple of dozen integers. A
|
|
49892
|
+
* histogram on `deviceId` with `bucketSize: 1` is `GROUP BY deviceId`, and
|
|
49893
|
+
* the planner serves it from an index that already exists:
|
|
49894
|
+
*
|
|
49895
|
+
* ```
|
|
49896
|
+
* SCAN pipeline-analytics:tracks USING COVERING INDEX idx_tracks_device_lastSeen
|
|
49897
|
+
* USE TEMP B-TREE FOR GROUP BY
|
|
49898
|
+
* ```
|
|
49899
|
+
*
|
|
49900
|
+
* 384 KB of index instead of 155 MB of table, ONE statement instead of 36:
|
|
49901
|
+
* **6.0 ms against 678 ms** on the same copy, warm cache both times. No new
|
|
49902
|
+
* index, so no write amplification and no extra bytes — the cheap question
|
|
49903
|
+
* came first, it just was not being asked (D56).
|
|
49904
|
+
*
|
|
49905
|
+
* `bucket` IS the deviceId: the column is INTEGER and the bucket expression
|
|
49906
|
+
* is `CAST((deviceId - 0) / 1 AS INTEGER)`. Best-effort, as before — a
|
|
49907
|
+
* failure yields [] and the caller treats it as "no devices", which is the
|
|
49908
|
+
* same answer it got from a failed page.
|
|
49909
|
+
*/
|
|
49862
49910
|
async collectDeviceIds(fromLastSeen) {
|
|
49863
|
-
const seenDevices = /* @__PURE__ */ new Set();
|
|
49864
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
49865
|
-
const PAGE = 500;
|
|
49866
|
-
let cursor = fromLastSeen;
|
|
49867
49911
|
try {
|
|
49868
|
-
|
|
49869
|
-
|
|
49870
|
-
|
|
49871
|
-
|
|
49872
|
-
|
|
49873
|
-
|
|
49874
|
-
|
|
49875
|
-
|
|
49876
|
-
|
|
49877
|
-
|
|
49878
|
-
}
|
|
49879
|
-
});
|
|
49880
|
-
if (rows.length === 0) break;
|
|
49881
|
-
let newInPage = 0;
|
|
49882
|
-
let maxLastSeen = cursor;
|
|
49883
|
-
for (const r of rows) {
|
|
49884
|
-
if (typeof r.id === "string" && !seenIds.has(r.id)) {
|
|
49885
|
-
seenIds.add(r.id);
|
|
49886
|
-
newInPage++;
|
|
49887
|
-
}
|
|
49888
|
-
const ls = Number(r.data["lastSeen"]);
|
|
49889
|
-
if (Number.isFinite(ls) && ls > maxLastSeen) maxLastSeen = ls;
|
|
49890
|
-
const deviceId = Number(r.data["deviceId"]);
|
|
49891
|
-
if (Number.isFinite(deviceId)) seenDevices.add(deviceId);
|
|
49892
|
-
}
|
|
49893
|
-
if (rows.length < PAGE || newInPage === 0) break;
|
|
49894
|
-
cursor = maxLastSeen;
|
|
49895
|
-
}
|
|
49912
|
+
const buckets = await this.store.histogram.query({
|
|
49913
|
+
collection: TRACKS_COLLECTION,
|
|
49914
|
+
field: "deviceId",
|
|
49915
|
+
bucketSize: 1,
|
|
49916
|
+
origin: 0,
|
|
49917
|
+
...fromLastSeen > 0 ? { filter: { whereBetween: { lastSeen: [fromLastSeen, Number.MAX_SAFE_INTEGER] } } } : {}
|
|
49918
|
+
});
|
|
49919
|
+
const ids = [];
|
|
49920
|
+
for (const b of buckets) if (b.count > 0 && Number.isFinite(b.bucket)) ids.push(b.bucket);
|
|
49921
|
+
return ids;
|
|
49896
49922
|
} catch (err) {
|
|
49897
49923
|
this.logger.warn("TrackStore.listDeviceIds failed", { meta: { error: String(err) } });
|
|
49924
|
+
return [];
|
|
49898
49925
|
}
|
|
49899
|
-
return [...seenDevices];
|
|
49900
49926
|
}
|
|
49901
49927
|
/** Historical query — hits the persisted collection. With `zone` set,
|
|
49902
49928
|
* candidates are SQL-prefiltered on the envelope columns (overlap test via
|
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.125",
|
|
4
4
|
"description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|