@switchlabs/verify-ai-react-native 1.1.0 → 1.1.1

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.
Files changed (48) hide show
  1. package/README.md +11 -2
  2. package/lib/client/index.d.ts +22 -2
  3. package/lib/client/index.js +132 -19
  4. package/lib/components/VerifyAIScanner.d.ts +12 -6
  5. package/lib/components/VerifyAIScanner.js +157 -15
  6. package/lib/hooks/useVerifyAI.d.ts +32 -10
  7. package/lib/hooks/useVerifyAI.js +246 -14
  8. package/lib/index.d.ts +5 -2
  9. package/lib/index.js +3 -0
  10. package/lib/ml/featureExtractor.d.ts +16 -0
  11. package/lib/ml/featureExtractor.js +123 -0
  12. package/lib/ml/imagePreprocessor.d.ts +2 -0
  13. package/lib/ml/imagePreprocessor.js +48 -0
  14. package/lib/ml/index.d.ts +5 -0
  15. package/lib/ml/index.js +4 -0
  16. package/lib/ml/inferenceEngine.d.ts +24 -0
  17. package/lib/ml/inferenceEngine.js +156 -0
  18. package/lib/ml/modelManager.d.ts +26 -0
  19. package/lib/ml/modelManager.js +207 -0
  20. package/lib/ml/policyEngine.d.ts +14 -0
  21. package/lib/ml/policyEngine.js +161 -0
  22. package/lib/ml/types.d.ts +84 -0
  23. package/lib/ml/types.js +4 -0
  24. package/lib/storage/offlineQueue.js +1 -1
  25. package/lib/telemetry/TelemetryContext.d.ts +4 -0
  26. package/lib/telemetry/TelemetryContext.js +5 -0
  27. package/lib/telemetry/TelemetryReporter.d.ts +23 -0
  28. package/lib/telemetry/TelemetryReporter.js +140 -0
  29. package/lib/types/index.d.ts +18 -0
  30. package/lib/version.d.ts +1 -0
  31. package/lib/version.js +1 -0
  32. package/package.json +23 -2
  33. package/src/client/index.ts +176 -25
  34. package/src/components/VerifyAIScanner.tsx +198 -18
  35. package/src/hooks/useVerifyAI.ts +332 -18
  36. package/src/index.ts +20 -1
  37. package/src/ml/featureExtractor.ts +160 -0
  38. package/src/ml/imagePreprocessor.ts +72 -0
  39. package/src/ml/index.ts +14 -0
  40. package/src/ml/inferenceEngine.ts +200 -0
  41. package/src/ml/modelManager.ts +265 -0
  42. package/src/ml/policyEngine.ts +201 -0
  43. package/src/ml/types.ts +104 -0
  44. package/src/storage/offlineQueue.ts +1 -1
  45. package/src/telemetry/TelemetryContext.tsx +8 -0
  46. package/src/telemetry/TelemetryReporter.ts +181 -0
  47. package/src/types/index.ts +20 -0
  48. package/src/version.ts +1 -0
@@ -1,13 +1,17 @@
1
- import { useMemo, useCallback, useState, useEffect } from 'react';
2
- import { AppState, type AppStateStatus } from 'react-native';
1
+ import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react';
2
+ import { AppState, Platform, type AppStateStatus } from 'react-native';
3
3
  import { VerifyAIClient, VerifyAIRequestError } from '../client';
4
4
  import { OfflineQueue } from '../storage/offlineQueue';
5
+ import { TelemetryContext } from '../telemetry/TelemetryContext';
6
+ import type { TelemetryReporter } from '../telemetry/TelemetryReporter';
5
7
  import type {
6
8
  VerifyAIConfig,
7
9
  VerificationRequest,
10
+ MultipartVerificationRequest,
8
11
  VerificationResult,
9
12
  VerificationListParams,
10
13
  VerificationListResponse,
14
+ VerifyOptions,
11
15
  } from '../types';
12
16
 
13
17
  function isQueueableError(error: Error): boolean {
@@ -28,9 +32,30 @@ function isQueueableError(error: Error): boolean {
28
32
  );
29
33
  }
30
34
 
35
+ function isOfflineError(error: Error): boolean {
36
+ const message = error.message.toLowerCase();
37
+ return (
38
+ message.includes('network') ||
39
+ message.includes('failed to fetch') ||
40
+ message.includes('no internet')
41
+ );
42
+ }
43
+
44
+ export interface UseVerifyAIConfig extends VerifyAIConfig {
45
+ /** Enable on-device ML inference for faster, offline-capable verification. */
46
+ enableOnDeviceML?: boolean;
47
+ /** Minimum confidence threshold for on-device results. Below this, falls back to cloud. Default: 0.7 */
48
+ onDeviceConfidenceThreshold?: number;
49
+ }
50
+
31
51
  export interface UseVerifyAIReturn {
32
- /** Submit a verification. Uses offline queue if offlineMode is enabled and request fails. */
33
- verify: (request: VerificationRequest) => Promise<VerificationResult | null>;
52
+ /** Submit a verification. Uses on-device ML if enabled and model is cached. */
53
+ verify: (request: VerificationRequest, options?: VerifyOptions) => Promise<VerificationResult | null>;
54
+ /** Submit a multipart verification. Uses on-device ML if enabled and model is cached. */
55
+ verifyMultipart: (
56
+ request: MultipartVerificationRequest,
57
+ options?: VerifyOptions,
58
+ ) => Promise<VerificationResult | null>;
34
59
  /** List past verifications. */
35
60
  listVerifications: (params?: VerificationListParams) => Promise<VerificationListResponse>;
36
61
  /** Get a single verification by ID. */
@@ -49,41 +74,102 @@ export interface UseVerifyAIReturn {
49
74
  client: VerifyAIClient;
50
75
  /** The offline queue instance (null if offlineMode is disabled). */
51
76
  offlineQueue: OfflineQueue | null;
77
+ /** Whether an on-device model is loaded and ready. */
78
+ mlModelReady: boolean;
79
+ /** Initialize or update the on-device ML model for a policy. */
80
+ initializeMLModel: (policyId: string) => Promise<void>;
81
+ /** Telemetry reporter instance (null if telemetry is disabled). */
82
+ telemetry: TelemetryReporter | null;
83
+ /** TelemetryProvider component — wrap your scanner tree with this. */
84
+ TelemetryProvider: React.FC<{ children: React.ReactNode }>;
52
85
  }
53
86
 
54
87
  /**
55
- * React hook for Verify AI. Provides verification methods,
56
- * loading/error state, and optional offline queue management.
88
+ * React hook for Verify AI with optional on-device ML inference.
57
89
  *
58
90
  * @example
59
91
  * ```tsx
60
- * const { verify, loading, lastResult, error } = useVerifyAI({
92
+ * const { verify, loading, lastResult, mlModelReady, initializeMLModel } = useVerifyAI({
61
93
  * apiKey: 'vai_your_api_key',
62
94
  * offlineMode: true,
95
+ * enableOnDeviceML: true,
63
96
  * });
64
97
  *
98
+ * // Initialize ML model on mount
99
+ * useEffect(() => {
100
+ * initializeMLModel('scooter_parking');
101
+ * }, []);
102
+ *
65
103
  * const handleCapture = async (base64Image: string) => {
66
104
  * const result = await verify({
67
105
  * image: base64Image,
68
106
  * policy: 'scooter_parking',
69
107
  * });
70
- * if (result?.is_compliant) {
71
- * // Success
72
- * }
73
108
  * };
74
109
  * ```
75
110
  */
76
- export function useVerifyAI(config: VerifyAIConfig): UseVerifyAIReturn {
77
- const client = useMemo(() => new VerifyAIClient(config), [config.apiKey, config.baseUrl, config.timeout]);
111
+ export function useVerifyAI(config: UseVerifyAIConfig): UseVerifyAIReturn {
112
+ const {
113
+ enableOnDeviceML = false,
114
+ onDeviceConfidenceThreshold = 0.7,
115
+ ...baseConfig
116
+ } = config;
117
+
118
+ const client = useMemo(
119
+ () => new VerifyAIClient({
120
+ apiKey: baseConfig.apiKey,
121
+ baseUrl: baseConfig.baseUrl,
122
+ timeout: baseConfig.timeout,
123
+ offlineMode: baseConfig.offlineMode,
124
+ telemetry: baseConfig.telemetry,
125
+ }),
126
+ [baseConfig.apiKey, baseConfig.baseUrl, baseConfig.timeout, baseConfig.offlineMode, baseConfig.telemetry],
127
+ );
78
128
  const offlineQueue = useMemo(
79
- () => (config.offlineMode ? new OfflineQueue(client) : null),
80
- [client, config.offlineMode]
129
+ () => (baseConfig.offlineMode ? new OfflineQueue(client) : null),
130
+ [client, baseConfig.offlineMode]
81
131
  );
82
132
 
133
+ // ML components — lazily loaded to avoid requiring ML deps when not enabled
134
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
135
+ const modelManagerRef = useRef<any>(null);
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ const inferenceEngineRef = useRef<any>(null);
138
+
83
139
  const [loading, setLoading] = useState(false);
84
140
  const [error, setError] = useState<Error | null>(null);
85
141
  const [lastResult, setLastResult] = useState<VerificationResult | null>(null);
86
142
  const [queueSize, setQueueSize] = useState(0);
143
+ const [mlModelReady, setMlModelReady] = useState(false);
144
+
145
+ // Initialize ML components lazily via dynamic import
146
+ useEffect(() => {
147
+ if (!enableOnDeviceML || !baseConfig.apiKey) return;
148
+
149
+ let disposed = false;
150
+
151
+ (async () => {
152
+ try {
153
+ const [{ ModelManager }, { InferenceEngine }] = await Promise.all([
154
+ import('../ml/modelManager'),
155
+ import('../ml/inferenceEngine'),
156
+ ]);
157
+ if (disposed) return;
158
+ modelManagerRef.current = new ModelManager({
159
+ apiKey: baseConfig.apiKey,
160
+ baseUrl: baseConfig.baseUrl,
161
+ });
162
+ inferenceEngineRef.current = new InferenceEngine();
163
+ } catch (err) {
164
+ console.warn('[VerifyAI] Failed to load ML modules:', err);
165
+ }
166
+ })();
167
+
168
+ return () => {
169
+ disposed = true;
170
+ inferenceEngineRef.current?.dispose();
171
+ };
172
+ }, [enableOnDeviceML, baseConfig.apiKey, baseConfig.baseUrl]);
87
173
 
88
174
  // Refresh queue size
89
175
  const refreshQueueSize = useCallback(async () => {
@@ -109,20 +195,176 @@ export function useVerifyAI(config: VerifyAIConfig): UseVerifyAIReturn {
109
195
  return () => subscription.remove();
110
196
  }, [offlineQueue, refreshQueueSize]);
111
197
 
198
+ /** Initialize or update the on-device ML model for a policy. */
199
+ const initializeMLModel = useCallback(async (policyId: string) => {
200
+ if (!enableOnDeviceML || !modelManagerRef.current || !inferenceEngineRef.current) return;
201
+
202
+ try {
203
+ // Load cached bundle first
204
+ const bundle = await modelManagerRef.current.loadCachedBundle(policyId);
205
+
206
+ if (bundle) {
207
+ await inferenceEngineRef.current.loadModel(bundle);
208
+ setMlModelReady(true);
209
+ }
210
+
211
+ // Check for updates in background
212
+ const platform = Platform.OS === 'ios' ? 'ios' : 'android';
213
+ const updatedBundle = await modelManagerRef.current.checkForUpdates(policyId, platform);
214
+
215
+ if (updatedBundle && updatedBundle.bundleVersion !== bundle?.bundleVersion) {
216
+ await inferenceEngineRef.current.loadModel(updatedBundle);
217
+ setMlModelReady(true);
218
+ }
219
+ } catch (err) {
220
+ setMlModelReady(false);
221
+ client.telemetry?.track('ml_model_load_failure', {
222
+ component: 'useVerifyAI',
223
+ error: err,
224
+ });
225
+ }
226
+ }, [enableOnDeviceML, client.telemetry]);
227
+
228
+ /** Try on-device verification. Returns null if inference fails or low confidence. */
229
+ const tryOnDeviceVerification = useCallback(
230
+ async (
231
+ params: {
232
+ policy: string;
233
+ metadata?: Record<string, unknown>;
234
+ preprocessedInput: Float32Array;
235
+ },
236
+ ignoreThreshold = false,
237
+ ): Promise<VerificationResult | null> => {
238
+ try {
239
+ const modelManager = modelManagerRef.current;
240
+ const inferenceEngine = inferenceEngineRef.current;
241
+ const bundle = modelManager?.currentBundle;
242
+
243
+ if (!inferenceEngine?.isLoaded || !bundle) return null;
244
+
245
+ // Lazily import ML modules
246
+ const [{ extractFeatures }, { evaluatePolicy }] = await Promise.all([
247
+ import('../ml/featureExtractor'),
248
+ import('../ml/policyEngine'),
249
+ ]);
250
+
251
+ // Run inference
252
+ const rawDetections = inferenceEngine.runInference(params.preprocessedInput);
253
+
254
+ // Extract features
255
+ const features = extractFeatures(rawDetections);
256
+
257
+ // Evaluate policy
258
+ const policyResult = evaluatePolicy(bundle.policyAst, features);
259
+
260
+ // Check confidence threshold
261
+ if (!ignoreThreshold && policyResult.confidence < onDeviceConfidenceThreshold) {
262
+ return null;
263
+ }
264
+
265
+ return {
266
+ id: `local_${Date.now()}`,
267
+ created_at: new Date().toISOString(),
268
+ status: 'success',
269
+ is_compliant: policyResult.is_compliant,
270
+ confidence: policyResult.confidence,
271
+ policy: params.policy,
272
+ violation_reasons: policyResult.violation_reasons,
273
+ feedback: policyResult.feedback,
274
+ metadata: {
275
+ ...params.metadata,
276
+ evaluation_source: 'on_device',
277
+ bundle_version: bundle.bundleVersion,
278
+ category: policyResult.category_id,
279
+ },
280
+ image_url: null,
281
+ };
282
+ } catch (err) {
283
+ client.telemetry?.track('ml_inference_failure', {
284
+ component: 'useVerifyAI',
285
+ error: err,
286
+ });
287
+ return null;
288
+ }
289
+ },
290
+ [onDeviceConfidenceThreshold, client.telemetry],
291
+ );
292
+
293
+ const tryOnDeviceBase64Verification = useCallback(
294
+ async (
295
+ request: VerificationRequest,
296
+ ignoreThreshold = false,
297
+ ): Promise<VerificationResult | null> => {
298
+ try {
299
+ const { preprocessImageBase64 } = await import('../ml/imagePreprocessor');
300
+ const preprocessedInput = preprocessImageBase64(request.image);
301
+ return tryOnDeviceVerification({
302
+ policy: request.policy,
303
+ metadata: request.metadata,
304
+ preprocessedInput,
305
+ }, ignoreThreshold);
306
+ } catch {
307
+ return null;
308
+ }
309
+ },
310
+ [tryOnDeviceVerification],
311
+ );
312
+
313
+ const tryOnDeviceMultipartVerification = useCallback(
314
+ async (
315
+ request: MultipartVerificationRequest,
316
+ ignoreThreshold = false,
317
+ ): Promise<VerificationResult | null> => {
318
+ try {
319
+ const { preprocessImageUri } = await import('../ml/imagePreprocessor');
320
+ const preprocessedInput = await preprocessImageUri(request.imageUri);
321
+ return tryOnDeviceVerification({
322
+ policy: request.policy,
323
+ metadata: request.metadata,
324
+ preprocessedInput,
325
+ }, ignoreThreshold);
326
+ } catch {
327
+ return null;
328
+ }
329
+ },
330
+ [tryOnDeviceVerification],
331
+ );
332
+
112
333
  const verify = useCallback(
113
- async (request: VerificationRequest): Promise<VerificationResult | null> => {
334
+ async (request: VerificationRequest, options?: VerifyOptions): Promise<VerificationResult | null> => {
114
335
  setLoading(true);
115
336
  setError(null);
116
337
 
117
338
  try {
118
- const result = await client.verify(request);
339
+ // Try on-device inference first
340
+ if (enableOnDeviceML && mlModelReady) {
341
+ const localResult = await tryOnDeviceBase64Verification(request);
342
+ if (localResult) {
343
+ setLastResult(localResult);
344
+ setLoading(false);
345
+ return localResult;
346
+ }
347
+ // Fall through to cloud
348
+ }
349
+
350
+ const result = await client.verify(request, options);
119
351
  setLastResult(result);
120
352
  return result;
121
353
  } catch (err) {
122
354
  const error = err instanceof Error ? err : new Error(String(err));
123
355
  setError(error);
124
356
 
125
- // Queue only transient failures so invalid requests are surfaced immediately.
357
+ // If offline and we have a model, try on-device regardless of confidence
358
+ if (enableOnDeviceML && mlModelReady && isOfflineError(error)) {
359
+ const localResult = await tryOnDeviceBase64Verification(request, true);
360
+ if (localResult) {
361
+ setLastResult(localResult);
362
+ setLoading(false);
363
+ return localResult;
364
+ }
365
+ }
366
+
367
+ // Queue only transient failures
126
368
  if (offlineQueue && isQueueableError(error)) {
127
369
  await offlineQueue.enqueue(request);
128
370
  await refreshQueueSize();
@@ -134,7 +376,47 @@ export function useVerifyAI(config: VerifyAIConfig): UseVerifyAIReturn {
134
376
  setLoading(false);
135
377
  }
136
378
  },
137
- [client, offlineQueue, refreshQueueSize]
379
+ [client, offlineQueue, refreshQueueSize, enableOnDeviceML, mlModelReady, tryOnDeviceBase64Verification]
380
+ );
381
+
382
+ const verifyMultipart = useCallback(
383
+ async (
384
+ request: MultipartVerificationRequest,
385
+ options?: VerifyOptions,
386
+ ): Promise<VerificationResult | null> => {
387
+ setLoading(true);
388
+ setError(null);
389
+
390
+ try {
391
+ if (enableOnDeviceML && mlModelReady) {
392
+ const localResult = await tryOnDeviceMultipartVerification(request);
393
+ if (localResult) {
394
+ setLastResult(localResult);
395
+ return localResult;
396
+ }
397
+ }
398
+
399
+ const result = await client.verifyMultipart(request, options);
400
+ setLastResult(result);
401
+ return result;
402
+ } catch (err) {
403
+ const error = err instanceof Error ? err : new Error(String(err));
404
+ setError(error);
405
+
406
+ if (enableOnDeviceML && mlModelReady && isOfflineError(error)) {
407
+ const localResult = await tryOnDeviceMultipartVerification(request, true);
408
+ if (localResult) {
409
+ setLastResult(localResult);
410
+ return localResult;
411
+ }
412
+ }
413
+
414
+ throw error;
415
+ } finally {
416
+ setLoading(false);
417
+ }
418
+ },
419
+ [client, enableOnDeviceML, mlModelReady, tryOnDeviceMultipartVerification],
138
420
  );
139
421
 
140
422
  const listVerifications = useCallback(
@@ -153,8 +435,36 @@ export function useVerifyAI(config: VerifyAIConfig): UseVerifyAIReturn {
153
435
  await refreshQueueSize();
154
436
  }, [offlineQueue, refreshQueueSize]);
155
437
 
438
+ // Flush telemetry on app background, dispose on unmount
439
+ useEffect(() => {
440
+ const reporter = client.telemetry;
441
+ if (!reporter) return;
442
+
443
+ const handleAppState = (state: AppStateStatus) => {
444
+ if (state === 'background' || state === 'inactive') {
445
+ reporter.flush();
446
+ }
447
+ };
448
+
449
+ const subscription = AppState.addEventListener('change', handleAppState);
450
+ return () => {
451
+ subscription.remove();
452
+ reporter.dispose();
453
+ };
454
+ }, [client.telemetry]);
455
+
456
+ // Memoized TelemetryProvider component
457
+ const TelemetryProvider = useMemo(() => {
458
+ const reporter = client.telemetry;
459
+ const Provider: React.FC<{ children: React.ReactNode }> = ({ children }) =>
460
+ React.createElement(TelemetryContext.Provider, { value: reporter }, children);
461
+ Provider.displayName = 'TelemetryProvider';
462
+ return Provider;
463
+ }, [client.telemetry]);
464
+
156
465
  return {
157
466
  verify,
467
+ verifyMultipart,
158
468
  listVerifications,
159
469
  getVerification,
160
470
  loading,
@@ -164,5 +474,9 @@ export function useVerifyAI(config: VerifyAIConfig): UseVerifyAIReturn {
164
474
  processQueue,
165
475
  client,
166
476
  offlineQueue,
477
+ mlModelReady,
478
+ initializeMLModel,
479
+ telemetry: client.telemetry,
480
+ TelemetryProvider,
167
481
  };
168
482
  }
package/src/index.ts CHANGED
@@ -3,19 +3,38 @@ export { VerifyAIClient, VerifyAIRequestError } from './client';
3
3
 
4
4
  // Hooks
5
5
  export { useVerifyAI } from './hooks/useVerifyAI';
6
- export type { UseVerifyAIReturn } from './hooks/useVerifyAI';
6
+ export type { UseVerifyAIReturn, UseVerifyAIConfig } from './hooks/useVerifyAI';
7
7
 
8
8
  // Components
9
9
  export { VerifyAIScanner } from './components/VerifyAIScanner';
10
10
  export type { VerifyAIScannerProps } from './components/VerifyAIScanner';
11
11
 
12
+ // Telemetry
13
+ export { TelemetryReporter } from './telemetry/TelemetryReporter';
14
+ export { TelemetryContext } from './telemetry/TelemetryContext';
15
+
12
16
  // Offline Queue
13
17
  export { OfflineQueue } from './storage/offlineQueue';
14
18
 
19
+ // ML (on-device inference) — types are safe to re-export statically.
20
+ // Value exports use a lazy namespace so consuming apps don't need ML deps
21
+ // (buffer, jpeg-js, @noble/hashes, react-native-fast-tflite) unless they
22
+ // explicitly opt in via `enableOnDeviceML: true`.
23
+ export type {
24
+ BundleManifest,
25
+ ModelArtifact,
26
+ FeatureVector,
27
+ Detection as MLDetection,
28
+ PolicyAST,
29
+ PolicyResult,
30
+ RuleResult,
31
+ } from './ml/types';
32
+
15
33
  // Types
16
34
  export type {
17
35
  VerifyAIConfig,
18
36
  VerificationRequest,
37
+ MultipartVerificationRequest,
19
38
  VerificationResult,
20
39
  VerificationListResponse,
21
40
  VerificationListParams,
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Feature extractor — converts raw detections into a structured FeatureVector.
3
+ * Must produce identical output to the Dart and server TypeScript implementations.
4
+ */
5
+
6
+ import type { FeatureVector, Detection, ImageQuality } from './types';
7
+
8
+ const SCHEMA_VERSION = '1.0.0';
9
+
10
+ /** Vehicle class IDs (must match ontology.ts) */
11
+ const VEHICLE_CLASS_IDS = new Set([1, 2, 3, 4, 5]);
12
+ /** Surface class IDs */
13
+ const SURFACE_CLASS_IDS = new Set([60, 61, 62, 63, 64, 65, 66]);
14
+
15
+ /** Ontology class name mapping (must match ontology.ts) */
16
+ export const ONTOLOGY_CLASS_NAMES: Record<number, string> = {
17
+ 1: 'scooter', 2: 'bicycle', 3: 'ebike', 4: 'moped', 5: 'motorcycle',
18
+ 20: 'kickstand', 21: 'handlebar', 22: 'wheel', 23: 'saddle', 24: 'basket',
19
+ 40: 'bike_rack', 41: 'curb', 42: 'parking_bay', 43: 'green_bay',
20
+ 44: 'tactile_paving', 45: 'bollard', 46: 'fence', 47: 'pole', 48: 'yellow_lines',
21
+ 60: 'sidewalk', 61: 'road', 62: 'grass', 63: 'crosswalk',
22
+ 64: 'dirt', 65: 'gravel', 66: 'parking_lot',
23
+ 80: 'person', 81: 'entrance', 82: 'obstruction', 83: 'bench',
24
+ 84: 'garbage_bin', 85: 'fire_escape', 86: 'ramp', 87: 'driveway',
25
+ };
26
+
27
+ /** Detection model class index -> ontology class ID mapping. */
28
+ export const MODEL_OUTPUT_CLASS_IDS = [
29
+ 1, 2, 3, 4, 5,
30
+ 20, 21, 22, 23, 24,
31
+ 40, 41, 42, 43, 44, 45, 46, 47, 48,
32
+ 60, 61, 62, 63, 64, 65, 66,
33
+ 80, 81, 82, 83, 84, 85, 86, 87,
34
+ ] as const;
35
+
36
+ interface RawDetection {
37
+ classId: number;
38
+ confidence: number;
39
+ bbox: [number, number, number, number];
40
+ }
41
+
42
+ export function extractFeatures(rawDetections: RawDetection[]): FeatureVector {
43
+ // Convert raw detections to typed detections
44
+ const detections: Detection[] = rawDetections.map((raw) => ({
45
+ class_id: raw.classId,
46
+ class_name: ONTOLOGY_CLASS_NAMES[raw.classId] || `unknown_${raw.classId}`,
47
+ confidence: raw.confidence,
48
+ bbox: raw.bbox,
49
+ }));
50
+
51
+ // Find primary vehicle (highest confidence)
52
+ let primaryVehicle: Detection | null = null;
53
+ for (const det of detections) {
54
+ if (VEHICLE_CLASS_IDS.has(det.class_id)) {
55
+ if (!primaryVehicle || det.confidence > primaryVehicle.confidence) {
56
+ primaryVehicle = det;
57
+ }
58
+ }
59
+ }
60
+
61
+ // Determine surface under vehicle
62
+ let vehicleOnSurface: string | null = null;
63
+ if (primaryVehicle) {
64
+ vehicleOnSurface = findSurfaceUnderVehicle(primaryVehicle, detections);
65
+ }
66
+
67
+ // Find objects near the vehicle
68
+ const vehicleNear: string[] = [];
69
+ if (primaryVehicle) {
70
+ vehicleNear.push(...findObjectsNearVehicle(primaryVehicle, detections));
71
+ }
72
+
73
+ // Assess image quality
74
+ const imageQuality: ImageQuality = {
75
+ is_blurry: false,
76
+ is_dark: false,
77
+ has_vehicle: primaryVehicle !== null,
78
+ };
79
+
80
+ return {
81
+ schema_version: SCHEMA_VERSION,
82
+ detections,
83
+ primary_vehicle: primaryVehicle,
84
+ image_quality: imageQuality,
85
+ vehicle_on_surface: vehicleOnSurface,
86
+ vehicle_near: vehicleNear,
87
+ };
88
+ }
89
+
90
+ function findSurfaceUnderVehicle(vehicle: Detection, allDetections: Detection[]): string | null {
91
+ const surfaces = allDetections.filter((d) => SURFACE_CLASS_IDS.has(d.class_id));
92
+
93
+ let bestSurface: string | null = null;
94
+ let bestOverlap = 0;
95
+
96
+ for (const surface of surfaces) {
97
+ // Check overlap with vehicle bottom half
98
+ const vehicleBottom: [number, number, number, number] = [
99
+ vehicle.bbox[0],
100
+ (vehicle.bbox[1] + vehicle.bbox[3]) / 2,
101
+ vehicle.bbox[2],
102
+ vehicle.bbox[3],
103
+ ];
104
+ const overlap = computeIoU(vehicleBottom, surface.bbox);
105
+
106
+ if (overlap > bestOverlap) {
107
+ bestOverlap = overlap;
108
+ bestSurface = surface.class_name;
109
+ }
110
+ }
111
+
112
+ return bestOverlap > 0.1 ? bestSurface : null;
113
+ }
114
+
115
+ function findObjectsNearVehicle(vehicle: Detection, allDetections: Detection[]): string[] {
116
+ const nearObjects = new Set<string>();
117
+
118
+ for (const det of allDetections) {
119
+ if (VEHICLE_CLASS_IDS.has(det.class_id)) continue;
120
+ if (SURFACE_CLASS_IDS.has(det.class_id)) continue;
121
+
122
+ if (isNearby(vehicle.bbox, det.bbox, 0.2)) {
123
+ nearObjects.add(det.class_name);
124
+ }
125
+ }
126
+
127
+ return Array.from(nearObjects);
128
+ }
129
+
130
+ function isNearby(
131
+ bboxA: [number, number, number, number],
132
+ bboxB: [number, number, number, number],
133
+ threshold: number,
134
+ ): boolean {
135
+ const centerAx = (bboxA[0] + bboxA[2]) / 2;
136
+ const centerAy = (bboxA[1] + bboxA[3]) / 2;
137
+ const centerBx = (bboxB[0] + bboxB[2]) / 2;
138
+ const centerBy = (bboxB[1] + bboxB[3]) / 2;
139
+
140
+ return Math.abs(centerAx - centerBx) < threshold || Math.abs(centerAy - centerBy) < threshold;
141
+ }
142
+
143
+ function computeIoU(
144
+ a: [number, number, number, number],
145
+ b: [number, number, number, number],
146
+ ): number {
147
+ const x1 = Math.max(a[0], b[0]);
148
+ const y1 = Math.max(a[1], b[1]);
149
+ const x2 = Math.min(a[2], b[2]);
150
+ const y2 = Math.min(a[3], b[3]);
151
+
152
+ if (x2 <= x1 || y2 <= y1) return 0;
153
+
154
+ const intersection = (x2 - x1) * (y2 - y1);
155
+ const areaA = (a[2] - a[0]) * (a[3] - a[1]);
156
+ const areaB = (b[2] - b[0]) * (b[3] - b[1]);
157
+ const union = areaA + areaB - intersection;
158
+
159
+ return union > 0 ? intersection / union : 0;
160
+ }