@switchlabs/verify-ai-react-native 1.0.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 +14 -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 +201 -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,7 +1,8 @@
1
- import { useMemo, useCallback, useState, useEffect } from 'react';
2
- import { AppState } from 'react-native';
1
+ import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react';
2
+ import { AppState, Platform } from 'react-native';
3
3
  import { VerifyAIClient, VerifyAIRequestError } from '../client';
4
4
  import { OfflineQueue } from '../storage/offlineQueue';
5
+ import { TelemetryContext } from '../telemetry/TelemetryContext';
5
6
  function isQueueableError(error) {
6
7
  if (error instanceof VerifyAIRequestError) {
7
8
  return error.isRetryable;
@@ -15,35 +16,84 @@ function isQueueableError(error) {
15
16
  message.includes('timed out') ||
16
17
  message.includes('failed to fetch'));
17
18
  }
19
+ function isOfflineError(error) {
20
+ const message = error.message.toLowerCase();
21
+ return (message.includes('network') ||
22
+ message.includes('failed to fetch') ||
23
+ message.includes('no internet'));
24
+ }
18
25
  /**
19
- * React hook for Verify AI. Provides verification methods,
20
- * loading/error state, and optional offline queue management.
26
+ * React hook for Verify AI with optional on-device ML inference.
21
27
  *
22
28
  * @example
23
29
  * ```tsx
24
- * const { verify, loading, lastResult, error } = useVerifyAI({
30
+ * const { verify, loading, lastResult, mlModelReady, initializeMLModel } = useVerifyAI({
25
31
  * apiKey: 'vai_your_api_key',
26
32
  * offlineMode: true,
33
+ * enableOnDeviceML: true,
27
34
  * });
28
35
  *
36
+ * // Initialize ML model on mount
37
+ * useEffect(() => {
38
+ * initializeMLModel('scooter_parking');
39
+ * }, []);
40
+ *
29
41
  * const handleCapture = async (base64Image: string) => {
30
42
  * const result = await verify({
31
43
  * image: base64Image,
32
44
  * policy: 'scooter_parking',
33
45
  * });
34
- * if (result?.is_compliant) {
35
- * // Success
36
- * }
37
46
  * };
38
47
  * ```
39
48
  */
40
49
  export function useVerifyAI(config) {
41
- const client = useMemo(() => new VerifyAIClient(config), [config.apiKey, config.baseUrl, config.timeout]);
42
- const offlineQueue = useMemo(() => (config.offlineMode ? new OfflineQueue(client) : null), [client, config.offlineMode]);
50
+ const { enableOnDeviceML = false, onDeviceConfidenceThreshold = 0.7, ...baseConfig } = config;
51
+ const client = useMemo(() => new VerifyAIClient({
52
+ apiKey: baseConfig.apiKey,
53
+ baseUrl: baseConfig.baseUrl,
54
+ timeout: baseConfig.timeout,
55
+ offlineMode: baseConfig.offlineMode,
56
+ telemetry: baseConfig.telemetry,
57
+ }), [baseConfig.apiKey, baseConfig.baseUrl, baseConfig.timeout, baseConfig.offlineMode, baseConfig.telemetry]);
58
+ const offlineQueue = useMemo(() => (baseConfig.offlineMode ? new OfflineQueue(client) : null), [client, baseConfig.offlineMode]);
59
+ // ML components — lazily loaded to avoid requiring ML deps when not enabled
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ const modelManagerRef = useRef(null);
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ const inferenceEngineRef = useRef(null);
43
64
  const [loading, setLoading] = useState(false);
44
65
  const [error, setError] = useState(null);
45
66
  const [lastResult, setLastResult] = useState(null);
46
67
  const [queueSize, setQueueSize] = useState(0);
68
+ const [mlModelReady, setMlModelReady] = useState(false);
69
+ // Initialize ML components lazily via dynamic import
70
+ useEffect(() => {
71
+ if (!enableOnDeviceML || !baseConfig.apiKey)
72
+ return;
73
+ let disposed = false;
74
+ (async () => {
75
+ try {
76
+ const [{ ModelManager }, { InferenceEngine }] = await Promise.all([
77
+ import('../ml/modelManager'),
78
+ import('../ml/inferenceEngine'),
79
+ ]);
80
+ if (disposed)
81
+ return;
82
+ modelManagerRef.current = new ModelManager({
83
+ apiKey: baseConfig.apiKey,
84
+ baseUrl: baseConfig.baseUrl,
85
+ });
86
+ inferenceEngineRef.current = new InferenceEngine();
87
+ }
88
+ catch (err) {
89
+ console.warn('[VerifyAI] Failed to load ML modules:', err);
90
+ }
91
+ })();
92
+ return () => {
93
+ disposed = true;
94
+ inferenceEngineRef.current?.dispose();
95
+ };
96
+ }, [enableOnDeviceML, baseConfig.apiKey, baseConfig.baseUrl]);
47
97
  // Refresh queue size
48
98
  const refreshQueueSize = useCallback(async () => {
49
99
  if (offlineQueue) {
@@ -64,18 +114,141 @@ export function useVerifyAI(config) {
64
114
  const subscription = AppState.addEventListener('change', handleAppState);
65
115
  return () => subscription.remove();
66
116
  }, [offlineQueue, refreshQueueSize]);
67
- const verify = useCallback(async (request) => {
117
+ /** Initialize or update the on-device ML model for a policy. */
118
+ const initializeMLModel = useCallback(async (policyId) => {
119
+ if (!enableOnDeviceML || !modelManagerRef.current || !inferenceEngineRef.current)
120
+ return;
121
+ try {
122
+ // Load cached bundle first
123
+ const bundle = await modelManagerRef.current.loadCachedBundle(policyId);
124
+ if (bundle) {
125
+ await inferenceEngineRef.current.loadModel(bundle);
126
+ setMlModelReady(true);
127
+ }
128
+ // Check for updates in background
129
+ const platform = Platform.OS === 'ios' ? 'ios' : 'android';
130
+ const updatedBundle = await modelManagerRef.current.checkForUpdates(policyId, platform);
131
+ if (updatedBundle && updatedBundle.bundleVersion !== bundle?.bundleVersion) {
132
+ await inferenceEngineRef.current.loadModel(updatedBundle);
133
+ setMlModelReady(true);
134
+ }
135
+ }
136
+ catch (err) {
137
+ setMlModelReady(false);
138
+ client.telemetry?.track('ml_model_load_failure', {
139
+ component: 'useVerifyAI',
140
+ error: err,
141
+ });
142
+ }
143
+ }, [enableOnDeviceML, client.telemetry]);
144
+ /** Try on-device verification. Returns null if inference fails or low confidence. */
145
+ const tryOnDeviceVerification = useCallback(async (params, ignoreThreshold = false) => {
146
+ try {
147
+ const modelManager = modelManagerRef.current;
148
+ const inferenceEngine = inferenceEngineRef.current;
149
+ const bundle = modelManager?.currentBundle;
150
+ if (!inferenceEngine?.isLoaded || !bundle)
151
+ return null;
152
+ // Lazily import ML modules
153
+ const [{ extractFeatures }, { evaluatePolicy }] = await Promise.all([
154
+ import('../ml/featureExtractor'),
155
+ import('../ml/policyEngine'),
156
+ ]);
157
+ // Run inference
158
+ const rawDetections = inferenceEngine.runInference(params.preprocessedInput);
159
+ // Extract features
160
+ const features = extractFeatures(rawDetections);
161
+ // Evaluate policy
162
+ const policyResult = evaluatePolicy(bundle.policyAst, features);
163
+ // Check confidence threshold
164
+ if (!ignoreThreshold && policyResult.confidence < onDeviceConfidenceThreshold) {
165
+ return null;
166
+ }
167
+ return {
168
+ id: `local_${Date.now()}`,
169
+ created_at: new Date().toISOString(),
170
+ status: 'success',
171
+ is_compliant: policyResult.is_compliant,
172
+ confidence: policyResult.confidence,
173
+ policy: params.policy,
174
+ violation_reasons: policyResult.violation_reasons,
175
+ feedback: policyResult.feedback,
176
+ metadata: {
177
+ ...params.metadata,
178
+ evaluation_source: 'on_device',
179
+ bundle_version: bundle.bundleVersion,
180
+ category: policyResult.category_id,
181
+ },
182
+ image_url: null,
183
+ };
184
+ }
185
+ catch (err) {
186
+ client.telemetry?.track('ml_inference_failure', {
187
+ component: 'useVerifyAI',
188
+ error: err,
189
+ });
190
+ return null;
191
+ }
192
+ }, [onDeviceConfidenceThreshold, client.telemetry]);
193
+ const tryOnDeviceBase64Verification = useCallback(async (request, ignoreThreshold = false) => {
194
+ try {
195
+ const { preprocessImageBase64 } = await import('../ml/imagePreprocessor');
196
+ const preprocessedInput = preprocessImageBase64(request.image);
197
+ return tryOnDeviceVerification({
198
+ policy: request.policy,
199
+ metadata: request.metadata,
200
+ preprocessedInput,
201
+ }, ignoreThreshold);
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ }, [tryOnDeviceVerification]);
207
+ const tryOnDeviceMultipartVerification = useCallback(async (request, ignoreThreshold = false) => {
208
+ try {
209
+ const { preprocessImageUri } = await import('../ml/imagePreprocessor');
210
+ const preprocessedInput = await preprocessImageUri(request.imageUri);
211
+ return tryOnDeviceVerification({
212
+ policy: request.policy,
213
+ metadata: request.metadata,
214
+ preprocessedInput,
215
+ }, ignoreThreshold);
216
+ }
217
+ catch {
218
+ return null;
219
+ }
220
+ }, [tryOnDeviceVerification]);
221
+ const verify = useCallback(async (request, options) => {
68
222
  setLoading(true);
69
223
  setError(null);
70
224
  try {
71
- const result = await client.verify(request);
225
+ // Try on-device inference first
226
+ if (enableOnDeviceML && mlModelReady) {
227
+ const localResult = await tryOnDeviceBase64Verification(request);
228
+ if (localResult) {
229
+ setLastResult(localResult);
230
+ setLoading(false);
231
+ return localResult;
232
+ }
233
+ // Fall through to cloud
234
+ }
235
+ const result = await client.verify(request, options);
72
236
  setLastResult(result);
73
237
  return result;
74
238
  }
75
239
  catch (err) {
76
240
  const error = err instanceof Error ? err : new Error(String(err));
77
241
  setError(error);
78
- // Queue only transient failures so invalid requests are surfaced immediately.
242
+ // If offline and we have a model, try on-device regardless of confidence
243
+ if (enableOnDeviceML && mlModelReady && isOfflineError(error)) {
244
+ const localResult = await tryOnDeviceBase64Verification(request, true);
245
+ if (localResult) {
246
+ setLastResult(localResult);
247
+ setLoading(false);
248
+ return localResult;
249
+ }
250
+ }
251
+ // Queue only transient failures
79
252
  if (offlineQueue && isQueueableError(error)) {
80
253
  await offlineQueue.enqueue(request);
81
254
  await refreshQueueSize();
@@ -86,7 +259,38 @@ export function useVerifyAI(config) {
86
259
  finally {
87
260
  setLoading(false);
88
261
  }
89
- }, [client, offlineQueue, refreshQueueSize]);
262
+ }, [client, offlineQueue, refreshQueueSize, enableOnDeviceML, mlModelReady, tryOnDeviceBase64Verification]);
263
+ const verifyMultipart = useCallback(async (request, options) => {
264
+ setLoading(true);
265
+ setError(null);
266
+ try {
267
+ if (enableOnDeviceML && mlModelReady) {
268
+ const localResult = await tryOnDeviceMultipartVerification(request);
269
+ if (localResult) {
270
+ setLastResult(localResult);
271
+ return localResult;
272
+ }
273
+ }
274
+ const result = await client.verifyMultipart(request, options);
275
+ setLastResult(result);
276
+ return result;
277
+ }
278
+ catch (err) {
279
+ const error = err instanceof Error ? err : new Error(String(err));
280
+ setError(error);
281
+ if (enableOnDeviceML && mlModelReady && isOfflineError(error)) {
282
+ const localResult = await tryOnDeviceMultipartVerification(request, true);
283
+ if (localResult) {
284
+ setLastResult(localResult);
285
+ return localResult;
286
+ }
287
+ }
288
+ throw error;
289
+ }
290
+ finally {
291
+ setLoading(false);
292
+ }
293
+ }, [client, enableOnDeviceML, mlModelReady, tryOnDeviceMultipartVerification]);
90
294
  const listVerifications = useCallback((params) => client.listVerifications(params), [client]);
91
295
  const getVerification = useCallback((id) => client.getVerification(id), [client]);
92
296
  const processQueue = useCallback(async () => {
@@ -95,8 +299,32 @@ export function useVerifyAI(config) {
95
299
  await offlineQueue.processQueue((_, result) => setLastResult(result));
96
300
  await refreshQueueSize();
97
301
  }, [offlineQueue, refreshQueueSize]);
302
+ // Flush telemetry on app background, dispose on unmount
303
+ useEffect(() => {
304
+ const reporter = client.telemetry;
305
+ if (!reporter)
306
+ return;
307
+ const handleAppState = (state) => {
308
+ if (state === 'background' || state === 'inactive') {
309
+ reporter.flush();
310
+ }
311
+ };
312
+ const subscription = AppState.addEventListener('change', handleAppState);
313
+ return () => {
314
+ subscription.remove();
315
+ reporter.dispose();
316
+ };
317
+ }, [client.telemetry]);
318
+ // Memoized TelemetryProvider component
319
+ const TelemetryProvider = useMemo(() => {
320
+ const reporter = client.telemetry;
321
+ const Provider = ({ children }) => React.createElement(TelemetryContext.Provider, { value: reporter }, children);
322
+ Provider.displayName = 'TelemetryProvider';
323
+ return Provider;
324
+ }, [client.telemetry]);
98
325
  return {
99
326
  verify,
327
+ verifyMultipart,
100
328
  listVerifications,
101
329
  getVerification,
102
330
  loading,
@@ -106,5 +334,9 @@ export function useVerifyAI(config) {
106
334
  processQueue,
107
335
  client,
108
336
  offlineQueue,
337
+ mlModelReady,
338
+ initializeMLModel,
339
+ telemetry: client.telemetry,
340
+ TelemetryProvider,
109
341
  };
110
342
  }
package/lib/index.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  export { VerifyAIClient, VerifyAIRequestError } from './client';
2
2
  export { useVerifyAI } from './hooks/useVerifyAI';
3
- export type { UseVerifyAIReturn } from './hooks/useVerifyAI';
3
+ export type { UseVerifyAIReturn, UseVerifyAIConfig } from './hooks/useVerifyAI';
4
4
  export { VerifyAIScanner } from './components/VerifyAIScanner';
5
5
  export type { VerifyAIScannerProps } from './components/VerifyAIScanner';
6
+ export { TelemetryReporter } from './telemetry/TelemetryReporter';
7
+ export { TelemetryContext } from './telemetry/TelemetryContext';
6
8
  export { OfflineQueue } from './storage/offlineQueue';
7
- export type { VerifyAIConfig, VerificationRequest, VerificationResult, VerificationListResponse, VerificationListParams, QueueItem, VerifyAIError, ScannerStatus, ScannerOverlayConfig, PolicyConfigResponse, } from './types';
9
+ export type { BundleManifest, ModelArtifact, FeatureVector, Detection as MLDetection, PolicyAST, PolicyResult, RuleResult, } from './ml/types';
10
+ export type { VerifyAIConfig, VerificationRequest, MultipartVerificationRequest, VerificationResult, VerificationListResponse, VerificationListParams, QueueItem, VerifyAIError, ScannerStatus, ScannerOverlayConfig, PolicyConfigResponse, } from './types';
package/lib/index.js CHANGED
@@ -4,5 +4,8 @@ export { VerifyAIClient, VerifyAIRequestError } from './client';
4
4
  export { useVerifyAI } from './hooks/useVerifyAI';
5
5
  // Components
6
6
  export { VerifyAIScanner } from './components/VerifyAIScanner';
7
+ // Telemetry
8
+ export { TelemetryReporter } from './telemetry/TelemetryReporter';
9
+ export { TelemetryContext } from './telemetry/TelemetryContext';
7
10
  // Offline Queue
8
11
  export { OfflineQueue } from './storage/offlineQueue';
@@ -0,0 +1,16 @@
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
+ import type { FeatureVector } from './types';
6
+ /** Ontology class name mapping (must match ontology.ts) */
7
+ export declare const ONTOLOGY_CLASS_NAMES: Record<number, string>;
8
+ /** Detection model class index -> ontology class ID mapping. */
9
+ export declare const MODEL_OUTPUT_CLASS_IDS: readonly [1, 2, 3, 4, 5, 20, 21, 22, 23, 24, 40, 41, 42, 43, 44, 45, 46, 47, 48, 60, 61, 62, 63, 64, 65, 66, 80, 81, 82, 83, 84, 85, 86, 87];
10
+ interface RawDetection {
11
+ classId: number;
12
+ confidence: number;
13
+ bbox: [number, number, number, number];
14
+ }
15
+ export declare function extractFeatures(rawDetections: RawDetection[]): FeatureVector;
16
+ export {};
@@ -0,0 +1,123 @@
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
+ const SCHEMA_VERSION = '1.0.0';
6
+ /** Vehicle class IDs (must match ontology.ts) */
7
+ const VEHICLE_CLASS_IDS = new Set([1, 2, 3, 4, 5]);
8
+ /** Surface class IDs */
9
+ const SURFACE_CLASS_IDS = new Set([60, 61, 62, 63, 64, 65, 66]);
10
+ /** Ontology class name mapping (must match ontology.ts) */
11
+ export const ONTOLOGY_CLASS_NAMES = {
12
+ 1: 'scooter', 2: 'bicycle', 3: 'ebike', 4: 'moped', 5: 'motorcycle',
13
+ 20: 'kickstand', 21: 'handlebar', 22: 'wheel', 23: 'saddle', 24: 'basket',
14
+ 40: 'bike_rack', 41: 'curb', 42: 'parking_bay', 43: 'green_bay',
15
+ 44: 'tactile_paving', 45: 'bollard', 46: 'fence', 47: 'pole', 48: 'yellow_lines',
16
+ 60: 'sidewalk', 61: 'road', 62: 'grass', 63: 'crosswalk',
17
+ 64: 'dirt', 65: 'gravel', 66: 'parking_lot',
18
+ 80: 'person', 81: 'entrance', 82: 'obstruction', 83: 'bench',
19
+ 84: 'garbage_bin', 85: 'fire_escape', 86: 'ramp', 87: 'driveway',
20
+ };
21
+ /** Detection model class index -> ontology class ID mapping. */
22
+ export const MODEL_OUTPUT_CLASS_IDS = [
23
+ 1, 2, 3, 4, 5,
24
+ 20, 21, 22, 23, 24,
25
+ 40, 41, 42, 43, 44, 45, 46, 47, 48,
26
+ 60, 61, 62, 63, 64, 65, 66,
27
+ 80, 81, 82, 83, 84, 85, 86, 87,
28
+ ];
29
+ export function extractFeatures(rawDetections) {
30
+ // Convert raw detections to typed detections
31
+ const detections = rawDetections.map((raw) => ({
32
+ class_id: raw.classId,
33
+ class_name: ONTOLOGY_CLASS_NAMES[raw.classId] || `unknown_${raw.classId}`,
34
+ confidence: raw.confidence,
35
+ bbox: raw.bbox,
36
+ }));
37
+ // Find primary vehicle (highest confidence)
38
+ let primaryVehicle = null;
39
+ for (const det of detections) {
40
+ if (VEHICLE_CLASS_IDS.has(det.class_id)) {
41
+ if (!primaryVehicle || det.confidence > primaryVehicle.confidence) {
42
+ primaryVehicle = det;
43
+ }
44
+ }
45
+ }
46
+ // Determine surface under vehicle
47
+ let vehicleOnSurface = null;
48
+ if (primaryVehicle) {
49
+ vehicleOnSurface = findSurfaceUnderVehicle(primaryVehicle, detections);
50
+ }
51
+ // Find objects near the vehicle
52
+ const vehicleNear = [];
53
+ if (primaryVehicle) {
54
+ vehicleNear.push(...findObjectsNearVehicle(primaryVehicle, detections));
55
+ }
56
+ // Assess image quality
57
+ const imageQuality = {
58
+ is_blurry: false,
59
+ is_dark: false,
60
+ has_vehicle: primaryVehicle !== null,
61
+ };
62
+ return {
63
+ schema_version: SCHEMA_VERSION,
64
+ detections,
65
+ primary_vehicle: primaryVehicle,
66
+ image_quality: imageQuality,
67
+ vehicle_on_surface: vehicleOnSurface,
68
+ vehicle_near: vehicleNear,
69
+ };
70
+ }
71
+ function findSurfaceUnderVehicle(vehicle, allDetections) {
72
+ const surfaces = allDetections.filter((d) => SURFACE_CLASS_IDS.has(d.class_id));
73
+ let bestSurface = null;
74
+ let bestOverlap = 0;
75
+ for (const surface of surfaces) {
76
+ // Check overlap with vehicle bottom half
77
+ const vehicleBottom = [
78
+ vehicle.bbox[0],
79
+ (vehicle.bbox[1] + vehicle.bbox[3]) / 2,
80
+ vehicle.bbox[2],
81
+ vehicle.bbox[3],
82
+ ];
83
+ const overlap = computeIoU(vehicleBottom, surface.bbox);
84
+ if (overlap > bestOverlap) {
85
+ bestOverlap = overlap;
86
+ bestSurface = surface.class_name;
87
+ }
88
+ }
89
+ return bestOverlap > 0.1 ? bestSurface : null;
90
+ }
91
+ function findObjectsNearVehicle(vehicle, allDetections) {
92
+ const nearObjects = new Set();
93
+ for (const det of allDetections) {
94
+ if (VEHICLE_CLASS_IDS.has(det.class_id))
95
+ continue;
96
+ if (SURFACE_CLASS_IDS.has(det.class_id))
97
+ continue;
98
+ if (isNearby(vehicle.bbox, det.bbox, 0.2)) {
99
+ nearObjects.add(det.class_name);
100
+ }
101
+ }
102
+ return Array.from(nearObjects);
103
+ }
104
+ function isNearby(bboxA, bboxB, threshold) {
105
+ const centerAx = (bboxA[0] + bboxA[2]) / 2;
106
+ const centerAy = (bboxA[1] + bboxA[3]) / 2;
107
+ const centerBx = (bboxB[0] + bboxB[2]) / 2;
108
+ const centerBy = (bboxB[1] + bboxB[3]) / 2;
109
+ return Math.abs(centerAx - centerBx) < threshold || Math.abs(centerAy - centerBy) < threshold;
110
+ }
111
+ function computeIoU(a, b) {
112
+ const x1 = Math.max(a[0], b[0]);
113
+ const y1 = Math.max(a[1], b[1]);
114
+ const x2 = Math.min(a[2], b[2]);
115
+ const y2 = Math.min(a[3], b[3]);
116
+ if (x2 <= x1 || y2 <= y1)
117
+ return 0;
118
+ const intersection = (x2 - x1) * (y2 - y1);
119
+ const areaA = (a[2] - a[0]) * (a[3] - a[1]);
120
+ const areaB = (b[2] - b[0]) * (b[3] - b[1]);
121
+ const union = areaA + areaB - intersection;
122
+ return union > 0 ? intersection / union : 0;
123
+ }
@@ -0,0 +1,2 @@
1
+ export declare function preprocessImageBase64(base64: string, inputSize?: number): Float32Array;
2
+ export declare function preprocessImageUri(imageUri: string, inputSize?: number): Promise<Float32Array>;
@@ -0,0 +1,48 @@
1
+ import { Buffer } from 'buffer';
2
+ import { decode as decodeJpeg } from 'jpeg-js';
3
+ const DEFAULT_INPUT_SIZE = 640;
4
+ function stripDataUriPrefix(base64) {
5
+ const marker = 'base64,';
6
+ const markerIndex = base64.indexOf(marker);
7
+ return markerIndex >= 0 ? base64.slice(markerIndex + marker.length) : base64;
8
+ }
9
+ function isJpeg(bytes) {
10
+ return bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xd8;
11
+ }
12
+ function decodeBase64(base64) {
13
+ return Uint8Array.from(Buffer.from(stripDataUriPrefix(base64), 'base64'));
14
+ }
15
+ function resizeRgbaToChw(rgba, sourceWidth, sourceHeight, inputSize) {
16
+ const totalPixels = inputSize * inputSize;
17
+ const tensor = new Float32Array(3 * totalPixels);
18
+ for (let y = 0; y < inputSize; y++) {
19
+ const sourceY = Math.min(sourceHeight - 1, Math.floor((y * sourceHeight) / inputSize));
20
+ for (let x = 0; x < inputSize; x++) {
21
+ const sourceX = Math.min(sourceWidth - 1, Math.floor((x * sourceWidth) / inputSize));
22
+ const sourceOffset = (sourceY * sourceWidth + sourceX) * 4;
23
+ const outputIndex = y * inputSize + x;
24
+ tensor[outputIndex] = rgba[sourceOffset] / 255;
25
+ tensor[totalPixels + outputIndex] = rgba[sourceOffset + 1] / 255;
26
+ tensor[2 * totalPixels + outputIndex] = rgba[sourceOffset + 2] / 255;
27
+ }
28
+ }
29
+ return tensor;
30
+ }
31
+ export function preprocessImageBase64(base64, inputSize = DEFAULT_INPUT_SIZE) {
32
+ const bytes = decodeBase64(base64);
33
+ if (!isJpeg(bytes)) {
34
+ throw new Error('On-device inference currently supports JPEG inputs only');
35
+ }
36
+ const decoded = decodeJpeg(bytes, { useTArray: true, tolerantDecoding: true });
37
+ if (!decoded.width || !decoded.height) {
38
+ throw new Error('Failed to decode JPEG for on-device inference');
39
+ }
40
+ return resizeRgbaToChw(decoded.data, decoded.width, decoded.height, inputSize);
41
+ }
42
+ export async function preprocessImageUri(imageUri, inputSize = DEFAULT_INPUT_SIZE) {
43
+ const fileSystem = await import('expo-file-system');
44
+ const base64 = await fileSystem.readAsStringAsync(imageUri, {
45
+ encoding: fileSystem.EncodingType.Base64,
46
+ });
47
+ return preprocessImageBase64(base64, inputSize);
48
+ }
@@ -0,0 +1,5 @@
1
+ export { ModelManager } from './modelManager';
2
+ export { InferenceEngine } from './inferenceEngine';
3
+ export { extractFeatures, ONTOLOGY_CLASS_NAMES } from './featureExtractor';
4
+ export { evaluatePolicy } from './policyEngine';
5
+ export type { BundleManifest, ModelArtifact, FeatureVector, Detection, ImageQuality, PolicyAST, PolicyResult, RuleResult, } from './types';
@@ -0,0 +1,4 @@
1
+ export { ModelManager } from './modelManager';
2
+ export { InferenceEngine } from './inferenceEngine';
3
+ export { extractFeatures, ONTOLOGY_CLASS_NAMES } from './featureExtractor';
4
+ export { evaluatePolicy } from './policyEngine';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * On-device inference engine for React Native.
3
+ * Uses react-native-fast-tflite for TFLite inference (uses CoreML delegate on iOS).
4
+ */
5
+ import type { BundleManifest } from './types';
6
+ interface RawDetection {
7
+ classId: number;
8
+ confidence: number;
9
+ bbox: [number, number, number, number];
10
+ }
11
+ export declare class InferenceEngine {
12
+ private model;
13
+ private _isLoaded;
14
+ private classIds;
15
+ get isLoaded(): boolean;
16
+ /** Load the model from a cached bundle artifact. */
17
+ loadModel(bundle: BundleManifest): Promise<void>;
18
+ /** Run inference on preprocessed image data. */
19
+ runInference(input: Float32Array, threshold?: number): RawDetection[];
20
+ private parseDetections;
21
+ private resolveClassId;
22
+ dispose(): void;
23
+ }
24
+ export {};