@blueharford/scrypted-spatial-awareness 0.6.12 → 0.6.14

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/plugin.zip CHANGED
Binary file
@@ -36943,6 +36943,11 @@ class TrackingEngine {
36943
36943
  trainingConfig = training_1.DEFAULT_TRAINING_CONFIG;
36944
36944
  /** Callback for training status updates */
36945
36945
  onTrainingStatusUpdate;
36946
+ // ==================== Snapshot Cache ====================
36947
+ /** Cached snapshots for tracked objects (for faster notifications) */
36948
+ snapshotCache = new Map();
36949
+ /** Pending LLM description promises (started when snapshot is captured) */
36950
+ pendingDescriptions = new Map();
36946
36951
  constructor(topology, state, alertManager, config, console) {
36947
36952
  this.topology = topology;
36948
36953
  this.state = state;
@@ -37166,6 +37171,12 @@ class TrackingEngine {
37166
37171
  const lastSighting = (0, tracked_object_1.getLastSighting)(tracked);
37167
37172
  if (lastSighting && lastSighting.cameraId !== sighting.cameraId) {
37168
37173
  const transitDuration = sighting.timestamp - lastSighting.timestamp;
37174
+ // Update cached snapshot from new camera (object is now visible here)
37175
+ if (this.config.useLlmDescriptions) {
37176
+ this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId).catch(e => {
37177
+ this.console.warn(`[Transition Snapshot] Failed to update snapshot: ${e}`);
37178
+ });
37179
+ }
37169
37180
  // Add journey segment
37170
37181
  this.state.addJourney(tracked.globalId, {
37171
37182
  fromCameraId: lastSighting.cameraId,
@@ -37232,6 +37243,13 @@ class TrackingEngine {
37232
37243
  }
37233
37244
  /** Schedule an alert after loitering threshold passes */
37234
37245
  scheduleLoiteringAlert(globalId, sighting, isEntryPoint) {
37246
+ // Capture snapshot IMMEDIATELY when object is first detected (don't wait for loitering threshold)
37247
+ // This ensures we have a good image while the person/object is still in frame
37248
+ if (this.config.useLlmDescriptions) {
37249
+ this.captureAndCacheSnapshot(globalId, sighting.cameraId).catch(e => {
37250
+ this.console.warn(`[Snapshot] Failed to cache initial snapshot: ${e}`);
37251
+ });
37252
+ }
37235
37253
  // Check after loitering threshold if object is still being tracked
37236
37254
  setTimeout(async () => {
37237
37255
  const tracked = this.state.getObject(globalId);
@@ -37240,26 +37258,28 @@ class TrackingEngine {
37240
37258
  // Check if we've already alerted for this object
37241
37259
  if (this.isInAlertCooldown(globalId))
37242
37260
  return;
37243
- // Get snapshot for LLM description (if LLM is enabled)
37244
- let mediaObject;
37245
- this.console.log(`[Entry Alert] useLlmDescriptions=${this.config.useLlmDescriptions}`);
37246
- if (this.config.useLlmDescriptions) {
37261
+ // Use prefetched LLM result if available (started when snapshot was captured)
37262
+ let spatialResult;
37263
+ const pendingDescription = this.pendingDescriptions.get(globalId);
37264
+ if (pendingDescription) {
37265
+ this.console.log(`[Entry Alert] Using prefetched LLM result for ${globalId.slice(0, 8)}`);
37247
37266
  try {
37248
- const camera = systemManager.getDeviceById(sighting.cameraId);
37249
- this.console.log(`[Entry Alert] Camera ${sighting.cameraId} has Camera interface: ${camera?.interfaces?.includes(sdk_1.ScryptedInterface.Camera)}`);
37250
- if (camera?.interfaces?.includes(sdk_1.ScryptedInterface.Camera)) {
37251
- mediaObject = await camera.takePicture();
37252
- this.console.log(`[Entry Alert] Got snapshot: ${!!mediaObject}`);
37253
- }
37267
+ spatialResult = await pendingDescription;
37268
+ this.console.log(`[Entry Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
37254
37269
  }
37255
37270
  catch (e) {
37256
- this.console.warn('[Entry Alert] Failed to get snapshot:', e);
37271
+ this.console.warn(`[Entry Alert] Prefetch failed, generating fallback: ${e}`);
37272
+ spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId);
37257
37273
  }
37274
+ this.pendingDescriptions.delete(globalId);
37275
+ }
37276
+ else {
37277
+ // Fallback: generate description now (slower path)
37278
+ this.console.log(`[Entry Alert] No prefetch available, generating now`);
37279
+ const mediaObject = this.snapshotCache.get(globalId);
37280
+ spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId, mediaObject);
37281
+ this.console.log(`[Entry Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
37258
37282
  }
37259
- // Generate spatial description (now async with LLM support)
37260
- this.console.log(`[Entry Alert] Calling generateEntryDescription with mediaObject=${!!mediaObject}`);
37261
- const spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId, mediaObject);
37262
- this.console.log(`[Entry Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
37263
37283
  if (isEntryPoint) {
37264
37284
  // Entry point - generate property entry alert
37265
37285
  await this.alertManager.checkAndAlert('property_entry', tracked, {
@@ -37290,6 +37310,37 @@ class TrackingEngine {
37290
37310
  this.recordAlertTime(globalId);
37291
37311
  }, this.config.loiteringThreshold);
37292
37312
  }
37313
+ /** Capture and cache a snapshot for a tracked object, and start LLM analysis immediately */
37314
+ async captureAndCacheSnapshot(globalId, cameraId, eventType = 'entry') {
37315
+ try {
37316
+ const camera = systemManager.getDeviceById(cameraId);
37317
+ if (camera?.interfaces?.includes(sdk_1.ScryptedInterface.Camera)) {
37318
+ const mediaObject = await camera.takePicture();
37319
+ if (mediaObject) {
37320
+ this.snapshotCache.set(globalId, mediaObject);
37321
+ this.console.log(`[Snapshot] Cached snapshot for ${globalId.slice(0, 8)} from ${cameraId}`);
37322
+ // Start LLM analysis immediately in parallel (don't await)
37323
+ const tracked = this.state.getObject(globalId);
37324
+ if (tracked && this.config.useLlmDescriptions) {
37325
+ this.console.log(`[LLM Prefetch] Starting ${eventType} analysis for ${globalId.slice(0, 8)}`);
37326
+ const descriptionPromise = eventType === 'exit'
37327
+ ? this.spatialReasoning.generateExitDescription(tracked, cameraId, mediaObject)
37328
+ : this.spatialReasoning.generateEntryDescription(tracked, cameraId, mediaObject);
37329
+ this.pendingDescriptions.set(globalId, descriptionPromise);
37330
+ // Log when complete (for debugging)
37331
+ descriptionPromise.then(result => {
37332
+ this.console.log(`[LLM Prefetch] ${eventType} analysis ready for ${globalId.slice(0, 8)}: "${result.description.substring(0, 40)}..."`);
37333
+ }).catch(e => {
37334
+ this.console.warn(`[LLM Prefetch] Failed for ${globalId.slice(0, 8)}: ${e}`);
37335
+ });
37336
+ }
37337
+ }
37338
+ }
37339
+ }
37340
+ catch (e) {
37341
+ this.console.warn(`[Snapshot] Failed to capture snapshot: ${e}`);
37342
+ }
37343
+ }
37293
37344
  /** Attempt to correlate a sighting with existing tracked objects */
37294
37345
  async correlateDetection(sighting) {
37295
37346
  const activeObjects = this.state.getActiveObjects();
@@ -37329,31 +37380,40 @@ class TrackingEngine {
37329
37380
  handlePotentialExit(tracked, sighting) {
37330
37381
  // Mark as pending and set timer
37331
37382
  this.state.markPending(tracked.globalId);
37383
+ // Capture a fresh snapshot now while object is still visible (before they leave)
37384
+ // Also starts LLM analysis immediately in parallel
37385
+ if (this.config.useLlmDescriptions) {
37386
+ this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId, 'exit').catch(e => {
37387
+ this.console.warn(`[Exit Snapshot] Failed to update snapshot: ${e}`);
37388
+ });
37389
+ }
37332
37390
  // Wait for correlation window before marking as exited
37333
37391
  const timer = setTimeout(async () => {
37334
37392
  const current = this.state.getObject(tracked.globalId);
37335
37393
  if (current && current.state === 'pending') {
37336
37394
  this.state.markExited(tracked.globalId, sighting.cameraId, sighting.cameraName);
37337
- // Get snapshot for LLM description (if LLM is enabled)
37338
- let mediaObject;
37339
- this.console.log(`[Exit Alert] useLlmDescriptions=${this.config.useLlmDescriptions}`);
37340
- if (this.config.useLlmDescriptions) {
37395
+ // Use prefetched LLM result if available (started when exit was first detected)
37396
+ let spatialResult;
37397
+ const pendingDescription = this.pendingDescriptions.get(tracked.globalId);
37398
+ if (pendingDescription) {
37399
+ this.console.log(`[Exit Alert] Using prefetched LLM result for ${tracked.globalId.slice(0, 8)}`);
37341
37400
  try {
37342
- const camera = systemManager.getDeviceById(sighting.cameraId);
37343
- this.console.log(`[Exit Alert] Camera ${sighting.cameraId} has Camera interface: ${camera?.interfaces?.includes(sdk_1.ScryptedInterface.Camera)}`);
37344
- if (camera?.interfaces?.includes(sdk_1.ScryptedInterface.Camera)) {
37345
- mediaObject = await camera.takePicture();
37346
- this.console.log(`[Exit Alert] Got snapshot: ${!!mediaObject}`);
37347
- }
37401
+ spatialResult = await pendingDescription;
37402
+ this.console.log(`[Exit Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
37348
37403
  }
37349
37404
  catch (e) {
37350
- this.console.warn('[Exit Alert] Failed to get snapshot:', e);
37405
+ this.console.warn(`[Exit Alert] Prefetch failed, generating fallback: ${e}`);
37406
+ spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId);
37351
37407
  }
37408
+ this.pendingDescriptions.delete(tracked.globalId);
37409
+ }
37410
+ else {
37411
+ // Fallback: generate description now (slower path)
37412
+ this.console.log(`[Exit Alert] No prefetch available, generating now`);
37413
+ const mediaObject = this.snapshotCache.get(tracked.globalId);
37414
+ spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId, mediaObject);
37415
+ this.console.log(`[Exit Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
37352
37416
  }
37353
- // Generate rich exit description using topology context (now async with LLM support)
37354
- this.console.log(`[Exit Alert] Calling generateExitDescription with mediaObject=${!!mediaObject}`);
37355
- const spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId, mediaObject);
37356
- this.console.log(`[Exit Alert] Object ${tracked.globalId.slice(0, 8)} exited: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
37357
37417
  await this.alertManager.checkAndAlert('property_exit', current, {
37358
37418
  cameraId: sighting.cameraId,
37359
37419
  cameraName: sighting.cameraName,
@@ -37362,6 +37422,9 @@ class TrackingEngine {
37362
37422
  involvedLandmarks: spatialResult.involvedLandmarks?.map(l => l.name),
37363
37423
  usedLlm: spatialResult.usedLlm,
37364
37424
  });
37425
+ // Clean up cached snapshot and pending descriptions after exit alert
37426
+ this.snapshotCache.delete(tracked.globalId);
37427
+ this.pendingDescriptions.delete(tracked.globalId);
37365
37428
  }
37366
37429
  this.pendingTimers.delete(tracked.globalId);
37367
37430
  }, this.config.correlationWindow);
@@ -37377,6 +37440,9 @@ class TrackingEngine {
37377
37440
  this.state.markLost(tracked.globalId);
37378
37441
  this.console.log(`Object ${tracked.globalId.slice(0, 8)} marked as lost ` +
37379
37442
  `(not seen for ${Math.round(timeSinceSeen / 1000)}s)`);
37443
+ // Clean up cached snapshot and pending descriptions
37444
+ this.snapshotCache.delete(tracked.globalId);
37445
+ this.pendingDescriptions.delete(tracked.globalId);
37380
37446
  this.alertManager.checkAndAlert('lost_tracking', tracked, {
37381
37447
  objectClass: tracked.className,
37382
37448
  objectLabel: tracked.label,