remote-inference.service.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import { HttpClient } from '@angular/common/http';
  2. import { Injectable, OnDestroy } from '@angular/core';
  3. import { map, Observable, Subject, take } from 'rxjs';
  4. import { DpService } from 'dp-ui/dp.service';
  5. import { FisAppMessage, MessageHeader, AppMessageType } from 'dp-ui/fisappmessage/apprequestmessagetype';
  6. import { InferenceFrame, DetectionResult, MPOB_CLASSES, HEALTH_ALERT_CLASSES } from './inference.service';
  7. interface PalmVisionConfig {
  8. connection: {
  9. uacp: string;
  10. uacp_ws: string;
  11. uacpEmulation: string;
  12. };
  13. }
  14. export interface EdgeResultPayload {
  15. frame: string;
  16. filename?: string;
  17. batchId?: string;
  18. detections: DetectionResult[];
  19. industrial_summary: Record<string, number>;
  20. inference_ms: number;
  21. processing_ms?: number;
  22. }
  23. @Injectable({ providedIn: 'root' })
  24. export class RemoteInferenceService implements OnDestroy {
  25. private config: PalmVisionConfig | null = null;
  26. private destroyed$ = new Subject<void>();
  27. constructor(
  28. private http: HttpClient,
  29. private dpService: DpService,
  30. ) {
  31. this.http.get<PalmVisionConfig>('./config/config.json')
  32. .pipe(take(1))
  33. .subscribe({ next: cfg => (this.config = cfg) });
  34. }
  35. analyze(file: File, sourceLabel?: string, batchId?: string): Observable<InferenceFrame> {
  36. return new Observable<InferenceFrame>(observer => {
  37. const reader = new FileReader();
  38. reader.onload = () => {
  39. const frame = reader.result as string;
  40. this.send<any>('PalmVision', 'analyze', { frame, sourceLabel, batchId })
  41. .subscribe({
  42. next: raw => observer.next(this.mapAnalysisResponse(raw, batchId)),
  43. error: err => observer.error(err),
  44. complete: () => observer.complete(),
  45. });
  46. };
  47. reader.onerror = () => observer.error(new Error('FileReader failed to read image'));
  48. reader.readAsDataURL(file);
  49. });
  50. }
  51. getHistory(): Observable<any[]> {
  52. return this.send<any>('History', 'getAll', undefined).pipe(
  53. map(body => {
  54. console.log('[Vault Transport Debug] Raw History Network Payload:', body);
  55. if (Array.isArray(body)) {
  56. return body;
  57. }
  58. if (body && typeof body === 'object') {
  59. const extractedRecords = body.records || body.data || body.items || body.history;
  60. if (Array.isArray(extractedRecords)) {
  61. return extractedRecords;
  62. }
  63. const values = Object.values(body);
  64. if (values.length > 0 && values.every(v => v !== null && typeof v === 'object')) {
  65. return values as any[];
  66. }
  67. }
  68. return [];
  69. })
  70. );
  71. }
  72. deleteRecord(archiveId: string): Observable<{ deleted: boolean }> {
  73. return this.send('History', 'delete', { archiveId });
  74. }
  75. clearHistory(): Observable<{ deleted: number }> {
  76. return this.send('History', 'clearAll', undefined);
  77. }
  78. getImage(archiveId: string): Observable<{ archiveId: string; image_data: string }> {
  79. return this.send('PalmHistory', 'GetImage', { archiveId });
  80. }
  81. saveExternalResult(payload: EdgeResultPayload): Observable<any> {
  82. const adjustedPayload: any = {
  83. ...payload,
  84. frame: (payload as any).frame || (payload as any).imageDataUrl || '',
  85. };
  86. if (adjustedPayload.imageDataUrl) {
  87. delete adjustedPayload.imageDataUrl;
  88. }
  89. return this.send('PalmHistory', 'SaveExternalResult', adjustedPayload);
  90. }
  91. ngOnDestroy(): void {
  92. this.destroyed$.next();
  93. this.destroyed$.complete();
  94. }
  95. /**
  96. * Builds a compliant FIS envelope and dispatches it via the framework's official DpService stream.
  97. * Leverages core multiplexed transport pipelines rather than direct Socket.io interfaces.
  98. */
  99. private send<T>(serviceId: string, operation: string, payload: unknown): Observable<T> {
  100. const messageID = crypto.randomUUID();
  101. // Package parameters inside a fully compliant enterprise envelope structure
  102. const message: FisAppMessage = {
  103. header: {
  104. messageID,
  105. serviceId,
  106. messageName: operation,
  107. messageType: AppMessageType.Command
  108. } as unknown as MessageHeader,
  109. data: payload,
  110. };
  111. return new Observable<T>(observer => {
  112. // Direct call routing through the shared enterprise stream engine
  113. this.dpService.stream(message).subscribe({
  114. next: (res: any) => {
  115. // Gracefully intercept and isolate system finalization frames before parsing
  116. if (res && res.complete === true) {
  117. observer.complete();
  118. return;
  119. }
  120. // Extract body mapping parameters directly from enterprise results packets
  121. const body = typeof res === 'string' ? JSON.parse(res) : (res?.message ? JSON.parse(res.message) : res);
  122. if (body?.error) {
  123. observer.error(new Error(body.error));
  124. } else {
  125. observer.next(body as T);
  126. }
  127. },
  128. error: err => observer.error(err),
  129. complete: () => observer.complete(),
  130. });
  131. });
  132. }
  133. private mapAnalysisResponse(raw: any, batchId?: string): InferenceFrame {
  134. const detections: DetectionResult[] = (raw?.detections ?? []).map((d: any) => ({
  135. bunch_id: d.bunch_id,
  136. class: d.class,
  137. confidence: d.confidence,
  138. is_health_alert: HEALTH_ALERT_CLASSES.includes(d.class),
  139. box: d.box,
  140. norm_box: d.norm_box,
  141. }));
  142. const industrial_summary: Record<string, number> = raw?.industrial_summary
  143. ?? raw?.technical_evidence?.industrial_summary
  144. ?? {};
  145. return {
  146. frameId: raw?.archive_id ?? crypto.randomUUID(),
  147. batchId,
  148. imageDataUrl: raw?.image_data ?? raw?.imageDataUrl ?? '',
  149. detections,
  150. inference_ms: raw?.inference_ms ?? 0,
  151. processing_ms: raw?.processing_ms ?? 0,
  152. total_count: detections.length,
  153. industrial_summary,
  154. source: 'remote',
  155. };
  156. }
  157. }