@camstack/addon-post-analysis 1.2.218 → 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-CbQHenLE.js → dist-Cnlk6Tyz.js} +60 -2
- package/dist/{dist-BqkhuC85.mjs → dist-D0l3xZqa.mjs} +60 -2
- 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-JPlVJb-8.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C8d8YCvw.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-CqdkxAhm.mjs → hostInit-BsYVmiZ9.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +363 -104
- package/dist/pipeline-analytics/index.mjs +363 -104
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as VISIT_MERGE_GAP_MS, $t as object, A as NcRulePatchSchema, Bt as CamProfileSchema, C as NC_ALARM_SYSTEM_EVENT_KINDS, D as NC_TAXONOMY, Dt as readDeviceStateFrom, Et as plateGalleryCapability, F as NcSnoozeSchema, Ft as vectorDimFromBase64, G as SCENE_DEFAULT_UNCOVERED_POLICY, Gt as nodePin, H as RetrainStatusSchema, Ht as createEvent, I as NcSnoozeSuppressedSchema, It as videoclipsCapability, J as TIMELAPSE_DENSE_FLOOR_SEC, Jt as array, K as SCENE_DIVERGED, Kt as sleep$1, L as NcSystemEventKindSchema, Lt as zoneAnalyticsCapability, M as NcRuleTargetSchema, Mt as storageOccupancyCapability, N as NcScheduleSchema, Nt as subKindsOf, O as NcConditionDescriptorSchema, Ot as readTimelapseGeneratedAt, P as NcSnoozeInputSchema, Pt as systemEventFilterApplies, Q as TrackSourceSchema, Qt as number, R as NcTaxonomySchema, Rt as errMsg$1, S as MediaFileKindEnum, St as notificationRulesCapability, T as NC_DEFAULT_SNOOZE_MINUTES, Tt as pipelineAnalyticsCapability, Ut as hydrateSchema, V as RECORDING_EXPORT_MAX_READ_BYTES, Vt as DeviceType, W as SCENE_DEFAULT_ANCHOR_THRESHOLD, Wt as isDeviceScopedCap, X as TimelapseRulePatchSchema, Xt as discriminatedUnion, Y as TimelapseRuleInputSchema, Yt as boolean, Z as TimelapseRuleSchema, Zt as literal, _ as LabelAttributionSchema, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, at as buildEventKindDescriptor, b as MAX_BIRTH_DECISION_RECORDS, bt as kebabToCamel, ct as defineCustomActions, dt as encodeVectorBase64, en as partialRecord, et as addonWidgetsSourceCapability, f as DeclaredDevices, ft as evaluateSensorEdge, g as FailureCounters, h as FULL_IMAGE_BBOX, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as EventCategory, it as audioModeOf, j as NcRuleSchema, jt as sceneMonitorCapability, k as NcRuleInputSchema, kt as resolveLocationMode, lt as deriveRecordingMode, m as EVENT_PAD_MS, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as string, nt as assertTimelapseCadences, o as COCO_TO_MACRO, ot as cosineSimilarity$1, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SceneMonitorSchema, qt as _enum, r as BaseDevice, rn as unknown, rt as audioMetricsCapability, s as DEFAULT_EVENT_COLOR, st as customAction, t as AUDIO_MACRO_LABELS, tn as record, tt as alarmPanelCapability, u as DETECTION_MACRO_CLASSES, v as MACRO_LABELS, vt as isScheduleActive, w as NC_CONDITION_CATALOG, wt as pickClusterStepModels, xt as mayWriteToLocation, y as MAX_ARCHIVED_DEBUG_NOTES, yt as isSourceCap, z as OpsLogEntrySchema, zt as BaseAddon } from "../dist-
|
|
1
|
+
import { $ as VISIT_MERGE_GAP_MS, $t as object, A as NcRulePatchSchema, Bt as CamProfileSchema, C as NC_ALARM_SYSTEM_EVENT_KINDS, D as NC_TAXONOMY, Dt as readDeviceStateFrom, Et as plateGalleryCapability, F as NcSnoozeSchema, Ft as vectorDimFromBase64, G as SCENE_DEFAULT_UNCOVERED_POLICY, Gt as nodePin, H as RetrainStatusSchema, Ht as createEvent, I as NcSnoozeSuppressedSchema, It as videoclipsCapability, J as TIMELAPSE_DENSE_FLOOR_SEC, Jt as array, K as SCENE_DIVERGED, Kt as sleep$1, L as NcSystemEventKindSchema, Lt as zoneAnalyticsCapability, M as NcRuleTargetSchema, Mt as storageOccupancyCapability, N as NcScheduleSchema, Nt as subKindsOf, O as NcConditionDescriptorSchema, Ot as readTimelapseGeneratedAt, P as NcSnoozeInputSchema, Pt as systemEventFilterApplies, Q as TrackSourceSchema, Qt as number, R as NcTaxonomySchema, Rt as errMsg$1, S as MediaFileKindEnum, St as notificationRulesCapability, T as NC_DEFAULT_SNOOZE_MINUTES, Tt as pipelineAnalyticsCapability, Ut as hydrateSchema, V as RECORDING_EXPORT_MAX_READ_BYTES, Vt as DeviceType, W as SCENE_DEFAULT_ANCHOR_THRESHOLD, Wt as isDeviceScopedCap, X as TimelapseRulePatchSchema, Xt as discriminatedUnion, Y as TimelapseRuleInputSchema, Yt as boolean, Z as TimelapseRuleSchema, Zt as literal, _ as LabelAttributionSchema, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, at as buildEventKindDescriptor, b as MAX_BIRTH_DECISION_RECORDS, bt as kebabToCamel, ct as defineCustomActions, dt as encodeVectorBase64, en as partialRecord, et as addonWidgetsSourceCapability, f as DeclaredDevices, ft as evaluateSensorEdge, g as FailureCounters, h as FULL_IMAGE_BBOX, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as EventCategory, it as audioModeOf, j as NcRuleSchema, jt as sceneMonitorCapability, k as NcRuleInputSchema, kt as resolveLocationMode, lt as deriveRecordingMode, m as EVENT_PAD_MS, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as string, nt as assertTimelapseCadences, o as COCO_TO_MACRO, ot as cosineSimilarity$1, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SceneMonitorSchema, qt as _enum, r as BaseDevice, rn as unknown, rt as audioMetricsCapability, s as DEFAULT_EVENT_COLOR, st as customAction, t as AUDIO_MACRO_LABELS, tn as record, tt as alarmPanelCapability, u as DETECTION_MACRO_CLASSES, v as MACRO_LABELS, vt as isScheduleActive, w as NC_CONDITION_CATALOG, wt as pickClusterStepModels, xt as mayWriteToLocation, y as MAX_ARCHIVED_DEBUG_NOTES, yt as isSourceCap, z as OpsLogEntrySchema, zt as BaseAddon } from "../dist-D0l3xZqa.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -8334,8 +8334,55 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
8334
8334
|
default: return null;
|
|
8335
8335
|
}
|
|
8336
8336
|
}
|
|
8337
|
+
/** The shells whose update has its own sentence. Anything else gets the plain
|
|
8338
|
+
* one: a body that names a shell nobody observed is worse than a vague body. */
|
|
8339
|
+
var WRAPPER_BODY_KINDS = new Set([
|
|
8340
|
+
"docker",
|
|
8341
|
+
"electron",
|
|
8342
|
+
"native"
|
|
8343
|
+
]);
|
|
8344
|
+
/**
|
|
8345
|
+
* The SHELL announcement (D437) — the container / app bundle a node runs
|
|
8346
|
+
* inside is behind the release contract.
|
|
8347
|
+
*
|
|
8348
|
+
* It is deliberately NOT folded into `server-update-available`: the versions
|
|
8349
|
+
* here are the shell's (a seed closure, an app bundle), not the running code's,
|
|
8350
|
+
* and on 2026-08-02 the running code was perfectly current on a hub whose image
|
|
8351
|
+
* was 13 days old from a repository nothing rebuilt. It also mints no button —
|
|
8352
|
+
* no process can replace the shell it is running inside — so the BODY is the
|
|
8353
|
+
* whole delivery, and it is chosen by `wrapperKind` so a docker operator is
|
|
8354
|
+
* handed the exact command instead of a button that cannot exist.
|
|
8355
|
+
*/
|
|
8356
|
+
function incomingFromWrapperUpdate(facts, data) {
|
|
8357
|
+
const packages = updatePackages(data);
|
|
8358
|
+
if (packages.length === 0) return null;
|
|
8359
|
+
const head = packages[0];
|
|
8360
|
+
const nodeId = head.nodeId ?? stringField(data, "nodeId");
|
|
8361
|
+
const wrapperKind = stringField(data, "wrapperKind");
|
|
8362
|
+
const bodySuffix = wrapperKind !== void 0 && WRAPPER_BODY_KINDS.has(wrapperKind) ? wrapperKind : null;
|
|
8363
|
+
return systemSubject({
|
|
8364
|
+
facts,
|
|
8365
|
+
kind: "wrapper-update-available",
|
|
8366
|
+
subject: `wrapper:${nodeId ?? "hub"}`,
|
|
8367
|
+
titleKey: "system.wrapper-update-available.title",
|
|
8368
|
+
bodyKey: bodySuffix === null ? "system.wrapper-update-available.body" : `system.wrapper-update-available.body.${bodySuffix}`,
|
|
8369
|
+
textParams: {
|
|
8370
|
+
currentVersion: head.currentVersion,
|
|
8371
|
+
latestVersion: head.latestVersion,
|
|
8372
|
+
...nodeId !== void 0 ? { nodeId } : {}
|
|
8373
|
+
},
|
|
8374
|
+
textCount: 1,
|
|
8375
|
+
packageName: head.packageName,
|
|
8376
|
+
packageNames: [head.packageName],
|
|
8377
|
+
...nodeId !== void 0 ? {
|
|
8378
|
+
nodeId,
|
|
8379
|
+
nodeIds: [nodeId]
|
|
8380
|
+
} : {}
|
|
8381
|
+
});
|
|
8382
|
+
}
|
|
8337
8383
|
function incomingFromUpdateAvailable(facts, data) {
|
|
8338
8384
|
const target = stringField(data, "target");
|
|
8385
|
+
if (target === "wrapper") return incomingFromWrapperUpdate(facts, data);
|
|
8339
8386
|
if (target !== "addon" && target !== "server") return null;
|
|
8340
8387
|
const packages = updatePackages(data);
|
|
8341
8388
|
if (packages.length === 0) return null;
|
|
@@ -9445,7 +9492,7 @@ var CAMERA_DEVICE_TYPE = "camera";
|
|
|
9445
9492
|
/** Kinds whose action is "go and apply an ADDON update". */
|
|
9446
9493
|
var ADDON_UPDATE_KINDS = new Set(["addon-update-available"]);
|
|
9447
9494
|
/** Kinds whose action is "go and apply a NODE's own (`@camstack/server`) update". */
|
|
9448
|
-
var NODE_UPDATE_KINDS = new Set(["server-update-available"]);
|
|
9495
|
+
var NODE_UPDATE_KINDS = new Set(["server-update-available", "wrapper-update-available"]);
|
|
9449
9496
|
/**
|
|
9450
9497
|
* Kinds whose action is "go and look at this device's stream".
|
|
9451
9498
|
*
|
|
@@ -10010,6 +10057,11 @@ var en_default = {
|
|
|
10010
10057
|
"system.server-update-available.body": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}})",
|
|
10011
10058
|
"system.server-update-available.body.onNode": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}}) on node {{nodeId}}",
|
|
10012
10059
|
"system.server-update-available.body.onNodes": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}}) on {{nodes}}",
|
|
10060
|
+
"system.wrapper-update-available.title": "The container or app on {{nodeId}} is out of date",
|
|
10061
|
+
"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.",
|
|
10062
|
+
"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",
|
|
10063
|
+
"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.",
|
|
10064
|
+
"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.",
|
|
10013
10065
|
"system.addon-updated.title": {
|
|
10014
10066
|
"one": "{{count}} addon updated",
|
|
10015
10067
|
"other": "{{count}} addons updated"
|
|
@@ -10265,6 +10317,11 @@ var it_default = {
|
|
|
10265
10317
|
"system.server-update-available.body": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}})",
|
|
10266
10318
|
"system.server-update-available.body.onNode": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}}) sul nodo {{nodeId}}",
|
|
10267
10319
|
"system.server-update-available.body.onNodes": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}}) su {{nodes}}",
|
|
10320
|
+
"system.wrapper-update-available.title": "Il contenitore o l'app di {{nodeId}} è indietro",
|
|
10321
|
+
"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.",
|
|
10322
|
+
"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",
|
|
10323
|
+
"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.",
|
|
10324
|
+
"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.",
|
|
10268
10325
|
"system.addon-updated.title": {
|
|
10269
10326
|
"one": "{{count}} addon aggiornato",
|
|
10270
10327
|
"other": "{{count}} addon aggiornati"
|
|
@@ -25802,6 +25859,7 @@ function systemEventSubject(kind, scope) {
|
|
|
25802
25859
|
if (kind === "detection-blind") return `detection-blind:${scope.deviceId ?? 0}`;
|
|
25803
25860
|
if (kind === "addon-crash-loop") return `addon-crash:${scope.nodeId ?? "hub"}:${TEST_CRASHED_RUNNER}`;
|
|
25804
25861
|
if (kind === "addon-update-available" || kind === "server-update-available") return `package:${scope.packageName ?? "@camstack/server"}`;
|
|
25862
|
+
if (kind === "wrapper-update-available") return `wrapper:${scope.nodeId ?? "hub"}`;
|
|
25805
25863
|
if (kind === "addon-updated") return `packages:addon-updated:${scope.packageName ?? "pkg"}`;
|
|
25806
25864
|
if (kind === "server-updated") return `server-updated:${scope.nodeId ?? "hub"}`;
|
|
25807
25865
|
if (kind === "export-completed") return `export:${scope.deviceId ?? 0}`;
|
|
@@ -33403,7 +33461,12 @@ var FaceGalleryProvider = class {
|
|
|
33403
33461
|
identityId: input.identityId
|
|
33404
33462
|
});
|
|
33405
33463
|
assigned += 1;
|
|
33406
|
-
} catch {
|
|
33464
|
+
} catch (err) {
|
|
33465
|
+
this.logger.error("assignFaces refused a face", { meta: {
|
|
33466
|
+
faceId,
|
|
33467
|
+
identityId: input.identityId,
|
|
33468
|
+
error: err instanceof Error ? `${err.name}: ${err.message}` : String(err)
|
|
33469
|
+
} });
|
|
33407
33470
|
failed.push(faceId);
|
|
33408
33471
|
}
|
|
33409
33472
|
return {
|
|
@@ -34785,6 +34848,21 @@ var SpawnBesideCoastingSampler = class {
|
|
|
34785
34848
|
this.counts.clear();
|
|
34786
34849
|
}
|
|
34787
34850
|
};
|
|
34851
|
+
/** Per-cause counter + sampling decision. One instance per tracker, so the
|
|
34852
|
+
* counts are per camera and per detection source. */
|
|
34853
|
+
var TrackLossSampler = class {
|
|
34854
|
+
counts = /* @__PURE__ */ new Map();
|
|
34855
|
+
/** The running count when this loss should be emitted, else `undefined`. */
|
|
34856
|
+
next(cause) {
|
|
34857
|
+
const count = (this.counts.get(cause) ?? 0) + 1;
|
|
34858
|
+
this.counts.set(cause, count);
|
|
34859
|
+
if (count <= 10) return count;
|
|
34860
|
+
return count % 50 === 0 ? count : void 0;
|
|
34861
|
+
}
|
|
34862
|
+
reset() {
|
|
34863
|
+
this.counts.clear();
|
|
34864
|
+
}
|
|
34865
|
+
};
|
|
34788
34866
|
/** Per-reason counter + sampling decision. One instance per tracker, so the
|
|
34789
34867
|
* counts are per camera and per detection source. */
|
|
34790
34868
|
var SpawnRefusalSampler = class {
|
|
@@ -34893,6 +34971,16 @@ var SortTracker = class SortTracker {
|
|
|
34893
34971
|
refusalSink = null;
|
|
34894
34972
|
/** Per-reason counters behind the sampled spawn-beside-coasting diagnostics. */
|
|
34895
34973
|
spawnBesideSampler = new SpawnBesideCoastingSampler();
|
|
34974
|
+
/**
|
|
34975
|
+
* Deliveries to {@link update} since this tracker was constructed. One per
|
|
34976
|
+
* frame the pipeline actually hands over — which is exactly what "the camera
|
|
34977
|
+
* was looking" means, and is NOT derivable from wall clock: a quiet camera
|
|
34978
|
+
* and a busy one span the same seconds (D438).
|
|
34979
|
+
*/
|
|
34980
|
+
frameSeq = 0;
|
|
34981
|
+
/** Sink for track deaths — see {@link setTrackLossObserver}. */
|
|
34982
|
+
trackLossObserver = null;
|
|
34983
|
+
trackLossSampler = new TrackLossSampler();
|
|
34896
34984
|
/** Sink for fragmentation diagnostics — see {@link setSpawnBesideCoastingObserver}. */
|
|
34897
34985
|
spawnBesideObserver = null;
|
|
34898
34986
|
constructor(config = {}) {
|
|
@@ -34925,6 +35013,13 @@ var SortTracker = class SortTracker {
|
|
|
34925
35013
|
* Same contract as the other two sinks — the tracker has no `deviceId`, the
|
|
34926
35014
|
* caller stamps it, and nothing is computed when no observer is wired.
|
|
34927
35015
|
*/
|
|
35016
|
+
/**
|
|
35017
|
+
* Wire the sink for {@link TrackLossRecord}. Nothing is computed when no
|
|
35018
|
+
* observer is attached.
|
|
35019
|
+
*/
|
|
35020
|
+
setTrackLossObserver(observer) {
|
|
35021
|
+
this.trackLossObserver = observer;
|
|
35022
|
+
}
|
|
34928
35023
|
setSpawnBesideCoastingObserver(observer) {
|
|
34929
35024
|
this.spawnBesideObserver = observer;
|
|
34930
35025
|
}
|
|
@@ -34939,13 +35034,13 @@ var SortTracker = class SortTracker {
|
|
|
34939
35034
|
reportSpawnBesideCoasting(spawnedId, det, coasting, detections, frameArea, timestamp) {
|
|
34940
35035
|
const observer = this.spawnBesideObserver;
|
|
34941
35036
|
if (!observer) return;
|
|
34942
|
-
const
|
|
35037
|
+
const family = SortTracker.spawnBesideFamily(det.class);
|
|
34943
35038
|
const dc = bboxCentroid(det.bbox);
|
|
34944
35039
|
let best;
|
|
34945
35040
|
let bestDist = Number.POSITIVE_INFINITY;
|
|
34946
35041
|
let bestReach = 0;
|
|
34947
35042
|
for (const track of coasting) {
|
|
34948
|
-
if (
|
|
35043
|
+
if (SortTracker.spawnBesideFamily(track.class) !== family) continue;
|
|
34949
35044
|
const tc = bboxCentroid(track.bbox);
|
|
34950
35045
|
const dist = Math.hypot(tc.x - dc.x, tc.y - dc.y);
|
|
34951
35046
|
const reach = this.reacquireReach(track, timestamp, true);
|
|
@@ -34960,6 +35055,7 @@ var SortTracker = class SortTracker {
|
|
|
34960
35055
|
const declinedBy = this.explainDecline(best, det, detections, frameArea, timestamp, bestDist);
|
|
34961
35056
|
const spawnCount = this.spawnBesideSampler.next(declinedBy);
|
|
34962
35057
|
if (spawnCount === void 0) return;
|
|
35058
|
+
const crowd = declinedBy === "crowd-guard" ? this.crowdAround(best, det, detections) : void 0;
|
|
34963
35059
|
observer({
|
|
34964
35060
|
spawnedTrackId: spawnedId,
|
|
34965
35061
|
spawnedClass: det.class,
|
|
@@ -34976,6 +35072,8 @@ var SortTracker = class SortTracker {
|
|
|
34976
35072
|
gapMs: timestamp - best.lastSeen,
|
|
34977
35073
|
trackAgeMs: timestamp - best.firstSeen,
|
|
34978
35074
|
declinedBy,
|
|
35075
|
+
...crowd === void 0 ? {} : { competitorCount: crowd.count },
|
|
35076
|
+
...crowd?.nearestPx === void 0 ? {} : { nearestCompetitorPx: crowd.nearestPx },
|
|
34979
35077
|
spawnCount,
|
|
34980
35078
|
sampledEvery: 10
|
|
34981
35079
|
});
|
|
@@ -34990,8 +35088,7 @@ var SortTracker = class SortTracker {
|
|
|
34990
35088
|
* `already-claimed` means.
|
|
34991
35089
|
*/
|
|
34992
35090
|
explainDecline(track, det, detections, frameArea, timestamp, centroidDist) {
|
|
34993
|
-
if (
|
|
34994
|
-
if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return "assoc-group";
|
|
35091
|
+
if (track.class !== det.class) return det.nativeRecovery !== void 0 ? "native-measured-class" : "cross-class";
|
|
34995
35092
|
if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "already-claimed";
|
|
34996
35093
|
if (centroidDist <= this.reacquireReach(track, timestamp, true)) {
|
|
34997
35094
|
if (!this.isPlausibleBirth(det, frameArea)) return "already-claimed";
|
|
@@ -35050,14 +35147,44 @@ var SortTracker = class SortTracker {
|
|
|
35050
35147
|
* come back — a graveyarded track may still resurrect, and a line about work
|
|
35051
35148
|
* that was not lost is worse than no line.
|
|
35052
35149
|
*/
|
|
35150
|
+
/**
|
|
35151
|
+
* Report one track that ended with nothing taking over (D438).
|
|
35152
|
+
*
|
|
35153
|
+
* Deliberately separate from {@link reportTrackLoss}, which speaks only for
|
|
35154
|
+
* spawns that never reached `minHits`. The population under investigation —
|
|
35155
|
+
* animal tracks dying mid-frame after six frames — had all EMITTED, so that
|
|
35156
|
+
* path never saw a single one of them.
|
|
35157
|
+
*
|
|
35158
|
+
* `timestamp` is the frame doing the sweeping, not the track's own clock: the
|
|
35159
|
+
* question is how much passed while nobody was looking.
|
|
35160
|
+
*/
|
|
35161
|
+
reportTrackDeath(track, cause, timestamp) {
|
|
35162
|
+
const observer = this.trackLossObserver;
|
|
35163
|
+
if (!observer) return;
|
|
35164
|
+
const lossCount = this.trackLossSampler.next(cause);
|
|
35165
|
+
if (lossCount === void 0) return;
|
|
35166
|
+
observer({
|
|
35167
|
+
trackId: track.id,
|
|
35168
|
+
trackClass: track.class,
|
|
35169
|
+
cause,
|
|
35170
|
+
framesSinceLastSeen: this.frameSeq - track.lastSeenFrameSeq,
|
|
35171
|
+
msSinceLastSeen: timestamp - track.lastSeen,
|
|
35172
|
+
lifeMs: track.lastSeen - track.firstSeen,
|
|
35173
|
+
positions: track.hits,
|
|
35174
|
+
emitted: track.emitted,
|
|
35175
|
+
lossCount,
|
|
35176
|
+
sampledEvery: 50
|
|
35177
|
+
});
|
|
35178
|
+
}
|
|
35053
35179
|
reportTrackLoss(track) {
|
|
35054
35180
|
if (track.emitted || track.lossReported) return;
|
|
35055
35181
|
track.lossReported = true;
|
|
35056
35182
|
this.reportRefusal("unconfirmed-spawn-lost", {
|
|
35057
|
-
class: track.
|
|
35183
|
+
class: track.frameClass,
|
|
35058
35184
|
originalClass: track.originalClass,
|
|
35059
35185
|
score: track.score,
|
|
35060
|
-
bbox: track.bbox
|
|
35186
|
+
bbox: track.bbox,
|
|
35187
|
+
...track.lastNativeRecovery !== void 0 ? { nativeRecovery: track.lastNativeRecovery } : {}
|
|
35061
35188
|
}, {
|
|
35062
35189
|
threshold: this.config.minHits,
|
|
35063
35190
|
measured: track.hits,
|
|
@@ -35112,43 +35239,73 @@ var SortTracker = class SortTracker {
|
|
|
35112
35239
|
static pairKey(a, b) {
|
|
35113
35240
|
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
35114
35241
|
}
|
|
35115
|
-
/**
|
|
35116
|
-
|
|
35117
|
-
|
|
35118
|
-
|
|
35119
|
-
|
|
35120
|
-
|
|
35242
|
+
/** The two macro-classes a coarse pass confuses on a non-upright person. */
|
|
35243
|
+
static isPersonAnimalPair(a, b) {
|
|
35244
|
+
return a === PERSON_CLASS && b === ANIMAL_CLASS || a === ANIMAL_CLASS && b === PERSON_CLASS;
|
|
35245
|
+
}
|
|
35246
|
+
/**
|
|
35247
|
+
* The family a spawn-beside-coasting line is reported AGAINST — {person,
|
|
35248
|
+
* animal} as one, every other class its own. Reporting only, never a binding
|
|
35249
|
+
* rule: a cross-class pair is exactly what that line exists to name (the
|
|
35250
|
+
* D378 veto, the D439 salvage rule), so a pair the tracker refused must still
|
|
35251
|
+
* be reportable. Independent of `classGroupAssoc` for the same reason.
|
|
35252
|
+
*/
|
|
35253
|
+
static spawnBesideFamily(cls) {
|
|
35254
|
+
if (cls === PERSON_CLASS || cls === ANIMAL_CLASS) return `${PERSON_CLASS}|${ANIMAL_CLASS}`;
|
|
35121
35255
|
return cls;
|
|
35122
35256
|
}
|
|
35123
|
-
/**
|
|
35124
|
-
*
|
|
35125
|
-
*
|
|
35126
|
-
*
|
|
35257
|
+
/**
|
|
35258
|
+
* A CROSS-CLASS BIND IS A FLIP CLAIM, AND A FLIP IS WEAK (D439).
|
|
35259
|
+
*
|
|
35260
|
+
* `classGroupAssoc` exists for one failure of the COARSE full-frame pass: a
|
|
35261
|
+
* bending person re-labelled `animal` for a frame, over the person's own
|
|
35262
|
+
* box, at the 0.4-0.6 the coarse pass gives a shape it half recognises. That
|
|
35263
|
+
* claim has three parts, and this predicate is all three:
|
|
35264
|
+
*
|
|
35265
|
+
* - the box was measured by the coarse pass — never `nativeRecovery`. The
|
|
35266
|
+
* runner's second pass measures the OPPOSITE thing: a 640-800 px native
|
|
35267
|
+
* square around a motion region the full-frame boxes do NOT account for,
|
|
35268
|
+
* which is by construction not the subject the full pass already has.
|
|
35269
|
+
* 2026-09-07 07:51:33, an `animal` at `nativeRecoveryScore=0.833`
|
|
35270
|
+
* absorbed into a person: the D378 half of this rule;
|
|
35271
|
+
* - it is a {person, animal} pair;
|
|
35272
|
+
* - it is WEAK — below {@link TrackerConfig.animalOverPersonMaxScore}, the
|
|
35273
|
+
* same bar the spawn gate uses for the same claim. A `person` at 0.86 or
|
|
35274
|
+
* an `animal` at 0.92 is not a half-recognised shape, it is a subject,
|
|
35275
|
+
* and binding it to the other class's track hands one subject's id to
|
|
35276
|
+
* another (camera 615, 17:11:01: person 0.8616 → animal track
|
|
35277
|
+
* `a28c1557`, which was then born as a person).
|
|
35278
|
+
*
|
|
35279
|
+
* Where the caller may use it is the fourth part, and it is the caller's:
|
|
35280
|
+
* only the predicted-box passes ({@link greedyAssociate}), where "same place"
|
|
35281
|
+
* is measured against a prediction on the strict `iouThreshold`. The salvage
|
|
35282
|
+
* passes ({@link looseMatch}) never cross classes — their whole premise is
|
|
35283
|
+
* weak overlap, and a weak-overlap cross-class bind is a dog beside its
|
|
35284
|
+
* owner, every time.
|
|
35285
|
+
*/
|
|
35286
|
+
crossClassFlip(trackClass, det) {
|
|
35287
|
+
if (!this.config.classGroupAssoc) return false;
|
|
35288
|
+
if (det.nativeRecovery !== void 0) return false;
|
|
35289
|
+
if (!SortTracker.isPersonAnimalPair(trackClass, det.class)) return false;
|
|
35290
|
+
return det.score < this.config.animalOverPersonMaxScore;
|
|
35291
|
+
}
|
|
35292
|
+
/** Whether a detection may associate to a track on a PREDICTED-BOX pass:
|
|
35293
|
+
* its own class, or — with `classGroupAssoc` — a weak coarse flip of the
|
|
35294
|
+
* {person, animal} pair ({@link crossClassFlip}). `classGating` off means no
|
|
35295
|
+
* gate at all, matching the legacy contract, except that a native-measured
|
|
35296
|
+
* box still binds only its own class (D378). */
|
|
35127
35297
|
associable(trackClass, det) {
|
|
35128
|
-
if (
|
|
35298
|
+
if (trackClass === det.class) return true;
|
|
35299
|
+
if (det.nativeRecovery !== void 0) return false;
|
|
35129
35300
|
if (!this.config.classGating) return true;
|
|
35130
|
-
return this.
|
|
35301
|
+
return this.crossClassFlip(trackClass, det);
|
|
35131
35302
|
}
|
|
35132
35303
|
/**
|
|
35133
|
-
* A NATIVE-MEASURED BOX IS ITS OWN SUBJECT ACROSS CLASSES (D378)
|
|
35134
|
-
*
|
|
35135
|
-
*
|
|
35136
|
-
*
|
|
35137
|
-
*
|
|
35138
|
-
* the person track is strictly better than minting a concurrent id.
|
|
35139
|
-
*
|
|
35140
|
-
* The runner's second pass makes the opposite measurement — the same detector
|
|
35141
|
-
* on a 640-800 px native square around a motion region the full-frame boxes
|
|
35142
|
-
* do NOT account for. Such a box is, by construction, not the subject the
|
|
35143
|
-
* full pass already has; absorbing it into a person track means the animal is
|
|
35144
|
-
* never seen at all, which on 2026-09-07 07:51:33 is exactly what happened to
|
|
35145
|
-
* an `animal` at `nativeRecoveryScore=0.833`.
|
|
35146
|
-
*
|
|
35147
|
-
* So the widening does not apply to a detection carrying `nativeRecovery`:
|
|
35148
|
-
* it associates to its OWN class or it spawns. Every other guard on that
|
|
35149
|
-
* spawn still runs — the per-class score floor, the area floor, the
|
|
35150
|
-
* `dedupSpawnIou` overlap, and `resolveDuplicates` afterwards, where a person
|
|
35151
|
-
* still beats a cross-class animal.
|
|
35304
|
+
* A NATIVE-MEASURED BOX IS ITS OWN SUBJECT ACROSS CLASSES (D378) — the
|
|
35305
|
+
* occlusion gate's copy of the first clause of {@link crossClassFlip}. A
|
|
35306
|
+
* cross-class occluder is not a same-subject claim for a box the native
|
|
35307
|
+
* pass measured: a dog at its owner's knee is 100% contained in the person's
|
|
35308
|
+
* box and is not the person.
|
|
35152
35309
|
*/
|
|
35153
35310
|
sameGroupAsMeasured(trackClass, det) {
|
|
35154
35311
|
if (det.nativeRecovery === void 0) return true;
|
|
@@ -35321,9 +35478,16 @@ var SortTracker = class SortTracker {
|
|
|
35321
35478
|
};
|
|
35322
35479
|
}
|
|
35323
35480
|
/**
|
|
35324
|
-
* How many
|
|
35325
|
-
*
|
|
35326
|
-
*
|
|
35481
|
+
* How many SAME-CLASS subjects OTHER than `corpse` share the scene with it:
|
|
35482
|
+
* every track LIVE on this frame, plus any other resurrectable corpse whose
|
|
35483
|
+
* lifetime OVERLAPPED the candidate's.
|
|
35484
|
+
*
|
|
35485
|
+
* Exact class, not the {person, animal} family (D439): a competitor is a
|
|
35486
|
+
* subject the id could actually be handed to, and since a salvage pass binds
|
|
35487
|
+
* its own class only, a person cannot take a dog's id there. Counting the
|
|
35488
|
+
* owner as the dog's competitor split one dog's id 12 times on camera 615 in
|
|
35489
|
+
* 6.6 h — every bind the reach clause accepted, the guard then refused
|
|
35490
|
+
* because the person was on screen.
|
|
35327
35491
|
*
|
|
35328
35492
|
* The overlap clause on the graveyard half is what keeps this guard off the
|
|
35329
35493
|
* case the resurrection pass exists for. A single flickering subject leaves a
|
|
@@ -35337,30 +35501,30 @@ var SortTracker = class SortTracker {
|
|
|
35337
35501
|
* steals an id is very often one whose OWN track was born after the corpse
|
|
35338
35502
|
* died (camera 618 — the toddler's track post-dates the older boy's).
|
|
35339
35503
|
*/
|
|
35340
|
-
|
|
35341
|
-
const group = this.assocGroup(corpse.class);
|
|
35504
|
+
sameClassPeerCount(corpse, live) {
|
|
35342
35505
|
let peers = 0;
|
|
35343
|
-
for (const t of live) if (t !== corpse &&
|
|
35506
|
+
for (const t of live) if (t !== corpse && t.class === corpse.class) peers++;
|
|
35344
35507
|
for (const other of this.lostTracks) {
|
|
35345
35508
|
if (other === corpse) continue;
|
|
35346
35509
|
if (!other.resurrectable) continue;
|
|
35347
|
-
if (
|
|
35510
|
+
if (other.class !== corpse.class) continue;
|
|
35348
35511
|
if (other.firstSeen <= corpse.lastSeen && corpse.firstSeen <= other.lastSeen) peers++;
|
|
35349
35512
|
}
|
|
35350
35513
|
return peers;
|
|
35351
35514
|
}
|
|
35352
35515
|
/**
|
|
35353
|
-
* Loose
|
|
35354
|
-
*
|
|
35355
|
-
*
|
|
35356
|
-
*
|
|
35357
|
-
*
|
|
35358
|
-
*
|
|
35359
|
-
*
|
|
35516
|
+
* Loose gate used by the rescue and resurrection passes: accept when the
|
|
35517
|
+
* detection overlaps the track's LAST-KNOWN bbox by `rescueIou`, or its
|
|
35518
|
+
* centroid is within reach. EXACT-class, always, independent of
|
|
35519
|
+
* `classGating` and of `classGroupAssoc` (D439): a salvage pass binds on
|
|
35520
|
+
* weak overlap by definition, and a weak-overlap bind across {person,
|
|
35521
|
+
* animal} is not a crouch flip — a flip sits on the subject's own box — it
|
|
35522
|
+
* is a dog beside its owner, or the owner beside the dog. Until 2026-09-10
|
|
35523
|
+
* this gate crossed the group, and it is where a `person` at 0.86 took over
|
|
35524
|
+
* an animal's id (camera 615, 17:11:01).
|
|
35360
35525
|
*/
|
|
35361
35526
|
looseMatch(track, det, timestamp, timeScaled = true) {
|
|
35362
|
-
if (
|
|
35363
|
-
if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return null;
|
|
35527
|
+
if (track.class !== det.class) return null;
|
|
35364
35528
|
if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
|
|
35365
35529
|
const tc = bboxCentroid(track.bbox);
|
|
35366
35530
|
const dc = bboxCentroid(det.bbox);
|
|
@@ -35406,12 +35570,41 @@ var SortTracker = class SortTracker {
|
|
|
35406
35570
|
* bind time and at report time — the spawn-beside-coasting line has to be able
|
|
35407
35571
|
* to say which gate declined without re-running the greedy competition.
|
|
35408
35572
|
*/
|
|
35573
|
+
/**
|
|
35574
|
+
* WHO the crowd was, for the record only — this changes no decision.
|
|
35575
|
+
*
|
|
35576
|
+
* {@link isUnambiguousReach} refuses on PRESENCE, with no distance in the
|
|
35577
|
+
* question, so a person at the far corner of a room splits a track as surely
|
|
35578
|
+
* as one standing beside it. Counting them, and measuring the nearest, is
|
|
35579
|
+
* what makes narrowing that rule a decision rather than a guess (D436).
|
|
35580
|
+
*/
|
|
35581
|
+
crowdAround(track, det, detections) {
|
|
35582
|
+
const cx = det.bbox.x + det.bbox.w / 2;
|
|
35583
|
+
const cy = det.bbox.y + det.bbox.h / 2;
|
|
35584
|
+
const counted = [];
|
|
35585
|
+
let nearest;
|
|
35586
|
+
const consider = (bbox) => {
|
|
35587
|
+
counted.push(bbox);
|
|
35588
|
+
const d = Math.hypot(bbox.x + bbox.w / 2 - cx, bbox.y + bbox.h / 2 - cy);
|
|
35589
|
+
if (nearest === void 0 || d < nearest) nearest = d;
|
|
35590
|
+
};
|
|
35591
|
+
for (const t of this.tracks) if (t !== track && t.class === track.class) consider(t.bbox);
|
|
35592
|
+
for (const other of detections) {
|
|
35593
|
+
if (other === det) continue;
|
|
35594
|
+
if (other.class !== track.class) continue;
|
|
35595
|
+
if (counted.some((b) => iou$3(b, other.bbox) > 0)) continue;
|
|
35596
|
+
consider(other.bbox);
|
|
35597
|
+
}
|
|
35598
|
+
return {
|
|
35599
|
+
count: counted.length,
|
|
35600
|
+
nearestPx: nearest
|
|
35601
|
+
};
|
|
35602
|
+
}
|
|
35409
35603
|
isUnambiguousReach(track, det, detections) {
|
|
35410
|
-
if (this.
|
|
35411
|
-
const group = this.assocGroup(track.class);
|
|
35604
|
+
if (this.sameClassPeerCount(track, this.tracks) > 0) return false;
|
|
35412
35605
|
for (const other of detections) {
|
|
35413
35606
|
if (other === det) continue;
|
|
35414
|
-
if (
|
|
35607
|
+
if (other.class === track.class) return false;
|
|
35415
35608
|
}
|
|
35416
35609
|
return true;
|
|
35417
35610
|
}
|
|
@@ -35448,8 +35641,8 @@ var SortTracker = class SortTracker {
|
|
|
35448
35641
|
* weight; otherwise the latest-frame class (ambiguous vote or voting off).
|
|
35449
35642
|
*/
|
|
35450
35643
|
resolveReportedClass(t) {
|
|
35451
|
-
if (!this.config.classVotingEnabled || t.classVotes.size === 0) return t.
|
|
35452
|
-
let bestClass = t.
|
|
35644
|
+
if (!this.config.classVotingEnabled || t.classVotes.size === 0) return t.frameClass;
|
|
35645
|
+
let bestClass = t.frameClass;
|
|
35453
35646
|
let bestVote = -1;
|
|
35454
35647
|
let total = 0;
|
|
35455
35648
|
for (const [cls, v] of t.classVotes) {
|
|
@@ -35459,8 +35652,8 @@ var SortTracker = class SortTracker {
|
|
|
35459
35652
|
bestClass = cls;
|
|
35460
35653
|
}
|
|
35461
35654
|
}
|
|
35462
|
-
if (total <= 0) return t.
|
|
35463
|
-
return bestVote / total >= this.config.classVoteMinFraction ? bestClass : t.
|
|
35655
|
+
if (total <= 0) return t.frameClass;
|
|
35656
|
+
return bestVote / total >= this.config.classVoteMinFraction ? bestClass : t.frameClass;
|
|
35464
35657
|
}
|
|
35465
35658
|
/** Whether a detection clears its per-class spawn score floor. Only gates NEW
|
|
35466
35659
|
* spawns — an established track still matches below its floor. */
|
|
@@ -35476,16 +35669,67 @@ var SortTracker = class SortTracker {
|
|
|
35476
35669
|
return this.config.confirmBypassEnabled && t.score >= this.config.confirmBypassScore;
|
|
35477
35670
|
}
|
|
35478
35671
|
/**
|
|
35672
|
+
* The live tracks split into the two tiers that claim a box in a fixed
|
|
35673
|
+
* order (D442, see {@link update}): those already confirmed for emission,
|
|
35674
|
+
* and the provisional ones still under `minHits`. Decided on the state at
|
|
35675
|
+
* the start of the frame, as ByteTrack does. With `minHits: 1` the second
|
|
35676
|
+
* tier is always empty and every pass over it is a no-op.
|
|
35677
|
+
*/
|
|
35678
|
+
associationTiers() {
|
|
35679
|
+
const established = [];
|
|
35680
|
+
const provisional = [];
|
|
35681
|
+
for (const track of this.tracks) if (this.isConfirmedForEmit(track)) established.push(track);
|
|
35682
|
+
else provisional.push(track);
|
|
35683
|
+
return [established, provisional];
|
|
35684
|
+
}
|
|
35685
|
+
/**
|
|
35686
|
+
* The rescue pass. HIGH-score detections the greedy pass left unmatched get
|
|
35687
|
+
* a second, looser chance against the still-unmatched (coasting) tracks of
|
|
35688
|
+
* `tracks` — measured on the track's LAST-KNOWN bbox, never the drifted
|
|
35689
|
+
* prediction. Recovers half-box re-acquisition and small-drift flicker
|
|
35690
|
+
* without spawning a duplicate. Best (highest last-known IoU) pairs first;
|
|
35691
|
+
* LOW detections are reserved for the ByteTrack low stage.
|
|
35692
|
+
*/
|
|
35693
|
+
rescueAssociate(tracks, gates, detections, lowSet, frameArea, timestamp, used, matchedTracks, matched, continuationOnly) {
|
|
35694
|
+
const rescuePairs = [];
|
|
35695
|
+
for (const track of tracks) {
|
|
35696
|
+
if (matchedTracks.has(track)) continue;
|
|
35697
|
+
for (let di = 0; di < detections.length; di++) {
|
|
35698
|
+
if (used.has(di) || lowSet.has(di) || continuationOnly.has(di)) continue;
|
|
35699
|
+
const det = detections[di];
|
|
35700
|
+
const gate = this.looseMatch(track, det, timestamp);
|
|
35701
|
+
if (gate === null || !gates.has(gate)) continue;
|
|
35702
|
+
if (gate === "centroid" && this.isPlausibleBirth(det, frameArea) && !(this.config.reacquireTimeScaled && this.isUnambiguousReach(track, det, detections))) continue;
|
|
35703
|
+
rescuePairs.push({
|
|
35704
|
+
track,
|
|
35705
|
+
detIdx: di,
|
|
35706
|
+
score: iou$3(track.bbox, det.bbox),
|
|
35707
|
+
gate
|
|
35708
|
+
});
|
|
35709
|
+
}
|
|
35710
|
+
}
|
|
35711
|
+
rescuePairs.sort((a, b) => b.score - a.score);
|
|
35712
|
+
for (const pair of rescuePairs) {
|
|
35713
|
+
if (matchedTracks.has(pair.track) || used.has(pair.detIdx)) continue;
|
|
35714
|
+
const det = detections[pair.detIdx];
|
|
35715
|
+
matched.set(pair.track, det);
|
|
35716
|
+
matchedTracks.add(pair.track);
|
|
35717
|
+
used.add(pair.detIdx);
|
|
35718
|
+
this.reportBind("rescue", pair.gate, pair.track, det, timestamp);
|
|
35719
|
+
}
|
|
35720
|
+
}
|
|
35721
|
+
/**
|
|
35479
35722
|
* One greedy IoU association pass over a candidate detection subset against the
|
|
35480
|
-
*
|
|
35723
|
+
* still-UNMATCHED tracks of `tracks` (one association tier — see
|
|
35724
|
+
* {@link associationTiers}), on each track's PREDICTED box. Highest-IoU pairs
|
|
35481
35725
|
* bind first; a track/detection binds at most once. Shared by the high-score
|
|
35482
35726
|
* first stage and the low-score second stage (different candidate sets + IoU
|
|
35483
35727
|
* gates) so the two-tier ByteTrack association stays a single implementation.
|
|
35484
35728
|
* Mutates `used` / `matchedTracks` / `matched` in place.
|
|
35485
35729
|
*/
|
|
35486
|
-
greedyAssociate(detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly) {
|
|
35730
|
+
greedyAssociate(tracks, detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly) {
|
|
35487
35731
|
const pairs = [];
|
|
35488
|
-
for (const track of
|
|
35732
|
+
for (const track of tracks) {
|
|
35489
35733
|
if (matchedTracks.has(track)) continue;
|
|
35490
35734
|
const pbox = this.predicted(track, timestamp);
|
|
35491
35735
|
const confirmed = this.isConfirmedForEmit(track);
|
|
@@ -35511,6 +35755,7 @@ var SortTracker = class SortTracker {
|
|
|
35511
35755
|
}
|
|
35512
35756
|
}
|
|
35513
35757
|
update(detections, timestamp, frameContext) {
|
|
35758
|
+
this.frameSeq++;
|
|
35514
35759
|
const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
|
|
35515
35760
|
const continuationOnly = frameContext?.continuationOnly ?? EMPTY_INDEX_SET;
|
|
35516
35761
|
const resurrectedScene = /* @__PURE__ */ new Map();
|
|
@@ -35521,6 +35766,7 @@ var SortTracker = class SortTracker {
|
|
|
35521
35766
|
track.lostAt = timestamp;
|
|
35522
35767
|
track.resurrectable = false;
|
|
35523
35768
|
this.reportTrackLoss(track);
|
|
35769
|
+
this.reportTrackDeath(track, "lifetime-cap", timestamp);
|
|
35524
35770
|
this.lostTracks.push(track);
|
|
35525
35771
|
} else alive.push(track);
|
|
35526
35772
|
this.tracks = alive;
|
|
@@ -35533,13 +35779,17 @@ var SortTracker = class SortTracker {
|
|
|
35533
35779
|
track.lostAt = track.lastSeen;
|
|
35534
35780
|
track.resurrectable = false;
|
|
35535
35781
|
this.reportTrackLoss(track);
|
|
35782
|
+
this.reportTrackDeath(track, "stale-beyond-budgets", timestamp);
|
|
35536
35783
|
this.lostTracks.push(track);
|
|
35537
35784
|
} else alive.push(track);
|
|
35538
35785
|
this.tracks = alive;
|
|
35539
35786
|
}
|
|
35540
35787
|
const stillResurrectable = [];
|
|
35541
35788
|
for (const t of this.lostTracks) if (timestamp - t.lostAt <= this.config.resurrectionWindowMs) stillResurrectable.push(t);
|
|
35542
|
-
else
|
|
35789
|
+
else {
|
|
35790
|
+
this.reportTrackLoss(t);
|
|
35791
|
+
this.reportTrackDeath(t, "graveyard-expired", timestamp);
|
|
35792
|
+
}
|
|
35543
35793
|
this.lostTracks = stillResurrectable;
|
|
35544
35794
|
const used = /* @__PURE__ */ new Set();
|
|
35545
35795
|
const matchedTracks = /* @__PURE__ */ new Set();
|
|
@@ -35549,36 +35799,21 @@ var SortTracker = class SortTracker {
|
|
|
35549
35799
|
for (let di = 0; di < detections.length; di++) if (this.config.byteTrackEnabled && detections[di].score < this.config.byteTrackHighThreshold) lowIdxs.push(di);
|
|
35550
35800
|
else highIdxs.push(di);
|
|
35551
35801
|
const lowSet = new Set(lowIdxs);
|
|
35552
|
-
|
|
35553
|
-
const
|
|
35554
|
-
|
|
35555
|
-
|
|
35556
|
-
|
|
35557
|
-
|
|
35558
|
-
|
|
35559
|
-
|
|
35560
|
-
|
|
35561
|
-
if (gate === "centroid" && this.isPlausibleBirth(det, frameArea) && !(this.config.reacquireTimeScaled && this.isUnambiguousReach(track, det, detections))) continue;
|
|
35562
|
-
rescuePairs.push({
|
|
35563
|
-
track,
|
|
35564
|
-
detIdx: di,
|
|
35565
|
-
score: iou$3(track.bbox, det.bbox),
|
|
35566
|
-
gate
|
|
35567
|
-
});
|
|
35568
|
-
}
|
|
35569
|
-
}
|
|
35570
|
-
rescuePairs.sort((a, b) => b.score - a.score);
|
|
35571
|
-
for (const pair of rescuePairs) {
|
|
35572
|
-
if (matchedTracks.has(pair.track) || used.has(pair.detIdx)) continue;
|
|
35573
|
-
const det = detections[pair.detIdx];
|
|
35574
|
-
matched.set(pair.track, det);
|
|
35575
|
-
matchedTracks.add(pair.track);
|
|
35576
|
-
used.add(pair.detIdx);
|
|
35577
|
-
this.reportBind("rescue", pair.gate, pair.track, det, timestamp);
|
|
35578
|
-
}
|
|
35579
|
-
if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
|
|
35802
|
+
const [established, provisional] = this.associationTiers();
|
|
35803
|
+
const OVERLAP = new Set(["iou"]);
|
|
35804
|
+
const PROXIMITY = new Set(["centroid", "growth"]);
|
|
35805
|
+
this.greedyAssociate(established, detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
|
|
35806
|
+
this.rescueAssociate(established, OVERLAP, detections, lowSet, frameArea, timestamp, used, matchedTracks, matched, continuationOnly);
|
|
35807
|
+
this.greedyAssociate(provisional, detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
|
|
35808
|
+
this.rescueAssociate(established, PROXIMITY, detections, lowSet, frameArea, timestamp, used, matchedTracks, matched, continuationOnly);
|
|
35809
|
+
this.rescueAssociate(provisional, new Set([...OVERLAP, ...PROXIMITY]), detections, lowSet, frameArea, timestamp, used, matchedTracks, matched, continuationOnly);
|
|
35810
|
+
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);
|
|
35580
35811
|
for (const [track, det] of matched) {
|
|
35581
|
-
if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, {
|
|
35812
|
+
if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, {
|
|
35813
|
+
against: track,
|
|
35814
|
+
measured: iou$3(this.predicted(track, timestamp), det.bbox),
|
|
35815
|
+
threshold: this.config.animalOverPersonMaxScore
|
|
35816
|
+
});
|
|
35582
35817
|
const prevCenter = bboxCentroid({
|
|
35583
35818
|
x: track.bbox.x,
|
|
35584
35819
|
y: track.bbox.y,
|
|
@@ -35594,12 +35829,15 @@ var SortTracker = class SortTracker {
|
|
|
35594
35829
|
const frames = track.age + 1;
|
|
35595
35830
|
const dtMs = timestamp - track.lastSeen;
|
|
35596
35831
|
track.bbox = det.bbox;
|
|
35597
|
-
track.class = det.class;
|
|
35832
|
+
if (!this.crossClassFlip(track.class, det)) track.class = det.class;
|
|
35833
|
+
track.frameClass = det.class;
|
|
35598
35834
|
track.originalClass = det.originalClass;
|
|
35599
35835
|
track.score = det.score;
|
|
35836
|
+
track.lastNativeRecovery = det.nativeRecovery;
|
|
35600
35837
|
track.age = 0;
|
|
35601
35838
|
track.hits++;
|
|
35602
35839
|
track.lastSeen = timestamp;
|
|
35840
|
+
track.lastSeenFrameSeq = this.frameSeq;
|
|
35603
35841
|
if (dtMs > 0) {
|
|
35604
35842
|
track.velocity = {
|
|
35605
35843
|
dx: (newCenter.x - prevCenter.x) / dtMs,
|
|
@@ -35648,7 +35886,7 @@ var SortTracker = class SortTracker {
|
|
|
35648
35886
|
if (gate === null) continue;
|
|
35649
35887
|
if (gate === "centroid" && this.isPlausibleBirth(det, frameArea)) continue;
|
|
35650
35888
|
const score = iou$3(lost.bbox, det.bbox);
|
|
35651
|
-
const peers = this.
|
|
35889
|
+
const peers = this.sameClassPeerCount(lost, surviving);
|
|
35652
35890
|
if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
|
|
35653
35891
|
if (score > refusedScore) {
|
|
35654
35892
|
refused = lost;
|
|
@@ -35673,12 +35911,15 @@ var SortTracker = class SortTracker {
|
|
|
35673
35911
|
this.lostTracks = this.lostTracks.filter((t) => t !== best);
|
|
35674
35912
|
best.bbox = det.bbox;
|
|
35675
35913
|
best.class = det.class;
|
|
35914
|
+
best.frameClass = det.class;
|
|
35676
35915
|
best.originalClass = det.originalClass;
|
|
35677
35916
|
best.score = det.score;
|
|
35917
|
+
best.lastNativeRecovery = det.nativeRecovery;
|
|
35678
35918
|
best.age = 0;
|
|
35679
35919
|
best.hits++;
|
|
35680
35920
|
best.lost = false;
|
|
35681
35921
|
best.lastSeen = timestamp;
|
|
35922
|
+
best.lastSeenFrameSeq = this.frameSeq;
|
|
35682
35923
|
best.velocity = {
|
|
35683
35924
|
dx: 0,
|
|
35684
35925
|
dy: 0
|
|
@@ -35735,6 +35976,7 @@ var SortTracker = class SortTracker {
|
|
|
35735
35976
|
id: spawnedId,
|
|
35736
35977
|
bbox: det.bbox,
|
|
35737
35978
|
class: det.class,
|
|
35979
|
+
frameClass: det.class,
|
|
35738
35980
|
originalClass: det.originalClass,
|
|
35739
35981
|
score: det.score,
|
|
35740
35982
|
age: 0,
|
|
@@ -35742,6 +35984,7 @@ var SortTracker = class SortTracker {
|
|
|
35742
35984
|
path: [det.bbox],
|
|
35743
35985
|
firstSeen: timestamp,
|
|
35744
35986
|
lastSeen: timestamp,
|
|
35987
|
+
lastSeenFrameSeq: this.frameSeq,
|
|
35745
35988
|
velocity: {
|
|
35746
35989
|
dx: 0,
|
|
35747
35990
|
dy: 0
|
|
@@ -35752,7 +35995,8 @@ var SortTracker = class SortTracker {
|
|
|
35752
35995
|
resurrectable: true,
|
|
35753
35996
|
classVotes: new Map([[det.class, det.score > 0 ? det.score : Number.EPSILON]]),
|
|
35754
35997
|
emitted: false,
|
|
35755
|
-
lossReported: false
|
|
35998
|
+
lossReported: false,
|
|
35999
|
+
lastNativeRecovery: det.nativeRecovery
|
|
35756
36000
|
});
|
|
35757
36001
|
}
|
|
35758
36002
|
this.tracks = surviving;
|
|
@@ -36282,6 +36526,10 @@ var FrameProcessor = class {
|
|
|
36282
36526
|
/** Wire (or clear) the tracker's fragmentation sink — a new id minted beside a
|
|
36283
36527
|
* live coasting track that could have claimed it (D399). Same `deviceId`
|
|
36284
36528
|
* contract as the two sinks above. */
|
|
36529
|
+
/** Wire the sink for track deaths (D438) — pure passthrough to the tracker. */
|
|
36530
|
+
setTrackLossObserver(observer) {
|
|
36531
|
+
this.tracker.setTrackLossObserver(observer);
|
|
36532
|
+
}
|
|
36285
36533
|
setSpawnBesideCoastingObserver(observer) {
|
|
36286
36534
|
this.tracker.setSpawnBesideCoastingObserver(observer);
|
|
36287
36535
|
}
|
|
@@ -66622,10 +66870,12 @@ var TrackingSettingsSchema = object({
|
|
|
66622
66870
|
minSpawnAreaFracAnimal: number().min(0).max(1).default(0),
|
|
66623
66871
|
/** Min spawn bbox area for `vehicle` as a fraction of the frame (0 = ungated). */
|
|
66624
66872
|
minSpawnAreaFracVehicle: number().min(0).max(1).default(0),
|
|
66625
|
-
/**
|
|
66626
|
-
*
|
|
66627
|
-
*
|
|
66628
|
-
*
|
|
66873
|
+
/** Let a person that flips to `animal` for a frame re-match its existing
|
|
66874
|
+
* track (no concurrent animal id). A flip is the same subject in the same
|
|
66875
|
+
* place, weakly read (D439): the bind is allowed only on the predicted-box
|
|
66876
|
+
* passes, only for a coarse box below `animalOverPersonMaxScore`, never on
|
|
66877
|
+
* the rescue / resurrection passes. Reported label stays via the class
|
|
66878
|
+
* vote. Off = exact per-frame class gating. */
|
|
66629
66879
|
classGroupAssoc: boolean().default(true)
|
|
66630
66880
|
});
|
|
66631
66881
|
var TRACKING_DEFAULTS = TrackingSettingsSchema.parse({});
|
|
@@ -67215,7 +67465,7 @@ function buildDetectionSettingsSections() {
|
|
|
67215
67465
|
type: "boolean",
|
|
67216
67466
|
key: "classGroupAssoc",
|
|
67217
67467
|
label: "Person/animal group matching",
|
|
67218
|
-
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.",
|
|
67468
|
+
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.",
|
|
67219
67469
|
default: TRACKING_DEFAULTS.classGroupAssoc
|
|
67220
67470
|
},
|
|
67221
67471
|
{
|
|
@@ -77686,6 +77936,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
77686
77936
|
}
|
|
77687
77937
|
});
|
|
77688
77938
|
});
|
|
77939
|
+
p.setTrackLossObserver((record) => {
|
|
77940
|
+
this.ctx.logger.info("tracker: a track ended and nothing took over", {
|
|
77941
|
+
tags: { deviceId },
|
|
77942
|
+
meta: {
|
|
77943
|
+
source,
|
|
77944
|
+
...record
|
|
77945
|
+
}
|
|
77946
|
+
});
|
|
77947
|
+
});
|
|
77689
77948
|
p.setSpawnBesideCoastingObserver((record) => {
|
|
77690
77949
|
this.ctx.logger.info("tracker: new id minted beside a live coasting track", {
|
|
77691
77950
|
tags: { deviceId },
|