@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/lib/hooks/useVerifyAI.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
42
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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 {
|
|
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,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';
|
package/lib/ml/index.js
ADDED
|
@@ -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 {};
|