@blueharford/scrypted-spatial-awareness 0.6.14 → 0.6.16

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
@@ -36173,59 +36173,140 @@ const topology_1 = __webpack_require__(/*! ../models/topology */ "./src/models/t
36173
36173
  const spatial_reasoning_1 = __webpack_require__(/*! ./spatial-reasoning */ "./src/core/spatial-reasoning.ts");
36174
36174
  const { systemManager } = sdk_1.default;
36175
36175
  /** Scene analysis prompt for single camera */
36176
- const SCENE_ANALYSIS_PROMPT = `Analyze this security camera image and identify what you see.
36176
+ const SCENE_ANALYSIS_PROMPT = `You are analyzing a security camera image. Describe EVERYTHING you can see in detail.
36177
+
36178
+ ## INSTRUCTIONS
36179
+ Look at this image carefully and identify ALL visible objects, structures, and areas. Be thorough - even small or partially visible items are important for security awareness.
36180
+
36181
+ ## 1. LANDMARKS - List EVERY distinct object or feature you can see:
36182
+
36183
+ **Structures** (buildings, parts of buildings):
36184
+ - Houses, garages, sheds, porches, decks, patios, carports, gazebos
36185
+ - Walls, pillars, columns, railings, stairs, steps
36186
+
36187
+ **Vegetation** (plants, trees, landscaping):
36188
+ - Trees (describe type if identifiable: oak, palm, pine, etc.)
36189
+ - Bushes, shrubs, hedges
36190
+ - Flower beds, gardens, planters, potted plants
36191
+ - Grass/lawn areas, mulch beds
36192
+
36193
+ **Boundaries & Barriers**:
36194
+ - Fences (wood, chain-link, aluminum, vinyl, iron, privacy)
36195
+ - Walls (brick, stone, concrete, retaining)
36196
+ - Gates, gate posts
36197
+ - Hedges used as boundaries
36198
+
36199
+ **Access Points & Pathways**:
36200
+ - Doors (front, side, garage, screen)
36201
+ - Driveways (concrete, asphalt, gravel, pavers)
36202
+ - Walkways, sidewalks, paths, stepping stones
36203
+ - Stairs, ramps, porches
36204
+
36205
+ **Utility & Fixtures**:
36206
+ - Mailboxes, package boxes
36207
+ - Light fixtures, lamp posts, solar lights
36208
+ - A/C units, utility boxes, meters
36209
+ - Trash cans, recycling bins
36210
+ - Hoses, spigots, sprinklers
36211
+
36212
+ **Outdoor Items**:
36213
+ - Vehicles (cars, trucks, motorcycles, boats, trailers)
36214
+ - Furniture (chairs, tables, benches, swings)
36215
+ - Grills, fire pits, outdoor kitchens
36216
+ - Play equipment, trampolines, pools
36217
+ - Decorations, flags, signs
36218
+
36219
+ **Off-Property Elements** (important for security context):
36220
+ - Street, road, sidewalk
36221
+ - Neighbor's property/fence/house
36222
+ - Public areas visible
36223
+
36224
+ For EACH landmark, estimate its DISTANCE from the camera:
36225
+ - "close" = 0-10 feet (within arm's reach of camera)
36226
+ - "near" = 10-30 feet
36227
+ - "medium" = 30-60 feet
36228
+ - "far" = 60-100 feet
36229
+ - "distant" = 100+ feet (edge of property or beyond)
36230
+
36231
+ ## 2. ZONES - Identify distinct AREAS visible:
36232
+ - Front yard, backyard, side yard
36233
+ - Driveway, parking area
36234
+ - Patio, deck, porch
36235
+ - Garden area, lawn
36236
+ - Street/road
36237
+ - Neighbor's yard
36238
+
36239
+ For each zone, estimate what percentage of the image it covers (0.0 to 1.0).
36240
+
36241
+ ## 3. EDGES - What's at each edge of the frame:
36242
+ This helps understand what's just out of view.
36243
+
36244
+ ## 4. CAMERA CONTEXT:
36245
+ - Estimated mounting height (ground level, 8ft, 12ft, roofline, etc.)
36246
+ - Approximate field of view (narrow, medium, wide)
36247
+ - Facing direction if determinable (north, south, street-facing, etc.)
36177
36248
 
36178
- 1. LANDMARKS - Identify fixed features visible:
36179
- - Structures (house, garage, shed, porch, deck)
36180
- - Features (mailbox, tree, pool, garden, fountain)
36181
- - Access points (door, gate, driveway entrance, walkway)
36182
- - Boundaries (fence, wall, hedge)
36183
-
36184
- 2. ZONES - Identify area types visible:
36185
- - What type of area is this? (front yard, backyard, driveway, street, patio, walkway)
36186
- - Estimate what percentage of the frame each zone covers (0.0 to 1.0)
36187
-
36188
- 3. EDGES - What's visible at the frame edges:
36189
- - Top edge: (sky, roof, trees, etc.)
36190
- - Left edge: (fence, neighbor, street, etc.)
36191
- - Right edge: (fence, garage, etc.)
36192
- - Bottom edge: (ground, driveway, grass, etc.)
36193
-
36194
- 4. ORIENTATION - Estimate camera facing direction based on shadows, sun position, or landmarks
36195
-
36196
- Respond with ONLY valid JSON in this exact format:
36249
+ Respond with ONLY valid JSON:
36197
36250
  {
36198
36251
  "landmarks": [
36199
- {"name": "Front Door", "type": "access", "confidence": 0.9, "description": "White front door with black frame"}
36252
+ {"name": "Mailbox", "type": "feature", "distance": "medium", "confidence": 0.95, "description": "Black metal mailbox on wooden post, approximately 40 feet from camera"},
36253
+ {"name": "Aluminum Fence", "type": "boundary", "distance": "near", "confidence": 0.9, "description": "Silver aluminum fence running along left side of property, about 15-20 feet away"},
36254
+ {"name": "Large Oak Tree", "type": "feature", "distance": "far", "confidence": 0.85, "description": "Mature oak tree near property line, roughly 80 feet from camera"}
36200
36255
  ],
36201
36256
  "zones": [
36202
- {"name": "Front Yard", "type": "yard", "coverage": 0.4, "description": "Grass lawn area"}
36257
+ {"name": "Front Yard", "type": "yard", "coverage": 0.5, "description": "Grass lawn with some bare patches"},
36258
+ {"name": "Driveway", "type": "driveway", "coverage": 0.25, "description": "Concrete driveway leading to garage"}
36203
36259
  ],
36204
- "edges": {"top": "sky with clouds", "left": "fence and trees", "right": "garage wall", "bottom": "concrete walkway"},
36205
- "orientation": "north"
36206
- }`;
36260
+ "edges": {
36261
+ "top": "sky, tree canopy",
36262
+ "left": "aluminum fence, neighbor's yard beyond",
36263
+ "right": "side of house, garage door",
36264
+ "bottom": "concrete walkway, grass edge"
36265
+ },
36266
+ "cameraContext": {
36267
+ "mountHeight": "8 feet",
36268
+ "fieldOfView": "wide",
36269
+ "facingDirection": "street-facing"
36270
+ }
36271
+ }
36272
+
36273
+ BE THOROUGH. List every distinct item you can identify. A typical outdoor scene should have 5-15+ landmarks.`;
36207
36274
  /** Multi-camera correlation prompt */
36208
- const CORRELATION_PROMPT = `I have scene analyses from multiple security cameras at the same property. Help me correlate them to understand the property layout.
36275
+ const CORRELATION_PROMPT = `I have detailed scene analyses from multiple security cameras at the same property. Help me understand which landmarks appear in multiple camera views.
36209
36276
 
36210
36277
  CAMERA SCENES:
36211
36278
  {scenes}
36212
36279
 
36213
- Identify:
36214
- 1. Shared landmarks - Features that appear in multiple camera views
36215
- 2. Camera connections - How someone could move between camera views and estimated walking time
36216
- 3. Overall layout - Describe the property layout based on what you see
36280
+ ## PRIORITY ORDER (most important first):
36281
+
36282
+ ### 1. SHARED LANDMARKS (HIGHEST PRIORITY)
36283
+ Identify features that are visible from MULTIPLE cameras. This is crucial for understanding the property layout.
36284
+ - Look for the SAME fence, tree, mailbox, driveway, structure, etc. appearing in different camera views
36285
+ - Even partial visibility counts (e.g., a tree visible in full from one camera and just the edge from another)
36286
+ - Include landmarks that are at the boundary between camera views
36287
+
36288
+ ### 2. PROPERTY LAYOUT
36289
+ Based on what each camera sees and their overlapping features, describe:
36290
+ - Which areas each camera covers
36291
+ - How the cameras relate spatially (e.g., "Camera A looks toward Camera B's direction")
36292
+ - Overall property shape and features
36293
+
36294
+ ### 3. CONNECTIONS (Lower Priority)
36295
+ Only if clearly determinable, suggest walking paths between camera views.
36217
36296
 
36218
36297
  IMPORTANT: For camera references, use the EXACT device ID shown in parentheses (e.g., "device_123"), NOT the camera name.
36219
36298
 
36220
36299
  Respond with ONLY valid JSON:
36221
36300
  {
36222
36301
  "sharedLandmarks": [
36223
- {"name": "Driveway", "type": "access", "seenByCameras": ["device_123", "device_456"], "confidence": 0.8, "description": "Concrete driveway"}
36302
+ {"name": "Aluminum Fence", "type": "boundary", "seenByCameras": ["device_123", "device_456"], "confidence": 0.85, "description": "Silver aluminum fence visible on right edge of Camera A and left edge of Camera B"},
36303
+ {"name": "Large Oak Tree", "type": "feature", "seenByCameras": ["device_123", "device_789"], "confidence": 0.9, "description": "Mature oak tree in front yard, visible from both front and side cameras"},
36304
+ {"name": "Concrete Driveway", "type": "access", "seenByCameras": ["device_123", "device_456", "device_789"], "confidence": 0.95, "description": "Driveway visible from multiple angles"}
36224
36305
  ],
36225
36306
  "connections": [
36226
- {"from": "device_123", "to": "device_456", "transitSeconds": 10, "via": "driveway", "confidence": 0.7, "bidirectional": true}
36307
+ {"from": "device_123", "to": "device_456", "transitSeconds": 8, "via": "along driveway", "confidence": 0.6, "bidirectional": true}
36227
36308
  ],
36228
- "layoutDescription": "Single-story house with front yard facing street, driveway on the left side, backyard accessible through side gate"
36309
+ "layoutDescription": "Ranch-style house. Front camera covers front yard and street. Garage camera covers driveway entrance. Side camera covers side yard with aluminum fence separating from neighbor. Backyard camera shows deck and pool area."
36229
36310
  }`;
36230
36311
  class TopologyDiscoveryEngine {
36231
36312
  config;
@@ -36411,7 +36492,7 @@ class TopologyDiscoveryEngine {
36411
36492
  ],
36412
36493
  },
36413
36494
  ],
36414
- max_tokens: 1500,
36495
+ max_tokens: 4000, // Increased for detailed scene analysis
36415
36496
  temperature: 0.3,
36416
36497
  });
36417
36498
  const content = result?.choices?.[0]?.message?.content;
@@ -36422,13 +36503,15 @@ class TopologyDiscoveryEngine {
36422
36503
  if (jsonStr.startsWith('```')) {
36423
36504
  jsonStr = jsonStr.replace(/```json?\n?/g, '').replace(/```$/g, '').trim();
36424
36505
  }
36425
- const parsed = JSON.parse(jsonStr);
36506
+ // Try to recover truncated JSON
36507
+ const parsed = this.parseJsonWithRecovery(jsonStr, cameraName);
36426
36508
  // Map parsed data to our types
36427
36509
  if (Array.isArray(parsed.landmarks)) {
36428
36510
  analysis.landmarks = parsed.landmarks.map((l) => ({
36429
36511
  name: l.name || 'Unknown',
36430
36512
  type: this.mapLandmarkType(l.type),
36431
36513
  confidence: typeof l.confidence === 'number' ? l.confidence : 0.7,
36514
+ distance: this.mapDistance(l.distance),
36432
36515
  description: l.description || '',
36433
36516
  boundingBox: l.boundingBox,
36434
36517
  }));
@@ -36546,6 +36629,108 @@ class TopologyDiscoveryEngine {
36546
36629
  return 'west';
36547
36630
  return 'unknown';
36548
36631
  }
36632
+ /** Map LLM distance to our type */
36633
+ mapDistance(distance) {
36634
+ const dist = distance?.toLowerCase();
36635
+ if (dist?.includes('close'))
36636
+ return 'close';
36637
+ if (dist?.includes('near'))
36638
+ return 'near';
36639
+ if (dist?.includes('medium'))
36640
+ return 'medium';
36641
+ if (dist?.includes('far') && !dist?.includes('distant'))
36642
+ return 'far';
36643
+ if (dist?.includes('distant'))
36644
+ return 'distant';
36645
+ return 'medium'; // Default to medium if not specified
36646
+ }
36647
+ /** Try to parse JSON with recovery for truncated responses */
36648
+ parseJsonWithRecovery(jsonStr, context) {
36649
+ // First, try direct parse
36650
+ try {
36651
+ return JSON.parse(jsonStr);
36652
+ }
36653
+ catch (e) {
36654
+ // Log the raw response for debugging (first 500 chars)
36655
+ this.console.log(`[Discovery] Raw LLM response for ${context} (first 500 chars): ${jsonStr.substring(0, 500)}...`);
36656
+ }
36657
+ // Try to recover truncated JSON by finding complete sections
36658
+ try {
36659
+ // Find where valid JSON might end (look for last complete object/array)
36660
+ let recoveredJson = jsonStr;
36661
+ // Try to close unclosed strings
36662
+ const lastQuote = recoveredJson.lastIndexOf('"');
36663
+ const lastColon = recoveredJson.lastIndexOf(':');
36664
+ if (lastQuote > lastColon) {
36665
+ // We might be in the middle of a string value
36666
+ const beforeQuote = recoveredJson.substring(0, lastQuote);
36667
+ const afterLastCompleteEntry = beforeQuote.lastIndexOf('},');
36668
+ if (afterLastCompleteEntry > 0) {
36669
+ recoveredJson = beforeQuote.substring(0, afterLastCompleteEntry + 1);
36670
+ }
36671
+ }
36672
+ // Close any unclosed arrays/objects
36673
+ let openBraces = (recoveredJson.match(/{/g) || []).length;
36674
+ let closeBraces = (recoveredJson.match(/}/g) || []).length;
36675
+ let openBrackets = (recoveredJson.match(/\[/g) || []).length;
36676
+ let closeBrackets = (recoveredJson.match(/\]/g) || []).length;
36677
+ // Add missing closing brackets/braces
36678
+ while (closeBrackets < openBrackets) {
36679
+ recoveredJson += ']';
36680
+ closeBrackets++;
36681
+ }
36682
+ while (closeBraces < openBraces) {
36683
+ recoveredJson += '}';
36684
+ closeBraces++;
36685
+ }
36686
+ const recovered = JSON.parse(recoveredJson);
36687
+ this.console.log(`[Discovery] Recovered truncated JSON for ${context}`);
36688
+ return recovered;
36689
+ }
36690
+ catch (recoveryError) {
36691
+ // Last resort: try to extract just landmarks array
36692
+ try {
36693
+ const landmarksMatch = jsonStr.match(/"landmarks"\s*:\s*\[([\s\S]*?)(?:\]|$)/);
36694
+ const zonesMatch = jsonStr.match(/"zones"\s*:\s*\[([\s\S]*?)(?:\]|$)/);
36695
+ const result = { landmarks: [], zones: [], edges: {}, orientation: 'unknown' };
36696
+ if (landmarksMatch) {
36697
+ // Try to parse individual landmark objects
36698
+ const landmarksStr = landmarksMatch[1];
36699
+ const landmarkObjects = landmarksStr.match(/\{[^{}]*\}/g) || [];
36700
+ result.landmarks = landmarkObjects.map((obj) => {
36701
+ try {
36702
+ return JSON.parse(obj);
36703
+ }
36704
+ catch {
36705
+ return null;
36706
+ }
36707
+ }).filter(Boolean);
36708
+ this.console.log(`[Discovery] Extracted ${result.landmarks.length} landmarks from partial response for ${context}`);
36709
+ }
36710
+ if (zonesMatch) {
36711
+ const zonesStr = zonesMatch[1];
36712
+ const zoneObjects = zonesStr.match(/\{[^{}]*\}/g) || [];
36713
+ result.zones = zoneObjects.map((obj) => {
36714
+ try {
36715
+ return JSON.parse(obj);
36716
+ }
36717
+ catch {
36718
+ return null;
36719
+ }
36720
+ }).filter(Boolean);
36721
+ this.console.log(`[Discovery] Extracted ${result.zones.length} zones from partial response for ${context}`);
36722
+ }
36723
+ if (result.landmarks.length > 0 || result.zones.length > 0) {
36724
+ return result;
36725
+ }
36726
+ }
36727
+ catch (extractError) {
36728
+ // Give up
36729
+ }
36730
+ this.console.warn(`[Discovery] Could not recover JSON for ${context}`);
36731
+ throw new Error(`Failed to parse LLM response: truncated or malformed JSON`);
36732
+ }
36733
+ }
36549
36734
  /** Resolve a camera reference (name or deviceId) to its deviceId */
36550
36735
  resolveCameraRef(ref) {
36551
36736
  if (!this.topology?.cameras || !ref)
@@ -36721,9 +36906,12 @@ class TopologyDiscoveryEngine {
36721
36906
  generateSuggestionsFromAnalysis(analysis) {
36722
36907
  if (!analysis.isValid)
36723
36908
  return;
36909
+ this.console.log(`[Discovery] Generating suggestions from ${analysis.landmarks.length} landmarks, ${analysis.zones.length} zones`);
36724
36910
  // Generate landmark suggestions
36725
36911
  for (const landmark of analysis.landmarks) {
36726
36912
  if (landmark.confidence >= this.config.minLandmarkConfidence) {
36913
+ // Calculate distance in feet from distance estimate
36914
+ const distanceFeet = landmark.distance ? (0, discovery_1.distanceToFeet)(landmark.distance) : 50;
36727
36915
  const suggestion = {
36728
36916
  id: `landmark_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
36729
36917
  type: 'landmark',
@@ -36736,26 +36924,30 @@ class TopologyDiscoveryEngine {
36736
36924
  type: landmark.type,
36737
36925
  description: landmark.description,
36738
36926
  visibleFromCameras: [analysis.cameraId],
36739
- // Include bounding box for positioning (will be used by applyDiscoverySuggestion)
36927
+ // Include extra metadata for positioning
36740
36928
  boundingBox: landmark.boundingBox,
36741
- }, // boundingBox is extra metadata not in Landmark interface
36929
+ distance: landmark.distance,
36930
+ distanceFeet: distanceFeet,
36931
+ }, // Extra metadata not in base Landmark interface
36742
36932
  };
36743
36933
  this.suggestions.set(suggestion.id, suggestion);
36934
+ this.console.log(`[Discovery] Landmark suggestion: ${landmark.name} (${landmark.type}, ${landmark.distance || 'medium'}, ~${distanceFeet}ft)`);
36744
36935
  }
36745
36936
  }
36746
- // Generate zone suggestions
36937
+ // Generate zone suggestions (even for smaller coverage - 10% is enough)
36747
36938
  for (const zone of analysis.zones) {
36748
- if (zone.coverage >= 0.2) {
36939
+ if (zone.coverage >= 0.1) {
36749
36940
  const suggestion = {
36750
36941
  id: `zone_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
36751
36942
  type: 'zone',
36752
36943
  timestamp: Date.now(),
36753
36944
  sourceCameras: [analysis.cameraId],
36754
- confidence: 0.7,
36945
+ confidence: Math.min(0.9, 0.5 + zone.coverage), // Higher coverage = higher confidence
36755
36946
  status: 'pending',
36756
36947
  zone: zone,
36757
36948
  };
36758
36949
  this.suggestions.set(suggestion.id, suggestion);
36950
+ this.console.log(`[Discovery] Zone suggestion: ${zone.name} (${zone.type}, ${Math.round(zone.coverage * 100)}% coverage)`);
36759
36951
  }
36760
36952
  }
36761
36953
  }
@@ -41157,6 +41349,7 @@ function createAlert(type, trackedObjectId, details, severity = 'info', ruleId)
41157
41349
  */
41158
41350
  Object.defineProperty(exports, "__esModule", ({ value: true }));
41159
41351
  exports.DEFAULT_DISCOVERY_STATUS = exports.RATE_LIMIT_WARNING_THRESHOLD = exports.DEFAULT_DISCOVERY_CONFIG = void 0;
41352
+ exports.distanceToFeet = distanceToFeet;
41160
41353
  /** Default discovery configuration */
41161
41354
  exports.DEFAULT_DISCOVERY_CONFIG = {
41162
41355
  discoveryIntervalHours: 0, // Disabled by default
@@ -41166,6 +41359,17 @@ exports.DEFAULT_DISCOVERY_CONFIG = {
41166
41359
  };
41167
41360
  /** Rate limit warning thresholds (in hours) */
41168
41361
  exports.RATE_LIMIT_WARNING_THRESHOLD = 1; // Warn if interval is less than 1 hour
41362
+ /** Convert distance estimate to approximate feet */
41363
+ function distanceToFeet(distance) {
41364
+ switch (distance) {
41365
+ case 'close': return 5; // 0-10 feet
41366
+ case 'near': return 20; // 10-30 feet
41367
+ case 'medium': return 45; // 30-60 feet
41368
+ case 'far': return 80; // 60-100 feet
41369
+ case 'distant': return 150; // 100+ feet
41370
+ default: return 50;
41371
+ }
41372
+ }
41169
41373
  /** Default discovery status */
41170
41374
  exports.DEFAULT_DISCOVERY_STATUS = {
41171
41375
  isRunning: false,