@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
|
@@ -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"
|
|
@@ -20070,11 +20127,19 @@ var TimelapseRenderer = class {
|
|
|
20070
20127
|
async render(request) {
|
|
20071
20128
|
const { deviceId, window } = request;
|
|
20072
20129
|
const tags = { deviceId };
|
|
20073
|
-
const
|
|
20130
|
+
const first = await this.ports.getAvailability({
|
|
20074
20131
|
deviceId,
|
|
20075
20132
|
fromMs: window.startMs,
|
|
20076
20133
|
toMs: window.endMs
|
|
20077
20134
|
});
|
|
20135
|
+
const cheapest = [...first.profilesWithFootage].sort((a, b) => profileCost(a) - profileCost(b))[0];
|
|
20136
|
+
const describes = first.ranges[0]?.profile;
|
|
20137
|
+
const availability = cheapest !== void 0 && cheapest !== describes ? (await this.ports.getAvailability({
|
|
20138
|
+
deviceId,
|
|
20139
|
+
fromMs: window.startMs,
|
|
20140
|
+
toMs: window.endMs,
|
|
20141
|
+
profile: cheapest
|
|
20142
|
+
})).ranges : first.ranges;
|
|
20078
20143
|
const chosen = pickProfile(availability, window);
|
|
20079
20144
|
if (chosen === null || chosen.coverage.coveredMs === 0) {
|
|
20080
20145
|
this.ports.logger.warn("timelapse skipped: no footage covers the window", {
|
|
@@ -25794,6 +25859,7 @@ function systemEventSubject(kind, scope) {
|
|
|
25794
25859
|
if (kind === "detection-blind") return `detection-blind:${scope.deviceId ?? 0}`;
|
|
25795
25860
|
if (kind === "addon-crash-loop") return `addon-crash:${scope.nodeId ?? "hub"}:${TEST_CRASHED_RUNNER}`;
|
|
25796
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"}`;
|
|
25797
25863
|
if (kind === "addon-updated") return `packages:addon-updated:${scope.packageName ?? "pkg"}`;
|
|
25798
25864
|
if (kind === "server-updated") return `server-updated:${scope.nodeId ?? "hub"}`;
|
|
25799
25865
|
if (kind === "export-completed") return `export:${scope.deviceId ?? 0}`;
|
|
@@ -33395,7 +33461,12 @@ var FaceGalleryProvider = class {
|
|
|
33395
33461
|
identityId: input.identityId
|
|
33396
33462
|
});
|
|
33397
33463
|
assigned += 1;
|
|
33398
|
-
} 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
|
+
} });
|
|
33399
33470
|
failed.push(faceId);
|
|
33400
33471
|
}
|
|
33401
33472
|
return {
|
|
@@ -34777,6 +34848,21 @@ var SpawnBesideCoastingSampler = class {
|
|
|
34777
34848
|
this.counts.clear();
|
|
34778
34849
|
}
|
|
34779
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
|
+
};
|
|
34780
34866
|
/** Per-reason counter + sampling decision. One instance per tracker, so the
|
|
34781
34867
|
* counts are per camera and per detection source. */
|
|
34782
34868
|
var SpawnRefusalSampler = class {
|
|
@@ -34885,6 +34971,16 @@ var SortTracker = class SortTracker {
|
|
|
34885
34971
|
refusalSink = null;
|
|
34886
34972
|
/** Per-reason counters behind the sampled spawn-beside-coasting diagnostics. */
|
|
34887
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();
|
|
34888
34984
|
/** Sink for fragmentation diagnostics — see {@link setSpawnBesideCoastingObserver}. */
|
|
34889
34985
|
spawnBesideObserver = null;
|
|
34890
34986
|
constructor(config = {}) {
|
|
@@ -34917,6 +35013,13 @@ var SortTracker = class SortTracker {
|
|
|
34917
35013
|
* Same contract as the other two sinks — the tracker has no `deviceId`, the
|
|
34918
35014
|
* caller stamps it, and nothing is computed when no observer is wired.
|
|
34919
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
|
+
}
|
|
34920
35023
|
setSpawnBesideCoastingObserver(observer) {
|
|
34921
35024
|
this.spawnBesideObserver = observer;
|
|
34922
35025
|
}
|
|
@@ -34931,13 +35034,13 @@ var SortTracker = class SortTracker {
|
|
|
34931
35034
|
reportSpawnBesideCoasting(spawnedId, det, coasting, detections, frameArea, timestamp) {
|
|
34932
35035
|
const observer = this.spawnBesideObserver;
|
|
34933
35036
|
if (!observer) return;
|
|
34934
|
-
const
|
|
35037
|
+
const family = SortTracker.spawnBesideFamily(det.class);
|
|
34935
35038
|
const dc = bboxCentroid(det.bbox);
|
|
34936
35039
|
let best;
|
|
34937
35040
|
let bestDist = Number.POSITIVE_INFINITY;
|
|
34938
35041
|
let bestReach = 0;
|
|
34939
35042
|
for (const track of coasting) {
|
|
34940
|
-
if (
|
|
35043
|
+
if (SortTracker.spawnBesideFamily(track.class) !== family) continue;
|
|
34941
35044
|
const tc = bboxCentroid(track.bbox);
|
|
34942
35045
|
const dist = Math.hypot(tc.x - dc.x, tc.y - dc.y);
|
|
34943
35046
|
const reach = this.reacquireReach(track, timestamp, true);
|
|
@@ -34952,6 +35055,7 @@ var SortTracker = class SortTracker {
|
|
|
34952
35055
|
const declinedBy = this.explainDecline(best, det, detections, frameArea, timestamp, bestDist);
|
|
34953
35056
|
const spawnCount = this.spawnBesideSampler.next(declinedBy);
|
|
34954
35057
|
if (spawnCount === void 0) return;
|
|
35058
|
+
const crowd = declinedBy === "crowd-guard" ? this.crowdAround(best, det, detections) : void 0;
|
|
34955
35059
|
observer({
|
|
34956
35060
|
spawnedTrackId: spawnedId,
|
|
34957
35061
|
spawnedClass: det.class,
|
|
@@ -34968,6 +35072,8 @@ var SortTracker = class SortTracker {
|
|
|
34968
35072
|
gapMs: timestamp - best.lastSeen,
|
|
34969
35073
|
trackAgeMs: timestamp - best.firstSeen,
|
|
34970
35074
|
declinedBy,
|
|
35075
|
+
...crowd === void 0 ? {} : { competitorCount: crowd.count },
|
|
35076
|
+
...crowd?.nearestPx === void 0 ? {} : { nearestCompetitorPx: crowd.nearestPx },
|
|
34971
35077
|
spawnCount,
|
|
34972
35078
|
sampledEvery: 10
|
|
34973
35079
|
});
|
|
@@ -34982,8 +35088,7 @@ var SortTracker = class SortTracker {
|
|
|
34982
35088
|
* `already-claimed` means.
|
|
34983
35089
|
*/
|
|
34984
35090
|
explainDecline(track, det, detections, frameArea, timestamp, centroidDist) {
|
|
34985
|
-
if (
|
|
34986
|
-
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";
|
|
34987
35092
|
if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "already-claimed";
|
|
34988
35093
|
if (centroidDist <= this.reacquireReach(track, timestamp, true)) {
|
|
34989
35094
|
if (!this.isPlausibleBirth(det, frameArea)) return "already-claimed";
|
|
@@ -35042,14 +35147,44 @@ var SortTracker = class SortTracker {
|
|
|
35042
35147
|
* come back — a graveyarded track may still resurrect, and a line about work
|
|
35043
35148
|
* that was not lost is worse than no line.
|
|
35044
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
|
+
}
|
|
35045
35179
|
reportTrackLoss(track) {
|
|
35046
35180
|
if (track.emitted || track.lossReported) return;
|
|
35047
35181
|
track.lossReported = true;
|
|
35048
35182
|
this.reportRefusal("unconfirmed-spawn-lost", {
|
|
35049
|
-
class: track.
|
|
35183
|
+
class: track.frameClass,
|
|
35050
35184
|
originalClass: track.originalClass,
|
|
35051
35185
|
score: track.score,
|
|
35052
|
-
bbox: track.bbox
|
|
35186
|
+
bbox: track.bbox,
|
|
35187
|
+
...track.lastNativeRecovery !== void 0 ? { nativeRecovery: track.lastNativeRecovery } : {}
|
|
35053
35188
|
}, {
|
|
35054
35189
|
threshold: this.config.minHits,
|
|
35055
35190
|
measured: track.hits,
|
|
@@ -35104,43 +35239,73 @@ var SortTracker = class SortTracker {
|
|
|
35104
35239
|
static pairKey(a, b) {
|
|
35105
35240
|
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
35106
35241
|
}
|
|
35107
|
-
/**
|
|
35108
|
-
|
|
35109
|
-
|
|
35110
|
-
|
|
35111
|
-
|
|
35112
|
-
|
|
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}`;
|
|
35113
35255
|
return cls;
|
|
35114
35256
|
}
|
|
35115
|
-
/**
|
|
35116
|
-
*
|
|
35117
|
-
*
|
|
35118
|
-
*
|
|
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). */
|
|
35119
35297
|
associable(trackClass, det) {
|
|
35120
|
-
if (
|
|
35298
|
+
if (trackClass === det.class) return true;
|
|
35299
|
+
if (det.nativeRecovery !== void 0) return false;
|
|
35121
35300
|
if (!this.config.classGating) return true;
|
|
35122
|
-
return this.
|
|
35301
|
+
return this.crossClassFlip(trackClass, det);
|
|
35123
35302
|
}
|
|
35124
35303
|
/**
|
|
35125
|
-
* A NATIVE-MEASURED BOX IS ITS OWN SUBJECT ACROSS CLASSES (D378)
|
|
35126
|
-
*
|
|
35127
|
-
*
|
|
35128
|
-
*
|
|
35129
|
-
*
|
|
35130
|
-
* the person track is strictly better than minting a concurrent id.
|
|
35131
|
-
*
|
|
35132
|
-
* The runner's second pass makes the opposite measurement — the same detector
|
|
35133
|
-
* on a 640-800 px native square around a motion region the full-frame boxes
|
|
35134
|
-
* do NOT account for. Such a box is, by construction, not the subject the
|
|
35135
|
-
* full pass already has; absorbing it into a person track means the animal is
|
|
35136
|
-
* never seen at all, which on 2026-09-07 07:51:33 is exactly what happened to
|
|
35137
|
-
* an `animal` at `nativeRecoveryScore=0.833`.
|
|
35138
|
-
*
|
|
35139
|
-
* So the widening does not apply to a detection carrying `nativeRecovery`:
|
|
35140
|
-
* it associates to its OWN class or it spawns. Every other guard on that
|
|
35141
|
-
* spawn still runs — the per-class score floor, the area floor, the
|
|
35142
|
-
* `dedupSpawnIou` overlap, and `resolveDuplicates` afterwards, where a person
|
|
35143
|
-
* 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.
|
|
35144
35309
|
*/
|
|
35145
35310
|
sameGroupAsMeasured(trackClass, det) {
|
|
35146
35311
|
if (det.nativeRecovery === void 0) return true;
|
|
@@ -35313,9 +35478,16 @@ var SortTracker = class SortTracker {
|
|
|
35313
35478
|
};
|
|
35314
35479
|
}
|
|
35315
35480
|
/**
|
|
35316
|
-
* How many
|
|
35317
|
-
*
|
|
35318
|
-
*
|
|
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.
|
|
35319
35491
|
*
|
|
35320
35492
|
* The overlap clause on the graveyard half is what keeps this guard off the
|
|
35321
35493
|
* case the resurrection pass exists for. A single flickering subject leaves a
|
|
@@ -35329,30 +35501,30 @@ var SortTracker = class SortTracker {
|
|
|
35329
35501
|
* steals an id is very often one whose OWN track was born after the corpse
|
|
35330
35502
|
* died (camera 618 — the toddler's track post-dates the older boy's).
|
|
35331
35503
|
*/
|
|
35332
|
-
|
|
35333
|
-
const group = this.assocGroup(corpse.class);
|
|
35504
|
+
sameClassPeerCount(corpse, live) {
|
|
35334
35505
|
let peers = 0;
|
|
35335
|
-
for (const t of live) if (t !== corpse &&
|
|
35506
|
+
for (const t of live) if (t !== corpse && t.class === corpse.class) peers++;
|
|
35336
35507
|
for (const other of this.lostTracks) {
|
|
35337
35508
|
if (other === corpse) continue;
|
|
35338
35509
|
if (!other.resurrectable) continue;
|
|
35339
|
-
if (
|
|
35510
|
+
if (other.class !== corpse.class) continue;
|
|
35340
35511
|
if (other.firstSeen <= corpse.lastSeen && corpse.firstSeen <= other.lastSeen) peers++;
|
|
35341
35512
|
}
|
|
35342
35513
|
return peers;
|
|
35343
35514
|
}
|
|
35344
35515
|
/**
|
|
35345
|
-
* Loose
|
|
35346
|
-
*
|
|
35347
|
-
*
|
|
35348
|
-
*
|
|
35349
|
-
*
|
|
35350
|
-
*
|
|
35351
|
-
*
|
|
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).
|
|
35352
35525
|
*/
|
|
35353
35526
|
looseMatch(track, det, timestamp, timeScaled = true) {
|
|
35354
|
-
if (
|
|
35355
|
-
if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return null;
|
|
35527
|
+
if (track.class !== det.class) return null;
|
|
35356
35528
|
if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
|
|
35357
35529
|
const tc = bboxCentroid(track.bbox);
|
|
35358
35530
|
const dc = bboxCentroid(det.bbox);
|
|
@@ -35398,12 +35570,41 @@ var SortTracker = class SortTracker {
|
|
|
35398
35570
|
* bind time and at report time — the spawn-beside-coasting line has to be able
|
|
35399
35571
|
* to say which gate declined without re-running the greedy competition.
|
|
35400
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
|
+
}
|
|
35401
35603
|
isUnambiguousReach(track, det, detections) {
|
|
35402
|
-
if (this.
|
|
35403
|
-
const group = this.assocGroup(track.class);
|
|
35604
|
+
if (this.sameClassPeerCount(track, this.tracks) > 0) return false;
|
|
35404
35605
|
for (const other of detections) {
|
|
35405
35606
|
if (other === det) continue;
|
|
35406
|
-
if (
|
|
35607
|
+
if (other.class === track.class) return false;
|
|
35407
35608
|
}
|
|
35408
35609
|
return true;
|
|
35409
35610
|
}
|
|
@@ -35440,8 +35641,8 @@ var SortTracker = class SortTracker {
|
|
|
35440
35641
|
* weight; otherwise the latest-frame class (ambiguous vote or voting off).
|
|
35441
35642
|
*/
|
|
35442
35643
|
resolveReportedClass(t) {
|
|
35443
|
-
if (!this.config.classVotingEnabled || t.classVotes.size === 0) return t.
|
|
35444
|
-
let bestClass = t.
|
|
35644
|
+
if (!this.config.classVotingEnabled || t.classVotes.size === 0) return t.frameClass;
|
|
35645
|
+
let bestClass = t.frameClass;
|
|
35445
35646
|
let bestVote = -1;
|
|
35446
35647
|
let total = 0;
|
|
35447
35648
|
for (const [cls, v] of t.classVotes) {
|
|
@@ -35451,8 +35652,8 @@ var SortTracker = class SortTracker {
|
|
|
35451
35652
|
bestClass = cls;
|
|
35452
35653
|
}
|
|
35453
35654
|
}
|
|
35454
|
-
if (total <= 0) return t.
|
|
35455
|
-
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;
|
|
35456
35657
|
}
|
|
35457
35658
|
/** Whether a detection clears its per-class spawn score floor. Only gates NEW
|
|
35458
35659
|
* spawns — an established track still matches below its floor. */
|
|
@@ -35468,16 +35669,67 @@ var SortTracker = class SortTracker {
|
|
|
35468
35669
|
return this.config.confirmBypassEnabled && t.score >= this.config.confirmBypassScore;
|
|
35469
35670
|
}
|
|
35470
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
|
+
/**
|
|
35471
35722
|
* One greedy IoU association pass over a candidate detection subset against the
|
|
35472
|
-
*
|
|
35723
|
+
* still-UNMATCHED tracks of `tracks` (one association tier — see
|
|
35724
|
+
* {@link associationTiers}), on each track's PREDICTED box. Highest-IoU pairs
|
|
35473
35725
|
* bind first; a track/detection binds at most once. Shared by the high-score
|
|
35474
35726
|
* first stage and the low-score second stage (different candidate sets + IoU
|
|
35475
35727
|
* gates) so the two-tier ByteTrack association stays a single implementation.
|
|
35476
35728
|
* Mutates `used` / `matchedTracks` / `matched` in place.
|
|
35477
35729
|
*/
|
|
35478
|
-
greedyAssociate(detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly) {
|
|
35730
|
+
greedyAssociate(tracks, detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly) {
|
|
35479
35731
|
const pairs = [];
|
|
35480
|
-
for (const track of
|
|
35732
|
+
for (const track of tracks) {
|
|
35481
35733
|
if (matchedTracks.has(track)) continue;
|
|
35482
35734
|
const pbox = this.predicted(track, timestamp);
|
|
35483
35735
|
const confirmed = this.isConfirmedForEmit(track);
|
|
@@ -35503,6 +35755,7 @@ var SortTracker = class SortTracker {
|
|
|
35503
35755
|
}
|
|
35504
35756
|
}
|
|
35505
35757
|
update(detections, timestamp, frameContext) {
|
|
35758
|
+
this.frameSeq++;
|
|
35506
35759
|
const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
|
|
35507
35760
|
const continuationOnly = frameContext?.continuationOnly ?? EMPTY_INDEX_SET;
|
|
35508
35761
|
const resurrectedScene = /* @__PURE__ */ new Map();
|
|
@@ -35513,6 +35766,7 @@ var SortTracker = class SortTracker {
|
|
|
35513
35766
|
track.lostAt = timestamp;
|
|
35514
35767
|
track.resurrectable = false;
|
|
35515
35768
|
this.reportTrackLoss(track);
|
|
35769
|
+
this.reportTrackDeath(track, "lifetime-cap", timestamp);
|
|
35516
35770
|
this.lostTracks.push(track);
|
|
35517
35771
|
} else alive.push(track);
|
|
35518
35772
|
this.tracks = alive;
|
|
@@ -35525,13 +35779,17 @@ var SortTracker = class SortTracker {
|
|
|
35525
35779
|
track.lostAt = track.lastSeen;
|
|
35526
35780
|
track.resurrectable = false;
|
|
35527
35781
|
this.reportTrackLoss(track);
|
|
35782
|
+
this.reportTrackDeath(track, "stale-beyond-budgets", timestamp);
|
|
35528
35783
|
this.lostTracks.push(track);
|
|
35529
35784
|
} else alive.push(track);
|
|
35530
35785
|
this.tracks = alive;
|
|
35531
35786
|
}
|
|
35532
35787
|
const stillResurrectable = [];
|
|
35533
35788
|
for (const t of this.lostTracks) if (timestamp - t.lostAt <= this.config.resurrectionWindowMs) stillResurrectable.push(t);
|
|
35534
|
-
else
|
|
35789
|
+
else {
|
|
35790
|
+
this.reportTrackLoss(t);
|
|
35791
|
+
this.reportTrackDeath(t, "graveyard-expired", timestamp);
|
|
35792
|
+
}
|
|
35535
35793
|
this.lostTracks = stillResurrectable;
|
|
35536
35794
|
const used = /* @__PURE__ */ new Set();
|
|
35537
35795
|
const matchedTracks = /* @__PURE__ */ new Set();
|
|
@@ -35541,36 +35799,21 @@ var SortTracker = class SortTracker {
|
|
|
35541
35799
|
for (let di = 0; di < detections.length; di++) if (this.config.byteTrackEnabled && detections[di].score < this.config.byteTrackHighThreshold) lowIdxs.push(di);
|
|
35542
35800
|
else highIdxs.push(di);
|
|
35543
35801
|
const lowSet = new Set(lowIdxs);
|
|
35544
|
-
|
|
35545
|
-
const
|
|
35546
|
-
|
|
35547
|
-
|
|
35548
|
-
|
|
35549
|
-
|
|
35550
|
-
|
|
35551
|
-
|
|
35552
|
-
|
|
35553
|
-
if (gate === "centroid" && this.isPlausibleBirth(det, frameArea) && !(this.config.reacquireTimeScaled && this.isUnambiguousReach(track, det, detections))) continue;
|
|
35554
|
-
rescuePairs.push({
|
|
35555
|
-
track,
|
|
35556
|
-
detIdx: di,
|
|
35557
|
-
score: iou$3(track.bbox, det.bbox),
|
|
35558
|
-
gate
|
|
35559
|
-
});
|
|
35560
|
-
}
|
|
35561
|
-
}
|
|
35562
|
-
rescuePairs.sort((a, b) => b.score - a.score);
|
|
35563
|
-
for (const pair of rescuePairs) {
|
|
35564
|
-
if (matchedTracks.has(pair.track) || used.has(pair.detIdx)) continue;
|
|
35565
|
-
const det = detections[pair.detIdx];
|
|
35566
|
-
matched.set(pair.track, det);
|
|
35567
|
-
matchedTracks.add(pair.track);
|
|
35568
|
-
used.add(pair.detIdx);
|
|
35569
|
-
this.reportBind("rescue", pair.gate, pair.track, det, timestamp);
|
|
35570
|
-
}
|
|
35571
|
-
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);
|
|
35572
35811
|
for (const [track, det] of matched) {
|
|
35573
|
-
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
|
+
});
|
|
35574
35817
|
const prevCenter = bboxCentroid({
|
|
35575
35818
|
x: track.bbox.x,
|
|
35576
35819
|
y: track.bbox.y,
|
|
@@ -35586,12 +35829,15 @@ var SortTracker = class SortTracker {
|
|
|
35586
35829
|
const frames = track.age + 1;
|
|
35587
35830
|
const dtMs = timestamp - track.lastSeen;
|
|
35588
35831
|
track.bbox = det.bbox;
|
|
35589
|
-
track.class = det.class;
|
|
35832
|
+
if (!this.crossClassFlip(track.class, det)) track.class = det.class;
|
|
35833
|
+
track.frameClass = det.class;
|
|
35590
35834
|
track.originalClass = det.originalClass;
|
|
35591
35835
|
track.score = det.score;
|
|
35836
|
+
track.lastNativeRecovery = det.nativeRecovery;
|
|
35592
35837
|
track.age = 0;
|
|
35593
35838
|
track.hits++;
|
|
35594
35839
|
track.lastSeen = timestamp;
|
|
35840
|
+
track.lastSeenFrameSeq = this.frameSeq;
|
|
35595
35841
|
if (dtMs > 0) {
|
|
35596
35842
|
track.velocity = {
|
|
35597
35843
|
dx: (newCenter.x - prevCenter.x) / dtMs,
|
|
@@ -35640,7 +35886,7 @@ var SortTracker = class SortTracker {
|
|
|
35640
35886
|
if (gate === null) continue;
|
|
35641
35887
|
if (gate === "centroid" && this.isPlausibleBirth(det, frameArea)) continue;
|
|
35642
35888
|
const score = iou$3(lost.bbox, det.bbox);
|
|
35643
|
-
const peers = this.
|
|
35889
|
+
const peers = this.sameClassPeerCount(lost, surviving);
|
|
35644
35890
|
if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
|
|
35645
35891
|
if (score > refusedScore) {
|
|
35646
35892
|
refused = lost;
|
|
@@ -35665,12 +35911,15 @@ var SortTracker = class SortTracker {
|
|
|
35665
35911
|
this.lostTracks = this.lostTracks.filter((t) => t !== best);
|
|
35666
35912
|
best.bbox = det.bbox;
|
|
35667
35913
|
best.class = det.class;
|
|
35914
|
+
best.frameClass = det.class;
|
|
35668
35915
|
best.originalClass = det.originalClass;
|
|
35669
35916
|
best.score = det.score;
|
|
35917
|
+
best.lastNativeRecovery = det.nativeRecovery;
|
|
35670
35918
|
best.age = 0;
|
|
35671
35919
|
best.hits++;
|
|
35672
35920
|
best.lost = false;
|
|
35673
35921
|
best.lastSeen = timestamp;
|
|
35922
|
+
best.lastSeenFrameSeq = this.frameSeq;
|
|
35674
35923
|
best.velocity = {
|
|
35675
35924
|
dx: 0,
|
|
35676
35925
|
dy: 0
|
|
@@ -35727,6 +35976,7 @@ var SortTracker = class SortTracker {
|
|
|
35727
35976
|
id: spawnedId,
|
|
35728
35977
|
bbox: det.bbox,
|
|
35729
35978
|
class: det.class,
|
|
35979
|
+
frameClass: det.class,
|
|
35730
35980
|
originalClass: det.originalClass,
|
|
35731
35981
|
score: det.score,
|
|
35732
35982
|
age: 0,
|
|
@@ -35734,6 +35984,7 @@ var SortTracker = class SortTracker {
|
|
|
35734
35984
|
path: [det.bbox],
|
|
35735
35985
|
firstSeen: timestamp,
|
|
35736
35986
|
lastSeen: timestamp,
|
|
35987
|
+
lastSeenFrameSeq: this.frameSeq,
|
|
35737
35988
|
velocity: {
|
|
35738
35989
|
dx: 0,
|
|
35739
35990
|
dy: 0
|
|
@@ -35744,7 +35995,8 @@ var SortTracker = class SortTracker {
|
|
|
35744
35995
|
resurrectable: true,
|
|
35745
35996
|
classVotes: new Map([[det.class, det.score > 0 ? det.score : Number.EPSILON]]),
|
|
35746
35997
|
emitted: false,
|
|
35747
|
-
lossReported: false
|
|
35998
|
+
lossReported: false,
|
|
35999
|
+
lastNativeRecovery: det.nativeRecovery
|
|
35748
36000
|
});
|
|
35749
36001
|
}
|
|
35750
36002
|
this.tracks = surviving;
|
|
@@ -36137,7 +36389,6 @@ var FrameProcessor = class {
|
|
|
36137
36389
|
* filter). Shares the tracker's vocabulary — see {@link SpawnRefusalReason}. */
|
|
36138
36390
|
spawnRefusalObserver = null;
|
|
36139
36391
|
/** Where a collected refusal is forwarded for LOGGING, when anyone asked. */
|
|
36140
|
-
spawnRefusalForward = null;
|
|
36141
36392
|
/**
|
|
36142
36393
|
* The spawn refusals of the frame being processed.
|
|
36143
36394
|
*
|
|
@@ -36151,6 +36402,10 @@ var FrameProcessor = class {
|
|
|
36151
36402
|
/** Running count of refusals-in-zone SEEN — never the throttled number. */
|
|
36152
36403
|
zoneRefusalEventCount = 0;
|
|
36153
36404
|
zoneRefusalObserver = null;
|
|
36405
|
+
/** Track ids whose BIRTH zone history has already been folded in — the fold
|
|
36406
|
+
* happens once, on the frame a track first becomes visible. */
|
|
36407
|
+
birthZonesSeeded = /* @__PURE__ */ new Set();
|
|
36408
|
+
birthZoneTrailObserver = null;
|
|
36154
36409
|
/** Last report time per collapsed key, for {@link ZONE_REFUSAL_THROTTLE_MS}. */
|
|
36155
36410
|
zoneRefusalLastReport = /* @__PURE__ */ new Map();
|
|
36156
36411
|
zoneRefusalSampler = new SpawnRefusalSampler();
|
|
@@ -36261,12 +36516,20 @@ var FrameProcessor = class {
|
|
|
36261
36516
|
setZoneRefusalObserver(observer) {
|
|
36262
36517
|
this.zoneRefusalObserver = observer;
|
|
36263
36518
|
}
|
|
36519
|
+
/** Where "this track was born already carrying a zone" is reported. */
|
|
36520
|
+
setBirthZoneTrailObserver(observer) {
|
|
36521
|
+
this.birthZoneTrailObserver = observer;
|
|
36522
|
+
}
|
|
36264
36523
|
setBindObserver(observer) {
|
|
36265
36524
|
this.tracker.setBindObserver(observer);
|
|
36266
36525
|
}
|
|
36267
36526
|
/** Wire (or clear) the tracker's fragmentation sink — a new id minted beside a
|
|
36268
36527
|
* live coasting track that could have claimed it (D399). Same `deviceId`
|
|
36269
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
|
+
}
|
|
36270
36533
|
setSpawnBesideCoastingObserver(observer) {
|
|
36271
36534
|
this.tracker.setSpawnBesideCoastingObserver(observer);
|
|
36272
36535
|
}
|
|
@@ -36350,8 +36613,8 @@ var FrameProcessor = class {
|
|
|
36350
36613
|
memberships.map((m) => m.zoneId).join("+"),
|
|
36351
36614
|
cell(r.bbox.x),
|
|
36352
36615
|
cell(r.bbox.y),
|
|
36353
|
-
cell(r.bbox.
|
|
36354
|
-
cell(r.bbox.
|
|
36616
|
+
cell(r.bbox.w),
|
|
36617
|
+
cell(r.bbox.h)
|
|
36355
36618
|
].join("|");
|
|
36356
36619
|
const last = this.zoneRefusalLastReport.get(key);
|
|
36357
36620
|
if (last !== void 0 && timestamp - last < ZONE_REFUSAL_THROTTLE_MS) return false;
|
|
@@ -36596,6 +36859,32 @@ var FrameProcessor = class {
|
|
|
36596
36859
|
if (memberships.length > 0) overlapsByTrack.set(td.trackId, memberships);
|
|
36597
36860
|
if (belowBar.length > 0) rejectedByTrack.set(td.trackId, belowBar);
|
|
36598
36861
|
}
|
|
36862
|
+
for (const td of trackedDetections) {
|
|
36863
|
+
if (this.birthZonesSeeded.has(td.trackId)) continue;
|
|
36864
|
+
this.birthZonesSeeded.add(td.trackId);
|
|
36865
|
+
const trail = td.path;
|
|
36866
|
+
if (trail.length <= 1) continue;
|
|
36867
|
+
const atBirth = zonesByTrack.get(td.trackId) ?? [];
|
|
36868
|
+
const zones = new Set(atBirth);
|
|
36869
|
+
const bornWith = zones.size;
|
|
36870
|
+
for (let i = 0; i < trail.length - 1; i++) {
|
|
36871
|
+
const box = trail[i];
|
|
36872
|
+
if (box === void 0) continue;
|
|
36873
|
+
const { memberships } = this.zoneEngine.splitDetectionZones(box, this.zones, frameWidth, frameHeight, void 0, void 0, void 0, this.zoneMembershipMinOverlap);
|
|
36874
|
+
for (const m of memberships) zones.add(m.zoneId);
|
|
36875
|
+
}
|
|
36876
|
+
if (zones.size === bornWith) continue;
|
|
36877
|
+
const inherited = [...zones].filter((z) => !atBirth.includes(z));
|
|
36878
|
+
zonesByTrack.set(td.trackId, [...zones]);
|
|
36879
|
+
this.birthZoneTrailObserver?.({
|
|
36880
|
+
deviceId: this.deviceId,
|
|
36881
|
+
trackId: td.trackId,
|
|
36882
|
+
className: td.class,
|
|
36883
|
+
measurementsBeforeBirth: trail.length - 1,
|
|
36884
|
+
zonesAtBirth: [...atBirth],
|
|
36885
|
+
inheritedZones: inherited
|
|
36886
|
+
});
|
|
36887
|
+
}
|
|
36599
36888
|
this.lastZoneOverlaps = overlapsByTrack;
|
|
36600
36889
|
this.lastZoneRejections = rejectedByTrack;
|
|
36601
36890
|
for (const td of trackedDetections) this.lastDetectionByTrack.set(td.trackId, td);
|
|
@@ -36668,6 +36957,7 @@ var FrameProcessor = class {
|
|
|
36668
36957
|
for (const state of objectStates) {
|
|
36669
36958
|
if (state.state !== "leaving") continue;
|
|
36670
36959
|
this.lastZonesByTrack.delete(state.trackId);
|
|
36960
|
+
this.birthZonesSeeded.delete(state.trackId);
|
|
36671
36961
|
this.lastDetectionByTrack.delete(state.trackId);
|
|
36672
36962
|
this.reportedRefusalByTrack.delete(state.trackId);
|
|
36673
36963
|
}
|
|
@@ -39595,6 +39885,62 @@ function tieredLabelColumnData(patch) {
|
|
|
39595
39885
|
};
|
|
39596
39886
|
}
|
|
39597
39887
|
//#endregion
|
|
39888
|
+
//#region src/pipeline-analytics/store/audio-level-density.ts
|
|
39889
|
+
/**
|
|
39890
|
+
* The energy-weighted mean of a set of dBFS values, in dBFS.
|
|
39891
|
+
*
|
|
39892
|
+
* dBFS is logarithmic, so an arithmetic mean of dB numbers is the mean of
|
|
39893
|
+
* nothing physical — it under-weights the loud moment that is the reason to
|
|
39894
|
+
* look at a bar at all. Averaging POWER and converting back is what a meter is
|
|
39895
|
+
* expected to show, and it is the same arithmetic as an RMS over the interval.
|
|
39896
|
+
*
|
|
39897
|
+
* `undefined` for an empty set, and for a set with no finite sample: a bucket
|
|
39898
|
+
* nobody measured must not be reported as silence, and −∞ dBFS is not a number
|
|
39899
|
+
* a bar can paint.
|
|
39900
|
+
*/
|
|
39901
|
+
function energyWeightedMeanDbfs(samples) {
|
|
39902
|
+
let power = 0;
|
|
39903
|
+
let n = 0;
|
|
39904
|
+
for (const dbfs of samples) {
|
|
39905
|
+
if (Number.isNaN(dbfs)) continue;
|
|
39906
|
+
if (dbfs === Number.NEGATIVE_INFINITY) {
|
|
39907
|
+
n += 1;
|
|
39908
|
+
continue;
|
|
39909
|
+
}
|
|
39910
|
+
if (!Number.isFinite(dbfs)) continue;
|
|
39911
|
+
power += 10 ** (dbfs / 10);
|
|
39912
|
+
n += 1;
|
|
39913
|
+
}
|
|
39914
|
+
if (n === 0) return void 0;
|
|
39915
|
+
const mean = power / n;
|
|
39916
|
+
if (mean <= 0) return void 0;
|
|
39917
|
+
return 10 * Math.log10(mean);
|
|
39918
|
+
}
|
|
39919
|
+
/**
|
|
39920
|
+
* Energy-weighted mean dBFS per bucket index, for the buckets that HAVE audio.
|
|
39921
|
+
*
|
|
39922
|
+
* A bucket with no events is absent from the map rather than present at zero:
|
|
39923
|
+
* "nothing was heard" and "nothing was measured" are different claims, and the
|
|
39924
|
+
* lane has to be able to draw nothing.
|
|
39925
|
+
*/
|
|
39926
|
+
function audioDbfsByBucket(events, since, bucketMs) {
|
|
39927
|
+
const out = /* @__PURE__ */ new Map();
|
|
39928
|
+
if (bucketMs <= 0) return out;
|
|
39929
|
+
const byBucket = /* @__PURE__ */ new Map();
|
|
39930
|
+
for (const ev of events) {
|
|
39931
|
+
const idx = Math.floor((ev.timestamp - since) / bucketMs);
|
|
39932
|
+
if (idx < 0) continue;
|
|
39933
|
+
const bin = byBucket.get(idx);
|
|
39934
|
+
if (bin === void 0) byBucket.set(idx, [ev.dbfs]);
|
|
39935
|
+
else bin.push(ev.dbfs);
|
|
39936
|
+
}
|
|
39937
|
+
for (const [idx, samples] of byBucket) {
|
|
39938
|
+
const mean = energyWeightedMeanDbfs(samples);
|
|
39939
|
+
if (mean !== void 0) out.set(idx, mean);
|
|
39940
|
+
}
|
|
39941
|
+
return out;
|
|
39942
|
+
}
|
|
39943
|
+
//#endregion
|
|
39598
39944
|
//#region src/pipeline-analytics/store/event-store.ts
|
|
39599
39945
|
/**
|
|
39600
39946
|
* @durable class=ledger owner=pipeline-analytics
|
|
@@ -40433,10 +40779,11 @@ var EventStore = class {
|
|
|
40433
40779
|
});
|
|
40434
40780
|
return new Map(rows.map((r) => [r.bucket, r.count]));
|
|
40435
40781
|
};
|
|
40436
|
-
const [motionMap, objectMap, audioMap] = await Promise.all([
|
|
40782
|
+
const [motionMap, objectMap, audioMap, audioDbfsMap] = await Promise.all([
|
|
40437
40783
|
run(MOTION_EVENTS_COLLECTION),
|
|
40438
40784
|
run(OBJECT_EVENTS_COLLECTION),
|
|
40439
|
-
run(AUDIO_EVENTS_COLLECTION)
|
|
40785
|
+
run(AUDIO_EVENTS_COLLECTION),
|
|
40786
|
+
this.audioDbfsPerBucket(q)
|
|
40440
40787
|
]);
|
|
40441
40788
|
const buckets = Math.max(0, Math.ceil((q.until - q.since) / q.bucketMs));
|
|
40442
40789
|
const out = [];
|
|
@@ -40445,16 +40792,46 @@ var EventStore = class {
|
|
|
40445
40792
|
const o = objectMap.get(i) ?? 0;
|
|
40446
40793
|
const a = audioMap.get(i) ?? 0;
|
|
40447
40794
|
if (m + o + a === 0) continue;
|
|
40795
|
+
const dbfs = audioDbfsMap.get(i);
|
|
40448
40796
|
out.push({
|
|
40449
40797
|
bucketStart: q.since + i * q.bucketMs,
|
|
40450
40798
|
motion: m,
|
|
40451
40799
|
object: o,
|
|
40452
|
-
audio: a
|
|
40800
|
+
audio: a,
|
|
40801
|
+
...dbfs !== void 0 ? { audioDbfs: dbfs } : {}
|
|
40453
40802
|
});
|
|
40454
40803
|
}
|
|
40455
40804
|
return out;
|
|
40456
40805
|
}
|
|
40457
40806
|
/**
|
|
40807
|
+
* Energy-weighted mean dBFS per bucket index, for the buckets that have
|
|
40808
|
+
* audio. Reads the window's audio events once, `slim`, and folds them —
|
|
40809
|
+
* `histogram` can only count, and a level is not a count.
|
|
40810
|
+
*
|
|
40811
|
+
* A read that fails yields an EMPTY map, so the lane draws no level rather
|
|
40812
|
+
* than a wrong one: the density counts beside it are unaffected, and a
|
|
40813
|
+
* missing bar is the honest rendering of "not measured" (D431).
|
|
40814
|
+
*/
|
|
40815
|
+
async audioDbfsPerBucket(q) {
|
|
40816
|
+
try {
|
|
40817
|
+
return audioDbfsByBucket((await this.queryAudio({
|
|
40818
|
+
deviceId: q.deviceId,
|
|
40819
|
+
since: q.since,
|
|
40820
|
+
until: q.until,
|
|
40821
|
+
projection: "slim"
|
|
40822
|
+
})).map((e) => ({
|
|
40823
|
+
timestamp: e.timestamp,
|
|
40824
|
+
dbfs: e.dbfs
|
|
40825
|
+
})), q.since, q.bucketMs);
|
|
40826
|
+
} catch (err) {
|
|
40827
|
+
this.logger.warn("density: audio level aggregation failed — the lane will draw no level", {
|
|
40828
|
+
tags: { deviceId: q.deviceId },
|
|
40829
|
+
meta: { error: String(err) }
|
|
40830
|
+
});
|
|
40831
|
+
return /* @__PURE__ */ new Map();
|
|
40832
|
+
}
|
|
40833
|
+
}
|
|
40834
|
+
/**
|
|
40458
40835
|
* Age-evict the TRACKLESS event kinds — motion and audio.
|
|
40459
40836
|
*
|
|
40460
40837
|
* **Object events are not here, and that is the point.** An object event is a
|
|
@@ -66493,10 +66870,12 @@ var TrackingSettingsSchema = object({
|
|
|
66493
66870
|
minSpawnAreaFracAnimal: number().min(0).max(1).default(0),
|
|
66494
66871
|
/** Min spawn bbox area for `vehicle` as a fraction of the frame (0 = ungated). */
|
|
66495
66872
|
minSpawnAreaFracVehicle: number().min(0).max(1).default(0),
|
|
66496
|
-
/**
|
|
66497
|
-
*
|
|
66498
|
-
*
|
|
66499
|
-
*
|
|
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. */
|
|
66500
66879
|
classGroupAssoc: boolean().default(true)
|
|
66501
66880
|
});
|
|
66502
66881
|
var TRACKING_DEFAULTS = TrackingSettingsSchema.parse({});
|
|
@@ -67086,7 +67465,7 @@ function buildDetectionSettingsSections() {
|
|
|
67086
67465
|
type: "boolean",
|
|
67087
67466
|
key: "classGroupAssoc",
|
|
67088
67467
|
label: "Person/animal group matching",
|
|
67089
|
-
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.",
|
|
67090
67469
|
default: TRACKING_DEFAULTS.classGroupAssoc
|
|
67091
67470
|
},
|
|
67092
67471
|
{
|
|
@@ -72742,12 +73121,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
72742
73121
|
*/
|
|
72743
73122
|
buildTimelapsePorts(api, stores) {
|
|
72744
73123
|
return {
|
|
72745
|
-
getAvailability: async ({ deviceId, fromMs, toMs }) => {
|
|
72746
|
-
|
|
73124
|
+
getAvailability: async ({ deviceId, fromMs, toMs, profile }) => {
|
|
73125
|
+
const availability = await api.recording.getAvailability.query({
|
|
72747
73126
|
deviceId,
|
|
72748
73127
|
fromMs,
|
|
72749
|
-
toMs
|
|
72750
|
-
|
|
73128
|
+
toMs,
|
|
73129
|
+
...profile !== void 0 ? { profile } : {}
|
|
73130
|
+
});
|
|
73131
|
+
return {
|
|
73132
|
+
ranges: availability.ranges,
|
|
73133
|
+
profilesWithFootage: availability.profilesWithFootage
|
|
73134
|
+
};
|
|
72751
73135
|
},
|
|
72752
73136
|
listTracks: async ({ deviceId, since, until }) => {
|
|
72753
73137
|
return (await stores.trackStore.queryHistorical({
|
|
@@ -77534,6 +77918,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
77534
77918
|
}
|
|
77535
77919
|
});
|
|
77536
77920
|
});
|
|
77921
|
+
p.setBirthZoneTrailObserver((trail) => {
|
|
77922
|
+
this.ctx.logger.info("track born carrying zones it crossed before it was a track", {
|
|
77923
|
+
tags: { deviceId },
|
|
77924
|
+
meta: {
|
|
77925
|
+
source,
|
|
77926
|
+
...trail
|
|
77927
|
+
}
|
|
77928
|
+
});
|
|
77929
|
+
});
|
|
77537
77930
|
p.setZoneRefusalObserver((observation) => {
|
|
77538
77931
|
this.ctx.logger.info("detection refused inside a watched zone — not notified", {
|
|
77539
77932
|
tags: { deviceId },
|
|
@@ -77543,6 +77936,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
77543
77936
|
}
|
|
77544
77937
|
});
|
|
77545
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
|
+
});
|
|
77546
77948
|
p.setSpawnBesideCoastingObserver((record) => {
|
|
77547
77949
|
this.ctx.logger.info("tracker: new id minted beside a live coasting track", {
|
|
77548
77950
|
tags: { deviceId },
|