@camstack/addon-advanced-notifier 1.1.4 → 1.1.6
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 +120 -5
- package/dist/addon.mjs +120 -5
- 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";
|
|
@@ -7122,6 +7122,20 @@ var EncodeProfileSchema = object({
|
|
|
7122
7122
|
*/
|
|
7123
7123
|
outputArgs: array(string()).optional()
|
|
7124
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
|
+
}
|
|
7125
7139
|
var YAMNET_TO_MACRO = {
|
|
7126
7140
|
mapping: {
|
|
7127
7141
|
Speech: "speech",
|
|
@@ -12660,7 +12674,8 @@ method(object({
|
|
|
12660
12674
|
sessionId: string().optional()
|
|
12661
12675
|
}), ConvertResultSchema, {
|
|
12662
12676
|
kind: "mutation",
|
|
12663
|
-
auth: "admin"
|
|
12677
|
+
auth: "admin",
|
|
12678
|
+
timeoutMs: 6e5
|
|
12664
12679
|
});
|
|
12665
12680
|
var AddonHttpRouteSchema = object({
|
|
12666
12681
|
method: _enum([
|
|
@@ -14204,7 +14219,14 @@ var NotificationRuleConditionsSchema = object({
|
|
|
14204
14219
|
/** Match against `event.data.eventType` token (e.g. `'press_long'`). When non-empty, only events
|
|
14205
14220
|
* carrying a matching `data.eventType` string pass this condition. Rules without this field are
|
|
14206
14221
|
* unaffected (back-compat). Distinct from `rule.eventTypes` which holds EventCategory strings. */
|
|
14207
|
-
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()
|
|
14208
14230
|
});
|
|
14209
14231
|
var NotificationRuleTemplateSchema = object({
|
|
14210
14232
|
title: string(),
|
|
@@ -14458,6 +14480,16 @@ var TrackedDetectionSchema = object({
|
|
|
14458
14480
|
zones: array(string()).readonly(),
|
|
14459
14481
|
state: TrackStateSchema
|
|
14460
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
|
+
});
|
|
14461
14493
|
DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).readonly()), method(object({
|
|
14462
14494
|
deviceId: number(),
|
|
14463
14495
|
trackId: string()
|
|
@@ -14492,7 +14524,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
14492
14524
|
}), method(object({
|
|
14493
14525
|
eventId: string(),
|
|
14494
14526
|
kind: MediaFileKindEnum.optional()
|
|
14495
|
-
}), 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({
|
|
14496
14528
|
deviceId: number(),
|
|
14497
14529
|
timestamp: number(),
|
|
14498
14530
|
frameWidth: number(),
|
|
@@ -19142,6 +19174,12 @@ Object.freeze({
|
|
|
19142
19174
|
addonId: null,
|
|
19143
19175
|
access: "create"
|
|
19144
19176
|
},
|
|
19177
|
+
"pipelineAnalytics.searchObjectEvents": {
|
|
19178
|
+
capName: "pipeline-analytics",
|
|
19179
|
+
capScope: "device",
|
|
19180
|
+
addonId: null,
|
|
19181
|
+
access: "view"
|
|
19182
|
+
},
|
|
19145
19183
|
"pipelineExecutor.cacheFrameInPool": {
|
|
19146
19184
|
capName: "pipeline-executor",
|
|
19147
19185
|
capScope: "system",
|
|
@@ -21064,6 +21102,17 @@ var RuleStore = class {
|
|
|
21064
21102
|
//#endregion
|
|
21065
21103
|
//#region src/rules/rule-engine.ts
|
|
21066
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
|
+
}
|
|
21067
21116
|
evaluate(event, rules) {
|
|
21068
21117
|
return rules.filter((rule) => this.matchesRule(event, rule));
|
|
21069
21118
|
}
|
|
@@ -21108,6 +21157,17 @@ var RuleEngine = class {
|
|
|
21108
21157
|
if (typeof rawToken !== "string") return false;
|
|
21109
21158
|
if (!conditions.eventTypeTokens.includes(rawToken)) return false;
|
|
21110
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
|
+
}
|
|
21111
21171
|
return true;
|
|
21112
21172
|
}
|
|
21113
21173
|
};
|
|
@@ -21232,6 +21292,8 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21232
21292
|
outputs = [];
|
|
21233
21293
|
auditLog;
|
|
21234
21294
|
unsubscribers = [];
|
|
21295
|
+
clipTextCache = /* @__PURE__ */ new Map();
|
|
21296
|
+
clipEncoderModelId = void 0;
|
|
21235
21297
|
notifier = {
|
|
21236
21298
|
getRules: async () => ({ rules: this.cachedRules }),
|
|
21237
21299
|
upsertRule: async ({ rule }) => {
|
|
@@ -21243,6 +21305,8 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21243
21305
|
...this.cachedRules.slice(idx + 1)
|
|
21244
21306
|
];
|
|
21245
21307
|
else this.cachedRules = [...this.cachedRules, rule];
|
|
21308
|
+
const api = this.ctx.api;
|
|
21309
|
+
if (api) await this.warmClipCache(this.cachedRules, api);
|
|
21246
21310
|
return { success: true };
|
|
21247
21311
|
},
|
|
21248
21312
|
deleteRule: async ({ ruleId }) => {
|
|
@@ -21271,7 +21335,10 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21271
21335
|
}
|
|
21272
21336
|
};
|
|
21273
21337
|
async onInitialize() {
|
|
21274
|
-
this.ruleEngine = new RuleEngine(
|
|
21338
|
+
this.ruleEngine = new RuleEngine({
|
|
21339
|
+
textEmbeddingCache: this.clipTextCache,
|
|
21340
|
+
encoderModelId: this.clipEncoderModelId
|
|
21341
|
+
});
|
|
21275
21342
|
this.cooldown = new CooldownTracker();
|
|
21276
21343
|
this.consoleOutput = new ConsoleOutput(this.ctx.logger.child("console-output"));
|
|
21277
21344
|
this.outputs = [this.consoleOutput];
|
|
@@ -21281,6 +21348,7 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21281
21348
|
this.auditLog = new AuditLog(api);
|
|
21282
21349
|
this.cachedRules = await this.ruleStore.getRules();
|
|
21283
21350
|
this.ctx.logger.info("Loaded notification rules", { meta: { count: this.cachedRules.length } });
|
|
21351
|
+
await this.warmClipCache(this.cachedRules, api);
|
|
21284
21352
|
} else this.ctx.logger.warn("No ctx.api — rules will not be persisted");
|
|
21285
21353
|
for (const category of EVENT_CATEGORIES) {
|
|
21286
21354
|
const unsubscribe = this.ctx.eventBus.subscribe({ category }, (event) => {
|
|
@@ -21350,6 +21418,53 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21350
21418
|
]
|
|
21351
21419
|
}] });
|
|
21352
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
|
+
}
|
|
21353
21468
|
async handleEvent(event) {
|
|
21354
21469
|
const matchedRules = this.ruleEngine.evaluate(event, this.cachedRules);
|
|
21355
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";
|
|
@@ -7118,6 +7118,20 @@ var EncodeProfileSchema = object({
|
|
|
7118
7118
|
*/
|
|
7119
7119
|
outputArgs: array(string()).optional()
|
|
7120
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
|
+
}
|
|
7121
7135
|
var YAMNET_TO_MACRO = {
|
|
7122
7136
|
mapping: {
|
|
7123
7137
|
Speech: "speech",
|
|
@@ -12656,7 +12670,8 @@ method(object({
|
|
|
12656
12670
|
sessionId: string().optional()
|
|
12657
12671
|
}), ConvertResultSchema, {
|
|
12658
12672
|
kind: "mutation",
|
|
12659
|
-
auth: "admin"
|
|
12673
|
+
auth: "admin",
|
|
12674
|
+
timeoutMs: 6e5
|
|
12660
12675
|
});
|
|
12661
12676
|
var AddonHttpRouteSchema = object({
|
|
12662
12677
|
method: _enum([
|
|
@@ -14200,7 +14215,14 @@ var NotificationRuleConditionsSchema = object({
|
|
|
14200
14215
|
/** Match against `event.data.eventType` token (e.g. `'press_long'`). When non-empty, only events
|
|
14201
14216
|
* carrying a matching `data.eventType` string pass this condition. Rules without this field are
|
|
14202
14217
|
* unaffected (back-compat). Distinct from `rule.eventTypes` which holds EventCategory strings. */
|
|
14203
|
-
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()
|
|
14204
14226
|
});
|
|
14205
14227
|
var NotificationRuleTemplateSchema = object({
|
|
14206
14228
|
title: string(),
|
|
@@ -14454,6 +14476,16 @@ var TrackedDetectionSchema = object({
|
|
|
14454
14476
|
zones: array(string()).readonly(),
|
|
14455
14477
|
state: TrackStateSchema
|
|
14456
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
|
+
});
|
|
14457
14489
|
DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).readonly()), method(object({
|
|
14458
14490
|
deviceId: number(),
|
|
14459
14491
|
trackId: string()
|
|
@@ -14488,7 +14520,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
14488
14520
|
}), method(object({
|
|
14489
14521
|
eventId: string(),
|
|
14490
14522
|
kind: MediaFileKindEnum.optional()
|
|
14491
|
-
}), 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({
|
|
14492
14524
|
deviceId: number(),
|
|
14493
14525
|
timestamp: number(),
|
|
14494
14526
|
frameWidth: number(),
|
|
@@ -19138,6 +19170,12 @@ Object.freeze({
|
|
|
19138
19170
|
addonId: null,
|
|
19139
19171
|
access: "create"
|
|
19140
19172
|
},
|
|
19173
|
+
"pipelineAnalytics.searchObjectEvents": {
|
|
19174
|
+
capName: "pipeline-analytics",
|
|
19175
|
+
capScope: "device",
|
|
19176
|
+
addonId: null,
|
|
19177
|
+
access: "view"
|
|
19178
|
+
},
|
|
19141
19179
|
"pipelineExecutor.cacheFrameInPool": {
|
|
19142
19180
|
capName: "pipeline-executor",
|
|
19143
19181
|
capScope: "system",
|
|
@@ -21060,6 +21098,17 @@ var RuleStore = class {
|
|
|
21060
21098
|
//#endregion
|
|
21061
21099
|
//#region src/rules/rule-engine.ts
|
|
21062
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
|
+
}
|
|
21063
21112
|
evaluate(event, rules) {
|
|
21064
21113
|
return rules.filter((rule) => this.matchesRule(event, rule));
|
|
21065
21114
|
}
|
|
@@ -21104,6 +21153,17 @@ var RuleEngine = class {
|
|
|
21104
21153
|
if (typeof rawToken !== "string") return false;
|
|
21105
21154
|
if (!conditions.eventTypeTokens.includes(rawToken)) return false;
|
|
21106
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
|
+
}
|
|
21107
21167
|
return true;
|
|
21108
21168
|
}
|
|
21109
21169
|
};
|
|
@@ -21228,6 +21288,8 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21228
21288
|
outputs = [];
|
|
21229
21289
|
auditLog;
|
|
21230
21290
|
unsubscribers = [];
|
|
21291
|
+
clipTextCache = /* @__PURE__ */ new Map();
|
|
21292
|
+
clipEncoderModelId = void 0;
|
|
21231
21293
|
notifier = {
|
|
21232
21294
|
getRules: async () => ({ rules: this.cachedRules }),
|
|
21233
21295
|
upsertRule: async ({ rule }) => {
|
|
@@ -21239,6 +21301,8 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21239
21301
|
...this.cachedRules.slice(idx + 1)
|
|
21240
21302
|
];
|
|
21241
21303
|
else this.cachedRules = [...this.cachedRules, rule];
|
|
21304
|
+
const api = this.ctx.api;
|
|
21305
|
+
if (api) await this.warmClipCache(this.cachedRules, api);
|
|
21242
21306
|
return { success: true };
|
|
21243
21307
|
},
|
|
21244
21308
|
deleteRule: async ({ ruleId }) => {
|
|
@@ -21267,7 +21331,10 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21267
21331
|
}
|
|
21268
21332
|
};
|
|
21269
21333
|
async onInitialize() {
|
|
21270
|
-
this.ruleEngine = new RuleEngine(
|
|
21334
|
+
this.ruleEngine = new RuleEngine({
|
|
21335
|
+
textEmbeddingCache: this.clipTextCache,
|
|
21336
|
+
encoderModelId: this.clipEncoderModelId
|
|
21337
|
+
});
|
|
21271
21338
|
this.cooldown = new CooldownTracker();
|
|
21272
21339
|
this.consoleOutput = new ConsoleOutput(this.ctx.logger.child("console-output"));
|
|
21273
21340
|
this.outputs = [this.consoleOutput];
|
|
@@ -21277,6 +21344,7 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21277
21344
|
this.auditLog = new AuditLog(api);
|
|
21278
21345
|
this.cachedRules = await this.ruleStore.getRules();
|
|
21279
21346
|
this.ctx.logger.info("Loaded notification rules", { meta: { count: this.cachedRules.length } });
|
|
21347
|
+
await this.warmClipCache(this.cachedRules, api);
|
|
21280
21348
|
} else this.ctx.logger.warn("No ctx.api — rules will not be persisted");
|
|
21281
21349
|
for (const category of EVENT_CATEGORIES) {
|
|
21282
21350
|
const unsubscribe = this.ctx.eventBus.subscribe({ category }, (event) => {
|
|
@@ -21346,6 +21414,53 @@ var AdvancedNotifierAddon = class extends BaseAddon {
|
|
|
21346
21414
|
]
|
|
21347
21415
|
}] });
|
|
21348
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
|
+
}
|
|
21349
21464
|
async handleEvent(event) {
|
|
21350
21465
|
const matchedRules = this.ruleEngine.evaluate(event, this.cachedRules);
|
|
21351
21466
|
for (const rule of matchedRules) {
|