@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/dist/main.nodejs.js +1 -1
- package/dist/main.nodejs.js.map +1 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +127 -28
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/spatial-reasoning.ts +33 -24
- package/src/core/topology-discovery.ts +2 -2
- package/src/main.ts +96 -5
- package/src/ui/editor-html.ts +6 -0
package/out/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -84,41 +84,50 @@ export interface ImageData {
|
|
|
84
84
|
*/
|
|
85
85
|
export async function mediaObjectToBase64(mediaObject: MediaObject): Promise<ImageData | null> {
|
|
86
86
|
try {
|
|
87
|
-
|
|
87
|
+
const mimeType = mediaObject?.mimeType || 'image/jpeg';
|
|
88
|
+
console.log(`[Image] Converting MediaObject, mimeType=${mimeType}`);
|
|
88
89
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
|
110
|
-
if (
|
|
111
|
-
console.log(`[Image] Got data
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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]
|
|
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:
|
|
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:
|
|
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
|
|
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) {
|
package/src/ui/editor-html.ts
CHANGED
|
@@ -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();
|