@camstack/addon-post-analysis 1.2.291 → 1.2.293

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.
Files changed (15) hide show
  1. package/dist/{clip-model-registry-CTY5co6P.js → clip-model-registry-BAuTHS_s.js} +2323 -2033
  2. package/dist/{clip-model-registry-Cudz--Ql.mjs → clip-model-registry-D2zqXamW.mjs} +2324 -2034
  3. package/dist/embedding-encoder/index.js +1 -1
  4. package/dist/embedding-encoder/index.mjs +1 -1
  5. package/dist/pipeline-analytics/_stub.js +2 -2
  6. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CBHxwCSj.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DW6-zerK.mjs} +2 -2
  7. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Boh18P08.mjs +26 -0
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Bs5QFgmh.mjs +26 -0
  9. package/dist/pipeline-analytics/{hostInit-DSKT2dE4.mjs → hostInit-CAJh3aX8.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +445 -45
  11. package/dist/pipeline-analytics/index.mjs +445 -45
  12. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  13. package/package.json +1 -1
  14. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-2OwTV_D2.mjs +0 -26
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DAAKQ3sC.mjs +0 -26
@@ -1,4 +1,4 @@
1
- import { $ as NcRuleSchema, $n as EventCategory, $t as isSourceCap, A as LabelAttributionSchema, An as canonicalHash, At as audioMetricsCapability, B as NC_DEFAULT_SNOOZE_MINUTES, Bt as embeddingRefusalOf, C as EMBEDDING_REQUIREMENTS_FAILED, Cn as systemEventFilterApplies, Ct as TimelapseRuleInputSchema, D as FIRST_LEVEL_MACRO_CLASSES, Dn as vectorDimFromBase64, Dt as addonWidgetsSourceCapability, E as EVENT_OWNER_TYPES, En as templateVarsFor, Et as TrackSourceSchema, F as MOTION_CLOSE_AFTER_MS, Fn as customAction, Ft as deriveRecordingMode, G as NC_TAXONOMY, Gn as discriminatedUnion, Gt as evictionPolicyOfLocation, Hn as _enum, I as MediaFileKindEnum, In as defineCustomActions, It as describeSystemEventDeliveryProblem, J as NcAlarmSettingsPatchSchema, Jn as object, Jt as isDetectionMacroClass, K as NC_TEMPLATE_VARS, Kn as literal, Kt as faceGalleryCapability, L as NC_ALARM_SYSTEM_EVENT_KINDS, Ln as hydrateSchema, Lt as describeSystemEventFamilyProblem, M as MAX_ARCHIVED_DEBUG_NOTES, Mn as CamProfileSchema, Mt as buildEventKindDescriptor, N as MAX_BIRTH_DECISION_RECORDS, Nn as DeviceType, Nt as consumableLowPercentOf, O as FULL_IMAGE_BBOX, On as zoneAnalyticsCapability, Ot as alarmPanelCapability, Pn as createEvent, Pt as cosineSimilarity$1, Q as NcRulePatchSchema, Qn as unknown, Qt as isScheduleActive, R as NC_CONDITION_CATALOG, Rn as isDeviceScopedCap, S as EMBEDDING_MODEL_FAILED, Sn as systemEventFamilyProblems, St as TIMELAPSE_DENSE_FLOOR_SEC, T as EVENT_KIND_BY_CAP, Tn as templateCarriesSummary, Tt as TimelapseRuleSchema, U as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, Un as array, Ut as encodeVectorBase64, V as NC_SENSOR_TRANSITION_KINDS, Vn as sleep$1, W as NC_TANK_SYSTEM_EVENT_KINDS, Wn as boolean, Wt as evaluateSensorEdge, X as NcConditionsSchema, Xn as record, Xt as isKnownTemplateVar, Y as NcConditionDescriptorSchema, Yn as partialRecord, Yt as isEventOwnerType, Z as NcRuleInputSchema, Zn as string, Zt as isMediaOwnerType, _n as sensorRaisedOf, a as CLIP_EMBEDDING_STEP_ID, an as notificationRulesCapability, at as NcSnoozeSuppressedSchema, bn as subKindsOf, bt as SCENE_DIVERGED, c as resolveClusterModelPin, cn as pipelineAnalyticsCapability, ct as NcTemplatePreviewInputSchema, d as ArchivedDebugNoteSchema, dn as readTimelapseGeneratedAt, dt as OpsLogEntrySchema, en as isSystemDelivery, et as NcRuleTargetSchema, f as BaseDevice, fn as resolveClipSearchMinScore, g as DEFAULT_EVENT_COLOR, gn as sceneMonitorCapability, gt as RetrainStatusSchema, h as CoverStateSchema, hn as ruleBelongsToAlarm$1, ht as RebuildObjectEmbeddingsRefusalSchema, in as mayWriteToLocation, it as NcSnoozeSchema, j as LockStateSchema, jn as BaseAddon, jt as audioModeOf, k as FailureCounters, kn as errMsg$1, kt as assertTimelapseCadences, l as AUDIO_MACRO_LABELS, ln as plateGalleryCapability, lt as NcTemplatePreviewSchema, m as COCO_TO_MACRO, mt as RECORDING_SOURCE_CAMSTACK_ADDON, nn as isVectorIndexDimConflict, nt as NcScheduleSchema, o as FACE_EMBEDDING_STEP_ID, on as occupancyScope, ot as NcSystemEventKindSchema, p as BirthDecisionRecordSchema, pn as resolveLocationMode, pt as RECORDING_EXPORT_MAX_READ_BYTES, q as NcAlarmConfigSchema, qn as number, qt as failureContributionCapability, rn as kebabToCamel, rt as NcSnoozeInputSchema, s as readClusterModelPinStrict, st as NcTaxonomySchema, t as BUILTIN_CLIP_MODELS, tn as isSystemEventSummaryRule, tt as NcRuleUpdateInputSchema, u as AlarmArmModeSchema, un as readDeviceStateFrom, ut as NcTemplateVarDescriptorSchema, vn as servedTemplateVars, vt as SCENE_DEFAULT_ANCHOR_THRESHOLD, w as EMBEDDING_REQUIREMENTS_UNAVAILABLE, wn as systemEventSummaryScheduleOf, wt as TimelapseRulePatchSchema, x as DeclaredDevices, xn as systemEventDeliveryProblems, xt as SceneMonitorSchema, y as DETECTION_MACRO_CLASSES, yn as storageOccupancyCapability, yt as SCENE_DEFAULT_UNCOVERED_POLICY, z as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, zn as nodePin } from "../clip-model-registry-Cudz--Ql.mjs";
1
+ import { $ as NcRuleSchema, $n as EventCategory, $t as isSourceCap, A as LabelAttributionSchema, An as DeviceType, At as audioMetricsCapability, B as NC_DEFAULT_SNOOZE_MINUTES, Bt as embeddingRefusalOf, C as EMBEDDING_REQUIREMENTS_FAILED, Cn as systemEventFilterApplies, Ct as TimelapseRuleInputSchema, D as FIRST_LEVEL_MACRO_CLASSES, Dn as vectorDimFromBase64, Dt as addonWidgetsSourceCapability, E as EVENT_OWNER_TYPES, En as templateVarsFor, Et as TrackSourceSchema, F as MOTION_CLOSE_AFTER_MS, Fn as customAction, Ft as deriveRecordingMode, G as NC_TAXONOMY, Gn as discriminatedUnion, Gt as evictionPolicyOfLocation, Hn as _enum, I as MediaFileKindEnum, In as defineCustomActions, It as describeSystemEventDeliveryProblem, J as NcAlarmSettingsPatchSchema, Jn as object, Jt as isDetectionMacroClass, K as NC_TEMPLATE_VARS, Kn as literal, Kt as faceGalleryCapability, L as NC_ALARM_SYSTEM_EVENT_KINDS, Ln as hydrateSchema, Lt as describeSystemEventFamilyProblem, M as MAX_ARCHIVED_DEBUG_NOTES, Mn as BaseAddon, Mt as buildEventKindDescriptor, N as MAX_BIRTH_DECISION_RECORDS, Nn as CamProfileSchema, Nt as consumableLowPercentOf, O as FULL_IMAGE_BBOX, On as zoneAnalyticsCapability, Ot as alarmPanelCapability, Pn as createEvent, Pt as cosineSimilarity$1, Q as NcRulePatchSchema, Qn as unknown, Qt as isScheduleActive, R as NC_CONDITION_CATALOG, Rn as isDeviceScopedCap, S as EMBEDDING_MODEL_FAILED, Sn as systemEventFamilyProblems, St as TIMELAPSE_DENSE_FLOOR_SEC, T as EVENT_KIND_BY_CAP, Tn as templateCarriesSummary, Tt as TimelapseRuleSchema, U as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, Un as array, Ut as encodeVectorBase64, V as NC_SENSOR_TRANSITION_KINDS, Vn as sleep$1, W as NC_TANK_SYSTEM_EVENT_KINDS, Wn as boolean, Wt as evaluateSensorEdge, X as NcConditionsSchema, Xn as record, Xt as isKnownTemplateVar, Y as NcConditionDescriptorSchema, Yn as partialRecord, Yt as isEventOwnerType, Z as NcRuleInputSchema, Zn as string, Zt as isMediaOwnerType, _n as sensorRaisedOf, a as CLIP_EMBEDDING_STEP_ID, an as notificationRulesCapability, at as NcSnoozeSuppressedSchema, bn as subKindsOf, bt as SCENE_DIVERGED, c as resolveClusterModelPin, cn as pipelineAnalyticsCapability, ct as NcTemplatePreviewInputSchema, d as ArchivedDebugNoteSchema, dn as readTimelapseGeneratedAt, dt as OpsLogEntrySchema, en as isSystemDelivery, et as NcRuleTargetSchema, f as BaseDevice, fn as resolveClipSearchMinScore, g as DEFAULT_EVENT_COLOR, gn as sceneMonitorCapability, gt as RetrainStatusSchema, h as CoverStateSchema, hn as ruleBelongsToAlarm$1, ht as RebuildObjectEmbeddingsRefusalSchema, in as mayWriteToLocation, it as NcSnoozeSchema, j as LockStateSchema, jn as errMsg$1, jt as audioModeOf, k as FailureCounters, kn as canonicalHash, kt as assertTimelapseCadences, l as AUDIO_MACRO_LABELS, ln as plateGalleryCapability, lt as NcTemplatePreviewSchema, m as COCO_TO_MACRO, mt as RECORDING_SOURCE_CAMSTACK_ADDON, nn as isVectorIndexDimConflict, nt as NcScheduleSchema, o as FACE_EMBEDDING_STEP_ID, on as occupancyScope, ot as NcSystemEventKindSchema, p as BirthDecisionRecordSchema, pn as resolveLocationMode, pt as RECORDING_EXPORT_MAX_READ_BYTES, q as NcAlarmConfigSchema, qn as number, qt as failureContributionCapability, rn as kebabToCamel, rt as NcSnoozeInputSchema, s as readClusterModelPinStrict, st as NcTaxonomySchema, t as BUILTIN_CLIP_MODELS, tn as isSystemEventSummaryRule, tt as NcRuleUpdateInputSchema, u as AlarmArmModeSchema, un as readDeviceStateFrom, ut as NcTemplateVarDescriptorSchema, vn as servedTemplateVars, vt as SCENE_DEFAULT_ANCHOR_THRESHOLD, w as EMBEDDING_REQUIREMENTS_UNAVAILABLE, wn as systemEventSummaryScheduleOf, wt as TimelapseRulePatchSchema, x as DeclaredDevices, xn as systemEventDeliveryProblems, xt as SceneMonitorSchema, y as DETECTION_MACRO_CLASSES, yn as storageOccupancyCapability, yt as SCENE_DEFAULT_UNCOVERED_POLICY, z as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, zn as nodePin } from "../clip-model-registry-D2zqXamW.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";
@@ -47029,6 +47029,42 @@ function pairRiders(detections) {
47029
47029
  return pairs;
47030
47030
  }
47031
47031
  //#endregion
47032
+ //#region src/pipeline-analytics/pipeline/tracker/low-score-continuation.ts
47033
+ var LOW_SCORE_CONTINUATION_FLOOR_DEFAULT = .35;
47034
+ var LOW_SCORE_CONTINUATION_CLASSES = new Set(["person", "animal"]);
47035
+ /** `[x1, y1, x2, y2]` (the discard trail's tuple) → the tracker's rectangle. */
47036
+ function toRect(bbox) {
47037
+ const [x1, y1, x2, y2] = bbox;
47038
+ return {
47039
+ x: x1,
47040
+ y: y1,
47041
+ w: x2 - x1,
47042
+ h: y2 - y1
47043
+ };
47044
+ }
47045
+ /**
47046
+ * The continuation candidates of one frame. `floor === null` means the tier is
47047
+ * off for this camera and nothing is selected.
47048
+ */
47049
+ function selectLowScoreContinuations(discarded, floor) {
47050
+ if (floor === null || discarded === void 0) return [];
47051
+ const out = [];
47052
+ for (const d of discarded) {
47053
+ if (d.reason !== "below-threshold") continue;
47054
+ if (d.score < floor) continue;
47055
+ if (!LOW_SCORE_CONTINUATION_CLASSES.has(d.macroClass)) continue;
47056
+ const bbox = toRect(d.bbox);
47057
+ if (bbox.w <= 0 || bbox.h <= 0) continue;
47058
+ out.push({
47059
+ class: d.macroClass,
47060
+ originalClass: d.macroClass,
47061
+ score: d.score,
47062
+ bbox
47063
+ });
47064
+ }
47065
+ return out;
47066
+ }
47067
+ //#endregion
47032
47068
  //#region src/pipeline-analytics/pipeline/track-id.ts
47033
47069
  /**
47034
47070
  * A track's identity: short, random, and mintable inside a frame.
@@ -47177,6 +47213,30 @@ function reacquireReachPx(bbox, elapsedMs) {
47177
47213
  const elapsed = Math.max(0, elapsedMs);
47178
47214
  return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
47179
47215
  }
47216
+ /**
47217
+ * Resurrection reach, in box DIAGONALS per second of gap (D665).
47218
+ *
47219
+ * A corpse is reached only after `maxMissedMs` (4 s) without a box, so the
47220
+ * rescue rate above — which is at its 1.5-diagonal ceiling by 1 s — says
47221
+ * nothing useful here. A subject the detector has not boxed for four seconds
47222
+ * is, on this fleet's evidence, one that stood still or stood half-hidden,
47223
+ * not one striding across the frame (a striding person is detected): 0.25
47224
+ * diagonals per second is a slow walk for a mid-field person and a pause for
47225
+ * a near one. At the 4 s floor of the graveyard band it gives 1.0 diagonal;
47226
+ * camera 590's refused returns sat at 1.05–1.82× the old flat 0.75 reach,
47227
+ * i.e. 0.79–1.37 diagonals, which this covers from about 5.5 s of gap.
47228
+ */
47229
+ var RESURRECTION_REACH_DIAGONALS_PER_SEC = .25;
47230
+ /** Never tighter than the flat `rescueCentroidFactor` default it replaces. */
47231
+ var RESURRECTION_REACH_MIN_DIAGONALS = .75;
47232
+ /**
47233
+ * How far from a corpse's last box a detection may sit and still resurrect it
47234
+ * on the proximity clause, for a corpse last seen `gapMs` ago.
47235
+ */
47236
+ function resurrectionReachPx(bbox, gapMs) {
47237
+ const diag = Math.hypot(bbox.w, bbox.h);
47238
+ return clampRange(diag * RESURRECTION_REACH_DIAGONALS_PER_SEC * (Math.max(0, gapMs) / 1e3), diag * RESURRECTION_REACH_MIN_DIAGONALS, diag * 2);
47239
+ }
47180
47240
  //#endregion
47181
47241
  //#region src/pipeline-analytics/pipeline/tracker/rescue-growth.ts
47182
47242
  /**
@@ -47325,7 +47385,10 @@ var DEFAULT_TRACKER_CONFIG = {
47325
47385
  byteTrackLowIouThreshold: .2,
47326
47386
  minSpawnAreaEnabled: true,
47327
47387
  classSpawnAreaFracs: { person: 5e-4 },
47328
- classGroupAssoc: true
47388
+ classGroupAssoc: true,
47389
+ lowScoreContinuationEnabled: true,
47390
+ lowScoreContinuationMaxMs: 8e3,
47391
+ resurrectionTimeScaled: true
47329
47392
  };
47330
47393
  /** Macro-classes the duplicate resolver may collapse across (people in
47331
47394
  * non-upright poses misdetected as animals). Any other cross-class pair is
@@ -47333,6 +47396,15 @@ var DEFAULT_TRACKER_CONFIG = {
47333
47396
  var PERSON_CLASS = "person";
47334
47397
  var ANIMAL_CLASS = "animal";
47335
47398
  var MAX_PATH_LENGTH = 300;
47399
+ function emptyLowScoreStats() {
47400
+ return {
47401
+ offered: 0,
47402
+ continued: 0,
47403
+ resumed: 0,
47404
+ sustainCapDrops: 0,
47405
+ shadowed: 0
47406
+ };
47407
+ }
47336
47408
  /** No stationary-held detections on this frame — the default for every caller
47337
47409
  * that has no gate (replay, tests, the non-pipeline planes). */
47338
47410
  var EMPTY_INDEX_SET = /* @__PURE__ */ new Set();
@@ -47404,6 +47476,8 @@ var SortTracker = class SortTracker {
47404
47476
  trackLossSampler = new TrackLossSampler();
47405
47477
  /** Sink for fragmentation diagnostics — see {@link setSpawnBesideCoastingObserver}. */
47406
47478
  spawnBesideObserver = null;
47479
+ /** Running counts behind {@link drainLowScoreStats} (D665). */
47480
+ lowScoreStats = emptyLowScoreStats();
47407
47481
  constructor(config = {}) {
47408
47482
  this.config = {
47409
47483
  ...DEFAULT_TRACKER_CONFIG,
@@ -47464,7 +47538,7 @@ var SortTracker = class SortTracker {
47464
47538
  if (SortTracker.spawnBesideFamily(track.class) !== family) continue;
47465
47539
  const tc = bboxCentroid(track.bbox);
47466
47540
  const dist = Math.hypot(tc.x - dc.x, tc.y - dc.y);
47467
- const reach = this.reacquireReach(track, timestamp, true);
47541
+ const reach = this.reacquireReach(track, timestamp, "rescue");
47468
47542
  if (dist > reach * 2) continue;
47469
47543
  if (dist < bestDist) {
47470
47544
  best = track;
@@ -47511,7 +47585,7 @@ var SortTracker = class SortTracker {
47511
47585
  explainDecline(track, det, detections, frameArea, timestamp, centroidDist) {
47512
47586
  if (track.class !== det.class) return det.nativeRecovery !== void 0 ? "native-measured-class" : "cross-class";
47513
47587
  if (iou$2(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "already-claimed";
47514
- if (centroidDist <= this.reacquireReach(track, timestamp, true)) {
47588
+ if (centroidDist <= this.reacquireReach(track, timestamp, "rescue")) {
47515
47589
  if (!this.isPlausibleBirth(det, frameArea)) return "already-claimed";
47516
47590
  if (!this.config.reacquireTimeScaled) return "plausible-birth-veto";
47517
47591
  return this.isUnambiguousReach(track, det, detections) ? "already-claimed" : "crowd-guard";
@@ -47966,34 +48040,38 @@ var SortTracker = class SortTracker {
47966
48040
  * this gate crossed the group, and it is where a `person` at 0.86 took over
47967
48041
  * an animal's id (camera 615, 17:11:01).
47968
48042
  */
47969
- looseMatch(track, det, timestamp, timeScaled = true) {
48043
+ looseMatch(track, det, timestamp, pass = "rescue") {
47970
48044
  if (track.class !== det.class) return null;
47971
48045
  if (iou$2(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
47972
48046
  const tc = bboxCentroid(track.bbox);
47973
48047
  const dc = bboxCentroid(det.bbox);
47974
- if (Math.hypot(tc.x - dc.x, tc.y - dc.y) <= this.reacquireReach(track, timestamp, timeScaled)) return "centroid";
48048
+ if (Math.hypot(tc.x - dc.x, tc.y - dc.y) <= this.reacquireReach(track, timestamp, pass)) return "centroid";
47975
48049
  if (growthAdjacentMatch(track.bbox, det.bbox, this.growthContext(track, timestamp))) return "growth";
47976
48050
  return null;
47977
48051
  }
47978
48052
  /**
47979
48053
  * The proximity budget the `centroid` clause is measured against.
47980
48054
  *
47981
- * On the RESCUE pass (`timeScaled`) it is {@link reacquireReachPx} — a
47982
- * multiple of the track's diagonal that grows with the elapsed gap and stops
47983
- * at a ceiling, because a live coasting track's whereabouts genuinely become
47984
- * less certain the longer it has gone unseen (D399).
47985
- *
47986
- * On the RESURRECTION pass it stays the flat `rescueCentroidFactor × diagonal`
47987
- * it always was. That pass re-attaches a CORPSE, and it exists for one thing:
47988
- * "a flickering stationary object keeps its identity". Elapsed time is not the
47989
- * variable there — a stationary subject does not travel further because it
47990
- * blinked for longer — and the corpse is strictly weaker evidence than a live
47991
- * track, which is the same reasoning `resurrectionCrowdedIouThreshold` already
47992
- * encodes.
47993
- */
47994
- reacquireReach(track, timestamp, timeScaled) {
48055
+ * On the RESCUE pass it is {@link reacquireReachPx} — a multiple of the
48056
+ * track's diagonal that grows with the elapsed gap and stops at a ceiling,
48057
+ * because a live coasting track's whereabouts genuinely become less certain
48058
+ * the longer it has gone unseen (D399).
48059
+ *
48060
+ * On the RESURRECTION pass it is {@link resurrectionReachPx} (D665): the same
48061
+ * idea on the graveyard's own time scale — 0.25 diagonals per second of gap,
48062
+ * never below the flat 0.75 it replaces, never above 2. Until D665 this pass
48063
+ * kept the flat `rescueCentroidFactor × diagonal` on the argument that it
48064
+ * exists for "a flickering stationary object", and a stationary subject does
48065
+ * not travel further because it blinked for longer. True of the object it
48066
+ * was written for; not of the pass as it runs: on camera 590 (2026-09-27)
48067
+ * 38% of all re-splits were PEOPLE returning after 4–12 s, further than the
48068
+ * flat gate, with frames flowing the whole time. `resurrectionTimeScaled`
48069
+ * off restores the flat gate.
48070
+ */
48071
+ reacquireReach(track, timestamp, pass) {
47995
48072
  const diag = Math.hypot(track.bbox.w, track.bbox.h);
47996
- if (!timeScaled || !this.config.reacquireTimeScaled) return this.config.rescueCentroidFactor * diag;
48073
+ if (pass === "resurrection") return this.config.resurrectionTimeScaled ? resurrectionReachPx(track.bbox, timestamp - track.lastSeen) : this.config.rescueCentroidFactor * diag;
48074
+ if (!this.config.reacquireTimeScaled) return this.config.rescueCentroidFactor * diag;
47997
48075
  return reacquireReachPx(track.bbox, timestamp - track.lastSeen);
47998
48076
  }
47999
48077
  /**
@@ -48043,6 +48121,23 @@ var SortTracker = class SortTracker {
48043
48121
  nearestPx: nearest
48044
48122
  };
48045
48123
  }
48124
+ /**
48125
+ * The resurrection counterpart of {@link isUnambiguousReach}: no other
48126
+ * same-class subject is live or resurrectable beside `corpse`
48127
+ * ({@link sameClassPeerCount} over the SURVIVING set, which is what the crowd
48128
+ * guard counts), and `det` is the only box of that class on the frame —
48129
+ * the sub-floor `lowScore` boxes included.
48130
+ */
48131
+ isSoleSubjectResurrection(corpse, det, detections, surviving, lowScore) {
48132
+ if (this.sameClassPeerCount(corpse, surviving) > 0) return false;
48133
+ for (const other of detections) if (other !== det && other.class === corpse.class) return false;
48134
+ for (const other of lowScore) {
48135
+ if (other.class !== corpse.class) continue;
48136
+ if (iou$2(other.bbox, det.bbox) >= this.config.byteTrackLowIouThreshold) continue;
48137
+ return false;
48138
+ }
48139
+ return true;
48140
+ }
48046
48141
  isUnambiguousReach(track, det, detections) {
48047
48142
  if (this.sameClassPeerCount(track, this.tracks) > 0) return false;
48048
48143
  for (const other of detections) {
@@ -48197,6 +48292,64 @@ var SortTracker = class SortTracker {
48197
48292
  used.add(pair.detIdx);
48198
48293
  }
48199
48294
  }
48295
+ /** A CONFIDENT box fed `track` (match, resurrection): the emitted box
48296
+ * follows it again, and a hold that ends this way is an id the tier kept. */
48297
+ markConfident(track, bbox, timestamp) {
48298
+ track.confidentBbox = bbox;
48299
+ track.confidentSeenAt = timestamp;
48300
+ if (track.lowScoreHeld) {
48301
+ track.lowScoreHeld = false;
48302
+ this.lowScoreStats.resumed++;
48303
+ }
48304
+ }
48305
+ /**
48306
+ * The low-score continuation stage (D665): greedy, highest IoU first, of the
48307
+ * sub-floor boxes against the CONFIRMED tracks nothing claimed this frame,
48308
+ * same class, on the predicted box at `byteTrackLowIouThreshold`.
48309
+ *
48310
+ * A bound box moves the track's association state only — `bbox`, `lastSeen`,
48311
+ * `age` — so the next confident box meets the subject where it is. Not
48312
+ * `hits` (it confirms nothing), not `score`, `class`, the vote, the path or
48313
+ * the velocity (it describes nothing downstream may act on). A track past
48314
+ * `lowScoreContinuationMaxMs` since its last confident box is not offered.
48315
+ */
48316
+ continueOnLowScore(established, lowScore, timestamp, matchedTracks, matched) {
48317
+ const claimed = [...matched.values()].map((d) => d.bbox);
48318
+ const candidates = lowScore.filter((det) => {
48319
+ const shadow = claimed.some((b) => iou$2(b, det.bbox) >= this.config.byteTrackLowIouThreshold);
48320
+ if (shadow) this.lowScoreStats.shadowed++;
48321
+ return !shadow;
48322
+ });
48323
+ const pairs = [];
48324
+ for (const track of established) {
48325
+ if (matchedTracks.has(track)) continue;
48326
+ if (timestamp - track.confidentSeenAt > this.config.lowScoreContinuationMaxMs) continue;
48327
+ const pbox = this.predicted(track, timestamp);
48328
+ for (const det of candidates) {
48329
+ if (det.class !== track.class) continue;
48330
+ const score = iou$2(pbox, det.bbox);
48331
+ if (score >= this.config.byteTrackLowIouThreshold) pairs.push({
48332
+ track,
48333
+ det,
48334
+ score
48335
+ });
48336
+ }
48337
+ }
48338
+ pairs.sort((a, b) => b.score - a.score);
48339
+ const usedTracks = /* @__PURE__ */ new Set();
48340
+ const usedDets = /* @__PURE__ */ new Set();
48341
+ for (const pair of pairs) {
48342
+ if (usedTracks.has(pair.track) || usedDets.has(pair.det)) continue;
48343
+ usedTracks.add(pair.track);
48344
+ usedDets.add(pair.det);
48345
+ pair.track.bbox = pair.det.bbox;
48346
+ pair.track.lastSeen = timestamp;
48347
+ pair.track.lastSeenFrameSeq = this.frameSeq;
48348
+ pair.track.age = 0;
48349
+ pair.track.lowScoreHeld = true;
48350
+ this.lowScoreStats.continued++;
48351
+ }
48352
+ }
48200
48353
  update(detections, timestamp, frameContext) {
48201
48354
  this.frameSeq++;
48202
48355
  const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
@@ -48254,6 +48407,9 @@ var SortTracker = class SortTracker {
48254
48407
  this.rescueAssociate(established, PROXIMITY, detections, lowSet, frameArea, timestamp, used, matchedTracks, matched, continuationOnly);
48255
48408
  this.rescueAssociate(provisional, new Set([...OVERLAP, ...PROXIMITY]), detections, lowSet, frameArea, timestamp, used, matchedTracks, matched, continuationOnly);
48256
48409
  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);
48410
+ const lowScore = frameContext?.lowScore ?? [];
48411
+ this.lowScoreStats.offered += lowScore.length;
48412
+ if (this.config.lowScoreContinuationEnabled && lowScore.length > 0) this.continueOnLowScore(established, lowScore, timestamp, matchedTracks, matched);
48257
48413
  for (const [track, det] of matched) {
48258
48414
  if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, {
48259
48415
  against: track,
@@ -48284,6 +48440,7 @@ var SortTracker = class SortTracker {
48284
48440
  track.hits++;
48285
48441
  track.lastSeen = timestamp;
48286
48442
  track.lastSeenFrameSeq = this.frameSeq;
48443
+ this.markConfident(track, det.bbox, timestamp);
48287
48444
  if (dtMs > 0) {
48288
48445
  track.velocity = {
48289
48446
  dx: (newCenter.x - prevCenter.x) / dtMs,
@@ -48308,10 +48465,15 @@ var SortTracker = class SortTracker {
48308
48465
  }
48309
48466
  const occluded = this.config.occlusionEnabled && occluders.some((ob) => containment(this.predicted(track, timestamp), ob.bbox) >= this.config.occlusionContainment);
48310
48467
  track.age++;
48311
- if (timestamp - track.lastSeen > (occluded ? this.config.occlusionMaxMissedMs : this.config.maxMissedMs)) {
48468
+ const coastMs = timestamp - track.lastSeen;
48469
+ const limitMs = occluded ? this.config.occlusionMaxMissedMs : this.config.maxMissedMs;
48470
+ const heldTooLong = track.lowScoreHeld && timestamp - track.confidentSeenAt > this.config.lowScoreContinuationMaxMs;
48471
+ if (heldTooLong) this.lowScoreStats.sustainCapDrops++;
48472
+ if (coastMs > limitMs || heldTooLong) {
48312
48473
  track.lost = true;
48313
48474
  track.lostAt = timestamp;
48314
48475
  track.resurrectable = true;
48476
+ track.lowScoreHeld = false;
48315
48477
  this.lostTracks.push(track);
48316
48478
  } else surviving.push(track);
48317
48479
  }
@@ -48322,15 +48484,17 @@ var SortTracker = class SortTracker {
48322
48484
  let bestScore = -1;
48323
48485
  let bestGate = "iou";
48324
48486
  let bestPeers = 0;
48487
+ let bestVetoLifted = false;
48325
48488
  let refused;
48326
48489
  let refusedGate = "iou";
48327
48490
  let refusedScore = -1;
48328
48491
  for (const lost of this.lostTracks) {
48329
48492
  if (!lost.resurrectable) continue;
48330
48493
  if (timestamp - lost.lostAt > this.config.resurrectionWindowMs) continue;
48331
- const gate = this.looseMatch(lost, det, timestamp, false);
48494
+ const gate = this.looseMatch(lost, det, timestamp, "resurrection");
48332
48495
  if (gate === null) continue;
48333
- if (gate === "centroid" && this.isPlausibleBirth(det, frameArea)) continue;
48496
+ const vetoLifted = gate === "centroid" && this.isPlausibleBirth(det, frameArea);
48497
+ if (vetoLifted && !(this.config.resurrectionTimeScaled && this.isSoleSubjectResurrection(lost, det, detections, surviving, lowScore))) continue;
48334
48498
  const score = iou$2(lost.bbox, det.bbox);
48335
48499
  const peers = this.sameClassPeerCount(lost, surviving);
48336
48500
  if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
@@ -48346,13 +48510,14 @@ var SortTracker = class SortTracker {
48346
48510
  bestScore = score;
48347
48511
  bestGate = gate;
48348
48512
  bestPeers = peers;
48513
+ bestVetoLifted = vetoLifted;
48349
48514
  }
48350
48515
  }
48351
48516
  if (!best) {
48352
48517
  if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp);
48353
48518
  continue;
48354
48519
  }
48355
- resurrectedScene.set(best, bestPeers > 0 ? "multi-subject" : "sole-subject");
48520
+ resurrectedScene.set(best, bestPeers > 0 ? "multi-subject" : bestVetoLifted ? "sole-subject-veto-lifted" : "sole-subject");
48356
48521
  this.reportBind("resurrection", bestGate, best, det, timestamp);
48357
48522
  this.lostTracks = this.lostTracks.filter((t) => t !== best);
48358
48523
  best.bbox = det.bbox;
@@ -48366,6 +48531,7 @@ var SortTracker = class SortTracker {
48366
48531
  best.lost = false;
48367
48532
  best.lastSeen = timestamp;
48368
48533
  best.lastSeenFrameSeq = this.frameSeq;
48534
+ this.markConfident(best, det.bbox, timestamp);
48369
48535
  best.velocity = {
48370
48536
  dx: 0,
48371
48537
  dy: 0
@@ -48442,7 +48608,10 @@ var SortTracker = class SortTracker {
48442
48608
  classVotes: new Map([[det.class, det.score > 0 ? det.score : Number.EPSILON]]),
48443
48609
  emitted: false,
48444
48610
  lossReported: false,
48445
- lastNativeRecovery: det.nativeRecovery
48611
+ lastNativeRecovery: det.nativeRecovery,
48612
+ confidentBbox: det.bbox,
48613
+ confidentSeenAt: timestamp,
48614
+ lowScoreHeld: false
48446
48615
  });
48447
48616
  }
48448
48617
  this.tracks = surviving;
@@ -48450,11 +48619,12 @@ var SortTracker = class SortTracker {
48450
48619
  return this.tracks.filter((t) => this.isConfirmedForEmit(t)).map((t) => {
48451
48620
  t.emitted = true;
48452
48621
  const scene = resurrectedScene.get(t);
48622
+ const held = t.lowScoreHeld;
48453
48623
  return {
48454
48624
  class: this.resolveReportedClass(t),
48455
48625
  originalClass: t.originalClass,
48456
48626
  score: t.score,
48457
- bbox: t.bbox,
48627
+ bbox: held ? t.confidentBbox : t.bbox,
48458
48628
  trackId: t.id,
48459
48629
  trackAge: t.hits,
48460
48630
  velocity: {
@@ -48462,8 +48632,9 @@ var SortTracker = class SortTracker {
48462
48632
  dy: t.velocity.dy * t.emitIntervalMs
48463
48633
  },
48464
48634
  path: [...t.path],
48465
- matchedThisFrame: t.lastSeen === timestamp,
48466
- ...scene !== void 0 ? { resurrectionScene: scene } : {}
48635
+ matchedThisFrame: t.lastSeen === timestamp && !held,
48636
+ ...scene !== void 0 ? { resurrectionScene: scene } : {},
48637
+ ...held ? { heldByLowScore: true } : {}
48467
48638
  };
48468
48639
  });
48469
48640
  }
@@ -48477,6 +48648,16 @@ var SortTracker = class SortTracker {
48477
48648
  this.tracks = this.tracks.filter((t) => t.id !== trackId);
48478
48649
  this.lostTracks = this.lostTracks.filter((t) => t.id !== trackId);
48479
48650
  }
48651
+ /**
48652
+ * What the low-score continuation stage did since the last call, and reset
48653
+ * (D665). The tracker has no `deviceId` and no logger; its owner reads this
48654
+ * on its own cadence and logs it per camera.
48655
+ */
48656
+ drainLowScoreStats() {
48657
+ const out = this.lowScoreStats;
48658
+ this.lowScoreStats = emptyLowScoreStats();
48659
+ return out;
48660
+ }
48480
48661
  getActiveTracks() {
48481
48662
  return this.tracks;
48482
48663
  }
@@ -48851,6 +49032,8 @@ var FrameProcessor = class {
48851
49032
  detectionRules;
48852
49033
  /** See {@link setZoneMembershipMinOverlap}. 0 = any positive overlap. */
48853
49034
  zoneMembershipMinOverlap;
49035
+ /** See {@link setLowScoreContinuationFloor}. `null` = tier off. */
49036
+ lowScoreFloor = LOW_SCORE_CONTINUATION_FLOOR_DEFAULT;
48854
49037
  /** See {@link getLastZoneOverlaps}. */
48855
49038
  lastZoneOverlaps;
48856
49039
  /** See {@link getLastZoneRejections}. */
@@ -48963,6 +49146,18 @@ var FrameProcessor = class {
48963
49146
  * failure mode this whole area keeps producing. {@link lastZoneOverlaps}
48964
49147
  * exists so the distribution can be read before a number is picked.
48965
49148
  */
49149
+ /**
49150
+ * The score at or above which a box the operator's class floor dropped is
49151
+ * handed to the tracker as low-score continuation evidence (D665); `null`
49152
+ * turns the tier off for this camera. Pushed per frame, like the zones.
49153
+ */
49154
+ setLowScoreContinuationFloor(floor) {
49155
+ this.lowScoreFloor = floor !== null && Number.isFinite(floor) && floor >= 0 && floor <= 1 ? floor : null;
49156
+ }
49157
+ /** The tracker's low-score continuation counts since the last call (D665). */
49158
+ drainLowScoreStats() {
49159
+ return this.tracker.drainLowScoreStats();
49160
+ }
48966
49161
  setZoneMembershipMinOverlap(minOverlap) {
48967
49162
  this.zoneMembershipMinOverlap = Number.isFinite(minOverlap) && minOverlap >= 0 && minOverlap <= 1 ? minOverlap : 0;
48968
49163
  }
@@ -49355,10 +49550,16 @@ var FrameProcessor = class {
49355
49550
  }
49356
49551
  trackerInput.push(det);
49357
49552
  }
49553
+ const lowCandidates = selectLowScoreContinuations(frame.discarded, this.lowScoreFloor);
49554
+ const lowScore = lowCandidates.length === 0 ? lowCandidates : this.zoneEngine.filterDetections(lowCandidates.map((d) => ({
49555
+ ...d.bbox,
49556
+ detection: d
49557
+ })), this.zones, this.detectionRules, frameWidth, frameHeight, (fd) => fd.detection.class).passed.map((fd) => fd.detection);
49358
49558
  const trackedDetections = this.tracker.update(trackerInput, timestamp, {
49359
49559
  frameWidth,
49360
49560
  frameHeight,
49361
- continuationOnly
49561
+ continuationOnly,
49562
+ lowScore
49362
49563
  });
49363
49564
  const stationaryContinued = continuableBoxes.size === 0 ? 0 : trackedDetections.filter((t) => t.matchedThisFrame !== false && continuableBoxes.has(t.bbox)).length;
49364
49565
  const riderFolds = foldByVehicleBbox.size === 0 ? EMPTY_RIDER_FOLDS : [...foldByVehicleBbox].map(([bbox, fold]) => {
@@ -49600,6 +49801,19 @@ var FrameProcessor = class {
49600
49801
  }
49601
49802
  };
49602
49803
  //#endregion
49804
+ //#region src/pipeline-analytics/face-identity-retraction.ts
49805
+ /** The recognizer's step id — the attribution a face identity label carries. */
49806
+ var FACE_RECOGNIZER_STEP_ID = "face-recognizer";
49807
+ async function retractFaceIdentityLabel(stores, trackId) {
49808
+ const meta = stores.trackStore.getActiveByTrack(trackId)?.subLabelMeta;
49809
+ if (meta === void 0) return "no-label";
49810
+ if (meta.stepId !== "face-recognizer") return "not-face-recognizer";
49811
+ await stores.eventStore.clearLabelForTrack(trackId, 2);
49812
+ await stores.trackStore.clearLabel(trackId, 2);
49813
+ await stores.recomputeImportance?.(trackId);
49814
+ return "retracted";
49815
+ }
49816
+ //#endregion
49603
49817
  //#region src/pipeline-analytics/pipeline/track-provenance.ts
49604
49818
  function trackProvenanceOf(frame) {
49605
49819
  if (!(frame.width > 0 && frame.height > 0)) return void 0;
@@ -63204,8 +63418,6 @@ function faceSizeScore(detectionFrameShortSidePx) {
63204
63418
  }
63205
63419
  //#endregion
63206
63420
  //#region src/pipeline-analytics/face-recognizer.ts
63207
- /** Attribution of an identity resolved by the in-process ArcFace matcher. */
63208
- var FACE_RECOGNIZER_STEP_ID = "face-recognizer";
63209
63421
  /** At most one "dropping imageless track" log per this interval, per recognizer. */
63210
63422
  var FACE_IMAGELESS_LOG_THROTTLE_MS = 6e4;
63211
63423
  /** At most one 'embedding magnitude' sample line per device per this interval. */
@@ -63231,6 +63443,9 @@ var FaceRecognizer = class {
63231
63443
  gallery = [];
63232
63444
  names = /* @__PURE__ */ new Map();
63233
63445
  aggregates = /* @__PURE__ */ new Map();
63446
+ /** The aggregate a veto-lifted quarantine set aside, restored if the first
63447
+ * face proves the same person (D665). */
63448
+ stashedIdentity = /* @__PURE__ */ new Map();
63234
63449
  bestFace = /* @__PURE__ */ new Map();
63235
63450
  /** Frame instant of each live track's last source park (D644). */
63236
63451
  lastSourceParkAt = /* @__PURE__ */ new Map();
@@ -63422,9 +63637,35 @@ var FaceRecognizer = class {
63422
63637
  * • the face becomes non-promotable: {@link onTrackEnd} will not write it to
63423
63638
  * the gallery while the flag stands.
63424
63639
  *
63425
- * The aggregate is deliberately left alone — see the note in `onTrackEnd`.
63426
- */
63427
- quarantineHeldFace(deviceId, trackId) {
63640
+ * The identity AGGREGATE depends on the scene (D665):
63641
+ *
63642
+ * • `multi-subject` — a bind that needed real overlap (IoU ≥ 0.3) with the
63643
+ * corpse's last box. The aggregate is LEFT ALONE: the next face either
63644
+ * confirms (same person, name intact at no cost — the common case, a
63645
+ * seated person occluded for a few seconds) or is rejected by cosine, and
63646
+ * a rejection is a PROVEN subject change: the aggregate is deleted and the
63647
+ * written identity label retracted then.
63648
+ * • `sole-subject-veto-lifted` — a bind on proximity alone. The aggregate is
63649
+ * STASHED and reset, and the written identity label retracted at once:
63650
+ * `updateTrackAggregate` keeps an assignment through every unmatched frame,
63651
+ * so without the reset a stranger who never matched anybody would carry
63652
+ * the name. The first face-bearing frame decides — `confirmed` restores the
63653
+ * stash and rewrites the label (the same person, back immediately);
63654
+ * `rejected`, or no face before the track ends, discards it.
63655
+ */
63656
+ async quarantineHeldFace(deviceId, trackId, scene) {
63657
+ if (scene === "sole-subject-veto-lifted") {
63658
+ const aggregate = this.aggregates.get(trackId);
63659
+ if (aggregate?.assignedIdentityId != null) {
63660
+ const peak = this.bestMatchScore.get(trackId);
63661
+ this.stashedIdentity.set(trackId, {
63662
+ aggregate,
63663
+ ...peak !== void 0 ? { bestMatchScore: peak } : {}
63664
+ });
63665
+ this.aggregates.delete(trackId);
63666
+ await this.retractIdentity(deviceId, trackId, scene, aggregate.assignedIdentityId);
63667
+ } else this.aggregates.delete(trackId);
63668
+ }
63428
63669
  const held = this.bestFace.get(trackId);
63429
63670
  if (held === void 0 || held.unverified === true) return;
63430
63671
  const quarantined = {
@@ -63447,6 +63688,7 @@ var FaceRecognizer = class {
63447
63688
  trackId
63448
63689
  },
63449
63690
  meta: {
63691
+ scene,
63450
63692
  heldScore: held.score,
63451
63693
  heldAt: held.timestamp,
63452
63694
  strippedIdentityId: held.recognizedIdentityId ?? null,
@@ -63455,6 +63697,40 @@ var FaceRecognizer = class {
63455
63697
  });
63456
63698
  }
63457
63699
  /**
63700
+ * Take the identity label back from the track and its events — only when the
63701
+ * face recognizer wrote it (`retractFaceIdentityLabel`). Stated either way.
63702
+ */
63703
+ async retractIdentity(deviceId, trackId, reason, identityId) {
63704
+ const retract = this.deps.retractIdentityLabel;
63705
+ if (retract === void 0) return;
63706
+ try {
63707
+ const outcome = await retract(trackId);
63708
+ this.deps.logger.info("face: identity label retracted after an unverified re-attach", {
63709
+ tags: {
63710
+ deviceId,
63711
+ trackId
63712
+ },
63713
+ meta: {
63714
+ reason,
63715
+ identityId,
63716
+ outcome
63717
+ }
63718
+ });
63719
+ } catch (err) {
63720
+ this.deps.logger.warn("face: identity label retraction failed", {
63721
+ tags: {
63722
+ deviceId,
63723
+ trackId
63724
+ },
63725
+ meta: {
63726
+ reason,
63727
+ identityId,
63728
+ error: String(err)
63729
+ }
63730
+ });
63731
+ }
63732
+ }
63733
+ /**
63458
63734
  * Ask a FRESH face whether the quarantined held face is still about the same
63459
63735
  * person, at the same bar the gallery uses to say two vectors are one person
63460
63736
  * (`similarityThreshold` — one knob over one band, D222).
@@ -63476,7 +63752,7 @@ var FaceRecognizer = class {
63476
63752
  }
63477
63753
  async processFrame(input) {
63478
63754
  const { settings } = input;
63479
- for (const t of input.tracked) if (t.resurrectionScene === "multi-subject") this.quarantineHeldFace(input.deviceId, t.trackId);
63755
+ for (const t of input.tracked) if (t.resurrectionScene !== void 0 && t.resurrectionScene !== "sole-subject") await this.quarantineHeldFace(input.deviceId, t.trackId, t.resurrectionScene);
63480
63756
  const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && (t.nativeFaceShortSidePx ?? (t.faceBbox ? Math.min(t.faceBbox.w, t.faceBbox.h) : void 0) ?? Number.POSITIVE_INFINITY) >= settings.minFacePx);
63481
63757
  if (candidates.length === 0) return;
63482
63758
  this.deps.logger.debug("face: frame candidates", {
@@ -63543,11 +63819,26 @@ var FaceRecognizer = class {
63543
63819
  embedding: c.embedding,
63544
63820
  embeddingModelId: c.embeddingModelId
63545
63821
  }, settings.similarityThreshold);
63546
- if (verdict === "confirmed") this.bestFace.set(c.trackId, {
63547
- ...quarantined,
63548
- unverified: false
63549
- });
63550
- else if (verdict === "rejected") {
63822
+ const stashed = this.stashedIdentity.get(c.trackId);
63823
+ this.stashedIdentity.delete(c.trackId);
63824
+ if (verdict === "confirmed") {
63825
+ this.bestFace.set(c.trackId, {
63826
+ ...quarantined,
63827
+ unverified: false
63828
+ });
63829
+ const name = stashed?.aggregate.assignedIdentityId;
63830
+ if (stashed !== void 0 && name != null) {
63831
+ this.aggregates.set(c.trackId, stashed.aggregate);
63832
+ if (stashed.bestMatchScore !== void 0) this.bestMatchScore.set(c.trackId, stashed.bestMatchScore);
63833
+ labelWork.push({
63834
+ trackId: c.trackId,
63835
+ assignedIdentityId: name,
63836
+ matchScore: stashed.bestMatchScore ?? null
63837
+ });
63838
+ }
63839
+ } else if (verdict === "rejected") {
63840
+ const named = this.aggregates.get(c.trackId)?.assignedIdentityId;
63841
+ if (named != null) await this.retractIdentity(input.deviceId, c.trackId, "rejected", named);
63551
63842
  this.bestFace.delete(c.trackId);
63552
63843
  this.bestTracker.delete(c.trackId);
63553
63844
  this.aggregates.delete(c.trackId);
@@ -63762,6 +64053,7 @@ var FaceRecognizer = class {
63762
64053
  const bestMatchScore = this.bestMatchScore.get(trackId);
63763
64054
  const bestSuggestion = this.bestSuggestion.get(trackId);
63764
64055
  this.aggregates.delete(trackId);
64056
+ this.stashedIdentity.delete(trackId);
63765
64057
  this.bestFace.delete(trackId);
63766
64058
  this.bestTracker.delete(trackId);
63767
64059
  this.bestMatchScore.delete(trackId);
@@ -67923,6 +68215,19 @@ function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSki
67923
68215
  if (activeTrackCount <= 0) return false;
67924
68216
  return consecutiveSkips < cfg.maxSkipFrames;
67925
68217
  }
68218
+ /**
68219
+ * How much evidence a frame carries for the dropout decision: its detections,
68220
+ * plus the sub-floor boxes the tracker may continue a track on (D665).
68221
+ *
68222
+ * Until D665 the skip counted `frame.detections` only, so a frame whose sole
68223
+ * box was a person at 0.5 read as a detector glitch and never reached the
68224
+ * tracker — on camera 590 more than half the frames of a re-split gap were
68225
+ * dropout-skipped. A frame with a continuable box is not empty; `floor` is the
68226
+ * camera's low-score floor, `null` when the tier is off.
68227
+ */
68228
+ function dropoutEvidenceCount(frame, floor) {
68229
+ return frame.detections.length + selectLowScoreContinuations(frame.discarded, floor).length;
68230
+ }
67926
68231
  //#endregion
67927
68232
  //#region src/pipeline-analytics/pipeline/key-event-query.ts
67928
68233
  async function rankKeyEvents(candidates, options, peakLookup) {
@@ -82876,7 +83181,21 @@ var TrackingSettingsSchema = object({
82876
83181
  * passes, only for a coarse box below `animalOverPersonMaxScore`, never on
82877
83182
  * the rescue / resurrection passes. Reported label stays via the class
82878
83183
  * vote. Off = exact per-frame class gating. */
82879
- classGroupAssoc: boolean().default(true)
83184
+ classGroupAssoc: boolean().default(true),
83185
+ /** Hand the boxes the per-class confidence floor dropped to the tracker as
83186
+ * CONTINUATION-ONLY evidence (D665): they may keep a confirmed track alive,
83187
+ * never open one, never produce media, events or notifications. */
83188
+ lowScoreContinuationEnabled: boolean().default(true),
83189
+ /** Score at/above which a floor-dropped box counts as continuation evidence
83190
+ * (D665 — justified in `low-score-continuation.ts`). */
83191
+ lowScoreContinuationFloor: number().min(0).max(1).default(.35),
83192
+ /** Longest a track may be held by floor-dropped boxes alone, from its last
83193
+ * confident box (ms). */
83194
+ lowScoreContinuationMaxMs: number().min(0).default(8e3),
83195
+ /** Graveyard reach scaled by the gap, and the plausible-birth veto lifted for
83196
+ * a sole subject (D665). Off = the flat pre-D665 resurrection gate — the
83197
+ * per-camera rollback that needs no redeploy. */
83198
+ resurrectionTimeScaled: boolean().default(true)
82880
83199
  });
82881
83200
  var TRACKING_DEFAULTS = TrackingSettingsSchema.parse({});
82882
83201
  /**
@@ -82931,7 +83250,11 @@ function resolveTrackingSettings(raw) {
82931
83250
  minSpawnAreaFracPerson: s.minSpawnAreaFracPerson.catch(TRACKING_DEFAULTS.minSpawnAreaFracPerson).parse(raw.minSpawnAreaFracPerson),
82932
83251
  minSpawnAreaFracAnimal: s.minSpawnAreaFracAnimal.catch(TRACKING_DEFAULTS.minSpawnAreaFracAnimal).parse(raw.minSpawnAreaFracAnimal),
82933
83252
  minSpawnAreaFracVehicle: s.minSpawnAreaFracVehicle.catch(TRACKING_DEFAULTS.minSpawnAreaFracVehicle).parse(raw.minSpawnAreaFracVehicle),
82934
- classGroupAssoc: s.classGroupAssoc.catch(TRACKING_DEFAULTS.classGroupAssoc).parse(raw.classGroupAssoc)
83253
+ classGroupAssoc: s.classGroupAssoc.catch(TRACKING_DEFAULTS.classGroupAssoc).parse(raw.classGroupAssoc),
83254
+ lowScoreContinuationEnabled: s.lowScoreContinuationEnabled.catch(TRACKING_DEFAULTS.lowScoreContinuationEnabled).parse(raw.lowScoreContinuationEnabled),
83255
+ lowScoreContinuationFloor: s.lowScoreContinuationFloor.catch(TRACKING_DEFAULTS.lowScoreContinuationFloor).parse(raw.lowScoreContinuationFloor),
83256
+ lowScoreContinuationMaxMs: s.lowScoreContinuationMaxMs.catch(TRACKING_DEFAULTS.lowScoreContinuationMaxMs).parse(raw.lowScoreContinuationMaxMs),
83257
+ resurrectionTimeScaled: s.resurrectionTimeScaled.catch(TRACKING_DEFAULTS.resurrectionTimeScaled).parse(raw.resurrectionTimeScaled)
82935
83258
  };
82936
83259
  }
82937
83260
  //#endregion
@@ -83361,6 +83684,40 @@ function buildDetectionSettingsSections() {
83361
83684
  step: .05,
83362
83685
  default: TRACKING_DEFAULTS.byteTrackLowIouThreshold
83363
83686
  },
83687
+ {
83688
+ type: "boolean",
83689
+ key: "lowScoreContinuationEnabled",
83690
+ label: "Keep tracks through low-confidence frames",
83691
+ description: "Boxes below the class confidence floor may keep an EXISTING track alive while the detector is unsure, so a person read at 0.5 for a few seconds keeps one id instead of splitting. They never start a track and never produce events, notifications or snapshots.",
83692
+ default: TRACKING_DEFAULTS.lowScoreContinuationEnabled
83693
+ },
83694
+ {
83695
+ type: "number",
83696
+ key: "lowScoreContinuationFloor",
83697
+ label: "Low-confidence continuation floor",
83698
+ description: "Score at or above which a box the class floor dropped still counts as evidence that a tracked subject is there. Only used with the option above.",
83699
+ min: 0,
83700
+ max: 1,
83701
+ step: .05,
83702
+ default: TRACKING_DEFAULTS.lowScoreContinuationFloor
83703
+ },
83704
+ {
83705
+ type: "number",
83706
+ key: "lowScoreContinuationMaxMs",
83707
+ label: "Low-confidence hold limit (ms)",
83708
+ description: "Longest a track may be kept alive by low-confidence boxes alone, counted from its last confident box.",
83709
+ min: 0,
83710
+ max: 3e4,
83711
+ step: 500,
83712
+ default: TRACKING_DEFAULTS.lowScoreContinuationMaxMs
83713
+ },
83714
+ {
83715
+ type: "boolean",
83716
+ key: "resurrectionTimeScaled",
83717
+ label: "Wider re-attach after a long gap",
83718
+ description: "Let a track that was lost for a few seconds be re-attached to a subject that reappears further away, the longer the gap (up to two box diagonals) — only when nobody else of that class is in view. A recognised face is re-checked after such a re-attach. Off = the fixed short re-attach distance.",
83719
+ default: TRACKING_DEFAULTS.resurrectionTimeScaled
83720
+ },
83364
83721
  {
83365
83722
  type: "boolean",
83366
83723
  key: "minSpawnAreaEnabled",
@@ -87861,6 +88218,8 @@ function decodeEmbeddingBase64(base64) {
87861
88218
  var PHANTOM_CELL_PX = 32;
87862
88219
  /** How long a cell remembers its closes. */
87863
88220
  var PHANTOM_CELL_WINDOW_MS = 360 * 6e4;
88221
+ /** Cadence of the per-camera low-score continuation line (D665). */
88222
+ var LOW_SCORE_REPORT_INTERVAL_MS = 6e4;
87864
88223
  var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
87865
88224
  /** Recent SHORT+MOTIONLESS track closes per `<device>:<class>:<cell>` —
87866
88225
  * see {@link noteShortMotionlessTrack}. Measurement only; each entry is
@@ -88528,6 +88887,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88528
88887
  packageBackground = null;
88529
88888
  /** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
88530
88889
  dropoutSkipsByKey = /* @__PURE__ */ new Map();
88890
+ /** Last time each processor's low-score continuation counts were logged (D665). */
88891
+ lowScoreReportAtByKey = /* @__PURE__ */ new Map();
88531
88892
  /** The per-track close-path orchestrator (S2, refactor spec 2026-07-22):
88532
88893
  * owns the single-flight sweep latch + the FULL per-track close sequence
88533
88894
  * (zero-media policy, lastFrame promotion, recognizer ends with the
@@ -90086,6 +90447,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90086
90447
  mediaStore: stores.mediaStore,
90087
90448
  trackStore: stores.trackStore,
90088
90449
  eventStore: stores.eventStore,
90450
+ retractIdentityLabel: (trackId) => retractFaceIdentityLabel({
90451
+ trackStore: stores.trackStore,
90452
+ eventStore: stores.eventStore,
90453
+ recomputeImportance: (id) => recomputeTrackImportance({
90454
+ trackStore: stores.trackStore,
90455
+ eventStore: stores.eventStore
90456
+ }, id)
90457
+ }, trackId),
90089
90458
  captureCrop: (frameHandle, bbox, frameWidth, frameHeight, padding, ownerId, deviceId) => this.captureScheduler.request({
90090
90459
  deviceId,
90091
90460
  trackId: ownerId,
@@ -92040,6 +92409,31 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92040
92409
  * tracking/zone/event state never crosses between sources. Emits the
92041
92410
  * SAME canonical events, distinguished only by `source`.
92042
92411
  */
92412
+ /**
92413
+ * One line per camera per minute with what the low-score continuation tier
92414
+ * did (D665): boxes offered, tracks continued, holds that ended in a
92415
+ * confident box again (`resumed` — an id the tier kept), holds cut at the
92416
+ * cap. Never one line per box. Silent while nothing was offered.
92417
+ */
92418
+ reportLowScoreContinuation(deviceId, key, processor) {
92419
+ const now = Date.now();
92420
+ const last = this.lowScoreReportAtByKey.get(key);
92421
+ if (last === void 0) {
92422
+ this.lowScoreReportAtByKey.set(key, now);
92423
+ return;
92424
+ }
92425
+ if (now - last < LOW_SCORE_REPORT_INTERVAL_MS) return;
92426
+ this.lowScoreReportAtByKey.set(key, now);
92427
+ const stats = processor.drainLowScoreStats();
92428
+ if (stats.offered === 0 && stats.sustainCapDrops === 0) return;
92429
+ this.ctx.logger.info("tracker: low-score continuation", {
92430
+ tags: { deviceId },
92431
+ meta: {
92432
+ ...stats,
92433
+ windowMs: now - last
92434
+ }
92435
+ });
92436
+ }
92043
92437
  async processFrame(deviceId, frame, source, rawFrameHandle, detailSteps, occasion = "session") {
92044
92438
  if (this.shuttingDown) return;
92045
92439
  if (!await this.bindingCache.isActive(deviceId)) {
@@ -92064,7 +92458,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92064
92458
  const trk = await this.resolveDeviceTrackingSettings(deviceId);
92065
92459
  const activeCount = this.lastActiveTrackIds.get(key)?.size ?? 0;
92066
92460
  const skips = this.dropoutSkipsByKey.get(key) ?? 0;
92067
- if (shouldSkipDropoutFrame(activeCount, frame.detections.length, skips, {
92461
+ const lowScoreFloor = trk.lowScoreContinuationEnabled ? trk.lowScoreContinuationFloor : null;
92462
+ if (shouldSkipDropoutFrame(activeCount, dropoutEvidenceCount(frame, lowScoreFloor), skips, {
92068
92463
  enabled: trk.dropoutSkipEnabled,
92069
92464
  maxSkipFrames: trk.dropoutMaxSkipFrames
92070
92465
  })) {
@@ -92092,6 +92487,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92092
92487
  processor.setZones(liveZones);
92093
92488
  processor.setDetectionRules(liveRules);
92094
92489
  processor.setZoneMembershipMinOverlap(trk.zoneMembershipMinOverlap);
92490
+ processor.setLowScoreContinuationFloor(lowScoreFloor);
92095
92491
  const injection = injectSecondarySubjects(frame, this.secondarySubjects.take(key, Date.now()));
92096
92492
  for (const id of injection.covered) this.secondarySubjects.drop(key, id);
92097
92493
  const trackerFrame = injection.frame;
@@ -92107,6 +92503,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92107
92503
  timestamp: frame.timestamp,
92108
92504
  frame: trackerFrame
92109
92505
  });
92506
+ this.reportLowScoreContinuation(deviceId, key, processor);
92110
92507
  for (const r of processor.getLastLabelWriteRefusals()) {
92111
92508
  const message = r.reason.kind === "macro-class" ? "label tier collapse refused" : "label write refused";
92112
92509
  this.ctx.logger.info(message, {
@@ -95794,7 +96191,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95794
96191
  animal: trk.minSpawnAreaFracAnimal,
95795
96192
  vehicle: trk.minSpawnAreaFracVehicle
95796
96193
  },
95797
- classGroupAssoc: trk.classGroupAssoc
96194
+ classGroupAssoc: trk.classGroupAssoc,
96195
+ lowScoreContinuationEnabled: trk.lowScoreContinuationEnabled,
96196
+ lowScoreContinuationMaxMs: trk.lowScoreContinuationMaxMs,
96197
+ resurrectionTimeScaled: trk.resurrectionTimeScaled
95798
96198
  }, {
95799
96199
  stationaryThresholdSec,
95800
96200
  ...stillSpeedFracPerSec !== void 0 ? { stillSpeedFracPerSec } : {}