@camstack/addon-post-analysis 1.2.290 → 1.2.292

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.
@@ -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-CoYBCvtr.mjs";
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";
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";
@@ -39654,7 +39654,12 @@ var RebuildInputSchema = object({
39654
39654
  * Milliseconds between tracks; omit for the default, `0` to run flat out.
39655
39655
  * A rebuild shares hub-main's only thread with every live request.
39656
39656
  */
39657
- pacingMs: number().int().nonnegative().optional()
39657
+ pacingMs: number().int().nonnegative().optional(),
39658
+ /**
39659
+ * Re-embed ONLY tracks already in the pinned space — to redo vectors cut
39660
+ * from the wrong box (D664). Requires `maxTracks`.
39661
+ */
39662
+ force: boolean().optional()
39658
39663
  });
39659
39664
  var RebuildStartedSchema = object({
39660
39665
  started: boolean(),
@@ -39674,7 +39679,20 @@ var RebuildStatusSchema = object({
39674
39679
  scanned: number(),
39675
39680
  rebuilt: number(),
39676
39681
  missingKeyFrame: number(),
39682
+ /** No object event at all — its own number since D664. */
39683
+ missingEvent: number(),
39677
39684
  missingBbox: number(),
39685
+ /** Which rule placed each judged track's box (D664). */
39686
+ boxSource: object({
39687
+ subjectBox: number(),
39688
+ positions: number(),
39689
+ event: number(),
39690
+ refused: number()
39691
+ }),
39692
+ alreadyInSpace: number(),
39693
+ /** Whether the current / last pass was a `force` pass (D664). */
39694
+ force: boolean().nullable(),
39695
+ forcedInSpace: number(),
39678
39696
  /** Refused by the executing node (unreadable key frame / bad box), not broken. */
39679
39697
  notRunnable: number(),
39680
39698
  /** The pass STOPPED: no node in the cluster could serve the pinned model. */
@@ -40949,7 +40967,7 @@ function toPixelBox(b) {
40949
40967
  h: b.height
40950
40968
  };
40951
40969
  }
40952
- function refused(reason, detail) {
40970
+ function refused$1(reason, detail) {
40953
40971
  return {
40954
40972
  kind: "refused",
40955
40973
  reason,
@@ -40977,8 +40995,8 @@ function comparableFaces(input) {
40977
40995
  /** Refuse before choosing when nothing in the photo can become the sample. */
40978
40996
  function precondition(input) {
40979
40997
  const enrollable = input.faces.filter((f) => f.enrollable && f.embedding !== void 0);
40980
- if (enrollable.length === 0) return refused("no-enrollable-face", `${input.faces.length} face(s) found, none enrollable (${input.faces.map((f) => f.refusedReason ?? "unknown").join(", ")})`);
40981
- if (!enrollable.some((f) => isEnrollableIn(f, input.modelId))) return refused("model-mismatch", `faces were embedded by ${[...new Set(enrollable.map((f) => f.modelId))].join(", ")}, the old sample by ${input.modelId} — re-embed the gallery first`);
40998
+ if (enrollable.length === 0) return refused$1("no-enrollable-face", `${input.faces.length} face(s) found, none enrollable (${input.faces.map((f) => f.refusedReason ?? "unknown").join(", ")})`);
40999
+ if (!enrollable.some((f) => isEnrollableIn(f, input.modelId))) return refused$1("model-mismatch", `faces were embedded by ${[...new Set(enrollable.map((f) => f.modelId))].join(", ")}, the old sample by ${input.modelId} — re-embed the gallery first`);
40982
41000
  return null;
40983
41001
  }
40984
41002
  function chooseByIou(input, oldBox, scored) {
@@ -40987,13 +41005,13 @@ function chooseByIou(input, oldBox, scored) {
40987
41005
  face,
40988
41006
  iou: iou$3(box, toPixelBox(face.bbox))
40989
41007
  })).filter((c) => c.iou >= PHOTO_REENROL_MIN_IOU);
40990
- if (overlapping.length === 0) return refused("no-iou-match", `no face overlaps the old box at IoU ≥ ${PHOTO_REENROL_MIN_IOU}`);
40991
- if (overlapping.length > 1) return refused("ambiguous", `${overlapping.length} faces overlap the old box (IoU ${overlapping.map((c) => c.iou.toFixed(2)).join(", ")})`);
41008
+ if (overlapping.length === 0) return refused$1("no-iou-match", `no face overlaps the old box at IoU ≥ ${PHOTO_REENROL_MIN_IOU}`);
41009
+ if (overlapping.length > 1) return refused$1("ambiguous", `${overlapping.length} faces overlap the old box (IoU ${overlapping.map((c) => c.iou.toFixed(2)).join(", ")})`);
40992
41010
  const [only] = overlapping;
40993
- if (only === void 0) return refused("no-iou-match", "no overlapping face");
40994
- if (!isEnrollableIn(only.face, input.modelId)) return refused("no-enrollable-face", `the face on the old box is not enrollable (${only.face.refusedReason ?? `model ${only.face.modelId ?? "none"}`})`);
41011
+ if (only === void 0) return refused$1("no-iou-match", "no overlapping face");
41012
+ if (!isEnrollableIn(only.face, input.modelId)) return refused$1("no-enrollable-face", `the face on the old box is not enrollable (${only.face.refusedReason ?? `model ${only.face.modelId ?? "none"}`})`);
40995
41013
  const cosine = scored.find((s) => s.face === only.face)?.cosine;
40996
- if (cosine === void 0 || cosine < .5) return refused("below-floor", `the overlapping face scores cosine ${cosine?.toFixed(3) ?? "n/a"} < ${PHOTO_REENROL_COSINE_FLOOR}`);
41014
+ if (cosine === void 0 || cosine < .5) return refused$1("below-floor", `the overlapping face scores cosine ${cosine?.toFixed(3) ?? "n/a"} < ${PHOTO_REENROL_COSINE_FLOOR}`);
40997
41015
  return {
40998
41016
  kind: "chosen",
40999
41017
  face: only.face,
@@ -41004,10 +41022,10 @@ function chooseByIou(input, oldBox, scored) {
41004
41022
  }
41005
41023
  function chooseByCosine(input, scored) {
41006
41024
  const [best, runnerUp] = [...scored].toSorted((a, b) => b.cosine - a.cosine);
41007
- if (best === void 0) return refused("model-mismatch", "no face has a comparable vector");
41008
- if (best.cosine < .5) return refused("below-floor", `best cosine ${best.cosine.toFixed(3)} < ${PHOTO_REENROL_COSINE_FLOOR}`);
41009
- if (runnerUp !== void 0 && runnerUp.cosine >= .5 && best.cosine - runnerUp.cosine < .1) return refused("ambiguous", `two faces clear the floor within ${PHOTO_REENROL_AMBIGUITY_MARGIN}: ${best.cosine.toFixed(3)} vs ${runnerUp.cosine.toFixed(3)}`);
41010
- if (!isEnrollableIn(best.face, input.modelId)) return refused("ambiguous", `the closest face (cosine ${best.cosine.toFixed(3)}) is not enrollable (${best.face.refusedReason ?? "unknown"}) — the enrollable ones may be someone else`);
41025
+ if (best === void 0) return refused$1("model-mismatch", "no face has a comparable vector");
41026
+ if (best.cosine < .5) return refused$1("below-floor", `best cosine ${best.cosine.toFixed(3)} < ${PHOTO_REENROL_COSINE_FLOOR}`);
41027
+ if (runnerUp !== void 0 && runnerUp.cosine >= .5 && best.cosine - runnerUp.cosine < .1) return refused$1("ambiguous", `two faces clear the floor within ${PHOTO_REENROL_AMBIGUITY_MARGIN}: ${best.cosine.toFixed(3)} vs ${runnerUp.cosine.toFixed(3)}`);
41028
+ if (!isEnrollableIn(best.face, input.modelId)) return refused$1("ambiguous", `the closest face (cosine ${best.cosine.toFixed(3)}) is not enrollable (${best.face.refusedReason ?? "unknown"}) — the enrollable ones may be someone else`);
41011
41029
  return {
41012
41030
  kind: "chosen",
41013
41031
  face: best.face,
@@ -47011,6 +47029,42 @@ function pairRiders(detections) {
47011
47029
  return pairs;
47012
47030
  }
47013
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
47014
47068
  //#region src/pipeline-analytics/pipeline/track-id.ts
47015
47069
  /**
47016
47070
  * A track's identity: short, random, and mintable inside a frame.
@@ -47159,6 +47213,30 @@ function reacquireReachPx(bbox, elapsedMs) {
47159
47213
  const elapsed = Math.max(0, elapsedMs);
47160
47214
  return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
47161
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
+ }
47162
47240
  //#endregion
47163
47241
  //#region src/pipeline-analytics/pipeline/tracker/rescue-growth.ts
47164
47242
  /**
@@ -47307,7 +47385,10 @@ var DEFAULT_TRACKER_CONFIG = {
47307
47385
  byteTrackLowIouThreshold: .2,
47308
47386
  minSpawnAreaEnabled: true,
47309
47387
  classSpawnAreaFracs: { person: 5e-4 },
47310
- classGroupAssoc: true
47388
+ classGroupAssoc: true,
47389
+ lowScoreContinuationEnabled: true,
47390
+ lowScoreContinuationMaxMs: 8e3,
47391
+ resurrectionTimeScaled: true
47311
47392
  };
47312
47393
  /** Macro-classes the duplicate resolver may collapse across (people in
47313
47394
  * non-upright poses misdetected as animals). Any other cross-class pair is
@@ -47315,6 +47396,15 @@ var DEFAULT_TRACKER_CONFIG = {
47315
47396
  var PERSON_CLASS = "person";
47316
47397
  var ANIMAL_CLASS = "animal";
47317
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
+ }
47318
47408
  /** No stationary-held detections on this frame — the default for every caller
47319
47409
  * that has no gate (replay, tests, the non-pipeline planes). */
47320
47410
  var EMPTY_INDEX_SET = /* @__PURE__ */ new Set();
@@ -47386,6 +47476,8 @@ var SortTracker = class SortTracker {
47386
47476
  trackLossSampler = new TrackLossSampler();
47387
47477
  /** Sink for fragmentation diagnostics — see {@link setSpawnBesideCoastingObserver}. */
47388
47478
  spawnBesideObserver = null;
47479
+ /** Running counts behind {@link drainLowScoreStats} (D665). */
47480
+ lowScoreStats = emptyLowScoreStats();
47389
47481
  constructor(config = {}) {
47390
47482
  this.config = {
47391
47483
  ...DEFAULT_TRACKER_CONFIG,
@@ -47446,7 +47538,7 @@ var SortTracker = class SortTracker {
47446
47538
  if (SortTracker.spawnBesideFamily(track.class) !== family) continue;
47447
47539
  const tc = bboxCentroid(track.bbox);
47448
47540
  const dist = Math.hypot(tc.x - dc.x, tc.y - dc.y);
47449
- const reach = this.reacquireReach(track, timestamp, true);
47541
+ const reach = this.reacquireReach(track, timestamp, "rescue");
47450
47542
  if (dist > reach * 2) continue;
47451
47543
  if (dist < bestDist) {
47452
47544
  best = track;
@@ -47493,7 +47585,7 @@ var SortTracker = class SortTracker {
47493
47585
  explainDecline(track, det, detections, frameArea, timestamp, centroidDist) {
47494
47586
  if (track.class !== det.class) return det.nativeRecovery !== void 0 ? "native-measured-class" : "cross-class";
47495
47587
  if (iou$2(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "already-claimed";
47496
- if (centroidDist <= this.reacquireReach(track, timestamp, true)) {
47588
+ if (centroidDist <= this.reacquireReach(track, timestamp, "rescue")) {
47497
47589
  if (!this.isPlausibleBirth(det, frameArea)) return "already-claimed";
47498
47590
  if (!this.config.reacquireTimeScaled) return "plausible-birth-veto";
47499
47591
  return this.isUnambiguousReach(track, det, detections) ? "already-claimed" : "crowd-guard";
@@ -47948,34 +48040,38 @@ var SortTracker = class SortTracker {
47948
48040
  * this gate crossed the group, and it is where a `person` at 0.86 took over
47949
48041
  * an animal's id (camera 615, 17:11:01).
47950
48042
  */
47951
- looseMatch(track, det, timestamp, timeScaled = true) {
48043
+ looseMatch(track, det, timestamp, pass = "rescue") {
47952
48044
  if (track.class !== det.class) return null;
47953
48045
  if (iou$2(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
47954
48046
  const tc = bboxCentroid(track.bbox);
47955
48047
  const dc = bboxCentroid(det.bbox);
47956
- 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";
47957
48049
  if (growthAdjacentMatch(track.bbox, det.bbox, this.growthContext(track, timestamp))) return "growth";
47958
48050
  return null;
47959
48051
  }
47960
48052
  /**
47961
48053
  * The proximity budget the `centroid` clause is measured against.
47962
48054
  *
47963
- * On the RESCUE pass (`timeScaled`) it is {@link reacquireReachPx} — a
47964
- * multiple of the track's diagonal that grows with the elapsed gap and stops
47965
- * at a ceiling, because a live coasting track's whereabouts genuinely become
47966
- * less certain the longer it has gone unseen (D399).
47967
- *
47968
- * On the RESURRECTION pass it stays the flat `rescueCentroidFactor × diagonal`
47969
- * it always was. That pass re-attaches a CORPSE, and it exists for one thing:
47970
- * "a flickering stationary object keeps its identity". Elapsed time is not the
47971
- * variable there — a stationary subject does not travel further because it
47972
- * blinked for longer — and the corpse is strictly weaker evidence than a live
47973
- * track, which is the same reasoning `resurrectionCrowdedIouThreshold` already
47974
- * encodes.
47975
- */
47976
- 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) {
47977
48072
  const diag = Math.hypot(track.bbox.w, track.bbox.h);
47978
- 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;
47979
48075
  return reacquireReachPx(track.bbox, timestamp - track.lastSeen);
47980
48076
  }
47981
48077
  /**
@@ -48025,6 +48121,23 @@ var SortTracker = class SortTracker {
48025
48121
  nearestPx: nearest
48026
48122
  };
48027
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
+ }
48028
48141
  isUnambiguousReach(track, det, detections) {
48029
48142
  if (this.sameClassPeerCount(track, this.tracks) > 0) return false;
48030
48143
  for (const other of detections) {
@@ -48179,6 +48292,64 @@ var SortTracker = class SortTracker {
48179
48292
  used.add(pair.detIdx);
48180
48293
  }
48181
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
+ }
48182
48353
  update(detections, timestamp, frameContext) {
48183
48354
  this.frameSeq++;
48184
48355
  const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
@@ -48236,6 +48407,9 @@ var SortTracker = class SortTracker {
48236
48407
  this.rescueAssociate(established, PROXIMITY, detections, lowSet, frameArea, timestamp, used, matchedTracks, matched, continuationOnly);
48237
48408
  this.rescueAssociate(provisional, new Set([...OVERLAP, ...PROXIMITY]), detections, lowSet, frameArea, timestamp, used, matchedTracks, matched, continuationOnly);
48238
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);
48239
48413
  for (const [track, det] of matched) {
48240
48414
  if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, {
48241
48415
  against: track,
@@ -48266,6 +48440,7 @@ var SortTracker = class SortTracker {
48266
48440
  track.hits++;
48267
48441
  track.lastSeen = timestamp;
48268
48442
  track.lastSeenFrameSeq = this.frameSeq;
48443
+ this.markConfident(track, det.bbox, timestamp);
48269
48444
  if (dtMs > 0) {
48270
48445
  track.velocity = {
48271
48446
  dx: (newCenter.x - prevCenter.x) / dtMs,
@@ -48290,10 +48465,15 @@ var SortTracker = class SortTracker {
48290
48465
  }
48291
48466
  const occluded = this.config.occlusionEnabled && occluders.some((ob) => containment(this.predicted(track, timestamp), ob.bbox) >= this.config.occlusionContainment);
48292
48467
  track.age++;
48293
- 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) {
48294
48473
  track.lost = true;
48295
48474
  track.lostAt = timestamp;
48296
48475
  track.resurrectable = true;
48476
+ track.lowScoreHeld = false;
48297
48477
  this.lostTracks.push(track);
48298
48478
  } else surviving.push(track);
48299
48479
  }
@@ -48304,15 +48484,17 @@ var SortTracker = class SortTracker {
48304
48484
  let bestScore = -1;
48305
48485
  let bestGate = "iou";
48306
48486
  let bestPeers = 0;
48487
+ let bestVetoLifted = false;
48307
48488
  let refused;
48308
48489
  let refusedGate = "iou";
48309
48490
  let refusedScore = -1;
48310
48491
  for (const lost of this.lostTracks) {
48311
48492
  if (!lost.resurrectable) continue;
48312
48493
  if (timestamp - lost.lostAt > this.config.resurrectionWindowMs) continue;
48313
- const gate = this.looseMatch(lost, det, timestamp, false);
48494
+ const gate = this.looseMatch(lost, det, timestamp, "resurrection");
48314
48495
  if (gate === null) continue;
48315
- 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;
48316
48498
  const score = iou$2(lost.bbox, det.bbox);
48317
48499
  const peers = this.sameClassPeerCount(lost, surviving);
48318
48500
  if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
@@ -48328,13 +48510,14 @@ var SortTracker = class SortTracker {
48328
48510
  bestScore = score;
48329
48511
  bestGate = gate;
48330
48512
  bestPeers = peers;
48513
+ bestVetoLifted = vetoLifted;
48331
48514
  }
48332
48515
  }
48333
48516
  if (!best) {
48334
48517
  if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp);
48335
48518
  continue;
48336
48519
  }
48337
- resurrectedScene.set(best, bestPeers > 0 ? "multi-subject" : "sole-subject");
48520
+ resurrectedScene.set(best, bestPeers > 0 ? "multi-subject" : bestVetoLifted ? "sole-subject-veto-lifted" : "sole-subject");
48338
48521
  this.reportBind("resurrection", bestGate, best, det, timestamp);
48339
48522
  this.lostTracks = this.lostTracks.filter((t) => t !== best);
48340
48523
  best.bbox = det.bbox;
@@ -48348,6 +48531,7 @@ var SortTracker = class SortTracker {
48348
48531
  best.lost = false;
48349
48532
  best.lastSeen = timestamp;
48350
48533
  best.lastSeenFrameSeq = this.frameSeq;
48534
+ this.markConfident(best, det.bbox, timestamp);
48351
48535
  best.velocity = {
48352
48536
  dx: 0,
48353
48537
  dy: 0
@@ -48424,7 +48608,10 @@ var SortTracker = class SortTracker {
48424
48608
  classVotes: new Map([[det.class, det.score > 0 ? det.score : Number.EPSILON]]),
48425
48609
  emitted: false,
48426
48610
  lossReported: false,
48427
- lastNativeRecovery: det.nativeRecovery
48611
+ lastNativeRecovery: det.nativeRecovery,
48612
+ confidentBbox: det.bbox,
48613
+ confidentSeenAt: timestamp,
48614
+ lowScoreHeld: false
48428
48615
  });
48429
48616
  }
48430
48617
  this.tracks = surviving;
@@ -48432,11 +48619,12 @@ var SortTracker = class SortTracker {
48432
48619
  return this.tracks.filter((t) => this.isConfirmedForEmit(t)).map((t) => {
48433
48620
  t.emitted = true;
48434
48621
  const scene = resurrectedScene.get(t);
48622
+ const held = t.lowScoreHeld;
48435
48623
  return {
48436
48624
  class: this.resolveReportedClass(t),
48437
48625
  originalClass: t.originalClass,
48438
48626
  score: t.score,
48439
- bbox: t.bbox,
48627
+ bbox: held ? t.confidentBbox : t.bbox,
48440
48628
  trackId: t.id,
48441
48629
  trackAge: t.hits,
48442
48630
  velocity: {
@@ -48444,8 +48632,9 @@ var SortTracker = class SortTracker {
48444
48632
  dy: t.velocity.dy * t.emitIntervalMs
48445
48633
  },
48446
48634
  path: [...t.path],
48447
- matchedThisFrame: t.lastSeen === timestamp,
48448
- ...scene !== void 0 ? { resurrectionScene: scene } : {}
48635
+ matchedThisFrame: t.lastSeen === timestamp && !held,
48636
+ ...scene !== void 0 ? { resurrectionScene: scene } : {},
48637
+ ...held ? { heldByLowScore: true } : {}
48449
48638
  };
48450
48639
  });
48451
48640
  }
@@ -48459,6 +48648,16 @@ var SortTracker = class SortTracker {
48459
48648
  this.tracks = this.tracks.filter((t) => t.id !== trackId);
48460
48649
  this.lostTracks = this.lostTracks.filter((t) => t.id !== trackId);
48461
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
+ }
48462
48661
  getActiveTracks() {
48463
48662
  return this.tracks;
48464
48663
  }
@@ -48833,6 +49032,8 @@ var FrameProcessor = class {
48833
49032
  detectionRules;
48834
49033
  /** See {@link setZoneMembershipMinOverlap}. 0 = any positive overlap. */
48835
49034
  zoneMembershipMinOverlap;
49035
+ /** See {@link setLowScoreContinuationFloor}. `null` = tier off. */
49036
+ lowScoreFloor = LOW_SCORE_CONTINUATION_FLOOR_DEFAULT;
48836
49037
  /** See {@link getLastZoneOverlaps}. */
48837
49038
  lastZoneOverlaps;
48838
49039
  /** See {@link getLastZoneRejections}. */
@@ -48945,6 +49146,18 @@ var FrameProcessor = class {
48945
49146
  * failure mode this whole area keeps producing. {@link lastZoneOverlaps}
48946
49147
  * exists so the distribution can be read before a number is picked.
48947
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
+ }
48948
49161
  setZoneMembershipMinOverlap(minOverlap) {
48949
49162
  this.zoneMembershipMinOverlap = Number.isFinite(minOverlap) && minOverlap >= 0 && minOverlap <= 1 ? minOverlap : 0;
48950
49163
  }
@@ -49337,10 +49550,16 @@ var FrameProcessor = class {
49337
49550
  }
49338
49551
  trackerInput.push(det);
49339
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);
49340
49558
  const trackedDetections = this.tracker.update(trackerInput, timestamp, {
49341
49559
  frameWidth,
49342
49560
  frameHeight,
49343
- continuationOnly
49561
+ continuationOnly,
49562
+ lowScore
49344
49563
  });
49345
49564
  const stationaryContinued = continuableBoxes.size === 0 ? 0 : trackedDetections.filter((t) => t.matchedThisFrame !== false && continuableBoxes.has(t.bbox)).length;
49346
49565
  const riderFolds = foldByVehicleBbox.size === 0 ? EMPTY_RIDER_FOLDS : [...foldByVehicleBbox].map(([bbox, fold]) => {
@@ -49582,6 +49801,19 @@ var FrameProcessor = class {
49582
49801
  }
49583
49802
  };
49584
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
49585
49817
  //#region src/pipeline-analytics/pipeline/track-provenance.ts
49586
49818
  function trackProvenanceOf(frame) {
49587
49819
  if (!(frame.width > 0 && frame.height > 0)) return void 0;
@@ -53721,11 +53953,10 @@ var EventStore = class {
53721
53953
  let entryId;
53722
53954
  for (const row of rows) {
53723
53955
  const bbox = row.data["bbox"];
53724
- if (bbox !== null && typeof bbox === "object") {
53725
- const bw = "w" in bbox && typeof bbox.w === "number" ? bbox.w : 0;
53726
- const bh = "h" in bbox && typeof bbox.h === "number" ? bbox.h : 0;
53727
- if (bw <= 0 || bh <= 0) continue;
53728
- }
53956
+ if (bbox === null || typeof bbox !== "object") continue;
53957
+ const bw = "w" in bbox && typeof bbox.w === "number" ? bbox.w : 0;
53958
+ const bh = "h" in bbox && typeof bbox.h === "number" ? bbox.h : 0;
53959
+ if (bw <= 0 || bh <= 0) continue;
53729
53960
  const conf = typeof row.data["confidence"] === "number" ? row.data["confidence"] : 0;
53730
53961
  if (conf > bestConf) {
53731
53962
  bestConf = conf;
@@ -62377,6 +62608,26 @@ var EmbeddingDispatcher = class {
62377
62608
  return this.pendingCrops.size;
62378
62609
  }
62379
62610
  };
62611
+ //#endregion
62612
+ //#region src/pipeline-analytics/embedding-rebuild.ts
62613
+ /**
62614
+ * Track sources a rebuild never lists (D664). An audio track and a synthetic
62615
+ * sensor track have no object event and no subject to embed — on the live hub
62616
+ * 6,726 + 180 of 42,254 tracks — and walking them only inflated the skip
62617
+ * counters an operator reads to judge media coverage.
62618
+ */
62619
+ var REBUILD_EXCLUDED_SOURCES = ["audio", "sensor"];
62620
+ /**
62621
+ * One page of the rebuild listing as a `queryRecentIds` query. Pure, so the
62622
+ * exclusion is assertable: dropping it would look identical in every result
62623
+ * except the skip counters.
62624
+ */
62625
+ function rebuildTrackPageQuery(window) {
62626
+ return {
62627
+ ...window,
62628
+ excludeSources: REBUILD_EXCLUDED_SOURCES
62629
+ };
62630
+ }
62380
62631
  /**
62381
62632
  * Re-embed every listed track from its key frame.
62382
62633
  *
@@ -62392,6 +62643,14 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
62392
62643
  let recoveredKeyFrame = 0;
62393
62644
  let missingEvent = 0;
62394
62645
  let missingBbox = 0;
62646
+ let boxSubject = 0;
62647
+ let boxPositions = 0;
62648
+ let boxEvent = 0;
62649
+ let boxRefused = 0;
62650
+ let forcedInSpace = 0;
62651
+ let skippedOutOfSpace = 0;
62652
+ const refusalsByDevice = /* @__PURE__ */ new Map();
62653
+ const force = options.force === true;
62395
62654
  let alreadyInSpace = 0;
62396
62655
  let notRunnable = 0;
62397
62656
  let noCapableNode = 0;
@@ -62402,6 +62661,12 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
62402
62661
  let budgetExhausted = false;
62403
62662
  /** Set when the cluster ran out of nodes — the pass stops, `complete` is false. */
62404
62663
  let abandoned = null;
62664
+ const boxSourceCounts = () => ({
62665
+ subjectBox: boxSubject,
62666
+ positions: boxPositions,
62667
+ event: boxEvent,
62668
+ refused: boxRefused
62669
+ });
62405
62670
  while (scanned < maxTracks && abandoned === null) {
62406
62671
  const page = await deps.listTrackIdPage(cursor);
62407
62672
  if (page.trackIds.length === 0) {
@@ -62409,9 +62674,14 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
62409
62674
  break;
62410
62675
  }
62411
62676
  let pageIds = page.trackIds;
62677
+ let forcedPage = false;
62412
62678
  if (deps.filterOutOfSpace) {
62413
62679
  const split = await deps.filterOutOfSpace(page.trackIds);
62414
- if (split !== null) {
62680
+ if (split !== null && force) {
62681
+ skippedOutOfSpace += split.outOfSpace.length;
62682
+ pageIds = split.inSpace;
62683
+ forcedPage = true;
62684
+ } else if (split !== null) {
62415
62685
  alreadyInSpace += split.alreadyInSpace;
62416
62686
  pageIds = split.outOfSpace;
62417
62687
  }
@@ -62427,6 +62697,7 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
62427
62697
  const room = maxTracks - scanned;
62428
62698
  const batch = pageIds.slice(0, Number.isFinite(room) ? room : void 0);
62429
62699
  if (batch.length < pageIds.length) budgetExhausted = true;
62700
+ if (forcedPage) forcedInSpace += batch.length;
62430
62701
  for (const trackId of batch) {
62431
62702
  if (!deps.canDispatch()) {
62432
62703
  abandoned = "no node left in the rotation can serve the pinned model — stopped before loading any further media";
@@ -62445,6 +62716,25 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
62445
62716
  missingEvent += 1;
62446
62717
  return;
62447
62718
  }
62719
+ if (source.bbox === null || source.bbox.w <= 0 || source.bbox.h <= 0) {
62720
+ missingBbox += 1;
62721
+ boxRefused += 1;
62722
+ const reason = source.boxSource.kind === "refused" ? source.boxSource.reason : "degenerate-box";
62723
+ const n = (refusalsByDevice.get(source.deviceId) ?? 0) + 1;
62724
+ refusalsByDevice.set(source.deviceId, n);
62725
+ if (n === 1 || n % 100 === 0) deps.logger.warn("embedding rebuild: no box measured on the key frame — track refused (D303)", {
62726
+ tags: { deviceId: source.deviceId },
62727
+ meta: {
62728
+ trackId,
62729
+ reason,
62730
+ refusedOnThisCamera: n
62731
+ }
62732
+ });
62733
+ return;
62734
+ }
62735
+ if (source.boxSource.kind === "subjectBox") boxSubject += 1;
62736
+ else if (source.boxSource.kind === "positions") boxPositions += 1;
62737
+ else boxEvent += 1;
62448
62738
  let keyFrame = source.keyFrame;
62449
62739
  let recovered = false;
62450
62740
  if (keyFrame === null && deps.recoverKeyFrame) {
@@ -62456,10 +62746,6 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
62456
62746
  return;
62457
62747
  }
62458
62748
  if (recovered) recoveredKeyFrame += 1;
62459
- if (source.bbox === null || source.bbox.w <= 0 || source.bbox.h <= 0) {
62460
- missingBbox += 1;
62461
- return;
62462
- }
62463
62749
  const outcome = await deps.embed(recovered ? {
62464
62750
  ...source,
62465
62751
  keyFrame
@@ -62527,6 +62813,9 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
62527
62813
  recoveredKeyFrame,
62528
62814
  missingEvent,
62529
62815
  missingBbox,
62816
+ boxSource: boxSourceCounts(),
62817
+ forcedInSpace,
62818
+ skippedOutOfSpace,
62530
62819
  alreadyInSpace,
62531
62820
  notRunnable,
62532
62821
  noCapableNode,
@@ -62550,6 +62839,10 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
62550
62839
  recoveredKeyFrame,
62551
62840
  missingEvent,
62552
62841
  missingBbox,
62842
+ boxSource: boxSourceCounts(),
62843
+ force,
62844
+ forcedInSpace,
62845
+ skippedOutOfSpace,
62553
62846
  alreadyInSpace,
62554
62847
  notRunnable,
62555
62848
  noCapableNode,
@@ -62564,6 +62857,9 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
62564
62857
  recoveredKeyFrame,
62565
62858
  missingEvent,
62566
62859
  missingBbox,
62860
+ boxSource: boxSourceCounts(),
62861
+ forcedInSpace,
62862
+ skippedOutOfSpace,
62567
62863
  alreadyInSpace,
62568
62864
  notRunnable,
62569
62865
  noCapableNode,
@@ -63122,8 +63418,6 @@ function faceSizeScore(detectionFrameShortSidePx) {
63122
63418
  }
63123
63419
  //#endregion
63124
63420
  //#region src/pipeline-analytics/face-recognizer.ts
63125
- /** Attribution of an identity resolved by the in-process ArcFace matcher. */
63126
- var FACE_RECOGNIZER_STEP_ID = "face-recognizer";
63127
63421
  /** At most one "dropping imageless track" log per this interval, per recognizer. */
63128
63422
  var FACE_IMAGELESS_LOG_THROTTLE_MS = 6e4;
63129
63423
  /** At most one 'embedding magnitude' sample line per device per this interval. */
@@ -63149,6 +63443,9 @@ var FaceRecognizer = class {
63149
63443
  gallery = [];
63150
63444
  names = /* @__PURE__ */ new Map();
63151
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();
63152
63449
  bestFace = /* @__PURE__ */ new Map();
63153
63450
  /** Frame instant of each live track's last source park (D644). */
63154
63451
  lastSourceParkAt = /* @__PURE__ */ new Map();
@@ -63340,9 +63637,35 @@ var FaceRecognizer = class {
63340
63637
  * • the face becomes non-promotable: {@link onTrackEnd} will not write it to
63341
63638
  * the gallery while the flag stands.
63342
63639
  *
63343
- * The aggregate is deliberately left alone — see the note in `onTrackEnd`.
63344
- */
63345
- 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
+ }
63346
63669
  const held = this.bestFace.get(trackId);
63347
63670
  if (held === void 0 || held.unverified === true) return;
63348
63671
  const quarantined = {
@@ -63365,6 +63688,7 @@ var FaceRecognizer = class {
63365
63688
  trackId
63366
63689
  },
63367
63690
  meta: {
63691
+ scene,
63368
63692
  heldScore: held.score,
63369
63693
  heldAt: held.timestamp,
63370
63694
  strippedIdentityId: held.recognizedIdentityId ?? null,
@@ -63373,6 +63697,40 @@ var FaceRecognizer = class {
63373
63697
  });
63374
63698
  }
63375
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
+ /**
63376
63734
  * Ask a FRESH face whether the quarantined held face is still about the same
63377
63735
  * person, at the same bar the gallery uses to say two vectors are one person
63378
63736
  * (`similarityThreshold` — one knob over one band, D222).
@@ -63394,7 +63752,7 @@ var FaceRecognizer = class {
63394
63752
  }
63395
63753
  async processFrame(input) {
63396
63754
  const { settings } = input;
63397
- 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);
63398
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);
63399
63757
  if (candidates.length === 0) return;
63400
63758
  this.deps.logger.debug("face: frame candidates", {
@@ -63461,11 +63819,26 @@ var FaceRecognizer = class {
63461
63819
  embedding: c.embedding,
63462
63820
  embeddingModelId: c.embeddingModelId
63463
63821
  }, settings.similarityThreshold);
63464
- if (verdict === "confirmed") this.bestFace.set(c.trackId, {
63465
- ...quarantined,
63466
- unverified: false
63467
- });
63468
- 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);
63469
63842
  this.bestFace.delete(c.trackId);
63470
63843
  this.bestTracker.delete(c.trackId);
63471
63844
  this.aggregates.delete(c.trackId);
@@ -63680,6 +64053,7 @@ var FaceRecognizer = class {
63680
64053
  const bestMatchScore = this.bestMatchScore.get(trackId);
63681
64054
  const bestSuggestion = this.bestSuggestion.get(trackId);
63682
64055
  this.aggregates.delete(trackId);
64056
+ this.stashedIdentity.delete(trackId);
63683
64057
  this.bestFace.delete(trackId);
63684
64058
  this.bestTracker.delete(trackId);
63685
64059
  this.bestMatchScore.delete(trackId);
@@ -67841,6 +68215,19 @@ function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSki
67841
68215
  if (activeTrackCount <= 0) return false;
67842
68216
  return consecutiveSkips < cfg.maxSkipFrames;
67843
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
+ }
67844
68231
  //#endregion
67845
68232
  //#region src/pipeline-analytics/pipeline/key-event-query.ts
67846
68233
  async function rankKeyEvents(candidates, options, peakLookup) {
@@ -72962,6 +73349,58 @@ function hasKeyFrame(source) {
72962
73349
  }
72963
73350
  //#endregion
72964
73351
  //#region src/pipeline-analytics/rebuild-source-loader.ts
73352
+ function refused(reason) {
73353
+ return {
73354
+ bbox: null,
73355
+ boxSource: {
73356
+ kind: "refused",
73357
+ reason
73358
+ }
73359
+ };
73360
+ }
73361
+ function normaliseMeasured(box) {
73362
+ if (box.frameWidth <= 0 || box.frameHeight <= 0 || box.w <= 0 || box.h <= 0) return null;
73363
+ return {
73364
+ x: box.x / box.frameWidth,
73365
+ y: box.y / box.frameHeight,
73366
+ w: box.w / box.frameWidth,
73367
+ h: box.h / box.frameHeight
73368
+ };
73369
+ }
73370
+ /**
73371
+ * The box measured on THIS key frame: its own `subjectBox`, else the track
73372
+ * position at exactly its instant, else a named refusal. Never another
73373
+ * frame's box (D303).
73374
+ */
73375
+ async function keyFrameBox(deps, trackId, keyFrame) {
73376
+ const own = (await deps.media.listSubjectBoxesByOwner("track", trackId)).find((row) => row.mediaId === keyFrame.mediaId);
73377
+ if (own !== void 0) {
73378
+ const bbox = normaliseMeasured(own.subjectBox);
73379
+ if (bbox !== null) return {
73380
+ bbox,
73381
+ boxSource: { kind: "subjectBox" }
73382
+ };
73383
+ }
73384
+ const track = await deps.tracks.getPersistedByTrackId(trackId);
73385
+ if (track === null) return refused("track-row-missing");
73386
+ const { frameWidth, frameHeight } = track;
73387
+ if (frameWidth === void 0 || frameHeight === void 0) return refused("track-frame-size-unknown");
73388
+ const at = track.positions.find((p) => p.timestamp === keyFrame.timestamp);
73389
+ if (at === void 0) return refused("no-position-at-key-frame");
73390
+ const bbox = normaliseMeasured({
73391
+ ...at.bbox,
73392
+ frameWidth,
73393
+ frameHeight
73394
+ });
73395
+ if (bbox === null) return refused("no-position-at-key-frame");
73396
+ return {
73397
+ bbox,
73398
+ boxSource: { kind: "positions" }
73399
+ };
73400
+ }
73401
+ function hasBox(event) {
73402
+ return event.bbox !== void 0 && event.bbox.w > 0 && event.bbox.h > 0;
73403
+ }
72965
73404
  /**
72966
73405
  * Assemble one track's rebuild inputs, reading at most ONE blob.
72967
73406
  *
@@ -72969,7 +73408,8 @@ function hasKeyFrame(source) {
72969
73408
  * to describe, and the caller counts it. A track WITH an event but WITHOUT a
72970
73409
  * key frame still returns a source (with `keyFrame: null`) so the caller's
72971
73410
  * `missingKeyFrame` counter stays the one place that decision is made; no blob
72972
- * is read in that case either.
73411
+ * is read in that case either. A key frame with no box measured on it returns
73412
+ * `bbox: null` and a named refusal, also without reading the blob.
72973
73413
  */
72974
73414
  async function loadRebuildSource(deps, trackId) {
72975
73415
  const media = await deps.media.listInfoByOwner("track", trackId);
@@ -72977,26 +73417,44 @@ async function loadRebuildSource(deps, trackId) {
72977
73417
  const cropInfo = media.find((m) => m.kind === "thumbnail");
72978
73418
  const peak = await deps.events.peakForTrack(trackId);
72979
73419
  const events = await deps.events.queryObjectByTrackIds([trackId]);
72980
- const best = events.find((e) => String(e.id) === peak.bestEventId) ?? events[0];
73420
+ const best = events.find((e) => String(e.id) === peak.bestEventId) ?? events.find(hasBox) ?? events[0];
72981
73421
  if (best === void 0) return null;
72982
73422
  const common = {
72983
73423
  trackId,
72984
73424
  deviceId: best.deviceId,
72985
- timestamp: best.timestamp,
72986
73425
  className: best.className,
72987
73426
  confidence: best.confidence ?? 0,
72988
- bbox: normaliseEventBbox(best),
72989
73427
  ...cropInfo !== void 0 ? { mediaId: cropInfo.mediaId } : {}
72990
73428
  };
72991
- if (keyFrameInfo === void 0) return {
73429
+ if (keyFrameInfo === void 0) {
73430
+ const bbox = normaliseEventBbox(best);
73431
+ return {
73432
+ ...common,
73433
+ timestamp: best.timestamp,
73434
+ bbox,
73435
+ boxSource: bbox !== null ? { kind: "event" } : {
73436
+ kind: "refused",
73437
+ reason: "event-without-box"
73438
+ },
73439
+ keyFrame: null
73440
+ };
73441
+ }
73442
+ const box = await keyFrameBox(deps, trackId, keyFrameInfo);
73443
+ const keyed = {
72992
73444
  ...common,
73445
+ timestamp: keyFrameInfo.timestamp,
73446
+ bbox: box.bbox,
73447
+ boxSource: box.boxSource,
73448
+ keyFrameMediaId: keyFrameInfo.mediaId
73449
+ };
73450
+ if (box.bbox === null) return {
73451
+ ...keyed,
72993
73452
  keyFrame: null
72994
73453
  };
72995
73454
  const keyFrameFile = await deps.media.getById(keyFrameInfo.mediaId);
72996
73455
  return {
72997
- ...common,
72998
- keyFrame: keyFrameFile ? Buffer.from(keyFrameFile.base64, "base64") : null,
72999
- keyFrameMediaId: keyFrameInfo.mediaId
73456
+ ...keyed,
73457
+ keyFrame: keyFrameFile ? Buffer.from(keyFrameFile.base64, "base64") : null
73000
73458
  };
73001
73459
  }
73002
73460
  //#endregion
@@ -82723,7 +83181,21 @@ var TrackingSettingsSchema = object({
82723
83181
  * passes, only for a coarse box below `animalOverPersonMaxScore`, never on
82724
83182
  * the rescue / resurrection passes. Reported label stays via the class
82725
83183
  * vote. Off = exact per-frame class gating. */
82726
- 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)
82727
83199
  });
82728
83200
  var TRACKING_DEFAULTS = TrackingSettingsSchema.parse({});
82729
83201
  /**
@@ -82778,7 +83250,11 @@ function resolveTrackingSettings(raw) {
82778
83250
  minSpawnAreaFracPerson: s.minSpawnAreaFracPerson.catch(TRACKING_DEFAULTS.minSpawnAreaFracPerson).parse(raw.minSpawnAreaFracPerson),
82779
83251
  minSpawnAreaFracAnimal: s.minSpawnAreaFracAnimal.catch(TRACKING_DEFAULTS.minSpawnAreaFracAnimal).parse(raw.minSpawnAreaFracAnimal),
82780
83252
  minSpawnAreaFracVehicle: s.minSpawnAreaFracVehicle.catch(TRACKING_DEFAULTS.minSpawnAreaFracVehicle).parse(raw.minSpawnAreaFracVehicle),
82781
- 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)
82782
83258
  };
82783
83259
  }
82784
83260
  //#endregion
@@ -83208,6 +83684,40 @@ function buildDetectionSettingsSections() {
83208
83684
  step: .05,
83209
83685
  default: TRACKING_DEFAULTS.byteTrackLowIouThreshold
83210
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
+ },
83211
83721
  {
83212
83722
  type: "boolean",
83213
83723
  key: "minSpawnAreaEnabled",
@@ -83545,7 +84055,7 @@ function buildGlobalSettingsSchema(cap) {
83545
84055
  addonId: "pipeline-analytics",
83546
84056
  action: "embedding.rebuildStatus",
83547
84057
  buttonLabel: "Check progress",
83548
- successMessage: "{modelId} → {index}: indexed {indexed} · scanned {scanned} · rebuilt {rebuilt} · no key frame {missingKeyFrame} · not runnable {notRunnable} · failed {failed}"
84058
+ successMessage: "{modelId} → {index}: indexed {indexed} · scanned {scanned} · rebuilt {rebuilt} · no key frame {missingKeyFrame} · no box {missingBbox} · no event {missingEvent} · not runnable {notRunnable} · failed {failed}"
83549
84059
  },
83550
84060
  {
83551
84061
  type: "addon-action-button",
@@ -87708,6 +88218,8 @@ function decodeEmbeddingBase64(base64) {
87708
88218
  var PHANTOM_CELL_PX = 32;
87709
88219
  /** How long a cell remembers its closes. */
87710
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;
87711
88223
  var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
87712
88224
  /** Recent SHORT+MOTIONLESS track closes per `<device>:<class>:<cell>` —
87713
88225
  * see {@link noteShortMotionlessTrack}. Measurement only; each entry is
@@ -88375,6 +88887,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88375
88887
  packageBackground = null;
88376
88888
  /** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
88377
88889
  dropoutSkipsByKey = /* @__PURE__ */ new Map();
88890
+ /** Last time each processor's low-score continuation counts were logged (D665). */
88891
+ lowScoreReportAtByKey = /* @__PURE__ */ new Map();
88378
88892
  /** The per-track close-path orchestrator (S2, refactor spec 2026-07-22):
88379
88893
  * owns the single-flight sweep latch + the FULL per-track close sequence
88380
88894
  * (zero-media policy, lastFrame promotion, recognizer ends with the
@@ -89933,6 +90447,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89933
90447
  mediaStore: stores.mediaStore,
89934
90448
  trackStore: stores.trackStore,
89935
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),
89936
90458
  captureCrop: (frameHandle, bbox, frameWidth, frameHeight, padding, ownerId, deviceId) => this.captureScheduler.request({
89937
90459
  deviceId,
89938
90460
  trackId: ownerId,
@@ -91887,6 +92409,31 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91887
92409
  * tracking/zone/event state never crosses between sources. Emits the
91888
92410
  * SAME canonical events, distinguished only by `source`.
91889
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
+ }
91890
92437
  async processFrame(deviceId, frame, source, rawFrameHandle, detailSteps, occasion = "session") {
91891
92438
  if (this.shuttingDown) return;
91892
92439
  if (!await this.bindingCache.isActive(deviceId)) {
@@ -91911,7 +92458,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91911
92458
  const trk = await this.resolveDeviceTrackingSettings(deviceId);
91912
92459
  const activeCount = this.lastActiveTrackIds.get(key)?.size ?? 0;
91913
92460
  const skips = this.dropoutSkipsByKey.get(key) ?? 0;
91914
- if (shouldSkipDropoutFrame(activeCount, frame.detections.length, skips, {
92461
+ const lowScoreFloor = trk.lowScoreContinuationEnabled ? trk.lowScoreContinuationFloor : null;
92462
+ if (shouldSkipDropoutFrame(activeCount, dropoutEvidenceCount(frame, lowScoreFloor), skips, {
91915
92463
  enabled: trk.dropoutSkipEnabled,
91916
92464
  maxSkipFrames: trk.dropoutMaxSkipFrames
91917
92465
  })) {
@@ -91939,6 +92487,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91939
92487
  processor.setZones(liveZones);
91940
92488
  processor.setDetectionRules(liveRules);
91941
92489
  processor.setZoneMembershipMinOverlap(trk.zoneMembershipMinOverlap);
92490
+ processor.setLowScoreContinuationFloor(lowScoreFloor);
91942
92491
  const injection = injectSecondarySubjects(frame, this.secondarySubjects.take(key, Date.now()));
91943
92492
  for (const id of injection.covered) this.secondarySubjects.drop(key, id);
91944
92493
  const trackerFrame = injection.frame;
@@ -91954,6 +92503,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91954
92503
  timestamp: frame.timestamp,
91955
92504
  frame: trackerFrame
91956
92505
  });
92506
+ this.reportLowScoreContinuation(deviceId, key, processor);
91957
92507
  for (const r of processor.getLastLabelWriteRefusals()) {
91958
92508
  const message = r.reason.kind === "macro-class" ? "label tier collapse refused" : "label write refused";
91959
92509
  this.ctx.logger.info(message, {
@@ -95641,7 +96191,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95641
96191
  animal: trk.minSpawnAreaFracAnimal,
95642
96192
  vehicle: trk.minSpawnAreaFracVehicle
95643
96193
  },
95644
- classGroupAssoc: trk.classGroupAssoc
96194
+ classGroupAssoc: trk.classGroupAssoc,
96195
+ lowScoreContinuationEnabled: trk.lowScoreContinuationEnabled,
96196
+ lowScoreContinuationMaxMs: trk.lowScoreContinuationMaxMs,
96197
+ resurrectionTimeScaled: trk.resurrectionTimeScaled
95645
96198
  }, {
95646
96199
  stationaryThresholdSec,
95647
96200
  ...stillSpeedFracPerSec !== void 0 ? { stillSpeedFracPerSec } : {}
@@ -97132,6 +97685,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97132
97685
  refusal: "stores-unavailable"
97133
97686
  };
97134
97687
  }
97688
+ const force = input.force === true;
97689
+ if (force && input.maxTracks === void 0) {
97690
+ this.ctx.logger.warn("rebuildObjectEmbeddings: refusing a force pass with no maxTracks", { meta: { refusal: "force-requires-max-tracks" } });
97691
+ return {
97692
+ started: false,
97693
+ alreadyRunning: false,
97694
+ refusal: "force-requires-max-tracks"
97695
+ };
97696
+ }
97135
97697
  const confirmation = await this.getClusterPinConfirmer(api, "clip-rebuild").confirm();
97136
97698
  if (confirmation.kind === "refused") {
97137
97699
  this.ctx.logger.warn("rebuildObjectEmbeddings: the cluster CLIP model is not confirmed — refusing to start a pass that cannot pin one", { meta: { refusal: confirmation.reason } });
@@ -97175,16 +97737,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97175
97737
  logger: this.ctx.logger
97176
97738
  }));
97177
97739
  const deps = {
97178
- listTrackIdPage: async (cursor) => trackStore.queryRecentIds({
97740
+ listTrackIdPage: async (cursor) => trackStore.queryRecentIds(rebuildTrackPageQuery({
97179
97741
  deviceIds,
97180
97742
  ...input.since !== void 0 ? { since: input.since } : {},
97181
97743
  ...input.until !== void 0 ? { until: input.until } : {},
97182
97744
  ...cursor !== null ? { cursor } : {},
97183
97745
  limit: REBUILD_PAGE_SIZE
97184
- }),
97746
+ })),
97185
97747
  loadSource: (trackId) => loadRebuildSource({
97186
97748
  media: mediaStore,
97187
- events: eventStore
97749
+ events: eventStore,
97750
+ tracks: trackStore
97188
97751
  }, trackId),
97189
97752
  recoverKeyFrame: async (source) => {
97190
97753
  const recovered = await recordedStills.recover(source.deviceId, source.timestamp, RECOVERED_KEY_FRAME_MAX_WIDTH);
@@ -97210,7 +97773,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97210
97773
  if (split === null) return null;
97211
97774
  return {
97212
97775
  outOfSpace: split.outOfSpaceIds,
97213
- alreadyInSpace: split.inSpace
97776
+ alreadyInSpace: split.inSpace,
97777
+ inSpace: split.inSpaceIds
97214
97778
  };
97215
97779
  },
97216
97780
  logger: {
@@ -97222,6 +97786,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97222
97786
  running: true,
97223
97787
  modelId,
97224
97788
  index,
97789
+ force,
97225
97790
  startedAtMs: Date.now(),
97226
97791
  progress: {
97227
97792
  scanned: 0,
@@ -97230,6 +97795,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97230
97795
  recoveredKeyFrame: 0,
97231
97796
  missingEvent: 0,
97232
97797
  missingBbox: 0,
97798
+ boxSource: {
97799
+ subjectBox: 0,
97800
+ positions: 0,
97801
+ event: 0,
97802
+ refused: 0
97803
+ },
97804
+ forcedInSpace: 0,
97805
+ skippedOutOfSpace: 0,
97233
97806
  alreadyInSpace: 0,
97234
97807
  notRunnable: 0,
97235
97808
  noCapableNode: 0,
@@ -97237,6 +97810,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97237
97810
  }
97238
97811
  };
97239
97812
  rebuildObjectEmbeddings(deps, {
97813
+ force,
97240
97814
  ...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {},
97241
97815
  ...input.pacingMs !== void 0 ? { pacingMs: input.pacingMs } : {},
97242
97816
  onProgress: (progress) => {
@@ -97248,6 +97822,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97248
97822
  running: false,
97249
97823
  modelId,
97250
97824
  index,
97825
+ force,
97251
97826
  startedAtMs: this.rebuildState.startedAtMs ?? Date.now(),
97252
97827
  finishedAtMs: Date.now(),
97253
97828
  progress: outcome,
@@ -97263,6 +97838,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97263
97838
  running: false,
97264
97839
  modelId,
97265
97840
  index,
97841
+ force,
97266
97842
  startedAtMs: this.rebuildState.startedAtMs ?? Date.now(),
97267
97843
  finishedAtMs: Date.now(),
97268
97844
  error: String(err)
@@ -97277,7 +97853,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97277
97853
  modelId,
97278
97854
  index,
97279
97855
  deviceId: input.deviceId ?? "all",
97280
- maxTracks: input.maxTracks ?? "unbounded"
97856
+ maxTracks: input.maxTracks ?? "unbounded",
97857
+ force
97281
97858
  } });
97282
97859
  return {
97283
97860
  started: true,
@@ -98279,8 +98856,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
98279
98856
  running: state.running,
98280
98857
  scanned: p?.scanned ?? 0,
98281
98858
  rebuilt: p?.rebuilt ?? 0,
98282
- missingKeyFrame: (p?.missingKeyFrame ?? 0) + (p?.missingEvent ?? 0),
98859
+ missingKeyFrame: p?.missingKeyFrame ?? 0,
98860
+ missingEvent: p?.missingEvent ?? 0,
98283
98861
  missingBbox: p?.missingBbox ?? 0,
98862
+ boxSource: p?.boxSource ?? {
98863
+ subjectBox: 0,
98864
+ positions: 0,
98865
+ event: 0,
98866
+ refused: 0
98867
+ },
98868
+ alreadyInSpace: p?.alreadyInSpace ?? 0,
98869
+ force: state.force ?? null,
98870
+ forcedInSpace: p?.forcedInSpace ?? 0,
98284
98871
  notRunnable: p?.notRunnable ?? 0,
98285
98872
  noCapableNode: p?.noCapableNode ?? 0,
98286
98873
  failed: p?.failed ?? 0,