Browse Source

update backend for bvatch processing

Dr-Swopt 3 hours ago
parent
commit
8c4cb6596a

+ 55 - 0
CLAUDE.md

@@ -0,0 +1,55 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What This Is
+
+NestJS backend for the PalmOilAI system. Provides server-side YOLOv8 ONNX inference, SQLite history persistence, WebSocket gateways for real-time vision streaming and chat proxying, and a process surveillance monitor for n8n and Ollama.
+
+Detection classes: `Ripe`, `Unripe`, `Underripe`, `Overripe`, `Abnormal`, `Empty_Bunch` (MPOB standard).
+
+## Commands
+
+```bash
+npm install
+npm run start:dev               # Watch mode dev server (port 3000)
+npm run start                   # Single-run dev server
+npm run start:prod              # Production (from dist/)
+npm run build                   # Compile → dist/
+npm run lint                    # ESLint with auto-fix
+npm run test                    # Jest unit tests
+npm run test:watch              # Jest watch mode
+npm run test:cov                # Coverage report
+npm run test:e2e                # End-to-end tests
+jest --testPathPattern=palm-oil # Run a single test file
+```
+
+Set `PORT` env var to override the default port 3000. Set `N8N_WEBHOOK_URL` in `.env` for chat proxy and agent readiness probing.
+
+## Architecture
+
+### Modules
+
+**`PalmOilModule`** (`src/palm-oil/`) — Core feature module:
+- `ScannerProvider` — ONNX inference pipeline: `sharp` resizes to 640×640, strips alpha, transposes HWC→CHW, normalizes to `[0.0, 1.0]`, feeds `[1, 3, 640, 640]` tensor to `onnxruntime-node`. Output is `[1, N, 6]` (`x1, y1, x2, y2, confidence, class_index`), filtered at 0.25 confidence by default.
+- `PalmOilService` — Orchestrates inference + SQLite persistence. `analyzeImage()` is the main entry point; it fully awaits the `historyRepository.save()` before returning (intentional blocking — guarantees DB write before socket emit).
+- `VisionGateway` — WebSocket gateway on `/vision` namespace. Handles `vision:analyze` (Base64 image → ONNX inference → `vision:result`) and `chat:send` (proxies to n8n webhook → `chat:result`). Receives raw, uncompressed Base64 strings only — no binary frames, no WebRTC.
+- `HistoryEntity` — TypeORM entity; fields: `archive_id`, `filename`, `total_count`, `industrial_summary` (JSON), `detections` (JSON), `inference_ms`, `processing_ms`, `created_at`.
+- `mpob-standards.ts` — Source of truth for class names, grade colors, and health alert flag list (`Abnormal`, `Empty_Bunch`).
+
+**`SurveillanceModule`** (`src/surveillance/`) — Process monitoring module:
+- `SurveillanceService` — Boots on `OnModuleInit`. Discovers PIDs for n8n (Node.js process containing "n8n" in cmd, port 5678 must be accepting TCP connections) and Ollama (`ollama_llama_server` or `ollama`). Polls every 500ms via `pidusage`. Probes `N8N_WEBHOOK_URL` every 10s to determine agent readiness. PIDs are evicted if the process dies or n8n's port goes silent.
+- `SurveillanceGateway` — WebSocket gateway on `/monitor` namespace. Broadcasts `monitor:data` (CPU/memory metrics) on every 500ms tick and `monitor:status` (n8n webhook ready/not ready) when status changes. Pushes an immediate snapshot on client connect.
+
+### Database
+SQLite file `palm_history.db` in the project root. Managed by TypeORM with `synchronize: true` (auto-creates tables — dev only). No external DB setup required.
+
+### Required File
+`best.onnx` must be placed in the **project root directory** (not `src/`). This is the YOLOv8 ONNX model loaded by `ScannerProvider` at inference time.
+
+### WebSocket Event Contracts
+| Namespace | Client → Server | Server → Client |
+|---|---|---|
+| `/vision` | `vision:analyze` `{ frame: string, sourceLabel?: string }` | `vision:result`, `vision:error` |
+| `/vision` | `chat:send` `{ message: string }` | `chat:result`, `chat:error` |
+| `/monitor` | `monitor:subscribe` | `monitor:data` (MonitorPayload[]), `monitor:status` |

+ 1 - 0
src/palm-oil/interfaces/palm-analysis.interface.ts

@@ -28,4 +28,5 @@ export interface AnalysisResponse {
   inference_ms: number;
   processing_ms: number;
   archive_id: string;
+  raw_tensor_sample: number[][];
 }

+ 3 - 2
src/palm-oil/palm-oil.service.ts

@@ -42,7 +42,7 @@ export class PalmOilService {
     const inferenceMs = performance.now() - inferenceStart;
 
     // 4. Post-process
-    const detections = await this.scanner.postprocess(
+    const { detections, raw_tensor_sample } = await this.scanner.postprocess(
       outputTensor,
       originalWidth,
       originalHeight,
@@ -94,10 +94,11 @@ export class PalmOilService {
       total_count: detections.length,
       industrial_summary: industrialSummary,
       detections: history.detections,
-      image_data: `data:image/jpeg;base64,${imageBuffer.toString('base64')}`, // Pass the image back
+      image_data: `data:image/jpeg;base64,${imageBuffer.toString('base64')}`,
       inference_ms: history.inference_ms,
       processing_ms: history.processing_ms,
       archive_id: archiveId,
+      raw_tensor_sample,
     };
   }
 

+ 28 - 9
src/palm-oil/providers/scanner.provider.ts

@@ -5,6 +5,11 @@ import * as path from 'path';
 import { MPOB_CLASSES, HEALTH_ALERT_CLASSES } from '../constants/mpob-standards';
 import { DetectionResult } from '../interfaces/palm-analysis.interface';
 
+export interface ScanResult {
+  detections: DetectionResult[];
+  raw_tensor_sample: number[][];
+}
+
 @Injectable()
 export class ScannerProvider implements OnModuleInit {
   private session!: onnx.InferenceSession;
@@ -70,11 +75,26 @@ export class ScannerProvider implements OnModuleInit {
     originalWidth: number,
     originalHeight: number,
     threshold: number = 0.25,
-  ): Promise<DetectionResult[]> {
+  ): Promise<ScanResult> {
     const data = outputTensor.data as Float32Array;
     // Expected shape: [1, 300, 6]
     // Each candidate: [x1, y1, x2, y2, confidence, class_index]
-    
+
+    // Capture first 5 raw rows before NMS filtering — the AI's unfiltered "thought process"
+    const sampleRows = Math.min(5, outputTensor.dims[1]);
+    const raw_tensor_sample: number[][] = [];
+    for (let i = 0; i < sampleRows; i++) {
+      const offset = i * 6;
+      raw_tensor_sample.push([
+        parseFloat(data[offset].toFixed(6)),
+        parseFloat(data[offset + 1].toFixed(6)),
+        parseFloat(data[offset + 2].toFixed(6)),
+        parseFloat(data[offset + 3].toFixed(6)),
+        parseFloat(data[offset + 4].toFixed(6)),
+        parseFloat(data[offset + 5].toFixed(6)),
+      ]);
+    }
+
     const results: DetectionResult[] = [];
     const numCandidates = outputTensor.dims[1];
 
@@ -94,17 +114,16 @@ export class ScannerProvider implements OnModuleInit {
           class: className,
           confidence: parseFloat(confidence.toFixed(4)),
           is_health_alert: HEALTH_ALERT_CLASSES.includes(className),
-          // HEAVY LIFTING: Multiply ratio (0.0-1.0) by original pixels
           box: [
-            data[offset] * originalWidth,      // x1
-            data[offset + 1] * originalHeight, // y1
-            data[offset + 2] * originalWidth,  // x2
-            data[offset + 3] * originalHeight  // y2
-          ], 
+            data[offset] * originalWidth,
+            data[offset + 1] * originalHeight,
+            data[offset + 2] * originalWidth,
+            data[offset + 3] * originalHeight,
+          ],
         });
       }
     }
 
-    return results;
+    return { detections: results, raw_tensor_sample };
   }
 }

+ 13 - 1
src/palm-oil/vision.gateway.ts

@@ -131,7 +131,19 @@ export class VisionGateway
     }
 
     // ── STEP 4 — emit AFTER the SQLite write is confirmed ────────────────────
-    client.emit('vision:result', result);
+    // Attach technical_evidence block so the frontend audit manifest has
+    // engine metadata, archive pointer, and raw tensor snapshot per frame.
+    client.emit('vision:result', {
+      ...result,
+      technical_evidence: {
+        engine: 'NestJS-ONNX' as const,
+        archive_id: result.archive_id,
+        total_count: result.total_count,
+        threshold: result.current_threshold,
+        industrial_summary: result.industrial_summary,
+        raw_tensor_sample: result.raw_tensor_sample,
+      },
+    });
   }
 
   // ─── chat:send handler ──────────────────────────────────────────────────────