@camstack/addon-post-analysis 1.2.234 → 1.2.237
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-Du1qDTXV.mjs → dist-BDpucx_W.mjs} +202 -9
- package/dist/{dist-DTRcLvW5.js → dist-IoI_ufBo.js} +202 -9
- 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-D-bBcfRY.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DIQIycqg.mjs} +3 -3
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C5YuStaN.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dl-N-7EJ.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-C9lFCeZi.mjs → hostInit-B5V7K95U.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +494 -29
- package/dist/pipeline-analytics/index.mjs +494 -29
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as TimelapseRuleSchema, $t as boolean, A as NcConditionDescriptorSchema, At as plateGalleryCapability, B as NcTaxonomySchema, Bt as videoclipsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as isSourceCap, D as NC_DEFAULT_SNOOZE_MINUTES, E as NC_CONDITION_CATALOG, Et as notificationRulesCapability, F as NcScheduleSchema, Ft as sceneMonitorCapability, Gt as DeviceType, Ht as errMsg$1, I as NcSnoozeInputSchema, It as storageOccupancyCapability, J as SCENE_DIVERGED, Jt as isDeviceScopedCap, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as createEvent, L as NcSnoozeSchema, Lt as subKindsOf, M as NcRulePatchSchema, Mt as readTimelapseGeneratedAt, N as NcRuleSchema, Nt as resolveLocationMode, Ot as pickClusterStepModels, P as NcRuleTargetSchema, Q as TimelapseRulePatchSchema, Qt as array, R as NcSnoozeSuppressedSchema, Rt as systemEventFilterApplies, St as isScheduleActive, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as mayWriteToLocation, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as BaseAddon, V as OpsLogEntrySchema, Vt as zoneAnalyticsCapability, W as RetrainStatusSchema, Wt as CamProfileSchema, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as sleep$1, Y as SceneMonitorSchema, Yt as nodePin, Z as TimelapseRuleInputSchema, Zt as _enum, _ as FULL_IMAGE_BBOX, _t as failureContributionCapability, a as CLUSTER_MODEL_SCOPED_STEPS, an as record, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isEventOwnerType, cn as EventCategory, ct as cosineSimilarity$1, dt as deriveRecordingMode, en as discriminatedUnion, et as TrackSourceSchema, f as DeclaredDevices, g as FIRST_LEVEL_MACRO_CLASSES, gt as faceGalleryCapability, h as EVENT_PAD_MS, ht as evictionPolicyOfLocation, i as BirthDecisionRecordSchema, in as partialRecord, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readDeviceStateFrom, k as NC_TAXONOMY, kt as pipelineAnalyticsCapability, lt as customAction, m as EVENT_OWNER_TYPES, mt as evaluateSensorEdge, n as ArchivedDebugNoteSchema, nn as number, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as string, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as encodeVectorBase64, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as hydrateSchema, r as BaseDevice, rn as object, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as unknown, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as literal, tt as VISIT_MERGE_GAP_MS, u as DETECTION_MACRO_CLASSES, ut as defineCustomActions, v as FailureCounters, w as MediaFileKindEnum, wt as kebabToCamel, x as MAX_BIRTH_DECISION_RECORDS, xt as isMediaOwnerType, y as LabelAttributionSchema, yt as isDetectionMacroClass, z as NcSystemEventKindSchema, zt as vectorDimFromBase64 } from "../dist-
|
|
1
|
+
import { $ as TimelapseRuleSchema, $t as boolean, A as NcConditionDescriptorSchema, At as plateGalleryCapability, B as NcTaxonomySchema, Bt as videoclipsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as isSourceCap, D as NC_DEFAULT_SNOOZE_MINUTES, E as NC_CONDITION_CATALOG, Et as notificationRulesCapability, F as NcScheduleSchema, Ft as sceneMonitorCapability, Gt as DeviceType, Ht as errMsg$1, I as NcSnoozeInputSchema, It as storageOccupancyCapability, J as SCENE_DIVERGED, Jt as isDeviceScopedCap, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as createEvent, L as NcSnoozeSchema, Lt as subKindsOf, M as NcRulePatchSchema, Mt as readTimelapseGeneratedAt, N as NcRuleSchema, Nt as resolveLocationMode, Ot as pickClusterStepModels, P as NcRuleTargetSchema, Q as TimelapseRulePatchSchema, Qt as array, R as NcSnoozeSuppressedSchema, Rt as systemEventFilterApplies, St as isScheduleActive, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as mayWriteToLocation, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as BaseAddon, V as OpsLogEntrySchema, Vt as zoneAnalyticsCapability, W as RetrainStatusSchema, Wt as CamProfileSchema, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as sleep$1, Y as SceneMonitorSchema, Yt as nodePin, Z as TimelapseRuleInputSchema, Zt as _enum, _ as FULL_IMAGE_BBOX, _t as failureContributionCapability, a as CLUSTER_MODEL_SCOPED_STEPS, an as record, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isEventOwnerType, cn as EventCategory, ct as cosineSimilarity$1, dt as deriveRecordingMode, en as discriminatedUnion, et as TrackSourceSchema, f as DeclaredDevices, g as FIRST_LEVEL_MACRO_CLASSES, gt as faceGalleryCapability, h as EVENT_PAD_MS, ht as evictionPolicyOfLocation, i as BirthDecisionRecordSchema, in as partialRecord, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readDeviceStateFrom, k as NC_TAXONOMY, kt as pipelineAnalyticsCapability, lt as customAction, m as EVENT_OWNER_TYPES, mt as evaluateSensorEdge, n as ArchivedDebugNoteSchema, nn as number, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as string, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as encodeVectorBase64, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as hydrateSchema, r as BaseDevice, rn as object, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as unknown, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as literal, tt as VISIT_MERGE_GAP_MS, u as DETECTION_MACRO_CLASSES, ut as defineCustomActions, v as FailureCounters, w as MediaFileKindEnum, wt as kebabToCamel, x as MAX_BIRTH_DECISION_RECORDS, xt as isMediaOwnerType, y as LabelAttributionSchema, yt as isDetectionMacroClass, z as NcSystemEventKindSchema, zt as vectorDimFromBase64 } from "../dist-BDpucx_W.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";
|
|
@@ -7438,6 +7438,16 @@ var NcAlertStore = class {
|
|
|
7438
7438
|
};
|
|
7439
7439
|
}
|
|
7440
7440
|
};
|
|
7441
|
+
function deriveConsumableBand(input) {
|
|
7442
|
+
const { level, status } = input.reading;
|
|
7443
|
+
const measured = level !== null && Number.isFinite(level);
|
|
7444
|
+
if (!measured && status === null) return null;
|
|
7445
|
+
if (status === "replace") return "low";
|
|
7446
|
+
if (!measured) return "normal";
|
|
7447
|
+
const value = level;
|
|
7448
|
+
if (input.previous === "low") return value >= input.thresholdPercent + 5 ? "normal" : "low";
|
|
7449
|
+
return value <= input.thresholdPercent ? "low" : "normal";
|
|
7450
|
+
}
|
|
7441
7451
|
//#endregion
|
|
7442
7452
|
//#region src/notification-center/device-directory.ts
|
|
7443
7453
|
/**
|
|
@@ -9838,6 +9848,40 @@ function incomingFromBatteryBand(input) {
|
|
|
9838
9848
|
});
|
|
9839
9849
|
}
|
|
9840
9850
|
/**
|
|
9851
|
+
* A consumable BAND, as a system-event subject.
|
|
9852
|
+
*
|
|
9853
|
+
* Built exactly like {@link incomingFromBatteryBand} — the operator asked for
|
|
9854
|
+
* these to be treated as the battery notifications are — with two differences
|
|
9855
|
+
* that come from the subject and not from taste:
|
|
9856
|
+
*
|
|
9857
|
+
* - **the subject carries the ITEM.** `consumable:<deviceId>:<key>`, because a
|
|
9858
|
+
* vacuum has a brush AND a filter AND a mop pad, and one subject for all
|
|
9859
|
+
* three would let the brush's alert cancel the filter's.
|
|
9860
|
+
* - **the body variant follows what is KNOWN.** An item with no percentage
|
|
9861
|
+
* gets a finished sentence rather than a template with a stand-in number in
|
|
9862
|
+
* it, for the same reason a binary battery does.
|
|
9863
|
+
*
|
|
9864
|
+
* The recovery keeps the SAME subject: it is the same episode ending.
|
|
9865
|
+
*/
|
|
9866
|
+
function incomingFromConsumableBand(input) {
|
|
9867
|
+
const kind = input.band === "low" ? "device-consumable-low" : "device-consumable-normal";
|
|
9868
|
+
const measured = input.item.level !== null && Number.isFinite(input.item.level);
|
|
9869
|
+
return systemSubject({
|
|
9870
|
+
facts: input.facts,
|
|
9871
|
+
kind,
|
|
9872
|
+
subject: `consumable:${input.deviceId}:${input.item.key}`,
|
|
9873
|
+
titleKey: `system.${kind}.title`,
|
|
9874
|
+
bodyKey: measured ? `system.${kind}.body` : `system.${kind}.body.unmeasured`,
|
|
9875
|
+
textParams: {
|
|
9876
|
+
...deviceTextParams(input.deviceId, input.identity),
|
|
9877
|
+
item: input.item.label,
|
|
9878
|
+
...measured ? { percentage: String(Math.round(input.item.level)) } : {}
|
|
9879
|
+
},
|
|
9880
|
+
deviceId: input.deviceId,
|
|
9881
|
+
...input.identity !== void 0 ? { deviceType: input.identity.type } : {}
|
|
9882
|
+
});
|
|
9883
|
+
}
|
|
9884
|
+
/**
|
|
9841
9885
|
* Normalize the infrastructure categories consumed by Notification Center.
|
|
9842
9886
|
* Malformed payloads are rejected instead of guessing an identity.
|
|
9843
9887
|
*
|
|
@@ -9989,6 +10033,7 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
9989
10033
|
}
|
|
9990
10034
|
case "system.restart-completed": return incomingFromServerUpdated(facts, data);
|
|
9991
10035
|
case "recording.export.completed": return incomingFromExportCompleted(facts, data);
|
|
10036
|
+
case "backup.run-completed": return incomingFromBackupCompleted(facts, data);
|
|
9992
10037
|
default: return null;
|
|
9993
10038
|
}
|
|
9994
10039
|
}
|
|
@@ -10152,6 +10197,37 @@ function incomingFromServerUpdated(facts, data) {
|
|
|
10152
10197
|
nodeIds: [nodeId]
|
|
10153
10198
|
});
|
|
10154
10199
|
}
|
|
10200
|
+
/**
|
|
10201
|
+
* A finished backup RUN.
|
|
10202
|
+
*
|
|
10203
|
+
* Subject is the run, not the destination: `backup.completed` fires once per
|
|
10204
|
+
* destination and an operator with three of them would be told three times.
|
|
10205
|
+
* The counts travel in the text because a PARTIAL run must not read as a clean
|
|
10206
|
+
* one — two destinations of three is news, and a bare "backup completed" would
|
|
10207
|
+
* hide it.
|
|
10208
|
+
*/
|
|
10209
|
+
function incomingFromBackupCompleted(facts, data) {
|
|
10210
|
+
const ok = numberField(data, "destinationsOk");
|
|
10211
|
+
const total = numberField(data, "destinationsTotal");
|
|
10212
|
+
if (ok === void 0 || total === void 0) return null;
|
|
10213
|
+
const failed = numberField(data, "destinationsFailed") ?? Math.max(0, total - ok);
|
|
10214
|
+
const sizeMB = numberField(data, "sizeMB");
|
|
10215
|
+
const backupId = stringField(data, "backupId");
|
|
10216
|
+
return systemSubject({
|
|
10217
|
+
facts,
|
|
10218
|
+
kind: "backup-completed",
|
|
10219
|
+
subject: `backup:${backupId ?? facts.timestamp}`,
|
|
10220
|
+
titleKey: "system.backup-completed.title",
|
|
10221
|
+
bodyKey: failed > 0 ? "system.backup-completed.body.partial" : "system.backup-completed.body",
|
|
10222
|
+
textParams: {
|
|
10223
|
+
destinationsOk: String(ok),
|
|
10224
|
+
destinationsTotal: String(total),
|
|
10225
|
+
destinationsFailed: String(failed),
|
|
10226
|
+
...sizeMB !== void 0 ? { sizeMB: String(sizeMB) } : {},
|
|
10227
|
+
...backupId !== void 0 ? { backupId } : {}
|
|
10228
|
+
}
|
|
10229
|
+
});
|
|
10230
|
+
}
|
|
10155
10231
|
function incomingFromExportCompleted(facts, data) {
|
|
10156
10232
|
const exportId = stringField(data, "exportId");
|
|
10157
10233
|
if (exportId === void 0) return null;
|
|
@@ -10489,6 +10565,8 @@ var NcDeviceAlerts = class {
|
|
|
10489
10565
|
* durable.
|
|
10490
10566
|
*/
|
|
10491
10567
|
lastReading = /* @__PURE__ */ new Map();
|
|
10568
|
+
/** The last reading per consumable SUBJECT, for the reminder sweep. */
|
|
10569
|
+
lastConsumable = /* @__PURE__ */ new Map();
|
|
10492
10570
|
constructor(deps) {
|
|
10493
10571
|
this.deps = deps;
|
|
10494
10572
|
this.now = deps.now ?? Date.now;
|
|
@@ -10526,6 +10604,44 @@ var NcDeviceAlerts = class {
|
|
|
10526
10604
|
}));
|
|
10527
10605
|
}
|
|
10528
10606
|
/**
|
|
10607
|
+
* ONE consumable item, from a device's `consumables` slice.
|
|
10608
|
+
*
|
|
10609
|
+
* Per ITEM, never per device: a vacuum has a brush, a filter and a mop pad,
|
|
10610
|
+
* and folding them into one subject would make the brush's alert cancel the
|
|
10611
|
+
* filter's — the same collision D141 split `device-switch:<id>` off liveness
|
|
10612
|
+
* to avoid. Each item is its own episode, its own cooldown, its own recovery.
|
|
10613
|
+
*
|
|
10614
|
+
* An item that has told us nothing leaves EVERYTHING untouched, for the
|
|
10615
|
+
* reason `observeBattery` does: seeding it healthy is a claim, and the first
|
|
10616
|
+
* genuine `replace` would then read as a transition out of a state nobody
|
|
10617
|
+
* observed.
|
|
10618
|
+
*/
|
|
10619
|
+
observeConsumable(deviceId, item) {
|
|
10620
|
+
const subject = `consumable:${deviceId}:${item.key}`;
|
|
10621
|
+
const previous = this.deps.rows.get(subject);
|
|
10622
|
+
const settings = this.deps.settings();
|
|
10623
|
+
const band = deriveConsumableBand({
|
|
10624
|
+
reading: {
|
|
10625
|
+
level: item.level,
|
|
10626
|
+
status: item.status
|
|
10627
|
+
},
|
|
10628
|
+
thresholdPercent: settings.consumableLowPercent,
|
|
10629
|
+
previous: previous?.alerting === true ? "low" : previous === void 0 ? void 0 : "normal"
|
|
10630
|
+
});
|
|
10631
|
+
if (band === null) return;
|
|
10632
|
+
this.lastConsumable.set(subject, {
|
|
10633
|
+
deviceId,
|
|
10634
|
+
item
|
|
10635
|
+
});
|
|
10636
|
+
this.apply(subject, band === "low", settings, (facts, alerting) => incomingFromConsumableBand({
|
|
10637
|
+
facts,
|
|
10638
|
+
deviceId,
|
|
10639
|
+
band: alerting ? "low" : "normal",
|
|
10640
|
+
item,
|
|
10641
|
+
identity: this.deps.lookup(deviceId)
|
|
10642
|
+
}));
|
|
10643
|
+
}
|
|
10644
|
+
/**
|
|
10529
10645
|
* The reminder sweep. Called from an existing periodic tick; it allocates
|
|
10530
10646
|
* nothing on the healthy path.
|
|
10531
10647
|
*
|
|
@@ -10539,6 +10655,17 @@ var NcDeviceAlerts = class {
|
|
|
10539
10655
|
if (settings.renotifyMinutes <= 0) return;
|
|
10540
10656
|
for (const row of this.deps.rows.all()) {
|
|
10541
10657
|
if (!row.alerting) continue;
|
|
10658
|
+
const consumable = this.lastConsumable.get(row.subject);
|
|
10659
|
+
if (consumable !== void 0) {
|
|
10660
|
+
this.apply(row.subject, true, settings, (facts) => incomingFromConsumableBand({
|
|
10661
|
+
facts,
|
|
10662
|
+
deviceId: consumable.deviceId,
|
|
10663
|
+
band: "low",
|
|
10664
|
+
item: consumable.item,
|
|
10665
|
+
identity: this.deps.lookup(consumable.deviceId)
|
|
10666
|
+
}));
|
|
10667
|
+
continue;
|
|
10668
|
+
}
|
|
10542
10669
|
const restatement = this.lastReading.get(row.subject);
|
|
10543
10670
|
if (restatement === void 0) continue;
|
|
10544
10671
|
this.apply(row.subject, true, settings, (facts) => incomingFromBatteryBand({
|
|
@@ -11647,6 +11774,15 @@ var en_default = {
|
|
|
11647
11774
|
"system.export-completed.title": "Export ready",
|
|
11648
11775
|
"system.export-completed.body": "The footage export is ready",
|
|
11649
11776
|
"system.export-completed.body.titled": "{{title}} is ready",
|
|
11777
|
+
"system.backup-completed.title": "Backup completed",
|
|
11778
|
+
"system.backup-completed.body": "Backup saved to {{destinationsOk}} of {{destinationsTotal}} destinations ({{sizeMB}} MB)",
|
|
11779
|
+
"system.backup-completed.body.partial": "Backup saved to {{destinationsOk}} of {{destinationsTotal}} destinations — {{destinationsFailed}} failed",
|
|
11780
|
+
"system.device-consumable-low.title": "Consumable low",
|
|
11781
|
+
"system.device-consumable-low.body": "{{item}} on {{deviceName}} is at {{percentage}}%",
|
|
11782
|
+
"system.device-consumable-low.body.unmeasured": "{{item}} on {{deviceName}} needs replacing",
|
|
11783
|
+
"system.device-consumable-normal.title": "Consumable replaced",
|
|
11784
|
+
"system.device-consumable-normal.body": "{{item}} on {{deviceName}} is back to {{percentage}}%",
|
|
11785
|
+
"system.device-consumable-normal.body.unmeasured": "{{item}} on {{deviceName}} is OK again",
|
|
11650
11786
|
"system.camera-online.title": "Device online",
|
|
11651
11787
|
"system.camera-online.body": "Device {{deviceName}} is online",
|
|
11652
11788
|
"system.camera-offline.title": "Device offline",
|
|
@@ -11907,6 +12043,15 @@ var it_default = {
|
|
|
11907
12043
|
"system.export-completed.title": "Export pronto",
|
|
11908
12044
|
"system.export-completed.body": "L'export del filmato è pronto",
|
|
11909
12045
|
"system.export-completed.body.titled": "{{title}} è pronto",
|
|
12046
|
+
"system.backup-completed.title": "Backup completato",
|
|
12047
|
+
"system.backup-completed.body": "Backup salvato su {{destinationsOk}} destinazioni su {{destinationsTotal}} ({{sizeMB}} MB)",
|
|
12048
|
+
"system.backup-completed.body.partial": "Backup salvato su {{destinationsOk}} destinazioni su {{destinationsTotal}} — {{destinationsFailed}} fallite",
|
|
12049
|
+
"system.device-consumable-low.title": "Consumabile quasi esaurito",
|
|
12050
|
+
"system.device-consumable-low.body": "{{item}} su {{deviceName}} è al {{percentage}}%",
|
|
12051
|
+
"system.device-consumable-low.body.unmeasured": "{{item}} su {{deviceName}} va sostituito",
|
|
12052
|
+
"system.device-consumable-normal.title": "Consumabile sostituito",
|
|
12053
|
+
"system.device-consumable-normal.body": "{{item}} su {{deviceName}} è tornato al {{percentage}}%",
|
|
12054
|
+
"system.device-consumable-normal.body.unmeasured": "{{item}} su {{deviceName}} è di nuovo a posto",
|
|
11910
12055
|
"system.camera-online.title": "Dispositivo online",
|
|
11911
12056
|
"system.camera-online.body": "Il dispositivo {{deviceName}} è online",
|
|
11912
12057
|
"system.camera-offline.title": "Dispositivo offline",
|
|
@@ -22215,6 +22360,7 @@ function alarmButtonKind(kind) {
|
|
|
22215
22360
|
*/
|
|
22216
22361
|
var NC_DEVICE_ALERT_FALLBACK = {
|
|
22217
22362
|
batteryLowPercent: 20,
|
|
22363
|
+
consumableLowPercent: 15,
|
|
22218
22364
|
renotifyMinutes: 720,
|
|
22219
22365
|
notifyRecovery: true
|
|
22220
22366
|
};
|
|
@@ -22247,6 +22393,52 @@ function batterySliceOf(event) {
|
|
|
22247
22393
|
};
|
|
22248
22394
|
}
|
|
22249
22395
|
/**
|
|
22396
|
+
* A `device.state-changed` carrying a CONSUMABLES slice, reduced to the items
|
|
22397
|
+
* the band derivation reads.
|
|
22398
|
+
*
|
|
22399
|
+
* Same tap as the battery's: `DeviceStateChanged` is already subscribed, so
|
|
22400
|
+
* every `consumables` write already arrives here and was simply dropped. No
|
|
22401
|
+
* new bus category and no producer change.
|
|
22402
|
+
*
|
|
22403
|
+
* An item is kept only when it carries a usable `key` — the subject is built
|
|
22404
|
+
* from it, and an item with no key could not be told apart from its sibling.
|
|
22405
|
+
* `level` and `status` are each optional; an item with NEITHER is still
|
|
22406
|
+
* returned, because deciding that it has told us nothing belongs to
|
|
22407
|
+
* `deriveConsumableBand` and not to a parser (D393: absent is not zero, and
|
|
22408
|
+
* the parser is not the place that judges).
|
|
22409
|
+
*/
|
|
22410
|
+
function consumablesSliceOf(event) {
|
|
22411
|
+
if (event.category !== "device.state-changed") return null;
|
|
22412
|
+
const data = event.data;
|
|
22413
|
+
if (data["capName"] !== "consumables") return null;
|
|
22414
|
+
const deviceId = data["deviceId"];
|
|
22415
|
+
const slice = data["slice"];
|
|
22416
|
+
if (typeof deviceId !== "number") return null;
|
|
22417
|
+
if (typeof slice !== "object" || slice === null || Array.isArray(slice)) return null;
|
|
22418
|
+
const rawItems = slice["items"];
|
|
22419
|
+
if (!Array.isArray(rawItems)) return null;
|
|
22420
|
+
const items = [];
|
|
22421
|
+
for (const entry of rawItems) {
|
|
22422
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
|
22423
|
+
const row = entry;
|
|
22424
|
+
const key = row["key"];
|
|
22425
|
+
if (typeof key !== "string" || key.length === 0) continue;
|
|
22426
|
+
const level = row["level"];
|
|
22427
|
+
const status = row["status"];
|
|
22428
|
+
const label = row["label"];
|
|
22429
|
+
items.push({
|
|
22430
|
+
key,
|
|
22431
|
+
label: typeof label === "string" && label.length > 0 ? label : key,
|
|
22432
|
+
level: typeof level === "number" ? level : null,
|
|
22433
|
+
status: status === "ok" || status === "replace" ? status : null
|
|
22434
|
+
});
|
|
22435
|
+
}
|
|
22436
|
+
return items.length === 0 ? null : {
|
|
22437
|
+
deviceId,
|
|
22438
|
+
items
|
|
22439
|
+
};
|
|
22440
|
+
}
|
|
22441
|
+
/**
|
|
22250
22442
|
* What `getAlarmConfig` answers on a node with no panel.
|
|
22251
22443
|
*
|
|
22252
22444
|
* The DEFAULTS, not zeroes: paired with `deviceId: null` this tells a client
|
|
@@ -24295,6 +24487,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
24295
24487
|
this.deviceAlerts.observeBattery(battery.deviceId, battery.reading);
|
|
24296
24488
|
return;
|
|
24297
24489
|
}
|
|
24490
|
+
const consumables = consumablesSliceOf(event);
|
|
24491
|
+
if (consumables !== null) {
|
|
24492
|
+
for (const item of consumables.items) this.deviceAlerts.observeConsumable(consumables.deviceId, item);
|
|
24493
|
+
return;
|
|
24494
|
+
}
|
|
24298
24495
|
const lookup = this.deviceDirectory.lookup();
|
|
24299
24496
|
const incoming = incomingFromSystemEvent(event, lookup);
|
|
24300
24497
|
if (incoming === null) {
|
|
@@ -25907,6 +26104,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
25907
26104
|
deviceAlertSettings() {
|
|
25908
26105
|
return this.deps.readDeviceAlertSettings?.() ?? {
|
|
25909
26106
|
batteryLowPercent: NC_DEVICE_ALERT_FALLBACK.batteryLowPercent,
|
|
26107
|
+
consumableLowPercent: NC_DEVICE_ALERT_FALLBACK.consumableLowPercent,
|
|
25910
26108
|
renotifyMinutes: NC_DEVICE_ALERT_FALLBACK.renotifyMinutes,
|
|
25911
26109
|
notifyRecovery: NC_DEVICE_ALERT_FALLBACK.notifyRecovery
|
|
25912
26110
|
};
|
|
@@ -26522,6 +26720,7 @@ function systemEventSubject(kind, scope) {
|
|
|
26522
26720
|
if (kind === "addon-updated") return `packages:addon-updated:${scope.packageName ?? "pkg"}`;
|
|
26523
26721
|
if (kind === "server-updated") return `server-updated:${scope.nodeId ?? "hub"}`;
|
|
26524
26722
|
if (kind === "export-completed") return `export:${scope.deviceId ?? 0}`;
|
|
26723
|
+
if (kind === "backup-completed") return "backup:test";
|
|
26525
26724
|
return `alarm:${scope.deviceId ?? 0}`;
|
|
26526
26725
|
}
|
|
26527
26726
|
/**
|
|
@@ -31631,7 +31830,8 @@ function analyseDebugTrack(inventory) {
|
|
|
31631
31830
|
}
|
|
31632
31831
|
}
|
|
31633
31832
|
if (closed && facts.bestEventId === void 0) out.push(finding("NO_BEST_EVENT", "info", "No representative event was chosen for this track."));
|
|
31634
|
-
if (facts.
|
|
31833
|
+
if (facts.labelStepId === "detector:raw-class") out.push(finding("UNSCORED_LABEL", "info", `The label is the detector's raw class (${facts.label ?? facts.className}) — no scored classifier answered for this track.`));
|
|
31834
|
+
else if (facts.labelScore !== void 0 && facts.labelScore < .5) out.push(finding("LOW_BEST_SCORE", "info", `Best label score ${facts.labelScore.toFixed(2)} is under ${String(LOW_BEST_SCORE)}.`));
|
|
31635
31835
|
if (thumbnailProvenance === "standin") out.push(finding("BEST_SHOT_STANDIN", "warn", "Best shot is a stand-in: a face/plate detail crop copied in because no frame of the subject was captured."));
|
|
31636
31836
|
if (thumbnailProvenance === "raster") out.push(finding("BEST_SHOT_RASTER", "info", "Best shot is a sub-native raster crop of the subject, not a native capture."));
|
|
31637
31837
|
return out;
|
|
@@ -31689,6 +31889,7 @@ function toFacts(track) {
|
|
|
31689
31889
|
className: track.className,
|
|
31690
31890
|
...track.label === void 0 ? {} : { label: track.label },
|
|
31691
31891
|
...track.labelScore === void 0 ? {} : { labelScore: track.labelScore },
|
|
31892
|
+
...track.labelMeta?.stepId === void 0 ? {} : { labelStepId: track.labelMeta.stepId },
|
|
31692
31893
|
firstSeen: track.firstSeen,
|
|
31693
31894
|
lastSeen: track.lastSeen,
|
|
31694
31895
|
active: track.active,
|
|
@@ -47308,31 +47509,54 @@ var EventMediaService = class {
|
|
|
47308
47509
|
//#endregion
|
|
47309
47510
|
//#region src/shared/frame/crop-extractor.ts
|
|
47310
47511
|
/**
|
|
47311
|
-
*
|
|
47312
|
-
*
|
|
47512
|
+
* The ONE reading of a normalized box against a frame: round, then clamp so
|
|
47513
|
+
* the rectangle is inside the frame and never empty. Both extractors below
|
|
47514
|
+
* share it, so the JPEG half and the raw half can never disagree about WHICH
|
|
47515
|
+
* pixels the crop is — only about how they are encoded.
|
|
47516
|
+
*/
|
|
47517
|
+
function clampNormalizedBox(frameWidth, frameHeight, bbox) {
|
|
47518
|
+
const left = Math.max(0, Math.min(Math.round(bbox.x * frameWidth), frameWidth - 1));
|
|
47519
|
+
const top = Math.max(0, Math.min(Math.round(bbox.y * frameHeight), frameHeight - 1));
|
|
47520
|
+
return {
|
|
47521
|
+
left,
|
|
47522
|
+
top,
|
|
47523
|
+
width: Math.max(1, Math.min(Math.round(bbox.w * frameWidth), frameWidth - left)),
|
|
47524
|
+
height: Math.max(1, Math.min(Math.round(bbox.h * frameHeight), frameHeight - top))
|
|
47525
|
+
};
|
|
47526
|
+
}
|
|
47527
|
+
/**
|
|
47528
|
+
* Extracts a **JPEG-encoded** crop from a raw frame buffer using a normalized
|
|
47529
|
+
* bounding box. The name does not say JPEG; the return type is what you must
|
|
47530
|
+
* read. A caller whose surrounding frame is raw wants {@link extractCropRaw} —
|
|
47531
|
+
* handing this output on as raw pixels is what produced 717 `VipsImage: memory
|
|
47532
|
+
* area too small` failures and cost the fleet a day of group shots.
|
|
47313
47533
|
*/
|
|
47314
47534
|
async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
|
|
47315
|
-
const
|
|
47316
|
-
const rawTop = Math.round(bbox.y * frameHeight);
|
|
47317
|
-
const rawWidth = Math.round(bbox.w * frameWidth);
|
|
47318
|
-
const rawHeight = Math.round(bbox.h * frameHeight);
|
|
47319
|
-
const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
|
|
47320
|
-
const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
|
|
47321
|
-
const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
|
|
47322
|
-
const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
|
|
47535
|
+
const rect = clampNormalizedBox(frameWidth, frameHeight, bbox);
|
|
47323
47536
|
return {
|
|
47324
47537
|
crop: await sharp(frameData, { raw: {
|
|
47325
47538
|
width: frameWidth,
|
|
47326
47539
|
height: frameHeight,
|
|
47327
47540
|
channels: 3
|
|
47328
|
-
} }).extract({
|
|
47329
|
-
|
|
47330
|
-
|
|
47331
|
-
|
|
47332
|
-
|
|
47333
|
-
|
|
47334
|
-
|
|
47335
|
-
|
|
47541
|
+
} }).extract(rect).jpeg({ quality: 90 }).toBuffer(),
|
|
47542
|
+
width: rect.width,
|
|
47543
|
+
height: rect.height
|
|
47544
|
+
};
|
|
47545
|
+
}
|
|
47546
|
+
/**
|
|
47547
|
+
* The same cut, raw in and **raw RGB out** — for a caller that carries a
|
|
47548
|
+
* `DecodedFrame` and must keep `format: 'rgb'` true of the bytes it holds.
|
|
47549
|
+
*/
|
|
47550
|
+
async function extractCropRaw(frameData, frameWidth, frameHeight, bbox) {
|
|
47551
|
+
const rect = clampNormalizedBox(frameWidth, frameHeight, bbox);
|
|
47552
|
+
return {
|
|
47553
|
+
data: await sharp(frameData, { raw: {
|
|
47554
|
+
width: frameWidth,
|
|
47555
|
+
height: frameHeight,
|
|
47556
|
+
channels: 3
|
|
47557
|
+
} }).extract(rect).raw().toBuffer(),
|
|
47558
|
+
width: rect.width,
|
|
47559
|
+
height: rect.height
|
|
47336
47560
|
};
|
|
47337
47561
|
}
|
|
47338
47562
|
//#endregion
|
|
@@ -49873,6 +50097,19 @@ var DeviceAlertSettingsSchema = object({
|
|
|
49873
50097
|
*/
|
|
49874
50098
|
batteryLowPercent: number().int().min(1).max(99).default(20),
|
|
49875
50099
|
/**
|
|
50100
|
+
* What counts as a nearly-exhausted CONSUMABLE — a filter, a brush, a mop pad.
|
|
50101
|
+
*
|
|
50102
|
+
* Its own number and not the battery's, because the two are different chores
|
|
50103
|
+
* on different clocks: a battery at 20 % is hours from flat, a brush at 20 %
|
|
50104
|
+
* is weeks from worn, and an operator who wants to be told early about one
|
|
50105
|
+
* does not necessarily want to be told early about the other.
|
|
50106
|
+
*
|
|
50107
|
+
* Applies to the MEASURED side only. An item whose provider states a
|
|
50108
|
+
* `replace` verdict is low whatever this says — the provider decided, and a
|
|
50109
|
+
* threshold cannot talk it out of it (`alerts/consumable-band.ts`).
|
|
50110
|
+
*/
|
|
50111
|
+
consumableLowPercent: number().int().min(1).max(99).default(15),
|
|
50112
|
+
/**
|
|
49876
50113
|
* How long a still-true alert waits before it says itself again.
|
|
49877
50114
|
*
|
|
49878
50115
|
* **0 = say it once.** That is the behaviour of every other notification this
|
|
@@ -58518,7 +58755,9 @@ function stationaryEvidenceKey(deviceId, className, anchorId) {
|
|
|
58518
58755
|
* refused is the FIRST thing it failed, not the last.
|
|
58519
58756
|
*/
|
|
58520
58757
|
function evaluateStationaryEvidence(row, policy, classEnabled) {
|
|
58521
|
-
const
|
|
58758
|
+
const windowSeen = row.windowSeen + row.prevWindowSeen;
|
|
58759
|
+
const windowAgreed = row.windowAgreed + row.prevWindowAgreed;
|
|
58760
|
+
const ratio = windowSeen > 0 ? windowAgreed / windowSeen : 0;
|
|
58522
58761
|
const spanMs = row.lastAgreedAt - row.firstAgreedAt;
|
|
58523
58762
|
const base = {
|
|
58524
58763
|
agreed: row.agreed,
|
|
@@ -58552,6 +58791,36 @@ function evaluateStationaryEvidence(row, policy, classEnabled) {
|
|
|
58552
58791
|
};
|
|
58553
58792
|
}
|
|
58554
58793
|
/**
|
|
58794
|
+
* Slide the window forward to `now`.
|
|
58795
|
+
*
|
|
58796
|
+
* One bucket width past: the current bucket becomes the previous one and a
|
|
58797
|
+
* fresh one opens. TWO or more past: the candidate was not observed for the
|
|
58798
|
+
* whole window, so both buckets are empty — and an empty window answers `0`,
|
|
58799
|
+
* which is an honest "nothing recent to judge by" rather than a stale verdict.
|
|
58800
|
+
* The lifetime counters are untouched, so nothing a candidate has earned is
|
|
58801
|
+
* lost — only its claim about the present expires.
|
|
58802
|
+
*/
|
|
58803
|
+
function rotateWindow(row, now) {
|
|
58804
|
+
const elapsed = now - row.windowStartedAt;
|
|
58805
|
+
if (elapsed < 9e5) return row;
|
|
58806
|
+
if (elapsed < 2 * 9e5) return {
|
|
58807
|
+
...row,
|
|
58808
|
+
prevWindowAgreed: row.windowAgreed,
|
|
58809
|
+
prevWindowSeen: row.windowSeen,
|
|
58810
|
+
windowAgreed: 0,
|
|
58811
|
+
windowSeen: 0,
|
|
58812
|
+
windowStartedAt: now
|
|
58813
|
+
};
|
|
58814
|
+
return {
|
|
58815
|
+
...row,
|
|
58816
|
+
prevWindowAgreed: 0,
|
|
58817
|
+
prevWindowSeen: 0,
|
|
58818
|
+
windowAgreed: 0,
|
|
58819
|
+
windowSeen: 0,
|
|
58820
|
+
windowStartedAt: now
|
|
58821
|
+
};
|
|
58822
|
+
}
|
|
58823
|
+
/**
|
|
58555
58824
|
* The candidate this observation belongs to: the same-class row whose anchor it
|
|
58556
58825
|
* overlaps most, provided the overlap reaches `agreeIou`. Overlap is the ONLY
|
|
58557
58826
|
* notion of sameness here, and it is the same {@link iou} the suppression gate
|
|
@@ -58585,15 +58854,19 @@ function matchEvidenceCandidate(rows, className, bbox, agreeIou) {
|
|
|
58585
58854
|
*/
|
|
58586
58855
|
function foldObservation(row, observation, agrees, overlap) {
|
|
58587
58856
|
const seen = row.seen + 1;
|
|
58857
|
+
const rotated = rotateWindow(row, observation.timestamp);
|
|
58588
58858
|
if (!agrees) return {
|
|
58589
|
-
...
|
|
58859
|
+
...rotated,
|
|
58590
58860
|
seen,
|
|
58861
|
+
windowSeen: rotated.windowSeen + 1,
|
|
58591
58862
|
updatedAt: observation.timestamp
|
|
58592
58863
|
};
|
|
58593
58864
|
const improves = overlap >= iou(row.anchor, row.bestBbox);
|
|
58594
58865
|
return {
|
|
58595
|
-
...
|
|
58866
|
+
...rotated,
|
|
58596
58867
|
seen,
|
|
58868
|
+
windowSeen: rotated.windowSeen + 1,
|
|
58869
|
+
windowAgreed: rotated.windowAgreed + 1,
|
|
58597
58870
|
agreed: row.agreed + 1,
|
|
58598
58871
|
firstAgreedAt: Math.min(row.firstAgreedAt, observation.timestamp),
|
|
58599
58872
|
lastAgreedAt: Math.max(row.lastAgreedAt, observation.timestamp),
|
|
@@ -58617,6 +58890,11 @@ function newEvidenceRow(observation, anchorId) {
|
|
|
58617
58890
|
anchor: { ...observation.bbox },
|
|
58618
58891
|
agreed: 1,
|
|
58619
58892
|
seen: 1,
|
|
58893
|
+
windowAgreed: 1,
|
|
58894
|
+
windowSeen: 1,
|
|
58895
|
+
prevWindowAgreed: 0,
|
|
58896
|
+
prevWindowSeen: 0,
|
|
58897
|
+
windowStartedAt: observation.timestamp,
|
|
58620
58898
|
firstAgreedAt: observation.timestamp,
|
|
58621
58899
|
lastAgreedAt: observation.timestamp,
|
|
58622
58900
|
bestBbox: { ...observation.bbox },
|
|
@@ -58755,6 +59033,11 @@ var StationaryEvidenceLedger = class {
|
|
|
58755
59033
|
anchor: { ...row.anchor },
|
|
58756
59034
|
agreed: row.agreed,
|
|
58757
59035
|
seen: row.seen,
|
|
59036
|
+
windowAgreed: row.windowAgreed,
|
|
59037
|
+
windowSeen: row.windowSeen,
|
|
59038
|
+
prevWindowAgreed: row.prevWindowAgreed,
|
|
59039
|
+
prevWindowSeen: row.prevWindowSeen,
|
|
59040
|
+
windowStartedAt: row.windowStartedAt,
|
|
58758
59041
|
firstAgreedAt: row.firstAgreedAt,
|
|
58759
59042
|
lastAgreedAt: row.lastAgreedAt,
|
|
58760
59043
|
bestBbox: { ...row.bestBbox },
|
|
@@ -58783,6 +59066,11 @@ var StationaryEvidenceLedger = class {
|
|
|
58783
59066
|
anchor,
|
|
58784
59067
|
agreed: numberOr(data.agreed, 0),
|
|
58785
59068
|
seen: numberOr(data.seen, 0),
|
|
59069
|
+
windowAgreed: numberOr(data.windowAgreed, 0),
|
|
59070
|
+
windowSeen: numberOr(data.windowSeen, 0),
|
|
59071
|
+
prevWindowAgreed: numberOr(data.prevWindowAgreed, 0),
|
|
59072
|
+
prevWindowSeen: numberOr(data.prevWindowSeen, 0),
|
|
59073
|
+
windowStartedAt: numberOr(data.windowStartedAt, updatedAt),
|
|
58786
59074
|
firstAgreedAt: numberOr(data.firstAgreedAt, updatedAt),
|
|
58787
59075
|
lastAgreedAt: numberOr(data.lastAgreedAt, updatedAt),
|
|
58788
59076
|
bestBbox,
|
|
@@ -67487,7 +67775,8 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
|
|
|
67487
67775
|
EventCategory.UpdateAvailable,
|
|
67488
67776
|
EventCategory.AddonUpdated,
|
|
67489
67777
|
EventCategory.SystemRestartCompleted,
|
|
67490
|
-
EventCategory.RecordingExportCompleted
|
|
67778
|
+
EventCategory.RecordingExportCompleted,
|
|
67779
|
+
EventCategory.BackupRunCompleted
|
|
67491
67780
|
];
|
|
67492
67781
|
/**
|
|
67493
67782
|
* Install the addon's bus subscriptions. Identical set + relative order to the
|
|
@@ -70570,6 +70859,17 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
70570
70859
|
default: DEVICE_ALERT_DEFAULTS.batteryLowPercent,
|
|
70571
70860
|
unit: "%"
|
|
70572
70861
|
},
|
|
70862
|
+
{
|
|
70863
|
+
type: "number",
|
|
70864
|
+
key: "consumableLowPercent",
|
|
70865
|
+
label: "Low consumable at",
|
|
70866
|
+
description: "At or below this, a measured consumable — a filter, a brush, a mop pad — is nearly exhausted. Its own number rather than the battery’s: a battery at 20% is hours from flat, a brush at 20% is weeks from worn. An item whose device reports “replace” outright is announced whatever this says.",
|
|
70867
|
+
min: 1,
|
|
70868
|
+
max: 99,
|
|
70869
|
+
step: 1,
|
|
70870
|
+
default: DEVICE_ALERT_DEFAULTS.consumableLowPercent,
|
|
70871
|
+
unit: "%"
|
|
70872
|
+
},
|
|
70573
70873
|
{
|
|
70574
70874
|
type: "number",
|
|
70575
70875
|
key: "alertRenotifyMinutes",
|
|
@@ -72081,6 +72381,92 @@ var RawGlobalStoreMirror = class {
|
|
|
72081
72381
|
}
|
|
72082
72382
|
};
|
|
72083
72383
|
//#endregion
|
|
72384
|
+
//#region src/pipeline-analytics/summary/group-crop.ts
|
|
72385
|
+
/** Padding around the union, as a fraction of its own size. */
|
|
72386
|
+
var GROUP_CROP_PADDING = .12;
|
|
72387
|
+
/**
|
|
72388
|
+
* Smallest crop, per axis, as a fraction of the frame.
|
|
72389
|
+
*
|
|
72390
|
+
* A quarter. Below that a thumbnail stops being legible at the sizes the
|
|
72391
|
+
* gallery draws it, and the extra context costs nothing — the subjects stay the
|
|
72392
|
+
* dominant thing in the picture at this size.
|
|
72393
|
+
*/
|
|
72394
|
+
var MIN_CROP_FRACTION = .25;
|
|
72395
|
+
/**
|
|
72396
|
+
* The crop, or `null` when there is nothing to cut to — no visible member, or
|
|
72397
|
+
* a frame with no dimensions. `null` means the caller keeps the full frame,
|
|
72398
|
+
* which is the only honest answer when nobody could say where the group was.
|
|
72399
|
+
*/
|
|
72400
|
+
function groupCropRect(input) {
|
|
72401
|
+
const { frameWidth: fw, frameHeight: fh } = input;
|
|
72402
|
+
if (fw <= 0 || fh <= 0) return null;
|
|
72403
|
+
const boxes = input.boxes.filter((b) => b.width > 0 && b.height > 0);
|
|
72404
|
+
if (boxes.length === 0) return null;
|
|
72405
|
+
let left = Number.POSITIVE_INFINITY;
|
|
72406
|
+
let top = Number.POSITIVE_INFINITY;
|
|
72407
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
72408
|
+
let bottom = Number.NEGATIVE_INFINITY;
|
|
72409
|
+
for (const b of boxes) {
|
|
72410
|
+
left = Math.min(left, b.x);
|
|
72411
|
+
top = Math.min(top, b.y);
|
|
72412
|
+
right = Math.max(right, b.x + b.width);
|
|
72413
|
+
bottom = Math.max(bottom, b.y + b.height);
|
|
72414
|
+
}
|
|
72415
|
+
const padX = (right - left) * GROUP_CROP_PADDING;
|
|
72416
|
+
const padY = (bottom - top) * GROUP_CROP_PADDING;
|
|
72417
|
+
let x0 = left - padX;
|
|
72418
|
+
let y0 = top - padY;
|
|
72419
|
+
let x1 = right + padX;
|
|
72420
|
+
let y1 = bottom + padY;
|
|
72421
|
+
const minW = fw * MIN_CROP_FRACTION;
|
|
72422
|
+
const minH = fh * MIN_CROP_FRACTION;
|
|
72423
|
+
if (x1 - x0 < minW) {
|
|
72424
|
+
const cx = (x0 + x1) / 2;
|
|
72425
|
+
x0 = cx - minW / 2;
|
|
72426
|
+
x1 = cx + minW / 2;
|
|
72427
|
+
}
|
|
72428
|
+
if (y1 - y0 < minH) {
|
|
72429
|
+
const cy = (y0 + y1) / 2;
|
|
72430
|
+
y0 = cy - minH / 2;
|
|
72431
|
+
y1 = cy + minH / 2;
|
|
72432
|
+
}
|
|
72433
|
+
const width = Math.min(x1 - x0, fw);
|
|
72434
|
+
const height = Math.min(y1 - y0, fh);
|
|
72435
|
+
const x = Math.max(0, Math.min(x0, fw - width));
|
|
72436
|
+
const y = Math.max(0, Math.min(y0, fh - height));
|
|
72437
|
+
return {
|
|
72438
|
+
x: Math.round(x),
|
|
72439
|
+
y: Math.round(y),
|
|
72440
|
+
width: Math.round(width),
|
|
72441
|
+
height: Math.round(height)
|
|
72442
|
+
};
|
|
72443
|
+
}
|
|
72444
|
+
/**
|
|
72445
|
+
* Carry a crop computed on the DETECTION frame onto the NATIVE surface.
|
|
72446
|
+
*
|
|
72447
|
+
* The two are different sizes by construction: detection runs on a downscaled
|
|
72448
|
+
* frame and the group shot is fetched at up to 4096 px. Applying the rectangle
|
|
72449
|
+
* raw would cut a corner of the picture and call it the group — the failure
|
|
72450
|
+
* mode is silent and looks like a framing bug.
|
|
72451
|
+
*
|
|
72452
|
+
* Scaled per axis rather than by one factor, because the two surfaces are not
|
|
72453
|
+
* guaranteed to share an aspect, and clamped to the native frame so rounding
|
|
72454
|
+
* can never ask for a pixel that is not there.
|
|
72455
|
+
*/
|
|
72456
|
+
function scaleCropToFrame(crop, from, to) {
|
|
72457
|
+
if (from.width <= 0 || from.height <= 0 || to.width <= 0 || to.height <= 0) return null;
|
|
72458
|
+
const sx = to.width / from.width;
|
|
72459
|
+
const sy = to.height / from.height;
|
|
72460
|
+
const x = Math.max(0, Math.min(Math.round(crop.x * sx), to.width - 1));
|
|
72461
|
+
const y = Math.max(0, Math.min(Math.round(crop.y * sy), to.height - 1));
|
|
72462
|
+
return {
|
|
72463
|
+
x,
|
|
72464
|
+
y,
|
|
72465
|
+
width: Math.max(1, Math.min(Math.round(crop.width * sx), to.width - x)),
|
|
72466
|
+
height: Math.max(1, Math.min(Math.round(crop.height * sy), to.height - y))
|
|
72467
|
+
};
|
|
72468
|
+
}
|
|
72469
|
+
//#endregion
|
|
72084
72470
|
//#region src/pipeline-analytics/summary/summary-score.ts
|
|
72085
72471
|
/** A member whose own box is covered this much by others is hidden. */
|
|
72086
72472
|
var OCCLUDED_COVER_FRACTION = .5;
|
|
@@ -72129,12 +72515,24 @@ function scoreSummaryFrame(input) {
|
|
|
72129
72515
|
const visible = present.filter((d) => coveredFraction(d.bbox, present.filter((o) => o !== d).map((o) => o.bbox)) < OCCLUDED_COVER_FRACTION);
|
|
72130
72516
|
if (visible.length === 0) return {
|
|
72131
72517
|
visible: 0,
|
|
72132
|
-
quality: 0
|
|
72518
|
+
quality: 0,
|
|
72519
|
+
crop: null
|
|
72133
72520
|
};
|
|
72134
72521
|
const quality = visible.reduce((sum, d) => sum + memberQuality(d, input.frameWidth, input.frameHeight), 0) / visible.length;
|
|
72522
|
+
const crop = groupCropRect({
|
|
72523
|
+
boxes: visible.map((d) => ({
|
|
72524
|
+
x: d.bbox.x,
|
|
72525
|
+
y: d.bbox.y,
|
|
72526
|
+
width: d.bbox.width,
|
|
72527
|
+
height: d.bbox.height
|
|
72528
|
+
})),
|
|
72529
|
+
frameWidth: input.frameWidth,
|
|
72530
|
+
frameHeight: input.frameHeight
|
|
72531
|
+
});
|
|
72135
72532
|
return {
|
|
72136
72533
|
visible: visible.length,
|
|
72137
|
-
quality
|
|
72534
|
+
quality,
|
|
72535
|
+
crop
|
|
72138
72536
|
};
|
|
72139
72537
|
}
|
|
72140
72538
|
/**
|
|
@@ -72254,6 +72652,54 @@ var GroupShotTracker = class {
|
|
|
72254
72652
|
};
|
|
72255
72653
|
//#endregion
|
|
72256
72654
|
//#region src/pipeline-analytics/summary/group-shot-materialiser.ts
|
|
72655
|
+
/**
|
|
72656
|
+
* Apply the group crop to the native surface, or hand it back untouched.
|
|
72657
|
+
*
|
|
72658
|
+
* Every reason to skip is a reason NOT to guess: no rectangle, no detection
|
|
72659
|
+
* geometry to scale from, or a cropper this deployment does not have. The full
|
|
72660
|
+
* frame is worse than a crop and far better than a wrong crop.
|
|
72661
|
+
*/
|
|
72662
|
+
async function cropNative(native, input, deps) {
|
|
72663
|
+
const crop = input.crop;
|
|
72664
|
+
if (!crop || !deps.cropFrame) return native;
|
|
72665
|
+
const fw = input.cropFrameWidth;
|
|
72666
|
+
const fh = input.cropFrameHeight;
|
|
72667
|
+
if (fw === void 0 || fh === void 0) return native;
|
|
72668
|
+
const scaled = scaleCropToFrame(crop, {
|
|
72669
|
+
width: fw,
|
|
72670
|
+
height: fh
|
|
72671
|
+
}, native);
|
|
72672
|
+
if (scaled === null) return native;
|
|
72673
|
+
if (scaled.width >= native.width && scaled.height >= native.height) return native;
|
|
72674
|
+
try {
|
|
72675
|
+
const cut = await deps.cropFrame(native, scaled);
|
|
72676
|
+
if (cut === null) return native;
|
|
72677
|
+
const expected = cut.width * cut.height * 3;
|
|
72678
|
+
if (cut.format === "rgb" && cut.data.length !== expected) {
|
|
72679
|
+
deps.logger.warn("group shot crop returned a surface that is not its geometry", {
|
|
72680
|
+
tags: { deviceId: input.deviceId },
|
|
72681
|
+
meta: {
|
|
72682
|
+
summaryId: input.summaryId,
|
|
72683
|
+
width: cut.width,
|
|
72684
|
+
height: cut.height,
|
|
72685
|
+
expectedBytes: expected,
|
|
72686
|
+
gotBytes: cut.data.length
|
|
72687
|
+
}
|
|
72688
|
+
});
|
|
72689
|
+
return native;
|
|
72690
|
+
}
|
|
72691
|
+
return cut;
|
|
72692
|
+
} catch (err) {
|
|
72693
|
+
deps.logger.warn("group shot crop failed — storing the full frame", {
|
|
72694
|
+
tags: { deviceId: input.deviceId },
|
|
72695
|
+
meta: {
|
|
72696
|
+
summaryId: input.summaryId,
|
|
72697
|
+
error: errMsg$1(err)
|
|
72698
|
+
}
|
|
72699
|
+
});
|
|
72700
|
+
return native;
|
|
72701
|
+
}
|
|
72702
|
+
}
|
|
72257
72703
|
async function persistSummaryGroupShot(deps, input) {
|
|
72258
72704
|
const { deviceId, summaryId, frameHandle } = input;
|
|
72259
72705
|
const drop = {
|
|
@@ -72287,7 +72733,8 @@ async function persistSummaryGroupShot(deps, input) {
|
|
|
72287
72733
|
});
|
|
72288
72734
|
return { kind: "ram-tier" };
|
|
72289
72735
|
}
|
|
72290
|
-
const
|
|
72736
|
+
const surface = await cropNative(native, input, deps);
|
|
72737
|
+
const { keyFrame, keyFrameSmall } = await deps.encodeVariants(surface);
|
|
72291
72738
|
const keyFrameMediaId = await deps.media.putReplacing({
|
|
72292
72739
|
deviceId,
|
|
72293
72740
|
ownerKind: "summary",
|
|
@@ -76163,6 +76610,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
76163
76610
|
},
|
|
76164
76611
|
readDeviceAlertSettings: () => ({
|
|
76165
76612
|
batteryLowPercent: this.deviceAlertSettingsMirror.batteryLowPercent,
|
|
76613
|
+
consumableLowPercent: this.deviceAlertSettingsMirror.consumableLowPercent,
|
|
76166
76614
|
renotifyMinutes: this.deviceAlertSettingsMirror.alertRenotifyMinutes,
|
|
76167
76615
|
notifyRecovery: this.deviceAlertSettingsMirror.alertNotifyRecovery
|
|
76168
76616
|
}),
|
|
@@ -79506,13 +79954,30 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
79506
79954
|
fetchNativeFullFrame: getNative,
|
|
79507
79955
|
encodeVariants: encodeKeyFrameVariants,
|
|
79508
79956
|
media: mediaStore,
|
|
79509
|
-
logger: this.ctx.logger
|
|
79957
|
+
logger: this.ctx.logger,
|
|
79958
|
+
cropFrame: async (frame, rect) => {
|
|
79959
|
+
const out = await extractCropRaw(frame.data, frame.width, frame.height, {
|
|
79960
|
+
x: rect.x / frame.width,
|
|
79961
|
+
y: rect.y / frame.height,
|
|
79962
|
+
w: rect.width / frame.width,
|
|
79963
|
+
h: rect.height / frame.height
|
|
79964
|
+
});
|
|
79965
|
+
return {
|
|
79966
|
+
...frame,
|
|
79967
|
+
data: out.data,
|
|
79968
|
+
width: out.width,
|
|
79969
|
+
height: out.height
|
|
79970
|
+
};
|
|
79971
|
+
}
|
|
79510
79972
|
}, {
|
|
79511
79973
|
deviceId,
|
|
79512
79974
|
summaryId,
|
|
79513
79975
|
timestamp: result.timestamp,
|
|
79514
79976
|
frameHandle,
|
|
79515
|
-
maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
|
|
79977
|
+
maxWidth: KEYFRAME_NATIVE_MAX_WIDTH,
|
|
79978
|
+
crop: score.crop,
|
|
79979
|
+
cropFrameWidth: result.frameWidth,
|
|
79980
|
+
cropFrameHeight: result.frameHeight
|
|
79516
79981
|
})
|
|
79517
79982
|
}).then((outcome) => {
|
|
79518
79983
|
if (outcome?.kind === "landed") {
|