@camstack/addon-pipeline 1.2.203 → 1.2.205
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/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{default-detection-model-ClY8fk-K.mjs → default-detection-model-23wBHkOQ.mjs} +1 -1
- package/dist/{default-detection-model-CfIEdJop.js → default-detection-model-BGsoboIK.js} +1 -1
- package/dist/detection-pipeline/index.js +5 -5
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-Czm3CW8M.js → dist-CR3_dWir.js} +17 -7
- package/dist/{dist-B5x2ogXC.mjs → dist-FaCzok3C.mjs} +17 -7
- package/dist/{lazy-sharp-AhHg_Hb-.js → lazy-sharp-CKXcDP-V.js} +1 -1
- package/dist/motion-wasm/index.js +296 -18
- package/dist/motion-wasm/index.mjs +295 -17
- package/dist/{node-u0M7kQe-.js → node-Re3uLz8K.js} +1 -1
- package/dist/{node-Bx76sJbU.mjs → node-f4fUlGHQ.mjs} +1 -1
- package/dist/pipeline-runner/index.js +5 -5
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/{process-memory-D5JcMSDA.js → process-memory-DNheSoSc.js} +1 -1
- package/dist/{process-memory-iiE4Q2mt.mjs → process-memory-MC7wcxqk.mjs} +1 -1
- package/dist/recorder/index.js +61 -13
- package/dist/recorder/index.mjs +61 -13
- package/dist/{segment-demux-js-CSt1T2Vi.js → segment-demux-js-CQYpP1SU.js} +1 -1
- package/dist/{segment-demux-js-BcbB_hcJ.mjs → segment-demux-js-DUpovUKi.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BSWELhoJ.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D_m12d-s.mjs} +3 -3
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-BJlv_j8i.mjs → hostInit-Dfu865rx.mjs} +3 -3
- package/dist/stream-broker/index.js +3 -3
- package/dist/stream-broker/index.mjs +3 -3
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-ClwpoGoP.mjs → worker-protocol-Dz6_u2y6.mjs} +1 -1
- package/dist/{worker-protocol-B4C4seRh.js → worker-protocol-lIjbPNbH.js} +1 -1
- package/package.json +1 -1
|
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_process_memory = require("../process-memory-
|
|
5
|
+
const require_dist = require("../dist-CR3_dWir.js");
|
|
6
|
+
const require_process_memory = require("../process-memory-DNheSoSc.js");
|
|
7
7
|
let node_fs = require("node:fs");
|
|
8
8
|
node_fs = require_dist.__toESM(node_fs);
|
|
9
9
|
let node_path = require("node:path");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as __require } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { Dt as resolvePoolMemoryPolicy, It as errMsg, Jt as hydrateSchema, M as audioAnalyzerCapability, S as PoolMemoryWatchdog, Ut as BaseAddon, j as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, ot as mapAudioLabelToMacro, s as DEFAULT_AUDIO_ANALYZER_CONFIG, v as HF_BASE_URL } from "../dist-
|
|
3
|
-
import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-
|
|
2
|
+
import { Dt as resolvePoolMemoryPolicy, It as errMsg, Jt as hydrateSchema, M as audioAnalyzerCapability, S as PoolMemoryWatchdog, Ut as BaseAddon, j as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, ot as mapAudioLabelToMacro, s as DEFAULT_AUDIO_ANALYZER_CONFIG, v as HF_BASE_URL } from "../dist-FaCzok3C.mjs";
|
|
3
|
+
import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-MC7wcxqk.mjs";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path$1 from "node:path";
|
|
6
6
|
import { downloadFile } from "@camstack/system/addon-utils";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { N as audioKindId, a as COCO_80_LABELS, o as COCO_TO_MACRO, r as AUDIO_MACRO_LABELS, tt as hfModelUrl } from "./dist-
|
|
1
|
+
import { N as audioKindId, a as COCO_80_LABELS, o as COCO_TO_MACRO, r as AUDIO_MACRO_LABELS, tt as hfModelUrl } from "./dist-FaCzok3C.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
//#region src/detection-pipeline/registry/model-catalogs.ts
|
|
4
4
|
var HF_REPO = "camstack/camstack-models";
|
|
@@ -2,12 +2,12 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_node = require("../node-
|
|
7
|
-
const require_default_detection_model = require("../default-detection-model-
|
|
8
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
5
|
+
const require_dist = require("../dist-CR3_dWir.js");
|
|
6
|
+
const require_node = require("../node-Re3uLz8K.js");
|
|
7
|
+
const require_default_detection_model = require("../default-detection-model-BGsoboIK.js");
|
|
8
|
+
const require_lazy_sharp = require("../lazy-sharp-CKXcDP-V.js");
|
|
9
9
|
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
|
|
10
|
-
const require_process_memory = require("../process-memory-
|
|
10
|
+
const require_process_memory = require("../process-memory-DNheSoSc.js");
|
|
11
11
|
let node_os = require("node:os");
|
|
12
12
|
node_os = require_dist.__toESM(node_os);
|
|
13
13
|
let node_fs = require("node:fs");
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { n as __require } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $t as parseJsonUnknown, B as defaultDeviceFor$1, Dt as resolvePoolMemoryPolicy, G as detectionPipelineCapability, It as errMsg, J as enumerateInferenceDevices, Jt as hydrateSchema, Pt as supportedRuntimes$1, Qt as nodePin, S as PoolMemoryWatchdog, Ut as BaseAddon, X as evaluateZoneRules, c as DEFAULT_CLUSTER_STEP_MODELS, gt as pickClusterStepSettings, hn as EventCategory, ht as pickClusterStepModels, in as array, it as loadContributionCapability, k as YAMNET_TO_MACRO, kt as runtimeDevices$1, l as DEFAULT_CLUSTER_STEP_SETTINGS, m as DEVICE_BACKEND_TO_FORMAT, mn as union, nn as sleep, nt as inferModelProvider, pn as string, pt as overlayClusterStepSettings, qt as createEvent, t as APPLE_SA_TO_MACRO, un as object, wt as resolveClusterStepModelId, yt as pipelineExecutorCapability } from "../dist-
|
|
3
|
-
import { o as readProcessCost } from "../node-
|
|
4
|
-
import { a as ALL_PIPELINE_STEPS, c as getStep, d as landmarkPrecisionVerdict, l as getStepDefinition, n as resolveDefaultDetectionModel, o as ALL_STEPS, r as localFrameRegistry, s as getDefaultModelForFormat, u as resolveModelForFormat } from "../default-detection-model-
|
|
2
|
+
import { $t as parseJsonUnknown, B as defaultDeviceFor$1, Dt as resolvePoolMemoryPolicy, G as detectionPipelineCapability, It as errMsg, J as enumerateInferenceDevices, Jt as hydrateSchema, Pt as supportedRuntimes$1, Qt as nodePin, S as PoolMemoryWatchdog, Ut as BaseAddon, X as evaluateZoneRules, c as DEFAULT_CLUSTER_STEP_MODELS, gt as pickClusterStepSettings, hn as EventCategory, ht as pickClusterStepModels, in as array, it as loadContributionCapability, k as YAMNET_TO_MACRO, kt as runtimeDevices$1, l as DEFAULT_CLUSTER_STEP_SETTINGS, m as DEVICE_BACKEND_TO_FORMAT, mn as union, nn as sleep, nt as inferModelProvider, pn as string, pt as overlayClusterStepSettings, qt as createEvent, t as APPLE_SA_TO_MACRO, un as object, wt as resolveClusterStepModelId, yt as pipelineExecutorCapability } from "../dist-FaCzok3C.mjs";
|
|
3
|
+
import { o as readProcessCost } from "../node-f4fUlGHQ.mjs";
|
|
4
|
+
import { a as ALL_PIPELINE_STEPS, c as getStep, d as landmarkPrecisionVerdict, l as getStepDefinition, n as resolveDefaultDetectionModel, o as ALL_STEPS, r as localFrameRegistry, s as getDefaultModelForFormat, u as resolveModelForFormat } from "../default-detection-model-23wBHkOQ.mjs";
|
|
5
5
|
import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
|
|
6
6
|
import { n as startEventLoopStallMonitor } from "../event-loop-stall-monitor-DXLNBMkY.mjs";
|
|
7
|
-
import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-
|
|
7
|
+
import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-MC7wcxqk.mjs";
|
|
8
8
|
import * as os from "node:os";
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as path$1 from "node:path";
|
|
@@ -14074,6 +14074,9 @@ var QueryFilterSchema = object({
|
|
|
14074
14074
|
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
14075
14075
|
/** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
|
|
14076
14076
|
whereNot: record(string(), unknown()).optional(),
|
|
14077
|
+
/** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
|
|
14078
|
+
* empty list excludes nothing. See `QueryFilter.whereNotIn`. */
|
|
14079
|
+
whereNotIn: record(string(), array(unknown())).optional(),
|
|
14077
14080
|
orderBy: object({
|
|
14078
14081
|
field: string(),
|
|
14079
14082
|
direction: _enum(["asc", "desc"])
|
|
@@ -14094,7 +14097,8 @@ var MutationFilterSchema = object({
|
|
|
14094
14097
|
where: record(string(), unknown()).optional(),
|
|
14095
14098
|
whereIn: record(string(), array(unknown())).optional(),
|
|
14096
14099
|
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
14097
|
-
whereNot: record(string(), unknown()).optional()
|
|
14100
|
+
whereNot: record(string(), unknown()).optional(),
|
|
14101
|
+
whereNotIn: record(string(), array(unknown())).optional()
|
|
14098
14102
|
});
|
|
14099
14103
|
/**
|
|
14100
14104
|
* One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
|
|
@@ -21196,10 +21200,12 @@ var TrackSourceSchema = _enum([
|
|
|
21196
21200
|
* Terminal for the plain `markForTrain` toggle: returning it to `staging` is
|
|
21197
21201
|
* a deliberate action of the retrain page, not a side effect of a checkbox.
|
|
21198
21202
|
*
|
|
21199
|
-
* There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'`
|
|
21200
|
-
*
|
|
21201
|
-
*
|
|
21202
|
-
*
|
|
21203
|
+
* There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` so that
|
|
21204
|
+
* every row is selectable by a positive predicate. (The filter language has
|
|
21205
|
+
* since grown the NULL-safe `whereNot` / `whereNotIn` and an `IS NULL` reading
|
|
21206
|
+
* of `where: { f: null }`, which is how {@link TrackSourceSchema} can be
|
|
21207
|
+
* filtered on a NULLABLE column — see `excludeSources`. It did not when this
|
|
21208
|
+
* column was designed, and a NOT NULL column is still the better shape.)
|
|
21203
21209
|
*/
|
|
21204
21210
|
var RetrainStatusSchema = _enum([
|
|
21205
21211
|
"none",
|
|
@@ -21935,7 +21941,9 @@ var RecentTracksQueryInput = object({
|
|
|
21935
21941
|
* whose class list is unreadable are kept, and the client's rule stays the
|
|
21936
21942
|
* exact one.
|
|
21937
21943
|
*/
|
|
21938
|
-
classes: array(string()).optional()
|
|
21944
|
+
classes: array(string()).optional(),
|
|
21945
|
+
/** See {@link ExcludeSourcesDoc}. */
|
|
21946
|
+
excludeSources: array(TrackSourceSchema).optional()
|
|
21939
21947
|
});
|
|
21940
21948
|
/**
|
|
21941
21949
|
* A Summary (D359): the per-camera session envelope that references tracks.
|
|
@@ -22363,7 +22371,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
22363
22371
|
* selected is an operator who has not narrowed anything, and an empty
|
|
22364
22372
|
* timeline would read as a camera that saw nothing.
|
|
22365
22373
|
*/
|
|
22366
|
-
classes: array(string()).optional()
|
|
22374
|
+
classes: array(string()).optional(),
|
|
22375
|
+
/** See {@link ExcludeSourcesDoc}. */
|
|
22376
|
+
excludeSources: array(TrackSourceSchema).optional()
|
|
22367
22377
|
}), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(SummariesQueryInput, SummariesPageSchema), method(object({ id: string() }), SummaryDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
|
|
22368
22378
|
kind: "mutation",
|
|
22369
22379
|
auth: "admin"
|
|
@@ -14051,6 +14051,9 @@ var QueryFilterSchema = object({
|
|
|
14051
14051
|
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
14052
14052
|
/** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
|
|
14053
14053
|
whereNot: record(string(), unknown()).optional(),
|
|
14054
|
+
/** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
|
|
14055
|
+
* empty list excludes nothing. See `QueryFilter.whereNotIn`. */
|
|
14056
|
+
whereNotIn: record(string(), array(unknown())).optional(),
|
|
14054
14057
|
orderBy: object({
|
|
14055
14058
|
field: string(),
|
|
14056
14059
|
direction: _enum(["asc", "desc"])
|
|
@@ -14071,7 +14074,8 @@ var MutationFilterSchema = object({
|
|
|
14071
14074
|
where: record(string(), unknown()).optional(),
|
|
14072
14075
|
whereIn: record(string(), array(unknown())).optional(),
|
|
14073
14076
|
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
14074
|
-
whereNot: record(string(), unknown()).optional()
|
|
14077
|
+
whereNot: record(string(), unknown()).optional(),
|
|
14078
|
+
whereNotIn: record(string(), array(unknown())).optional()
|
|
14075
14079
|
});
|
|
14076
14080
|
/**
|
|
14077
14081
|
* One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
|
|
@@ -21173,10 +21177,12 @@ var TrackSourceSchema = _enum([
|
|
|
21173
21177
|
* Terminal for the plain `markForTrain` toggle: returning it to `staging` is
|
|
21174
21178
|
* a deliberate action of the retrain page, not a side effect of a checkbox.
|
|
21175
21179
|
*
|
|
21176
|
-
* There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'`
|
|
21177
|
-
*
|
|
21178
|
-
*
|
|
21179
|
-
*
|
|
21180
|
+
* There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` so that
|
|
21181
|
+
* every row is selectable by a positive predicate. (The filter language has
|
|
21182
|
+
* since grown the NULL-safe `whereNot` / `whereNotIn` and an `IS NULL` reading
|
|
21183
|
+
* of `where: { f: null }`, which is how {@link TrackSourceSchema} can be
|
|
21184
|
+
* filtered on a NULLABLE column — see `excludeSources`. It did not when this
|
|
21185
|
+
* column was designed, and a NOT NULL column is still the better shape.)
|
|
21180
21186
|
*/
|
|
21181
21187
|
var RetrainStatusSchema = _enum([
|
|
21182
21188
|
"none",
|
|
@@ -21912,7 +21918,9 @@ var RecentTracksQueryInput = object({
|
|
|
21912
21918
|
* whose class list is unreadable are kept, and the client's rule stays the
|
|
21913
21919
|
* exact one.
|
|
21914
21920
|
*/
|
|
21915
|
-
classes: array(string()).optional()
|
|
21921
|
+
classes: array(string()).optional(),
|
|
21922
|
+
/** See {@link ExcludeSourcesDoc}. */
|
|
21923
|
+
excludeSources: array(TrackSourceSchema).optional()
|
|
21916
21924
|
});
|
|
21917
21925
|
/**
|
|
21918
21926
|
* A Summary (D359): the per-camera session envelope that references tracks.
|
|
@@ -22340,7 +22348,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
22340
22348
|
* selected is an operator who has not narrowed anything, and an empty
|
|
22341
22349
|
* timeline would read as a camera that saw nothing.
|
|
22342
22350
|
*/
|
|
22343
|
-
classes: array(string()).optional()
|
|
22351
|
+
classes: array(string()).optional(),
|
|
22352
|
+
/** See {@link ExcludeSourcesDoc}. */
|
|
22353
|
+
excludeSources: array(TrackSourceSchema).optional()
|
|
22344
22354
|
}), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(SummariesQueryInput, SummariesPageSchema), method(object({ id: string() }), SummaryDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
|
|
22345
22355
|
kind: "mutation",
|
|
22346
22356
|
auth: "admin"
|
|
@@ -2,10 +2,185 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
5
|
+
const require_dist = require("../dist-CR3_dWir.js");
|
|
6
|
+
const require_lazy_sharp = require("../lazy-sharp-CKXcDP-V.js");
|
|
7
7
|
let node_fs = require("node:fs");
|
|
8
8
|
let node_path = require("node:path");
|
|
9
|
+
//#region src/motion-wasm/region-grouping.ts
|
|
10
|
+
/**
|
|
11
|
+
* Default area bar, as a fraction of the motion view.
|
|
12
|
+
*
|
|
13
|
+
* Chosen from the measured separation between true-positive group areas and
|
|
14
|
+
* quiet-footage group areas — see `docs/benchmarks/motion-region-area.md`.
|
|
15
|
+
* On the 320×180 production motion view this is ~490 px, against the 1500 px
|
|
16
|
+
* absolute filter it replaces.
|
|
17
|
+
*/
|
|
18
|
+
var DEFAULT_MIN_AREA_FRACTION = .0085;
|
|
19
|
+
/**
|
|
20
|
+
* The DEFAULT is the rule that shipped before D394: a 1500 px absolute bar per
|
|
21
|
+
* component. The measured rule is per-camera opt-in (`minArea: 0`), because
|
|
22
|
+
* the measurement says duty cycle rises between ×1.45 and ×2.82 and ten fleet
|
|
23
|
+
* cameras turn that into recorded hours that cannot be measured from their own
|
|
24
|
+
* footage. The end state is still one relative constant for the fleet; the
|
|
25
|
+
* path there is one camera at a time.
|
|
26
|
+
*/
|
|
27
|
+
var DEFAULT_MIN_AREA = 1500;
|
|
28
|
+
/**
|
|
29
|
+
* Default storm bar. A group of changed pixels covering more than this
|
|
30
|
+
* fraction of the view is not a subject — it is weather, an exposure step or
|
|
31
|
+
* an IR cut. See the storm section of `docs/benchmarks/motion-region-area.md`.
|
|
32
|
+
*/
|
|
33
|
+
var DEFAULT_MAX_COVERAGE_FRACTION = .5;
|
|
34
|
+
/** The bar in pixels: the absolute one when set, the relative rule otherwise. */
|
|
35
|
+
function resolveMinGroupArea(config) {
|
|
36
|
+
if (config.minArea > 0) return config.minArea;
|
|
37
|
+
const area = Math.max(0, config.viewWidth) * Math.max(0, config.viewHeight);
|
|
38
|
+
return Math.round(config.minAreaFraction * area);
|
|
39
|
+
}
|
|
40
|
+
function grow(region, radius) {
|
|
41
|
+
const cx = region.x + region.w / 2;
|
|
42
|
+
const cy = region.y + region.h / 2;
|
|
43
|
+
const halfW = Math.max(region.w, radius) / 2;
|
|
44
|
+
const halfH = Math.max(region.h, radius) / 2;
|
|
45
|
+
return {
|
|
46
|
+
x0: cx - halfW,
|
|
47
|
+
y0: cy - halfH,
|
|
48
|
+
x1: cx + halfW,
|
|
49
|
+
y1: cy + halfH
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function overlaps(a, b) {
|
|
53
|
+
return a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Merge fragments into subjects and judge each subject on its own summed area.
|
|
57
|
+
*
|
|
58
|
+
* Groups come back in the order of their first member, so the frame's reading
|
|
59
|
+
* order is stable across calls — a UI overlay does not reshuffle.
|
|
60
|
+
*
|
|
61
|
+
* A group over `maxCoverageFraction` is a storm and does not pass as a group;
|
|
62
|
+
* its components are then judged individually, so a subject the storm merged
|
|
63
|
+
* with is not silenced along with it.
|
|
64
|
+
*
|
|
65
|
+
* A positive `config.minArea` short-circuits all of it and runs the pre-D394
|
|
66
|
+
* rule instead — see {@link RegionGroupingConfig.minArea}.
|
|
67
|
+
*/
|
|
68
|
+
function groupMotionRegions(regions, config) {
|
|
69
|
+
if (regions.length === 0) return {
|
|
70
|
+
groups: [],
|
|
71
|
+
passed: [],
|
|
72
|
+
suppressedByCoverage: 0
|
|
73
|
+
};
|
|
74
|
+
if (config.minArea > 0) {
|
|
75
|
+
const legacy = regions.map((r) => ({
|
|
76
|
+
x: r.x,
|
|
77
|
+
y: r.y,
|
|
78
|
+
w: r.w,
|
|
79
|
+
h: r.h,
|
|
80
|
+
pixels: r.pixels,
|
|
81
|
+
members: 1
|
|
82
|
+
}));
|
|
83
|
+
return {
|
|
84
|
+
groups: legacy,
|
|
85
|
+
passed: legacy.filter((g) => g.pixels >= config.minArea),
|
|
86
|
+
suppressedByCoverage: 0
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const radius = Math.max(1, Math.floor(Math.min(config.viewWidth, config.viewHeight) / Math.max(1, config.mergeRadiusDivisor)));
|
|
90
|
+
const grown = regions.map((r) => grow(r, radius));
|
|
91
|
+
const parent = regions.map((_, i) => i);
|
|
92
|
+
const find = (i) => {
|
|
93
|
+
let root = i;
|
|
94
|
+
while (parent[root] !== root) root = parent[root];
|
|
95
|
+
let cur = i;
|
|
96
|
+
while (parent[cur] !== root) {
|
|
97
|
+
const next = parent[cur];
|
|
98
|
+
parent[cur] = root;
|
|
99
|
+
cur = next;
|
|
100
|
+
}
|
|
101
|
+
return root;
|
|
102
|
+
};
|
|
103
|
+
for (let i = 0; i < regions.length; i++) for (let j = i + 1; j < regions.length; j++) {
|
|
104
|
+
if (!overlaps(grown[i], grown[j])) continue;
|
|
105
|
+
const ri = find(i);
|
|
106
|
+
const rj = find(j);
|
|
107
|
+
if (ri !== rj) parent[rj] = ri;
|
|
108
|
+
}
|
|
109
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
110
|
+
const order = [];
|
|
111
|
+
for (let i = 0; i < regions.length; i++) {
|
|
112
|
+
const region = regions[i];
|
|
113
|
+
const root = find(i);
|
|
114
|
+
const existing = byRoot.get(root);
|
|
115
|
+
if (!existing) {
|
|
116
|
+
order.push(root);
|
|
117
|
+
byRoot.set(root, {
|
|
118
|
+
x0: region.x,
|
|
119
|
+
y0: region.y,
|
|
120
|
+
x1: region.x + region.w,
|
|
121
|
+
y1: region.y + region.h,
|
|
122
|
+
pixels: region.pixels,
|
|
123
|
+
members: 1
|
|
124
|
+
});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
existing.x0 = Math.min(existing.x0, region.x);
|
|
128
|
+
existing.y0 = Math.min(existing.y0, region.y);
|
|
129
|
+
existing.x1 = Math.max(existing.x1, region.x + region.w);
|
|
130
|
+
existing.y1 = Math.max(existing.y1, region.y + region.h);
|
|
131
|
+
existing.pixels += region.pixels;
|
|
132
|
+
existing.members += 1;
|
|
133
|
+
}
|
|
134
|
+
const groups = order.map((root) => {
|
|
135
|
+
const a = byRoot.get(root);
|
|
136
|
+
return {
|
|
137
|
+
x: a.x0,
|
|
138
|
+
y: a.y0,
|
|
139
|
+
w: a.x1 - a.x0,
|
|
140
|
+
h: a.y1 - a.y0,
|
|
141
|
+
pixels: a.pixels,
|
|
142
|
+
members: a.members
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
const bar = resolveMinGroupArea(config);
|
|
146
|
+
const viewArea = Math.max(0, config.viewWidth) * Math.max(0, config.viewHeight);
|
|
147
|
+
const stormBar = config.maxCoverageFraction >= 1 ? Infinity : config.maxCoverageFraction * viewArea;
|
|
148
|
+
const membersByGroup = /* @__PURE__ */ new Map();
|
|
149
|
+
for (let i = 0; i < regions.length; i++) {
|
|
150
|
+
const root = find(i);
|
|
151
|
+
const list = membersByGroup.get(root) ?? [];
|
|
152
|
+
list.push(regions[i]);
|
|
153
|
+
membersByGroup.set(root, list);
|
|
154
|
+
}
|
|
155
|
+
const passed = [];
|
|
156
|
+
let suppressedByCoverage = 0;
|
|
157
|
+
for (let g = 0; g < groups.length; g++) {
|
|
158
|
+
const group = groups[g];
|
|
159
|
+
if (group.pixels < bar) continue;
|
|
160
|
+
if (group.pixels <= stormBar) {
|
|
161
|
+
passed.push(group);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
suppressedByCoverage += 1;
|
|
165
|
+
for (const member of membersByGroup.get(order[g]) ?? []) {
|
|
166
|
+
if (member.pixels < bar || member.pixels > stormBar) continue;
|
|
167
|
+
passed.push({
|
|
168
|
+
x: member.x,
|
|
169
|
+
y: member.y,
|
|
170
|
+
w: member.w,
|
|
171
|
+
h: member.h,
|
|
172
|
+
pixels: member.pixels,
|
|
173
|
+
members: 1
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
groups,
|
|
179
|
+
passed,
|
|
180
|
+
suppressedByCoverage
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
9
184
|
//#region src/motion-wasm/wasm-motion-detector.ts
|
|
10
185
|
/**
|
|
11
186
|
* WasmMotionDetector — loads the AssemblyScript WASM module and provides
|
|
@@ -13,18 +188,33 @@ let node_path = require("node:path");
|
|
|
13
188
|
*
|
|
14
189
|
* Pipeline: blur → diff → threshold → dilate → CCL → bounding boxes
|
|
15
190
|
* All in one WASM call, ~3ms for 640×360 grayscale frames.
|
|
191
|
+
*
|
|
192
|
+
* The area filter that follows is NOT applied per connected component — see
|
|
193
|
+
* {@link groupMotionRegions}. Fragments of one subject are merged first and the
|
|
194
|
+
* bar is applied to the merged subject, relative to the motion view. D394.
|
|
195
|
+
*/
|
|
196
|
+
/**
|
|
197
|
+
* The one place the motion defaults live. The addon's schema defaults read
|
|
198
|
+
* from here — two copies of this object drifted apart once already (3000 here
|
|
199
|
+
* against 1500 in the addon), and the addon's copy is the one that ran.
|
|
16
200
|
*/
|
|
17
|
-
var DEFAULT_CONFIG
|
|
201
|
+
var DEFAULT_CONFIG = {
|
|
18
202
|
threshold: 45,
|
|
19
203
|
blurRadius: 1,
|
|
20
204
|
dilateRadius: 4,
|
|
21
|
-
minArea:
|
|
205
|
+
minArea: DEFAULT_MIN_AREA,
|
|
206
|
+
minAreaFraction: DEFAULT_MIN_AREA_FRACTION,
|
|
207
|
+
mergeRadiusDivisor: 6,
|
|
208
|
+
maxCoverageFraction: DEFAULT_MAX_COVERAGE_FRACTION
|
|
22
209
|
};
|
|
23
210
|
var WasmMotionDetector = class {
|
|
24
211
|
wasm = null;
|
|
25
212
|
prevOffset = 0;
|
|
26
213
|
currOffset = 0;
|
|
27
214
|
regionOffset = 0;
|
|
215
|
+
/** The motion view the regions are expressed in — the relative bar's denominator. */
|
|
216
|
+
width = 0;
|
|
217
|
+
height = 0;
|
|
28
218
|
/** Load the WASM module. Call once before detect(). */
|
|
29
219
|
async load() {
|
|
30
220
|
const wasmBytes = (0, node_fs.readFileSync)((0, node_path.join)(__dirname, "..", "..", "wasm", "motion.wasm"));
|
|
@@ -48,10 +238,25 @@ var WasmMotionDetector = class {
|
|
|
48
238
|
}
|
|
49
239
|
this.wasm = exports;
|
|
50
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Initialize for these frame dimensions when they are not already the ones
|
|
243
|
+
* the WASM is sized for. One detector instance serves every camera on the
|
|
244
|
+
* node, so two cameras with different views interleaving would otherwise
|
|
245
|
+
* leave the WASM sized for whichever one called `init` last — and, since
|
|
246
|
+
* D394, would divide the relative area bar by the wrong view. `init` only
|
|
247
|
+
* recomputes offsets and grows memory, so a call that changes nothing is
|
|
248
|
+
* skipped and a call that does is cheap.
|
|
249
|
+
*/
|
|
250
|
+
ensureSize(w, h) {
|
|
251
|
+
if (this.width === w && this.height === h) return;
|
|
252
|
+
this.init(w, h);
|
|
253
|
+
}
|
|
51
254
|
/** Initialize for given frame dimensions. Call when resolution changes. */
|
|
52
255
|
init(w, h) {
|
|
53
256
|
if (!this.wasm) throw new Error("WASM not loaded — call load() first");
|
|
54
257
|
this.wasm.init(w, h);
|
|
258
|
+
this.width = w;
|
|
259
|
+
this.height = h;
|
|
55
260
|
this.prevOffset = this.wasm.getPrevOffset();
|
|
56
261
|
this.currOffset = this.wasm.getCurrOffset();
|
|
57
262
|
this.regionOffset = this.wasm.getRegionOffset();
|
|
@@ -62,12 +267,12 @@ var WasmMotionDetector = class {
|
|
|
62
267
|
* @param prevGray - previous frame (Uint8Array, width×height)
|
|
63
268
|
* @param currGray - current frame (Uint8Array, width×height)
|
|
64
269
|
* @param config - detection parameters (optional, uses defaults)
|
|
65
|
-
* @returns raw
|
|
270
|
+
* @returns raw CCL regions, the subjects they merge into, and the survivors
|
|
66
271
|
*/
|
|
67
272
|
detect(prevGray, currGray, config = {}) {
|
|
68
273
|
if (!this.wasm) throw new Error("WASM not loaded");
|
|
69
274
|
const cfg = {
|
|
70
|
-
...DEFAULT_CONFIG
|
|
275
|
+
...DEFAULT_CONFIG,
|
|
71
276
|
...config
|
|
72
277
|
};
|
|
73
278
|
const mem = new Uint8Array(this.wasm.memory.buffer);
|
|
@@ -76,7 +281,9 @@ var WasmMotionDetector = class {
|
|
|
76
281
|
const numRegions = this.wasm.detectMotion(cfg.threshold, cfg.blurRadius, cfg.dilateRadius, 0);
|
|
77
282
|
if (numRegions === 0) return {
|
|
78
283
|
raw: [],
|
|
79
|
-
filtered: []
|
|
284
|
+
filtered: [],
|
|
285
|
+
groups: [],
|
|
286
|
+
suppressedByCoverage: 0
|
|
80
287
|
};
|
|
81
288
|
const view = new DataView(this.wasm.memory.buffer);
|
|
82
289
|
const raw = [];
|
|
@@ -90,9 +297,19 @@ var WasmMotionDetector = class {
|
|
|
90
297
|
pixels: view.getInt32(base + 16, true)
|
|
91
298
|
});
|
|
92
299
|
}
|
|
300
|
+
const grouped = groupMotionRegions(raw, {
|
|
301
|
+
viewWidth: this.width,
|
|
302
|
+
viewHeight: this.height,
|
|
303
|
+
mergeRadiusDivisor: cfg.mergeRadiusDivisor,
|
|
304
|
+
minAreaFraction: cfg.minAreaFraction,
|
|
305
|
+
minArea: cfg.minArea,
|
|
306
|
+
maxCoverageFraction: cfg.maxCoverageFraction
|
|
307
|
+
});
|
|
93
308
|
return {
|
|
94
309
|
raw,
|
|
95
|
-
filtered:
|
|
310
|
+
filtered: grouped.passed,
|
|
311
|
+
groups: grouped.groups,
|
|
312
|
+
suppressedByCoverage: grouped.suppressedByCoverage
|
|
96
313
|
};
|
|
97
314
|
}
|
|
98
315
|
};
|
|
@@ -121,25 +338,27 @@ function gateMotionRegions(regions, zones, rules, frameWidth, frameHeight) {
|
|
|
121
338
|
*
|
|
122
339
|
* Settings redesign Phase 3: motion-wasm has ONLY device-level settings
|
|
123
340
|
* (all four fields were `scope: 'device'` in the legacy schema — every
|
|
124
|
-
* camera tunes threshold/
|
|
341
|
+
* camera tunes threshold/subject size/blurRadius/dilateRadius independently
|
|
125
342
|
* for its scene). So the addon implements `getDeviceSettings` /
|
|
126
343
|
* `updateDeviceSettings` and nothing else. Fresh cameras with no
|
|
127
344
|
* overrides get the schema defaults via `hydrateSchema()` at read time.
|
|
128
345
|
* There is no cluster-wide "default threshold" knob — if operators want
|
|
129
346
|
* to change the baseline, they edit the schema defaults in code.
|
|
130
347
|
*/
|
|
131
|
-
var DEFAULT_CONFIG = {
|
|
132
|
-
threshold: 45,
|
|
133
|
-
blurRadius: 1,
|
|
134
|
-
dilateRadius: 4,
|
|
135
|
-
minArea: 1500
|
|
136
|
-
};
|
|
137
348
|
var MotionWasmAddon = class MotionWasmAddon extends require_dist.BaseAddon {
|
|
138
349
|
detector = null;
|
|
139
350
|
cameras = /* @__PURE__ */ new Map();
|
|
140
351
|
deviceConfigCache = /* @__PURE__ */ new Map();
|
|
141
352
|
static DEVICE_CONFIG_TTL_MS = 6e4;
|
|
142
353
|
/**
|
|
354
|
+
* Last logged "which rule is this camera on" signature, per camera. The
|
|
355
|
+
* operator's gesture for D394 is a settings write, and a settings write is
|
|
356
|
+
* invisible: this is how they confirm it took, on the next analysed frame.
|
|
357
|
+
* Keyed and logged only on CHANGE, so it costs one line per camera per
|
|
358
|
+
* change rather than one per TTL expiry.
|
|
359
|
+
*/
|
|
360
|
+
loggedRule = /* @__PURE__ */ new Map();
|
|
361
|
+
/**
|
|
143
362
|
* Per-device {@link DeviceProxy} used for zone gating. Built lazily
|
|
144
363
|
* on first analyze() for a device; the proxy's reactive state
|
|
145
364
|
* handles (`state.zones`, `state.zoneRules`) keep their `.value`
|
|
@@ -256,7 +475,16 @@ var MotionWasmAddon = class MotionWasmAddon extends require_dist.BaseAddon {
|
|
|
256
475
|
}
|
|
257
476
|
state = this.cameras.get(cameraId);
|
|
258
477
|
const deviceCfg = await this.getDeviceConfig(cameraId);
|
|
259
|
-
|
|
478
|
+
this.detector.ensureSize(width, height);
|
|
479
|
+
const { raw, filtered, suppressedByCoverage } = this.detector.detect(state.prevGray, gray, deviceCfg);
|
|
480
|
+
if (suppressedByCoverage > 0) this.ctx.logger.info("motion storm suppressed", {
|
|
481
|
+
tags: { deviceId: Number(cameraId) },
|
|
482
|
+
meta: {
|
|
483
|
+
groups: suppressedByCoverage,
|
|
484
|
+
maxCoverageFraction: deviceCfg.maxCoverageFraction,
|
|
485
|
+
view: `${width}x${height}`
|
|
486
|
+
}
|
|
487
|
+
});
|
|
260
488
|
state.prevGray = Buffer.from(gray);
|
|
261
489
|
const toRegion = (r) => ({
|
|
262
490
|
bbox: {
|
|
@@ -347,28 +575,48 @@ var MotionWasmAddon = class MotionWasmAddon extends require_dist.BaseAddon {
|
|
|
347
575
|
threshold: typeof flat["threshold"] === "number" ? flat["threshold"] : DEFAULT_CONFIG.threshold,
|
|
348
576
|
blurRadius: typeof flat["blurRadius"] === "number" ? flat["blurRadius"] : DEFAULT_CONFIG.blurRadius,
|
|
349
577
|
dilateRadius: typeof flat["dilateRadius"] === "number" ? flat["dilateRadius"] : DEFAULT_CONFIG.dilateRadius,
|
|
350
|
-
minArea: typeof flat["minArea"] === "number" ? flat["minArea"] : DEFAULT_CONFIG.minArea
|
|
578
|
+
minArea: typeof flat["minArea"] === "number" ? flat["minArea"] : DEFAULT_CONFIG.minArea,
|
|
579
|
+
minAreaFraction: typeof flat["minAreaFraction"] === "number" ? flat["minAreaFraction"] : DEFAULT_CONFIG.minAreaFraction,
|
|
580
|
+
mergeRadiusDivisor: typeof flat["mergeRadiusDivisor"] === "number" ? flat["mergeRadiusDivisor"] : DEFAULT_CONFIG.mergeRadiusDivisor,
|
|
581
|
+
maxCoverageFraction: typeof flat["maxCoverageFraction"] === "number" ? flat["maxCoverageFraction"] : DEFAULT_CONFIG.maxCoverageFraction
|
|
351
582
|
};
|
|
352
583
|
this.deviceConfigCache.set(cameraId, {
|
|
353
584
|
config: resolved,
|
|
354
585
|
fetchedAt: now
|
|
355
586
|
});
|
|
587
|
+
const rule = resolved.minArea > 0 ? "legacy-per-region" : "merged-relative";
|
|
588
|
+
const signature = rule === "legacy-per-region" ? `${rule}:${resolved.minArea}` : `${rule}:${resolved.minAreaFraction}:${resolved.maxCoverageFraction}`;
|
|
589
|
+
if (this.loggedRule.get(cameraId) !== signature) {
|
|
590
|
+
this.loggedRule.set(cameraId, signature);
|
|
591
|
+
this.ctx.logger.info("motion area rule resolved", {
|
|
592
|
+
tags: { deviceId: Number(cameraId) },
|
|
593
|
+
meta: {
|
|
594
|
+
rule,
|
|
595
|
+
minArea: resolved.minArea,
|
|
596
|
+
minAreaFraction: resolved.minAreaFraction,
|
|
597
|
+
maxCoverageFraction: resolved.maxCoverageFraction
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
356
601
|
return resolved;
|
|
357
602
|
}
|
|
358
603
|
async removeCamera({ deviceId }) {
|
|
359
604
|
const key = String(deviceId);
|
|
360
605
|
this.cameras.delete(key);
|
|
361
606
|
this.deviceConfigCache.delete(key);
|
|
607
|
+
this.loggedRule.delete(key);
|
|
362
608
|
this.releaseProxy(deviceId);
|
|
363
609
|
}
|
|
364
610
|
async reset() {
|
|
365
611
|
this.cameras.clear();
|
|
366
612
|
this.deviceConfigCache.clear();
|
|
613
|
+
this.loggedRule.clear();
|
|
367
614
|
for (const id of this.proxies.keys()) this.releaseProxy(id);
|
|
368
615
|
}
|
|
369
616
|
async onShutdown() {
|
|
370
617
|
this.cameras.clear();
|
|
371
618
|
this.deviceConfigCache.clear();
|
|
619
|
+
this.loggedRule.clear();
|
|
372
620
|
for (const id of this.proxies.keys()) this.releaseProxy(id);
|
|
373
621
|
this.detector = null;
|
|
374
622
|
}
|
|
@@ -395,11 +643,26 @@ var MotionWasmAddon = class MotionWasmAddon extends require_dist.BaseAddon {
|
|
|
395
643
|
includes: "analyzer"
|
|
396
644
|
}
|
|
397
645
|
},
|
|
646
|
+
{
|
|
647
|
+
type: "slider",
|
|
648
|
+
key: "minAreaFraction",
|
|
649
|
+
label: "Min Subject Size",
|
|
650
|
+
description: "Smallest subject that triggers motion, as a share of the analysed view. Fragments of one subject are merged before this is applied, so a partly hidden animal counts once at its full size. Only in effect when Min Area is 0.",
|
|
651
|
+
min: .001,
|
|
652
|
+
max: .05,
|
|
653
|
+
step: 5e-4,
|
|
654
|
+
default: DEFAULT_CONFIG.minAreaFraction,
|
|
655
|
+
showValue: true,
|
|
656
|
+
showWhen: {
|
|
657
|
+
field: "motionSources",
|
|
658
|
+
includes: "analyzer"
|
|
659
|
+
}
|
|
660
|
+
},
|
|
398
661
|
{
|
|
399
662
|
type: "number",
|
|
400
663
|
key: "minArea",
|
|
401
664
|
label: "Min Area",
|
|
402
|
-
description: "
|
|
665
|
+
description: "Absolute pixel bar, applied to each motion region on its own. This is how motion has always worked, and it stays the default. Set it to 0 to move THIS camera to Min Subject Size, which merges the fragments of one subject before measuring it and keeps its meaning at any resolution — expect more motion, so move one camera at a time.",
|
|
403
666
|
min: 0,
|
|
404
667
|
max: 5e4,
|
|
405
668
|
step: 100,
|
|
@@ -410,6 +673,21 @@ var MotionWasmAddon = class MotionWasmAddon extends require_dist.BaseAddon {
|
|
|
410
673
|
includes: "analyzer"
|
|
411
674
|
}
|
|
412
675
|
},
|
|
676
|
+
{
|
|
677
|
+
type: "slider",
|
|
678
|
+
key: "maxCoverageFraction",
|
|
679
|
+
label: "Storm Cutoff",
|
|
680
|
+
description: "A merged region whose changed pixels cover more of the view than this is weather, an IR cut or an exposure step, not a subject, and does not fire motion. A real mover inside the storm still fires on its own region. Only in effect when Min Area is 0; 1 disables the cutoff.",
|
|
681
|
+
min: .2,
|
|
682
|
+
max: 1,
|
|
683
|
+
step: .05,
|
|
684
|
+
default: DEFAULT_CONFIG.maxCoverageFraction,
|
|
685
|
+
showValue: true,
|
|
686
|
+
showWhen: {
|
|
687
|
+
field: "motionSources",
|
|
688
|
+
includes: "analyzer"
|
|
689
|
+
}
|
|
690
|
+
},
|
|
413
691
|
{
|
|
414
692
|
type: "slider",
|
|
415
693
|
key: "blurRadius",
|