@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/plugin.zip CHANGED
Binary file
@@ -36481,18 +36481,31 @@ class TopologyDiscoveryEngine {
36481
36481
  for (const formatType of formatsToTry) {
36482
36482
  try {
36483
36483
  this.console.log(`[Discovery] Trying ${formatType} image format for ${cameraName}...`);
36484
+ // Build prompt with camera context (height)
36485
+ const cameraNode = this.topology ? (0, topology_1.findCamera)(this.topology, cameraId) : null;
36486
+ const mountHeight = cameraNode?.context?.mountHeight || 8;
36487
+ const cameraRange = cameraNode?.fov?.range || 80;
36488
+ // Add camera-specific context to the prompt
36489
+ const contextPrefix = `CAMERA INFORMATION:
36490
+ - Camera Name: ${cameraName}
36491
+ - Mount Height: ${mountHeight} feet above ground
36492
+ - Approximate viewing range: ${cameraRange} feet
36493
+
36494
+ 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.
36495
+
36496
+ `;
36484
36497
  // Build multimodal message with provider-specific image format
36485
36498
  const result = await llm.getChatCompletion({
36486
36499
  messages: [
36487
36500
  {
36488
36501
  role: 'user',
36489
36502
  content: [
36490
- { type: 'text', text: SCENE_ANALYSIS_PROMPT },
36503
+ { type: 'text', text: contextPrefix + SCENE_ANALYSIS_PROMPT },
36491
36504
  (0, spatial_reasoning_1.buildImageContent)(imageData, formatType),
36492
36505
  ],
36493
36506
  },
36494
36507
  ],
36495
- max_tokens: 1500,
36508
+ max_tokens: 4000, // Increased for detailed scene analysis
36496
36509
  temperature: 0.3,
36497
36510
  });
36498
36511
  const content = result?.choices?.[0]?.message?.content;
@@ -36503,7 +36516,8 @@ class TopologyDiscoveryEngine {
36503
36516
  if (jsonStr.startsWith('```')) {
36504
36517
  jsonStr = jsonStr.replace(/```json?\n?/g, '').replace(/```$/g, '').trim();
36505
36518
  }
36506
- const parsed = JSON.parse(jsonStr);
36519
+ // Try to recover truncated JSON
36520
+ const parsed = this.parseJsonWithRecovery(jsonStr, cameraName);
36507
36521
  // Map parsed data to our types
36508
36522
  if (Array.isArray(parsed.landmarks)) {
36509
36523
  analysis.landmarks = parsed.landmarks.map((l) => ({
@@ -36643,6 +36657,93 @@ class TopologyDiscoveryEngine {
36643
36657
  return 'distant';
36644
36658
  return 'medium'; // Default to medium if not specified
36645
36659
  }
36660
+ /** Try to parse JSON with recovery for truncated responses */
36661
+ parseJsonWithRecovery(jsonStr, context) {
36662
+ // First, try direct parse
36663
+ try {
36664
+ return JSON.parse(jsonStr);
36665
+ }
36666
+ catch (e) {
36667
+ // Log the raw response for debugging (first 500 chars)
36668
+ this.console.log(`[Discovery] Raw LLM response for ${context} (first 500 chars): ${jsonStr.substring(0, 500)}...`);
36669
+ }
36670
+ // Try to recover truncated JSON by finding complete sections
36671
+ try {
36672
+ // Find where valid JSON might end (look for last complete object/array)
36673
+ let recoveredJson = jsonStr;
36674
+ // Try to close unclosed strings
36675
+ const lastQuote = recoveredJson.lastIndexOf('"');
36676
+ const lastColon = recoveredJson.lastIndexOf(':');
36677
+ if (lastQuote > lastColon) {
36678
+ // We might be in the middle of a string value
36679
+ const beforeQuote = recoveredJson.substring(0, lastQuote);
36680
+ const afterLastCompleteEntry = beforeQuote.lastIndexOf('},');
36681
+ if (afterLastCompleteEntry > 0) {
36682
+ recoveredJson = beforeQuote.substring(0, afterLastCompleteEntry + 1);
36683
+ }
36684
+ }
36685
+ // Close any unclosed arrays/objects
36686
+ let openBraces = (recoveredJson.match(/{/g) || []).length;
36687
+ let closeBraces = (recoveredJson.match(/}/g) || []).length;
36688
+ let openBrackets = (recoveredJson.match(/\[/g) || []).length;
36689
+ let closeBrackets = (recoveredJson.match(/\]/g) || []).length;
36690
+ // Add missing closing brackets/braces
36691
+ while (closeBrackets < openBrackets) {
36692
+ recoveredJson += ']';
36693
+ closeBrackets++;
36694
+ }
36695
+ while (closeBraces < openBraces) {
36696
+ recoveredJson += '}';
36697
+ closeBraces++;
36698
+ }
36699
+ const recovered = JSON.parse(recoveredJson);
36700
+ this.console.log(`[Discovery] Recovered truncated JSON for ${context}`);
36701
+ return recovered;
36702
+ }
36703
+ catch (recoveryError) {
36704
+ // Last resort: try to extract just landmarks array
36705
+ try {
36706
+ const landmarksMatch = jsonStr.match(/"landmarks"\s*:\s*\[([\s\S]*?)(?:\]|$)/);
36707
+ const zonesMatch = jsonStr.match(/"zones"\s*:\s*\[([\s\S]*?)(?:\]|$)/);
36708
+ const result = { landmarks: [], zones: [], edges: {}, orientation: 'unknown' };
36709
+ if (landmarksMatch) {
36710
+ // Try to parse individual landmark objects
36711
+ const landmarksStr = landmarksMatch[1];
36712
+ const landmarkObjects = landmarksStr.match(/\{[^{}]*\}/g) || [];
36713
+ result.landmarks = landmarkObjects.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.landmarks.length} landmarks from partial response for ${context}`);
36722
+ }
36723
+ if (zonesMatch) {
36724
+ const zonesStr = zonesMatch[1];
36725
+ const zoneObjects = zonesStr.match(/\{[^{}]*\}/g) || [];
36726
+ result.zones = zoneObjects.map((obj) => {
36727
+ try {
36728
+ return JSON.parse(obj);
36729
+ }
36730
+ catch {
36731
+ return null;
36732
+ }
36733
+ }).filter(Boolean);
36734
+ this.console.log(`[Discovery] Extracted ${result.zones.length} zones from partial response for ${context}`);
36735
+ }
36736
+ if (result.landmarks.length > 0 || result.zones.length > 0) {
36737
+ return result;
36738
+ }
36739
+ }
36740
+ catch (extractError) {
36741
+ // Give up
36742
+ }
36743
+ this.console.warn(`[Discovery] Could not recover JSON for ${context}`);
36744
+ throw new Error(`Failed to parse LLM response: truncated or malformed JSON`);
36745
+ }
36746
+ }
36646
36747
  /** Resolve a camera reference (name or deviceId) to its deviceId */
36647
36748
  resolveCameraRef(ref) {
36648
36749
  if (!this.topology?.cameras || !ref)
@@ -43631,8 +43732,12 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43631
43732
  const fov = camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 };
43632
43733
  // Convert stored pixel range to feet for display
43633
43734
  const rangeInFeet = Math.round(pixelsToFeet(fov.range || 80));
43735
+ // Get mount height from context or default to 8
43736
+ const mountHeight = camera.context?.mountHeight || 8;
43634
43737
  panel.innerHTML = '<h3>Camera Properties</h3>' +
43635
43738
  '<div class="form-group"><label>Name</label><input type="text" value="' + camera.name + '" onchange="updateCameraName(\\'' + camera.deviceId + '\\', this.value)"></div>' +
43739
+ '<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>' +
43740
+ '<div style="font-size: 11px; color: #666; margin-top: -10px; margin-bottom: 10px;">Height affects distance estimation in discovery</div>' +
43636
43741
  '<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isEntryPoint ? 'checked' : '') + ' onchange="updateCameraEntry(\\'' + camera.deviceId + '\\', this.checked)">Entry Point</label></div>' +
43637
43742
  '<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isExitPoint ? 'checked' : '') + ' onchange="updateCameraExit(\\'' + camera.deviceId + '\\', this.checked)">Exit Point</label></div>' +
43638
43743
  '<h4 style="margin-top: 15px; margin-bottom: 10px; color: #888;">Field of View</h4>' +
@@ -43651,6 +43756,12 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43651
43756
  function updateCameraName(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.name = value; updateUI(); }
43652
43757
  function updateCameraEntry(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isEntryPoint = value; }
43653
43758
  function updateCameraExit(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isExitPoint = value; }
43759
+ function updateCameraMountHeight(id, value) {
43760
+ const camera = topology.cameras.find(c => c.deviceId === id);
43761
+ if (!camera) return;
43762
+ if (!camera.context) camera.context = {};
43763
+ camera.context.mountHeight = parseFloat(value) || 8;
43764
+ }
43654
43765
  function updateCameraFov(id, field, value) {
43655
43766
  const camera = topology.cameras.find(c => c.deviceId === id);
43656
43767
  if (!camera) return;