@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.
- package/README.md +11 -2
- package/lib/client/index.d.ts +22 -2
- package/lib/client/index.js +132 -19
- package/lib/components/VerifyAIScanner.d.ts +12 -6
- package/lib/components/VerifyAIScanner.js +157 -15
- package/lib/hooks/useVerifyAI.d.ts +32 -10
- package/lib/hooks/useVerifyAI.js +246 -14
- package/lib/index.d.ts +5 -2
- package/lib/index.js +3 -0
- package/lib/ml/featureExtractor.d.ts +16 -0
- package/lib/ml/featureExtractor.js +123 -0
- package/lib/ml/imagePreprocessor.d.ts +2 -0
- package/lib/ml/imagePreprocessor.js +48 -0
- package/lib/ml/index.d.ts +5 -0
- package/lib/ml/index.js +4 -0
- package/lib/ml/inferenceEngine.d.ts +24 -0
- package/lib/ml/inferenceEngine.js +156 -0
- package/lib/ml/modelManager.d.ts +26 -0
- package/lib/ml/modelManager.js +207 -0
- package/lib/ml/policyEngine.d.ts +14 -0
- package/lib/ml/policyEngine.js +161 -0
- package/lib/ml/types.d.ts +84 -0
- package/lib/ml/types.js +4 -0
- package/lib/storage/offlineQueue.js +1 -1
- package/lib/telemetry/TelemetryContext.d.ts +4 -0
- package/lib/telemetry/TelemetryContext.js +5 -0
- package/lib/telemetry/TelemetryReporter.d.ts +23 -0
- package/lib/telemetry/TelemetryReporter.js +140 -0
- package/lib/types/index.d.ts +18 -0
- package/lib/version.d.ts +1 -0
- package/lib/version.js +1 -0
- package/package.json +23 -2
- package/src/client/index.ts +176 -25
- package/src/components/VerifyAIScanner.tsx +198 -18
- package/src/hooks/useVerifyAI.ts +332 -18
- package/src/index.ts +20 -1
- package/src/ml/featureExtractor.ts +160 -0
- package/src/ml/imagePreprocessor.ts +72 -0
- package/src/ml/index.ts +14 -0
- package/src/ml/inferenceEngine.ts +200 -0
- package/src/ml/modelManager.ts +265 -0
- package/src/ml/policyEngine.ts +201 -0
- package/src/ml/types.ts +104 -0
- package/src/storage/offlineQueue.ts +1 -1
- package/src/telemetry/TelemetryContext.tsx +8 -0
- package/src/telemetry/TelemetryReporter.ts +181 -0
- package/src/types/index.ts +20 -0
- package/src/version.ts +1 -0
package/src/hooks/useVerifyAI.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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:
|
|
77
|
-
const
|
|
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
|
-
() => (
|
|
80
|
-
[client,
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
+
}
|