@blueharford/scrypted-spatial-awareness 0.6.15 → 0.6.16

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.6.15",
3
+ "version": "0.6.16",
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",
@@ -390,7 +390,7 @@ export class TopologyDiscoveryEngine {
390
390
  ],
391
391
  },
392
392
  ],
393
- max_tokens: 1500,
393
+ max_tokens: 4000, // Increased for detailed scene analysis
394
394
  temperature: 0.3,
395
395
  });
396
396
 
@@ -403,7 +403,8 @@ export class TopologyDiscoveryEngine {
403
403
  jsonStr = jsonStr.replace(/```json?\n?/g, '').replace(/```$/g, '').trim();
404
404
  }
405
405
 
406
- const parsed = JSON.parse(jsonStr);
406
+ // Try to recover truncated JSON
407
+ const parsed = this.parseJsonWithRecovery(jsonStr, cameraName);
407
408
 
408
409
  // Map parsed data to our types
409
410
  if (Array.isArray(parsed.landmarks)) {
@@ -544,6 +545,99 @@ export class TopologyDiscoveryEngine {
544
545
  return 'medium'; // Default to medium if not specified
545
546
  }
546
547
 
548
+ /** Try to parse JSON with recovery for truncated responses */
549
+ private parseJsonWithRecovery(jsonStr: string, context: string): any {
550
+ // First, try direct parse
551
+ try {
552
+ return JSON.parse(jsonStr);
553
+ } catch (e) {
554
+ // Log the raw response for debugging (first 500 chars)
555
+ this.console.log(`[Discovery] Raw LLM response for ${context} (first 500 chars): ${jsonStr.substring(0, 500)}...`);
556
+ }
557
+
558
+ // Try to recover truncated JSON by finding complete sections
559
+ try {
560
+ // Find where valid JSON might end (look for last complete object/array)
561
+ let recoveredJson = jsonStr;
562
+
563
+ // Try to close unclosed strings
564
+ const lastQuote = recoveredJson.lastIndexOf('"');
565
+ const lastColon = recoveredJson.lastIndexOf(':');
566
+ if (lastQuote > lastColon) {
567
+ // We might be in the middle of a string value
568
+ const beforeQuote = recoveredJson.substring(0, lastQuote);
569
+ const afterLastCompleteEntry = beforeQuote.lastIndexOf('},');
570
+ if (afterLastCompleteEntry > 0) {
571
+ recoveredJson = beforeQuote.substring(0, afterLastCompleteEntry + 1);
572
+ }
573
+ }
574
+
575
+ // Close any unclosed arrays/objects
576
+ let openBraces = (recoveredJson.match(/{/g) || []).length;
577
+ let closeBraces = (recoveredJson.match(/}/g) || []).length;
578
+ let openBrackets = (recoveredJson.match(/\[/g) || []).length;
579
+ let closeBrackets = (recoveredJson.match(/\]/g) || []).length;
580
+
581
+ // Add missing closing brackets/braces
582
+ while (closeBrackets < openBrackets) {
583
+ recoveredJson += ']';
584
+ closeBrackets++;
585
+ }
586
+ while (closeBraces < openBraces) {
587
+ recoveredJson += '}';
588
+ closeBraces++;
589
+ }
590
+
591
+ const recovered = JSON.parse(recoveredJson);
592
+ this.console.log(`[Discovery] Recovered truncated JSON for ${context}`);
593
+ return recovered;
594
+ } catch (recoveryError) {
595
+ // Last resort: try to extract just landmarks array
596
+ try {
597
+ const landmarksMatch = jsonStr.match(/"landmarks"\s*:\s*\[([\s\S]*?)(?:\]|$)/);
598
+ const zonesMatch = jsonStr.match(/"zones"\s*:\s*\[([\s\S]*?)(?:\]|$)/);
599
+
600
+ const result: any = { landmarks: [], zones: [], edges: {}, orientation: 'unknown' };
601
+
602
+ if (landmarksMatch) {
603
+ // Try to parse individual landmark objects
604
+ const landmarksStr = landmarksMatch[1];
605
+ const landmarkObjects = landmarksStr.match(/\{[^{}]*\}/g) || [];
606
+ result.landmarks = landmarkObjects.map((obj: string) => {
607
+ try {
608
+ return JSON.parse(obj);
609
+ } catch {
610
+ return null;
611
+ }
612
+ }).filter(Boolean);
613
+ this.console.log(`[Discovery] Extracted ${result.landmarks.length} landmarks from partial response for ${context}`);
614
+ }
615
+
616
+ if (zonesMatch) {
617
+ const zonesStr = zonesMatch[1];
618
+ const zoneObjects = zonesStr.match(/\{[^{}]*\}/g) || [];
619
+ result.zones = zoneObjects.map((obj: string) => {
620
+ try {
621
+ return JSON.parse(obj);
622
+ } catch {
623
+ return null;
624
+ }
625
+ }).filter(Boolean);
626
+ this.console.log(`[Discovery] Extracted ${result.zones.length} zones from partial response for ${context}`);
627
+ }
628
+
629
+ if (result.landmarks.length > 0 || result.zones.length > 0) {
630
+ return result;
631
+ }
632
+ } catch (extractError) {
633
+ // Give up
634
+ }
635
+
636
+ this.console.warn(`[Discovery] Could not recover JSON for ${context}`);
637
+ throw new Error(`Failed to parse LLM response: truncated or malformed JSON`);
638
+ }
639
+ }
640
+
547
641
  /** Resolve a camera reference (name or deviceId) to its deviceId */
548
642
  private resolveCameraRef(ref: string): string | null {
549
643
  if (!this.topology?.cameras || !ref) return null;