@blueharford/scrypted-spatial-awareness 0.4.8-beta.1 → 0.5.0-beta
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/README.md +94 -0
- package/package.json +1 -1
- package/src/core/spatial-reasoning.ts +61 -5
- package/src/core/topology-discovery.ts +641 -0
- package/src/main.ts +289 -1
- package/src/models/discovery.ts +210 -0
- package/src/models/topology.ts +53 -0
- package/src/ui/editor-html.ts +467 -1
- package/dist/main.nodejs.js +0 -3
- package/dist/main.nodejs.js.LICENSE.txt +0 -1
- package/dist/main.nodejs.js.map +0 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +0 -42753
- package/out/main.nodejs.js.map +0 -1
- package/out/plugin.zip +0 -0
package/src/main.ts
CHANGED
|
@@ -36,6 +36,8 @@ import { MqttPublisher, MqttConfig } from './integrations/mqtt-publisher';
|
|
|
36
36
|
import { EDITOR_HTML } from './ui/editor-html';
|
|
37
37
|
import { TRAINING_HTML } from './ui/training-html';
|
|
38
38
|
import { TrainingConfig, TrainingLandmark } from './models/training';
|
|
39
|
+
import { TopologyDiscoveryEngine } from './core/topology-discovery';
|
|
40
|
+
import { DiscoveryConfig, DiscoverySuggestion } from './models/discovery';
|
|
39
41
|
|
|
40
42
|
const { deviceManager, systemManager, mediaManager } = sdk;
|
|
41
43
|
|
|
@@ -49,6 +51,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
49
51
|
private trackingState: TrackingState;
|
|
50
52
|
private alertManager: AlertManager;
|
|
51
53
|
private mqttPublisher: MqttPublisher | null = null;
|
|
54
|
+
private discoveryEngine: TopologyDiscoveryEngine | null = null;
|
|
52
55
|
private devices: Map<string, any> = new Map();
|
|
53
56
|
|
|
54
57
|
storageSettings = new StorageSettings(this, {
|
|
@@ -169,6 +172,36 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
169
172
|
group: 'AI & Spatial Reasoning',
|
|
170
173
|
},
|
|
171
174
|
|
|
175
|
+
// Auto-Topology Discovery Settings
|
|
176
|
+
discoveryIntervalHours: {
|
|
177
|
+
title: 'Auto-Discovery Interval (hours)',
|
|
178
|
+
type: 'number',
|
|
179
|
+
defaultValue: 0,
|
|
180
|
+
description: 'Automatically scan cameras to discover landmarks and connections. Set to 0 to disable. Uses vision LLM to analyze camera views.',
|
|
181
|
+
group: 'Auto-Topology Discovery',
|
|
182
|
+
},
|
|
183
|
+
minLandmarkConfidence: {
|
|
184
|
+
title: 'Min Landmark Confidence',
|
|
185
|
+
type: 'number',
|
|
186
|
+
defaultValue: 0.6,
|
|
187
|
+
description: 'Minimum confidence (0-1) for discovered landmarks',
|
|
188
|
+
group: 'Auto-Topology Discovery',
|
|
189
|
+
},
|
|
190
|
+
minConnectionConfidence: {
|
|
191
|
+
title: 'Min Connection Confidence',
|
|
192
|
+
type: 'number',
|
|
193
|
+
defaultValue: 0.5,
|
|
194
|
+
description: 'Minimum confidence (0-1) for suggested camera connections',
|
|
195
|
+
group: 'Auto-Topology Discovery',
|
|
196
|
+
},
|
|
197
|
+
autoAcceptThreshold: {
|
|
198
|
+
title: 'Auto-Accept Threshold',
|
|
199
|
+
type: 'number',
|
|
200
|
+
defaultValue: 0.85,
|
|
201
|
+
description: 'Suggestions above this confidence are automatically accepted (0-1)',
|
|
202
|
+
group: 'Auto-Topology Discovery',
|
|
203
|
+
},
|
|
204
|
+
|
|
172
205
|
// MQTT Settings
|
|
173
206
|
enableMqtt: {
|
|
174
207
|
title: 'Enable MQTT',
|
|
@@ -355,6 +388,33 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
355
388
|
|
|
356
389
|
await this.trackingEngine.startTracking();
|
|
357
390
|
this.console.log('Tracking engine started');
|
|
391
|
+
|
|
392
|
+
// Initialize or update discovery engine
|
|
393
|
+
await this.initializeDiscoveryEngine(topology);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private async initializeDiscoveryEngine(topology: CameraTopology): Promise<void> {
|
|
397
|
+
const discoveryConfig: DiscoveryConfig = {
|
|
398
|
+
discoveryIntervalHours: this.storageSettings.values.discoveryIntervalHours as number ?? 0,
|
|
399
|
+
autoAcceptThreshold: this.storageSettings.values.autoAcceptThreshold as number ?? 0.85,
|
|
400
|
+
minLandmarkConfidence: this.storageSettings.values.minLandmarkConfidence as number ?? 0.6,
|
|
401
|
+
minConnectionConfidence: this.storageSettings.values.minConnectionConfidence as number ?? 0.5,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
if (this.discoveryEngine) {
|
|
405
|
+
// Update existing engine
|
|
406
|
+
this.discoveryEngine.updateConfig(discoveryConfig);
|
|
407
|
+
this.discoveryEngine.updateTopology(topology);
|
|
408
|
+
} else {
|
|
409
|
+
// Create new engine
|
|
410
|
+
this.discoveryEngine = new TopologyDiscoveryEngine(discoveryConfig, this.console);
|
|
411
|
+
this.discoveryEngine.updateTopology(topology);
|
|
412
|
+
|
|
413
|
+
// Start periodic discovery if enabled
|
|
414
|
+
if (discoveryConfig.discoveryIntervalHours > 0) {
|
|
415
|
+
this.discoveryEngine.startPeriodicDiscovery();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
358
418
|
}
|
|
359
419
|
|
|
360
420
|
// ==================== DeviceProvider Implementation ====================
|
|
@@ -874,6 +934,27 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
874
934
|
return this.handleTrainingApplyRequest(response);
|
|
875
935
|
}
|
|
876
936
|
|
|
937
|
+
// Discovery endpoints
|
|
938
|
+
if (path.endsWith('/api/discovery/scan')) {
|
|
939
|
+
return this.handleDiscoveryScanRequest(response);
|
|
940
|
+
}
|
|
941
|
+
if (path.endsWith('/api/discovery/status')) {
|
|
942
|
+
return this.handleDiscoveryStatusRequest(response);
|
|
943
|
+
}
|
|
944
|
+
if (path.endsWith('/api/discovery/suggestions')) {
|
|
945
|
+
return this.handleDiscoverySuggestionsRequest(response);
|
|
946
|
+
}
|
|
947
|
+
if (path.match(/\/api\/discovery\/suggestions\/[\w-]+\/(accept|reject)$/)) {
|
|
948
|
+
const parts = path.split('/');
|
|
949
|
+
const action = parts.pop()!;
|
|
950
|
+
const suggestionId = parts.pop()!;
|
|
951
|
+
return this.handleDiscoverySuggestionActionRequest(suggestionId, action, response);
|
|
952
|
+
}
|
|
953
|
+
if (path.match(/\/api\/discovery\/camera\/[\w-]+$/)) {
|
|
954
|
+
const cameraId = path.split('/').pop()!;
|
|
955
|
+
return this.handleDiscoveryCameraAnalysisRequest(cameraId, response);
|
|
956
|
+
}
|
|
957
|
+
|
|
877
958
|
// UI Routes
|
|
878
959
|
if (path.endsWith('/ui/editor') || path.endsWith('/ui/editor/')) {
|
|
879
960
|
return this.serveEditorUI(response);
|
|
@@ -890,7 +971,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
890
971
|
// Default: return info page
|
|
891
972
|
response.send(JSON.stringify({
|
|
892
973
|
name: 'Spatial Awareness Plugin',
|
|
893
|
-
version: '0.
|
|
974
|
+
version: '0.5.0-beta',
|
|
894
975
|
endpoints: {
|
|
895
976
|
api: {
|
|
896
977
|
trackedObjects: '/api/tracked-objects',
|
|
@@ -911,6 +992,12 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
911
992
|
landmark: '/api/training/landmark',
|
|
912
993
|
apply: '/api/training/apply',
|
|
913
994
|
},
|
|
995
|
+
discovery: {
|
|
996
|
+
scan: '/api/discovery/scan',
|
|
997
|
+
status: '/api/discovery/status',
|
|
998
|
+
suggestions: '/api/discovery/suggestions',
|
|
999
|
+
camera: '/api/discovery/camera/{cameraId}',
|
|
1000
|
+
},
|
|
914
1001
|
},
|
|
915
1002
|
ui: {
|
|
916
1003
|
editor: '/ui/editor',
|
|
@@ -1581,6 +1668,207 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
1581
1668
|
});
|
|
1582
1669
|
}
|
|
1583
1670
|
|
|
1671
|
+
// ==================== Discovery Handlers ====================
|
|
1672
|
+
|
|
1673
|
+
private async handleDiscoveryScanRequest(response: HttpResponse): Promise<void> {
|
|
1674
|
+
if (!this.discoveryEngine) {
|
|
1675
|
+
response.send(JSON.stringify({ error: 'Discovery engine not initialized. Configure topology first.' }), {
|
|
1676
|
+
code: 500,
|
|
1677
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1678
|
+
});
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
try {
|
|
1683
|
+
this.console.log('[Discovery] Manual scan triggered via API');
|
|
1684
|
+
const correlation = await this.discoveryEngine.runFullDiscovery();
|
|
1685
|
+
const status = this.discoveryEngine.getStatus();
|
|
1686
|
+
const suggestions = this.discoveryEngine.getPendingSuggestions();
|
|
1687
|
+
|
|
1688
|
+
response.send(JSON.stringify({
|
|
1689
|
+
success: true,
|
|
1690
|
+
status,
|
|
1691
|
+
correlation,
|
|
1692
|
+
suggestions,
|
|
1693
|
+
}), {
|
|
1694
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1695
|
+
});
|
|
1696
|
+
} catch (e) {
|
|
1697
|
+
this.console.error('[Discovery] Scan failed:', e);
|
|
1698
|
+
response.send(JSON.stringify({ error: `Scan failed: ${(e as Error).message}` }), {
|
|
1699
|
+
code: 500,
|
|
1700
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
private handleDiscoveryStatusRequest(response: HttpResponse): void {
|
|
1706
|
+
if (!this.discoveryEngine) {
|
|
1707
|
+
response.send(JSON.stringify({
|
|
1708
|
+
isRunning: false,
|
|
1709
|
+
isScanning: false,
|
|
1710
|
+
lastScanTime: null,
|
|
1711
|
+
nextScanTime: null,
|
|
1712
|
+
camerasAnalyzed: 0,
|
|
1713
|
+
pendingSuggestions: 0,
|
|
1714
|
+
}), {
|
|
1715
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1716
|
+
});
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const status = this.discoveryEngine.getStatus();
|
|
1721
|
+
response.send(JSON.stringify(status), {
|
|
1722
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
private handleDiscoverySuggestionsRequest(response: HttpResponse): void {
|
|
1727
|
+
if (!this.discoveryEngine) {
|
|
1728
|
+
response.send(JSON.stringify({ suggestions: [] }), {
|
|
1729
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1730
|
+
});
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const suggestions = this.discoveryEngine.getPendingSuggestions();
|
|
1735
|
+
response.send(JSON.stringify({ suggestions }), {
|
|
1736
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
private handleDiscoverySuggestionActionRequest(
|
|
1741
|
+
suggestionId: string,
|
|
1742
|
+
action: string,
|
|
1743
|
+
response: HttpResponse
|
|
1744
|
+
): void {
|
|
1745
|
+
if (!this.discoveryEngine) {
|
|
1746
|
+
response.send(JSON.stringify({ error: 'Discovery engine not initialized' }), {
|
|
1747
|
+
code: 500,
|
|
1748
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1749
|
+
});
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
if (action === 'accept') {
|
|
1754
|
+
const suggestion = this.discoveryEngine.acceptSuggestion(suggestionId);
|
|
1755
|
+
if (suggestion) {
|
|
1756
|
+
// Apply accepted suggestion to topology
|
|
1757
|
+
this.applyDiscoverySuggestion(suggestion);
|
|
1758
|
+
response.send(JSON.stringify({ success: true, suggestion }), {
|
|
1759
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1760
|
+
});
|
|
1761
|
+
} else {
|
|
1762
|
+
response.send(JSON.stringify({ error: 'Suggestion not found' }), {
|
|
1763
|
+
code: 404,
|
|
1764
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
} else if (action === 'reject') {
|
|
1768
|
+
const success = this.discoveryEngine.rejectSuggestion(suggestionId);
|
|
1769
|
+
if (success) {
|
|
1770
|
+
response.send(JSON.stringify({ success: true }), {
|
|
1771
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1772
|
+
});
|
|
1773
|
+
} else {
|
|
1774
|
+
response.send(JSON.stringify({ error: 'Suggestion not found' }), {
|
|
1775
|
+
code: 404,
|
|
1776
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
} else {
|
|
1780
|
+
response.send(JSON.stringify({ error: 'Invalid action' }), {
|
|
1781
|
+
code: 400,
|
|
1782
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
private async handleDiscoveryCameraAnalysisRequest(
|
|
1788
|
+
cameraId: string,
|
|
1789
|
+
response: HttpResponse
|
|
1790
|
+
): Promise<void> {
|
|
1791
|
+
if (!this.discoveryEngine) {
|
|
1792
|
+
response.send(JSON.stringify({ error: 'Discovery engine not initialized' }), {
|
|
1793
|
+
code: 500,
|
|
1794
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1795
|
+
});
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
try {
|
|
1800
|
+
const analysis = await this.discoveryEngine.analyzeScene(cameraId);
|
|
1801
|
+
response.send(JSON.stringify(analysis), {
|
|
1802
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1803
|
+
});
|
|
1804
|
+
} catch (e) {
|
|
1805
|
+
response.send(JSON.stringify({ error: `Analysis failed: ${(e as Error).message}` }), {
|
|
1806
|
+
code: 500,
|
|
1807
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
private applyDiscoverySuggestion(suggestion: DiscoverySuggestion): void {
|
|
1813
|
+
if (!this.trackingEngine) return;
|
|
1814
|
+
|
|
1815
|
+
const topology = this.trackingEngine.getTopology();
|
|
1816
|
+
let updated = false;
|
|
1817
|
+
|
|
1818
|
+
if (suggestion.type === 'landmark' && suggestion.landmark) {
|
|
1819
|
+
// Add new landmark to topology
|
|
1820
|
+
const landmark: Landmark = {
|
|
1821
|
+
id: `landmark_${Date.now()}`,
|
|
1822
|
+
name: suggestion.landmark.name!,
|
|
1823
|
+
type: suggestion.landmark.type!,
|
|
1824
|
+
position: suggestion.landmark.position || { x: 0, y: 0 },
|
|
1825
|
+
description: suggestion.landmark.description,
|
|
1826
|
+
visibleFromCameras: suggestion.landmark.visibleFromCameras,
|
|
1827
|
+
aiSuggested: true,
|
|
1828
|
+
aiConfidence: suggestion.confidence,
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
if (!topology.landmarks) {
|
|
1832
|
+
topology.landmarks = [];
|
|
1833
|
+
}
|
|
1834
|
+
topology.landmarks.push(landmark);
|
|
1835
|
+
updated = true;
|
|
1836
|
+
|
|
1837
|
+
this.console.log(`[Discovery] Added landmark: ${landmark.name}`);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
if (suggestion.type === 'connection' && suggestion.connection) {
|
|
1841
|
+
// Add new connection to topology
|
|
1842
|
+
const conn = suggestion.connection;
|
|
1843
|
+
const newConnection = {
|
|
1844
|
+
id: `conn_${Date.now()}`,
|
|
1845
|
+
fromCameraId: conn.fromCameraId,
|
|
1846
|
+
toCameraId: conn.toCameraId,
|
|
1847
|
+
bidirectional: conn.bidirectional,
|
|
1848
|
+
// Default exit/entry zones covering full frame
|
|
1849
|
+
exitZone: [[0, 0], [100, 0], [100, 100], [0, 100]] as [number, number][],
|
|
1850
|
+
entryZone: [[0, 0], [100, 0], [100, 100], [0, 100]] as [number, number][],
|
|
1851
|
+
transitTime: {
|
|
1852
|
+
typical: conn.transitSeconds * 1000,
|
|
1853
|
+
min: Math.max(1000, conn.transitSeconds * 500),
|
|
1854
|
+
max: conn.transitSeconds * 2000,
|
|
1855
|
+
},
|
|
1856
|
+
name: conn.via ? `Via ${conn.via}` : undefined,
|
|
1857
|
+
};
|
|
1858
|
+
|
|
1859
|
+
topology.connections.push(newConnection);
|
|
1860
|
+
updated = true;
|
|
1861
|
+
|
|
1862
|
+
this.console.log(`[Discovery] Added connection: ${conn.fromCameraId} -> ${conn.toCameraId}`);
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
if (updated) {
|
|
1866
|
+
// Save updated topology
|
|
1867
|
+
this.storage.setItem('topology', JSON.stringify(topology));
|
|
1868
|
+
this.trackingEngine.updateTopology(topology);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1584
1872
|
private serveEditorUI(response: HttpResponse): void {
|
|
1585
1873
|
response.send(EDITOR_HTML, {
|
|
1586
1874
|
headers: { 'Content-Type': 'text/html' },
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Topology Discovery Models
|
|
3
|
+
* Types for scene analysis and topology discovery via vision LLM
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { LandmarkType, Landmark, CameraConnection } from './topology';
|
|
7
|
+
|
|
8
|
+
// ==================== Discovery Configuration ====================
|
|
9
|
+
|
|
10
|
+
/** Configuration for the topology discovery engine */
|
|
11
|
+
export interface DiscoveryConfig {
|
|
12
|
+
/** Hours between automatic discovery scans (0 = disabled) */
|
|
13
|
+
discoveryIntervalHours: number;
|
|
14
|
+
/** Minimum confidence threshold for auto-accepting suggestions */
|
|
15
|
+
autoAcceptThreshold: number;
|
|
16
|
+
/** Minimum confidence for landmark suggestions */
|
|
17
|
+
minLandmarkConfidence: number;
|
|
18
|
+
/** Minimum confidence for connection suggestions */
|
|
19
|
+
minConnectionConfidence: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Default discovery configuration */
|
|
23
|
+
export const DEFAULT_DISCOVERY_CONFIG: DiscoveryConfig = {
|
|
24
|
+
discoveryIntervalHours: 0, // Disabled by default
|
|
25
|
+
autoAcceptThreshold: 0.85,
|
|
26
|
+
minLandmarkConfidence: 0.6,
|
|
27
|
+
minConnectionConfidence: 0.5,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Rate limit warning thresholds (in hours) */
|
|
31
|
+
export const RATE_LIMIT_WARNING_THRESHOLD = 1; // Warn if interval is less than 1 hour
|
|
32
|
+
|
|
33
|
+
// ==================== Scene Analysis Types ====================
|
|
34
|
+
|
|
35
|
+
/** Zone types that can be discovered in camera views */
|
|
36
|
+
export type DiscoveredZoneType =
|
|
37
|
+
| 'yard' // Front yard, back yard, side yard
|
|
38
|
+
| 'driveway' // Driveway, parking area
|
|
39
|
+
| 'street' // Street, road, sidewalk
|
|
40
|
+
| 'patio' // Patio, deck
|
|
41
|
+
| 'walkway' // Walkways, paths
|
|
42
|
+
| 'parking' // Parking lot, parking space
|
|
43
|
+
| 'garden' // Garden, landscaped area
|
|
44
|
+
| 'pool' // Pool area
|
|
45
|
+
| 'unknown'; // Unidentified area
|
|
46
|
+
|
|
47
|
+
/** A zone discovered in a camera view */
|
|
48
|
+
export interface DiscoveredZone {
|
|
49
|
+
/** Name of the zone (e.g., "Front Yard", "Driveway") */
|
|
50
|
+
name: string;
|
|
51
|
+
/** Type classification */
|
|
52
|
+
type: DiscoveredZoneType;
|
|
53
|
+
/** Estimated percentage of frame this zone covers (0-1) */
|
|
54
|
+
coverage: number;
|
|
55
|
+
/** Description from LLM analysis */
|
|
56
|
+
description: string;
|
|
57
|
+
/** Bounding box in normalized coordinates [x, y, width, height] (0-1) */
|
|
58
|
+
boundingBox?: [number, number, number, number];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A landmark discovered in a camera view */
|
|
62
|
+
export interface DiscoveredLandmark {
|
|
63
|
+
/** Name of the landmark */
|
|
64
|
+
name: string;
|
|
65
|
+
/** Type classification */
|
|
66
|
+
type: LandmarkType;
|
|
67
|
+
/** Confidence score from LLM (0-1) */
|
|
68
|
+
confidence: number;
|
|
69
|
+
/** Bounding box in normalized coordinates [x, y, width, height] (0-1) */
|
|
70
|
+
boundingBox?: [number, number, number, number];
|
|
71
|
+
/** Description from LLM analysis */
|
|
72
|
+
description: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Edge analysis - what's visible at frame boundaries */
|
|
76
|
+
export interface EdgeAnalysis {
|
|
77
|
+
/** What's visible at the top edge */
|
|
78
|
+
top: string;
|
|
79
|
+
/** What's visible at the left edge */
|
|
80
|
+
left: string;
|
|
81
|
+
/** What's visible at the right edge */
|
|
82
|
+
right: string;
|
|
83
|
+
/** What's visible at the bottom edge */
|
|
84
|
+
bottom: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Complete scene analysis result for a single camera */
|
|
88
|
+
export interface SceneAnalysis {
|
|
89
|
+
/** Camera device ID */
|
|
90
|
+
cameraId: string;
|
|
91
|
+
/** Camera name for reference */
|
|
92
|
+
cameraName: string;
|
|
93
|
+
/** When this analysis was performed */
|
|
94
|
+
timestamp: number;
|
|
95
|
+
/** Landmarks discovered in the scene */
|
|
96
|
+
landmarks: DiscoveredLandmark[];
|
|
97
|
+
/** Zones discovered in the scene */
|
|
98
|
+
zones: DiscoveredZone[];
|
|
99
|
+
/** Edge analysis for camera correlation */
|
|
100
|
+
edges: EdgeAnalysis;
|
|
101
|
+
/** Estimated camera facing direction */
|
|
102
|
+
orientation: 'north' | 'south' | 'east' | 'west' | 'northeast' | 'northwest' | 'southeast' | 'southwest' | 'unknown';
|
|
103
|
+
/** Camera IDs that may have overlapping views */
|
|
104
|
+
potentialOverlaps: string[];
|
|
105
|
+
/** Whether this analysis is still valid (not stale) */
|
|
106
|
+
isValid: boolean;
|
|
107
|
+
/** Error message if analysis failed */
|
|
108
|
+
error?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ==================== Correlation Types ====================
|
|
112
|
+
|
|
113
|
+
/** A landmark that appears in multiple camera views */
|
|
114
|
+
export interface SharedLandmark {
|
|
115
|
+
/** Suggested name for this landmark */
|
|
116
|
+
name: string;
|
|
117
|
+
/** Suggested type */
|
|
118
|
+
type: LandmarkType;
|
|
119
|
+
/** Camera IDs where this landmark is visible */
|
|
120
|
+
seenByCameras: string[];
|
|
121
|
+
/** Confidence in this correlation */
|
|
122
|
+
confidence: number;
|
|
123
|
+
/** Description of the shared landmark */
|
|
124
|
+
description?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A suggested connection between cameras */
|
|
128
|
+
export interface SuggestedConnection {
|
|
129
|
+
/** Source camera ID */
|
|
130
|
+
fromCameraId: string;
|
|
131
|
+
/** Destination camera ID */
|
|
132
|
+
toCameraId: string;
|
|
133
|
+
/** Estimated transit time in seconds */
|
|
134
|
+
transitSeconds: number;
|
|
135
|
+
/** Path description (e.g., "via driveway", "through front yard") */
|
|
136
|
+
via: string;
|
|
137
|
+
/** Confidence in this suggestion */
|
|
138
|
+
confidence: number;
|
|
139
|
+
/** Whether this is bidirectional */
|
|
140
|
+
bidirectional: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Result of correlating scenes across multiple cameras */
|
|
144
|
+
export interface TopologyCorrelation {
|
|
145
|
+
/** Landmarks that appear in multiple camera views */
|
|
146
|
+
sharedLandmarks: SharedLandmark[];
|
|
147
|
+
/** Suggested connections between cameras */
|
|
148
|
+
suggestedConnections: SuggestedConnection[];
|
|
149
|
+
/** Overall description of property layout from LLM */
|
|
150
|
+
layoutDescription: string;
|
|
151
|
+
/** When this correlation was performed */
|
|
152
|
+
timestamp: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ==================== Discovery Suggestions ====================
|
|
156
|
+
|
|
157
|
+
/** Status of a discovery suggestion */
|
|
158
|
+
export type SuggestionStatus = 'pending' | 'accepted' | 'rejected' | 'merged';
|
|
159
|
+
|
|
160
|
+
/** A pending discovery suggestion for user review */
|
|
161
|
+
export interface DiscoverySuggestion {
|
|
162
|
+
/** Unique ID for this suggestion */
|
|
163
|
+
id: string;
|
|
164
|
+
/** Type of suggestion */
|
|
165
|
+
type: 'landmark' | 'zone' | 'connection';
|
|
166
|
+
/** When this was discovered */
|
|
167
|
+
timestamp: number;
|
|
168
|
+
/** Cameras that contributed to this discovery */
|
|
169
|
+
sourceCameras: string[];
|
|
170
|
+
/** Confidence score */
|
|
171
|
+
confidence: number;
|
|
172
|
+
/** Current status */
|
|
173
|
+
status: SuggestionStatus;
|
|
174
|
+
/** The suggested landmark (if type is 'landmark') */
|
|
175
|
+
landmark?: Partial<Landmark>;
|
|
176
|
+
/** The suggested zone (if type is 'zone') */
|
|
177
|
+
zone?: DiscoveredZone;
|
|
178
|
+
/** The suggested connection (if type is 'connection') */
|
|
179
|
+
connection?: SuggestedConnection;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ==================== Discovery Status ====================
|
|
183
|
+
|
|
184
|
+
/** Current status of the discovery engine */
|
|
185
|
+
export interface DiscoveryStatus {
|
|
186
|
+
/** Whether discovery is currently running */
|
|
187
|
+
isRunning: boolean;
|
|
188
|
+
/** Whether a scan is in progress */
|
|
189
|
+
isScanning: boolean;
|
|
190
|
+
/** Last scan timestamp */
|
|
191
|
+
lastScanTime: number | null;
|
|
192
|
+
/** Next scheduled scan timestamp */
|
|
193
|
+
nextScanTime: number | null;
|
|
194
|
+
/** Number of cameras analyzed */
|
|
195
|
+
camerasAnalyzed: number;
|
|
196
|
+
/** Number of pending suggestions */
|
|
197
|
+
pendingSuggestions: number;
|
|
198
|
+
/** Any error from last scan */
|
|
199
|
+
lastError?: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Default discovery status */
|
|
203
|
+
export const DEFAULT_DISCOVERY_STATUS: DiscoveryStatus = {
|
|
204
|
+
isRunning: false,
|
|
205
|
+
isScanning: false,
|
|
206
|
+
lastScanTime: null,
|
|
207
|
+
nextScanTime: null,
|
|
208
|
+
camerasAnalyzed: 0,
|
|
209
|
+
pendingSuggestions: 0,
|
|
210
|
+
};
|
package/src/models/topology.ts
CHANGED
|
@@ -198,6 +198,57 @@ export interface CameraZoneMapping {
|
|
|
198
198
|
zone: ClipPath;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// ==================== Drawn Zones (Floor Plan) ====================
|
|
202
|
+
|
|
203
|
+
/** Zone type for floor plan zones */
|
|
204
|
+
export type DrawnZoneType =
|
|
205
|
+
| 'yard' // Front yard, back yard, side yard
|
|
206
|
+
| 'driveway' // Driveway, parking area
|
|
207
|
+
| 'street' // Street, sidewalk
|
|
208
|
+
| 'patio' // Patio, deck
|
|
209
|
+
| 'walkway' // Walkways, paths
|
|
210
|
+
| 'parking' // Parking lot, parking space
|
|
211
|
+
| 'garden' // Garden, landscaped area
|
|
212
|
+
| 'pool' // Pool area
|
|
213
|
+
| 'garage' // Garage area
|
|
214
|
+
| 'entrance' // Entry areas
|
|
215
|
+
| 'custom'; // Custom zone type
|
|
216
|
+
|
|
217
|
+
/** Zone colors by type */
|
|
218
|
+
export const ZONE_TYPE_COLORS: Record<DrawnZoneType, string> = {
|
|
219
|
+
yard: 'rgba(76, 175, 80, 0.3)', // Green
|
|
220
|
+
driveway: 'rgba(158, 158, 158, 0.3)', // Gray
|
|
221
|
+
street: 'rgba(96, 96, 96, 0.3)', // Dark gray
|
|
222
|
+
patio: 'rgba(255, 152, 0, 0.3)', // Orange
|
|
223
|
+
walkway: 'rgba(121, 85, 72, 0.3)', // Brown
|
|
224
|
+
parking: 'rgba(189, 189, 189, 0.3)', // Light gray
|
|
225
|
+
garden: 'rgba(139, 195, 74, 0.3)', // Light green
|
|
226
|
+
pool: 'rgba(33, 150, 243, 0.3)', // Blue
|
|
227
|
+
garage: 'rgba(117, 117, 117, 0.3)', // Medium gray
|
|
228
|
+
entrance: 'rgba(233, 30, 99, 0.3)', // Pink
|
|
229
|
+
custom: 'rgba(156, 39, 176, 0.3)', // Purple
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/** A zone drawn on the floor plan */
|
|
233
|
+
export interface DrawnZone {
|
|
234
|
+
/** Unique identifier */
|
|
235
|
+
id: string;
|
|
236
|
+
/** Display name */
|
|
237
|
+
name: string;
|
|
238
|
+
/** Zone type */
|
|
239
|
+
type: DrawnZoneType;
|
|
240
|
+
/** Polygon points on floor plan (x, y coordinates) */
|
|
241
|
+
polygon: Point[];
|
|
242
|
+
/** Custom color override (optional) */
|
|
243
|
+
color?: string;
|
|
244
|
+
/** Linked camera IDs that can see this zone */
|
|
245
|
+
linkedCameras?: string[];
|
|
246
|
+
/** Linked landmark IDs within this zone */
|
|
247
|
+
linkedLandmarks?: string[];
|
|
248
|
+
/** Description for context */
|
|
249
|
+
description?: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
201
252
|
// ==================== Spatial Relationships ====================
|
|
202
253
|
|
|
203
254
|
/** Types of spatial relationships between entities */
|
|
@@ -297,6 +348,8 @@ export interface CameraTopology {
|
|
|
297
348
|
relationships: SpatialRelationship[];
|
|
298
349
|
/** Named zones spanning multiple cameras */
|
|
299
350
|
globalZones: GlobalZone[];
|
|
351
|
+
/** Zones drawn on the floor plan */
|
|
352
|
+
drawnZones?: DrawnZone[];
|
|
300
353
|
/** Floor plan configuration (optional) */
|
|
301
354
|
floorPlan?: FloorPlanConfig;
|
|
302
355
|
/** Pending AI landmark suggestions */
|