@switchlabs/verify-ai-react-native 2.4.23 → 2.5.0
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/lib/components/VerifyAIScanner.js +9 -4
- package/lib/ml/featureExtractor.d.ts +1 -1
- package/lib/ml/featureExtractor.js +12 -4
- package/lib/ml/modelManager.d.ts +15 -2
- package/lib/ml/modelManager.js +20 -3
- package/lib/ml/policyEngine.js +71 -0
- package/lib/ml/types.d.ts +17 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +6 -1
- package/src/components/VerifyAIScanner.tsx +9 -4
- package/src/ml/featureExtractor.ts +12 -4
- package/src/ml/modelManager.ts +26 -3
- package/src/ml/policyEngine.ts +86 -0
- package/src/ml/types.ts +34 -1
- package/src/version.ts +1 -1
|
@@ -20,8 +20,10 @@ const CAMERA_INIT_ERROR_CODE = 'ERR_CAMERA_INIT_FAILED';
|
|
|
20
20
|
const DEFAULT_TERMINAL_RESULT_DISPLAY_MS = 3000;
|
|
21
21
|
const TRANSIENT_ERROR_DISPLAY_MS = 3000;
|
|
22
22
|
const CAMERA_CAPTURE_RETRY_DELAY_MS = 350;
|
|
23
|
-
const
|
|
24
|
-
const
|
|
23
|
+
const IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 3000;
|
|
24
|
+
const ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 10000;
|
|
25
|
+
const IOS_CAMERA_STARTUP_WATCHDOG_MS = 8000;
|
|
26
|
+
const ANDROID_CAMERA_STARTUP_WATCHDOG_MS = 10000;
|
|
25
27
|
const CAMERA_STARTUP_WATCHDOG_MAX_REMOUNTS = 5;
|
|
26
28
|
const ANDROID_ORIENTATION_NO_EVENT_TIMEOUT_MS = 3000;
|
|
27
29
|
const ANDROID_ORIENTATION_SAMPLE_TELEMETRY_MS = 3000;
|
|
@@ -884,7 +886,10 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
884
886
|
cameraReadyRef.current = false;
|
|
885
887
|
setCameraKey((key) => key + 1);
|
|
886
888
|
await sleep(CAMERA_CAPTURE_RETRY_DELAY_MS);
|
|
887
|
-
|
|
889
|
+
const captureRetryReadyTimeoutMs = Platform.OS === 'android'
|
|
890
|
+
? ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS
|
|
891
|
+
: IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS;
|
|
892
|
+
captureRetryReady = await waitForCameraReady(captureRetryReadyTimeoutMs);
|
|
888
893
|
telemetry?.track('camera_capture_retry', {
|
|
889
894
|
component: 'scanner',
|
|
890
895
|
error: normalized,
|
|
@@ -894,7 +899,7 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
894
899
|
capture_retry_attempted: captureRetryAttempted,
|
|
895
900
|
capture_retry_ready: captureRetryReady,
|
|
896
901
|
capture_retry_delay_ms: CAMERA_CAPTURE_RETRY_DELAY_MS,
|
|
897
|
-
capture_retry_ready_timeout_ms:
|
|
902
|
+
capture_retry_ready_timeout_ms: captureRetryReadyTimeoutMs,
|
|
898
903
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
899
904
|
}),
|
|
900
905
|
});
|
|
@@ -6,7 +6,7 @@ import type { FeatureVector } from './types';
|
|
|
6
6
|
/** Ontology class name mapping (must match ontology.ts) */
|
|
7
7
|
export declare const ONTOLOGY_CLASS_NAMES: Record<number, string>;
|
|
8
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];
|
|
9
|
+
export declare const MODEL_OUTPUT_CLASS_IDS: readonly [1, 2, 3, 4, 5, 6, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 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
10
|
interface RawDetection {
|
|
11
11
|
classId: number;
|
|
12
12
|
confidence: number;
|
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
* Feature extractor — converts raw detections into a structured FeatureVector.
|
|
3
3
|
* Must produce identical output to the Dart and server TypeScript implementations.
|
|
4
4
|
*/
|
|
5
|
-
const SCHEMA_VERSION = '
|
|
5
|
+
const SCHEMA_VERSION = '2.0.0';
|
|
6
6
|
/** Vehicle class IDs (must match ontology.ts) */
|
|
7
|
-
const VEHICLE_CLASS_IDS = new Set([1, 2, 3, 4, 5]);
|
|
7
|
+
const VEHICLE_CLASS_IDS = new Set([1, 2, 3, 4, 5, 6]);
|
|
8
8
|
/** Surface class IDs */
|
|
9
9
|
const SURFACE_CLASS_IDS = new Set([60, 61, 62, 63, 64, 65, 66]);
|
|
10
10
|
/** Ontology class name mapping (must match ontology.ts) */
|
|
11
11
|
export const ONTOLOGY_CLASS_NAMES = {
|
|
12
|
-
1: 'scooter', 2: 'bicycle', 3: 'ebike', 4: 'moped', 5: 'motorcycle',
|
|
12
|
+
1: 'scooter', 2: 'bicycle', 3: 'ebike', 4: 'moped', 5: 'motorcycle', 6: 'car',
|
|
13
13
|
20: 'kickstand', 21: 'handlebar', 22: 'wheel', 23: 'saddle', 24: 'basket',
|
|
14
|
+
// Car panels (ontology v2 — damage intelligence)
|
|
15
|
+
25: 'car_hood', 26: 'car_roof', 27: 'car_trunk',
|
|
16
|
+
28: 'car_front_bumper', 29: 'car_rear_bumper',
|
|
17
|
+
30: 'car_door_fl', 31: 'car_door_fr', 32: 'car_door_rl', 33: 'car_door_rr',
|
|
18
|
+
34: 'car_fender_fl', 35: 'car_fender_fr',
|
|
19
|
+
36: 'car_quarter_rl', 37: 'car_quarter_rr',
|
|
20
|
+
38: 'car_rocker', 39: 'car_mirror',
|
|
14
21
|
40: 'bike_rack', 41: 'curb', 42: 'parking_bay', 43: 'green_bay',
|
|
15
22
|
44: 'tactile_paving', 45: 'bollard', 46: 'fence', 47: 'pole', 48: 'yellow_lines',
|
|
16
23
|
60: 'sidewalk', 61: 'road', 62: 'grass', 63: 'crosswalk',
|
|
@@ -20,8 +27,9 @@ export const ONTOLOGY_CLASS_NAMES = {
|
|
|
20
27
|
};
|
|
21
28
|
/** Detection model class index -> ontology class ID mapping. */
|
|
22
29
|
export const MODEL_OUTPUT_CLASS_IDS = [
|
|
23
|
-
1, 2, 3, 4, 5,
|
|
30
|
+
1, 2, 3, 4, 5, 6,
|
|
24
31
|
20, 21, 22, 23, 24,
|
|
32
|
+
25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
|
|
25
33
|
40, 41, 42, 43, 44, 45, 46, 47, 48,
|
|
26
34
|
60, 61, 62, 63, 64, 65, 66,
|
|
27
35
|
80, 81, 82, 83, 84, 85, 86, 87,
|
package/lib/ml/modelManager.d.ts
CHANGED
|
@@ -15,8 +15,21 @@ export declare class ModelManager {
|
|
|
15
15
|
private _currentBundle;
|
|
16
16
|
constructor(config: ModelManagerConfig);
|
|
17
17
|
get currentBundle(): BundleManifest | null;
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Check for bundle updates from the server.
|
|
20
|
+
*
|
|
21
|
+
* `opts` lets callers narrow which bundle they receive when the server
|
|
22
|
+
* has per-asset / per-region / per-app-version targeted bundles
|
|
23
|
+
* configured (see `app/api/v1/models/latest`). All fields are optional;
|
|
24
|
+
* omitting them keeps the legacy behaviour of receiving the wildcard
|
|
25
|
+
* bundle for the policy.
|
|
26
|
+
*/
|
|
27
|
+
checkForUpdates(policyId: string, platform: 'ios' | 'android', opts?: {
|
|
28
|
+
assetType?: string;
|
|
29
|
+
region?: string;
|
|
30
|
+
appVersion?: string;
|
|
31
|
+
deviceId?: string;
|
|
32
|
+
}): Promise<BundleManifest | null>;
|
|
20
33
|
/** Download model artifacts and verify integrity. */
|
|
21
34
|
private downloadArtifacts;
|
|
22
35
|
private readFileSha256;
|
package/lib/ml/modelManager.js
CHANGED
|
@@ -26,8 +26,16 @@ export class ModelManager {
|
|
|
26
26
|
get currentBundle() {
|
|
27
27
|
return this._currentBundle;
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Check for bundle updates from the server.
|
|
31
|
+
*
|
|
32
|
+
* `opts` lets callers narrow which bundle they receive when the server
|
|
33
|
+
* has per-asset / per-region / per-app-version targeted bundles
|
|
34
|
+
* configured (see `app/api/v1/models/latest`). All fields are optional;
|
|
35
|
+
* omitting them keeps the legacy behaviour of receiving the wildcard
|
|
36
|
+
* bundle for the policy.
|
|
37
|
+
*/
|
|
38
|
+
async checkForUpdates(policyId, platform, opts) {
|
|
31
39
|
try {
|
|
32
40
|
const headers = {
|
|
33
41
|
'X-API-Key': this.apiKey,
|
|
@@ -36,7 +44,16 @@ export class ModelManager {
|
|
|
36
44
|
if (this.cachedETag) {
|
|
37
45
|
headers['If-None-Match'] = this.cachedETag;
|
|
38
46
|
}
|
|
39
|
-
const
|
|
47
|
+
const qs = new URLSearchParams({ policy: policyId, platform });
|
|
48
|
+
if (opts?.assetType)
|
|
49
|
+
qs.set('asset_type', opts.assetType);
|
|
50
|
+
if (opts?.region)
|
|
51
|
+
qs.set('region', opts.region);
|
|
52
|
+
if (opts?.appVersion)
|
|
53
|
+
qs.set('app_version', opts.appVersion);
|
|
54
|
+
if (opts?.deviceId)
|
|
55
|
+
qs.set('device_id', opts.deviceId);
|
|
56
|
+
const url = `${this.baseUrl}/models/latest?${qs.toString()}`;
|
|
40
57
|
const response = await fetch(url, { headers });
|
|
41
58
|
if (response.status === 304) {
|
|
42
59
|
return this._currentBundle;
|
package/lib/ml/policyEngine.js
CHANGED
|
@@ -5,6 +5,24 @@
|
|
|
5
5
|
* This file shares the same logic as lib/verify-ai/ml/policy-engine.ts
|
|
6
6
|
* but is self-contained for the SDK package.
|
|
7
7
|
*/
|
|
8
|
+
// Severity ordering for `severity_gte`. Must match server TS + Dart impls.
|
|
9
|
+
const SEVERITY_ORDER = ['none', 'light', 'medium', 'severe'];
|
|
10
|
+
function severityRank(s) {
|
|
11
|
+
if (typeof s !== 'string')
|
|
12
|
+
return -1;
|
|
13
|
+
return SEVERITY_ORDER.indexOf(s);
|
|
14
|
+
}
|
|
15
|
+
function compareNumber(a, op, b) {
|
|
16
|
+
switch (op) {
|
|
17
|
+
case 'eq': return a === b;
|
|
18
|
+
case 'neq': return a !== b;
|
|
19
|
+
case 'gt': return a > b;
|
|
20
|
+
case 'gte': return a >= b;
|
|
21
|
+
case 'lt': return a < b;
|
|
22
|
+
case 'lte': return a <= b;
|
|
23
|
+
default: return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
8
26
|
// ─── Field Resolution ───
|
|
9
27
|
function resolveField(features, field) {
|
|
10
28
|
const parts = field.split('.');
|
|
@@ -67,9 +85,62 @@ function applyOperator(fieldValue, operator, value) {
|
|
|
67
85
|
}
|
|
68
86
|
}
|
|
69
87
|
function evaluateCondition(features, condition) {
|
|
88
|
+
if (condition.operator === 'severity_gte' ||
|
|
89
|
+
condition.operator === 'panel_has_damage' ||
|
|
90
|
+
condition.operator === 'aggregate_count') {
|
|
91
|
+
return applyDamageOperator(features, condition);
|
|
92
|
+
}
|
|
70
93
|
const fieldValue = resolveField(features, condition.field);
|
|
71
94
|
return applyOperator(fieldValue, condition.operator, condition.value);
|
|
72
95
|
}
|
|
96
|
+
function applyDamageOperator(features, condition) {
|
|
97
|
+
const findings = Array.isArray(features.damage_findings)
|
|
98
|
+
? features.damage_findings
|
|
99
|
+
: [];
|
|
100
|
+
switch (condition.operator) {
|
|
101
|
+
case 'severity_gte': {
|
|
102
|
+
const minRank = severityRank(condition.value);
|
|
103
|
+
if (minRank < 0)
|
|
104
|
+
return false;
|
|
105
|
+
if (condition.field && condition.field !== 'damage_findings') {
|
|
106
|
+
const f = resolveField(features, condition.field);
|
|
107
|
+
return severityRank(f) >= minRank;
|
|
108
|
+
}
|
|
109
|
+
return findings.some((finding) => severityRank(finding.severity) >= minRank);
|
|
110
|
+
}
|
|
111
|
+
case 'panel_has_damage': {
|
|
112
|
+
const panelName = typeof condition.value === 'string'
|
|
113
|
+
? condition.value
|
|
114
|
+
: typeof condition.field === 'string' &&
|
|
115
|
+
condition.field !== 'damage_findings' &&
|
|
116
|
+
condition.field !== ''
|
|
117
|
+
? condition.field
|
|
118
|
+
: null;
|
|
119
|
+
if (!panelName)
|
|
120
|
+
return false;
|
|
121
|
+
return findings.some((finding) => finding.panel === panelName && finding.severity !== 'none');
|
|
122
|
+
}
|
|
123
|
+
case 'aggregate_count': {
|
|
124
|
+
const v = condition.value;
|
|
125
|
+
if (!v || typeof v !== 'object' || typeof v.value !== 'number' || !v.op)
|
|
126
|
+
return false;
|
|
127
|
+
const where = v.where || {};
|
|
128
|
+
const minSeverityRank = where.severity_gte !== undefined ? severityRank(where.severity_gte) : -1;
|
|
129
|
+
const count = findings.filter((finding) => {
|
|
130
|
+
if (where.panel && finding.panel !== where.panel)
|
|
131
|
+
return false;
|
|
132
|
+
if (where.damage_type && finding.damage_type !== where.damage_type)
|
|
133
|
+
return false;
|
|
134
|
+
if (minSeverityRank >= 0 && severityRank(finding.severity) < minSeverityRank)
|
|
135
|
+
return false;
|
|
136
|
+
return true;
|
|
137
|
+
}).length;
|
|
138
|
+
return compareNumber(count, v.op, v.value);
|
|
139
|
+
}
|
|
140
|
+
default:
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
73
144
|
function evaluateRule(features, rule) {
|
|
74
145
|
const passed = rule.conditions.every((c) => evaluateCondition(features, c));
|
|
75
146
|
return {
|
package/lib/ml/types.d.ts
CHANGED
|
@@ -32,6 +32,18 @@ export interface ImageQuality {
|
|
|
32
32
|
is_dark: boolean;
|
|
33
33
|
has_vehicle: boolean;
|
|
34
34
|
}
|
|
35
|
+
export type DamageSeverity = 'none' | 'light' | 'medium' | 'severe';
|
|
36
|
+
export type DamageType = 'scratch' | 'dent' | 'paint_chip' | 'crack' | 'broken' | 'missing' | 'rust' | 'tear' | 'stain' | 'glass_damage' | 'other';
|
|
37
|
+
export interface DamageFinding {
|
|
38
|
+
finding_id: string;
|
|
39
|
+
panel: string;
|
|
40
|
+
damage_type: DamageType;
|
|
41
|
+
severity: DamageSeverity;
|
|
42
|
+
severity_score: number;
|
|
43
|
+
bbox: [number, number, number, number];
|
|
44
|
+
area_pct: number;
|
|
45
|
+
confidence: number;
|
|
46
|
+
}
|
|
35
47
|
export interface FeatureVector {
|
|
36
48
|
schema_version: string;
|
|
37
49
|
detections: Detection[];
|
|
@@ -39,8 +51,12 @@ export interface FeatureVector {
|
|
|
39
51
|
image_quality: ImageQuality;
|
|
40
52
|
vehicle_on_surface: string | null;
|
|
41
53
|
vehicle_near: string[];
|
|
54
|
+
/** Damage observations (schema v2). Defaults to [] when damage mode disabled. */
|
|
55
|
+
damage_findings?: DamageFinding[];
|
|
56
|
+
/** Panels visible in this frame (schema v2). */
|
|
57
|
+
panel_inventory?: string[];
|
|
42
58
|
}
|
|
43
|
-
export type ComparisonOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'not_contains' | 'exists' | 'not_exists' | 'in' | 'not_in' | 'overlaps';
|
|
59
|
+
export type ComparisonOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'not_contains' | 'exists' | 'not_exists' | 'in' | 'not_in' | 'overlaps' | 'severity_gte' | 'panel_has_damage' | 'aggregate_count';
|
|
44
60
|
export interface Condition {
|
|
45
61
|
field: string;
|
|
46
62
|
operator: ComparisonOperator;
|
package/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "2.
|
|
1
|
+
export declare const SDK_VERSION = "2.5.0";
|
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '2.
|
|
1
|
+
export const SDK_VERSION = '2.5.0';
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@switchlabs/verify-ai-react-native",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "React Native SDK for Verify AI - photo verification with AI vision processing",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/levyelectric/verify.git",
|
|
8
|
+
"directory": "packages/verify-ai-react-native"
|
|
9
|
+
},
|
|
5
10
|
"main": "./lib/index.js",
|
|
6
11
|
"types": "./lib/index.d.ts",
|
|
7
12
|
"exports": {
|
|
@@ -45,8 +45,10 @@ const CAMERA_INIT_ERROR_CODE = 'ERR_CAMERA_INIT_FAILED';
|
|
|
45
45
|
const DEFAULT_TERMINAL_RESULT_DISPLAY_MS = 3000;
|
|
46
46
|
const TRANSIENT_ERROR_DISPLAY_MS = 3000;
|
|
47
47
|
const CAMERA_CAPTURE_RETRY_DELAY_MS = 350;
|
|
48
|
-
const
|
|
49
|
-
const
|
|
48
|
+
const IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 3000;
|
|
49
|
+
const ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 10000;
|
|
50
|
+
const IOS_CAMERA_STARTUP_WATCHDOG_MS = 8000;
|
|
51
|
+
const ANDROID_CAMERA_STARTUP_WATCHDOG_MS = 10000;
|
|
50
52
|
const CAMERA_STARTUP_WATCHDOG_MAX_REMOUNTS = 5;
|
|
51
53
|
const ANDROID_ORIENTATION_NO_EVENT_TIMEOUT_MS = 3000;
|
|
52
54
|
const ANDROID_ORIENTATION_SAMPLE_TELEMETRY_MS = 3000;
|
|
@@ -1079,7 +1081,10 @@ export function VerifyAIScanner({
|
|
|
1079
1081
|
cameraReadyRef.current = false;
|
|
1080
1082
|
setCameraKey((key) => key + 1);
|
|
1081
1083
|
await sleep(CAMERA_CAPTURE_RETRY_DELAY_MS);
|
|
1082
|
-
|
|
1084
|
+
const captureRetryReadyTimeoutMs = Platform.OS === 'android'
|
|
1085
|
+
? ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS
|
|
1086
|
+
: IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS;
|
|
1087
|
+
captureRetryReady = await waitForCameraReady(captureRetryReadyTimeoutMs);
|
|
1083
1088
|
telemetry?.track('camera_capture_retry', {
|
|
1084
1089
|
component: 'scanner',
|
|
1085
1090
|
error: normalized,
|
|
@@ -1089,7 +1094,7 @@ export function VerifyAIScanner({
|
|
|
1089
1094
|
capture_retry_attempted: captureRetryAttempted,
|
|
1090
1095
|
capture_retry_ready: captureRetryReady,
|
|
1091
1096
|
capture_retry_delay_ms: CAMERA_CAPTURE_RETRY_DELAY_MS,
|
|
1092
|
-
capture_retry_ready_timeout_ms:
|
|
1097
|
+
capture_retry_ready_timeout_ms: captureRetryReadyTimeoutMs,
|
|
1093
1098
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
1094
1099
|
}),
|
|
1095
1100
|
});
|
|
@@ -5,17 +5,24 @@
|
|
|
5
5
|
|
|
6
6
|
import type { FeatureVector, Detection, ImageQuality } from './types';
|
|
7
7
|
|
|
8
|
-
const SCHEMA_VERSION = '
|
|
8
|
+
const SCHEMA_VERSION = '2.0.0';
|
|
9
9
|
|
|
10
10
|
/** Vehicle class IDs (must match ontology.ts) */
|
|
11
|
-
const VEHICLE_CLASS_IDS = new Set([1, 2, 3, 4, 5]);
|
|
11
|
+
const VEHICLE_CLASS_IDS = new Set([1, 2, 3, 4, 5, 6]);
|
|
12
12
|
/** Surface class IDs */
|
|
13
13
|
const SURFACE_CLASS_IDS = new Set([60, 61, 62, 63, 64, 65, 66]);
|
|
14
14
|
|
|
15
15
|
/** Ontology class name mapping (must match ontology.ts) */
|
|
16
16
|
export const ONTOLOGY_CLASS_NAMES: Record<number, string> = {
|
|
17
|
-
1: 'scooter', 2: 'bicycle', 3: 'ebike', 4: 'moped', 5: 'motorcycle',
|
|
17
|
+
1: 'scooter', 2: 'bicycle', 3: 'ebike', 4: 'moped', 5: 'motorcycle', 6: 'car',
|
|
18
18
|
20: 'kickstand', 21: 'handlebar', 22: 'wheel', 23: 'saddle', 24: 'basket',
|
|
19
|
+
// Car panels (ontology v2 — damage intelligence)
|
|
20
|
+
25: 'car_hood', 26: 'car_roof', 27: 'car_trunk',
|
|
21
|
+
28: 'car_front_bumper', 29: 'car_rear_bumper',
|
|
22
|
+
30: 'car_door_fl', 31: 'car_door_fr', 32: 'car_door_rl', 33: 'car_door_rr',
|
|
23
|
+
34: 'car_fender_fl', 35: 'car_fender_fr',
|
|
24
|
+
36: 'car_quarter_rl', 37: 'car_quarter_rr',
|
|
25
|
+
38: 'car_rocker', 39: 'car_mirror',
|
|
19
26
|
40: 'bike_rack', 41: 'curb', 42: 'parking_bay', 43: 'green_bay',
|
|
20
27
|
44: 'tactile_paving', 45: 'bollard', 46: 'fence', 47: 'pole', 48: 'yellow_lines',
|
|
21
28
|
60: 'sidewalk', 61: 'road', 62: 'grass', 63: 'crosswalk',
|
|
@@ -26,8 +33,9 @@ export const ONTOLOGY_CLASS_NAMES: Record<number, string> = {
|
|
|
26
33
|
|
|
27
34
|
/** Detection model class index -> ontology class ID mapping. */
|
|
28
35
|
export const MODEL_OUTPUT_CLASS_IDS = [
|
|
29
|
-
1, 2, 3, 4, 5,
|
|
36
|
+
1, 2, 3, 4, 5, 6,
|
|
30
37
|
20, 21, 22, 23, 24,
|
|
38
|
+
25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
|
|
31
39
|
40, 41, 42, 43, 44, 45, 46, 47, 48,
|
|
32
40
|
60, 61, 62, 63, 64, 65, 66,
|
|
33
41
|
80, 81, 82, 83, 84, 85, 86, 87,
|
package/src/ml/modelManager.ts
CHANGED
|
@@ -62,8 +62,25 @@ export class ModelManager {
|
|
|
62
62
|
return this._currentBundle;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Check for bundle updates from the server.
|
|
67
|
+
*
|
|
68
|
+
* `opts` lets callers narrow which bundle they receive when the server
|
|
69
|
+
* has per-asset / per-region / per-app-version targeted bundles
|
|
70
|
+
* configured (see `app/api/v1/models/latest`). All fields are optional;
|
|
71
|
+
* omitting them keeps the legacy behaviour of receiving the wildcard
|
|
72
|
+
* bundle for the policy.
|
|
73
|
+
*/
|
|
74
|
+
async checkForUpdates(
|
|
75
|
+
policyId: string,
|
|
76
|
+
platform: 'ios' | 'android',
|
|
77
|
+
opts?: {
|
|
78
|
+
assetType?: string;
|
|
79
|
+
region?: string;
|
|
80
|
+
appVersion?: string;
|
|
81
|
+
deviceId?: string;
|
|
82
|
+
},
|
|
83
|
+
): Promise<BundleManifest | null> {
|
|
67
84
|
try {
|
|
68
85
|
const headers: Record<string, string> = {
|
|
69
86
|
'X-API-Key': this.apiKey,
|
|
@@ -74,7 +91,13 @@ export class ModelManager {
|
|
|
74
91
|
headers['If-None-Match'] = this.cachedETag;
|
|
75
92
|
}
|
|
76
93
|
|
|
77
|
-
const
|
|
94
|
+
const qs = new URLSearchParams({ policy: policyId, platform });
|
|
95
|
+
if (opts?.assetType) qs.set('asset_type', opts.assetType);
|
|
96
|
+
if (opts?.region) qs.set('region', opts.region);
|
|
97
|
+
if (opts?.appVersion) qs.set('app_version', opts.appVersion);
|
|
98
|
+
if (opts?.deviceId) qs.set('device_id', opts.deviceId);
|
|
99
|
+
|
|
100
|
+
const url = `${this.baseUrl}/models/latest?${qs.toString()}`;
|
|
78
101
|
const response = await fetch(url, { headers });
|
|
79
102
|
|
|
80
103
|
if (response.status === 304) {
|
package/src/ml/policyEngine.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type {
|
|
10
|
+
DamageFinding,
|
|
11
|
+
DamageSeverity,
|
|
10
12
|
FeatureVector,
|
|
11
13
|
PolicyAST,
|
|
12
14
|
PolicyResult,
|
|
@@ -17,6 +19,35 @@ import type {
|
|
|
17
19
|
ComparisonOperator,
|
|
18
20
|
} from './types';
|
|
19
21
|
|
|
22
|
+
// Severity ordering for `severity_gte`. Must match server TS + Dart impls.
|
|
23
|
+
const SEVERITY_ORDER: DamageSeverity[] = ['none', 'light', 'medium', 'severe'];
|
|
24
|
+
function severityRank(s: unknown): number {
|
|
25
|
+
if (typeof s !== 'string') return -1;
|
|
26
|
+
return SEVERITY_ORDER.indexOf(s as DamageSeverity);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface AggregateCountValue {
|
|
30
|
+
op: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte';
|
|
31
|
+
value: number;
|
|
32
|
+
where?: {
|
|
33
|
+
panel?: string;
|
|
34
|
+
damage_type?: string;
|
|
35
|
+
severity_gte?: DamageSeverity;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function compareNumber(a: number, op: AggregateCountValue['op'], b: number): boolean {
|
|
40
|
+
switch (op) {
|
|
41
|
+
case 'eq': return a === b;
|
|
42
|
+
case 'neq': return a !== b;
|
|
43
|
+
case 'gt': return a > b;
|
|
44
|
+
case 'gte': return a >= b;
|
|
45
|
+
case 'lt': return a < b;
|
|
46
|
+
case 'lte': return a <= b;
|
|
47
|
+
default: return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
20
51
|
// ─── Field Resolution ───
|
|
21
52
|
|
|
22
53
|
function resolveField(features: FeatureVector, field: string): unknown {
|
|
@@ -77,10 +108,65 @@ function applyOperator(fieldValue: unknown, operator: ComparisonOperator, value:
|
|
|
77
108
|
}
|
|
78
109
|
|
|
79
110
|
function evaluateCondition(features: FeatureVector, condition: Condition): boolean {
|
|
111
|
+
if (
|
|
112
|
+
condition.operator === 'severity_gte' ||
|
|
113
|
+
condition.operator === 'panel_has_damage' ||
|
|
114
|
+
condition.operator === 'aggregate_count'
|
|
115
|
+
) {
|
|
116
|
+
return applyDamageOperator(features, condition);
|
|
117
|
+
}
|
|
80
118
|
const fieldValue = resolveField(features, condition.field);
|
|
81
119
|
return applyOperator(fieldValue, condition.operator, condition.value);
|
|
82
120
|
}
|
|
83
121
|
|
|
122
|
+
function applyDamageOperator(features: FeatureVector, condition: Condition): boolean {
|
|
123
|
+
const findings: DamageFinding[] = Array.isArray(features.damage_findings)
|
|
124
|
+
? features.damage_findings
|
|
125
|
+
: [];
|
|
126
|
+
|
|
127
|
+
switch (condition.operator) {
|
|
128
|
+
case 'severity_gte': {
|
|
129
|
+
const minRank = severityRank(condition.value);
|
|
130
|
+
if (minRank < 0) return false;
|
|
131
|
+
if (condition.field && condition.field !== 'damage_findings') {
|
|
132
|
+
const f = resolveField(features, condition.field);
|
|
133
|
+
return severityRank(f) >= minRank;
|
|
134
|
+
}
|
|
135
|
+
return findings.some((finding) => severityRank(finding.severity) >= minRank);
|
|
136
|
+
}
|
|
137
|
+
case 'panel_has_damage': {
|
|
138
|
+
const panelName =
|
|
139
|
+
typeof condition.value === 'string'
|
|
140
|
+
? condition.value
|
|
141
|
+
: typeof condition.field === 'string' &&
|
|
142
|
+
condition.field !== 'damage_findings' &&
|
|
143
|
+
condition.field !== ''
|
|
144
|
+
? condition.field
|
|
145
|
+
: null;
|
|
146
|
+
if (!panelName) return false;
|
|
147
|
+
return findings.some(
|
|
148
|
+
(finding) => finding.panel === panelName && finding.severity !== 'none',
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
case 'aggregate_count': {
|
|
152
|
+
const v = condition.value as AggregateCountValue | null | undefined;
|
|
153
|
+
if (!v || typeof v !== 'object' || typeof v.value !== 'number' || !v.op) return false;
|
|
154
|
+
const where = v.where || {};
|
|
155
|
+
const minSeverityRank =
|
|
156
|
+
where.severity_gte !== undefined ? severityRank(where.severity_gte) : -1;
|
|
157
|
+
const count = findings.filter((finding) => {
|
|
158
|
+
if (where.panel && finding.panel !== where.panel) return false;
|
|
159
|
+
if (where.damage_type && finding.damage_type !== where.damage_type) return false;
|
|
160
|
+
if (minSeverityRank >= 0 && severityRank(finding.severity) < minSeverityRank) return false;
|
|
161
|
+
return true;
|
|
162
|
+
}).length;
|
|
163
|
+
return compareNumber(count, v.op, v.value);
|
|
164
|
+
}
|
|
165
|
+
default:
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
84
170
|
function evaluateRule(features: FeatureVector, rule: Rule): RuleResult {
|
|
85
171
|
const passed = rule.conditions.every((c) => evaluateCondition(features, c));
|
|
86
172
|
return {
|
package/src/ml/types.ts
CHANGED
|
@@ -39,6 +39,33 @@ export interface ImageQuality {
|
|
|
39
39
|
has_vehicle: boolean;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// ─── Damage Findings (schema v2) ───
|
|
43
|
+
|
|
44
|
+
export type DamageSeverity = 'none' | 'light' | 'medium' | 'severe';
|
|
45
|
+
export type DamageType =
|
|
46
|
+
| 'scratch'
|
|
47
|
+
| 'dent'
|
|
48
|
+
| 'paint_chip'
|
|
49
|
+
| 'crack'
|
|
50
|
+
| 'broken'
|
|
51
|
+
| 'missing'
|
|
52
|
+
| 'rust'
|
|
53
|
+
| 'tear'
|
|
54
|
+
| 'stain'
|
|
55
|
+
| 'glass_damage'
|
|
56
|
+
| 'other';
|
|
57
|
+
|
|
58
|
+
export interface DamageFinding {
|
|
59
|
+
finding_id: string;
|
|
60
|
+
panel: string;
|
|
61
|
+
damage_type: DamageType;
|
|
62
|
+
severity: DamageSeverity;
|
|
63
|
+
severity_score: number;
|
|
64
|
+
bbox: [number, number, number, number];
|
|
65
|
+
area_pct: number;
|
|
66
|
+
confidence: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
42
69
|
export interface FeatureVector {
|
|
43
70
|
schema_version: string;
|
|
44
71
|
detections: Detection[];
|
|
@@ -46,6 +73,10 @@ export interface FeatureVector {
|
|
|
46
73
|
image_quality: ImageQuality;
|
|
47
74
|
vehicle_on_surface: string | null;
|
|
48
75
|
vehicle_near: string[];
|
|
76
|
+
/** Damage observations (schema v2). Defaults to [] when damage mode disabled. */
|
|
77
|
+
damage_findings?: DamageFinding[];
|
|
78
|
+
/** Panels visible in this frame (schema v2). */
|
|
79
|
+
panel_inventory?: string[];
|
|
49
80
|
}
|
|
50
81
|
|
|
51
82
|
// ─── Policy AST Types ───
|
|
@@ -54,7 +85,9 @@ export type ComparisonOperator =
|
|
|
54
85
|
| 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte'
|
|
55
86
|
| 'contains' | 'not_contains'
|
|
56
87
|
| 'exists' | 'not_exists'
|
|
57
|
-
| 'in' | 'not_in' | 'overlaps'
|
|
88
|
+
| 'in' | 'not_in' | 'overlaps'
|
|
89
|
+
// AST v1.1 — damage operators
|
|
90
|
+
| 'severity_gte' | 'panel_has_damage' | 'aggregate_count';
|
|
58
91
|
|
|
59
92
|
export interface Condition {
|
|
60
93
|
field: string;
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '2.
|
|
1
|
+
export const SDK_VERSION = '2.5.0';
|