@camstack/addon-post-analysis 1.2.217 → 1.2.219
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-B_6UZCut.js → dist-Cnlk6Tyz.js} +101 -6
- package/dist/{dist-DWZqNymj.mjs → dist-D0l3xZqa.mjs} +101 -6
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BmDXGilB.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C8d8YCvw.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-DMDZJw2T.mjs → hostInit-BsYVmiZ9.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +517 -115
- package/dist/pipeline-analytics/index.mjs +517 -115
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -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-Cnlk6Tyz.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -8341,8 +8341,55 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
8341
8341
|
default: return null;
|
|
8342
8342
|
}
|
|
8343
8343
|
}
|
|
8344
|
+
/** The shells whose update has its own sentence. Anything else gets the plain
|
|
8345
|
+
* one: a body that names a shell nobody observed is worse than a vague body. */
|
|
8346
|
+
var WRAPPER_BODY_KINDS = new Set([
|
|
8347
|
+
"docker",
|
|
8348
|
+
"electron",
|
|
8349
|
+
"native"
|
|
8350
|
+
]);
|
|
8351
|
+
/**
|
|
8352
|
+
* The SHELL announcement (D437) — the container / app bundle a node runs
|
|
8353
|
+
* inside is behind the release contract.
|
|
8354
|
+
*
|
|
8355
|
+
* It is deliberately NOT folded into `server-update-available`: the versions
|
|
8356
|
+
* here are the shell's (a seed closure, an app bundle), not the running code's,
|
|
8357
|
+
* and on 2026-08-02 the running code was perfectly current on a hub whose image
|
|
8358
|
+
* was 13 days old from a repository nothing rebuilt. It also mints no button —
|
|
8359
|
+
* no process can replace the shell it is running inside — so the BODY is the
|
|
8360
|
+
* whole delivery, and it is chosen by `wrapperKind` so a docker operator is
|
|
8361
|
+
* handed the exact command instead of a button that cannot exist.
|
|
8362
|
+
*/
|
|
8363
|
+
function incomingFromWrapperUpdate(facts, data) {
|
|
8364
|
+
const packages = updatePackages(data);
|
|
8365
|
+
if (packages.length === 0) return null;
|
|
8366
|
+
const head = packages[0];
|
|
8367
|
+
const nodeId = head.nodeId ?? stringField(data, "nodeId");
|
|
8368
|
+
const wrapperKind = stringField(data, "wrapperKind");
|
|
8369
|
+
const bodySuffix = wrapperKind !== void 0 && WRAPPER_BODY_KINDS.has(wrapperKind) ? wrapperKind : null;
|
|
8370
|
+
return systemSubject({
|
|
8371
|
+
facts,
|
|
8372
|
+
kind: "wrapper-update-available",
|
|
8373
|
+
subject: `wrapper:${nodeId ?? "hub"}`,
|
|
8374
|
+
titleKey: "system.wrapper-update-available.title",
|
|
8375
|
+
bodyKey: bodySuffix === null ? "system.wrapper-update-available.body" : `system.wrapper-update-available.body.${bodySuffix}`,
|
|
8376
|
+
textParams: {
|
|
8377
|
+
currentVersion: head.currentVersion,
|
|
8378
|
+
latestVersion: head.latestVersion,
|
|
8379
|
+
...nodeId !== void 0 ? { nodeId } : {}
|
|
8380
|
+
},
|
|
8381
|
+
textCount: 1,
|
|
8382
|
+
packageName: head.packageName,
|
|
8383
|
+
packageNames: [head.packageName],
|
|
8384
|
+
...nodeId !== void 0 ? {
|
|
8385
|
+
nodeId,
|
|
8386
|
+
nodeIds: [nodeId]
|
|
8387
|
+
} : {}
|
|
8388
|
+
});
|
|
8389
|
+
}
|
|
8344
8390
|
function incomingFromUpdateAvailable(facts, data) {
|
|
8345
8391
|
const target = stringField(data, "target");
|
|
8392
|
+
if (target === "wrapper") return incomingFromWrapperUpdate(facts, data);
|
|
8346
8393
|
if (target !== "addon" && target !== "server") return null;
|
|
8347
8394
|
const packages = updatePackages(data);
|
|
8348
8395
|
if (packages.length === 0) return null;
|
|
@@ -9452,7 +9499,7 @@ var CAMERA_DEVICE_TYPE = "camera";
|
|
|
9452
9499
|
/** Kinds whose action is "go and apply an ADDON update". */
|
|
9453
9500
|
var ADDON_UPDATE_KINDS = new Set(["addon-update-available"]);
|
|
9454
9501
|
/** Kinds whose action is "go and apply a NODE's own (`@camstack/server`) update". */
|
|
9455
|
-
var NODE_UPDATE_KINDS = new Set(["server-update-available"]);
|
|
9502
|
+
var NODE_UPDATE_KINDS = new Set(["server-update-available", "wrapper-update-available"]);
|
|
9456
9503
|
/**
|
|
9457
9504
|
* Kinds whose action is "go and look at this device's stream".
|
|
9458
9505
|
*
|
|
@@ -10017,6 +10064,11 @@ var en_default = {
|
|
|
10017
10064
|
"system.server-update-available.body": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}})",
|
|
10018
10065
|
"system.server-update-available.body.onNode": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}}) on node {{nodeId}}",
|
|
10019
10066
|
"system.server-update-available.body.onNodes": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}}) on {{nodes}}",
|
|
10067
|
+
"system.wrapper-update-available.title": "The container or app on {{nodeId}} is out of date",
|
|
10068
|
+
"system.wrapper-update-available.body": "The shell this node runs inside carries {{currentVersion}}; the release contract delivers {{latestVersion}}. Updating the server keeps the code current but never replaces the shell.",
|
|
10069
|
+
"system.wrapper-update-available.body.docker": "This node's image carries seed {{currentVersion}}; the release contract delivers {{latestVersion}}. A container cannot replace its own image — recreate it from the contract image: docker compose pull && docker compose up -d",
|
|
10070
|
+
"system.wrapper-update-available.body.electron": "The desktop app on {{nodeId}} carries {{currentVersion}}; the release contract delivers {{latestVersion}}. The app downloads the new build on its own and installs it the next time it quits.",
|
|
10071
|
+
"system.wrapper-update-available.body.native": "The installed app on {{nodeId}} carries {{currentVersion}}; the release contract delivers {{latestVersion}}. Reinstall it from the current release to converge.",
|
|
10020
10072
|
"system.addon-updated.title": {
|
|
10021
10073
|
"one": "{{count}} addon updated",
|
|
10022
10074
|
"other": "{{count}} addons updated"
|
|
@@ -10272,6 +10324,11 @@ var it_default = {
|
|
|
10272
10324
|
"system.server-update-available.body": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}})",
|
|
10273
10325
|
"system.server-update-available.body.onNode": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}}) sul nodo {{nodeId}}",
|
|
10274
10326
|
"system.server-update-available.body.onNodes": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}}) su {{nodes}}",
|
|
10327
|
+
"system.wrapper-update-available.title": "Il contenitore o l'app di {{nodeId}} è indietro",
|
|
10328
|
+
"system.wrapper-update-available.body": "Il guscio in cui gira questo nodo porta {{currentVersion}}; il contratto di rilascio consegna {{latestVersion}}. Aggiornare il server tiene il codice aggiornato ma non sostituisce mai il guscio.",
|
|
10329
|
+
"system.wrapper-update-available.body.docker": "L'immagine di questo nodo porta il seed {{currentVersion}}; il contratto di rilascio consegna {{latestVersion}}. Un container non può sostituire la propria immagine — ricrealo dall'immagine del contratto: docker compose pull && docker compose up -d",
|
|
10330
|
+
"system.wrapper-update-available.body.electron": "L'app desktop su {{nodeId}} porta {{currentVersion}}; il contratto di rilascio consegna {{latestVersion}}. L'app scarica da sola la nuova build e la installa alla prossima chiusura.",
|
|
10331
|
+
"system.wrapper-update-available.body.native": "L'app installata su {{nodeId}} porta {{currentVersion}}; il contratto di rilascio consegna {{latestVersion}}. Reinstallala dal rilascio corrente per allinearla.",
|
|
10275
10332
|
"system.addon-updated.title": {
|
|
10276
10333
|
"one": "{{count}} addon aggiornato",
|
|
10277
10334
|
"other": "{{count}} addon aggiornati"
|
|
@@ -20092,11 +20149,19 @@ var TimelapseRenderer = class {
|
|
|
20092
20149
|
async render(request) {
|
|
20093
20150
|
const { deviceId, window } = request;
|
|
20094
20151
|
const tags = { deviceId };
|
|
20095
|
-
const
|
|
20152
|
+
const first = await this.ports.getAvailability({
|
|
20096
20153
|
deviceId,
|
|
20097
20154
|
fromMs: window.startMs,
|
|
20098
20155
|
toMs: window.endMs
|
|
20099
20156
|
});
|
|
20157
|
+
const cheapest = [...first.profilesWithFootage].sort((a, b) => profileCost(a) - profileCost(b))[0];
|
|
20158
|
+
const describes = first.ranges[0]?.profile;
|
|
20159
|
+
const availability = cheapest !== void 0 && cheapest !== describes ? (await this.ports.getAvailability({
|
|
20160
|
+
deviceId,
|
|
20161
|
+
fromMs: window.startMs,
|
|
20162
|
+
toMs: window.endMs,
|
|
20163
|
+
profile: cheapest
|
|
20164
|
+
})).ranges : first.ranges;
|
|
20100
20165
|
const chosen = pickProfile(availability, window);
|
|
20101
20166
|
if (chosen === null || chosen.coverage.coveredMs === 0) {
|
|
20102
20167
|
this.ports.logger.warn("timelapse skipped: no footage covers the window", {
|
|
@@ -25816,6 +25881,7 @@ function systemEventSubject(kind, scope) {
|
|
|
25816
25881
|
if (kind === "detection-blind") return `detection-blind:${scope.deviceId ?? 0}`;
|
|
25817
25882
|
if (kind === "addon-crash-loop") return `addon-crash:${scope.nodeId ?? "hub"}:${TEST_CRASHED_RUNNER}`;
|
|
25818
25883
|
if (kind === "addon-update-available" || kind === "server-update-available") return `package:${scope.packageName ?? "@camstack/server"}`;
|
|
25884
|
+
if (kind === "wrapper-update-available") return `wrapper:${scope.nodeId ?? "hub"}`;
|
|
25819
25885
|
if (kind === "addon-updated") return `packages:addon-updated:${scope.packageName ?? "pkg"}`;
|
|
25820
25886
|
if (kind === "server-updated") return `server-updated:${scope.nodeId ?? "hub"}`;
|
|
25821
25887
|
if (kind === "export-completed") return `export:${scope.deviceId ?? 0}`;
|
|
@@ -33418,7 +33484,12 @@ var FaceGalleryProvider = class {
|
|
|
33418
33484
|
identityId: input.identityId
|
|
33419
33485
|
});
|
|
33420
33486
|
assigned += 1;
|
|
33421
|
-
} catch {
|
|
33487
|
+
} catch (err) {
|
|
33488
|
+
this.logger.error("assignFaces refused a face", { meta: {
|
|
33489
|
+
faceId,
|
|
33490
|
+
identityId: input.identityId,
|
|
33491
|
+
error: err instanceof Error ? `${err.name}: ${err.message}` : String(err)
|
|
33492
|
+
} });
|
|
33422
33493
|
failed.push(faceId);
|
|
33423
33494
|
}
|
|
33424
33495
|
return {
|
|
@@ -34802,6 +34873,21 @@ var SpawnBesideCoastingSampler = class {
|
|
|
34802
34873
|
this.counts.clear();
|
|
34803
34874
|
}
|
|
34804
34875
|
};
|
|
34876
|
+
/** Per-cause counter + sampling decision. One instance per tracker, so the
|
|
34877
|
+
* counts are per camera and per detection source. */
|
|
34878
|
+
var TrackLossSampler = class {
|
|
34879
|
+
counts = /* @__PURE__ */ new Map();
|
|
34880
|
+
/** The running count when this loss should be emitted, else `undefined`. */
|
|
34881
|
+
next(cause) {
|
|
34882
|
+
const count = (this.counts.get(cause) ?? 0) + 1;
|
|
34883
|
+
this.counts.set(cause, count);
|
|
34884
|
+
if (count <= 10) return count;
|
|
34885
|
+
return count % 50 === 0 ? count : void 0;
|
|
34886
|
+
}
|
|
34887
|
+
reset() {
|
|
34888
|
+
this.counts.clear();
|
|
34889
|
+
}
|
|
34890
|
+
};
|
|
34805
34891
|
/** Per-reason counter + sampling decision. One instance per tracker, so the
|
|
34806
34892
|
* counts are per camera and per detection source. */
|
|
34807
34893
|
var SpawnRefusalSampler = class {
|
|
@@ -34910,6 +34996,16 @@ var SortTracker = class SortTracker {
|
|
|
34910
34996
|
refusalSink = null;
|
|
34911
34997
|
/** Per-reason counters behind the sampled spawn-beside-coasting diagnostics. */
|
|
34912
34998
|
spawnBesideSampler = new SpawnBesideCoastingSampler();
|
|
34999
|
+
/**
|
|
35000
|
+
* Deliveries to {@link update} since this tracker was constructed. One per
|
|
35001
|
+
* frame the pipeline actually hands over — which is exactly what "the camera
|
|
35002
|
+
* was looking" means, and is NOT derivable from wall clock: a quiet camera
|
|
35003
|
+
* and a busy one span the same seconds (D438).
|
|
35004
|
+
*/
|
|
35005
|
+
frameSeq = 0;
|
|
35006
|
+
/** Sink for track deaths — see {@link setTrackLossObserver}. */
|
|
35007
|
+
trackLossObserver = null;
|
|
35008
|
+
trackLossSampler = new TrackLossSampler();
|
|
34913
35009
|
/** Sink for fragmentation diagnostics — see {@link setSpawnBesideCoastingObserver}. */
|
|
34914
35010
|
spawnBesideObserver = null;
|
|
34915
35011
|
constructor(config = {}) {
|
|
@@ -34942,6 +35038,13 @@ var SortTracker = class SortTracker {
|
|
|
34942
35038
|
* Same contract as the other two sinks — the tracker has no `deviceId`, the
|
|
34943
35039
|
* caller stamps it, and nothing is computed when no observer is wired.
|
|
34944
35040
|
*/
|
|
35041
|
+
/**
|
|
35042
|
+
* Wire the sink for {@link TrackLossRecord}. Nothing is computed when no
|
|
35043
|
+
* observer is attached.
|
|
35044
|
+
*/
|
|
35045
|
+
setTrackLossObserver(observer) {
|
|
35046
|
+
this.trackLossObserver = observer;
|
|
35047
|
+
}
|
|
34945
35048
|
setSpawnBesideCoastingObserver(observer) {
|
|
34946
35049
|
this.spawnBesideObserver = observer;
|
|
34947
35050
|
}
|
|
@@ -34956,13 +35059,13 @@ var SortTracker = class SortTracker {
|
|
|
34956
35059
|
reportSpawnBesideCoasting(spawnedId, det, coasting, detections, frameArea, timestamp) {
|
|
34957
35060
|
const observer = this.spawnBesideObserver;
|
|
34958
35061
|
if (!observer) return;
|
|
34959
|
-
const
|
|
35062
|
+
const family = SortTracker.spawnBesideFamily(det.class);
|
|
34960
35063
|
const dc = bboxCentroid(det.bbox);
|
|
34961
35064
|
let best;
|
|
34962
35065
|
let bestDist = Number.POSITIVE_INFINITY;
|
|
34963
35066
|
let bestReach = 0;
|
|
34964
35067
|
for (const track of coasting) {
|
|
34965
|
-
if (
|
|
35068
|
+
if (SortTracker.spawnBesideFamily(track.class) !== family) continue;
|
|
34966
35069
|
const tc = bboxCentroid(track.bbox);
|
|
34967
35070
|
const dist = Math.hypot(tc.x - dc.x, tc.y - dc.y);
|
|
34968
35071
|
const reach = this.reacquireReach(track, timestamp, true);
|
|
@@ -34977,6 +35080,7 @@ var SortTracker = class SortTracker {
|
|
|
34977
35080
|
const declinedBy = this.explainDecline(best, det, detections, frameArea, timestamp, bestDist);
|
|
34978
35081
|
const spawnCount = this.spawnBesideSampler.next(declinedBy);
|
|
34979
35082
|
if (spawnCount === void 0) return;
|
|
35083
|
+
const crowd = declinedBy === "crowd-guard" ? this.crowdAround(best, det, detections) : void 0;
|
|
34980
35084
|
observer({
|
|
34981
35085
|
spawnedTrackId: spawnedId,
|
|
34982
35086
|
spawnedClass: det.class,
|
|
@@ -34993,6 +35097,8 @@ var SortTracker = class SortTracker {
|
|
|
34993
35097
|
gapMs: timestamp - best.lastSeen,
|
|
34994
35098
|
trackAgeMs: timestamp - best.firstSeen,
|
|
34995
35099
|
declinedBy,
|
|
35100
|
+
...crowd === void 0 ? {} : { competitorCount: crowd.count },
|
|
35101
|
+
...crowd?.nearestPx === void 0 ? {} : { nearestCompetitorPx: crowd.nearestPx },
|
|
34996
35102
|
spawnCount,
|
|
34997
35103
|
sampledEvery: 10
|
|
34998
35104
|
});
|
|
@@ -35007,8 +35113,7 @@ var SortTracker = class SortTracker {
|
|
|
35007
35113
|
* `already-claimed` means.
|
|
35008
35114
|
*/
|
|
35009
35115
|
explainDecline(track, det, detections, frameArea, timestamp, centroidDist) {
|
|
35010
|
-
if (
|
|
35011
|
-
if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return "assoc-group";
|
|
35116
|
+
if (track.class !== det.class) return det.nativeRecovery !== void 0 ? "native-measured-class" : "cross-class";
|
|
35012
35117
|
if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "already-claimed";
|
|
35013
35118
|
if (centroidDist <= this.reacquireReach(track, timestamp, true)) {
|
|
35014
35119
|
if (!this.isPlausibleBirth(det, frameArea)) return "already-claimed";
|
|
@@ -35067,14 +35172,44 @@ var SortTracker = class SortTracker {
|
|
|
35067
35172
|
* come back — a graveyarded track may still resurrect, and a line about work
|
|
35068
35173
|
* that was not lost is worse than no line.
|
|
35069
35174
|
*/
|
|
35175
|
+
/**
|
|
35176
|
+
* Report one track that ended with nothing taking over (D438).
|
|
35177
|
+
*
|
|
35178
|
+
* Deliberately separate from {@link reportTrackLoss}, which speaks only for
|
|
35179
|
+
* spawns that never reached `minHits`. The population under investigation —
|
|
35180
|
+
* animal tracks dying mid-frame after six frames — had all EMITTED, so that
|
|
35181
|
+
* path never saw a single one of them.
|
|
35182
|
+
*
|
|
35183
|
+
* `timestamp` is the frame doing the sweeping, not the track's own clock: the
|
|
35184
|
+
* question is how much passed while nobody was looking.
|
|
35185
|
+
*/
|
|
35186
|
+
reportTrackDeath(track, cause, timestamp) {
|
|
35187
|
+
const observer = this.trackLossObserver;
|
|
35188
|
+
if (!observer) return;
|
|
35189
|
+
const lossCount = this.trackLossSampler.next(cause);
|
|
35190
|
+
if (lossCount === void 0) return;
|
|
35191
|
+
observer({
|
|
35192
|
+
trackId: track.id,
|
|
35193
|
+
trackClass: track.class,
|
|
35194
|
+
cause,
|
|
35195
|
+
framesSinceLastSeen: this.frameSeq - track.lastSeenFrameSeq,
|
|
35196
|
+
msSinceLastSeen: timestamp - track.lastSeen,
|
|
35197
|
+
lifeMs: track.lastSeen - track.firstSeen,
|
|
35198
|
+
positions: track.hits,
|
|
35199
|
+
emitted: track.emitted,
|
|
35200
|
+
lossCount,
|
|
35201
|
+
sampledEvery: 50
|
|
35202
|
+
});
|
|
35203
|
+
}
|
|
35070
35204
|
reportTrackLoss(track) {
|
|
35071
35205
|
if (track.emitted || track.lossReported) return;
|
|
35072
35206
|
track.lossReported = true;
|
|
35073
35207
|
this.reportRefusal("unconfirmed-spawn-lost", {
|
|
35074
|
-
class: track.
|
|
35208
|
+
class: track.frameClass,
|
|
35075
35209
|
originalClass: track.originalClass,
|
|
35076
35210
|
score: track.score,
|
|
35077
|
-
bbox: track.bbox
|
|
35211
|
+
bbox: track.bbox,
|
|
35212
|
+
...track.lastNativeRecovery !== void 0 ? { nativeRecovery: track.lastNativeRecovery } : {}
|
|
35078
35213
|
}, {
|
|
35079
35214
|
threshold: this.config.minHits,
|
|
35080
35215
|
measured: track.hits,
|
|
@@ -35129,43 +35264,73 @@ var SortTracker = class SortTracker {
|
|
|
35129
35264
|
static pairKey(a, b) {
|
|
35130
35265
|
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
35131
35266
|
}
|
|
35132
|
-
/**
|
|
35133
|
-
|
|
35134
|
-
|
|
35135
|
-
|
|
35136
|
-
|
|
35137
|
-
|
|
35267
|
+
/** The two macro-classes a coarse pass confuses on a non-upright person. */
|
|
35268
|
+
static isPersonAnimalPair(a, b) {
|
|
35269
|
+
return a === PERSON_CLASS && b === ANIMAL_CLASS || a === ANIMAL_CLASS && b === PERSON_CLASS;
|
|
35270
|
+
}
|
|
35271
|
+
/**
|
|
35272
|
+
* The family a spawn-beside-coasting line is reported AGAINST — {person,
|
|
35273
|
+
* animal} as one, every other class its own. Reporting only, never a binding
|
|
35274
|
+
* rule: a cross-class pair is exactly what that line exists to name (the
|
|
35275
|
+
* D378 veto, the D439 salvage rule), so a pair the tracker refused must still
|
|
35276
|
+
* be reportable. Independent of `classGroupAssoc` for the same reason.
|
|
35277
|
+
*/
|
|
35278
|
+
static spawnBesideFamily(cls) {
|
|
35279
|
+
if (cls === PERSON_CLASS || cls === ANIMAL_CLASS) return `${PERSON_CLASS}|${ANIMAL_CLASS}`;
|
|
35138
35280
|
return cls;
|
|
35139
35281
|
}
|
|
35140
|
-
/**
|
|
35141
|
-
*
|
|
35142
|
-
*
|
|
35143
|
-
*
|
|
35282
|
+
/**
|
|
35283
|
+
* A CROSS-CLASS BIND IS A FLIP CLAIM, AND A FLIP IS WEAK (D439).
|
|
35284
|
+
*
|
|
35285
|
+
* `classGroupAssoc` exists for one failure of the COARSE full-frame pass: a
|
|
35286
|
+
* bending person re-labelled `animal` for a frame, over the person's own
|
|
35287
|
+
* box, at the 0.4-0.6 the coarse pass gives a shape it half recognises. That
|
|
35288
|
+
* claim has three parts, and this predicate is all three:
|
|
35289
|
+
*
|
|
35290
|
+
* - the box was measured by the coarse pass — never `nativeRecovery`. The
|
|
35291
|
+
* runner's second pass measures the OPPOSITE thing: a 640-800 px native
|
|
35292
|
+
* square around a motion region the full-frame boxes do NOT account for,
|
|
35293
|
+
* which is by construction not the subject the full pass already has.
|
|
35294
|
+
* 2026-09-07 07:51:33, an `animal` at `nativeRecoveryScore=0.833`
|
|
35295
|
+
* absorbed into a person: the D378 half of this rule;
|
|
35296
|
+
* - it is a {person, animal} pair;
|
|
35297
|
+
* - it is WEAK — below {@link TrackerConfig.animalOverPersonMaxScore}, the
|
|
35298
|
+
* same bar the spawn gate uses for the same claim. A `person` at 0.86 or
|
|
35299
|
+
* an `animal` at 0.92 is not a half-recognised shape, it is a subject,
|
|
35300
|
+
* and binding it to the other class's track hands one subject's id to
|
|
35301
|
+
* another (camera 615, 17:11:01: person 0.8616 → animal track
|
|
35302
|
+
* `a28c1557`, which was then born as a person).
|
|
35303
|
+
*
|
|
35304
|
+
* Where the caller may use it is the fourth part, and it is the caller's:
|
|
35305
|
+
* only the predicted-box passes ({@link greedyAssociate}), where "same place"
|
|
35306
|
+
* is measured against a prediction on the strict `iouThreshold`. The salvage
|
|
35307
|
+
* passes ({@link looseMatch}) never cross classes — their whole premise is
|
|
35308
|
+
* weak overlap, and a weak-overlap cross-class bind is a dog beside its
|
|
35309
|
+
* owner, every time.
|
|
35310
|
+
*/
|
|
35311
|
+
crossClassFlip(trackClass, det) {
|
|
35312
|
+
if (!this.config.classGroupAssoc) return false;
|
|
35313
|
+
if (det.nativeRecovery !== void 0) return false;
|
|
35314
|
+
if (!SortTracker.isPersonAnimalPair(trackClass, det.class)) return false;
|
|
35315
|
+
return det.score < this.config.animalOverPersonMaxScore;
|
|
35316
|
+
}
|
|
35317
|
+
/** Whether a detection may associate to a track on a PREDICTED-BOX pass:
|
|
35318
|
+
* its own class, or — with `classGroupAssoc` — a weak coarse flip of the
|
|
35319
|
+
* {person, animal} pair ({@link crossClassFlip}). `classGating` off means no
|
|
35320
|
+
* gate at all, matching the legacy contract, except that a native-measured
|
|
35321
|
+
* box still binds only its own class (D378). */
|
|
35144
35322
|
associable(trackClass, det) {
|
|
35145
|
-
if (
|
|
35323
|
+
if (trackClass === det.class) return true;
|
|
35324
|
+
if (det.nativeRecovery !== void 0) return false;
|
|
35146
35325
|
if (!this.config.classGating) return true;
|
|
35147
|
-
return this.
|
|
35326
|
+
return this.crossClassFlip(trackClass, det);
|
|
35148
35327
|
}
|
|
35149
35328
|
/**
|
|
35150
|
-
* A NATIVE-MEASURED BOX IS ITS OWN SUBJECT ACROSS CLASSES (D378)
|
|
35151
|
-
*
|
|
35152
|
-
*
|
|
35153
|
-
*
|
|
35154
|
-
*
|
|
35155
|
-
* the person track is strictly better than minting a concurrent id.
|
|
35156
|
-
*
|
|
35157
|
-
* The runner's second pass makes the opposite measurement — the same detector
|
|
35158
|
-
* on a 640-800 px native square around a motion region the full-frame boxes
|
|
35159
|
-
* do NOT account for. Such a box is, by construction, not the subject the
|
|
35160
|
-
* full pass already has; absorbing it into a person track means the animal is
|
|
35161
|
-
* never seen at all, which on 2026-09-07 07:51:33 is exactly what happened to
|
|
35162
|
-
* an `animal` at `nativeRecoveryScore=0.833`.
|
|
35163
|
-
*
|
|
35164
|
-
* So the widening does not apply to a detection carrying `nativeRecovery`:
|
|
35165
|
-
* it associates to its OWN class or it spawns. Every other guard on that
|
|
35166
|
-
* spawn still runs — the per-class score floor, the area floor, the
|
|
35167
|
-
* `dedupSpawnIou` overlap, and `resolveDuplicates` afterwards, where a person
|
|
35168
|
-
* still beats a cross-class animal.
|
|
35329
|
+
* A NATIVE-MEASURED BOX IS ITS OWN SUBJECT ACROSS CLASSES (D378) — the
|
|
35330
|
+
* occlusion gate's copy of the first clause of {@link crossClassFlip}. A
|
|
35331
|
+
* cross-class occluder is not a same-subject claim for a box the native
|
|
35332
|
+
* pass measured: a dog at its owner's knee is 100% contained in the person's
|
|
35333
|
+
* box and is not the person.
|
|
35169
35334
|
*/
|
|
35170
35335
|
sameGroupAsMeasured(trackClass, det) {
|
|
35171
35336
|
if (det.nativeRecovery === void 0) return true;
|
|
@@ -35338,9 +35503,16 @@ var SortTracker = class SortTracker {
|
|
|
35338
35503
|
};
|
|
35339
35504
|
}
|
|
35340
35505
|
/**
|
|
35341
|
-
* How many
|
|
35342
|
-
*
|
|
35343
|
-
*
|
|
35506
|
+
* How many SAME-CLASS subjects OTHER than `corpse` share the scene with it:
|
|
35507
|
+
* every track LIVE on this frame, plus any other resurrectable corpse whose
|
|
35508
|
+
* lifetime OVERLAPPED the candidate's.
|
|
35509
|
+
*
|
|
35510
|
+
* Exact class, not the {person, animal} family (D439): a competitor is a
|
|
35511
|
+
* subject the id could actually be handed to, and since a salvage pass binds
|
|
35512
|
+
* its own class only, a person cannot take a dog's id there. Counting the
|
|
35513
|
+
* owner as the dog's competitor split one dog's id 12 times on camera 615 in
|
|
35514
|
+
* 6.6 h — every bind the reach clause accepted, the guard then refused
|
|
35515
|
+
* because the person was on screen.
|
|
35344
35516
|
*
|
|
35345
35517
|
* The overlap clause on the graveyard half is what keeps this guard off the
|
|
35346
35518
|
* case the resurrection pass exists for. A single flickering subject leaves a
|
|
@@ -35354,30 +35526,30 @@ var SortTracker = class SortTracker {
|
|
|
35354
35526
|
* steals an id is very often one whose OWN track was born after the corpse
|
|
35355
35527
|
* died (camera 618 — the toddler's track post-dates the older boy's).
|
|
35356
35528
|
*/
|
|
35357
|
-
|
|
35358
|
-
const group = this.assocGroup(corpse.class);
|
|
35529
|
+
sameClassPeerCount(corpse, live) {
|
|
35359
35530
|
let peers = 0;
|
|
35360
|
-
for (const t of live) if (t !== corpse &&
|
|
35531
|
+
for (const t of live) if (t !== corpse && t.class === corpse.class) peers++;
|
|
35361
35532
|
for (const other of this.lostTracks) {
|
|
35362
35533
|
if (other === corpse) continue;
|
|
35363
35534
|
if (!other.resurrectable) continue;
|
|
35364
|
-
if (
|
|
35535
|
+
if (other.class !== corpse.class) continue;
|
|
35365
35536
|
if (other.firstSeen <= corpse.lastSeen && corpse.firstSeen <= other.lastSeen) peers++;
|
|
35366
35537
|
}
|
|
35367
35538
|
return peers;
|
|
35368
35539
|
}
|
|
35369
35540
|
/**
|
|
35370
|
-
* Loose
|
|
35371
|
-
*
|
|
35372
|
-
*
|
|
35373
|
-
*
|
|
35374
|
-
*
|
|
35375
|
-
*
|
|
35376
|
-
*
|
|
35541
|
+
* Loose gate used by the rescue and resurrection passes: accept when the
|
|
35542
|
+
* detection overlaps the track's LAST-KNOWN bbox by `rescueIou`, or its
|
|
35543
|
+
* centroid is within reach. EXACT-class, always, independent of
|
|
35544
|
+
* `classGating` and of `classGroupAssoc` (D439): a salvage pass binds on
|
|
35545
|
+
* weak overlap by definition, and a weak-overlap bind across {person,
|
|
35546
|
+
* animal} is not a crouch flip — a flip sits on the subject's own box — it
|
|
35547
|
+
* is a dog beside its owner, or the owner beside the dog. Until 2026-09-10
|
|
35548
|
+
* this gate crossed the group, and it is where a `person` at 0.86 took over
|
|
35549
|
+
* an animal's id (camera 615, 17:11:01).
|
|
35377
35550
|
*/
|
|
35378
35551
|
looseMatch(track, det, timestamp, timeScaled = true) {
|
|
35379
|
-
if (
|
|
35380
|
-
if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return null;
|
|
35552
|
+
if (track.class !== det.class) return null;
|
|
35381
35553
|
if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
|
|
35382
35554
|
const tc = bboxCentroid(track.bbox);
|
|
35383
35555
|
const dc = bboxCentroid(det.bbox);
|
|
@@ -35423,12 +35595,41 @@ var SortTracker = class SortTracker {
|
|
|
35423
35595
|
* bind time and at report time — the spawn-beside-coasting line has to be able
|
|
35424
35596
|
* to say which gate declined without re-running the greedy competition.
|
|
35425
35597
|
*/
|
|
35598
|
+
/**
|
|
35599
|
+
* WHO the crowd was, for the record only — this changes no decision.
|
|
35600
|
+
*
|
|
35601
|
+
* {@link isUnambiguousReach} refuses on PRESENCE, with no distance in the
|
|
35602
|
+
* question, so a person at the far corner of a room splits a track as surely
|
|
35603
|
+
* as one standing beside it. Counting them, and measuring the nearest, is
|
|
35604
|
+
* what makes narrowing that rule a decision rather than a guess (D436).
|
|
35605
|
+
*/
|
|
35606
|
+
crowdAround(track, det, detections) {
|
|
35607
|
+
const cx = det.bbox.x + det.bbox.w / 2;
|
|
35608
|
+
const cy = det.bbox.y + det.bbox.h / 2;
|
|
35609
|
+
const counted = [];
|
|
35610
|
+
let nearest;
|
|
35611
|
+
const consider = (bbox) => {
|
|
35612
|
+
counted.push(bbox);
|
|
35613
|
+
const d = Math.hypot(bbox.x + bbox.w / 2 - cx, bbox.y + bbox.h / 2 - cy);
|
|
35614
|
+
if (nearest === void 0 || d < nearest) nearest = d;
|
|
35615
|
+
};
|
|
35616
|
+
for (const t of this.tracks) if (t !== track && t.class === track.class) consider(t.bbox);
|
|
35617
|
+
for (const other of detections) {
|
|
35618
|
+
if (other === det) continue;
|
|
35619
|
+
if (other.class !== track.class) continue;
|
|
35620
|
+
if (counted.some((b) => iou$3(b, other.bbox) > 0)) continue;
|
|
35621
|
+
consider(other.bbox);
|
|
35622
|
+
}
|
|
35623
|
+
return {
|
|
35624
|
+
count: counted.length,
|
|
35625
|
+
nearestPx: nearest
|
|
35626
|
+
};
|
|
35627
|
+
}
|
|
35426
35628
|
isUnambiguousReach(track, det, detections) {
|
|
35427
|
-
if (this.
|
|
35428
|
-
const group = this.assocGroup(track.class);
|
|
35629
|
+
if (this.sameClassPeerCount(track, this.tracks) > 0) return false;
|
|
35429
35630
|
for (const other of detections) {
|
|
35430
35631
|
if (other === det) continue;
|
|
35431
|
-
if (
|
|
35632
|
+
if (other.class === track.class) return false;
|
|
35432
35633
|
}
|
|
35433
35634
|
return true;
|
|
35434
35635
|
}
|
|
@@ -35465,8 +35666,8 @@ var SortTracker = class SortTracker {
|
|
|
35465
35666
|
* weight; otherwise the latest-frame class (ambiguous vote or voting off).
|
|
35466
35667
|
*/
|
|
35467
35668
|
resolveReportedClass(t) {
|
|
35468
|
-
if (!this.config.classVotingEnabled || t.classVotes.size === 0) return t.
|
|
35469
|
-
let bestClass = t.
|
|
35669
|
+
if (!this.config.classVotingEnabled || t.classVotes.size === 0) return t.frameClass;
|
|
35670
|
+
let bestClass = t.frameClass;
|
|
35470
35671
|
let bestVote = -1;
|
|
35471
35672
|
let total = 0;
|
|
35472
35673
|
for (const [cls, v] of t.classVotes) {
|
|
@@ -35476,8 +35677,8 @@ var SortTracker = class SortTracker {
|
|
|
35476
35677
|
bestClass = cls;
|
|
35477
35678
|
}
|
|
35478
35679
|
}
|
|
35479
|
-
if (total <= 0) return t.
|
|
35480
|
-
return bestVote / total >= this.config.classVoteMinFraction ? bestClass : t.
|
|
35680
|
+
if (total <= 0) return t.frameClass;
|
|
35681
|
+
return bestVote / total >= this.config.classVoteMinFraction ? bestClass : t.frameClass;
|
|
35481
35682
|
}
|
|
35482
35683
|
/** Whether a detection clears its per-class spawn score floor. Only gates NEW
|
|
35483
35684
|
* spawns — an established track still matches below its floor. */
|
|
@@ -35493,16 +35694,67 @@ var SortTracker = class SortTracker {
|
|
|
35493
35694
|
return this.config.confirmBypassEnabled && t.score >= this.config.confirmBypassScore;
|
|
35494
35695
|
}
|
|
35495
35696
|
/**
|
|
35697
|
+
* The live tracks split into the two tiers that claim a box in a fixed
|
|
35698
|
+
* order (D442, see {@link update}): those already confirmed for emission,
|
|
35699
|
+
* and the provisional ones still under `minHits`. Decided on the state at
|
|
35700
|
+
* the start of the frame, as ByteTrack does. With `minHits: 1` the second
|
|
35701
|
+
* tier is always empty and every pass over it is a no-op.
|
|
35702
|
+
*/
|
|
35703
|
+
associationTiers() {
|
|
35704
|
+
const established = [];
|
|
35705
|
+
const provisional = [];
|
|
35706
|
+
for (const track of this.tracks) if (this.isConfirmedForEmit(track)) established.push(track);
|
|
35707
|
+
else provisional.push(track);
|
|
35708
|
+
return [established, provisional];
|
|
35709
|
+
}
|
|
35710
|
+
/**
|
|
35711
|
+
* The rescue pass. HIGH-score detections the greedy pass left unmatched get
|
|
35712
|
+
* a second, looser chance against the still-unmatched (coasting) tracks of
|
|
35713
|
+
* `tracks` — measured on the track's LAST-KNOWN bbox, never the drifted
|
|
35714
|
+
* prediction. Recovers half-box re-acquisition and small-drift flicker
|
|
35715
|
+
* without spawning a duplicate. Best (highest last-known IoU) pairs first;
|
|
35716
|
+
* LOW detections are reserved for the ByteTrack low stage.
|
|
35717
|
+
*/
|
|
35718
|
+
rescueAssociate(tracks, gates, detections, lowSet, frameArea, timestamp, used, matchedTracks, matched, continuationOnly) {
|
|
35719
|
+
const rescuePairs = [];
|
|
35720
|
+
for (const track of tracks) {
|
|
35721
|
+
if (matchedTracks.has(track)) continue;
|
|
35722
|
+
for (let di = 0; di < detections.length; di++) {
|
|
35723
|
+
if (used.has(di) || lowSet.has(di) || continuationOnly.has(di)) continue;
|
|
35724
|
+
const det = detections[di];
|
|
35725
|
+
const gate = this.looseMatch(track, det, timestamp);
|
|
35726
|
+
if (gate === null || !gates.has(gate)) continue;
|
|
35727
|
+
if (gate === "centroid" && this.isPlausibleBirth(det, frameArea) && !(this.config.reacquireTimeScaled && this.isUnambiguousReach(track, det, detections))) continue;
|
|
35728
|
+
rescuePairs.push({
|
|
35729
|
+
track,
|
|
35730
|
+
detIdx: di,
|
|
35731
|
+
score: iou$3(track.bbox, det.bbox),
|
|
35732
|
+
gate
|
|
35733
|
+
});
|
|
35734
|
+
}
|
|
35735
|
+
}
|
|
35736
|
+
rescuePairs.sort((a, b) => b.score - a.score);
|
|
35737
|
+
for (const pair of rescuePairs) {
|
|
35738
|
+
if (matchedTracks.has(pair.track) || used.has(pair.detIdx)) continue;
|
|
35739
|
+
const det = detections[pair.detIdx];
|
|
35740
|
+
matched.set(pair.track, det);
|
|
35741
|
+
matchedTracks.add(pair.track);
|
|
35742
|
+
used.add(pair.detIdx);
|
|
35743
|
+
this.reportBind("rescue", pair.gate, pair.track, det, timestamp);
|
|
35744
|
+
}
|
|
35745
|
+
}
|
|
35746
|
+
/**
|
|
35496
35747
|
* One greedy IoU association pass over a candidate detection subset against the
|
|
35497
|
-
*
|
|
35748
|
+
* still-UNMATCHED tracks of `tracks` (one association tier — see
|
|
35749
|
+
* {@link associationTiers}), on each track's PREDICTED box. Highest-IoU pairs
|
|
35498
35750
|
* bind first; a track/detection binds at most once. Shared by the high-score
|
|
35499
35751
|
* first stage and the low-score second stage (different candidate sets + IoU
|
|
35500
35752
|
* gates) so the two-tier ByteTrack association stays a single implementation.
|
|
35501
35753
|
* Mutates `used` / `matchedTracks` / `matched` in place.
|
|
35502
35754
|
*/
|
|
35503
|
-
greedyAssociate(detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly) {
|
|
35755
|
+
greedyAssociate(tracks, detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly) {
|
|
35504
35756
|
const pairs = [];
|
|
35505
|
-
for (const track of
|
|
35757
|
+
for (const track of tracks) {
|
|
35506
35758
|
if (matchedTracks.has(track)) continue;
|
|
35507
35759
|
const pbox = this.predicted(track, timestamp);
|
|
35508
35760
|
const confirmed = this.isConfirmedForEmit(track);
|
|
@@ -35528,6 +35780,7 @@ var SortTracker = class SortTracker {
|
|
|
35528
35780
|
}
|
|
35529
35781
|
}
|
|
35530
35782
|
update(detections, timestamp, frameContext) {
|
|
35783
|
+
this.frameSeq++;
|
|
35531
35784
|
const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
|
|
35532
35785
|
const continuationOnly = frameContext?.continuationOnly ?? EMPTY_INDEX_SET;
|
|
35533
35786
|
const resurrectedScene = /* @__PURE__ */ new Map();
|
|
@@ -35538,6 +35791,7 @@ var SortTracker = class SortTracker {
|
|
|
35538
35791
|
track.lostAt = timestamp;
|
|
35539
35792
|
track.resurrectable = false;
|
|
35540
35793
|
this.reportTrackLoss(track);
|
|
35794
|
+
this.reportTrackDeath(track, "lifetime-cap", timestamp);
|
|
35541
35795
|
this.lostTracks.push(track);
|
|
35542
35796
|
} else alive.push(track);
|
|
35543
35797
|
this.tracks = alive;
|
|
@@ -35550,13 +35804,17 @@ var SortTracker = class SortTracker {
|
|
|
35550
35804
|
track.lostAt = track.lastSeen;
|
|
35551
35805
|
track.resurrectable = false;
|
|
35552
35806
|
this.reportTrackLoss(track);
|
|
35807
|
+
this.reportTrackDeath(track, "stale-beyond-budgets", timestamp);
|
|
35553
35808
|
this.lostTracks.push(track);
|
|
35554
35809
|
} else alive.push(track);
|
|
35555
35810
|
this.tracks = alive;
|
|
35556
35811
|
}
|
|
35557
35812
|
const stillResurrectable = [];
|
|
35558
35813
|
for (const t of this.lostTracks) if (timestamp - t.lostAt <= this.config.resurrectionWindowMs) stillResurrectable.push(t);
|
|
35559
|
-
else
|
|
35814
|
+
else {
|
|
35815
|
+
this.reportTrackLoss(t);
|
|
35816
|
+
this.reportTrackDeath(t, "graveyard-expired", timestamp);
|
|
35817
|
+
}
|
|
35560
35818
|
this.lostTracks = stillResurrectable;
|
|
35561
35819
|
const used = /* @__PURE__ */ new Set();
|
|
35562
35820
|
const matchedTracks = /* @__PURE__ */ new Set();
|
|
@@ -35566,36 +35824,21 @@ var SortTracker = class SortTracker {
|
|
|
35566
35824
|
for (let di = 0; di < detections.length; di++) if (this.config.byteTrackEnabled && detections[di].score < this.config.byteTrackHighThreshold) lowIdxs.push(di);
|
|
35567
35825
|
else highIdxs.push(di);
|
|
35568
35826
|
const lowSet = new Set(lowIdxs);
|
|
35569
|
-
|
|
35570
|
-
const
|
|
35571
|
-
|
|
35572
|
-
|
|
35573
|
-
|
|
35574
|
-
|
|
35575
|
-
|
|
35576
|
-
|
|
35577
|
-
|
|
35578
|
-
if (gate === "centroid" && this.isPlausibleBirth(det, frameArea) && !(this.config.reacquireTimeScaled && this.isUnambiguousReach(track, det, detections))) continue;
|
|
35579
|
-
rescuePairs.push({
|
|
35580
|
-
track,
|
|
35581
|
-
detIdx: di,
|
|
35582
|
-
score: iou$3(track.bbox, det.bbox),
|
|
35583
|
-
gate
|
|
35584
|
-
});
|
|
35585
|
-
}
|
|
35586
|
-
}
|
|
35587
|
-
rescuePairs.sort((a, b) => b.score - a.score);
|
|
35588
|
-
for (const pair of rescuePairs) {
|
|
35589
|
-
if (matchedTracks.has(pair.track) || used.has(pair.detIdx)) continue;
|
|
35590
|
-
const det = detections[pair.detIdx];
|
|
35591
|
-
matched.set(pair.track, det);
|
|
35592
|
-
matchedTracks.add(pair.track);
|
|
35593
|
-
used.add(pair.detIdx);
|
|
35594
|
-
this.reportBind("rescue", pair.gate, pair.track, det, timestamp);
|
|
35595
|
-
}
|
|
35596
|
-
if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
|
|
35827
|
+
const [established, provisional] = this.associationTiers();
|
|
35828
|
+
const OVERLAP = new Set(["iou"]);
|
|
35829
|
+
const PROXIMITY = new Set(["centroid", "growth"]);
|
|
35830
|
+
this.greedyAssociate(established, detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
|
|
35831
|
+
this.rescueAssociate(established, OVERLAP, detections, lowSet, frameArea, timestamp, used, matchedTracks, matched, continuationOnly);
|
|
35832
|
+
this.greedyAssociate(provisional, detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
|
|
35833
|
+
this.rescueAssociate(established, PROXIMITY, detections, lowSet, frameArea, timestamp, used, matchedTracks, matched, continuationOnly);
|
|
35834
|
+
this.rescueAssociate(provisional, new Set([...OVERLAP, ...PROXIMITY]), detections, lowSet, frameArea, timestamp, used, matchedTracks, matched, continuationOnly);
|
|
35835
|
+
if (this.config.byteTrackEnabled && lowIdxs.length > 0) for (const tier of [established, provisional]) this.greedyAssociate(tier, detections, lowIdxs, this.config.byteTrackLowIouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
|
|
35597
35836
|
for (const [track, det] of matched) {
|
|
35598
|
-
if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, {
|
|
35837
|
+
if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, {
|
|
35838
|
+
against: track,
|
|
35839
|
+
measured: iou$3(this.predicted(track, timestamp), det.bbox),
|
|
35840
|
+
threshold: this.config.animalOverPersonMaxScore
|
|
35841
|
+
});
|
|
35599
35842
|
const prevCenter = bboxCentroid({
|
|
35600
35843
|
x: track.bbox.x,
|
|
35601
35844
|
y: track.bbox.y,
|
|
@@ -35611,12 +35854,15 @@ var SortTracker = class SortTracker {
|
|
|
35611
35854
|
const frames = track.age + 1;
|
|
35612
35855
|
const dtMs = timestamp - track.lastSeen;
|
|
35613
35856
|
track.bbox = det.bbox;
|
|
35614
|
-
track.class = det.class;
|
|
35857
|
+
if (!this.crossClassFlip(track.class, det)) track.class = det.class;
|
|
35858
|
+
track.frameClass = det.class;
|
|
35615
35859
|
track.originalClass = det.originalClass;
|
|
35616
35860
|
track.score = det.score;
|
|
35861
|
+
track.lastNativeRecovery = det.nativeRecovery;
|
|
35617
35862
|
track.age = 0;
|
|
35618
35863
|
track.hits++;
|
|
35619
35864
|
track.lastSeen = timestamp;
|
|
35865
|
+
track.lastSeenFrameSeq = this.frameSeq;
|
|
35620
35866
|
if (dtMs > 0) {
|
|
35621
35867
|
track.velocity = {
|
|
35622
35868
|
dx: (newCenter.x - prevCenter.x) / dtMs,
|
|
@@ -35665,7 +35911,7 @@ var SortTracker = class SortTracker {
|
|
|
35665
35911
|
if (gate === null) continue;
|
|
35666
35912
|
if (gate === "centroid" && this.isPlausibleBirth(det, frameArea)) continue;
|
|
35667
35913
|
const score = iou$3(lost.bbox, det.bbox);
|
|
35668
|
-
const peers = this.
|
|
35914
|
+
const peers = this.sameClassPeerCount(lost, surviving);
|
|
35669
35915
|
if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
|
|
35670
35916
|
if (score > refusedScore) {
|
|
35671
35917
|
refused = lost;
|
|
@@ -35690,12 +35936,15 @@ var SortTracker = class SortTracker {
|
|
|
35690
35936
|
this.lostTracks = this.lostTracks.filter((t) => t !== best);
|
|
35691
35937
|
best.bbox = det.bbox;
|
|
35692
35938
|
best.class = det.class;
|
|
35939
|
+
best.frameClass = det.class;
|
|
35693
35940
|
best.originalClass = det.originalClass;
|
|
35694
35941
|
best.score = det.score;
|
|
35942
|
+
best.lastNativeRecovery = det.nativeRecovery;
|
|
35695
35943
|
best.age = 0;
|
|
35696
35944
|
best.hits++;
|
|
35697
35945
|
best.lost = false;
|
|
35698
35946
|
best.lastSeen = timestamp;
|
|
35947
|
+
best.lastSeenFrameSeq = this.frameSeq;
|
|
35699
35948
|
best.velocity = {
|
|
35700
35949
|
dx: 0,
|
|
35701
35950
|
dy: 0
|
|
@@ -35752,6 +36001,7 @@ var SortTracker = class SortTracker {
|
|
|
35752
36001
|
id: spawnedId,
|
|
35753
36002
|
bbox: det.bbox,
|
|
35754
36003
|
class: det.class,
|
|
36004
|
+
frameClass: det.class,
|
|
35755
36005
|
originalClass: det.originalClass,
|
|
35756
36006
|
score: det.score,
|
|
35757
36007
|
age: 0,
|
|
@@ -35759,6 +36009,7 @@ var SortTracker = class SortTracker {
|
|
|
35759
36009
|
path: [det.bbox],
|
|
35760
36010
|
firstSeen: timestamp,
|
|
35761
36011
|
lastSeen: timestamp,
|
|
36012
|
+
lastSeenFrameSeq: this.frameSeq,
|
|
35762
36013
|
velocity: {
|
|
35763
36014
|
dx: 0,
|
|
35764
36015
|
dy: 0
|
|
@@ -35769,7 +36020,8 @@ var SortTracker = class SortTracker {
|
|
|
35769
36020
|
resurrectable: true,
|
|
35770
36021
|
classVotes: new Map([[det.class, det.score > 0 ? det.score : Number.EPSILON]]),
|
|
35771
36022
|
emitted: false,
|
|
35772
|
-
lossReported: false
|
|
36023
|
+
lossReported: false,
|
|
36024
|
+
lastNativeRecovery: det.nativeRecovery
|
|
35773
36025
|
});
|
|
35774
36026
|
}
|
|
35775
36027
|
this.tracks = surviving;
|
|
@@ -36162,7 +36414,6 @@ var FrameProcessor = class {
|
|
|
36162
36414
|
* filter). Shares the tracker's vocabulary — see {@link SpawnRefusalReason}. */
|
|
36163
36415
|
spawnRefusalObserver = null;
|
|
36164
36416
|
/** Where a collected refusal is forwarded for LOGGING, when anyone asked. */
|
|
36165
|
-
spawnRefusalForward = null;
|
|
36166
36417
|
/**
|
|
36167
36418
|
* The spawn refusals of the frame being processed.
|
|
36168
36419
|
*
|
|
@@ -36176,6 +36427,10 @@ var FrameProcessor = class {
|
|
|
36176
36427
|
/** Running count of refusals-in-zone SEEN — never the throttled number. */
|
|
36177
36428
|
zoneRefusalEventCount = 0;
|
|
36178
36429
|
zoneRefusalObserver = null;
|
|
36430
|
+
/** Track ids whose BIRTH zone history has already been folded in — the fold
|
|
36431
|
+
* happens once, on the frame a track first becomes visible. */
|
|
36432
|
+
birthZonesSeeded = /* @__PURE__ */ new Set();
|
|
36433
|
+
birthZoneTrailObserver = null;
|
|
36179
36434
|
/** Last report time per collapsed key, for {@link ZONE_REFUSAL_THROTTLE_MS}. */
|
|
36180
36435
|
zoneRefusalLastReport = /* @__PURE__ */ new Map();
|
|
36181
36436
|
zoneRefusalSampler = new SpawnRefusalSampler();
|
|
@@ -36286,12 +36541,20 @@ var FrameProcessor = class {
|
|
|
36286
36541
|
setZoneRefusalObserver(observer) {
|
|
36287
36542
|
this.zoneRefusalObserver = observer;
|
|
36288
36543
|
}
|
|
36544
|
+
/** Where "this track was born already carrying a zone" is reported. */
|
|
36545
|
+
setBirthZoneTrailObserver(observer) {
|
|
36546
|
+
this.birthZoneTrailObserver = observer;
|
|
36547
|
+
}
|
|
36289
36548
|
setBindObserver(observer) {
|
|
36290
36549
|
this.tracker.setBindObserver(observer);
|
|
36291
36550
|
}
|
|
36292
36551
|
/** Wire (or clear) the tracker's fragmentation sink — a new id minted beside a
|
|
36293
36552
|
* live coasting track that could have claimed it (D399). Same `deviceId`
|
|
36294
36553
|
* contract as the two sinks above. */
|
|
36554
|
+
/** Wire the sink for track deaths (D438) — pure passthrough to the tracker. */
|
|
36555
|
+
setTrackLossObserver(observer) {
|
|
36556
|
+
this.tracker.setTrackLossObserver(observer);
|
|
36557
|
+
}
|
|
36295
36558
|
setSpawnBesideCoastingObserver(observer) {
|
|
36296
36559
|
this.tracker.setSpawnBesideCoastingObserver(observer);
|
|
36297
36560
|
}
|
|
@@ -36375,8 +36638,8 @@ var FrameProcessor = class {
|
|
|
36375
36638
|
memberships.map((m) => m.zoneId).join("+"),
|
|
36376
36639
|
cell(r.bbox.x),
|
|
36377
36640
|
cell(r.bbox.y),
|
|
36378
|
-
cell(r.bbox.
|
|
36379
|
-
cell(r.bbox.
|
|
36641
|
+
cell(r.bbox.w),
|
|
36642
|
+
cell(r.bbox.h)
|
|
36380
36643
|
].join("|");
|
|
36381
36644
|
const last = this.zoneRefusalLastReport.get(key);
|
|
36382
36645
|
if (last !== void 0 && timestamp - last < ZONE_REFUSAL_THROTTLE_MS) return false;
|
|
@@ -36621,6 +36884,32 @@ var FrameProcessor = class {
|
|
|
36621
36884
|
if (memberships.length > 0) overlapsByTrack.set(td.trackId, memberships);
|
|
36622
36885
|
if (belowBar.length > 0) rejectedByTrack.set(td.trackId, belowBar);
|
|
36623
36886
|
}
|
|
36887
|
+
for (const td of trackedDetections) {
|
|
36888
|
+
if (this.birthZonesSeeded.has(td.trackId)) continue;
|
|
36889
|
+
this.birthZonesSeeded.add(td.trackId);
|
|
36890
|
+
const trail = td.path;
|
|
36891
|
+
if (trail.length <= 1) continue;
|
|
36892
|
+
const atBirth = zonesByTrack.get(td.trackId) ?? [];
|
|
36893
|
+
const zones = new Set(atBirth);
|
|
36894
|
+
const bornWith = zones.size;
|
|
36895
|
+
for (let i = 0; i < trail.length - 1; i++) {
|
|
36896
|
+
const box = trail[i];
|
|
36897
|
+
if (box === void 0) continue;
|
|
36898
|
+
const { memberships } = this.zoneEngine.splitDetectionZones(box, this.zones, frameWidth, frameHeight, void 0, void 0, void 0, this.zoneMembershipMinOverlap);
|
|
36899
|
+
for (const m of memberships) zones.add(m.zoneId);
|
|
36900
|
+
}
|
|
36901
|
+
if (zones.size === bornWith) continue;
|
|
36902
|
+
const inherited = [...zones].filter((z) => !atBirth.includes(z));
|
|
36903
|
+
zonesByTrack.set(td.trackId, [...zones]);
|
|
36904
|
+
this.birthZoneTrailObserver?.({
|
|
36905
|
+
deviceId: this.deviceId,
|
|
36906
|
+
trackId: td.trackId,
|
|
36907
|
+
className: td.class,
|
|
36908
|
+
measurementsBeforeBirth: trail.length - 1,
|
|
36909
|
+
zonesAtBirth: [...atBirth],
|
|
36910
|
+
inheritedZones: inherited
|
|
36911
|
+
});
|
|
36912
|
+
}
|
|
36624
36913
|
this.lastZoneOverlaps = overlapsByTrack;
|
|
36625
36914
|
this.lastZoneRejections = rejectedByTrack;
|
|
36626
36915
|
for (const td of trackedDetections) this.lastDetectionByTrack.set(td.trackId, td);
|
|
@@ -36693,6 +36982,7 @@ var FrameProcessor = class {
|
|
|
36693
36982
|
for (const state of objectStates) {
|
|
36694
36983
|
if (state.state !== "leaving") continue;
|
|
36695
36984
|
this.lastZonesByTrack.delete(state.trackId);
|
|
36985
|
+
this.birthZonesSeeded.delete(state.trackId);
|
|
36696
36986
|
this.lastDetectionByTrack.delete(state.trackId);
|
|
36697
36987
|
this.reportedRefusalByTrack.delete(state.trackId);
|
|
36698
36988
|
}
|
|
@@ -39620,6 +39910,62 @@ function tieredLabelColumnData(patch) {
|
|
|
39620
39910
|
};
|
|
39621
39911
|
}
|
|
39622
39912
|
//#endregion
|
|
39913
|
+
//#region src/pipeline-analytics/store/audio-level-density.ts
|
|
39914
|
+
/**
|
|
39915
|
+
* The energy-weighted mean of a set of dBFS values, in dBFS.
|
|
39916
|
+
*
|
|
39917
|
+
* dBFS is logarithmic, so an arithmetic mean of dB numbers is the mean of
|
|
39918
|
+
* nothing physical — it under-weights the loud moment that is the reason to
|
|
39919
|
+
* look at a bar at all. Averaging POWER and converting back is what a meter is
|
|
39920
|
+
* expected to show, and it is the same arithmetic as an RMS over the interval.
|
|
39921
|
+
*
|
|
39922
|
+
* `undefined` for an empty set, and for a set with no finite sample: a bucket
|
|
39923
|
+
* nobody measured must not be reported as silence, and −∞ dBFS is not a number
|
|
39924
|
+
* a bar can paint.
|
|
39925
|
+
*/
|
|
39926
|
+
function energyWeightedMeanDbfs(samples) {
|
|
39927
|
+
let power = 0;
|
|
39928
|
+
let n = 0;
|
|
39929
|
+
for (const dbfs of samples) {
|
|
39930
|
+
if (Number.isNaN(dbfs)) continue;
|
|
39931
|
+
if (dbfs === Number.NEGATIVE_INFINITY) {
|
|
39932
|
+
n += 1;
|
|
39933
|
+
continue;
|
|
39934
|
+
}
|
|
39935
|
+
if (!Number.isFinite(dbfs)) continue;
|
|
39936
|
+
power += 10 ** (dbfs / 10);
|
|
39937
|
+
n += 1;
|
|
39938
|
+
}
|
|
39939
|
+
if (n === 0) return void 0;
|
|
39940
|
+
const mean = power / n;
|
|
39941
|
+
if (mean <= 0) return void 0;
|
|
39942
|
+
return 10 * Math.log10(mean);
|
|
39943
|
+
}
|
|
39944
|
+
/**
|
|
39945
|
+
* Energy-weighted mean dBFS per bucket index, for the buckets that HAVE audio.
|
|
39946
|
+
*
|
|
39947
|
+
* A bucket with no events is absent from the map rather than present at zero:
|
|
39948
|
+
* "nothing was heard" and "nothing was measured" are different claims, and the
|
|
39949
|
+
* lane has to be able to draw nothing.
|
|
39950
|
+
*/
|
|
39951
|
+
function audioDbfsByBucket(events, since, bucketMs) {
|
|
39952
|
+
const out = /* @__PURE__ */ new Map();
|
|
39953
|
+
if (bucketMs <= 0) return out;
|
|
39954
|
+
const byBucket = /* @__PURE__ */ new Map();
|
|
39955
|
+
for (const ev of events) {
|
|
39956
|
+
const idx = Math.floor((ev.timestamp - since) / bucketMs);
|
|
39957
|
+
if (idx < 0) continue;
|
|
39958
|
+
const bin = byBucket.get(idx);
|
|
39959
|
+
if (bin === void 0) byBucket.set(idx, [ev.dbfs]);
|
|
39960
|
+
else bin.push(ev.dbfs);
|
|
39961
|
+
}
|
|
39962
|
+
for (const [idx, samples] of byBucket) {
|
|
39963
|
+
const mean = energyWeightedMeanDbfs(samples);
|
|
39964
|
+
if (mean !== void 0) out.set(idx, mean);
|
|
39965
|
+
}
|
|
39966
|
+
return out;
|
|
39967
|
+
}
|
|
39968
|
+
//#endregion
|
|
39623
39969
|
//#region src/pipeline-analytics/store/event-store.ts
|
|
39624
39970
|
/**
|
|
39625
39971
|
* @durable class=ledger owner=pipeline-analytics
|
|
@@ -40458,10 +40804,11 @@ var EventStore = class {
|
|
|
40458
40804
|
});
|
|
40459
40805
|
return new Map(rows.map((r) => [r.bucket, r.count]));
|
|
40460
40806
|
};
|
|
40461
|
-
const [motionMap, objectMap, audioMap] = await Promise.all([
|
|
40807
|
+
const [motionMap, objectMap, audioMap, audioDbfsMap] = await Promise.all([
|
|
40462
40808
|
run(MOTION_EVENTS_COLLECTION),
|
|
40463
40809
|
run(OBJECT_EVENTS_COLLECTION),
|
|
40464
|
-
run(AUDIO_EVENTS_COLLECTION)
|
|
40810
|
+
run(AUDIO_EVENTS_COLLECTION),
|
|
40811
|
+
this.audioDbfsPerBucket(q)
|
|
40465
40812
|
]);
|
|
40466
40813
|
const buckets = Math.max(0, Math.ceil((q.until - q.since) / q.bucketMs));
|
|
40467
40814
|
const out = [];
|
|
@@ -40470,16 +40817,46 @@ var EventStore = class {
|
|
|
40470
40817
|
const o = objectMap.get(i) ?? 0;
|
|
40471
40818
|
const a = audioMap.get(i) ?? 0;
|
|
40472
40819
|
if (m + o + a === 0) continue;
|
|
40820
|
+
const dbfs = audioDbfsMap.get(i);
|
|
40473
40821
|
out.push({
|
|
40474
40822
|
bucketStart: q.since + i * q.bucketMs,
|
|
40475
40823
|
motion: m,
|
|
40476
40824
|
object: o,
|
|
40477
|
-
audio: a
|
|
40825
|
+
audio: a,
|
|
40826
|
+
...dbfs !== void 0 ? { audioDbfs: dbfs } : {}
|
|
40478
40827
|
});
|
|
40479
40828
|
}
|
|
40480
40829
|
return out;
|
|
40481
40830
|
}
|
|
40482
40831
|
/**
|
|
40832
|
+
* Energy-weighted mean dBFS per bucket index, for the buckets that have
|
|
40833
|
+
* audio. Reads the window's audio events once, `slim`, and folds them —
|
|
40834
|
+
* `histogram` can only count, and a level is not a count.
|
|
40835
|
+
*
|
|
40836
|
+
* A read that fails yields an EMPTY map, so the lane draws no level rather
|
|
40837
|
+
* than a wrong one: the density counts beside it are unaffected, and a
|
|
40838
|
+
* missing bar is the honest rendering of "not measured" (D431).
|
|
40839
|
+
*/
|
|
40840
|
+
async audioDbfsPerBucket(q) {
|
|
40841
|
+
try {
|
|
40842
|
+
return audioDbfsByBucket((await this.queryAudio({
|
|
40843
|
+
deviceId: q.deviceId,
|
|
40844
|
+
since: q.since,
|
|
40845
|
+
until: q.until,
|
|
40846
|
+
projection: "slim"
|
|
40847
|
+
})).map((e) => ({
|
|
40848
|
+
timestamp: e.timestamp,
|
|
40849
|
+
dbfs: e.dbfs
|
|
40850
|
+
})), q.since, q.bucketMs);
|
|
40851
|
+
} catch (err) {
|
|
40852
|
+
this.logger.warn("density: audio level aggregation failed — the lane will draw no level", {
|
|
40853
|
+
tags: { deviceId: q.deviceId },
|
|
40854
|
+
meta: { error: String(err) }
|
|
40855
|
+
});
|
|
40856
|
+
return /* @__PURE__ */ new Map();
|
|
40857
|
+
}
|
|
40858
|
+
}
|
|
40859
|
+
/**
|
|
40483
40860
|
* Age-evict the TRACKLESS event kinds — motion and audio.
|
|
40484
40861
|
*
|
|
40485
40862
|
* **Object events are not here, and that is the point.** An object event is a
|
|
@@ -66567,10 +66944,12 @@ var TrackingSettingsSchema = require_dist.object({
|
|
|
66567
66944
|
minSpawnAreaFracAnimal: require_dist.number().min(0).max(1).default(0),
|
|
66568
66945
|
/** Min spawn bbox area for `vehicle` as a fraction of the frame (0 = ungated). */
|
|
66569
66946
|
minSpawnAreaFracVehicle: require_dist.number().min(0).max(1).default(0),
|
|
66570
|
-
/**
|
|
66571
|
-
*
|
|
66572
|
-
*
|
|
66573
|
-
*
|
|
66947
|
+
/** Let a person that flips to `animal` for a frame re-match its existing
|
|
66948
|
+
* track (no concurrent animal id). A flip is the same subject in the same
|
|
66949
|
+
* place, weakly read (D439): the bind is allowed only on the predicted-box
|
|
66950
|
+
* passes, only for a coarse box below `animalOverPersonMaxScore`, never on
|
|
66951
|
+
* the rescue / resurrection passes. Reported label stays via the class
|
|
66952
|
+
* vote. Off = exact per-frame class gating. */
|
|
66574
66953
|
classGroupAssoc: require_dist.boolean().default(true)
|
|
66575
66954
|
});
|
|
66576
66955
|
var TRACKING_DEFAULTS = TrackingSettingsSchema.parse({});
|
|
@@ -67160,7 +67539,7 @@ function buildDetectionSettingsSections() {
|
|
|
67160
67539
|
type: "boolean",
|
|
67161
67540
|
key: "classGroupAssoc",
|
|
67162
67541
|
label: "Person/animal group matching",
|
|
67163
|
-
description: "Let a detection that flips between person and animal for a frame (a crouching / bending person is often read as an animal) re-match its existing track instead of spawning a concurrent animal track. The reported class is still decided by the per-track class vote.",
|
|
67542
|
+
description: "Let a detection that flips between person and animal for a frame (a crouching / bending person is often read as an animal) re-match its existing track instead of spawning a concurrent animal track. A flip is the same subject in the same place, weakly read: the cross-class match happens only on the strict predicted-box pass and only for a full-frame box below the \"animal over person max score\" bar — a confident animal beside a person, or a person beside an animal, is its own subject and keeps its own track. The reported class is still decided by the per-track class vote.",
|
|
67164
67543
|
default: TRACKING_DEFAULTS.classGroupAssoc
|
|
67165
67544
|
},
|
|
67166
67545
|
{
|
|
@@ -72816,12 +73195,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
72816
73195
|
*/
|
|
72817
73196
|
buildTimelapsePorts(api, stores) {
|
|
72818
73197
|
return {
|
|
72819
|
-
getAvailability: async ({ deviceId, fromMs, toMs }) => {
|
|
72820
|
-
|
|
73198
|
+
getAvailability: async ({ deviceId, fromMs, toMs, profile }) => {
|
|
73199
|
+
const availability = await api.recording.getAvailability.query({
|
|
72821
73200
|
deviceId,
|
|
72822
73201
|
fromMs,
|
|
72823
|
-
toMs
|
|
72824
|
-
|
|
73202
|
+
toMs,
|
|
73203
|
+
...profile !== void 0 ? { profile } : {}
|
|
73204
|
+
});
|
|
73205
|
+
return {
|
|
73206
|
+
ranges: availability.ranges,
|
|
73207
|
+
profilesWithFootage: availability.profilesWithFootage
|
|
73208
|
+
};
|
|
72825
73209
|
},
|
|
72826
73210
|
listTracks: async ({ deviceId, since, until }) => {
|
|
72827
73211
|
return (await stores.trackStore.queryHistorical({
|
|
@@ -77608,6 +77992,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77608
77992
|
}
|
|
77609
77993
|
});
|
|
77610
77994
|
});
|
|
77995
|
+
p.setBirthZoneTrailObserver((trail) => {
|
|
77996
|
+
this.ctx.logger.info("track born carrying zones it crossed before it was a track", {
|
|
77997
|
+
tags: { deviceId },
|
|
77998
|
+
meta: {
|
|
77999
|
+
source,
|
|
78000
|
+
...trail
|
|
78001
|
+
}
|
|
78002
|
+
});
|
|
78003
|
+
});
|
|
77611
78004
|
p.setZoneRefusalObserver((observation) => {
|
|
77612
78005
|
this.ctx.logger.info("detection refused inside a watched zone — not notified", {
|
|
77613
78006
|
tags: { deviceId },
|
|
@@ -77617,6 +78010,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77617
78010
|
}
|
|
77618
78011
|
});
|
|
77619
78012
|
});
|
|
78013
|
+
p.setTrackLossObserver((record) => {
|
|
78014
|
+
this.ctx.logger.info("tracker: a track ended and nothing took over", {
|
|
78015
|
+
tags: { deviceId },
|
|
78016
|
+
meta: {
|
|
78017
|
+
source,
|
|
78018
|
+
...record
|
|
78019
|
+
}
|
|
78020
|
+
});
|
|
78021
|
+
});
|
|
77620
78022
|
p.setSpawnBesideCoastingObserver((record) => {
|
|
77621
78023
|
this.ctx.logger.info("tracker: new id minted beside a live coasting track", {
|
|
77622
78024
|
tags: { deviceId },
|