@blueharford/scrypted-spatial-awareness 0.5.7 → 0.5.9

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/out/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.5.7",
3
+ "version": "0.5.9",
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",
@@ -84,41 +84,50 @@ export interface ImageData {
84
84
  */
85
85
  export async function mediaObjectToBase64(mediaObject: MediaObject): Promise<ImageData | null> {
86
86
  try {
87
- console.log(`[Image] Converting MediaObject, mimeType=${mediaObject?.mimeType}`);
87
+ const mimeType = mediaObject?.mimeType || 'image/jpeg';
88
+ console.log(`[Image] Converting MediaObject, mimeType=${mimeType}`);
88
89
 
89
- // First convert to JPEG to ensure consistent format
90
- const jpegMediaObject = await mediaManager.convertMediaObject(mediaObject, 'image/jpeg') as MediaObject;
91
- console.log(`[Image] Converted to JPEG MediaObject`);
90
+ // Use createMediaObject to ensure we have a proper MediaObject with mimeType
91
+ // Then convert to buffer - this should handle the conversion internally
92
+ let buffer: Buffer;
92
93
 
93
- // Get the buffer from the converted media object
94
- const buffer = await mediaManager.convertMediaObjectToBuffer(jpegMediaObject, 'image/jpeg');
95
-
96
- // Check if we got an actual Buffer (not a proxy)
97
- const isRealBuffer = Buffer.isBuffer(buffer);
98
- const bufferLength = isRealBuffer ? buffer.length : 0;
99
-
100
- console.log(`[Image] Buffer: isBuffer=${isRealBuffer}, length=${bufferLength}`);
101
-
102
- if (!isRealBuffer || bufferLength === 0) {
103
- console.warn('[Image] Did not receive a valid Buffer');
94
+ try {
95
+ // Try direct conversion with the source mime type
96
+ buffer = await mediaManager.convertMediaObjectToBuffer(mediaObject, mimeType);
97
+ } catch (convErr) {
98
+ console.warn(`[Image] Direct conversion failed, trying with explicit JPEG:`, convErr);
104
99
 
105
- // Try alternate approach: get raw data using any type
100
+ // Try creating a new MediaObject with explicit mimeType
106
101
  try {
102
+ // Get raw data if available
107
103
  const anyMedia = mediaObject as any;
108
104
  if (typeof anyMedia.getData === 'function') {
109
- const data = await anyMedia.getData();
110
- if (data && Buffer.isBuffer(data)) {
111
- console.log(`[Image] Got data from getData(): ${data.length} bytes`);
112
- if (data.length > 1000) {
113
- const base64 = data.toString('base64');
114
- return { base64, mediaType: 'image/jpeg' };
115
- }
105
+ const rawData = await anyMedia.getData();
106
+ if (rawData && Buffer.isBuffer(rawData) && rawData.length > 1000) {
107
+ console.log(`[Image] Got raw data: ${rawData.length} bytes`);
108
+ buffer = rawData;
109
+ } else {
110
+ console.warn(`[Image] getData returned invalid data`);
111
+ return null;
116
112
  }
113
+ } else {
114
+ console.warn('[Image] No getData method available');
115
+ return null;
117
116
  }
118
117
  } catch (dataErr) {
119
- console.warn('[Image] getData() failed:', dataErr);
118
+ console.warn('[Image] Alternate data fetch failed:', dataErr);
119
+ return null;
120
120
  }
121
+ }
122
+
123
+ // Check if we got an actual Buffer (not a proxy)
124
+ const isRealBuffer = Buffer.isBuffer(buffer);
125
+ const bufferLength = isRealBuffer ? buffer.length : 0;
126
+
127
+ console.log(`[Image] Buffer: isBuffer=${isRealBuffer}, length=${bufferLength}`);
121
128
 
129
+ if (!isRealBuffer || bufferLength === 0) {
130
+ console.warn('[Image] Did not receive a valid Buffer');
122
131
  return null;
123
132
  }
124
133
 
@@ -305,7 +305,7 @@ export class TopologyDiscoveryEngine {
305
305
  ],
306
306
  },
307
307
  ],
308
- max_tokens: 500,
308
+ max_tokens: 1500,
309
309
  temperature: 0.3,
310
310
  });
311
311
 
@@ -530,7 +530,7 @@ export class TopologyDiscoveryEngine {
530
530
 
531
531
  const result = await llm.getChatCompletion({
532
532
  messages: [{ role: 'user', content: prompt }],
533
- max_tokens: 800,
533
+ max_tokens: 2000,
534
534
  temperature: 0.4,
535
535
  });
536
536
 
package/src/main.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  LandmarkSuggestion,
25
25
  LANDMARK_TEMPLATES,
26
26
  inferRelationships,
27
+ DrawnZoneType,
27
28
  } from './models/topology';
28
29
  import { TrackedObject } from './models/tracked-object';
29
30
  import { Alert, AlertRule, createDefaultRules } from './models/alert';
@@ -1819,12 +1820,39 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1819
1820
  let updated = false;
1820
1821
 
1821
1822
  if (suggestion.type === 'landmark' && suggestion.landmark) {
1823
+ // Calculate a reasonable position for the landmark
1824
+ // Use the first visible camera's position as a starting point, or canvas center
1825
+ let position = suggestion.landmark.position;
1826
+ if (!position || (position.x === 0 && position.y === 0)) {
1827
+ // Find a camera that can see this landmark
1828
+ const visibleCameraId = suggestion.landmark.visibleFromCameras?.[0];
1829
+ const camera = visibleCameraId ? topology.cameras.find(c => c.deviceId === visibleCameraId) : null;
1830
+
1831
+ if (camera?.floorPlanPosition) {
1832
+ // Position near the camera with some offset
1833
+ const offset = (topology.landmarks?.length || 0) * 30;
1834
+ position = {
1835
+ x: camera.floorPlanPosition.x + 50 + (offset % 100),
1836
+ y: camera.floorPlanPosition.y + 50 + Math.floor(offset / 100) * 30,
1837
+ };
1838
+ } else {
1839
+ // Position in a grid pattern starting from center
1840
+ const landmarkCount = topology.landmarks?.length || 0;
1841
+ const gridSize = 80;
1842
+ const cols = 5;
1843
+ position = {
1844
+ x: 200 + (landmarkCount % cols) * gridSize,
1845
+ y: 100 + Math.floor(landmarkCount / cols) * gridSize,
1846
+ };
1847
+ }
1848
+ }
1849
+
1822
1850
  // Add new landmark to topology
1823
1851
  const landmark: Landmark = {
1824
1852
  id: `landmark_${Date.now()}`,
1825
1853
  name: suggestion.landmark.name!,
1826
1854
  type: suggestion.landmark.type!,
1827
- position: suggestion.landmark.position || { x: 0, y: 0 },
1855
+ position,
1828
1856
  description: suggestion.landmark.description,
1829
1857
  visibleFromCameras: suggestion.landmark.visibleFromCameras,
1830
1858
  aiSuggested: true,
@@ -1837,16 +1865,79 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1837
1865
  topology.landmarks.push(landmark);
1838
1866
  updated = true;
1839
1867
 
1840
- this.console.log(`[Discovery] Added landmark: ${landmark.name}`);
1868
+ this.console.log(`[Discovery] Added landmark: ${landmark.name} at (${position.x}, ${position.y})`);
1869
+ }
1870
+
1871
+ if (suggestion.type === 'zone' && suggestion.zone) {
1872
+ // Create a drawn zone from the discovery zone
1873
+ const zone = suggestion.zone;
1874
+
1875
+ // Find cameras that see this zone type to determine position
1876
+ const cameraWithZone = suggestion.sourceCameras?.[0];
1877
+ const camera = cameraWithZone ? topology.cameras.find(c => c.deviceId === cameraWithZone || c.name === cameraWithZone) : null;
1878
+
1879
+ // Create a default polygon near the camera or at a default location
1880
+ let centerX = 300;
1881
+ let centerY = 200;
1882
+ if (camera?.floorPlanPosition) {
1883
+ centerX = camera.floorPlanPosition.x;
1884
+ centerY = camera.floorPlanPosition.y + 80;
1885
+ }
1886
+
1887
+ // Create a rectangular zone (user can edit later)
1888
+ const size = 100;
1889
+ const drawnZone = {
1890
+ id: `zone_${Date.now()}`,
1891
+ name: zone.name,
1892
+ type: (zone.type || 'custom') as DrawnZoneType,
1893
+ description: zone.description,
1894
+ polygon: [
1895
+ { x: centerX - size/2, y: centerY - size/2 },
1896
+ { x: centerX + size/2, y: centerY - size/2 },
1897
+ { x: centerX + size/2, y: centerY + size/2 },
1898
+ { x: centerX - size/2, y: centerY + size/2 },
1899
+ ] as any,
1900
+ };
1901
+
1902
+ if (!topology.drawnZones) {
1903
+ topology.drawnZones = [];
1904
+ }
1905
+ topology.drawnZones.push(drawnZone);
1906
+ updated = true;
1907
+
1908
+ this.console.log(`[Discovery] Added zone: ${zone.name} (${zone.type})`);
1841
1909
  }
1842
1910
 
1843
1911
  if (suggestion.type === 'connection' && suggestion.connection) {
1844
1912
  // Add new connection to topology
1845
1913
  const conn = suggestion.connection;
1914
+
1915
+ // Ensure cameras have floor plan positions for visibility
1916
+ const fromCamera = topology.cameras.find(c => c.deviceId === conn.fromCameraId || c.name === conn.fromCameraId);
1917
+ const toCamera = topology.cameras.find(c => c.deviceId === conn.toCameraId || c.name === conn.toCameraId);
1918
+
1919
+ // Auto-assign floor plan positions if missing
1920
+ if (fromCamera && !fromCamera.floorPlanPosition) {
1921
+ const idx = topology.cameras.indexOf(fromCamera);
1922
+ fromCamera.floorPlanPosition = {
1923
+ x: 150 + (idx % 3) * 200,
1924
+ y: 150 + Math.floor(idx / 3) * 150,
1925
+ };
1926
+ this.console.log(`[Discovery] Auto-positioned camera: ${fromCamera.name}`);
1927
+ }
1928
+ if (toCamera && !toCamera.floorPlanPosition) {
1929
+ const idx = topology.cameras.indexOf(toCamera);
1930
+ toCamera.floorPlanPosition = {
1931
+ x: 150 + (idx % 3) * 200,
1932
+ y: 150 + Math.floor(idx / 3) * 150,
1933
+ };
1934
+ this.console.log(`[Discovery] Auto-positioned camera: ${toCamera.name}`);
1935
+ }
1936
+
1846
1937
  const newConnection = {
1847
1938
  id: `conn_${Date.now()}`,
1848
- fromCameraId: conn.fromCameraId,
1849
- toCameraId: conn.toCameraId,
1939
+ fromCameraId: fromCamera?.deviceId || conn.fromCameraId,
1940
+ toCameraId: toCamera?.deviceId || conn.toCameraId,
1850
1941
  bidirectional: conn.bidirectional,
1851
1942
  // Default exit/entry zones covering full frame
1852
1943
  exitZone: [[0, 0], [100, 0], [100, 100], [0, 100]] as [number, number][],
@@ -1862,7 +1953,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1862
1953
  topology.connections.push(newConnection);
1863
1954
  updated = true;
1864
1955
 
1865
- this.console.log(`[Discovery] Added connection: ${conn.fromCameraId} -> ${conn.toCameraId}`);
1956
+ this.console.log(`[Discovery] Added connection: ${fromCamera?.name || conn.fromCameraId} -> ${toCamera?.name || conn.toCameraId}`);
1866
1957
  }
1867
1958
 
1868
1959
  if (updated) {
@@ -1038,6 +1038,12 @@ export const EDITOR_HTML = `<!DOCTYPE html>
1038
1038
  for (let i = 1; i < currentZonePoints.length; i++) {
1039
1039
  ctx.lineTo(currentZonePoints[i].x, currentZonePoints[i].y);
1040
1040
  }
1041
+ // Close the polygon if we have 3+ points
1042
+ if (currentZonePoints.length >= 3) {
1043
+ ctx.closePath();
1044
+ ctx.fillStyle = color;
1045
+ ctx.fill();
1046
+ }
1041
1047
  ctx.strokeStyle = strokeColor;
1042
1048
  ctx.lineWidth = 2;
1043
1049
  ctx.stroke();