@camstack/addon-post-analysis 1.2.267 → 1.2.269
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-DO_99QlZ.mjs → dist-ABnG0S3f.mjs} +1 -0
- package/dist/{dist-Cgy2NW9x.js → dist-B78I61U1.js} +1 -0
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CJW84Bpj.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D42gEB0t.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C8ZDqFq3.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-B3uXhoOC.mjs → hostInit-fxXc6Mk3.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +650 -57
- package/dist/pipeline-analytics/index.mjs +648 -57
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DtSv_IZa.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-B78I61U1.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -2301,12 +2301,65 @@ function artifactShelfRefusal(locations, localNodeId) {
|
|
|
2301
2301
|
//#endregion
|
|
2302
2302
|
//#region ../types/dist/node.mjs
|
|
2303
2303
|
var node_exports = /* @__PURE__ */ require_dist.__exportAll({
|
|
2304
|
+
FFMPEG_RELEASE: () => FFMPEG_RELEASE,
|
|
2304
2305
|
FilesystemStorageProvider: () => FilesystemStorageProvider,
|
|
2305
2306
|
PYTHON_VERSION: () => PYTHON_VERSION,
|
|
2306
2307
|
signExpiringUrl: () => signExpiringUrl,
|
|
2307
2308
|
verifyExpiringUrl: () => verifyExpiringUrl
|
|
2308
2309
|
});
|
|
2309
2310
|
/**
|
|
2311
|
+
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
2312
|
+
*
|
|
2313
|
+
* ## Why a download, on every node, including the container
|
|
2314
|
+
*
|
|
2315
|
+
* The version of ffmpeg used to be CODE: baked into the image, changeable only
|
|
2316
|
+
* by rebuilding and rolling it. Now it is DATA — this file states the release,
|
|
2317
|
+
* and a node that boots with a different one downloads it. Changing ffmpeg is a
|
|
2318
|
+
* framework bump, not an image rebuild.
|
|
2319
|
+
*
|
|
2320
|
+
* It also settles the licence question by removing it. FFmpeg's licence is
|
|
2321
|
+
* chosen at compile time: the core is LGPL 2.1+, `--enable-gpl` makes it GPL
|
|
2322
|
+
* (for x264/x265), `--enable-version3` makes that v3, and `--enable-nonfree`
|
|
2323
|
+
* makes the result undistributable. These are GPL v3 builds, and we do not
|
|
2324
|
+
* distribute them: the user's own node fetches the archive from the project
|
|
2325
|
+
* that publishes it, so we convey no GPL work and inherit none of the
|
|
2326
|
+
* obligations that come with conveying one. That is the whole reason the image
|
|
2327
|
+
* no longer installs ffmpeg at all — not a technical preference.
|
|
2328
|
+
*
|
|
2329
|
+
* ## Why jellyfin-ffmpeg
|
|
2330
|
+
*
|
|
2331
|
+
* One vendor covering linux-amd64, linux-arm64, darwin-arm64 and darwin-x64
|
|
2332
|
+
* from one release, with the widest hardware surface of anything maintained:
|
|
2333
|
+
* VAAPI, QSV, NVENC/NVDEC, AMF, Vulkan, OpenCL and DRM on amd64, plus Rockchip
|
|
2334
|
+
* MPP on arm64. It reaches the vendor libraries through dlopen trampolines,
|
|
2335
|
+
* so the binary is not linked against a driver stack it may not find.
|
|
2336
|
+
*
|
|
2337
|
+
* **What it still needs from the host, measured 2026-09-19.** On the Unraid
|
|
2338
|
+
* HOST, which has no libva, `-vaapi_device` aborts the process outright —
|
|
2339
|
+
* `implib-gen: libva-drm.so.2: failed to load library ... Assertion '0 &&
|
|
2340
|
+
* "Assertion in generated code"' failed`. Inside our container, which installs
|
|
2341
|
+
* libva plus the iHD driver, the same command encodes: verified end to end with
|
|
2342
|
+
* `testsrc → format=nv12,hwupload → h264_vaapi`. So the image drops ffmpeg and
|
|
2343
|
+
* KEEPS the Intel media stack; the drivers are the part a binary cannot bring.
|
|
2344
|
+
*
|
|
2345
|
+
* ## Why each artifact carries a checksum and a claim
|
|
2346
|
+
*
|
|
2347
|
+
* The download is now the only source, so `sha256` is verified before anything
|
|
2348
|
+
* is extracted — jellyfin publishes no checksums on its release assets, so
|
|
2349
|
+
* these are ours, taken at the version bump. And `expectedHwaccels` is what the
|
|
2350
|
+
* build is supposed to be able to do: a binary that probes short of its own
|
|
2351
|
+
* claim is a source that changed under us, which is precisely what went
|
|
2352
|
+
* unnoticed for months when a static build turned out to carry no VAAPI at all
|
|
2353
|
+
* (D536).
|
|
2354
|
+
*/
|
|
2355
|
+
/** The pinned jellyfin-ffmpeg release. Changing this is the version change. */
|
|
2356
|
+
var FFMPEG_RELEASE = "8.1.2-5";
|
|
2357
|
+
var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
|
|
2358
|
+
function portable(target) {
|
|
2359
|
+
return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
|
|
2360
|
+
}
|
|
2361
|
+
portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64");
|
|
2362
|
+
/**
|
|
2310
2363
|
* Download portable Python (headless) from bjia56/portable-python.
|
|
2311
2364
|
*
|
|
2312
2365
|
* Source: https://github.com/bjia56/portable-python
|
|
@@ -38384,7 +38437,7 @@ var StateAnalyzer = class {
|
|
|
38384
38437
|
}
|
|
38385
38438
|
const velocity = track.velocity ? Math.sqrt(track.velocity.dx ** 2 + track.velocity.dy ** 2) : 0;
|
|
38386
38439
|
const dwellTimeMs = timestamp - ts.enteredAt;
|
|
38387
|
-
const settled =
|
|
38440
|
+
const settled = this.speedSaysSettled(ts, track.bbox, velocity) || this.windowSaysStill(ts, track.bbox, timestamp);
|
|
38388
38441
|
let state;
|
|
38389
38442
|
if (ts.frameCount <= this.config.enteringFrames) state = "entering";
|
|
38390
38443
|
else if (settled) {
|
|
@@ -38420,6 +38473,37 @@ var StateAnalyzer = class {
|
|
|
38420
38473
|
return results;
|
|
38421
38474
|
}
|
|
38422
38475
|
/**
|
|
38476
|
+
* The SPEED half of the settled test.
|
|
38477
|
+
*
|
|
38478
|
+
* Uncalibrated (`stillSpeedFracPerSec` undefined) this is the original line
|
|
38479
|
+
* verbatim — the tracker's px-per-update velocity against the flat
|
|
38480
|
+
* `velocityThreshold`. Nothing about an uncalibrated camera changes.
|
|
38481
|
+
*
|
|
38482
|
+
* Calibrated, the same question is asked in the camera's own units: how far
|
|
38483
|
+
* the centroid moved between the last two OBSERVED samples, over the
|
|
38484
|
+
* subject's own box diagonal, over the elapsed SECONDS. Per second and not
|
|
38485
|
+
* per update because these cameras are `armed:on-motion` and deliver a
|
|
38486
|
+
* settle burst plus a poll — 56 ms to 2135 ms between stored positions on
|
|
38487
|
+
* one subject (D399) — so a per-update figure says more about the cadence
|
|
38488
|
+
* than about the subject.
|
|
38489
|
+
*
|
|
38490
|
+
* An interval that cannot be measured (a single sample, a non-positive
|
|
38491
|
+
* elapsed time, a box with no size) is UNKNOWN, and unknown never reads as
|
|
38492
|
+
* settled. A track that young is still inside `enteringFrames` anyway.
|
|
38493
|
+
*/
|
|
38494
|
+
speedSaysSettled(ts, bbox, velocity) {
|
|
38495
|
+
const bar = this.config.stillSpeedFracPerSec;
|
|
38496
|
+
if (bar === void 0) return velocity <= this.config.velocityThreshold;
|
|
38497
|
+
const last = ts.recent[ts.recent.length - 1];
|
|
38498
|
+
const prev = ts.recent[ts.recent.length - 2];
|
|
38499
|
+
if (!last || !prev) return false;
|
|
38500
|
+
const elapsedSec = (last.at - prev.at) / 1e3;
|
|
38501
|
+
if (!(elapsedSec > 0)) return false;
|
|
38502
|
+
const diagonal = Math.hypot(bbox.w, bbox.h);
|
|
38503
|
+
if (!(diagonal > 0)) return false;
|
|
38504
|
+
return Math.hypot(last.x - prev.x, last.y - prev.y) / diagonal / elapsedSec <= bar;
|
|
38505
|
+
}
|
|
38506
|
+
/**
|
|
38423
38507
|
* Whether the centroid has stayed inside a small span for the whole window.
|
|
38424
38508
|
*
|
|
38425
38509
|
* The span is measured across the samples themselves (their bounding box),
|
|
@@ -48128,6 +48212,338 @@ function makeViewerSettingsActionHandlers(store) {
|
|
|
48128
48212
|
};
|
|
48129
48213
|
}
|
|
48130
48214
|
//#endregion
|
|
48215
|
+
//#region src/pipeline-analytics/stillness-calibration-actions.ts
|
|
48216
|
+
/**
|
|
48217
|
+
* stillness-calibration-actions — "measure this camera's own stillness scale".
|
|
48218
|
+
*
|
|
48219
|
+
* ## Why an `addons.custom` action and not a cap method
|
|
48220
|
+
*
|
|
48221
|
+
* Same reason as `face.rescoreTrack` beside it: a cap method's wire schema
|
|
48222
|
+
* lives in `@camstack/types`, which travels inside the `@camstack/server`
|
|
48223
|
+
* closure, so a new one is not callable until a server train ships. Here the
|
|
48224
|
+
* schema lives in this addon's own dist and the hub forwards an opaque
|
|
48225
|
+
* envelope, so `camstack deploy` is the whole delivery.
|
|
48226
|
+
*
|
|
48227
|
+
* ## Why `apply` is explicit and not a fallback
|
|
48228
|
+
*
|
|
48229
|
+
* A calibration run READS recorded tracks and does arithmetic. Applying it
|
|
48230
|
+
* WRITES the camera's settings and changes what the tracker calls settled from
|
|
48231
|
+
* the next frame on. Those are different enough in consequence that a button
|
|
48232
|
+
* labelled "calibrate" must not choose between them on the operator's behalf —
|
|
48233
|
+
* the run reports, the operator applies. A REFUSED run never writes the bar
|
|
48234
|
+
* whatever `apply` says; it writes only the note, so the refusal and its reason
|
|
48235
|
+
* are visible in the form instead of being a toast that scrolls away.
|
|
48236
|
+
*/
|
|
48237
|
+
/** Mirrors `StillnessCalibrationRefusal` — kept literal so the wire schema is
|
|
48238
|
+
* self-describing and a new reason is a deliberate edit in both places. */
|
|
48239
|
+
var StillnessRefusalSchema = require_dist._enum([
|
|
48240
|
+
"no-tracks",
|
|
48241
|
+
"too-few-tracks",
|
|
48242
|
+
"too-few-samples",
|
|
48243
|
+
"no-motion-evidence"
|
|
48244
|
+
]);
|
|
48245
|
+
var StillnessMeasurementSchema = require_dist.object({
|
|
48246
|
+
/** Tracks that contributed at least one speed sample. */
|
|
48247
|
+
trackCount: require_dist.number().int(),
|
|
48248
|
+
/** Tracks the pass actually read, after the cost bound. */
|
|
48249
|
+
tracksRead: require_dist.number().int(),
|
|
48250
|
+
sampleCount: require_dist.number().int(),
|
|
48251
|
+
/** Speed quantiles, subject-box-diagonals per second. `null` when there was
|
|
48252
|
+
* nothing to measure — never 0, which would read as a measured stillness
|
|
48253
|
+
* (D393). */
|
|
48254
|
+
p25: require_dist.number().nullable(),
|
|
48255
|
+
p50: require_dist.number().nullable(),
|
|
48256
|
+
p90: require_dist.number().nullable(),
|
|
48257
|
+
/** Median gap between stored positions (s) — the camera's real cadence. */
|
|
48258
|
+
medianIntervalSec: require_dist.number().nullable(),
|
|
48259
|
+
/** Median subject box diagonal (px) the speeds were normalised by. */
|
|
48260
|
+
medianSubjectDiagonalPx: require_dist.number().nullable()
|
|
48261
|
+
});
|
|
48262
|
+
var StillnessCalibrateInputSchema = require_dist.object({
|
|
48263
|
+
deviceId: require_dist.number().int(),
|
|
48264
|
+
/** How far back to read. Bounded: this pass parses ~10 kB of trajectory a
|
|
48265
|
+
* row, so the window is a suggestion and `maxTracks` is the real bound. */
|
|
48266
|
+
sinceHours: require_dist.number().min(.25).max(720).default(72),
|
|
48267
|
+
/** Hard cost bound on tracks read, newest first. */
|
|
48268
|
+
maxTracks: require_dist.number().int().min(1).max(1e3).default(200),
|
|
48269
|
+
/**
|
|
48270
|
+
* Write the chosen bar onto the camera. Never implicit, and never done at
|
|
48271
|
+
* all for a refused run.
|
|
48272
|
+
*/
|
|
48273
|
+
apply: require_dist.boolean().default(false)
|
|
48274
|
+
});
|
|
48275
|
+
var StillnessCalibrateResultSchema = require_dist.object({
|
|
48276
|
+
deviceId: require_dist.number().int(),
|
|
48277
|
+
/** The window actually read (epoch ms). */
|
|
48278
|
+
since: require_dist.number().int(),
|
|
48279
|
+
until: require_dist.number().int(),
|
|
48280
|
+
accepted: require_dist.boolean(),
|
|
48281
|
+
/** Why it refused. Absent on an accepted run. */
|
|
48282
|
+
reason: StillnessRefusalSchema.optional(),
|
|
48283
|
+
/** The chosen bar, subject-box-diagonals per second. Absent on a refusal —
|
|
48284
|
+
* never 0, which the settings layer reads as "uncalibrated". */
|
|
48285
|
+
bar: require_dist.number().optional(),
|
|
48286
|
+
/** The raw quantile before the rails, so the clamp is arguable. */
|
|
48287
|
+
rawBar: require_dist.number().optional(),
|
|
48288
|
+
clampedBy: require_dist._enum([
|
|
48289
|
+
"none",
|
|
48290
|
+
"floor",
|
|
48291
|
+
"ceiling",
|
|
48292
|
+
"min",
|
|
48293
|
+
"max"
|
|
48294
|
+
]).optional(),
|
|
48295
|
+
measurement: StillnessMeasurementSchema,
|
|
48296
|
+
/** The bar this camera was on BEFORE the run — `null` = uncalibrated. Read
|
|
48297
|
+
* before any write, so the operator sees both numbers. */
|
|
48298
|
+
previousBar: require_dist.number().nullable(),
|
|
48299
|
+
/** Whether the camera's settings were actually written. */
|
|
48300
|
+
applied: require_dist.boolean(),
|
|
48301
|
+
/** The one-line note stored on the camera (accepted or refused). */
|
|
48302
|
+
note: require_dist.string()
|
|
48303
|
+
});
|
|
48304
|
+
var stillnessCalibrationActions = require_dist.defineCustomActions({
|
|
48305
|
+
/**
|
|
48306
|
+
* `admin` because it WRITES a per-camera detection threshold when `apply` is
|
|
48307
|
+
* set, and because the report describes how the camera's subjects move,
|
|
48308
|
+
* which is not a viewer-level fact.
|
|
48309
|
+
*/
|
|
48310
|
+
"stillness.calibrate": require_dist.customAction(StillnessCalibrateInputSchema, StillnessCalibrateResultSchema, {
|
|
48311
|
+
kind: "mutation",
|
|
48312
|
+
auth: "admin"
|
|
48313
|
+
}) });
|
|
48314
|
+
/**
|
|
48315
|
+
* The shipped limits.
|
|
48316
|
+
*
|
|
48317
|
+
* `minTracks` 12 / `minSamples` 200: the four live cameras sampled above
|
|
48318
|
+
* produced 697–8739 samples from 42–51 tracks in a single 60-track read, so a
|
|
48319
|
+
* camera that cannot clear these has genuinely not been watched enough.
|
|
48320
|
+
* `maxTracks` 200 bounds the read: the `positions` blob averages ~10 kB a row
|
|
48321
|
+
* (`track-store.ts`), so this pass is ~2 MB of JSON at its worst.
|
|
48322
|
+
*/
|
|
48323
|
+
var DEFAULT_STILLNESS_CALIBRATION_LIMITS = {
|
|
48324
|
+
minTracks: 12,
|
|
48325
|
+
minSamples: 200,
|
|
48326
|
+
maxTracks: 200,
|
|
48327
|
+
minIntervalSec: .02,
|
|
48328
|
+
maxIntervalSec: 3,
|
|
48329
|
+
quantile: .25,
|
|
48330
|
+
floorFactor: .02,
|
|
48331
|
+
ceilFactor: .25,
|
|
48332
|
+
minBar: .001,
|
|
48333
|
+
maxBar: 1
|
|
48334
|
+
};
|
|
48335
|
+
/** Linear-interpolated quantile. `NaN` for an empty list — never 0, which
|
|
48336
|
+
* would read as a measured zero (D393). */
|
|
48337
|
+
function quantileOf(values, p) {
|
|
48338
|
+
if (values.length === 0) return NaN;
|
|
48339
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
48340
|
+
const idx = (sorted.length - 1) * Math.min(Math.max(p, 0), 1);
|
|
48341
|
+
const lo = Math.floor(idx);
|
|
48342
|
+
const hi = Math.ceil(idx);
|
|
48343
|
+
const at = sorted[lo];
|
|
48344
|
+
const next = sorted[hi];
|
|
48345
|
+
if (at === void 0 || next === void 0) return NaN;
|
|
48346
|
+
return at + (next - at) * (idx - lo);
|
|
48347
|
+
}
|
|
48348
|
+
/**
|
|
48349
|
+
* Per-sample centroid speeds for a camera, in subject-box-diagonals per second.
|
|
48350
|
+
*
|
|
48351
|
+
* A pair of consecutive positions contributes a sample only when the interval
|
|
48352
|
+
* between them is inside `[minIntervalSec, maxIntervalSec]` and the later box
|
|
48353
|
+
* has a positive diagonal. A gap outside that band is a session boundary or a
|
|
48354
|
+
* duplicate read, not a measurement of how fast anything travelled; a box with
|
|
48355
|
+
* no size is unknown geometry, and unknown is never a sample.
|
|
48356
|
+
*/
|
|
48357
|
+
function collectStillnessSamples(tracks, limits) {
|
|
48358
|
+
const speeds = [];
|
|
48359
|
+
const intervals = [];
|
|
48360
|
+
const diagonals = [];
|
|
48361
|
+
let trackCount = 0;
|
|
48362
|
+
const read = tracks.slice(0, limits.maxTracks);
|
|
48363
|
+
for (const track of read) {
|
|
48364
|
+
const positions = [...track.positions].sort((a, b) => a.timestamp - b.timestamp);
|
|
48365
|
+
let contributed = false;
|
|
48366
|
+
for (let i = 1; i < positions.length; i++) {
|
|
48367
|
+
const prev = positions[i - 1];
|
|
48368
|
+
const cur = positions[i];
|
|
48369
|
+
if (prev === void 0 || cur === void 0) continue;
|
|
48370
|
+
const dtSec = (cur.timestamp - prev.timestamp) / 1e3;
|
|
48371
|
+
if (dtSec < limits.minIntervalSec || dtSec > limits.maxIntervalSec) continue;
|
|
48372
|
+
const diagonal = Math.hypot(cur.bbox.w, cur.bbox.h);
|
|
48373
|
+
if (!(diagonal > 0)) continue;
|
|
48374
|
+
const travelled = Math.hypot(cur.x - prev.x, cur.y - prev.y);
|
|
48375
|
+
speeds.push(travelled / diagonal / dtSec);
|
|
48376
|
+
intervals.push(dtSec);
|
|
48377
|
+
diagonals.push(diagonal);
|
|
48378
|
+
contributed = true;
|
|
48379
|
+
}
|
|
48380
|
+
if (contributed) trackCount++;
|
|
48381
|
+
}
|
|
48382
|
+
return {
|
|
48383
|
+
speeds,
|
|
48384
|
+
intervals,
|
|
48385
|
+
diagonals,
|
|
48386
|
+
trackCount,
|
|
48387
|
+
tracksRead: read.length
|
|
48388
|
+
};
|
|
48389
|
+
}
|
|
48390
|
+
function measure$1(samples) {
|
|
48391
|
+
return {
|
|
48392
|
+
trackCount: samples.trackCount,
|
|
48393
|
+
sampleCount: samples.speeds.length,
|
|
48394
|
+
p25: quantileOf(samples.speeds, .25),
|
|
48395
|
+
p50: quantileOf(samples.speeds, .5),
|
|
48396
|
+
p90: quantileOf(samples.speeds, .9),
|
|
48397
|
+
medianIntervalSec: quantileOf(samples.intervals, .5),
|
|
48398
|
+
medianSubjectDiagonalPx: quantileOf(samples.diagonals, .5)
|
|
48399
|
+
};
|
|
48400
|
+
}
|
|
48401
|
+
/**
|
|
48402
|
+
* Choose this camera's stillness speed bar, or refuse with a named reason.
|
|
48403
|
+
*
|
|
48404
|
+
* The order of the gates is the order of the questions: is there anything at
|
|
48405
|
+
* all, is it enough tracks, is it enough samples, and did anything on this
|
|
48406
|
+
* camera ever actually move. The last one is not pedantry — the whole method
|
|
48407
|
+
* scales the bar against the camera's own travel, so with no travel there is
|
|
48408
|
+
* nothing to scale against and the default is the honest answer.
|
|
48409
|
+
*/
|
|
48410
|
+
function calibrateStillnessSpeed(samples, limits) {
|
|
48411
|
+
const measurement = measure$1(samples);
|
|
48412
|
+
if (samples.tracksRead === 0) return {
|
|
48413
|
+
ok: false,
|
|
48414
|
+
reason: "no-tracks",
|
|
48415
|
+
measurement
|
|
48416
|
+
};
|
|
48417
|
+
if (samples.trackCount < limits.minTracks) return {
|
|
48418
|
+
ok: false,
|
|
48419
|
+
reason: "too-few-tracks",
|
|
48420
|
+
measurement
|
|
48421
|
+
};
|
|
48422
|
+
if (samples.speeds.length < limits.minSamples) return {
|
|
48423
|
+
ok: false,
|
|
48424
|
+
reason: "too-few-samples",
|
|
48425
|
+
measurement
|
|
48426
|
+
};
|
|
48427
|
+
if (!(measurement.p90 > 0)) return {
|
|
48428
|
+
ok: false,
|
|
48429
|
+
reason: "no-motion-evidence",
|
|
48430
|
+
measurement
|
|
48431
|
+
};
|
|
48432
|
+
const rawBar = quantileOf(samples.speeds, limits.quantile);
|
|
48433
|
+
const floor = measurement.p90 * limits.floorFactor;
|
|
48434
|
+
const ceiling = measurement.p90 * limits.ceilFactor;
|
|
48435
|
+
let bar = rawBar;
|
|
48436
|
+
let clampedBy = "none";
|
|
48437
|
+
if (bar < floor) {
|
|
48438
|
+
bar = floor;
|
|
48439
|
+
clampedBy = "floor";
|
|
48440
|
+
} else if (bar > ceiling) {
|
|
48441
|
+
bar = ceiling;
|
|
48442
|
+
clampedBy = "ceiling";
|
|
48443
|
+
}
|
|
48444
|
+
if (bar < limits.minBar) {
|
|
48445
|
+
bar = limits.minBar;
|
|
48446
|
+
clampedBy = "min";
|
|
48447
|
+
} else if (bar > limits.maxBar) {
|
|
48448
|
+
bar = limits.maxBar;
|
|
48449
|
+
clampedBy = "max";
|
|
48450
|
+
}
|
|
48451
|
+
return {
|
|
48452
|
+
ok: true,
|
|
48453
|
+
bar,
|
|
48454
|
+
rawBar,
|
|
48455
|
+
clampedBy,
|
|
48456
|
+
measurement
|
|
48457
|
+
};
|
|
48458
|
+
}
|
|
48459
|
+
/** One line an operator can read: what was measured, from how much, and what
|
|
48460
|
+
* rule produced the stored number. Kept short — it rides the settings blob. */
|
|
48461
|
+
function describeStillnessCalibration(outcome) {
|
|
48462
|
+
const m = outcome.measurement;
|
|
48463
|
+
const n = (v) => Number.isFinite(v) ? v.toFixed(4) : "unknown";
|
|
48464
|
+
const evidence = `${m.sampleCount} samples / ${m.trackCount} tracks, p25 ${n(m.p25)}, p50 ${n(m.p50)}, p90 ${n(m.p90)} subject-diagonals/s`;
|
|
48465
|
+
if (!outcome.ok) return `refused (${outcome.reason}) — ${evidence}`;
|
|
48466
|
+
return `bar ${n(outcome.bar)} (raw ${n(outcome.rawBar)}, clamp ${outcome.clampedBy}) — ${evidence}`;
|
|
48467
|
+
}
|
|
48468
|
+
//#endregion
|
|
48469
|
+
//#region src/pipeline-analytics/stillness-settings.ts
|
|
48470
|
+
/**
|
|
48471
|
+
* Per-device STILLNESS SCALE — the one number that says how fast a subject on
|
|
48472
|
+
* THIS camera has to move before it is travelling.
|
|
48473
|
+
*
|
|
48474
|
+
* A threshold belongs to whoever GENERATES the event, per device, never baked
|
|
48475
|
+
* into a classifier default. The judgement this feeds is
|
|
48476
|
+
* `StateAnalyzer.speedSaysSettled`; the measurement behind the number is
|
|
48477
|
+
* `pipeline/tracker/stillness-scale.ts`, which also carries the live
|
|
48478
|
+
* measurement showing why one number cannot serve a 30 m driveway and a 2 m
|
|
48479
|
+
* doorway.
|
|
48480
|
+
*
|
|
48481
|
+
* Cascade and shape mirror `stationary-settings` / `media-settings`: a
|
|
48482
|
+
* per-device override on top of the default, resolved per FIELD, and a parse
|
|
48483
|
+
* that never throws on a bad blob.
|
|
48484
|
+
*
|
|
48485
|
+
* **Absent behaves exactly as today.** The device store is a flat numeric blob
|
|
48486
|
+
* with no room for "unset", so the stored form of absent is `0` — and `0` is
|
|
48487
|
+
* not a bar anyone could want (it would mean nothing on this camera is ever
|
|
48488
|
+
* still) and not a value the calibration can produce (`minBar`). The decision
|
|
48489
|
+
* layer never sees it: {@link resolveStillSpeedFracPerSec} maps it to
|
|
48490
|
+
* `undefined`, and `undefined` is what leaves `StateAnalyzer` on its original
|
|
48491
|
+
* absolute `velocityThreshold` path.
|
|
48492
|
+
*/
|
|
48493
|
+
var StillnessSettingsSchema = require_dist.object({
|
|
48494
|
+
/**
|
|
48495
|
+
* The camera's "not travelling" bar: centroid speed as a fraction of the
|
|
48496
|
+
* SUBJECT's own box diagonal, per second. `0` = not calibrated (see above).
|
|
48497
|
+
*
|
|
48498
|
+
* Measured p50 on the live hub 2026-09-19, for scale: 0.0136 (device 592,
|
|
48499
|
+
* close view, mostly parked subjects) to 0.3081 (device 615, far view, only
|
|
48500
|
+
* transits).
|
|
48501
|
+
*/
|
|
48502
|
+
stillnessSpeedFracPerSec: require_dist.number().min(0).max(1).default(0),
|
|
48503
|
+
/**
|
|
48504
|
+
* When the number above was last CHOSEN — by a calibration run or by the
|
|
48505
|
+
* operator. `0` = never. Purely a readout; nothing gates on it.
|
|
48506
|
+
*/
|
|
48507
|
+
stillnessCalibratedAt: require_dist.number().int().min(0).default(0),
|
|
48508
|
+
/**
|
|
48509
|
+
* What the last calibration run measured and decided, in one line
|
|
48510
|
+
* (`describeStillnessCalibration`) — including a REFUSAL and its named
|
|
48511
|
+
* reason, because "why is this camera still on the default" has to be
|
|
48512
|
+
* answerable without re-running anything.
|
|
48513
|
+
*/
|
|
48514
|
+
stillnessCalibrationNote: require_dist.string().max(400).default("")
|
|
48515
|
+
});
|
|
48516
|
+
var STILLNESS_DEFAULTS = StillnessSettingsSchema.parse({});
|
|
48517
|
+
/**
|
|
48518
|
+
* Resolve a per-device store blob into typed stillness settings. Unknown or
|
|
48519
|
+
* invalid fields fall back to the default for that field — including a bar
|
|
48520
|
+
* outside the calibration's own rails, which is refused rather than clamped:
|
|
48521
|
+
* a number the calibration could never have produced is not evidence about
|
|
48522
|
+
* this camera, and silently bending it into range would present it as if it
|
|
48523
|
+
* were.
|
|
48524
|
+
*/
|
|
48525
|
+
function resolveStillnessSettings(raw) {
|
|
48526
|
+
const pick = (key) => {
|
|
48527
|
+
const parsed = StillnessSettingsSchema.shape[key].safeParse(raw[key]);
|
|
48528
|
+
return parsed.success ? parsed.data : STILLNESS_DEFAULTS[key];
|
|
48529
|
+
};
|
|
48530
|
+
return {
|
|
48531
|
+
stillnessSpeedFracPerSec: pick("stillnessSpeedFracPerSec"),
|
|
48532
|
+
stillnessCalibratedAt: pick("stillnessCalibratedAt"),
|
|
48533
|
+
stillnessCalibrationNote: pick("stillnessCalibrationNote")
|
|
48534
|
+
};
|
|
48535
|
+
}
|
|
48536
|
+
/**
|
|
48537
|
+
* The bar the decision layer gets: a positive, in-rails number, or
|
|
48538
|
+
* `undefined` for "this camera is not calibrated — judge it exactly as
|
|
48539
|
+
* before". The ONE place the stored-zero convention is interpreted.
|
|
48540
|
+
*/
|
|
48541
|
+
function resolveStillSpeedFracPerSec(settings) {
|
|
48542
|
+
const bar = settings.stillnessSpeedFracPerSec;
|
|
48543
|
+
if (!(bar >= .001) || bar > 1) return void 0;
|
|
48544
|
+
return bar;
|
|
48545
|
+
}
|
|
48546
|
+
//#endregion
|
|
48131
48547
|
//#region src/pipeline-analytics/viewer-settings-legacy-adoption.ts
|
|
48132
48548
|
/**
|
|
48133
48549
|
* @durable class=config owner=pipeline-analytics
|
|
@@ -71744,6 +72160,56 @@ var SyntheticTrackMaterializer = class {
|
|
|
71744
72160
|
return track;
|
|
71745
72161
|
}
|
|
71746
72162
|
};
|
|
72163
|
+
//#endregion
|
|
72164
|
+
//#region src/pipeline-analytics/stationary-settings.ts
|
|
72165
|
+
/**
|
|
72166
|
+
* Per-device stationary-object (parked/idle suppression) settings. Cascade: a
|
|
72167
|
+
* per-device override on top of the global default, resolved per field (an
|
|
72168
|
+
* invalid/missing value falls back to its default — parse never throws).
|
|
72169
|
+
* Mirrors `media-settings` / `tracking-settings`.
|
|
72170
|
+
*
|
|
72171
|
+
* The defaults are the SAME constants the registry uses at runtime
|
|
72172
|
+
* (`stationary-types.ts`), imported (not copied) so an unset value and a reset
|
|
72173
|
+
* value both resolve to exactly today's behaviour — "reset == unset == today",
|
|
72174
|
+
* with no drift. Guarded by a unit test asserting the equality.
|
|
72175
|
+
*/
|
|
72176
|
+
var StationarySettingsSchema = require_dist.object({
|
|
72177
|
+
/** Master switch. Off ⇒ this camera gathers no evidence and suppresses
|
|
72178
|
+
* nothing (every parked object keeps spawning normal tracks). */
|
|
72179
|
+
enabled: require_dist.boolean().default(true),
|
|
72180
|
+
/** IoU at/above which a detection is the same parked object, unmoved →
|
|
72181
|
+
* suppress its spawn. `SUPPRESS_IOU`. */
|
|
72182
|
+
suppressIou: require_dist.number().min(.3).max(.9).default(SUPPRESS_IOU),
|
|
72183
|
+
/** Minimum IoU for a detection to be ASSOCIATED with an entry (confirm or
|
|
72184
|
+
* wake it) — the overlap gate that stops a different vehicle from waking a
|
|
72185
|
+
* parked entry (the 617 flood fix). `WAKE_ASSOC_IOU`. */
|
|
72186
|
+
wakeAssocIou: require_dist.number().min(.02).max(.5).default(WAKE_ASSOC_IOU),
|
|
72187
|
+
/** Recent-window stillness after which a track stops holding its motion
|
|
72188
|
+
* session open (also the minimum track age). Creates nothing — birth is
|
|
72189
|
+
* decided by repeated evidence. `SESSION_STILLNESS_WINDOW_MS`. */
|
|
72190
|
+
sessionStillnessWindowMs: require_dist.number().int().min(5e3).max(12e4).default(SESSION_STILLNESS_WINDOW_MS),
|
|
72191
|
+
/** Observed-time TTL: retire an entry unconfirmed for this long (measured on
|
|
72192
|
+
* frames-flowing time, not wall-clock). `ENTRY_TTL_MS`. */
|
|
72193
|
+
entryTtlMs: require_dist.number().int().min(6e4).max(18e5).default(ENTRY_TTL_MS)
|
|
72194
|
+
});
|
|
72195
|
+
var STATIONARY_DEFAULTS = StationarySettingsSchema.parse({});
|
|
72196
|
+
/**
|
|
72197
|
+
* Resolve a per-device store blob into typed stationary settings. Unknown/invalid
|
|
72198
|
+
* fields fall back to the default for that field (never throws on a bad blob).
|
|
72199
|
+
*/
|
|
72200
|
+
function resolveStationarySettings(raw) {
|
|
72201
|
+
const pick = (key) => {
|
|
72202
|
+
const parsed = StationarySettingsSchema.shape[key].safeParse(raw[key]);
|
|
72203
|
+
return parsed.success ? parsed.data : STATIONARY_DEFAULTS[key];
|
|
72204
|
+
};
|
|
72205
|
+
return {
|
|
72206
|
+
enabled: pick("enabled"),
|
|
72207
|
+
suppressIou: pick("suppressIou"),
|
|
72208
|
+
wakeAssocIou: pick("wakeAssocIou"),
|
|
72209
|
+
sessionStillnessWindowMs: pick("sessionStillnessWindowMs"),
|
|
72210
|
+
entryTtlMs: pick("entryTtlMs")
|
|
72211
|
+
};
|
|
72212
|
+
}
|
|
71747
72213
|
var TrackingSettingsSchema = require_dist.object({
|
|
71748
72214
|
/**
|
|
71749
72215
|
* How much of a detection's box must lie inside a zone (0-1 fraction of the
|
|
@@ -71944,56 +72410,6 @@ function resolveTrackingSettings(raw) {
|
|
|
71944
72410
|
};
|
|
71945
72411
|
}
|
|
71946
72412
|
//#endregion
|
|
71947
|
-
//#region src/pipeline-analytics/stationary-settings.ts
|
|
71948
|
-
/**
|
|
71949
|
-
* Per-device stationary-object (parked/idle suppression) settings. Cascade: a
|
|
71950
|
-
* per-device override on top of the global default, resolved per field (an
|
|
71951
|
-
* invalid/missing value falls back to its default — parse never throws).
|
|
71952
|
-
* Mirrors `media-settings` / `tracking-settings`.
|
|
71953
|
-
*
|
|
71954
|
-
* The defaults are the SAME constants the registry uses at runtime
|
|
71955
|
-
* (`stationary-types.ts`), imported (not copied) so an unset value and a reset
|
|
71956
|
-
* value both resolve to exactly today's behaviour — "reset == unset == today",
|
|
71957
|
-
* with no drift. Guarded by a unit test asserting the equality.
|
|
71958
|
-
*/
|
|
71959
|
-
var StationarySettingsSchema = require_dist.object({
|
|
71960
|
-
/** Master switch. Off ⇒ this camera gathers no evidence and suppresses
|
|
71961
|
-
* nothing (every parked object keeps spawning normal tracks). */
|
|
71962
|
-
enabled: require_dist.boolean().default(true),
|
|
71963
|
-
/** IoU at/above which a detection is the same parked object, unmoved →
|
|
71964
|
-
* suppress its spawn. `SUPPRESS_IOU`. */
|
|
71965
|
-
suppressIou: require_dist.number().min(.3).max(.9).default(SUPPRESS_IOU),
|
|
71966
|
-
/** Minimum IoU for a detection to be ASSOCIATED with an entry (confirm or
|
|
71967
|
-
* wake it) — the overlap gate that stops a different vehicle from waking a
|
|
71968
|
-
* parked entry (the 617 flood fix). `WAKE_ASSOC_IOU`. */
|
|
71969
|
-
wakeAssocIou: require_dist.number().min(.02).max(.5).default(WAKE_ASSOC_IOU),
|
|
71970
|
-
/** Recent-window stillness after which a track stops holding its motion
|
|
71971
|
-
* session open (also the minimum track age). Creates nothing — birth is
|
|
71972
|
-
* decided by repeated evidence. `SESSION_STILLNESS_WINDOW_MS`. */
|
|
71973
|
-
sessionStillnessWindowMs: require_dist.number().int().min(5e3).max(12e4).default(SESSION_STILLNESS_WINDOW_MS),
|
|
71974
|
-
/** Observed-time TTL: retire an entry unconfirmed for this long (measured on
|
|
71975
|
-
* frames-flowing time, not wall-clock). `ENTRY_TTL_MS`. */
|
|
71976
|
-
entryTtlMs: require_dist.number().int().min(6e4).max(18e5).default(ENTRY_TTL_MS)
|
|
71977
|
-
});
|
|
71978
|
-
var STATIONARY_DEFAULTS = StationarySettingsSchema.parse({});
|
|
71979
|
-
/**
|
|
71980
|
-
* Resolve a per-device store blob into typed stationary settings. Unknown/invalid
|
|
71981
|
-
* fields fall back to the default for that field (never throws on a bad blob).
|
|
71982
|
-
*/
|
|
71983
|
-
function resolveStationarySettings(raw) {
|
|
71984
|
-
const pick = (key) => {
|
|
71985
|
-
const parsed = StationarySettingsSchema.shape[key].safeParse(raw[key]);
|
|
71986
|
-
return parsed.success ? parsed.data : STATIONARY_DEFAULTS[key];
|
|
71987
|
-
};
|
|
71988
|
-
return {
|
|
71989
|
-
enabled: pick("enabled"),
|
|
71990
|
-
suppressIou: pick("suppressIou"),
|
|
71991
|
-
wakeAssocIou: pick("wakeAssocIou"),
|
|
71992
|
-
sessionStillnessWindowMs: pick("sessionStillnessWindowMs"),
|
|
71993
|
-
entryTtlMs: pick("entryTtlMs")
|
|
71994
|
-
};
|
|
71995
|
-
}
|
|
71996
|
-
//#endregion
|
|
71997
72413
|
//#region src/pipeline-analytics/settings-schema-sections.ts
|
|
71998
72414
|
/** The detection-knob sections, in their original in-form order. */
|
|
71999
72415
|
function buildDetectionSettingsSections() {
|
|
@@ -72539,10 +72955,62 @@ function buildDetectionSettingsSections() {
|
|
|
72539
72955
|
displayScale: 1e3
|
|
72540
72956
|
}
|
|
72541
72957
|
]
|
|
72542
|
-
}
|
|
72958
|
+
},
|
|
72959
|
+
buildStillnessScaleSection()
|
|
72543
72960
|
];
|
|
72544
72961
|
}
|
|
72545
72962
|
/**
|
|
72963
|
+
* The per-camera STILLNESS SCALE — one measured number plus its provenance.
|
|
72964
|
+
*
|
|
72965
|
+
* Its own section rather than a field on `detection-sensitivity` because the
|
|
72966
|
+
* two answer different questions: `stationaryThresholdSec` is HOW LONG a
|
|
72967
|
+
* settled subject must stay settled, which is an operator preference; this is
|
|
72968
|
+
* HOW SLOW counts as settled, which is a fact about the camera and is
|
|
72969
|
+
* measured. Mixing them is how a measurement ends up being guessed at.
|
|
72970
|
+
*/
|
|
72971
|
+
function buildStillnessScaleSection() {
|
|
72972
|
+
return {
|
|
72973
|
+
id: "stillness-scale",
|
|
72974
|
+
title: "Stillness scale (per camera)",
|
|
72975
|
+
description: "How slowly a subject has to move on THIS camera before it counts as settled. A camera looking down a 30 m driveway and one watching a doorway two metres away disagree about this by more than an order of magnitude, so one fleet-wide number cannot be right for both. Leave it at 0 and the camera behaves exactly as it always has (a flat 2 px-per-update bar). Run the `stillness.calibrate` action to measure it from the camera’s own recorded tracks — the run reports what it measured and REFUSES, with a reason, when the evidence is too thin.",
|
|
72976
|
+
columns: 1,
|
|
72977
|
+
fields: [
|
|
72978
|
+
{
|
|
72979
|
+
type: "number",
|
|
72980
|
+
key: "stillnessSpeedFracPerSec",
|
|
72981
|
+
label: "Stillness speed bar",
|
|
72982
|
+
description: "Centroid speed, as a fraction of the subject’s OWN box diagonal, per second, at or below which the subject is not travelling. Subject-relative, so perspective is already divided out; per second, so an irregular on-motion cadence cannot masquerade as speed. 0 = not calibrated (the shipped absolute bar decides, exactly as before). Live reference: 0.0136 on a close, mostly-parked view; 0.3081 on a far view whose subjects only transit.",
|
|
72983
|
+
min: 0,
|
|
72984
|
+
max: 1,
|
|
72985
|
+
step: .001,
|
|
72986
|
+
default: STILLNESS_DEFAULTS.stillnessSpeedFracPerSec
|
|
72987
|
+
},
|
|
72988
|
+
{
|
|
72989
|
+
type: "text",
|
|
72990
|
+
key: "stillnessCalibrationNote",
|
|
72991
|
+
label: "Last calibration",
|
|
72992
|
+
description: "What the last run measured, from how many samples and tracks, and what it chose — or which named reason it refused for. Display only.",
|
|
72993
|
+
default: STILLNESS_DEFAULTS.stillnessCalibrationNote,
|
|
72994
|
+
readonlyField: true
|
|
72995
|
+
},
|
|
72996
|
+
{
|
|
72997
|
+
type: "addon-action-button",
|
|
72998
|
+
key: "__stillness-calibrate",
|
|
72999
|
+
addonId: "pipeline-analytics",
|
|
73000
|
+
action: "stillness.calibrate",
|
|
73001
|
+
label: "Calibrate from this camera",
|
|
73002
|
+
description: "Measure this camera’s own recent tracks and WRITE the bar above from them. It refuses — with a reason, and without writing — when the evidence is too thin: no tracks, too few tracks, too few samples, or no movement to scale against. The reading it took is left in “Last calibration” either way, so a refusal is as legible as a result.",
|
|
73003
|
+
buttonLabel: "Calibrate now",
|
|
73004
|
+
variant: "primary",
|
|
73005
|
+
includeDeviceId: true,
|
|
73006
|
+
paramsStatic: { apply: true },
|
|
73007
|
+
confirmMessage: "Measure this camera’s recent tracks and overwrite its stillness bar with what they show?",
|
|
73008
|
+
successMessage: "{note}"
|
|
73009
|
+
}
|
|
73010
|
+
]
|
|
73011
|
+
};
|
|
73012
|
+
}
|
|
73013
|
+
/**
|
|
72546
73014
|
* Re-home the global analytics sections into the per-device `Analytics`
|
|
72547
73015
|
* top-tab. Every section defaults to the `Analytics` tab (so the
|
|
72548
73016
|
* device-manager aggregator groups them) AND is marked
|
|
@@ -72568,6 +73036,7 @@ function toAnalyticsDeviceSections(sections) {
|
|
|
72568
73036
|
* - `tracking` — the tracker-tuning form incl. the FP knobs (dedup, person↔
|
|
72569
73037
|
* animal dedup, class voting, confirm-bypass, per-class min score).
|
|
72570
73038
|
* - `stationary-objects` — stationary promotion + occupancy tuning.
|
|
73039
|
+
* - `stillness-scale` — the per-camera measured "how slow is settled" bar.
|
|
72571
73040
|
* `DeviceDetail` folds `tab: 'detection-pipeline'` top-tab sections into the
|
|
72572
73041
|
* structural Detection pipeline tab, so re-tagging is all that's needed —
|
|
72573
73042
|
* there is no admin-ui change and no duplicate render (a section has one tab).
|
|
@@ -72576,7 +73045,8 @@ var DETECTION_PIPELINE_SECTION_IDS = new Set([
|
|
|
72576
73045
|
"detection-sensitivity",
|
|
72577
73046
|
"confirmation-gate",
|
|
72578
73047
|
"tracking",
|
|
72579
|
-
"stationary-objects"
|
|
73048
|
+
"stationary-objects",
|
|
73049
|
+
"stillness-scale"
|
|
72580
73050
|
]);
|
|
72581
73051
|
/**
|
|
72582
73052
|
* Re-home the detection-knob sections from the `Analytics` top-tab onto the
|
|
@@ -76308,7 +76778,8 @@ var customActions = {
|
|
|
76308
76778
|
...locationDiscardActions,
|
|
76309
76779
|
...viewerSettingsActions,
|
|
76310
76780
|
...photoEnrollActions,
|
|
76311
|
-
...faceRescoreActions
|
|
76781
|
+
...faceRescoreActions,
|
|
76782
|
+
...stillnessCalibrationActions
|
|
76312
76783
|
};
|
|
76313
76784
|
/**
|
|
76314
76785
|
* Width cap on the ONE full frame a debug crop is cut from.
|
|
@@ -77148,6 +77619,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77148
77619
|
* the frame path like the stationary one and a cold miss answers the
|
|
77149
77620
|
* measured default. */
|
|
77150
77621
|
summarySettingsCache = this.settingsCaches.registerDeviceCache("summary", SETTINGS_CACHE_TTL_MS);
|
|
77622
|
+
/** Per-device STILLNESS SCALE cache — the measured "how slow is settled" bar
|
|
77623
|
+
* (`stillness-settings.ts`). Read once per FrameProcessor build, so this is
|
|
77624
|
+
* a cold-path cache; it exists so a calibration applied through
|
|
77625
|
+
* `stillness.calibrate` reaches the tracker on the next processor rebuild
|
|
77626
|
+
* rather than on the next addon restart. */
|
|
77627
|
+
stillnessSettingsCache = this.settingsCaches.registerDeviceCache("stillness", SETTINGS_CACHE_TTL_MS);
|
|
77151
77628
|
/** Turns stationary appear/depart into package-delivered/picked-up events. */
|
|
77152
77629
|
packageDropDetector = null;
|
|
77153
77630
|
/**
|
|
@@ -77852,7 +78329,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77852
78329
|
...locationDiscardActions,
|
|
77853
78330
|
...viewerSettingsActions,
|
|
77854
78331
|
...photoEnrollActions,
|
|
77855
|
-
...faceRescoreActions
|
|
78332
|
+
...faceRescoreActions,
|
|
78333
|
+
...stillnessCalibrationActions
|
|
77856
78334
|
},
|
|
77857
78335
|
actionHandlers: {
|
|
77858
78336
|
...ncHandlers,
|
|
@@ -77860,6 +78338,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77860
78338
|
...this.buildDebugActionHandlers(),
|
|
77861
78339
|
...this.buildPhotoEnrollActionHandlers(),
|
|
77862
78340
|
...this.buildFaceRescoreActionHandlers(),
|
|
78341
|
+
...this.buildStillnessCalibrationActionHandlers(),
|
|
77863
78342
|
...this.viewerSettingsSnapshots ? makeViewerSettingsActionHandlers(this.viewerSettingsSnapshots) : {}
|
|
77864
78343
|
}
|
|
77865
78344
|
} : {}
|
|
@@ -81843,6 +82322,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
81843
82322
|
async resolveDeviceStationarySettings(deviceId) {
|
|
81844
82323
|
return this.stationarySettingsCache.get(deviceId, (id) => this.readDeviceSettings(id, resolveStationarySettings));
|
|
81845
82324
|
}
|
|
82325
|
+
/** Per-device stillness scale. An absent/zero bar resolves to `undefined`
|
|
82326
|
+
* ({@link resolveStillSpeedFracPerSec}) and leaves the state analyzer on
|
|
82327
|
+
* its original absolute threshold — uncalibrated is exactly today. */
|
|
82328
|
+
async resolveDeviceStillnessSettings(deviceId) {
|
|
82329
|
+
return this.stillnessSettingsCache.get(deviceId, (id) => this.readDeviceSettings(id, resolveStillnessSettings));
|
|
82330
|
+
}
|
|
81846
82331
|
/**
|
|
81847
82332
|
* Count SHORT + MOTIONLESS track closes per frame cell.
|
|
81848
82333
|
*
|
|
@@ -84239,6 +84724,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
84239
84724
|
if (!p) {
|
|
84240
84725
|
const { minHits, stationaryThresholdSec, minTrackAge, cooldownSec } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
|
|
84241
84726
|
const trk = await this.resolveDeviceTrackingSettings(deviceId);
|
|
84727
|
+
const stillSpeedFracPerSec = resolveStillSpeedFracPerSec(await this.resolveDeviceStillnessSettings(deviceId));
|
|
84242
84728
|
p = new FrameProcessor(deviceId, {
|
|
84243
84729
|
minHits,
|
|
84244
84730
|
iouThreshold: trk.iouThreshold,
|
|
@@ -84280,7 +84766,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
84280
84766
|
vehicle: trk.minSpawnAreaFracVehicle
|
|
84281
84767
|
},
|
|
84282
84768
|
classGroupAssoc: trk.classGroupAssoc
|
|
84283
|
-
}, {
|
|
84769
|
+
}, {
|
|
84770
|
+
stationaryThresholdSec,
|
|
84771
|
+
...stillSpeedFracPerSec !== void 0 ? { stillSpeedFracPerSec } : {}
|
|
84772
|
+
}, {
|
|
84284
84773
|
minTrackAge,
|
|
84285
84774
|
cooldownSec,
|
|
84286
84775
|
minTrackAgeMs: trk.minTrackAgeMs
|
|
@@ -86002,6 +86491,109 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
86002
86491
|
buildFaceRescoreActionHandlers() {
|
|
86003
86492
|
return { "face.rescoreTrack": (input) => this.rescoreFaceForTrack(input) };
|
|
86004
86493
|
}
|
|
86494
|
+
buildStillnessCalibrationActionHandlers() {
|
|
86495
|
+
return { "stillness.calibrate": (input) => this.calibrateStillnessForDevice(input) };
|
|
86496
|
+
}
|
|
86497
|
+
/**
|
|
86498
|
+
* Measure ONE camera's stillness scale from its own recorded tracks.
|
|
86499
|
+
*
|
|
86500
|
+
* Cost-bounded by construction: one `queryRecent` page of at most
|
|
86501
|
+
* `maxTracks` rows over the requested window, `projection: 'full'` because
|
|
86502
|
+
* the trajectory IS the measurement, and nothing else is read. The arithmetic
|
|
86503
|
+
* is `stillness-scale.ts` and is pure — this method only fetches, reports and
|
|
86504
|
+
* (on `apply`) writes.
|
|
86505
|
+
*
|
|
86506
|
+
* A REFUSAL never writes the bar. It does write the note, because "why is
|
|
86507
|
+
* this camera still on the default" has to be answerable from the form
|
|
86508
|
+
* rather than from a toast nobody kept.
|
|
86509
|
+
*/
|
|
86510
|
+
async calibrateStillnessForDevice(input) {
|
|
86511
|
+
const trackStore = this.trackStore;
|
|
86512
|
+
if (!trackStore) throw new Error("stillness.calibrate: the track store is not ready");
|
|
86513
|
+
const until = Date.now();
|
|
86514
|
+
const since = until - Math.round(input.sinceHours * 36e5);
|
|
86515
|
+
const limits = {
|
|
86516
|
+
...DEFAULT_STILLNESS_CALIBRATION_LIMITS,
|
|
86517
|
+
maxTracks: input.maxTracks
|
|
86518
|
+
};
|
|
86519
|
+
const previous = resolveStillSpeedFracPerSec(await this.resolveDeviceStillnessSettings(input.deviceId));
|
|
86520
|
+
const samples = collectStillnessSamples((await trackStore.queryRecent({
|
|
86521
|
+
deviceIds: [input.deviceId],
|
|
86522
|
+
since,
|
|
86523
|
+
until,
|
|
86524
|
+
limit: limits.maxTracks,
|
|
86525
|
+
projection: "full"
|
|
86526
|
+
})).tracks.map((t) => ({
|
|
86527
|
+
trackId: t.trackId,
|
|
86528
|
+
className: t.className,
|
|
86529
|
+
positions: t.positions.map((pos) => ({
|
|
86530
|
+
x: pos.x,
|
|
86531
|
+
y: pos.y,
|
|
86532
|
+
timestamp: pos.timestamp,
|
|
86533
|
+
bbox: {
|
|
86534
|
+
w: pos.bbox.w,
|
|
86535
|
+
h: pos.bbox.h
|
|
86536
|
+
}
|
|
86537
|
+
}))
|
|
86538
|
+
})), limits);
|
|
86539
|
+
const outcome = calibrateStillnessSpeed(samples, limits);
|
|
86540
|
+
const note = describeStillnessCalibration(outcome);
|
|
86541
|
+
const finite = (v) => Number.isFinite(v) ? v : null;
|
|
86542
|
+
const measurement = {
|
|
86543
|
+
trackCount: outcome.measurement.trackCount,
|
|
86544
|
+
tracksRead: samples.tracksRead,
|
|
86545
|
+
sampleCount: outcome.measurement.sampleCount,
|
|
86546
|
+
p25: finite(outcome.measurement.p25),
|
|
86547
|
+
p50: finite(outcome.measurement.p50),
|
|
86548
|
+
p90: finite(outcome.measurement.p90),
|
|
86549
|
+
medianIntervalSec: finite(outcome.measurement.medianIntervalSec),
|
|
86550
|
+
medianSubjectDiagonalPx: finite(outcome.measurement.medianSubjectDiagonalPx)
|
|
86551
|
+
};
|
|
86552
|
+
let applied = false;
|
|
86553
|
+
if (input.apply) {
|
|
86554
|
+
const patch = {
|
|
86555
|
+
stillnessCalibrationNote: note,
|
|
86556
|
+
stillnessCalibratedAt: until
|
|
86557
|
+
};
|
|
86558
|
+
if (outcome.ok) patch["stillnessSpeedFracPerSec"] = outcome.bar;
|
|
86559
|
+
await this.applyDeviceSettingsPatch({
|
|
86560
|
+
deviceId: input.deviceId,
|
|
86561
|
+
patch
|
|
86562
|
+
});
|
|
86563
|
+
applied = outcome.ok;
|
|
86564
|
+
}
|
|
86565
|
+
this.ctx.logger.info("stillness calibration", {
|
|
86566
|
+
tags: { deviceId: input.deviceId },
|
|
86567
|
+
meta: {
|
|
86568
|
+
accepted: outcome.ok,
|
|
86569
|
+
...outcome.ok ? {} : { reason: outcome.reason },
|
|
86570
|
+
...outcome.ok ? {
|
|
86571
|
+
bar: outcome.bar,
|
|
86572
|
+
clampedBy: outcome.clampedBy
|
|
86573
|
+
} : {},
|
|
86574
|
+
previousBar: previous ?? null,
|
|
86575
|
+
applied,
|
|
86576
|
+
windowHours: input.sinceHours,
|
|
86577
|
+
...measurement
|
|
86578
|
+
}
|
|
86579
|
+
});
|
|
86580
|
+
return {
|
|
86581
|
+
deviceId: input.deviceId,
|
|
86582
|
+
since,
|
|
86583
|
+
until,
|
|
86584
|
+
accepted: outcome.ok,
|
|
86585
|
+
...outcome.ok ? {} : { reason: outcome.reason },
|
|
86586
|
+
...outcome.ok ? {
|
|
86587
|
+
bar: outcome.bar,
|
|
86588
|
+
rawBar: outcome.rawBar,
|
|
86589
|
+
clampedBy: outcome.clampedBy
|
|
86590
|
+
} : {},
|
|
86591
|
+
measurement,
|
|
86592
|
+
previousBar: previous ?? null,
|
|
86593
|
+
applied,
|
|
86594
|
+
note
|
|
86595
|
+
};
|
|
86596
|
+
}
|
|
86005
86597
|
buildPhotoEnrollActionHandlers() {
|
|
86006
86598
|
return {
|
|
86007
86599
|
"photo.analyze": async (input) => {
|
|
@@ -87084,6 +87676,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
87084
87676
|
this.trackingSettingsCache.delete(input.deviceId);
|
|
87085
87677
|
this.confirmationGateSettingsCache.delete(input.deviceId);
|
|
87086
87678
|
this.mediaSettingsCache.delete(input.deviceId);
|
|
87679
|
+
this.stillnessSettingsCache.delete(input.deviceId);
|
|
87087
87680
|
this.forgetDeviceProcessors(input.deviceId);
|
|
87088
87681
|
return { success: true };
|
|
87089
87682
|
}
|