@blueharford/scrypted-spatial-awareness 0.4.5 → 0.4.7

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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueharford/scrypted-spatial-awareness",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Cross-camera object tracking for Scrypted NVR with spatial awareness",
5
5
  "author": "Joshua Seidel <blueharford>",
6
6
  "license": "Apache-2.0",
@@ -159,25 +159,32 @@ export class AlertManager {
159
159
  const prefix = alert.severity === 'critical' ? '🚨 ' :
160
160
  alert.severity === 'warning' ? '⚠️ ' : '';
161
161
 
162
+ // Use object class in title
163
+ const objectType = alert.details.objectClass
164
+ ? alert.details.objectClass.charAt(0).toUpperCase() + alert.details.objectClass.slice(1)
165
+ : 'Object';
166
+
162
167
  switch (alert.type) {
163
168
  case 'property_entry':
164
- return `${prefix}🚶 Entry Detected`;
169
+ return `${prefix}${objectType} Arrived`;
165
170
  case 'property_exit':
166
- return `${prefix}🚶 Exit Detected`;
171
+ return `${prefix}${objectType} Left`;
167
172
  case 'movement':
168
- return `${prefix}🚶 Movement Detected`;
173
+ // Include destination in title
174
+ const dest = alert.details.toCameraName || 'area';
175
+ return `${prefix}${objectType} → ${dest}`;
169
176
  case 'unusual_path':
170
- return `${prefix}Unusual Path`;
177
+ return `${prefix}Unusual Route`;
171
178
  case 'dwell_time':
172
- return `${prefix}⏱️ Extended Presence`;
179
+ return `${prefix}${objectType} Lingering`;
173
180
  case 'restricted_zone':
174
- return `${prefix}Restricted Zone Alert`;
181
+ return `${prefix}Restricted Zone!`;
175
182
  case 'lost_tracking':
176
- return `${prefix}Lost Tracking`;
183
+ return `${prefix}${objectType} Lost`;
177
184
  case 'reappearance':
178
- return `${prefix}Object Reappeared`;
185
+ return `${prefix}${objectType} Reappeared`;
179
186
  default:
180
- return `${prefix}Spatial Awareness Alert`;
187
+ return `${prefix}Spatial Alert`;
181
188
  }
182
189
  }
183
190
 
@@ -125,6 +125,9 @@ export class ObjectCorrelator {
125
125
  return 1.0;
126
126
  }
127
127
 
128
+ // Calculate transit time first
129
+ const transitTime = sighting.timestamp - lastSighting.timestamp;
130
+
128
131
  // Find connection between cameras
129
132
  const connection = findConnection(
130
133
  this.topology,
@@ -133,12 +136,17 @@ export class ObjectCorrelator {
133
136
  );
134
137
 
135
138
  if (!connection) {
136
- // No defined connection - low score but not zero
137
- // (allows for uncharted paths)
139
+ // No defined connection - still allow correlation based on reasonable timing
140
+ // Allow up to 2 minutes transit between any cameras
141
+ const MAX_UNCHARTED_TRANSIT = 120000; // 2 minutes
142
+ if (transitTime > 0 && transitTime < MAX_UNCHARTED_TRANSIT) {
143
+ // Score based on how reasonable the timing is
144
+ // Shorter transits are more likely to be the same object
145
+ const timingScore = Math.max(0.3, 1 - (transitTime / MAX_UNCHARTED_TRANSIT));
146
+ return timingScore;
147
+ }
138
148
  return 0.2;
139
149
  }
140
-
141
- const transitTime = sighting.timestamp - lastSighting.timestamp;
142
150
  const { min, typical, max } = connection.transitTime;
143
151
 
144
152
  // Way outside range
@@ -217,7 +225,8 @@ export class ObjectCorrelator {
217
225
  );
218
226
 
219
227
  if (!connection) {
220
- return 0.3; // No connection defined
228
+ // No connection defined - give neutral score (don't penalize)
229
+ return 0.5;
221
230
  }
222
231
 
223
232
  let score = 0;
package/src/main.ts CHANGED
@@ -78,8 +78,8 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
78
78
  correlationThreshold: {
79
79
  title: 'Correlation Confidence Threshold',
80
80
  type: 'number',
81
- defaultValue: 0.6,
82
- description: 'Minimum confidence (0-1) for automatic object correlation',
81
+ defaultValue: 0.35,
82
+ description: 'Minimum confidence (0-1) for automatic object correlation. Lower values allow more matches.',
83
83
  group: 'Tracking',
84
84
  },
85
85
  lostTimeout: {
@@ -324,7 +324,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
324
324
 
325
325
  const config: TrackingEngineConfig = {
326
326
  correlationWindow: (this.storageSettings.values.correlationWindow as number || 30) * 1000,
327
- correlationThreshold: this.storageSettings.values.correlationThreshold as number || 0.6,
327
+ correlationThreshold: this.storageSettings.values.correlationThreshold as number || 0.35,
328
328
  lostTimeout: (this.storageSettings.values.lostTimeout as number || 300) * 1000,
329
329
  useVisualMatching: this.storageSettings.values.useVisualMatching as boolean ?? true,
330
330
  loiteringThreshold: (this.storageSettings.values.loiteringThreshold as number || 3) * 1000,
@@ -214,9 +214,9 @@ export function generateAlertMessage(
214
214
 
215
215
  switch (type) {
216
216
  case 'property_entry':
217
- return `${objectDesc} entered property via ${details.cameraName || 'unknown camera'}`;
217
+ return `${objectDesc} arrived at ${details.cameraName || 'property'}`;
218
218
  case 'property_exit':
219
- return `${objectDesc} exited property via ${details.cameraName || 'unknown camera'}`;
219
+ return `${objectDesc} left from ${details.cameraName || 'property'}`;
220
220
  case 'movement':
221
221
  // If we have a rich description from LLM/RAG, use it
222
222
  if (details.objectLabel && details.usedLlm) {
@@ -986,14 +986,14 @@ export const EDITOR_HTML = `<!DOCTYPE html>
986
986
 
987
987
  function uploadFloorPlan() { document.getElementById('upload-modal').classList.add('active'); }
988
988
 
989
- // Compress and resize image to avoid 413 errors
990
- function compressImage(img, maxSize = 1600, quality = 0.8) {
989
+ // Compress and resize image to avoid 413 errors (Scrypted has ~50KB limit)
990
+ function compressImage(img, maxSize = 800, quality = 0.5) {
991
991
  return new Promise((resolve) => {
992
992
  const canvas = document.createElement('canvas');
993
993
  let width = img.width;
994
994
  let height = img.height;
995
995
 
996
- // Resize if larger than maxSize
996
+ // Always resize to fit within maxSize
997
997
  if (width > maxSize || height > maxSize) {
998
998
  if (width > height) {
999
999
  height = Math.round(height * maxSize / width);
@@ -1009,9 +1009,18 @@ export const EDITOR_HTML = `<!DOCTYPE html>
1009
1009
  const ctx = canvas.getContext('2d');
1010
1010
  ctx.drawImage(img, 0, 0, width, height);
1011
1011
 
1012
- // Convert to JPEG for better compression
1013
- const compressed = canvas.toDataURL('image/jpeg', quality);
1012
+ // Convert to JPEG with aggressive compression
1013
+ let compressed = canvas.toDataURL('image/jpeg', quality);
1014
1014
  console.log('Compressed image from', img.width, 'x', img.height, 'to', width, 'x', height, 'size:', Math.round(compressed.length / 1024), 'KB');
1015
+
1016
+ // If still too large, compress more
1017
+ let q = quality;
1018
+ while (compressed.length > 50000 && q > 0.1) {
1019
+ q -= 0.1;
1020
+ compressed = canvas.toDataURL('image/jpeg', q);
1021
+ console.log('Re-compressed at quality', q.toFixed(1), 'size:', Math.round(compressed.length / 1024), 'KB');
1022
+ }
1023
+
1015
1024
  resolve(compressed);
1016
1025
  });
1017
1026
  }