|
@@ -41,6 +41,10 @@ import { LocalHistoryService } from '../../services/local-history.service';
|
|
|
import { InferenceService, LocalEngine } from '../../core/services/inference.service';
|
|
import { InferenceService, LocalEngine } from '../../core/services/inference.service';
|
|
|
import { VisionSocketService } from '../../services/vision-socket.service';
|
|
import { VisionSocketService } from '../../services/vision-socket.service';
|
|
|
import { SurveillanceService } from '../../services/surveillance.service';
|
|
import { SurveillanceService } from '../../services/surveillance.service';
|
|
|
|
|
+import {
|
|
|
|
|
+ BatchResult,
|
|
|
|
|
+ FullSessionReport,
|
|
|
|
|
+} from '../../core/interfaces/palm-analysis.interface';
|
|
|
|
|
|
|
|
export type SnapEngine = 'tflite' | 'onnx' | 'socket';
|
|
export type SnapEngine = 'tflite' | 'onnx' | 'socket';
|
|
|
|
|
|
|
@@ -84,10 +88,17 @@ export class AnalyzerComponent implements OnInit, OnDestroy {
|
|
|
batchQueue = signal<File[]>([]);
|
|
batchQueue = signal<File[]>([]);
|
|
|
currentBatchIndex = signal<number>(0);
|
|
currentBatchIndex = signal<number>(0);
|
|
|
isBatchActive = computed(() => this.batchQueue().length > 0);
|
|
isBatchActive = computed(() => this.batchQueue().length > 0);
|
|
|
- /** Accumulates per-image results for 4.2/4.3 audit trail */
|
|
|
|
|
- sessionManifest: any[] = [];
|
|
|
|
|
|
|
|
|
|
- get totalBatchCount(): number { return this.batchQueue().length + this.currentBatchIndex(); }
|
|
|
|
|
|
|
+ sessionManifest: BatchResult[] = [];
|
|
|
|
|
+ completedReport = signal<FullSessionReport | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+ /** Wall-clock timestamp of when sendBase64 was called for the current image */
|
|
|
|
|
+ private _pingTime = 0;
|
|
|
|
|
+ /** Wall-clock timestamp of when startBatchProcessing was called */
|
|
|
|
|
+ private _batchStartTime = 0;
|
|
|
|
|
+ /** Total files in the current batch run — stored separately so it survives queue drain */
|
|
|
|
|
+ private _totalBatchCount = 0;
|
|
|
|
|
+ get totalBatchCount(): number { return this._totalBatchCount; }
|
|
|
|
|
|
|
|
// ── Socket-engine state ────────────────────────────────────────────────────
|
|
// ── Socket-engine state ────────────────────────────────────────────────────
|
|
|
/** 'webcam' = live camera snap | 'gallery' = file from device storage */
|
|
/** 'webcam' = live camera snap | 'gallery' = file from device storage */
|
|
@@ -120,6 +131,94 @@ export class AnalyzerComponent implements OnInit, OnDestroy {
|
|
|
this.abortBatch();
|
|
this.abortBatch();
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
|
|
+ // "Pong" — fires on every lastResult change. Guards on non-null result AND
|
|
|
|
|
+ // active batch so it never interferes with single-image gallery/snap flows.
|
|
|
|
|
+ effect(() => {
|
|
|
|
|
+ const res = this.visionSocket.lastResult();
|
|
|
|
|
+ if (!res || !this.isBatchActive()) return;
|
|
|
|
|
+
|
|
|
|
|
+ const idx = this.currentBatchIndex();
|
|
|
|
|
+ const queue = this.batchQueue();
|
|
|
|
|
+ const roundTripMs = performance.now() - this._pingTime;
|
|
|
|
|
+
|
|
|
|
|
+ const record: BatchResult = {
|
|
|
|
|
+ image_id: queue[idx]?.name ?? `file_${idx}`,
|
|
|
|
|
+ timestamp: new Date().toISOString(),
|
|
|
|
|
+ status: 'ok',
|
|
|
|
|
+ detections: res.detections.map(d => ({
|
|
|
|
|
+ bunch_id: d.bunch_id,
|
|
|
|
|
+ ripeness_class: d.class,
|
|
|
|
|
+ confidence_pct: Math.round(d.confidence * 10000) / 100,
|
|
|
|
|
+ is_health_alert: d.is_health_alert,
|
|
|
|
|
+ bounding_box: { x1: d.box[0], y1: d.box[1], x2: d.box[2], y2: d.box[3] },
|
|
|
|
|
+ norm_box: d.norm_box
|
|
|
|
|
+ ? { x1: d.norm_box[0], y1: d.norm_box[1], x2: d.norm_box[2], y2: d.norm_box[3] }
|
|
|
|
|
+ : null,
|
|
|
|
|
+ })),
|
|
|
|
|
+ performance: {
|
|
|
|
|
+ inference_ms: res.inference_ms,
|
|
|
|
|
+ processing_ms: res.processing_ms,
|
|
|
|
|
+ round_trip_ms: Math.round(roundTripMs),
|
|
|
|
|
+ },
|
|
|
|
|
+ technical_evidence: {
|
|
|
|
|
+ engine: 'NestJS-ONNX',
|
|
|
|
|
+ archive_id: res.archive_id,
|
|
|
|
|
+ total_count: res.total_count,
|
|
|
|
|
+ threshold: res.current_threshold,
|
|
|
|
|
+ industrial_summary: res.industrial_summary,
|
|
|
|
|
+ raw_tensor_sample: res.technical_evidence?.raw_tensor_sample ?? res.raw_tensor_sample ?? [],
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+ this.sessionManifest.push(record);
|
|
|
|
|
+
|
|
|
|
|
+ const next = idx + 1;
|
|
|
|
|
+ if (next < queue.length) {
|
|
|
|
|
+ this.currentBatchIndex.set(next);
|
|
|
|
|
+ this.processNextInBatch();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.finalizeBatch();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // "Error pong" — if a socket error fires mid-batch, log it and advance
|
|
|
|
|
+ effect(() => {
|
|
|
|
|
+ const err = this.visionSocket.lastError();
|
|
|
|
|
+ if (!err || !this.isBatchActive()) return;
|
|
|
|
|
+
|
|
|
|
|
+ const idx = this.currentBatchIndex();
|
|
|
|
|
+ const queue = this.batchQueue();
|
|
|
|
|
+
|
|
|
|
|
+ const record: BatchResult = {
|
|
|
|
|
+ image_id: queue[idx]?.name ?? `file_${idx}`,
|
|
|
|
|
+ timestamp: new Date().toISOString(),
|
|
|
|
|
+ status: 'error',
|
|
|
|
|
+ detections: [],
|
|
|
|
|
+ performance: {
|
|
|
|
|
+ inference_ms: 0,
|
|
|
|
|
+ processing_ms: 0,
|
|
|
|
|
+ round_trip_ms: Math.round(performance.now() - this._pingTime),
|
|
|
|
|
+ },
|
|
|
|
|
+ technical_evidence: {
|
|
|
|
|
+ engine: 'NestJS-ONNX',
|
|
|
|
|
+ archive_id: '',
|
|
|
|
|
+ total_count: 0,
|
|
|
|
|
+ threshold: 0,
|
|
|
|
|
+ industrial_summary: {},
|
|
|
|
|
+ raw_tensor_sample: [],
|
|
|
|
|
+ },
|
|
|
|
|
+ error: err,
|
|
|
|
|
+ };
|
|
|
|
|
+ this.sessionManifest.push(record);
|
|
|
|
|
+
|
|
|
|
|
+ const next = idx + 1;
|
|
|
|
|
+ if (next < queue.length) {
|
|
|
|
|
+ this.currentBatchIndex.set(next);
|
|
|
|
|
+ this.processNextInBatch();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.finalizeBatch();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
ngOnInit(): void {}
|
|
ngOnInit(): void {}
|
|
@@ -165,10 +264,97 @@ export class AnalyzerComponent implements OnInit, OnDestroy {
|
|
|
this.handleFile(file);
|
|
this.handleFile(file);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Placeholder — actual network loop is Task 4.2. Do NOT send files here. */
|
|
|
|
|
private startBatchProcessing(): void {
|
|
private startBatchProcessing(): void {
|
|
|
- // Queue is loaded; progress HUD is now visible via isBatchActive().
|
|
|
|
|
- // Task 4.2 will drive the sequential sendBase64() loop from here.
|
|
|
|
|
|
|
+ this._totalBatchCount = this.batchQueue().length;
|
|
|
|
|
+ this._batchStartTime = performance.now();
|
|
|
|
|
+ this.sessionManifest = [];
|
|
|
|
|
+ this.completedReport.set(null);
|
|
|
|
|
+ this.currentBatchIndex.set(0);
|
|
|
|
|
+ this.processNextInBatch();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private processNextInBatch(): void {
|
|
|
|
|
+ const file = this.batchQueue()[this.currentBatchIndex()];
|
|
|
|
|
+ if (!file) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Render file → 640×640 offscreen canvas → JPEG Base64 (Lego 11 format)
|
|
|
|
|
+ const reader = new FileReader();
|
|
|
|
|
+ reader.onload = (e) => {
|
|
|
|
|
+ const src = e.target?.result as string;
|
|
|
|
|
+ if (!src) { this.advanceBatchOnError(file.name, 'FileReader returned empty result'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ const img = new Image();
|
|
|
|
|
+ img.onerror = () => this.advanceBatchOnError(file.name, 'Image failed to decode');
|
|
|
|
|
+ img.onload = () => {
|
|
|
|
|
+ const offscreen = document.createElement('canvas');
|
|
|
|
|
+ offscreen.width = 640;
|
|
|
|
|
+ offscreen.height = 640;
|
|
|
|
|
+ offscreen.getContext('2d')!.drawImage(img, 0, 0, 640, 640);
|
|
|
|
|
+ const base64 = offscreen.toDataURL('image/jpeg');
|
|
|
|
|
+ this._pingTime = performance.now();
|
|
|
|
|
+ this.visionSocket.sendBase64(base64);
|
|
|
|
|
+ };
|
|
|
|
|
+ img.src = src;
|
|
|
|
|
+ };
|
|
|
|
|
+ reader.onerror = () => this.advanceBatchOnError(file.name, 'FileReader error');
|
|
|
|
|
+ reader.readAsDataURL(file);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Log a client-side read/decode error and advance to the next file */
|
|
|
|
|
+ private advanceBatchOnError(fileName: string, reason: string): void {
|
|
|
|
|
+ const record: BatchResult = {
|
|
|
|
|
+ image_id: fileName,
|
|
|
|
|
+ timestamp: new Date().toISOString(),
|
|
|
|
|
+ status: 'error',
|
|
|
|
|
+ detections: [],
|
|
|
|
|
+ performance: { inference_ms: 0, processing_ms: 0, round_trip_ms: 0 },
|
|
|
|
|
+ technical_evidence: {
|
|
|
|
|
+ engine: 'NestJS-ONNX',
|
|
|
|
|
+ archive_id: '',
|
|
|
|
|
+ total_count: 0,
|
|
|
|
|
+ threshold: 0,
|
|
|
|
|
+ industrial_summary: {},
|
|
|
|
|
+ raw_tensor_sample: [],
|
|
|
|
|
+ },
|
|
|
|
|
+ error: reason,
|
|
|
|
|
+ };
|
|
|
|
|
+ this.sessionManifest.push(record);
|
|
|
|
|
+ const next = this.currentBatchIndex() + 1;
|
|
|
|
|
+ if (next < this.batchQueue().length) {
|
|
|
|
|
+ this.currentBatchIndex.set(next);
|
|
|
|
|
+ this.processNextInBatch();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.finalizeBatch();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private finalizeBatch(): void {
|
|
|
|
|
+ const totalMs = Math.round(performance.now() - this._batchStartTime);
|
|
|
|
|
+ const ok = this.sessionManifest.filter(r => r.status === 'ok');
|
|
|
|
|
+ const avgInference = ok.length
|
|
|
|
|
+ ? Math.round(ok.reduce((s, r) => s + r.performance.inference_ms, 0) / ok.length)
|
|
|
|
|
+ : 0;
|
|
|
|
|
+ const avgRoundTrip = ok.length
|
|
|
|
|
+ ? Math.round(ok.reduce((s, r) => s + r.performance.round_trip_ms, 0) / ok.length)
|
|
|
|
|
+ : 0;
|
|
|
|
|
+
|
|
|
|
|
+ const report: FullSessionReport = {
|
|
|
|
|
+ session_id: crypto.randomUUID(),
|
|
|
|
|
+ generated_at: new Date().toISOString(),
|
|
|
|
|
+ meta: {
|
|
|
|
|
+ total_images: this._totalBatchCount,
|
|
|
|
|
+ successful: ok.length,
|
|
|
|
|
+ failed: this.sessionManifest.length - ok.length,
|
|
|
|
|
+ total_time_ms: totalMs,
|
|
|
|
|
+ avg_inference_ms: avgInference,
|
|
|
|
|
+ avg_round_trip_ms: avgRoundTrip,
|
|
|
|
|
+ },
|
|
|
|
|
+ results: this.sessionManifest,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ this.completedReport.set(report);
|
|
|
|
|
+ this.batchQueue.set([]);
|
|
|
|
|
+ this.currentBatchIndex.set(0);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
abortBatch(): void {
|
|
abortBatch(): void {
|