@camstack/addon-advanced-notifier 1.1.3 → 1.1.5
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/addon.js +136 -12
- package/dist/addon.mjs +136 -12
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -4632,7 +4632,7 @@ function _instanceof(cls, params = {}) {
|
|
|
4632
4632
|
return inst;
|
|
4633
4633
|
}
|
|
4634
4634
|
//#endregion
|
|
4635
|
-
//#region ../types/dist/sleep-
|
|
4635
|
+
//#region ../types/dist/sleep-NOH4yRwj.mjs
|
|
4636
4636
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4637
4637
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4638
4638
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -5747,15 +5747,24 @@ var BaseAddon = class {
|
|
|
5747
5747
|
} catch {}
|
|
5748
5748
|
}
|
|
5749
5749
|
/**
|
|
5750
|
-
* Resolve the shared models directory
|
|
5751
|
-
*
|
|
5752
|
-
*
|
|
5750
|
+
* Resolve the shared, node-local models directory: `<nodeDataDir>/models`.
|
|
5751
|
+
*
|
|
5752
|
+
* This is the SINGLE centralized models-dir resolver for every inference addon
|
|
5753
|
+
* (detection-pipeline, audio-analyzer, embedding-encoder, model-studio). It is
|
|
5754
|
+
* anchored at the node's own writable data root (`ctx.nodeDataDir`, i.e.
|
|
5755
|
+
* `CAMSTACK_DATA` ?? the kernel boot dir) — never the hub-singleton `storage`
|
|
5756
|
+
* cap, which returns the hub's `/data/models` to every node and breaks any node
|
|
5757
|
+
* whose real data dir differs (e.g. the macOS electron agent).
|
|
5758
|
+
*
|
|
5759
|
+
* `node:path`/`node:fs` are imported dynamically so the `@camstack/types` root
|
|
5760
|
+
* barrel (re-exported into browser bundles) stays free of node builtins.
|
|
5753
5761
|
*/
|
|
5754
5762
|
async resolveModelsDir() {
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5763
|
+
const { join } = await import("node:path");
|
|
5764
|
+
const { promises: fsp } = await import("node:fs");
|
|
5765
|
+
const dir = join(this.ctx.nodeDataDir, "models");
|
|
5766
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
5767
|
+
return dir;
|
|
5759
5768
|
}
|
|
5760
5769
|
/**
|
|
5761
5770
|
* Access the runtime capability registry for in-process provider lookups.
|
|
@@ -7113,6 +7122,20 @@ var EncodeProfileSchema = object({
|
|
|
7113
7122
|
*/
|
|
7114
7123
|
outputArgs: array(string()).optional()
|
|
7115
7124
|
});
|
|
7125
|
+
/** Cosine similarity between two embedding vectors */
|
|
7126
|
+
function cosineSimilarity(a, b) {
|
|
7127
|
+
if (a.length !== b.length) return 0;
|
|
7128
|
+
let dotProduct = 0;
|
|
7129
|
+
let normA = 0;
|
|
7130
|
+
let normB = 0;
|
|
7131
|
+
for (let i = 0; i < a.length; i++) {
|
|
7132
|
+
dotProduct += a[i] * b[i];
|
|
7133
|
+
normA += a[i] * a[i];
|
|
7134
|
+
normB += b[i] * b[i];
|
|
7135
|
+
}
|
|
7136
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
7137
|
+
return denom === 0 ? 0 : dotProduct / denom;
|
|
7138
|
+
}
|
|
7116
7139
|
var YAMNET_TO_MACRO = {
|
|
7117
7140
|
mapping: {
|
|
7118
7141
|
Speech: "speech",
|
|
@@ -12651,7 +12674,8 @@ method(object({
|
|
|
12651
12674
|
sessionId: string().optional()
|
|
12652
12675
|
}), ConvertResultSchema, {
|
|
12653
12676
|
kind: "mutation",
|
|
12654
|
-
auth: "admin"
|
|
12677
|
+
auth: "admin",
|
|
12678
|
+
timeoutMs: 6e5
|
|
12655
12679
|
});
|
|
12656
12680
|
var AddonHttpRouteSchema = object({
|
|
12657
12681
|
method: _enum([
|
|
@@ -14195,7 +14219,14 @@ var NotificationRuleConditionsSchema = object({
|
|
|
14195
14219
|
/** Match against `event.data.eventType` token (e.g. `'press_long'`). When non-empty, only events
|
|
14196
14220
|
* carrying a matching `data.eventType` string pass this condition. Rules without this field are
|
|
14197
14221
|
* unaffected (back-compat). Distinct from `rule.eventTypes` which holds EventCategory strings. */
|
|
14198
|
-
eventTypeTokens: array(string()).readonly().optional()
|
|
14222
|
+
eventTypeTokens: array(string()).readonly().optional(),
|
|
14223
|
+
/** Match detections whose CLIP image embedding is semantically similar to this free-text
|
|
14224
|
+
* description. Requires the embedding-encoder cap to have pre-warmed the text vector.
|
|
14225
|
+
* `minSimilarity` is the cosine similarity threshold in [0, 1]. */
|
|
14226
|
+
clipDescription: object({
|
|
14227
|
+
text: string().min(1),
|
|
14228
|
+
minSimilarity: number().min(0).max(1)
|
|
14229
|
+
}).optional()
|
|
14199
14230
|
});
|
|
14200
14231
|
var NotificationRuleTemplateSchema = object({
|
|
14201
14232
|
title: string(),
|
|
@@ -14449,6 +14480,16 @@ var TrackedDetectionSchema = object({
|
|
|
14449
14480
|
zones: array(string()).readonly(),
|
|
14450
14481
|
state: TrackStateSchema
|
|
14451
14482
|
});
|
|
14483
|
+
var ScoredObjectEventSchema = ObjectEventSchema.extend({ score: number() });
|
|
14484
|
+
var SearchObjectEventsInput = object({
|
|
14485
|
+
text: string(),
|
|
14486
|
+
deviceId: number().optional(),
|
|
14487
|
+
since: number().optional(),
|
|
14488
|
+
until: number().optional(),
|
|
14489
|
+
classFilter: string().optional(),
|
|
14490
|
+
limit: number().default(50),
|
|
14491
|
+
minScore: number().min(0).max(1).default(.2)
|
|
14492
|
+
});
|
|
14452
14493
|
DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).readonly()), method(object({
|
|
14453
14494
|
deviceId: number(),
|
|
14454
14495
|
trackId: string()
|
|
@@ -14483,7 +14524,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
14483
14524
|
}), method(object({
|
|
14484
14525
|
eventId: string(),
|
|
14485
14526
|
kind: MediaFileKindEnum.optional()
|
|
14486
|
-
}), array(MediaFileSchema).readonly()), method(object({ trackId: string() }), array(MediaFileSchema).readonly()), object({
|
|
14527
|
+
}), array(MediaFileSchema).readonly()), method(object({ trackId: string() }), array(MediaFileSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), object({
|
|
14487
14528
|
deviceId: number(),
|
|
14488
14529
|
timestamp: number(),
|
|
14489
14530
|
frameWidth: number(),
|
|
@@ -19133,6 +19174,12 @@ Object.freeze({
|
|
|
19133
19174
|
addonId: null,
|
|
19134
19175
|
access: "create"
|
|
19135
19176
|
},
|
|
19177
|
+
"pipelineAnalytics.searchObjectEvents": {
|
|
19178
|
+
capName: "pipeline-analytics",
|
|
19179
|
+
capScope: "device",
|
|
19180
|
+
addonId: null,
|
|
19181
|
+
access: "view"
|
|
19182
|
+
},
|
|
19136
19183
|
"pipelineExecutor.cacheFrameInPool": {
|
|
19137
19184
|
capName: "pipeline-executor",
|
|
19138
19185
|
capScope: "system",
|
|
@@ -21055,6 +21102,17 @@ var RuleStore = class {
|
|
|
21055
21102
|
//#endregion
|
|
21056
21103
|
//#region src/rules/rule-engine.ts
|
|
21057
21104
|
var RuleEngine = class {
|
|
21105
|
+
clip;
|
|
21106
|
+
constructor(clip) {
|
|
21107
|
+
this.clip = {
|
|
21108
|
+
textEmbeddingCache: clip?.textEmbeddingCache ?? /* @__PURE__ */ new Map(),
|
|
21109
|
+
encoderModelId: clip?.encoderModelId
|
|
21110
|
+
};
|
|
21111
|
+
}
|
|
21112
|
+
/** Update the active encoder's modelId after the cache is warmed. */
|
|
21113
|
+
updateEncoderModelId(modelId) {
|
|
21114
|
+
this.clip.encoderModelId = modelId;
|
|
21115
|
+
}
|
|
21058
21116
|
evaluate(event, rules) {
|
|
21059
21117
|
return rules.filter((rule) => this.matchesRule(event, rule));
|
|
21060
21118
|
}
|
|
@@ -21099,6 +21157,17 @@ var RuleEngine = class {
|
|
|
21099
21157
|
if (typeof rawToken !== "string") return false;
|
|
21100
21158
|
if (!conditions.eventTypeTokens.includes(rawToken)) return false;
|
|
21101
21159
|
}
|
|
21160
|
+
if (conditions.clipDescription !== void 0) {
|
|
21161
|
+
const { text, minSimilarity } = conditions.clipDescription;
|
|
21162
|
+
const eventModelId = data["embeddingModelId"];
|
|
21163
|
+
if (typeof eventModelId !== "string") return false;
|
|
21164
|
+
if (this.clip.encoderModelId !== void 0 && eventModelId !== this.clip.encoderModelId) return false;
|
|
21165
|
+
const textVec = this.clip.textEmbeddingCache.get(text);
|
|
21166
|
+
if (textVec === void 0) return false;
|
|
21167
|
+
const rawEmbedding = data["embedding"];
|
|
21168
|
+
if (!Array.isArray(rawEmbedding)) return false;
|
|
21169
|
+
if (cosineSimilarity(new Float32Array(textVec), new Float32Array(rawEmbedding)) < minSimilarity) return false;
|
|
21170
|
+
}
|
|
21102
21171
|
return true;
|
|
21103
21172
|
}
|
|
21104
21173
|
};
|
|
@@ -21223,6 +21292,8 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21223
21292
|
outputs = [];
|
|
21224
21293
|
auditLog;
|
|
21225
21294
|
unsubscribers = [];
|
|
21295
|
+
clipTextCache = /* @__PURE__ */ new Map();
|
|
21296
|
+
clipEncoderModelId = void 0;
|
|
21226
21297
|
notifier = {
|
|
21227
21298
|
getRules: async () => ({ rules: this.cachedRules }),
|
|
21228
21299
|
upsertRule: async ({ rule }) => {
|
|
@@ -21234,6 +21305,8 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21234
21305
|
...this.cachedRules.slice(idx + 1)
|
|
21235
21306
|
];
|
|
21236
21307
|
else this.cachedRules = [...this.cachedRules, rule];
|
|
21308
|
+
const api = this.ctx.api;
|
|
21309
|
+
if (api) await this.warmClipCache(this.cachedRules, api);
|
|
21237
21310
|
return { success: true };
|
|
21238
21311
|
},
|
|
21239
21312
|
deleteRule: async ({ ruleId }) => {
|
|
@@ -21262,7 +21335,10 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21262
21335
|
}
|
|
21263
21336
|
};
|
|
21264
21337
|
async onInitialize() {
|
|
21265
|
-
this.ruleEngine = new RuleEngine(
|
|
21338
|
+
this.ruleEngine = new RuleEngine({
|
|
21339
|
+
textEmbeddingCache: this.clipTextCache,
|
|
21340
|
+
encoderModelId: this.clipEncoderModelId
|
|
21341
|
+
});
|
|
21266
21342
|
this.cooldown = new CooldownTracker();
|
|
21267
21343
|
this.consoleOutput = new ConsoleOutput(this.ctx.logger.child("console-output"));
|
|
21268
21344
|
this.outputs = [this.consoleOutput];
|
|
@@ -21272,6 +21348,7 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21272
21348
|
this.auditLog = new AuditLog(api);
|
|
21273
21349
|
this.cachedRules = await this.ruleStore.getRules();
|
|
21274
21350
|
this.ctx.logger.info("Loaded notification rules", { meta: { count: this.cachedRules.length } });
|
|
21351
|
+
await this.warmClipCache(this.cachedRules, api);
|
|
21275
21352
|
} else this.ctx.logger.warn("No ctx.api — rules will not be persisted");
|
|
21276
21353
|
for (const category of EVENT_CATEGORIES) {
|
|
21277
21354
|
const unsubscribe = this.ctx.eventBus.subscribe({ category }, (event) => {
|
|
@@ -21341,6 +21418,53 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21341
21418
|
]
|
|
21342
21419
|
}] });
|
|
21343
21420
|
}
|
|
21421
|
+
/**
|
|
21422
|
+
* Pre-warm the CLIP text embedding cache for all `clipDescription.text`
|
|
21423
|
+
* values present in the current rule set.
|
|
21424
|
+
*
|
|
21425
|
+
* Called once at boot (after rules are loaded) and again after any
|
|
21426
|
+
* `upsertRule` mutation. Fetches the encoder's `modelId` via `getInfo` so
|
|
21427
|
+
* the evaluation gate (`embeddingModelId` on the detection event) is always
|
|
21428
|
+
* aligned with the active CLIP variant.
|
|
21429
|
+
*
|
|
21430
|
+
* This is the ONLY async boundary for CLIP in the notifier; `evaluateConditions`
|
|
21431
|
+
* in `RuleEngine` stays synchronous by doing an O(1) `Map.get()` lookup.
|
|
21432
|
+
*/
|
|
21433
|
+
async warmClipCache(rules, api) {
|
|
21434
|
+
const texts = /* @__PURE__ */ new Set();
|
|
21435
|
+
for (const rule of rules) {
|
|
21436
|
+
const text = rule.conditions.clipDescription?.text;
|
|
21437
|
+
if (text !== void 0) texts.add(text);
|
|
21438
|
+
}
|
|
21439
|
+
if (texts.size === 0) return;
|
|
21440
|
+
const info = await api.embeddingEncoder.getInfo.query().catch((err) => {
|
|
21441
|
+
this.ctx.logger.warn("warmClipCache: embeddingEncoder.getInfo failed", { meta: { error: String(err) } });
|
|
21442
|
+
return null;
|
|
21443
|
+
});
|
|
21444
|
+
if (!info?.ready) {
|
|
21445
|
+
this.ctx.logger.debug("warmClipCache: embedding encoder not ready — cache not warmed", { meta: { pendingTexts: texts.size } });
|
|
21446
|
+
return;
|
|
21447
|
+
}
|
|
21448
|
+
this.ruleEngine.updateEncoderModelId(info.modelId);
|
|
21449
|
+
this.clipEncoderModelId = info.modelId;
|
|
21450
|
+
for (const text of texts) {
|
|
21451
|
+
if (this.clipTextCache.has(text)) continue;
|
|
21452
|
+
const result = await api.embeddingEncoder.encodeText.query({ text }).catch((err) => {
|
|
21453
|
+
this.ctx.logger.warn("warmClipCache: encodeText failed", { meta: {
|
|
21454
|
+
text,
|
|
21455
|
+
error: String(err)
|
|
21456
|
+
} });
|
|
21457
|
+
return null;
|
|
21458
|
+
});
|
|
21459
|
+
if (result) {
|
|
21460
|
+
this.clipTextCache.set(text, result.embedding);
|
|
21461
|
+
this.ctx.logger.debug("warmClipCache: cached text embedding", { meta: {
|
|
21462
|
+
text,
|
|
21463
|
+
modelId: info.modelId
|
|
21464
|
+
} });
|
|
21465
|
+
}
|
|
21466
|
+
}
|
|
21467
|
+
}
|
|
21344
21468
|
async handleEvent(event) {
|
|
21345
21469
|
const matchedRules = this.ruleEngine.evaluate(event, this.cachedRules);
|
|
21346
21470
|
for (const rule of matchedRules) {
|
package/dist/addon.mjs
CHANGED
|
@@ -4628,7 +4628,7 @@ function _instanceof(cls, params = {}) {
|
|
|
4628
4628
|
return inst;
|
|
4629
4629
|
}
|
|
4630
4630
|
//#endregion
|
|
4631
|
-
//#region ../types/dist/sleep-
|
|
4631
|
+
//#region ../types/dist/sleep-NOH4yRwj.mjs
|
|
4632
4632
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4633
4633
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4634
4634
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -5743,15 +5743,24 @@ var BaseAddon = class {
|
|
|
5743
5743
|
} catch {}
|
|
5744
5744
|
}
|
|
5745
5745
|
/**
|
|
5746
|
-
* Resolve the shared models directory
|
|
5747
|
-
*
|
|
5748
|
-
*
|
|
5746
|
+
* Resolve the shared, node-local models directory: `<nodeDataDir>/models`.
|
|
5747
|
+
*
|
|
5748
|
+
* This is the SINGLE centralized models-dir resolver for every inference addon
|
|
5749
|
+
* (detection-pipeline, audio-analyzer, embedding-encoder, model-studio). It is
|
|
5750
|
+
* anchored at the node's own writable data root (`ctx.nodeDataDir`, i.e.
|
|
5751
|
+
* `CAMSTACK_DATA` ?? the kernel boot dir) — never the hub-singleton `storage`
|
|
5752
|
+
* cap, which returns the hub's `/data/models` to every node and breaks any node
|
|
5753
|
+
* whose real data dir differs (e.g. the macOS electron agent).
|
|
5754
|
+
*
|
|
5755
|
+
* `node:path`/`node:fs` are imported dynamically so the `@camstack/types` root
|
|
5756
|
+
* barrel (re-exported into browser bundles) stays free of node builtins.
|
|
5749
5757
|
*/
|
|
5750
5758
|
async resolveModelsDir() {
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5759
|
+
const { join } = await import("node:path");
|
|
5760
|
+
const { promises: fsp } = await import("node:fs");
|
|
5761
|
+
const dir = join(this.ctx.nodeDataDir, "models");
|
|
5762
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
5763
|
+
return dir;
|
|
5755
5764
|
}
|
|
5756
5765
|
/**
|
|
5757
5766
|
* Access the runtime capability registry for in-process provider lookups.
|
|
@@ -7109,6 +7118,20 @@ var EncodeProfileSchema = object({
|
|
|
7109
7118
|
*/
|
|
7110
7119
|
outputArgs: array(string()).optional()
|
|
7111
7120
|
});
|
|
7121
|
+
/** Cosine similarity between two embedding vectors */
|
|
7122
|
+
function cosineSimilarity(a, b) {
|
|
7123
|
+
if (a.length !== b.length) return 0;
|
|
7124
|
+
let dotProduct = 0;
|
|
7125
|
+
let normA = 0;
|
|
7126
|
+
let normB = 0;
|
|
7127
|
+
for (let i = 0; i < a.length; i++) {
|
|
7128
|
+
dotProduct += a[i] * b[i];
|
|
7129
|
+
normA += a[i] * a[i];
|
|
7130
|
+
normB += b[i] * b[i];
|
|
7131
|
+
}
|
|
7132
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
7133
|
+
return denom === 0 ? 0 : dotProduct / denom;
|
|
7134
|
+
}
|
|
7112
7135
|
var YAMNET_TO_MACRO = {
|
|
7113
7136
|
mapping: {
|
|
7114
7137
|
Speech: "speech",
|
|
@@ -12647,7 +12670,8 @@ method(object({
|
|
|
12647
12670
|
sessionId: string().optional()
|
|
12648
12671
|
}), ConvertResultSchema, {
|
|
12649
12672
|
kind: "mutation",
|
|
12650
|
-
auth: "admin"
|
|
12673
|
+
auth: "admin",
|
|
12674
|
+
timeoutMs: 6e5
|
|
12651
12675
|
});
|
|
12652
12676
|
var AddonHttpRouteSchema = object({
|
|
12653
12677
|
method: _enum([
|
|
@@ -14191,7 +14215,14 @@ var NotificationRuleConditionsSchema = object({
|
|
|
14191
14215
|
/** Match against `event.data.eventType` token (e.g. `'press_long'`). When non-empty, only events
|
|
14192
14216
|
* carrying a matching `data.eventType` string pass this condition. Rules without this field are
|
|
14193
14217
|
* unaffected (back-compat). Distinct from `rule.eventTypes` which holds EventCategory strings. */
|
|
14194
|
-
eventTypeTokens: array(string()).readonly().optional()
|
|
14218
|
+
eventTypeTokens: array(string()).readonly().optional(),
|
|
14219
|
+
/** Match detections whose CLIP image embedding is semantically similar to this free-text
|
|
14220
|
+
* description. Requires the embedding-encoder cap to have pre-warmed the text vector.
|
|
14221
|
+
* `minSimilarity` is the cosine similarity threshold in [0, 1]. */
|
|
14222
|
+
clipDescription: object({
|
|
14223
|
+
text: string().min(1),
|
|
14224
|
+
minSimilarity: number().min(0).max(1)
|
|
14225
|
+
}).optional()
|
|
14195
14226
|
});
|
|
14196
14227
|
var NotificationRuleTemplateSchema = object({
|
|
14197
14228
|
title: string(),
|
|
@@ -14445,6 +14476,16 @@ var TrackedDetectionSchema = object({
|
|
|
14445
14476
|
zones: array(string()).readonly(),
|
|
14446
14477
|
state: TrackStateSchema
|
|
14447
14478
|
});
|
|
14479
|
+
var ScoredObjectEventSchema = ObjectEventSchema.extend({ score: number() });
|
|
14480
|
+
var SearchObjectEventsInput = object({
|
|
14481
|
+
text: string(),
|
|
14482
|
+
deviceId: number().optional(),
|
|
14483
|
+
since: number().optional(),
|
|
14484
|
+
until: number().optional(),
|
|
14485
|
+
classFilter: string().optional(),
|
|
14486
|
+
limit: number().default(50),
|
|
14487
|
+
minScore: number().min(0).max(1).default(.2)
|
|
14488
|
+
});
|
|
14448
14489
|
DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).readonly()), method(object({
|
|
14449
14490
|
deviceId: number(),
|
|
14450
14491
|
trackId: string()
|
|
@@ -14479,7 +14520,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
14479
14520
|
}), method(object({
|
|
14480
14521
|
eventId: string(),
|
|
14481
14522
|
kind: MediaFileKindEnum.optional()
|
|
14482
|
-
}), array(MediaFileSchema).readonly()), method(object({ trackId: string() }), array(MediaFileSchema).readonly()), object({
|
|
14523
|
+
}), array(MediaFileSchema).readonly()), method(object({ trackId: string() }), array(MediaFileSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), object({
|
|
14483
14524
|
deviceId: number(),
|
|
14484
14525
|
timestamp: number(),
|
|
14485
14526
|
frameWidth: number(),
|
|
@@ -19129,6 +19170,12 @@ Object.freeze({
|
|
|
19129
19170
|
addonId: null,
|
|
19130
19171
|
access: "create"
|
|
19131
19172
|
},
|
|
19173
|
+
"pipelineAnalytics.searchObjectEvents": {
|
|
19174
|
+
capName: "pipeline-analytics",
|
|
19175
|
+
capScope: "device",
|
|
19176
|
+
addonId: null,
|
|
19177
|
+
access: "view"
|
|
19178
|
+
},
|
|
19132
19179
|
"pipelineExecutor.cacheFrameInPool": {
|
|
19133
19180
|
capName: "pipeline-executor",
|
|
19134
19181
|
capScope: "system",
|
|
@@ -21051,6 +21098,17 @@ var RuleStore = class {
|
|
|
21051
21098
|
//#endregion
|
|
21052
21099
|
//#region src/rules/rule-engine.ts
|
|
21053
21100
|
var RuleEngine = class {
|
|
21101
|
+
clip;
|
|
21102
|
+
constructor(clip) {
|
|
21103
|
+
this.clip = {
|
|
21104
|
+
textEmbeddingCache: clip?.textEmbeddingCache ?? /* @__PURE__ */ new Map(),
|
|
21105
|
+
encoderModelId: clip?.encoderModelId
|
|
21106
|
+
};
|
|
21107
|
+
}
|
|
21108
|
+
/** Update the active encoder's modelId after the cache is warmed. */
|
|
21109
|
+
updateEncoderModelId(modelId) {
|
|
21110
|
+
this.clip.encoderModelId = modelId;
|
|
21111
|
+
}
|
|
21054
21112
|
evaluate(event, rules) {
|
|
21055
21113
|
return rules.filter((rule) => this.matchesRule(event, rule));
|
|
21056
21114
|
}
|
|
@@ -21095,6 +21153,17 @@ var RuleEngine = class {
|
|
|
21095
21153
|
if (typeof rawToken !== "string") return false;
|
|
21096
21154
|
if (!conditions.eventTypeTokens.includes(rawToken)) return false;
|
|
21097
21155
|
}
|
|
21156
|
+
if (conditions.clipDescription !== void 0) {
|
|
21157
|
+
const { text, minSimilarity } = conditions.clipDescription;
|
|
21158
|
+
const eventModelId = data["embeddingModelId"];
|
|
21159
|
+
if (typeof eventModelId !== "string") return false;
|
|
21160
|
+
if (this.clip.encoderModelId !== void 0 && eventModelId !== this.clip.encoderModelId) return false;
|
|
21161
|
+
const textVec = this.clip.textEmbeddingCache.get(text);
|
|
21162
|
+
if (textVec === void 0) return false;
|
|
21163
|
+
const rawEmbedding = data["embedding"];
|
|
21164
|
+
if (!Array.isArray(rawEmbedding)) return false;
|
|
21165
|
+
if (cosineSimilarity(new Float32Array(textVec), new Float32Array(rawEmbedding)) < minSimilarity) return false;
|
|
21166
|
+
}
|
|
21098
21167
|
return true;
|
|
21099
21168
|
}
|
|
21100
21169
|
};
|
|
@@ -21219,6 +21288,8 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21219
21288
|
outputs = [];
|
|
21220
21289
|
auditLog;
|
|
21221
21290
|
unsubscribers = [];
|
|
21291
|
+
clipTextCache = /* @__PURE__ */ new Map();
|
|
21292
|
+
clipEncoderModelId = void 0;
|
|
21222
21293
|
notifier = {
|
|
21223
21294
|
getRules: async () => ({ rules: this.cachedRules }),
|
|
21224
21295
|
upsertRule: async ({ rule }) => {
|
|
@@ -21230,6 +21301,8 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21230
21301
|
...this.cachedRules.slice(idx + 1)
|
|
21231
21302
|
];
|
|
21232
21303
|
else this.cachedRules = [...this.cachedRules, rule];
|
|
21304
|
+
const api = this.ctx.api;
|
|
21305
|
+
if (api) await this.warmClipCache(this.cachedRules, api);
|
|
21233
21306
|
return { success: true };
|
|
21234
21307
|
},
|
|
21235
21308
|
deleteRule: async ({ ruleId }) => {
|
|
@@ -21258,7 +21331,10 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21258
21331
|
}
|
|
21259
21332
|
};
|
|
21260
21333
|
async onInitialize() {
|
|
21261
|
-
this.ruleEngine = new RuleEngine(
|
|
21334
|
+
this.ruleEngine = new RuleEngine({
|
|
21335
|
+
textEmbeddingCache: this.clipTextCache,
|
|
21336
|
+
encoderModelId: this.clipEncoderModelId
|
|
21337
|
+
});
|
|
21262
21338
|
this.cooldown = new CooldownTracker();
|
|
21263
21339
|
this.consoleOutput = new ConsoleOutput(this.ctx.logger.child("console-output"));
|
|
21264
21340
|
this.outputs = [this.consoleOutput];
|
|
@@ -21268,6 +21344,7 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21268
21344
|
this.auditLog = new AuditLog(api);
|
|
21269
21345
|
this.cachedRules = await this.ruleStore.getRules();
|
|
21270
21346
|
this.ctx.logger.info("Loaded notification rules", { meta: { count: this.cachedRules.length } });
|
|
21347
|
+
await this.warmClipCache(this.cachedRules, api);
|
|
21271
21348
|
} else this.ctx.logger.warn("No ctx.api — rules will not be persisted");
|
|
21272
21349
|
for (const category of EVENT_CATEGORIES) {
|
|
21273
21350
|
const unsubscribe = this.ctx.eventBus.subscribe({ category }, (event) => {
|
|
@@ -21337,6 +21414,53 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21337
21414
|
]
|
|
21338
21415
|
}] });
|
|
21339
21416
|
}
|
|
21417
|
+
/**
|
|
21418
|
+
* Pre-warm the CLIP text embedding cache for all `clipDescription.text`
|
|
21419
|
+
* values present in the current rule set.
|
|
21420
|
+
*
|
|
21421
|
+
* Called once at boot (after rules are loaded) and again after any
|
|
21422
|
+
* `upsertRule` mutation. Fetches the encoder's `modelId` via `getInfo` so
|
|
21423
|
+
* the evaluation gate (`embeddingModelId` on the detection event) is always
|
|
21424
|
+
* aligned with the active CLIP variant.
|
|
21425
|
+
*
|
|
21426
|
+
* This is the ONLY async boundary for CLIP in the notifier; `evaluateConditions`
|
|
21427
|
+
* in `RuleEngine` stays synchronous by doing an O(1) `Map.get()` lookup.
|
|
21428
|
+
*/
|
|
21429
|
+
async warmClipCache(rules, api) {
|
|
21430
|
+
const texts = /* @__PURE__ */ new Set();
|
|
21431
|
+
for (const rule of rules) {
|
|
21432
|
+
const text = rule.conditions.clipDescription?.text;
|
|
21433
|
+
if (text !== void 0) texts.add(text);
|
|
21434
|
+
}
|
|
21435
|
+
if (texts.size === 0) return;
|
|
21436
|
+
const info = await api.embeddingEncoder.getInfo.query().catch((err) => {
|
|
21437
|
+
this.ctx.logger.warn("warmClipCache: embeddingEncoder.getInfo failed", { meta: { error: String(err) } });
|
|
21438
|
+
return null;
|
|
21439
|
+
});
|
|
21440
|
+
if (!info?.ready) {
|
|
21441
|
+
this.ctx.logger.debug("warmClipCache: embedding encoder not ready — cache not warmed", { meta: { pendingTexts: texts.size } });
|
|
21442
|
+
return;
|
|
21443
|
+
}
|
|
21444
|
+
this.ruleEngine.updateEncoderModelId(info.modelId);
|
|
21445
|
+
this.clipEncoderModelId = info.modelId;
|
|
21446
|
+
for (const text of texts) {
|
|
21447
|
+
if (this.clipTextCache.has(text)) continue;
|
|
21448
|
+
const result = await api.embeddingEncoder.encodeText.query({ text }).catch((err) => {
|
|
21449
|
+
this.ctx.logger.warn("warmClipCache: encodeText failed", { meta: {
|
|
21450
|
+
text,
|
|
21451
|
+
error: String(err)
|
|
21452
|
+
} });
|
|
21453
|
+
return null;
|
|
21454
|
+
});
|
|
21455
|
+
if (result) {
|
|
21456
|
+
this.clipTextCache.set(text, result.embedding);
|
|
21457
|
+
this.ctx.logger.debug("warmClipCache: cached text embedding", { meta: {
|
|
21458
|
+
text,
|
|
21459
|
+
modelId: info.modelId
|
|
21460
|
+
} });
|
|
21461
|
+
}
|
|
21462
|
+
}
|
|
21463
|
+
}
|
|
21340
21464
|
async handleEvent(event) {
|
|
21341
21465
|
const matchedRules = this.ruleEngine.evaluate(event, this.cachedRules);
|
|
21342
21466
|
for (const rule of matchedRules) {
|