@camstack/addon-post-analysis 1.2.601 → 1.2.602

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (15) hide show
  1. package/dist/{clip-model-registry-DAZ9ie-f.js → clip-model-registry-D0-VNIW5.js} +381 -8
  2. package/dist/{clip-model-registry-BuM8eXlN.mjs → clip-model-registry-lQ7QWjLf.mjs} +376 -9
  3. package/dist/embedding-encoder/index.js +1 -1
  4. package/dist/embedding-encoder/index.mjs +1 -1
  5. package/dist/pipeline-analytics/_stub.js +2 -2
  6. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DAs_96VP.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BPA0T5-F.mjs} +2 -2
  7. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-1AlitJg2.mjs +26 -0
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Cfj8Mv5X.mjs +26 -0
  9. package/dist/pipeline-analytics/{hostInit-C3wjY7W8.mjs → hostInit-DIjMiIYW.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +281 -503
  11. package/dist/pipeline-analytics/index.mjs +281 -503
  12. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  13. package/package.json +1 -1
  14. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CElkWYPg.mjs +0 -26
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-SRvLcL_J.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_clip_model_registry = require("../clip-model-registry-DAZ9ie-f.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-D0-VNIW5.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -6938,17 +6938,6 @@ function ncAudioSampleFromFrame(frame, timestampMs, floor) {
6938
6938
  ...frame.classifyOutcome !== void 0 ? { classifyOutcome: frame.classifyOutcome } : {}
6939
6939
  };
6940
6940
  }
6941
- /** The level-path audio row for one frame — the one row kind with no label to explain it. */
6942
- function levelAudioEventDraft(deviceId, timestamp, frame) {
6943
- return {
6944
- deviceId,
6945
- timestamp,
6946
- kind: "audio",
6947
- rms: frame.level?.rms ?? 0,
6948
- dbfs: frame.level?.dbfs ?? 0,
6949
- ...frame.classifyOutcome !== void 0 ? { classifyOutcome: frame.classifyOutcome } : {}
6950
- };
6951
- }
6952
6941
  //#endregion
6953
6942
  //#region src/pipeline-analytics/scene/condition-resolver.ts
6954
6943
  var DAY = "day";
@@ -10930,7 +10919,7 @@ var NC_LIVENESS_SPEC = {
10930
10919
  writeMode: "write-behind",
10931
10920
  keyOf: (row) => row.subject,
10932
10921
  toValue: rowToValue$1,
10933
- fromRecord: recordToRow$2,
10922
+ fromRecord: recordToRow$1,
10934
10923
  equalFact: (held, incoming) => held.state === incoming.state,
10935
10924
  deviceIdOf: (row) => row.deviceId
10936
10925
  };
@@ -11117,7 +11106,7 @@ function rowToValue$1(row) {
11117
11106
  * (silent) rather than hydrating a state nothing can ever equal, which would
11118
11107
  * make every future event a phantom flip.
11119
11108
  */
11120
- function recordToRow$2(subject, data) {
11109
+ function recordToRow$1(subject, data) {
11121
11110
  if (subject.length === 0) return null;
11122
11111
  const state = data["state"];
11123
11112
  if (state !== "online" && state !== "offline") return null;
@@ -12598,7 +12587,7 @@ var NC_OCCUPANCY_SPEC = {
12598
12587
  writeMode: "write-through",
12599
12588
  keyOf: (row) => row.key,
12600
12589
  toValue: rowToValue,
12601
- fromRecord: recordToRow$1,
12590
+ fromRecord: recordToRow,
12602
12591
  deviceIdOf: (row) => row.deviceId
12603
12592
  };
12604
12593
  var OccupancyStore = class {
@@ -12675,7 +12664,7 @@ function rowToValue(row) {
12675
12664
  * {@link parseOccupancyKey} rejects — so the caller skips it (cold re-observe)
12676
12665
  * rather than hydrating a mis-parsed scope.
12677
12666
  */
12678
- function recordToRow$1(key, data) {
12667
+ function recordToRow(key, data) {
12679
12668
  const scope = parseOccupancyKey(key);
12680
12669
  if (scope === null) return null;
12681
12670
  const deviceId = Number(data["deviceId"]);
@@ -50543,7 +50532,7 @@ function previewBlurSigma(frameWidth) {
50543
50532
  }
50544
50533
  /** Break a word too long for one line into line-width pieces. Without this a
50545
50534
  * single 200-character token wraps to nothing and overflows both edges. */
50546
- function chunk$2(word, size) {
50535
+ function chunk$1(word, size) {
50547
50536
  const pieces = [];
50548
50537
  for (let i = 0; i < word.length; i += size) pieces.push(word.slice(i, i + size));
50549
50538
  return pieces;
@@ -50551,7 +50540,7 @@ function chunk$2(word, size) {
50551
50540
  /** Greedy word wrap at a character budget — the only measurement available,
50552
50541
  * since librsvg lays the text out long after this string is written. */
50553
50542
  function wrapLines(text, maxChars) {
50554
- return text.split(" ").filter((w) => w.length > 0).reduce((lines, word) => (word.length > maxChars ? chunk$2(word, maxChars) : [word]).reduce((acc, piece) => {
50543
+ return text.split(" ").filter((w) => w.length > 0).reduce((lines, word) => (word.length > maxChars ? chunk$1(word, maxChars) : [word]).reduce((acc, piece) => {
50555
50544
  const last = acc.at(-1);
50556
50545
  if (last === void 0) return [piece];
50557
50546
  const merged = `${last} ${piece}`;
@@ -60482,7 +60471,7 @@ async function readRescoreFingerprint(ports) {
60482
60471
  clusterModelId
60483
60472
  };
60484
60473
  }
60485
- function chunk$1(items, size) {
60474
+ function chunk(items, size) {
60486
60475
  const out = [];
60487
60476
  for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
60488
60477
  return out;
@@ -60604,7 +60593,7 @@ var FaceRescoreJob = class {
60604
60593
  fingerprint: fingerprint.combined
60605
60594
  });
60606
60595
  const paramsByDevice = /* @__PURE__ */ new Map();
60607
- for (const batchIds of chunk$1(ids, RESCORE_BATCH_SIZE)) {
60596
+ for (const batchIds of chunk(ids, RESCORE_BATCH_SIZE)) {
60608
60597
  if (this.stopped) throw new Error("stopped before completion");
60609
60598
  const storeStart = this.ports.now();
60610
60599
  const batch = await this.ports.loadRescoreBatch(batchIds);
@@ -98360,431 +98349,6 @@ var SweepCursorStore = class SweepCursorStore {
98360
98349
  }
98361
98350
  };
98362
98351
  //#endregion
98363
- //#region src/durable/lts-aggregator.ts
98364
- /** Wall-clock bucket width. Five minutes, matching HA's statistics tier. */
98365
- var LTS_BUCKET_MS = 5 * 6e4;
98366
- /**
98367
- * Row cap per (subject, series, scope) — one year of five-minute buckets, the
98368
- * same number HA's own docs use. At ~60 bytes a row that is ~6 MB per series
98369
- * per camera-year, against a `media` table already measured in hundreds of MB.
98370
- */
98371
- var LTS_MAX_ROWS_PER_SERIES = 105120;
98372
- /** Buckets written per transaction — bounds one statement, not the flush. */
98373
- var FLUSH_BATCH = 200;
98374
- /** How often the row cap is enforced. Rarely: it is a bound, not a deadline. */
98375
- var CAP_SWEEP_INTERVAL_MS = 6 * 36e5;
98376
- /**
98377
- * The row's identity IS its four key columns (D871): a composite
98378
- * `primaryKey` + `WITHOUT ROWID`, so the table is its own index and there is no
98379
- * TEXT `id` repeating what the row already says. The old layout kept
98380
- * `<subject>|<series>|<scope>|<bucketStart>` as a TEXT primary key beside the
98381
- * four columns — 85 MB of rows plus a 44 MB autoindex for 1.07 M rows. A
98382
- * composite-key collection is written with `upsertMany` (a bucket flushed twice
98383
- * replaces itself) and read with `query` / `count` / `deleteWhere`; the calls
98384
- * that address one row by id are refused by the engine.
98385
- */
98386
- var LTS_KEY_COLUMNS = [
98387
- "subject",
98388
- "series",
98389
- "scope",
98390
- "bucketStart"
98391
- ];
98392
- var LTS_COLUMNS = [
98393
- (
98394
- /** The camera id, or the node id. One column, because every query is
98395
- * "this thing over time" and the thing is one or the other. */
98396
- {
98397
- name: "subject",
98398
- type: "TEXT"
98399
- }),
98400
- (
98401
- /** Numeric mirror of `subject` for camera rows, so a per-camera question is
98402
- * answered by an integer index — every log line and every query about a
98403
- * device in this repo is keyed by the numeric id. `NULL` for node rows. */
98404
- {
98405
- name: "deviceId",
98406
- type: "INTEGER"
98407
- }),
98408
- {
98409
- name: "series",
98410
- type: "TEXT"
98411
- },
98412
- (
98413
- /** Sub-scope within a series: a zoneId for occupancy, `''` otherwise. */
98414
- {
98415
- name: "scope",
98416
- type: "TEXT"
98417
- }),
98418
- (
98419
- /** Bucket start, wall-clock aligned to {@link LTS_BUCKET_MS}. */
98420
- {
98421
- name: "bucketStart",
98422
- type: "INTEGER"
98423
- }),
98424
- {
98425
- name: "samples",
98426
- type: "INTEGER",
98427
- notNull: true
98428
- },
98429
- {
98430
- name: "sum",
98431
- type: "REAL",
98432
- notNull: true
98433
- },
98434
- {
98435
- name: "min",
98436
- type: "REAL",
98437
- notNull: true
98438
- },
98439
- {
98440
- name: "max",
98441
- type: "REAL",
98442
- notNull: true
98443
- }
98444
- ];
98445
- /**
98446
- * The primary key serves every per-subject read (`subject, series, scope` then a
98447
- * `bucketStart` range, already in bucket order); this index serves the read that
98448
- * names a series without a subject. The old `deviceId` index went with the TEXT
98449
- * id: nothing queries a series by the numeric mirror.
98450
- */
98451
- var LTS_INDEXES = [{
98452
- name: "idx_lts_series_bucket",
98453
- columns: ["series", "bucketStart"]
98454
- }];
98455
- function ltsBucketStart(at, bucketMs = LTS_BUCKET_MS) {
98456
- return Math.floor(at / bucketMs) * bucketMs;
98457
- }
98458
- var LtsAggregator = class {
98459
- open = /* @__PURE__ */ new Map();
98460
- /** Every (subject, series, scope) that has produced a row in this process —
98461
- * the groups the cap sweep has any reason to look at. */
98462
- groups = /* @__PURE__ */ new Set();
98463
- lastCapSweepAt = 0;
98464
- collection;
98465
- store;
98466
- logger;
98467
- nowFn;
98468
- bucketMs;
98469
- maxRows;
98470
- constructor(deps) {
98471
- this.collection = deps.collection;
98472
- this.store = deps.store;
98473
- this.logger = deps.logger;
98474
- this.nowFn = deps.now ?? (() => Date.now());
98475
- this.bucketMs = deps.bucketMs ?? LTS_BUCKET_MS;
98476
- this.maxRows = deps.maxRowsPerSeries ?? LTS_MAX_ROWS_PER_SERIES;
98477
- }
98478
- static declare(store, collection) {
98479
- return store.declareCollection.mutate({
98480
- collection,
98481
- columns: [...LTS_COLUMNS],
98482
- primaryKey: [...LTS_KEY_COLUMNS],
98483
- withoutRowid: true,
98484
- indexes: [...LTS_INDEXES]
98485
- });
98486
- }
98487
- /**
98488
- * Record one observation. Synchronous, allocation-free after the first
98489
- * sample of a bucket, and it cannot throw — it sits on paths that are
98490
- * already producing the value for another reason and must not learn a new
98491
- * failure mode.
98492
- *
98493
- * A non-finite value is DROPPED rather than folded in: one `NaN` would make
98494
- * `sum`, `min` and `max` all `NaN` for the whole bucket, turning a skewed
98495
- * row into a meaningless one.
98496
- */
98497
- note(input) {
98498
- if (!Number.isFinite(input.value)) return;
98499
- const at = input.at ?? this.nowFn();
98500
- const scope = input.scope ?? "";
98501
- const bucketStart = ltsBucketStart(at, this.bucketMs);
98502
- const key = rowId(input.subject, input.series, scope, bucketStart);
98503
- const held = this.open.get(key);
98504
- if (held === void 0) {
98505
- this.open.set(key, {
98506
- subject: input.subject,
98507
- ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
98508
- series: input.series,
98509
- scope,
98510
- bucketStart,
98511
- samples: 1,
98512
- sum: input.value,
98513
- min: input.value,
98514
- max: input.value
98515
- });
98516
- return;
98517
- }
98518
- held.samples += 1;
98519
- held.sum += input.value;
98520
- if (input.value < held.min) held.min = input.value;
98521
- if (input.value > held.max) held.max = input.value;
98522
- }
98523
- /** Buckets currently accumulating — diagnostics, and what a flush would write. */
98524
- openBuckets() {
98525
- return [...this.open.values()].map(toRow);
98526
- }
98527
- /**
98528
- * Write every bucket that has CLOSED (its window ended at or before `now`)
98529
- * and drop it from RAM. Returns the number of rows written.
98530
- *
98531
- * The current bucket is deliberately left alone: writing it early would mean
98532
- * rewriting it on the next tick, which turns one row into up to sixty and
98533
- * offers every one of them to the checkpoint lottery (D96).
98534
- */
98535
- async flushDue(now = this.nowFn()) {
98536
- const currentBucket = ltsBucketStart(now, this.bucketMs);
98537
- const due = [...this.open.values()].filter((b) => b.bucketStart < currentBucket);
98538
- if (due.length === 0) {
98539
- await this.maybeSweepCap(now);
98540
- return 0;
98541
- }
98542
- let written = 0;
98543
- for (const batch of chunk(due, FLUSH_BATCH)) {
98544
- written += await this.writeBatch(batch);
98545
- for (const bucket of batch) this.open.delete(rowId(bucket.subject, bucket.series, bucket.scope, bucket.bucketStart));
98546
- }
98547
- await this.maybeSweepCap(now);
98548
- return written;
98549
- }
98550
- /**
98551
- * Upsert one batch in a single transaction. If the batch is refused as a
98552
- * whole (all-or-nothing), each bucket is retried alone so one bad row costs
98553
- * one interval, not forty. Returns the number of rows written.
98554
- */
98555
- async writeBatch(batch) {
98556
- try {
98557
- await this.upsert(batch);
98558
- for (const bucket of batch) this.groups.add(groupKey(bucket.subject, bucket.series, bucket.scope));
98559
- return batch.length;
98560
- } catch (err) {
98561
- if (batch.length > 1) {
98562
- let written = 0;
98563
- for (const bucket of batch) written += await this.writeBatch([bucket]);
98564
- return written;
98565
- }
98566
- const bucket = batch[0];
98567
- this.logger.warn("lts bucket write failed — this interval will be missing", {
98568
- ...bucket?.deviceId !== void 0 ? { tags: { deviceId: bucket.deviceId } } : {},
98569
- meta: {
98570
- collection: this.collection,
98571
- series: bucket?.series,
98572
- subject: bucket?.subject,
98573
- bucketStart: bucket?.bucketStart,
98574
- error: String(err)
98575
- }
98576
- });
98577
- return 0;
98578
- }
98579
- }
98580
- async upsert(batch) {
98581
- await this.store.upsertMany.mutate({
98582
- collection: this.collection,
98583
- conflict: [...LTS_KEY_COLUMNS],
98584
- records: batch.map((bucket) => ({ data: {
98585
- subject: bucket.subject,
98586
- deviceId: bucket.deviceId ?? null,
98587
- series: bucket.series,
98588
- scope: bucket.scope,
98589
- bucketStart: bucket.bucketStart,
98590
- samples: bucket.samples,
98591
- sum: bucket.sum,
98592
- min: bucket.min,
98593
- max: bucket.max
98594
- } }))
98595
- });
98596
- }
98597
- /** Read closed buckets back. The read surface every chart will use. */
98598
- async read(query = {}) {
98599
- const where = {};
98600
- if (query.series !== void 0) where["series"] = query.series;
98601
- if (query.subject !== void 0) where["subject"] = query.subject;
98602
- if (query.scope !== void 0) where["scope"] = query.scope;
98603
- const records = await this.store.query.query({
98604
- collection: this.collection,
98605
- filter: {
98606
- ...Object.keys(where).length > 0 ? { where } : {},
98607
- ...query.from !== void 0 || query.to !== void 0 ? { whereBetween: { bucketStart: [query.from ?? 0, query.to ?? Number.MAX_SAFE_INTEGER] } } : {},
98608
- orderBy: {
98609
- field: "bucketStart",
98610
- direction: "asc"
98611
- },
98612
- limit: query.limit ?? 5e3
98613
- }
98614
- });
98615
- const rows = [];
98616
- for (const record of records) {
98617
- const row = recordToRow(record.data);
98618
- if (row !== null) rows.push(row);
98619
- }
98620
- return rows;
98621
- }
98622
- /**
98623
- * Enforce the per-(subject, series, scope) row cap, at most once every six
98624
- * hours. Only groups this process has written to are examined: a group with
98625
- * no new rows cannot have crossed a cap it was under.
98626
- */
98627
- async maybeSweepCap(now) {
98628
- if (now - this.lastCapSweepAt < CAP_SWEEP_INTERVAL_MS) return;
98629
- this.lastCapSweepAt = now;
98630
- for (const group of this.groups) {
98631
- const [subject, series, scope] = group.split("\0");
98632
- if (subject === void 0 || series === void 0 || scope === void 0) continue;
98633
- const where = {
98634
- subject,
98635
- series,
98636
- scope
98637
- };
98638
- try {
98639
- const excess = await this.store.count.query({
98640
- collection: this.collection,
98641
- filter: { where }
98642
- }) - this.maxRows;
98643
- if (excess <= 0) continue;
98644
- const cutoff = (await this.store.query.query({
98645
- collection: this.collection,
98646
- filter: {
98647
- where,
98648
- orderBy: {
98649
- field: "bucketStart",
98650
- direction: "asc"
98651
- },
98652
- limit: excess
98653
- }
98654
- })).at(-1)?.data["bucketStart"];
98655
- if (typeof cutoff !== "number") continue;
98656
- const { deleted } = await this.store.deleteWhere.mutate({
98657
- collection: this.collection,
98658
- filter: {
98659
- where,
98660
- whereBetween: { bucketStart: [0, cutoff] }
98661
- }
98662
- });
98663
- this.logger.info("lts row cap enforced", { meta: {
98664
- collection: this.collection,
98665
- series,
98666
- subject,
98667
- deleted,
98668
- cap: this.maxRows
98669
- } });
98670
- } catch (err) {
98671
- this.logger.warn("lts row cap sweep failed — the series keeps growing this cycle", { meta: {
98672
- collection: this.collection,
98673
- series,
98674
- subject,
98675
- error: String(err)
98676
- } });
98677
- }
98678
- }
98679
- }
98680
- };
98681
- function chunk(items, size) {
98682
- const out = [];
98683
- for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
98684
- return out;
98685
- }
98686
- function groupKey(subject, series, scope) {
98687
- return `${subject}${series}${scope}`;
98688
- }
98689
- function rowId(subject, series, scope, bucketStart) {
98690
- return `${subject}|${series}|${scope}|${bucketStart}`;
98691
- }
98692
- function toRow(bucket) {
98693
- return {
98694
- subject: bucket.subject,
98695
- ...bucket.deviceId !== void 0 ? { deviceId: bucket.deviceId } : {},
98696
- series: bucket.series,
98697
- scope: bucket.scope,
98698
- bucketStart: bucket.bucketStart,
98699
- samples: bucket.samples,
98700
- sum: bucket.sum,
98701
- min: bucket.min,
98702
- max: bucket.max
98703
- };
98704
- }
98705
- /** Structural validation on read. A malformed row is skipped, never charted. */
98706
- function recordToRow(data) {
98707
- const subject = data["subject"];
98708
- const series = data["series"];
98709
- const scope = data["scope"];
98710
- if (typeof subject !== "string" || typeof series !== "string") return null;
98711
- const bucketStart = Number(data["bucketStart"]);
98712
- const samples = Number(data["samples"]);
98713
- const sum = Number(data["sum"]);
98714
- const min = Number(data["min"]);
98715
- const max = Number(data["max"]);
98716
- if (![
98717
- bucketStart,
98718
- samples,
98719
- sum,
98720
- min,
98721
- max
98722
- ].every((n) => Number.isFinite(n))) return null;
98723
- const rawDeviceId = data["deviceId"];
98724
- const deviceId = typeof rawDeviceId === "number" && Number.isFinite(rawDeviceId) ? rawDeviceId : void 0;
98725
- return {
98726
- subject,
98727
- ...deviceId !== void 0 ? { deviceId } : {},
98728
- series,
98729
- scope: typeof scope === "string" ? scope : "",
98730
- bucketStart,
98731
- samples,
98732
- sum,
98733
- min,
98734
- max
98735
- };
98736
- }
98737
- //#endregion
98738
- //#region src/pipeline-analytics/store/lts-store.ts
98739
- /**
98740
- * @durable class=ledger owner=pipeline-analytics
98741
- * write="one row per (camera, series, zone, 5-min bucket), written ONCE when the bucket closes; a camera with no sample writes nothing"
98742
- * retention="row CAP per (subject, series, scope) — 105,120 rows ≈ one year of 5-minute buckets. Deliberately not an age sweep: an LTS row has no owner to be orphaned from and no clock but its own (D61)."
98743
- */
98744
- var LTS_COLLECTION = "pipeline-analytics:stats-5m-v2";
98745
- var AnalyticsLts = class {
98746
- lts;
98747
- constructor(deps) {
98748
- this.lts = new LtsAggregator({
98749
- collection: LTS_COLLECTION,
98750
- store: deps.store,
98751
- logger: deps.logger,
98752
- ...deps.now !== void 0 ? { now: deps.now } : {}
98753
- });
98754
- }
98755
- static declare(store) {
98756
- return LtsAggregator.declare(store, LTS_COLLECTION);
98757
- }
98758
- /**
98759
- * One camera sample. Synchronous and infallible by contract — every call
98760
- * site is a path that already produced the value for another reason, and
98761
- * none of them may learn a new failure mode because a chart exists.
98762
- */
98763
- note(series, deviceId, value, at, scope) {
98764
- this.lts.note({
98765
- subject: String(deviceId),
98766
- deviceId,
98767
- series,
98768
- ...scope !== void 0 ? { scope } : {},
98769
- value,
98770
- ...at !== void 0 ? { at } : {}
98771
- });
98772
- }
98773
- /** Write every bucket that has closed. Driven by the addon's flush timer. */
98774
- flushDue(now) {
98775
- return this.lts.flushDue(now);
98776
- }
98777
- /** The read surface charts will use. */
98778
- read(query) {
98779
- return this.lts.read(query);
98780
- }
98781
- /** Buckets still accumulating — makes "is it running" answerable at once,
98782
- * instead of after five minutes of silence. */
98783
- openBuckets() {
98784
- return this.lts.openBuckets();
98785
- }
98786
- };
98787
- //#endregion
98788
98352
  //#region src/pipeline-analytics/store/identity-store.ts
98789
98353
  /**
98790
98354
  * IdentityStore — per-person identity registry for face recognition.
@@ -101828,13 +101392,6 @@ var ANALYTICS_COLLECTIONS = [
101828
101392
  why: "a phantom cell is a property of the SCENE — \"this patch of pixels keeps producing 4-second motionless tracks\" — not of any track. The tracks that produced it are dead by construction (that is the entire premise: the stationary registry cannot see them because they never live long enough to be promoted), so there is no owner it could be orphaned from. Its bound is its own 24h close window plus a 512-cell-per-device cap, both enforced by the registry on write — deliberately not an age sweep, which would be a second clock (D61)."
101829
101393
  }
101830
101394
  },
101831
- {
101832
- collection: LTS_COLLECTION,
101833
- classification: {
101834
- kind: "never-orphaned",
101835
- why: "a five-minute statistics row summarises an INTERVAL, not an entity — there is no owner it can be orphaned from. Its bound is a ROW CAP per (subject, series, scope) enforced by the aggregator itself (105,120 rows ≈ one year), deliberately not an age sweep: D61 collapsed ten disagreeing age clocks into one, and an LTS series has no clock but its own."
101836
- }
101837
- },
101838
101395
  {
101839
101396
  collection: RETRAIN_ANNOTATIONS_COLLECTION,
101840
101397
  classification: {
@@ -107138,6 +106695,78 @@ Object.freeze({
107138
106695
  "path": "online",
107139
106696
  "valueType": "boolean"
107140
106697
  }],
106698
+ "dishwasher": [
106699
+ {
106700
+ "path": "state",
106701
+ "valueType": "enum",
106702
+ "enumValues": [
106703
+ "idle",
106704
+ "running",
106705
+ "paused",
106706
+ "finished",
106707
+ "error",
106708
+ "unavailable"
106709
+ ]
106710
+ },
106711
+ {
106712
+ "path": "program",
106713
+ "valueType": "string"
106714
+ },
106715
+ {
106716
+ "path": "remainingMinutes",
106717
+ "valueType": "number"
106718
+ },
106719
+ {
106720
+ "path": "progressPercent",
106721
+ "valueType": "number"
106722
+ },
106723
+ {
106724
+ "path": "doorOpen",
106725
+ "valueType": "boolean"
106726
+ },
106727
+ {
106728
+ "path": "remoteStartAllowed",
106729
+ "valueType": "boolean"
106730
+ },
106731
+ {
106732
+ "path": "powerOn",
106733
+ "valueType": "boolean"
106734
+ },
106735
+ {
106736
+ "path": "saltLevel",
106737
+ "valueType": "enum",
106738
+ "enumValues": [
106739
+ "ok",
106740
+ "low",
106741
+ "empty"
106742
+ ]
106743
+ },
106744
+ {
106745
+ "path": "rinseAidLevel",
106746
+ "valueType": "enum",
106747
+ "enumValues": [
106748
+ "ok",
106749
+ "low",
106750
+ "empty"
106751
+ ]
106752
+ },
106753
+ {
106754
+ "path": "halfLoad",
106755
+ "valueType": "boolean"
106756
+ },
106757
+ {
106758
+ "path": "hygienePlus",
106759
+ "valueType": "boolean"
106760
+ },
106761
+ {
106762
+ "path": "errorCode",
106763
+ "valueType": "number"
106764
+ },
106765
+ {
106766
+ "path": "errorLabel",
106767
+ "valueType": "string"
106768
+ }
106769
+ ],
107141
106770
  "doorbell": [{
107142
106771
  "path": "lastPressedAt",
107143
106772
  "valueType": "number"
@@ -107228,6 +106857,10 @@ Object.freeze({
107228
106857
  "path": "detected",
107229
106858
  "valueType": "boolean"
107230
106859
  }],
106860
+ "heating-valve": [{
106861
+ "path": "position",
106862
+ "valueType": "number"
106863
+ }],
107231
106864
  "humidifier": [
107232
106865
  {
107233
106866
  "path": "on",
@@ -110656,6 +110289,189 @@ var MigrationMarkerStore = class MigrationMarkerStore {
110656
110289
  };
110657
110290
  };
110658
110291
  //#endregion
110292
+ //#region src/pipeline-analytics/store/analytics-history-keys.ts
110293
+ /** Declared addon series keys (D850). */
110294
+ var ANALYTICS_HISTORY_BUCKET_SERIES = {
110295
+ motion: "addon:pipeline-analytics/motion",
110296
+ "audio-dbfs": "addon:pipeline-analytics/audio-dbfs",
110297
+ occupancy: "addon:pipeline-analytics/occupancy"
110298
+ };
110299
+ var ANALYTICS_AUDIO_LEVEL_KEY = "addon:pipeline-analytics/audio-level";
110300
+ /** Map the public cap enum to the device-history key (read path). */
110301
+ function historyKeyForLtsSeries(series) {
110302
+ if (series === "battery") return "cap:battery.percentage";
110303
+ return ANALYTICS_HISTORY_BUCKET_SERIES[series];
110304
+ }
110305
+ //#endregion
110306
+ //#region src/pipeline-analytics/store/analytics-history-producer.ts
110307
+ var BUCKET_SERIES_META = [
110308
+ {
110309
+ name: "motion",
110310
+ displayName: "Motion events (region count)"
110311
+ },
110312
+ {
110313
+ name: "audio-dbfs",
110314
+ displayName: "Audio noise baseline (dBFS)",
110315
+ unit: "dBFS"
110316
+ },
110317
+ {
110318
+ name: "occupancy",
110319
+ displayName: "Occupancy count"
110320
+ }
110321
+ ];
110322
+ var AUDIO_LEVEL_SERIES = {
110323
+ name: "audio-level",
110324
+ displayName: "Audio level (dBFS)",
110325
+ unit: "dBFS"
110326
+ };
110327
+ var AnalyticsHistoryProducer = class {
110328
+ accum = new require_clip_model_registry.BucketAccumulator();
110329
+ deviceHistory;
110330
+ logger;
110331
+ nowFn;
110332
+ declared = false;
110333
+ constructor(deps) {
110334
+ this.deviceHistory = deps.deviceHistory;
110335
+ this.logger = deps.logger;
110336
+ this.nowFn = deps.now ?? (() => Date.now());
110337
+ }
110338
+ async declare() {
110339
+ if (this.declared) return;
110340
+ for (const meta of BUCKET_SERIES_META) await this.deviceHistory.declareSeries({
110341
+ name: meta.name,
110342
+ subjectKind: "device",
110343
+ kind: "gauge",
110344
+ displayName: meta.displayName,
110345
+ ingest: "buckets",
110346
+ ...meta.unit !== void 0 ? { unit: meta.unit } : {}
110347
+ });
110348
+ await this.deviceHistory.declareSeries({
110349
+ name: AUDIO_LEVEL_SERIES.name,
110350
+ subjectKind: "device",
110351
+ kind: "gauge",
110352
+ displayName: AUDIO_LEVEL_SERIES.displayName,
110353
+ unit: AUDIO_LEVEL_SERIES.unit,
110354
+ ingest: "points"
110355
+ });
110356
+ this.declared = true;
110357
+ }
110358
+ note(series, deviceId, value, at, scope) {
110359
+ const key = ANALYTICS_HISTORY_BUCKET_SERIES[series];
110360
+ this.accum.note(key, {
110361
+ kind: "device",
110362
+ deviceId
110363
+ }, scope ?? "", value, at ?? this.nowFn());
110364
+ }
110365
+ recordAudioLevel(deviceId, dbfs, at) {
110366
+ const record = this.deviceHistory.record;
110367
+ if (record === void 0) return;
110368
+ record.call(this.deviceHistory, { points: [{
110369
+ key: ANALYTICS_AUDIO_LEVEL_KEY,
110370
+ subject: {
110371
+ kind: "device",
110372
+ deviceId
110373
+ },
110374
+ scope: "",
110375
+ ts: at,
110376
+ value: dbfs
110377
+ }] }).catch((err) => {
110378
+ this.logger.warn("audio level history point not stored", {
110379
+ tags: { deviceId },
110380
+ meta: { error: err instanceof Error ? err.message : String(err) }
110381
+ });
110382
+ });
110383
+ }
110384
+ async flushDue(now) {
110385
+ const at = now ?? this.nowFn();
110386
+ const closed = this.accum.takeClosed(at);
110387
+ if (closed.length === 0) return 0;
110388
+ try {
110389
+ return (await this.deviceHistory.recordBuckets({ buckets: closed })).accepted;
110390
+ } catch (err) {
110391
+ const dropped = this.accum.requeue(closed);
110392
+ this.logger.warn("analytics history bucket flush failed — requeued", { meta: {
110393
+ buckets: closed.length,
110394
+ dropped: dropped.length,
110395
+ error: err instanceof Error ? err.message : String(err)
110396
+ } });
110397
+ return 0;
110398
+ }
110399
+ }
110400
+ pendingBucketCount() {
110401
+ return this.accum.pendingBucketCount();
110402
+ }
110403
+ async readLongTermBuckets(input) {
110404
+ const key = historyKeyForLtsSeries(input.series);
110405
+ const result = await this.deviceHistory.query({
110406
+ deviceId: input.deviceId,
110407
+ key,
110408
+ ...input.scope !== void 0 ? { scope: input.scope } : {},
110409
+ from: input.from,
110410
+ to: input.to,
110411
+ resolution: "5m",
110412
+ ...input.limit !== void 0 ? { maxPoints: input.limit } : {}
110413
+ });
110414
+ const out = [];
110415
+ for (const s of result.series) for (const p of s.points) {
110416
+ if (p.mean === null) continue;
110417
+ out.push({
110418
+ scope: s.scope,
110419
+ bucketStart: p.t,
110420
+ samples: p.samples,
110421
+ sum: p.mean * p.samples,
110422
+ min: p.min,
110423
+ max: p.max
110424
+ });
110425
+ }
110426
+ return out.toSorted((a, b) => a.bucketStart - b.bucketStart);
110427
+ }
110428
+ /** Admin debug read — maps device-history 5m buckets to legacy LTS row shape. */
110429
+ async readDebug(input) {
110430
+ const deviceId = input.subject !== void 0 ? Number(input.subject) : NaN;
110431
+ if (!Number.isFinite(deviceId) || input.series === void 0) return [];
110432
+ const key = historyKeyForLtsSeries(input.series);
110433
+ const result = await this.deviceHistory.query({
110434
+ deviceId,
110435
+ key,
110436
+ ...input.scope !== void 0 ? { scope: input.scope } : {},
110437
+ from: input.from ?? 0,
110438
+ to: input.to ?? this.nowFn(),
110439
+ resolution: "5m",
110440
+ ...input.limit !== void 0 ? { maxPoints: input.limit } : {}
110441
+ });
110442
+ const rows = [];
110443
+ for (const s of result.series) for (const p of s.points) {
110444
+ if (p.mean === null) continue;
110445
+ rows.push({
110446
+ subject: String(deviceId),
110447
+ deviceId,
110448
+ series: input.series,
110449
+ scope: s.scope,
110450
+ bucketStart: p.t,
110451
+ samples: p.samples,
110452
+ sum: p.mean * p.samples,
110453
+ min: p.min,
110454
+ max: p.max
110455
+ });
110456
+ }
110457
+ return rows;
110458
+ }
110459
+ openBucketsForDebug() {
110460
+ return [];
110461
+ }
110462
+ };
110463
+ //#endregion
110464
+ //#region src/pipeline-analytics/store/device-history-write-client.ts
110465
+ /** Adapt tRPC-shaped `ctx.api.deviceHistory` to the producer's plain async surface. */
110466
+ function deviceHistoryWriteClient(api) {
110467
+ return {
110468
+ declareSeries: (input) => api.declareSeries.mutate(input),
110469
+ record: (input) => api.record.mutate({ points: [...input.points] }),
110470
+ recordBuckets: (input) => api.recordBuckets.mutate({ buckets: [...input.buckets] }),
110471
+ query: (input) => api.query.query(input)
110472
+ };
110473
+ }
110474
+ //#endregion
110659
110475
  //#region src/pipeline-analytics/store/media-subject-shot.ts
110660
110476
  /** Preference among rows that tie on time. Lower is better. */
110661
110477
  var SUBJECT_SHOT_KIND_RANK = {
@@ -112990,7 +112806,7 @@ var NcArtifactSecretSchema = require_clip_model_registry.string();
112990
112806
  var EmbeddingEnabledSchema = require_clip_model_registry.boolean();
112991
112807
  var SILENCE_FLOOR_DBFS = -55;
112992
112808
  var RETENTION_SWEEP_INTERVAL_MS = 5 * 6e4;
112993
- /** How often closed LTS buckets are written. See `store/lts-store.ts`. */
112809
+ /** How often closed history buckets are flushed. See `analytics-history-producer.ts`. */
112994
112810
  var LTS_FLUSH_INTERVAL_MS = 6e4;
112995
112811
  /**
112996
112812
  * How long the events ops-log is kept when the cluster has said nothing. 30
@@ -114646,7 +114462,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
114646
114462
  /**
114647
114463
  * Five-minute long-term statistics — the tier this system did not have.
114648
114464
  * Global for every camera (no opt-in: a baseline nobody enables is the
114649
- * failure mode `observe` mode was already in). See `store/lts-store.ts`.
114465
+ * failure mode `observe` mode was already in). See `analytics-history-producer.ts`.
114650
114466
  */
114651
114467
  lts = null;
114652
114468
  /** The runner node that last produced a frame for a camera — see
@@ -114813,7 +114629,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
114813
114629
  onAudioResult: (data) => this.handleAudioResult(data),
114814
114630
  onMotionAnalysis: (deviceId, data, timestampMs) => this.handleMotionAnalysis(deviceId, data, timestampMs),
114815
114631
  onOnboardMotion: (deviceId, data, timestampMs) => this.handleOnboardMotion(deviceId, data, timestampMs),
114816
- onBatterySample: (deviceId, percentage, at) => this.lts?.note("battery", deviceId, percentage, at),
114817
114632
  onSensorStateChanged: (data, timestampMs) => this.dispatchSensorStateChanged(data, timestampMs),
114818
114633
  onBindingsChanged: (data) => this.handleBindingsChangedEvent(data),
114819
114634
  onDeviceUnregistered: (deviceId) => this.handleDeviceUnregisteredEvent(deviceId),
@@ -115142,7 +114957,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
115142
114957
  TrackingTapeIndexStore.declare(api.settingsStore),
115143
114958
  BirthDecisionLedgerStore.declare(api.settingsStore),
115144
114959
  TrackRelationsStore.declare(api.settingsStore),
115145
- AnalyticsLts.declare(api.settingsStore),
115146
114960
  declarePaAnalyticsBootRecordBackedLegacy(api.settingsStore, isHub),
115147
114961
  declareNotificationCenterBootCollections(api.settingsStore, isHub)
115148
114962
  ]);
@@ -115498,9 +115312,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
115498
115312
  }
115499
115313
  });
115500
115314
  this.refreshDeviceAlertSettings();
115501
- this.lts = new AnalyticsLts({
115502
- store: api.settingsStore,
115503
- logger: logger.child("lts")
115315
+ this.lts = new AnalyticsHistoryProducer({
115316
+ deviceHistory: deviceHistoryWriteClient(api.deviceHistory),
115317
+ logger: logger.child("device-history")
115318
+ });
115319
+ this.lts.declare().catch((err) => {
115320
+ logger.warn("pipeline-analytics history series declare failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
115504
115321
  });
115505
115322
  const lts = this.lts;
115506
115323
  const eventStore = new EventStore({
@@ -116967,8 +116784,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
116967
116784
  open: []
116968
116785
  };
116969
116786
  return {
116970
- rows: (await lts.read(input)).map(withMean),
116971
- open: lts.openBuckets().map(withMean)
116787
+ rows: (await lts.readDebug(input)).map(withMean),
116788
+ open: lts.openBucketsForDebug().map(withMean)
116972
116789
  };
116973
116790
  },
116974
116791
  /**
@@ -121044,33 +120861,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
121044
120861
  });
121045
120862
  this.levelStateByDevice.set(deviceId, res.nextState);
121046
120863
  if (!res.emit) return;
121047
- const draft = levelAudioEventDraft(deviceId, timestamp, frame);
121048
- this.lastAudioInsertByDevice.set(deviceId, {
121049
- className: void 0,
121050
- atMs: timestamp
121051
- });
121052
- const insertedId = await this.eventStore.insertAudio(draft);
121053
- if (insertedId === null) return;
121054
- const ev = {
121055
- ...draft,
121056
- id: insertedId
121057
- };
121058
- this.ctx.eventBus.emit({
121059
- id: `pa-${ev.id}`,
121060
- timestamp: new Date(ev.timestamp),
121061
- source: {
121062
- type: "addon",
121063
- id: "pipeline-analytics",
121064
- addonId: "pipeline-analytics"
121065
- },
121066
- category: require_clip_model_registry.EventCategory.PipelineAnalyticsDetectionEvent,
121067
- data: {
121068
- deviceId,
121069
- kind: "audio",
121070
- eventId: ev.id,
121071
- timestamp: ev.timestamp
121072
- }
121073
- });
120864
+ const dbfs = level?.dbfs ?? Number.NEGATIVE_INFINITY;
120865
+ this.lts?.recordAudioLevel(deviceId, dbfs, timestamp);
121074
120866
  }
121075
120867
  /**
121076
120868
  * Persist motion-analysis bus events into the EPISODE tracker (D475): a
@@ -125185,21 +124977,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
125185
124977
  async readLongTermSeries(input) {
125186
124978
  const lts = this.lts;
125187
124979
  if (!lts) throw new Error("pipeline-analytics: the long-term store is not ready yet");
125188
- return (await lts.read({
125189
- subject: String(input.deviceId),
125190
- series: input.series,
125191
- ...input.scope !== void 0 ? { scope: input.scope } : {},
125192
- from: input.from,
125193
- to: input.to,
125194
- ...input.limit !== void 0 ? { limit: input.limit } : {}
125195
- })).map((r) => ({
125196
- scope: r.scope,
125197
- bucketStart: r.bucketStart,
125198
- samples: r.samples,
125199
- sum: r.sum,
125200
- min: r.min,
125201
- max: r.max
125202
- }));
124980
+ return lts.readLongTermBuckets(input);
125203
124981
  }
125204
124982
  async getEventStoreFootprint() {
125205
124983
  return this.queryFacade.getEventStoreFootprint();