@camstack/addon-motion-wasm 0.1.1

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/index.mjs ADDED
@@ -0,0 +1,472 @@
1
+ import {
2
+ __dirname,
3
+ init_esm_shims
4
+ } from "./chunk-FXGWBT3O.mjs";
5
+
6
+ // src/index.ts
7
+ init_esm_shims();
8
+
9
+ // src/addon/index.ts
10
+ init_esm_shims();
11
+ import { BaseAddon, DeviceType, hydrateSchema, motionDetectionCapability } from "@camstack/types";
12
+
13
+ // src/wasm-motion-detector.ts
14
+ init_esm_shims();
15
+ import { readFileSync } from "fs";
16
+ import { join } from "path";
17
+ var DEFAULT_CONFIG = {
18
+ threshold: 45,
19
+ blurRadius: 1,
20
+ dilateRadius: 4,
21
+ minArea: 3e3
22
+ };
23
+ var WasmMotionDetector = class {
24
+ wasm = null;
25
+ prevOffset = 0;
26
+ currOffset = 0;
27
+ regionOffset = 0;
28
+ /** Load the WASM module. Call once before detect(). */
29
+ async load() {
30
+ const wasmPath = join(__dirname, "..", "wasm", "motion.wasm");
31
+ const wasmBytes = readFileSync(wasmPath);
32
+ const { instance } = await WebAssembly.instantiate(wasmBytes, {
33
+ env: { abort: () => {
34
+ throw new Error("WASM abort");
35
+ } }
36
+ });
37
+ const exports = instance.exports;
38
+ const required = [
39
+ "memory",
40
+ "init",
41
+ "getPrevOffset",
42
+ "getCurrOffset",
43
+ "getRegionOffset",
44
+ "detectMotion"
45
+ ];
46
+ for (const name of required) {
47
+ const v = exports[name];
48
+ if (v === void 0) {
49
+ throw new Error(`motion.wasm contract violation: missing export "${String(name)}"`);
50
+ }
51
+ if (name === "memory") {
52
+ if (!(v instanceof WebAssembly.Memory)) {
53
+ throw new Error('motion.wasm contract violation: "memory" is not a WebAssembly.Memory');
54
+ }
55
+ } else if (typeof v !== "function") {
56
+ throw new Error(`motion.wasm contract violation: "${String(name)}" is not callable`);
57
+ }
58
+ }
59
+ this.wasm = exports;
60
+ }
61
+ /** Initialize for given frame dimensions. Call when resolution changes. */
62
+ init(w, h) {
63
+ if (!this.wasm) throw new Error("WASM not loaded \u2014 call load() first");
64
+ this.wasm.init(w, h);
65
+ this.prevOffset = this.wasm.getPrevOffset();
66
+ this.currOffset = this.wasm.getCurrOffset();
67
+ this.regionOffset = this.wasm.getRegionOffset();
68
+ }
69
+ /**
70
+ * Detect motion between previous and current grayscale frames.
71
+ *
72
+ * @param prevGray - previous frame (Uint8Array, width×height)
73
+ * @param currGray - current frame (Uint8Array, width×height)
74
+ * @param config - detection parameters (optional, uses defaults)
75
+ * @returns raw (all CCL regions) + filtered (passing minArea) regions
76
+ */
77
+ detect(prevGray, currGray, config = {}) {
78
+ if (!this.wasm) throw new Error("WASM not loaded");
79
+ const cfg = { ...DEFAULT_CONFIG, ...config };
80
+ const mem = new Uint8Array(this.wasm.memory.buffer);
81
+ mem.set(prevGray, this.prevOffset);
82
+ mem.set(currGray, this.currOffset);
83
+ const numRegions = this.wasm.detectMotion(
84
+ cfg.threshold,
85
+ cfg.blurRadius,
86
+ cfg.dilateRadius,
87
+ 0
88
+ );
89
+ if (numRegions === 0) return { raw: [], filtered: [] };
90
+ const view = new DataView(this.wasm.memory.buffer);
91
+ const raw = [];
92
+ for (let i = 0; i < numRegions; i++) {
93
+ const base = this.regionOffset + i * 20;
94
+ raw.push({
95
+ x: view.getInt32(base, true),
96
+ y: view.getInt32(base + 4, true),
97
+ w: view.getInt32(base + 8, true),
98
+ h: view.getInt32(base + 12, true),
99
+ pixels: view.getInt32(base + 16, true)
100
+ });
101
+ }
102
+ const filtered = raw.filter((r) => r.pixels >= cfg.minArea);
103
+ return { raw, filtered };
104
+ }
105
+ };
106
+
107
+ // src/zone-gate.ts
108
+ init_esm_shims();
109
+ import { evaluateZoneRules } from "@camstack/types";
110
+ function gateMotionRegions(regions, zones, rules, frameWidth, frameHeight) {
111
+ if (frameWidth === 0 || frameHeight === 0) {
112
+ return { passed: regions, excluded: [] };
113
+ }
114
+ return evaluateZoneRules(regions, zones, rules, (region) => ({
115
+ x: (region.bbox.x + region.bbox.w / 2) / frameWidth,
116
+ y: (region.bbox.y + region.bbox.h / 2) / frameHeight
117
+ }));
118
+ }
119
+
120
+ // src/addon/index.ts
121
+ var DEFAULT_CONFIG2 = { threshold: 45, blurRadius: 1, dilateRadius: 4, minArea: 1500 };
122
+ var MotionWasmAddon = class _MotionWasmAddon extends BaseAddon {
123
+ detector = null;
124
+ cameras = /* @__PURE__ */ new Map();
125
+ deviceConfigCache = /* @__PURE__ */ new Map();
126
+ static DEVICE_CONFIG_TTL_MS = 6e4;
127
+ /**
128
+ * Per-device {@link DeviceProxy} used for zone gating. Built lazily
129
+ * on first analyze() for a device; the proxy's reactive state
130
+ * handles (`state.zones`, `state.zoneRules`) keep their `.value`
131
+ * fresh via the kernel's runtime-state mirror, so the gate reads
132
+ * are sync and free of bus plumbing inside this addon.
133
+ */
134
+ proxies = /* @__PURE__ */ new Map();
135
+ /** Unsubscribe pins kept so the slice handles stay subscribed for
136
+ * the camera's lifetime — `.value` only flows through the cache
137
+ * when at least one watcher is active. */
138
+ proxyUnsubs = /* @__PURE__ */ new Map();
139
+ constructor() {
140
+ super({});
141
+ }
142
+ async onInitialize() {
143
+ this.detector = new WasmMotionDetector();
144
+ await this.detector.load();
145
+ this.ctx.logger.info("WASM motion detector loaded");
146
+ return [{
147
+ capability: motionDetectionCapability,
148
+ provider: this,
149
+ kind: "wrapper",
150
+ defaultActive: true
151
+ }];
152
+ }
153
+ // Sentinel key used by the pipeline-step `process()` path, which owns a
154
+ // dedicated per-addon instance (one per camera via AddonEngineManager)
155
+ // and therefore does not need a real deviceId.
156
+ static PIPELINE_STEP_KEY = "__pipeline__";
157
+ /**
158
+ * Pipeline step interface — called by PipelineRunner.
159
+ * Uses single-camera state (one instance per camera via AddonEngineManager).
160
+ */
161
+ async process(input) {
162
+ const start = performance.now();
163
+ const result = await this.analyzeInternal(_MotionWasmAddon.PIPELINE_STEP_KEY, input);
164
+ const toSpatial = (r) => ({
165
+ class: "motion",
166
+ originalClass: "motion",
167
+ score: Math.min(1, r.intensity / 128),
168
+ bbox: r.bbox
169
+ });
170
+ const detections = result.regions.map(toSpatial);
171
+ const rawDetections = result.rawRegions.map(toSpatial);
172
+ return { detections, rawDetections, inferenceMs: performance.now() - start, modelId: "wasm-motion" };
173
+ }
174
+ /**
175
+ * motion-detection cap: analyze a frame, return regions + stats.
176
+ * Per-device state keyed by the numeric deviceId (stringified internally
177
+ * so the pipeline-step sentinel shares the same Map).
178
+ */
179
+ async analyze({ deviceId, frame }) {
180
+ return this.analyzeInternal(String(deviceId), frame);
181
+ }
182
+ async analyzeInternal(cameraId, frame) {
183
+ if (!this.detector) {
184
+ return { detected: false, regionCount: 0, regions: [], rawRegions: [], frameWidth: 0, frameHeight: 0, analysisMs: 0 };
185
+ }
186
+ const start = performance.now();
187
+ let gray;
188
+ let width;
189
+ let height;
190
+ if (frame.format === "jpeg") {
191
+ const sharp = (await import("./lib-OC5G62TM.mjs")).default;
192
+ const { data, info } = await sharp(Buffer.from(frame.data)).grayscale().raw().toBuffer({ resolveWithObject: true });
193
+ gray = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
194
+ width = info.width;
195
+ height = info.height;
196
+ } else {
197
+ gray = frame.data instanceof Uint8Array ? frame.data : new Uint8Array(frame.data);
198
+ width = frame.width;
199
+ height = frame.height;
200
+ }
201
+ let state = this.cameras.get(cameraId);
202
+ if (!state || state.width !== width || state.height !== height) {
203
+ this.detector.init(width, height);
204
+ this.cameras.set(cameraId, { prevGray: gray, width, height });
205
+ return {
206
+ detected: false,
207
+ regionCount: 0,
208
+ regions: [],
209
+ rawRegions: [],
210
+ frameWidth: width,
211
+ frameHeight: height,
212
+ analysisMs: performance.now() - start
213
+ };
214
+ }
215
+ state = this.cameras.get(cameraId);
216
+ const deviceCfg = await this.getDeviceConfig(cameraId);
217
+ const { raw, filtered } = this.detector.detect(state.prevGray, gray, deviceCfg);
218
+ state.prevGray = Buffer.from(gray);
219
+ const toRegion = (r) => ({
220
+ bbox: { x: r.x, y: r.y, w: r.w, h: r.h },
221
+ pixelCount: r.pixels,
222
+ intensity: Math.min(255, Math.round(r.pixels / (r.w * r.h) * 255))
223
+ });
224
+ let regions = filtered.map(toRegion);
225
+ const rawRegions = raw.map(toRegion);
226
+ const numericDeviceId = Number(cameraId);
227
+ if (Number.isFinite(numericDeviceId)) {
228
+ const proxy = await this.ensureProxy(numericDeviceId);
229
+ if (proxy) {
230
+ const zones = proxy.state.zones.value?.zones ?? [];
231
+ const rules = proxy.state.zoneRules.value?.motion ?? [];
232
+ if (rules.length > 0 && zones.length > 0) {
233
+ const gated = gateMotionRegions(regions, zones, rules, width, height);
234
+ regions = [...gated.passed];
235
+ }
236
+ }
237
+ }
238
+ return {
239
+ detected: regions.length > 0,
240
+ regionCount: regions.length,
241
+ regions,
242
+ rawRegions,
243
+ frameWidth: width,
244
+ frameHeight: height,
245
+ analysisMs: performance.now() - start
246
+ };
247
+ }
248
+ /**
249
+ * Resolve (and cache) a {@link DeviceProxy} for the given device.
250
+ * Pins the `state.zones` + `state.zoneRules` slice handles so the
251
+ * kernel runtime-state mirror keeps `.value` warm — subsequent
252
+ * analyze() calls read both slices synchronously without any bus
253
+ * plumbing or per-frame cap query. Returns null on first-call
254
+ * failure (logged) so motion analysis never blocks on zone
255
+ * resolution.
256
+ */
257
+ async ensureProxy(deviceId) {
258
+ const cached = this.proxies.get(deviceId);
259
+ if (cached) return cached;
260
+ try {
261
+ const proxy = await this.ctx.fetchDevice(deviceId);
262
+ this.proxies.set(deviceId, proxy);
263
+ const unsubs = [
264
+ proxy.state.zones.subscribe(() => {
265
+ }),
266
+ proxy.state.zoneRules.subscribe(() => {
267
+ })
268
+ ];
269
+ this.proxyUnsubs.set(deviceId, unsubs);
270
+ return proxy;
271
+ } catch (err) {
272
+ this.ctx.logger.debug("ensureProxy failed \u2014 gating skipped", {
273
+ tags: { deviceId },
274
+ meta: { error: err instanceof Error ? err.message : String(err) }
275
+ });
276
+ return null;
277
+ }
278
+ }
279
+ /** Drop the proxy + slice subscriptions for a device. */
280
+ releaseProxy(deviceId) {
281
+ const unsubs = this.proxyUnsubs.get(deviceId);
282
+ if (unsubs) {
283
+ for (const u of unsubs) {
284
+ try {
285
+ u();
286
+ } catch {
287
+ }
288
+ }
289
+ }
290
+ this.proxyUnsubs.delete(deviceId);
291
+ this.proxies.delete(deviceId);
292
+ }
293
+ /**
294
+ * Resolve the effective per-device config for a camera. Reads the raw
295
+ * device store and overlays it on top of the schema defaults via
296
+ * `hydrateSchema()`, then narrows to the typed `WasmMotionConfig` shape.
297
+ * Cached with a TTL to avoid hammering the settings store on every frame.
298
+ */
299
+ async getDeviceConfig(cameraId) {
300
+ const now = Date.now();
301
+ const cached = this.deviceConfigCache.get(cameraId);
302
+ if (cached && now - cached.fetchedAt < _MotionWasmAddon.DEVICE_CONFIG_TTL_MS) {
303
+ return cached.config;
304
+ }
305
+ if (!this.ctx?.settings) return DEFAULT_CONFIG2;
306
+ const raw = await this.ctx.settings.readDeviceStore(Number(cameraId));
307
+ const hydrated = hydrateSchema(this.deviceSettingsSchema(), raw);
308
+ const flat = {};
309
+ for (const section of hydrated.sections) {
310
+ for (const field of section.fields) {
311
+ if (field.type === "separator" || field.type === "info" || field.type === "button") continue;
312
+ if (field.type === "group") continue;
313
+ flat[field.key] = field.value;
314
+ }
315
+ }
316
+ const resolved = {
317
+ threshold: typeof flat["threshold"] === "number" ? flat["threshold"] : DEFAULT_CONFIG2.threshold,
318
+ blurRadius: typeof flat["blurRadius"] === "number" ? flat["blurRadius"] : DEFAULT_CONFIG2.blurRadius,
319
+ dilateRadius: typeof flat["dilateRadius"] === "number" ? flat["dilateRadius"] : DEFAULT_CONFIG2.dilateRadius,
320
+ minArea: typeof flat["minArea"] === "number" ? flat["minArea"] : DEFAULT_CONFIG2.minArea
321
+ };
322
+ this.deviceConfigCache.set(cameraId, { config: resolved, fetchedAt: now });
323
+ return resolved;
324
+ }
325
+ async removeCamera({ deviceId }) {
326
+ const key = String(deviceId);
327
+ this.cameras.delete(key);
328
+ this.deviceConfigCache.delete(key);
329
+ this.releaseProxy(deviceId);
330
+ }
331
+ async reset() {
332
+ this.cameras.clear();
333
+ this.deviceConfigCache.clear();
334
+ for (const id of this.proxies.keys()) this.releaseProxy(id);
335
+ }
336
+ async onShutdown() {
337
+ this.cameras.clear();
338
+ this.deviceConfigCache.clear();
339
+ for (const id of this.proxies.keys()) this.releaseProxy(id);
340
+ this.detector = null;
341
+ }
342
+ // ── Standard ICamstackAddon — three-level settings API (Phase 3) ─────
343
+ //
344
+ // motion-wasm is a pure per-device addon: every parameter is tuned on
345
+ // a per-camera basis. It implements only `getDeviceSettings` /
346
+ // `updateDeviceSettings`. No addon-level, no node-global.
347
+ deviceSettingsSchema() {
348
+ return this.schema({
349
+ sections: [
350
+ {
351
+ id: "motion-wasm-settings",
352
+ title: "",
353
+ tab: "motion",
354
+ order: 10,
355
+ columns: 2,
356
+ fields: [
357
+ // Every motion-wasm knob is an analyzer-side tuning param —
358
+ // when the operator picks `onboard`-only on the orchestrator's
359
+ // motionSources field, the wasm analyzer doesn't run, so
360
+ // exposing the threshold/minArea/blur/dilate fields would be
361
+ // misleading. Gate on the same key the orchestrator reads
362
+ // (`motionSources`) so all four hide together.
363
+ {
364
+ type: "slider",
365
+ key: "threshold",
366
+ label: "Threshold",
367
+ description: "Pixel intensity difference to count as changed (higher = less sensitive)",
368
+ min: 1,
369
+ max: 255,
370
+ step: 1,
371
+ default: DEFAULT_CONFIG2.threshold,
372
+ showValue: true,
373
+ showWhen: { field: "motionSources", includes: "analyzer" }
374
+ },
375
+ {
376
+ type: "number",
377
+ key: "minArea",
378
+ label: "Min Area",
379
+ description: "Minimum pixel area of a motion region to trigger detection",
380
+ min: 0,
381
+ max: 5e4,
382
+ step: 100,
383
+ default: DEFAULT_CONFIG2.minArea,
384
+ unit: "px",
385
+ showWhen: { field: "motionSources", includes: "analyzer" }
386
+ },
387
+ {
388
+ type: "slider",
389
+ key: "blurRadius",
390
+ label: "Blur Radius",
391
+ description: "Gaussian blur before diff (reduces noise)",
392
+ min: 0,
393
+ max: 10,
394
+ step: 1,
395
+ default: DEFAULT_CONFIG2.blurRadius,
396
+ showValue: true,
397
+ showWhen: { field: "motionSources", includes: "analyzer" }
398
+ },
399
+ {
400
+ type: "slider",
401
+ key: "dilateRadius",
402
+ label: "Dilation Radius",
403
+ description: "Expand motion regions to merge nearby changes",
404
+ min: 0,
405
+ max: 10,
406
+ step: 1,
407
+ default: DEFAULT_CONFIG2.dilateRadius,
408
+ showValue: true,
409
+ showWhen: { field: "motionSources", includes: "analyzer" }
410
+ }
411
+ ]
412
+ }
413
+ ]
414
+ });
415
+ }
416
+ async getDeviceSettings(deviceId) {
417
+ const ctx = this.ctxIfReady;
418
+ const raw = await ctx?.settings?.readDeviceStore(deviceId) ?? {};
419
+ return hydrateSchema(this.deviceSettingsSchema(), raw);
420
+ }
421
+ async updateDeviceSettings(deviceId, patch) {
422
+ await this.ctx.settings?.writeDeviceStore(deviceId, patch);
423
+ this.deviceConfigCache.delete(String(deviceId));
424
+ }
425
+ // ── Device-details aggregator contribution (motion-detection cap) ────────
426
+ //
427
+ // motion-wasm contributes its 4 tuning knobs (threshold, blurRadius,
428
+ // dilateRadius, minArea) to the `detection` tab for every camera. Live
429
+ // info is skipped — motion runtime stats are already streamed via the
430
+ // pipeline-orchestrator live contribution.
431
+ async getDeviceSettingsContribution(input) {
432
+ if (!await this.isCameraDevice(input.deviceId)) return null;
433
+ const schema = this.deviceSettingsSchema();
434
+ if (!schema) return null;
435
+ const raw = await this.ctx?.settings?.readDeviceStore(input.deviceId) ?? {};
436
+ const withTab = {
437
+ ...schema,
438
+ sections: schema.sections.map((s) => ({ ...s, tab: s.tab ?? "detection" }))
439
+ };
440
+ return hydrateSchema(withTab, raw);
441
+ }
442
+ async getDeviceLiveContribution(_input) {
443
+ return null;
444
+ }
445
+ async applyDeviceSettingsPatch(input) {
446
+ await this.updateDeviceSettings(input.deviceId, input.patch);
447
+ return { success: true };
448
+ }
449
+ /**
450
+ * Best-effort camera-type check. Used by the settings/live contribution
451
+ * methods to short-circuit on non-camera devices (Lights, Switches,
452
+ * Sensors, Buttons). Returns `true` on lookup failure so a transient
453
+ * device-manager hiccup never silently hides legitimate camera
454
+ * sections.
455
+ */
456
+ async isCameraDevice(deviceId) {
457
+ const api = this.ctx?.api;
458
+ if (!api) return true;
459
+ try {
460
+ const dev = await api.deviceManager.getDevice.query({ deviceId });
461
+ if (!dev) return true;
462
+ return dev.type === DeviceType.Camera;
463
+ } catch {
464
+ return true;
465
+ }
466
+ }
467
+ };
468
+ export {
469
+ MotionWasmAddon,
470
+ WasmMotionDetector
471
+ };
472
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/addon/index.ts","../src/wasm-motion-detector.ts","../src/zone-gate.ts"],"sourcesContent":["export { default as MotionWasmAddon } from './addon/index.js'\nexport { WasmMotionDetector, type WasmMotionConfig, type WasmMotionRegion } from './wasm-motion-detector.js'\n","import type {\n ProviderRegistration,\n IMotionDetectionProvider, MotionAnalysisResult,\n FrameInput, DetectorOutput, SpatialDetection,\n} from '@camstack/types'\nimport type { DeviceProxy } from '@camstack/types'\nimport { BaseAddon, DeviceType, hydrateSchema, motionDetectionCapability } from '@camstack/types'\nimport type { ConfigUISchemaWithValues, ConfigUISchema } from '@camstack/types'\nimport { WasmMotionDetector, type WasmMotionConfig } from '../wasm-motion-detector.js'\nimport { gateMotionRegions } from '../zone-gate.js'\n\n/**\n * Motion detection addon using WebAssembly.\n *\n * Drop-in replacement for addon-motion-detection. Same IMotionDetector interface,\n * but uses WASM blur+diff+dilate+CCL instead of JS pixel loop.\n *\n * Produces bounding box regions (not just changed pixel count).\n * ~3ms/frame at 640×360 with full pipeline.\n *\n * Settings redesign Phase 3: motion-wasm has ONLY device-level settings\n * (all four fields were `scope: 'device'` in the legacy schema — every\n * camera tunes threshold/minArea/blurRadius/dilateRadius independently\n * for its scene). So the addon implements `getDeviceSettings` /\n * `updateDeviceSettings` and nothing else. Fresh cameras with no\n * overrides get the schema defaults via `hydrateSchema()` at read time.\n * There is no cluster-wide \"default threshold\" knob — if operators want\n * to change the baseline, they edit the schema defaults in code.\n */\nconst DEFAULT_CONFIG: WasmMotionConfig = { threshold: 45, blurRadius: 1, dilateRadius: 4, minArea: 1500 }\n\nexport default class MotionWasmAddon extends BaseAddon implements IMotionDetectionProvider {\n private detector: WasmMotionDetector | null = null\n\n private readonly cameras = new Map<string, {\n prevGray: Uint8Array | null\n width: number\n height: number\n }>()\n\n private readonly deviceConfigCache = new Map<string, { config: WasmMotionConfig; fetchedAt: number }>()\n private static readonly DEVICE_CONFIG_TTL_MS = 60_000\n\n /**\n * Per-device {@link DeviceProxy} used for zone gating. Built lazily\n * on first analyze() for a device; the proxy's reactive state\n * handles (`state.zones`, `state.zoneRules`) keep their `.value`\n * fresh via the kernel's runtime-state mirror, so the gate reads\n * are sync and free of bus plumbing inside this addon.\n */\n private readonly proxies = new Map<number, DeviceProxy>()\n /** Unsubscribe pins kept so the slice handles stay subscribed for\n * the camera's lifetime — `.value` only flows through the cache\n * when at least one watcher is active. */\n private readonly proxyUnsubs = new Map<number, ReadonlyArray<() => void>>()\n\n constructor() { super({}) }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.detector = new WasmMotionDetector()\n await this.detector.load()\n this.ctx.logger.info('WASM motion detector loaded')\n\n // Register as motion-detection wrapper with defaultActive so every\n // camera picks it up automatically via device-manager.getBindings.\n // Operators disable per-device via device-manager.setWrapperActive.\n return [{\n capability: motionDetectionCapability,\n provider: this,\n kind: 'wrapper',\n defaultActive: true,\n }]\n }\n\n // Sentinel key used by the pipeline-step `process()` path, which owns a\n // dedicated per-addon instance (one per camera via AddonEngineManager)\n // and therefore does not need a real deviceId.\n private static readonly PIPELINE_STEP_KEY = '__pipeline__'\n\n /**\n * Pipeline step interface — called by PipelineRunner.\n * Uses single-camera state (one instance per camera via AddonEngineManager).\n */\n async process(input: FrameInput): Promise<DetectorOutput & { rawDetections: ReadonlyArray<SpatialDetection> }> {\n const start = performance.now()\n const result = await this.analyzeInternal(MotionWasmAddon.PIPELINE_STEP_KEY, input)\n\n const toSpatial = (r: { bbox: { x: number; y: number; w: number; h: number }; intensity: number }): SpatialDetection => ({\n class: 'motion',\n originalClass: 'motion',\n score: Math.min(1, r.intensity / 128),\n bbox: r.bbox,\n })\n\n const detections = result.regions.map(toSpatial)\n const rawDetections = result.rawRegions.map(toSpatial)\n\n return { detections, rawDetections, inferenceMs: performance.now() - start, modelId: 'wasm-motion' }\n }\n\n /**\n * motion-detection cap: analyze a frame, return regions + stats.\n * Per-device state keyed by the numeric deviceId (stringified internally\n * so the pipeline-step sentinel shares the same Map).\n */\n async analyze({ deviceId, frame }: { deviceId: number; frame: FrameInput }): Promise<MotionAnalysisResult> {\n return this.analyzeInternal(String(deviceId), frame)\n }\n\n private async analyzeInternal(cameraId: string, frame: FrameInput): Promise<MotionAnalysisResult> {\n if (!this.detector) {\n return { detected: false, regionCount: 0, regions: [], rawRegions: [], frameWidth: 0, frameHeight: 0, analysisMs: 0 }\n }\n\n const start = performance.now()\n\n // The frame data is expected to be grayscale when coming from NodeAvDecoderSession.\n // If JPEG (from old decoder), convert via sharp.\n let gray: Uint8Array\n let width: number\n let height: number\n\n if (frame.format === 'jpeg') {\n // Fallback: decode JPEG to grayscale\n const sharp = (await import('sharp')).default\n const { data, info } = await sharp(Buffer.from(frame.data)).grayscale().raw().toBuffer({ resolveWithObject: true })\n gray = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)\n width = info.width\n height = info.height\n } else {\n // Already raw grayscale (from node-av decoder)\n gray = frame.data instanceof Uint8Array ? frame.data : new Uint8Array(frame.data)\n width = frame.width\n height = frame.height\n }\n\n // Get or create per-camera state\n let state = this.cameras.get(cameraId)\n if (!state || state.width !== width || state.height !== height) {\n // First frame or resolution change — init WASM for these dimensions\n this.detector.init(width, height)\n this.cameras.set(cameraId, { prevGray: gray, width, height })\n return {\n detected: false,\n regionCount: 0,\n regions: [],\n rawRegions: [],\n frameWidth: width,\n frameHeight: height,\n analysisMs: performance.now() - start,\n }\n }\n\n state = this.cameras.get(cameraId)!\n\n // Resolve per-device config (cached, TTL-based)\n const deviceCfg = await this.getDeviceConfig(cameraId)\n\n // Detect motion — returns raw (all CCL) + filtered (passing minArea)\n const { raw, filtered } = this.detector.detect(state.prevGray!, gray, deviceCfg)\n state.prevGray = Buffer.from(gray) // save for next frame\n\n const toRegion = (r: { x: number; y: number; w: number; h: number; pixels: number }) => ({\n bbox: { x: r.x, y: r.y, w: r.w, h: r.h },\n pixelCount: r.pixels,\n intensity: Math.min(255, Math.round(r.pixels / (r.w * r.h) * 255)),\n })\n\n let regions = filtered.map(toRegion)\n const rawRegions = raw.map(toRegion)\n\n // Apply zone-rule gating when running with a numeric deviceId.\n // The pipeline-step path uses a sentinel cameraId — `Number()`\n // returns NaN, so the lookup misses and gating is skipped, which\n // matches \"no per-device context, no zones\" semantics.\n const numericDeviceId = Number(cameraId)\n if (Number.isFinite(numericDeviceId)) {\n const proxy = await this.ensureProxy(numericDeviceId)\n if (proxy) {\n const zones = proxy.state.zones.value?.zones ?? []\n const rules = proxy.state.zoneRules.value?.motion ?? []\n if (rules.length > 0 && zones.length > 0) {\n const gated = gateMotionRegions(regions, zones, rules, width, height)\n regions = [...gated.passed]\n }\n }\n }\n\n return {\n detected: regions.length > 0,\n regionCount: regions.length,\n regions,\n rawRegions,\n frameWidth: width,\n frameHeight: height,\n analysisMs: performance.now() - start,\n }\n }\n\n /**\n * Resolve (and cache) a {@link DeviceProxy} for the given device.\n * Pins the `state.zones` + `state.zoneRules` slice handles so the\n * kernel runtime-state mirror keeps `.value` warm — subsequent\n * analyze() calls read both slices synchronously without any bus\n * plumbing or per-frame cap query. Returns null on first-call\n * failure (logged) so motion analysis never blocks on zone\n * resolution.\n */\n private async ensureProxy(deviceId: number): Promise<DeviceProxy | null> {\n const cached = this.proxies.get(deviceId)\n if (cached) return cached\n try {\n const proxy = await this.ctx.fetchDevice(deviceId)\n this.proxies.set(deviceId, proxy)\n const unsubs = [\n proxy.state.zones.subscribe(() => { /* keep slice warm */ }),\n proxy.state.zoneRules.subscribe(() => { /* keep slice warm */ }),\n ]\n this.proxyUnsubs.set(deviceId, unsubs)\n return proxy\n } catch (err: unknown) {\n this.ctx.logger.debug('ensureProxy failed — gating skipped', {\n tags: { deviceId },\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n return null\n }\n }\n\n /** Drop the proxy + slice subscriptions for a device. */\n private releaseProxy(deviceId: number): void {\n const unsubs = this.proxyUnsubs.get(deviceId)\n if (unsubs) {\n for (const u of unsubs) {\n try { u() } catch { /* swallow */ }\n }\n }\n this.proxyUnsubs.delete(deviceId)\n this.proxies.delete(deviceId)\n }\n\n /**\n * Resolve the effective per-device config for a camera. Reads the raw\n * device store and overlays it on top of the schema defaults via\n * `hydrateSchema()`, then narrows to the typed `WasmMotionConfig` shape.\n * Cached with a TTL to avoid hammering the settings store on every frame.\n */\n private async getDeviceConfig(cameraId: string): Promise<WasmMotionConfig> {\n const now = Date.now()\n const cached = this.deviceConfigCache.get(cameraId)\n if (cached && now - cached.fetchedAt < MotionWasmAddon.DEVICE_CONFIG_TTL_MS) {\n return cached.config\n }\n if (!this.ctx?.settings) return DEFAULT_CONFIG\n\n // cameraId in this addon is the stringified numeric device id —\n // convert once at the settings-store boundary.\n const raw = await this.ctx.settings.readDeviceStore(Number(cameraId))\n const hydrated = hydrateSchema(this.deviceSettingsSchema()!, raw)\n // Flatten hydrated fields into a key→value map.\n const flat: Record<string, unknown> = {}\n for (const section of hydrated.sections) {\n for (const field of section.fields) {\n if (field.type === 'separator' || field.type === 'info' || field.type === 'button') continue\n if (field.type === 'group') continue\n flat[field.key] = (field as { readonly value: unknown }).value\n }\n }\n const resolved: WasmMotionConfig = {\n threshold: typeof flat['threshold'] === 'number' ? flat['threshold'] : DEFAULT_CONFIG.threshold,\n blurRadius: typeof flat['blurRadius'] === 'number' ? flat['blurRadius'] : DEFAULT_CONFIG.blurRadius,\n dilateRadius: typeof flat['dilateRadius'] === 'number' ? flat['dilateRadius'] : DEFAULT_CONFIG.dilateRadius,\n minArea: typeof flat['minArea'] === 'number' ? flat['minArea'] : DEFAULT_CONFIG.minArea,\n }\n this.deviceConfigCache.set(cameraId, { config: resolved, fetchedAt: now })\n return resolved\n }\n\n async removeCamera({ deviceId }: { deviceId: number }): Promise<void> {\n const key = String(deviceId)\n this.cameras.delete(key)\n this.deviceConfigCache.delete(key)\n this.releaseProxy(deviceId)\n }\n\n async reset(): Promise<void> {\n this.cameras.clear()\n this.deviceConfigCache.clear()\n for (const id of this.proxies.keys()) this.releaseProxy(id)\n }\n\n protected async onShutdown(): Promise<void> {\n this.cameras.clear()\n this.deviceConfigCache.clear()\n for (const id of this.proxies.keys()) this.releaseProxy(id)\n this.detector = null\n }\n\n // ── Standard ICamstackAddon — three-level settings API (Phase 3) ─────\n //\n // motion-wasm is a pure per-device addon: every parameter is tuned on\n // a per-camera basis. It implements only `getDeviceSettings` /\n // `updateDeviceSettings`. No addon-level, no node-global.\n\n protected deviceSettingsSchema() {\n return this.schema({\n sections: [\n {\n id: 'motion-wasm-settings',\n title: '',\n tab: 'motion',\n order: 10,\n columns: 2,\n fields: [\n // Every motion-wasm knob is an analyzer-side tuning param —\n // when the operator picks `onboard`-only on the orchestrator's\n // motionSources field, the wasm analyzer doesn't run, so\n // exposing the threshold/minArea/blur/dilate fields would be\n // misleading. Gate on the same key the orchestrator reads\n // (`motionSources`) so all four hide together.\n {\n type: 'slider',\n key: 'threshold',\n label: 'Threshold',\n description: 'Pixel intensity difference to count as changed (higher = less sensitive)',\n min: 1,\n max: 255,\n step: 1,\n default: DEFAULT_CONFIG.threshold,\n showValue: true,\n showWhen: { field: 'motionSources', includes: 'analyzer' },\n },\n {\n type: 'number',\n key: 'minArea',\n label: 'Min Area',\n description: 'Minimum pixel area of a motion region to trigger detection',\n min: 0,\n max: 50000,\n step: 100,\n default: DEFAULT_CONFIG.minArea,\n unit: 'px',\n showWhen: { field: 'motionSources', includes: 'analyzer' },\n },\n {\n type: 'slider',\n key: 'blurRadius',\n label: 'Blur Radius',\n description: 'Gaussian blur before diff (reduces noise)',\n min: 0,\n max: 10,\n step: 1,\n default: DEFAULT_CONFIG.blurRadius,\n showValue: true,\n showWhen: { field: 'motionSources', includes: 'analyzer' },\n },\n {\n type: 'slider',\n key: 'dilateRadius',\n label: 'Dilation Radius',\n description: 'Expand motion regions to merge nearby changes',\n min: 0,\n max: 10,\n step: 1,\n default: DEFAULT_CONFIG.dilateRadius,\n showValue: true,\n showWhen: { field: 'motionSources', includes: 'analyzer' },\n },\n ],\n },\n ],\n })\n }\n\n async getDeviceSettings(deviceId: number) {\n // Device-details aggregator pings every addon at page mount, which\n // may fire before this addon's `initialize()` resolves. Return an\n // empty-hydrated schema in that window — the UI retries on the next\n // settings round-trip once we're live.\n const ctx = this.ctxIfReady\n const raw = (await ctx?.settings?.readDeviceStore(deviceId)) ?? {}\n return hydrateSchema(this.deviceSettingsSchema()!, raw)\n }\n\n async updateDeviceSettings(deviceId: number, patch: Record<string, unknown>): Promise<void> {\n await this.ctx.settings?.writeDeviceStore(deviceId, patch)\n // Invalidate the cached per-device config so the next analyze() call\n // picks up the new values immediately.\n this.deviceConfigCache.delete(String(deviceId))\n }\n\n // ── Device-details aggregator contribution (motion-detection cap) ────────\n //\n // motion-wasm contributes its 4 tuning knobs (threshold, blurRadius,\n // dilateRadius, minArea) to the `detection` tab for every camera. Live\n // info is skipped — motion runtime stats are already streamed via the\n // pipeline-orchestrator live contribution.\n\n async getDeviceSettingsContribution(input: { deviceId: number }): Promise<ConfigUISchemaWithValues | null> {\n // Camera-only — motion tuning is meaningless on Switches / Lights /\n // Sensors / Buttons (none of them produce video). Source-side gate\n // keeps the policy with the addon that owns the cap and avoids\n // ghost top-tabs in the device-detail UI.\n if (!(await this.isCameraDevice(input.deviceId))) return null\n const schema = this.deviceSettingsSchema()\n if (!schema) return null\n const raw = (await this.ctx?.settings?.readDeviceStore(input.deviceId)) ?? {}\n // Re-home the section into the `detection` tab so the aggregator merges\n // motion-wasm settings with orchestrator/pipeline contributions there.\n const withTab: ConfigUISchema = {\n ...schema,\n sections: schema.sections.map(s => ({ ...s, tab: s.tab ?? 'detection' })),\n }\n return hydrateSchema(withTab, raw)\n }\n\n async getDeviceLiveContribution(_input: { deviceId: number }): Promise<ConfigUISchemaWithValues | null> {\n return null\n }\n\n async applyDeviceSettingsPatch(input: { deviceId: number; patch: Record<string, unknown> }): Promise<{ success: true }> {\n await this.updateDeviceSettings(input.deviceId, input.patch)\n return { success: true as const }\n }\n\n /**\n * Best-effort camera-type check. Used by the settings/live contribution\n * methods to short-circuit on non-camera devices (Lights, Switches,\n * Sensors, Buttons). Returns `true` on lookup failure so a transient\n * device-manager hiccup never silently hides legitimate camera\n * sections.\n */\n private async isCameraDevice(deviceId: number): Promise<boolean> {\n const api = this.ctx?.api\n if (!api) return true\n try {\n const dev = await api.deviceManager.getDevice.query({ deviceId })\n if (!dev) return true\n return (dev as { type?: string }).type === DeviceType.Camera\n } catch {\n return true\n }\n }\n}\n","/**\n * WasmMotionDetector — loads the AssemblyScript WASM module and provides\n * a typed interface for motion detection.\n *\n * Pipeline: blur → diff → threshold → dilate → CCL → bounding boxes\n * All in one WASM call, ~3ms for 640×360 grayscale frames.\n */\nimport { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\n// `__dirname` is shimmed by tsup (`shims: true`) for both CJS and ESM\n// output, so it works regardless of how the bundle is loaded.\n\nexport interface WasmMotionConfig {\n readonly threshold: number // pixel diff threshold 0-255 (default: 25)\n readonly blurRadius: number // box blur radius (default: 1 = 3×3)\n readonly dilateRadius: number // dilation radius (default: 4)\n readonly minArea: number // minimum region area in pixels (default: 200)\n}\n\nexport interface WasmMotionRegion {\n readonly x: number\n readonly y: number\n readonly w: number\n readonly h: number\n readonly pixels: number\n}\n\n/** Result containing both all regions and those passing minArea filter. */\nexport interface WasmMotionResult {\n /** All regions from CCL (unfiltered). */\n readonly raw: ReadonlyArray<WasmMotionRegion>\n /** Regions passing the minArea threshold. */\n readonly filtered: ReadonlyArray<WasmMotionRegion>\n}\n\nconst DEFAULT_CONFIG: WasmMotionConfig = {\n threshold: 45,\n blurRadius: 1,\n dilateRadius: 4,\n minArea: 3000,\n}\n\ninterface WasmExports {\n memory: WebAssembly.Memory\n init(w: number, h: number): number\n getPrevOffset(): number\n getCurrOffset(): number\n getRegionOffset(): number\n detectMotion(threshold: number, blur: number, dilate: number, minArea: number): number\n}\n\nexport class WasmMotionDetector {\n private wasm: WasmExports | null = null\n private prevOffset = 0\n private currOffset = 0\n private regionOffset = 0\n\n /** Load the WASM module. Call once before detect(). */\n async load(): Promise<void> {\n const wasmPath = join(__dirname, '..', 'wasm', 'motion.wasm')\n\n const wasmBytes = readFileSync(wasmPath)\n const { instance } = await WebAssembly.instantiate(wasmBytes, {\n env: { abort: () => { throw new Error('WASM abort') } },\n })\n\n // Structural validation — fail fast at load() if the WASM binary\n // doesn't expose the contract we expect. Without this, contract\n // violations (renamed/removed exports, signature drift) surface only\n // at first detect() call, mid-stream, with cryptic TypeError noise.\n const exports = instance.exports\n const required: ReadonlyArray<keyof WasmExports> = [\n 'memory',\n 'init',\n 'getPrevOffset',\n 'getCurrOffset',\n 'getRegionOffset',\n 'detectMotion',\n ]\n for (const name of required) {\n const v = (exports as Record<string, unknown>)[name as string]\n if (v === undefined) {\n throw new Error(`motion.wasm contract violation: missing export \"${String(name)}\"`)\n }\n if (name === 'memory') {\n if (!(v instanceof WebAssembly.Memory)) {\n throw new Error('motion.wasm contract violation: \"memory\" is not a WebAssembly.Memory')\n }\n } else if (typeof v !== 'function') {\n throw new Error(`motion.wasm contract violation: \"${String(name)}\" is not callable`)\n }\n }\n this.wasm = exports as unknown as WasmExports\n }\n\n /** Initialize for given frame dimensions. Call when resolution changes. */\n init(w: number, h: number): void {\n if (!this.wasm) throw new Error('WASM not loaded — call load() first')\n this.wasm.init(w, h)\n this.prevOffset = this.wasm.getPrevOffset()\n this.currOffset = this.wasm.getCurrOffset()\n this.regionOffset = this.wasm.getRegionOffset()\n }\n\n /**\n * Detect motion between previous and current grayscale frames.\n *\n * @param prevGray - previous frame (Uint8Array, width×height)\n * @param currGray - current frame (Uint8Array, width×height)\n * @param config - detection parameters (optional, uses defaults)\n * @returns raw (all CCL regions) + filtered (passing minArea) regions\n */\n detect(\n prevGray: Uint8Array | Buffer,\n currGray: Uint8Array | Buffer,\n config: Partial<WasmMotionConfig> = {},\n ): WasmMotionResult {\n if (!this.wasm) throw new Error('WASM not loaded')\n\n const cfg = { ...DEFAULT_CONFIG, ...config }\n\n // Copy frames to WASM linear memory\n const mem = new Uint8Array(this.wasm.memory.buffer)\n mem.set(prevGray, this.prevOffset)\n mem.set(currGray, this.currOffset)\n\n // Run detection — WASM returns ALL regions, JS filters by minArea\n const numRegions = this.wasm.detectMotion(\n cfg.threshold, cfg.blurRadius, cfg.dilateRadius, 0,\n )\n\n if (numRegions === 0) return { raw: [], filtered: [] }\n\n // Read all regions from WASM\n const view = new DataView(this.wasm.memory.buffer)\n const raw: WasmMotionRegion[] = []\n for (let i = 0; i < numRegions; i++) {\n const base = this.regionOffset + i * 20\n raw.push({\n x: view.getInt32(base, true),\n y: view.getInt32(base + 4, true),\n w: view.getInt32(base + 8, true),\n h: view.getInt32(base + 12, true),\n pixels: view.getInt32(base + 16, true),\n })\n }\n\n // Filter by minArea in JS\n const filtered = raw.filter(r => r.pixels >= cfg.minArea)\n\n return { raw, filtered }\n }\n}\n","/**\n * Motion-stage zone gating — thin wrapper over the canonical\n * {@link evaluateZoneRules} helper in `@camstack/types/utils`.\n * Translates a motion region's pixel-space bbox into the normalised\n * (0–1) center the shared evaluator expects, and returns the\n * passed/excluded partition.\n *\n * Motion has no per-region class — the shared evaluator treats a\n * missing className as \"every classFilter matches\", which is the\n * intended semantics for the motion stage (operators using\n * `classFilter` on a motion rule typically do so by mistake; the\n * evaluator stays permissive instead of silently dropping motion\n * with no class).\n */\n\nimport type { Zone, ZoneRule, ZoneRuleEvalResult } from '@camstack/types'\nimport { evaluateZoneRules } from '@camstack/types'\n\nexport interface MotionRegionLike {\n readonly bbox: { readonly x: number; readonly y: number; readonly w: number; readonly h: number }\n}\n\nexport type GateResult<T extends MotionRegionLike> = ZoneRuleEvalResult<T>\n\nexport function gateMotionRegions<T extends MotionRegionLike>(\n regions: readonly T[],\n zones: readonly Zone[],\n rules: readonly ZoneRule[],\n frameWidth: number,\n frameHeight: number,\n): GateResult<T> {\n if (frameWidth === 0 || frameHeight === 0) {\n return { passed: regions, excluded: [] }\n }\n return evaluateZoneRules(regions, zones, rules, (region) => ({\n x: (region.bbox.x + region.bbox.w / 2) / frameWidth,\n y: (region.bbox.y + region.bbox.h / 2) / frameHeight,\n }))\n}\n"],"mappings":";;;;;;AAAA;;;ACAA;AAMA,SAAS,WAAW,YAAY,eAAe,iCAAiC;;;ACNhF;AAOA,SAAS,oBAAoB;AAC7B,SAAS,YAAY;AA4BrB,IAAM,iBAAmC;AAAA,EACvC,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,SAAS;AACX;AAWO,IAAM,qBAAN,MAAyB;AAAA,EACtB,OAA2B;AAAA,EAC3B,aAAa;AAAA,EACb,aAAa;AAAA,EACb,eAAe;AAAA;AAAA,EAGvB,MAAM,OAAsB;AAC1B,UAAM,WAAW,KAAK,WAAW,MAAM,QAAQ,aAAa;AAE5D,UAAM,YAAY,aAAa,QAAQ;AACvC,UAAM,EAAE,SAAS,IAAI,MAAM,YAAY,YAAY,WAAW;AAAA,MAC5D,KAAK,EAAE,OAAO,MAAM;AAAE,cAAM,IAAI,MAAM,YAAY;AAAA,MAAE,EAAE;AAAA,IACxD,CAAC;AAMD,UAAM,UAAU,SAAS;AACzB,UAAM,WAA6C;AAAA,MACjD;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,eAAW,QAAQ,UAAU;AAC3B,YAAM,IAAK,QAAoC,IAAc;AAC7D,UAAI,MAAM,QAAW;AACnB,cAAM,IAAI,MAAM,mDAAmD,OAAO,IAAI,CAAC,GAAG;AAAA,MACpF;AACA,UAAI,SAAS,UAAU;AACrB,YAAI,EAAE,aAAa,YAAY,SAAS;AACtC,gBAAM,IAAI,MAAM,sEAAsE;AAAA,QACxF;AAAA,MACF,WAAW,OAAO,MAAM,YAAY;AAClC,cAAM,IAAI,MAAM,oCAAoC,OAAO,IAAI,CAAC,mBAAmB;AAAA,MACrF;AAAA,IACF;AACA,SAAK,OAAO;AAAA,EACd;AAAA;AAAA,EAGA,KAAK,GAAW,GAAiB;AAC/B,QAAI,CAAC,KAAK,KAAM,OAAM,IAAI,MAAM,0CAAqC;AACrE,SAAK,KAAK,KAAK,GAAG,CAAC;AACnB,SAAK,aAAa,KAAK,KAAK,cAAc;AAC1C,SAAK,aAAa,KAAK,KAAK,cAAc;AAC1C,SAAK,eAAe,KAAK,KAAK,gBAAgB;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,OACE,UACA,UACA,SAAoC,CAAC,GACnB;AAClB,QAAI,CAAC,KAAK,KAAM,OAAM,IAAI,MAAM,iBAAiB;AAEjD,UAAM,MAAM,EAAE,GAAG,gBAAgB,GAAG,OAAO;AAG3C,UAAM,MAAM,IAAI,WAAW,KAAK,KAAK,OAAO,MAAM;AAClD,QAAI,IAAI,UAAU,KAAK,UAAU;AACjC,QAAI,IAAI,UAAU,KAAK,UAAU;AAGjC,UAAM,aAAa,KAAK,KAAK;AAAA,MAC3B,IAAI;AAAA,MAAW,IAAI;AAAA,MAAY,IAAI;AAAA,MAAc;AAAA,IACnD;AAEA,QAAI,eAAe,EAAG,QAAO,EAAE,KAAK,CAAC,GAAG,UAAU,CAAC,EAAE;AAGrD,UAAM,OAAO,IAAI,SAAS,KAAK,KAAK,OAAO,MAAM;AACjD,UAAM,MAA0B,CAAC;AACjC,aAAS,IAAI,GAAG,IAAI,YAAY,KAAK;AACnC,YAAM,OAAO,KAAK,eAAe,IAAI;AACrC,UAAI,KAAK;AAAA,QACP,GAAG,KAAK,SAAS,MAAM,IAAI;AAAA,QAC3B,GAAG,KAAK,SAAS,OAAO,GAAG,IAAI;AAAA,QAC/B,GAAG,KAAK,SAAS,OAAO,GAAG,IAAI;AAAA,QAC/B,GAAG,KAAK,SAAS,OAAO,IAAI,IAAI;AAAA,QAChC,QAAQ,KAAK,SAAS,OAAO,IAAI,IAAI;AAAA,MACvC,CAAC;AAAA,IACH;AAGA,UAAM,WAAW,IAAI,OAAO,OAAK,EAAE,UAAU,IAAI,OAAO;AAExD,WAAO,EAAE,KAAK,SAAS;AAAA,EACzB;AACF;;;ACzJA;AAgBA,SAAS,yBAAyB;AAQ3B,SAAS,kBACd,SACA,OACA,OACA,YACA,aACe;AACf,MAAI,eAAe,KAAK,gBAAgB,GAAG;AACzC,WAAO,EAAE,QAAQ,SAAS,UAAU,CAAC,EAAE;AAAA,EACzC;AACA,SAAO,kBAAkB,SAAS,OAAO,OAAO,CAAC,YAAY;AAAA,IAC3D,IAAI,OAAO,KAAK,IAAI,OAAO,KAAK,IAAI,KAAK;AAAA,IACzC,IAAI,OAAO,KAAK,IAAI,OAAO,KAAK,IAAI,KAAK;AAAA,EAC3C,EAAE;AACJ;;;AFTA,IAAMA,kBAAmC,EAAE,WAAW,IAAI,YAAY,GAAG,cAAc,GAAG,SAAS,KAAK;AAExG,IAAqB,kBAArB,MAAqB,yBAAwB,UAA8C;AAAA,EACjF,WAAsC;AAAA,EAE7B,UAAU,oBAAI,IAI5B;AAAA,EAEc,oBAAoB,oBAAI,IAA6D;AAAA,EACtG,OAAwB,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAS9B,UAAU,oBAAI,IAAyB;AAAA;AAAA;AAAA;AAAA,EAIvC,cAAc,oBAAI,IAAuC;AAAA,EAE1E,cAAc;AAAE,UAAM,CAAC,CAAC;AAAA,EAAE;AAAA,EAE1B,MAAgB,eAAgD;AAC9D,SAAK,WAAW,IAAI,mBAAmB;AACvC,UAAM,KAAK,SAAS,KAAK;AACzB,SAAK,IAAI,OAAO,KAAK,6BAA6B;AAKlD,WAAO,CAAC;AAAA,MACN,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,MAAM;AAAA,MACN,eAAe;AAAA,IACjB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,OAAwB,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA,EAM5C,MAAM,QAAQ,OAAiG;AAC7G,UAAM,QAAQ,YAAY,IAAI;AAC9B,UAAM,SAAS,MAAM,KAAK,gBAAgB,iBAAgB,mBAAmB,KAAK;AAElF,UAAM,YAAY,CAAC,OAAsG;AAAA,MACvH,OAAO;AAAA,MACP,eAAe;AAAA,MACf,OAAO,KAAK,IAAI,GAAG,EAAE,YAAY,GAAG;AAAA,MACpC,MAAM,EAAE;AAAA,IACV;AAEA,UAAM,aAAa,OAAO,QAAQ,IAAI,SAAS;AAC/C,UAAM,gBAAgB,OAAO,WAAW,IAAI,SAAS;AAErD,WAAO,EAAE,YAAY,eAAe,aAAa,YAAY,IAAI,IAAI,OAAO,SAAS,cAAc;AAAA,EACrG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,QAAQ,EAAE,UAAU,MAAM,GAA2E;AACzG,WAAO,KAAK,gBAAgB,OAAO,QAAQ,GAAG,KAAK;AAAA,EACrD;AAAA,EAEA,MAAc,gBAAgB,UAAkB,OAAkD;AAChG,QAAI,CAAC,KAAK,UAAU;AAClB,aAAO,EAAE,UAAU,OAAO,aAAa,GAAG,SAAS,CAAC,GAAG,YAAY,CAAC,GAAG,YAAY,GAAG,aAAa,GAAG,YAAY,EAAE;AAAA,IACtH;AAEA,UAAM,QAAQ,YAAY,IAAI;AAI9B,QAAI;AACJ,QAAI;AACJ,QAAI;AAEJ,QAAI,MAAM,WAAW,QAAQ;AAE3B,YAAM,SAAS,MAAM,OAAO,oBAAO,GAAG;AACtC,YAAM,EAAE,MAAM,KAAK,IAAI,MAAM,MAAM,OAAO,KAAK,MAAM,IAAI,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,mBAAmB,KAAK,CAAC;AAClH,aAAO,IAAI,WAAW,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU;AACnE,cAAQ,KAAK;AACb,eAAS,KAAK;AAAA,IAChB,OAAO;AAEL,aAAO,MAAM,gBAAgB,aAAa,MAAM,OAAO,IAAI,WAAW,MAAM,IAAI;AAChF,cAAQ,MAAM;AACd,eAAS,MAAM;AAAA,IACjB;AAGA,QAAI,QAAQ,KAAK,QAAQ,IAAI,QAAQ;AACrC,QAAI,CAAC,SAAS,MAAM,UAAU,SAAS,MAAM,WAAW,QAAQ;AAE9D,WAAK,SAAS,KAAK,OAAO,MAAM;AAChC,WAAK,QAAQ,IAAI,UAAU,EAAE,UAAU,MAAM,OAAO,OAAO,CAAC;AAC5D,aAAO;AAAA,QACL,UAAU;AAAA,QACV,aAAa;AAAA,QACb,SAAS,CAAC;AAAA,QACV,YAAY,CAAC;AAAA,QACb,YAAY;AAAA,QACZ,aAAa;AAAA,QACb,YAAY,YAAY,IAAI,IAAI;AAAA,MAClC;AAAA,IACF;AAEA,YAAQ,KAAK,QAAQ,IAAI,QAAQ;AAGjC,UAAM,YAAY,MAAM,KAAK,gBAAgB,QAAQ;AAGrD,UAAM,EAAE,KAAK,SAAS,IAAI,KAAK,SAAS,OAAO,MAAM,UAAW,MAAM,SAAS;AAC/E,UAAM,WAAW,OAAO,KAAK,IAAI;AAEjC,UAAM,WAAW,CAAC,OAAuE;AAAA,MACvF,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE,GAAG,GAAG,EAAE,GAAG,GAAG,EAAE,EAAE;AAAA,MACvC,YAAY,EAAE;AAAA,MACd,WAAW,KAAK,IAAI,KAAK,KAAK,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC;AAAA,IACnE;AAEA,QAAI,UAAU,SAAS,IAAI,QAAQ;AACnC,UAAM,aAAa,IAAI,IAAI,QAAQ;AAMnC,UAAM,kBAAkB,OAAO,QAAQ;AACvC,QAAI,OAAO,SAAS,eAAe,GAAG;AACpC,YAAM,QAAQ,MAAM,KAAK,YAAY,eAAe;AACpD,UAAI,OAAO;AACT,cAAM,QAAQ,MAAM,MAAM,MAAM,OAAO,SAAS,CAAC;AACjD,cAAM,QAAQ,MAAM,MAAM,UAAU,OAAO,UAAU,CAAC;AACtD,YAAI,MAAM,SAAS,KAAK,MAAM,SAAS,GAAG;AACxC,gBAAM,QAAQ,kBAAkB,SAAS,OAAO,OAAO,OAAO,MAAM;AACpE,oBAAU,CAAC,GAAG,MAAM,MAAM;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,UAAU,QAAQ,SAAS;AAAA,MAC3B,aAAa,QAAQ;AAAA,MACrB;AAAA,MACA;AAAA,MACA,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,YAAY,YAAY,IAAI,IAAI;AAAA,IAClC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAc,YAAY,UAA+C;AACvE,UAAM,SAAS,KAAK,QAAQ,IAAI,QAAQ;AACxC,QAAI,OAAQ,QAAO;AACnB,QAAI;AACF,YAAM,QAAQ,MAAM,KAAK,IAAI,YAAY,QAAQ;AACjD,WAAK,QAAQ,IAAI,UAAU,KAAK;AAChC,YAAM,SAAS;AAAA,QACb,MAAM,MAAM,MAAM,UAAU,MAAM;AAAA,QAAwB,CAAC;AAAA,QAC3D,MAAM,MAAM,UAAU,UAAU,MAAM;AAAA,QAAwB,CAAC;AAAA,MACjE;AACA,WAAK,YAAY,IAAI,UAAU,MAAM;AACrC,aAAO;AAAA,IACT,SAAS,KAAc;AACrB,WAAK,IAAI,OAAO,MAAM,4CAAuC;AAAA,QAC3D,MAAM,EAAE,SAAS;AAAA,QACjB,MAAM,EAAE,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE;AAAA,MAClE,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAGQ,aAAa,UAAwB;AAC3C,UAAM,SAAS,KAAK,YAAY,IAAI,QAAQ;AAC5C,QAAI,QAAQ;AACV,iBAAW,KAAK,QAAQ;AACtB,YAAI;AAAE,YAAE;AAAA,QAAE,QAAQ;AAAA,QAAgB;AAAA,MACpC;AAAA,IACF;AACA,SAAK,YAAY,OAAO,QAAQ;AAChC,SAAK,QAAQ,OAAO,QAAQ;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,gBAAgB,UAA6C;AACzE,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,SAAS,KAAK,kBAAkB,IAAI,QAAQ;AAClD,QAAI,UAAU,MAAM,OAAO,YAAY,iBAAgB,sBAAsB;AAC3E,aAAO,OAAO;AAAA,IAChB;AACA,QAAI,CAAC,KAAK,KAAK,SAAU,QAAOA;AAIhC,UAAM,MAAM,MAAM,KAAK,IAAI,SAAS,gBAAgB,OAAO,QAAQ,CAAC;AACpE,UAAM,WAAW,cAAc,KAAK,qBAAqB,GAAI,GAAG;AAEhE,UAAM,OAAgC,CAAC;AACvC,eAAW,WAAW,SAAS,UAAU;AACvC,iBAAW,SAAS,QAAQ,QAAQ;AAClC,YAAI,MAAM,SAAS,eAAe,MAAM,SAAS,UAAU,MAAM,SAAS,SAAU;AACpF,YAAI,MAAM,SAAS,QAAS;AAC5B,aAAK,MAAM,GAAG,IAAK,MAAsC;AAAA,MAC3D;AAAA,IACF;AACA,UAAM,WAA6B;AAAA,MACjC,WAAW,OAAO,KAAK,WAAW,MAAM,WAAW,KAAK,WAAW,IAAIA,gBAAe;AAAA,MACtF,YAAY,OAAO,KAAK,YAAY,MAAM,WAAW,KAAK,YAAY,IAAIA,gBAAe;AAAA,MACzF,cAAc,OAAO,KAAK,cAAc,MAAM,WAAW,KAAK,cAAc,IAAIA,gBAAe;AAAA,MAC/F,SAAS,OAAO,KAAK,SAAS,MAAM,WAAW,KAAK,SAAS,IAAIA,gBAAe;AAAA,IAClF;AACA,SAAK,kBAAkB,IAAI,UAAU,EAAE,QAAQ,UAAU,WAAW,IAAI,CAAC;AACzE,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,aAAa,EAAE,SAAS,GAAwC;AACpE,UAAM,MAAM,OAAO,QAAQ;AAC3B,SAAK,QAAQ,OAAO,GAAG;AACvB,SAAK,kBAAkB,OAAO,GAAG;AACjC,SAAK,aAAa,QAAQ;AAAA,EAC5B;AAAA,EAEA,MAAM,QAAuB;AAC3B,SAAK,QAAQ,MAAM;AACnB,SAAK,kBAAkB,MAAM;AAC7B,eAAW,MAAM,KAAK,QAAQ,KAAK,EAAG,MAAK,aAAa,EAAE;AAAA,EAC5D;AAAA,EAEA,MAAgB,aAA4B;AAC1C,SAAK,QAAQ,MAAM;AACnB,SAAK,kBAAkB,MAAM;AAC7B,eAAW,MAAM,KAAK,QAAQ,KAAK,EAAG,MAAK,aAAa,EAAE;AAC1D,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQU,uBAAuB;AAC/B,WAAO,KAAK,OAAO;AAAA,MACjB,UAAU;AAAA,QACR;AAAA,UACE,IAAI;AAAA,UACJ,OAAO;AAAA,UACP,KAAK;AAAA,UACL,OAAO;AAAA,UACP,SAAS;AAAA,UACT,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAON;AAAA,cACE,MAAM;AAAA,cACN,KAAK;AAAA,cACL,OAAO;AAAA,cACP,aAAa;AAAA,cACb,KAAK;AAAA,cACL,KAAK;AAAA,cACL,MAAM;AAAA,cACN,SAASA,gBAAe;AAAA,cACxB,WAAW;AAAA,cACX,UAAU,EAAE,OAAO,iBAAiB,UAAU,WAAW;AAAA,YAC3D;AAAA,YACA;AAAA,cACE,MAAM;AAAA,cACN,KAAK;AAAA,cACL,OAAO;AAAA,cACP,aAAa;AAAA,cACb,KAAK;AAAA,cACL,KAAK;AAAA,cACL,MAAM;AAAA,cACN,SAASA,gBAAe;AAAA,cACxB,MAAM;AAAA,cACN,UAAU,EAAE,OAAO,iBAAiB,UAAU,WAAW;AAAA,YAC3D;AAAA,YACA;AAAA,cACE,MAAM;AAAA,cACN,KAAK;AAAA,cACL,OAAO;AAAA,cACP,aAAa;AAAA,cACb,KAAK;AAAA,cACL,KAAK;AAAA,cACL,MAAM;AAAA,cACN,SAASA,gBAAe;AAAA,cACxB,WAAW;AAAA,cACX,UAAU,EAAE,OAAO,iBAAiB,UAAU,WAAW;AAAA,YAC3D;AAAA,YACA;AAAA,cACE,MAAM;AAAA,cACN,KAAK;AAAA,cACL,OAAO;AAAA,cACP,aAAa;AAAA,cACb,KAAK;AAAA,cACL,KAAK;AAAA,cACL,MAAM;AAAA,cACN,SAASA,gBAAe;AAAA,cACxB,WAAW;AAAA,cACX,UAAU,EAAE,OAAO,iBAAiB,UAAU,WAAW;AAAA,YAC3D;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,kBAAkB,UAAkB;AAKxC,UAAM,MAAM,KAAK;AACjB,UAAM,MAAO,MAAM,KAAK,UAAU,gBAAgB,QAAQ,KAAM,CAAC;AACjE,WAAO,cAAc,KAAK,qBAAqB,GAAI,GAAG;AAAA,EACxD;AAAA,EAEA,MAAM,qBAAqB,UAAkB,OAA+C;AAC1F,UAAM,KAAK,IAAI,UAAU,iBAAiB,UAAU,KAAK;AAGzD,SAAK,kBAAkB,OAAO,OAAO,QAAQ,CAAC;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,8BAA8B,OAAuE;AAKzG,QAAI,CAAE,MAAM,KAAK,eAAe,MAAM,QAAQ,EAAI,QAAO;AACzD,UAAM,SAAS,KAAK,qBAAqB;AACzC,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,MAAO,MAAM,KAAK,KAAK,UAAU,gBAAgB,MAAM,QAAQ,KAAM,CAAC;AAG5E,UAAM,UAA0B;AAAA,MAC9B,GAAG;AAAA,MACH,UAAU,OAAO,SAAS,IAAI,QAAM,EAAE,GAAG,GAAG,KAAK,EAAE,OAAO,YAAY,EAAE;AAAA,IAC1E;AACA,WAAO,cAAc,SAAS,GAAG;AAAA,EACnC;AAAA,EAEA,MAAM,0BAA0B,QAAwE;AACtG,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,yBAAyB,OAAyF;AACtH,UAAM,KAAK,qBAAqB,MAAM,UAAU,MAAM,KAAK;AAC3D,WAAO,EAAE,SAAS,KAAc;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,eAAe,UAAoC;AAC/D,UAAM,MAAM,KAAK,KAAK;AACtB,QAAI,CAAC,IAAK,QAAO;AACjB,QAAI;AACF,YAAM,MAAM,MAAM,IAAI,cAAc,UAAU,MAAM,EAAE,SAAS,CAAC;AAChE,UAAI,CAAC,IAAK,QAAO;AACjB,aAAQ,IAA0B,SAAS,WAAW;AAAA,IACxD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;","names":["DEFAULT_CONFIG"]}