@blueharford/scrypted-spatial-awareness 0.6.15 → 0.6.17
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 +114 -3
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/topology-discovery.ts +112 -3
- package/src/ui/editor-html.ts +10 -0
package/out/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -379,18 +379,33 @@ export class TopologyDiscoveryEngine {
|
|
|
379
379
|
try {
|
|
380
380
|
this.console.log(`[Discovery] Trying ${formatType} image format for ${cameraName}...`);
|
|
381
381
|
|
|
382
|
+
// Build prompt with camera context (height)
|
|
383
|
+
const cameraNode = this.topology ? findCamera(this.topology, cameraId) : null;
|
|
384
|
+
const mountHeight = cameraNode?.context?.mountHeight || 8;
|
|
385
|
+
const cameraRange = (cameraNode?.fov as any)?.range || 80;
|
|
386
|
+
|
|
387
|
+
// Add camera-specific context to the prompt
|
|
388
|
+
const contextPrefix = `CAMERA INFORMATION:
|
|
389
|
+
- Camera Name: ${cameraName}
|
|
390
|
+
- Mount Height: ${mountHeight} feet above ground
|
|
391
|
+
- Approximate viewing range: ${cameraRange} feet
|
|
392
|
+
|
|
393
|
+
Use the mount height to help estimate distances - objects at ground level will appear at different angles depending on distance from a camera mounted at ${mountHeight} feet.
|
|
394
|
+
|
|
395
|
+
`;
|
|
396
|
+
|
|
382
397
|
// Build multimodal message with provider-specific image format
|
|
383
398
|
const result = await llm.getChatCompletion({
|
|
384
399
|
messages: [
|
|
385
400
|
{
|
|
386
401
|
role: 'user',
|
|
387
402
|
content: [
|
|
388
|
-
{ type: 'text', text: SCENE_ANALYSIS_PROMPT },
|
|
403
|
+
{ type: 'text', text: contextPrefix + SCENE_ANALYSIS_PROMPT },
|
|
389
404
|
buildImageContent(imageData, formatType),
|
|
390
405
|
],
|
|
391
406
|
},
|
|
392
407
|
],
|
|
393
|
-
max_tokens:
|
|
408
|
+
max_tokens: 4000, // Increased for detailed scene analysis
|
|
394
409
|
temperature: 0.3,
|
|
395
410
|
});
|
|
396
411
|
|
|
@@ -403,7 +418,8 @@ export class TopologyDiscoveryEngine {
|
|
|
403
418
|
jsonStr = jsonStr.replace(/```json?\n?/g, '').replace(/```$/g, '').trim();
|
|
404
419
|
}
|
|
405
420
|
|
|
406
|
-
|
|
421
|
+
// Try to recover truncated JSON
|
|
422
|
+
const parsed = this.parseJsonWithRecovery(jsonStr, cameraName);
|
|
407
423
|
|
|
408
424
|
// Map parsed data to our types
|
|
409
425
|
if (Array.isArray(parsed.landmarks)) {
|
|
@@ -544,6 +560,99 @@ export class TopologyDiscoveryEngine {
|
|
|
544
560
|
return 'medium'; // Default to medium if not specified
|
|
545
561
|
}
|
|
546
562
|
|
|
563
|
+
/** Try to parse JSON with recovery for truncated responses */
|
|
564
|
+
private parseJsonWithRecovery(jsonStr: string, context: string): any {
|
|
565
|
+
// First, try direct parse
|
|
566
|
+
try {
|
|
567
|
+
return JSON.parse(jsonStr);
|
|
568
|
+
} catch (e) {
|
|
569
|
+
// Log the raw response for debugging (first 500 chars)
|
|
570
|
+
this.console.log(`[Discovery] Raw LLM response for ${context} (first 500 chars): ${jsonStr.substring(0, 500)}...`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Try to recover truncated JSON by finding complete sections
|
|
574
|
+
try {
|
|
575
|
+
// Find where valid JSON might end (look for last complete object/array)
|
|
576
|
+
let recoveredJson = jsonStr;
|
|
577
|
+
|
|
578
|
+
// Try to close unclosed strings
|
|
579
|
+
const lastQuote = recoveredJson.lastIndexOf('"');
|
|
580
|
+
const lastColon = recoveredJson.lastIndexOf(':');
|
|
581
|
+
if (lastQuote > lastColon) {
|
|
582
|
+
// We might be in the middle of a string value
|
|
583
|
+
const beforeQuote = recoveredJson.substring(0, lastQuote);
|
|
584
|
+
const afterLastCompleteEntry = beforeQuote.lastIndexOf('},');
|
|
585
|
+
if (afterLastCompleteEntry > 0) {
|
|
586
|
+
recoveredJson = beforeQuote.substring(0, afterLastCompleteEntry + 1);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Close any unclosed arrays/objects
|
|
591
|
+
let openBraces = (recoveredJson.match(/{/g) || []).length;
|
|
592
|
+
let closeBraces = (recoveredJson.match(/}/g) || []).length;
|
|
593
|
+
let openBrackets = (recoveredJson.match(/\[/g) || []).length;
|
|
594
|
+
let closeBrackets = (recoveredJson.match(/\]/g) || []).length;
|
|
595
|
+
|
|
596
|
+
// Add missing closing brackets/braces
|
|
597
|
+
while (closeBrackets < openBrackets) {
|
|
598
|
+
recoveredJson += ']';
|
|
599
|
+
closeBrackets++;
|
|
600
|
+
}
|
|
601
|
+
while (closeBraces < openBraces) {
|
|
602
|
+
recoveredJson += '}';
|
|
603
|
+
closeBraces++;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const recovered = JSON.parse(recoveredJson);
|
|
607
|
+
this.console.log(`[Discovery] Recovered truncated JSON for ${context}`);
|
|
608
|
+
return recovered;
|
|
609
|
+
} catch (recoveryError) {
|
|
610
|
+
// Last resort: try to extract just landmarks array
|
|
611
|
+
try {
|
|
612
|
+
const landmarksMatch = jsonStr.match(/"landmarks"\s*:\s*\[([\s\S]*?)(?:\]|$)/);
|
|
613
|
+
const zonesMatch = jsonStr.match(/"zones"\s*:\s*\[([\s\S]*?)(?:\]|$)/);
|
|
614
|
+
|
|
615
|
+
const result: any = { landmarks: [], zones: [], edges: {}, orientation: 'unknown' };
|
|
616
|
+
|
|
617
|
+
if (landmarksMatch) {
|
|
618
|
+
// Try to parse individual landmark objects
|
|
619
|
+
const landmarksStr = landmarksMatch[1];
|
|
620
|
+
const landmarkObjects = landmarksStr.match(/\{[^{}]*\}/g) || [];
|
|
621
|
+
result.landmarks = landmarkObjects.map((obj: string) => {
|
|
622
|
+
try {
|
|
623
|
+
return JSON.parse(obj);
|
|
624
|
+
} catch {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
}).filter(Boolean);
|
|
628
|
+
this.console.log(`[Discovery] Extracted ${result.landmarks.length} landmarks from partial response for ${context}`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (zonesMatch) {
|
|
632
|
+
const zonesStr = zonesMatch[1];
|
|
633
|
+
const zoneObjects = zonesStr.match(/\{[^{}]*\}/g) || [];
|
|
634
|
+
result.zones = zoneObjects.map((obj: string) => {
|
|
635
|
+
try {
|
|
636
|
+
return JSON.parse(obj);
|
|
637
|
+
} catch {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
}).filter(Boolean);
|
|
641
|
+
this.console.log(`[Discovery] Extracted ${result.zones.length} zones from partial response for ${context}`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (result.landmarks.length > 0 || result.zones.length > 0) {
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
647
|
+
} catch (extractError) {
|
|
648
|
+
// Give up
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
this.console.warn(`[Discovery] Could not recover JSON for ${context}`);
|
|
652
|
+
throw new Error(`Failed to parse LLM response: truncated or malformed JSON`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
547
656
|
/** Resolve a camera reference (name or deviceId) to its deviceId */
|
|
548
657
|
private resolveCameraRef(ref: string): string | null {
|
|
549
658
|
if (!this.topology?.cameras || !ref) return null;
|
package/src/ui/editor-html.ts
CHANGED
|
@@ -1632,8 +1632,12 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
1632
1632
|
const fov = camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 };
|
|
1633
1633
|
// Convert stored pixel range to feet for display
|
|
1634
1634
|
const rangeInFeet = Math.round(pixelsToFeet(fov.range || 80));
|
|
1635
|
+
// Get mount height from context or default to 8
|
|
1636
|
+
const mountHeight = camera.context?.mountHeight || 8;
|
|
1635
1637
|
panel.innerHTML = '<h3>Camera Properties</h3>' +
|
|
1636
1638
|
'<div class="form-group"><label>Name</label><input type="text" value="' + camera.name + '" onchange="updateCameraName(\\'' + camera.deviceId + '\\', this.value)"></div>' +
|
|
1639
|
+
'<div class="form-group"><label>Mount Height (feet)</label><input type="number" value="' + mountHeight + '" min="1" max="40" step="0.5" onchange="updateCameraMountHeight(\\'' + camera.deviceId + '\\', this.value)"></div>' +
|
|
1640
|
+
'<div style="font-size: 11px; color: #666; margin-top: -10px; margin-bottom: 10px;">Height affects distance estimation in discovery</div>' +
|
|
1637
1641
|
'<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isEntryPoint ? 'checked' : '') + ' onchange="updateCameraEntry(\\'' + camera.deviceId + '\\', this.checked)">Entry Point</label></div>' +
|
|
1638
1642
|
'<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isExitPoint ? 'checked' : '') + ' onchange="updateCameraExit(\\'' + camera.deviceId + '\\', this.checked)">Exit Point</label></div>' +
|
|
1639
1643
|
'<h4 style="margin-top: 15px; margin-bottom: 10px; color: #888;">Field of View</h4>' +
|
|
@@ -1652,6 +1656,12 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
1652
1656
|
function updateCameraName(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.name = value; updateUI(); }
|
|
1653
1657
|
function updateCameraEntry(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isEntryPoint = value; }
|
|
1654
1658
|
function updateCameraExit(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isExitPoint = value; }
|
|
1659
|
+
function updateCameraMountHeight(id, value) {
|
|
1660
|
+
const camera = topology.cameras.find(c => c.deviceId === id);
|
|
1661
|
+
if (!camera) return;
|
|
1662
|
+
if (!camera.context) camera.context = {};
|
|
1663
|
+
camera.context.mountHeight = parseFloat(value) || 8;
|
|
1664
|
+
}
|
|
1655
1665
|
function updateCameraFov(id, field, value) {
|
|
1656
1666
|
const camera = topology.cameras.find(c => c.deviceId === id);
|
|
1657
1667
|
if (!camera) return;
|