@souscheflabs/ml-vision 0.1.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/README.md +274 -0
- package/dist/components/DetectionOverlay.d.ts +57 -0
- package/dist/components/DetectionOverlay.js +133 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +9 -0
- package/dist/core/CacheManager.d.ts +168 -0
- package/dist/core/CacheManager.js +331 -0
- package/dist/core/MLVisionProvider.d.ts +90 -0
- package/dist/core/MLVisionProvider.js +188 -0
- package/dist/core/ServerClient.d.ts +131 -0
- package/dist/core/ServerClient.js +291 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +18 -0
- package/dist/hooks/classLabels.d.ts +35 -0
- package/dist/hooks/classLabels.js +439 -0
- package/dist/hooks/classLabelsCoco.d.ts +43 -0
- package/dist/hooks/classLabelsCoco.js +103 -0
- package/dist/hooks/index.d.ts +8 -0
- package/dist/hooks/index.js +27 -0
- package/dist/hooks/useMultiBarcodeScanner.d.ts +34 -0
- package/dist/hooks/useMultiBarcodeScanner.js +290 -0
- package/dist/hooks/useProductDetector.d.ts +38 -0
- package/dist/hooks/useProductDetector.js +679 -0
- package/dist/hooks/useReceiptScanner.d.ts +37 -0
- package/dist/hooks/useReceiptScanner.js +405 -0
- package/dist/hooks/useVideoScanner.d.ts +118 -0
- package/dist/hooks/useVideoScanner.js +383 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +130 -0
- package/dist/processors/detectionProcessor.d.ts +86 -0
- package/dist/processors/detectionProcessor.js +124 -0
- package/dist/processors/index.d.ts +5 -0
- package/dist/processors/index.js +16 -0
- package/dist/processors/tfliteFrameProcessor.d.ts +90 -0
- package/dist/processors/tfliteFrameProcessor.js +213 -0
- package/dist/types/barcode.d.ts +91 -0
- package/dist/types/barcode.js +19 -0
- package/dist/types/detection.d.ts +166 -0
- package/dist/types/detection.js +8 -0
- package/dist/types/index.d.ts +126 -0
- package/dist/types/index.js +25 -0
- package/dist/types/ocr.d.ts +202 -0
- package/dist/types/ocr.js +8 -0
- package/dist/utils/imagePreprocessor.d.ts +85 -0
- package/dist/utils/imagePreprocessor.js +304 -0
- package/dist/utils/yoloProcessor.d.ts +40 -0
- package/dist/utils/yoloProcessor.js +154 -0
- package/package.json +78 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useReceiptScanner Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for scanning and parsing grocery receipts.
|
|
5
|
+
* Uses MLKit text recognition with intelligent parsing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { useReceiptScanner } from '@souschef/ml-vision';
|
|
10
|
+
*
|
|
11
|
+
* function ReceiptScreen() {
|
|
12
|
+
* const {
|
|
13
|
+
* items,
|
|
14
|
+
* metadata,
|
|
15
|
+
* isProcessing,
|
|
16
|
+
* scanReceipt,
|
|
17
|
+
* } = useReceiptScanner({
|
|
18
|
+
* onParseComplete: (result) => console.log('Parsed:', result),
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* const handleCapture = async (photoUri: string) => {
|
|
22
|
+
* const result = await scanReceipt(photoUri);
|
|
23
|
+
* // result.items contains parsed line items
|
|
24
|
+
* };
|
|
25
|
+
*
|
|
26
|
+
* return (
|
|
27
|
+
* // Your UI
|
|
28
|
+
* );
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
import type { UseReceiptScannerOptions, UseReceiptScannerReturn } from '../types';
|
|
33
|
+
/**
|
|
34
|
+
* Hook for receipt scanning and parsing
|
|
35
|
+
*/
|
|
36
|
+
export declare function useReceiptScanner(options?: UseReceiptScannerOptions): UseReceiptScannerReturn;
|
|
37
|
+
export default useReceiptScanner;
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* useReceiptScanner Hook
|
|
4
|
+
*
|
|
5
|
+
* React hook for scanning and parsing grocery receipts.
|
|
6
|
+
* Uses MLKit text recognition with intelligent parsing.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { useReceiptScanner } from '@souschef/ml-vision';
|
|
11
|
+
*
|
|
12
|
+
* function ReceiptScreen() {
|
|
13
|
+
* const {
|
|
14
|
+
* items,
|
|
15
|
+
* metadata,
|
|
16
|
+
* isProcessing,
|
|
17
|
+
* scanReceipt,
|
|
18
|
+
* } = useReceiptScanner({
|
|
19
|
+
* onParseComplete: (result) => console.log('Parsed:', result),
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* const handleCapture = async (photoUri: string) => {
|
|
23
|
+
* const result = await scanReceipt(photoUri);
|
|
24
|
+
* // result.items contains parsed line items
|
|
25
|
+
* };
|
|
26
|
+
*
|
|
27
|
+
* return (
|
|
28
|
+
* // Your UI
|
|
29
|
+
* );
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
34
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
35
|
+
};
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.useReceiptScanner = useReceiptScanner;
|
|
38
|
+
const react_1 = require("react");
|
|
39
|
+
const text_recognition_1 = __importDefault(require("@react-native-ml-kit/text-recognition"));
|
|
40
|
+
const MLVisionProvider_1 = require("../core/MLVisionProvider");
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Receipt Parsing Patterns
|
|
43
|
+
// ============================================================================
|
|
44
|
+
/** Price pattern: matches $X.XX, X.XX, etc. */
|
|
45
|
+
const PRICE_PATTERN = /\$?\d{1,4}\.\d{2}/g;
|
|
46
|
+
/** Quantity pattern: matches Xkg, X lb, X ct, etc. */
|
|
47
|
+
const QUANTITY_PATTERN = /(\d+(?:\.\d+)?)\s*(kg|lb|oz|ct|ea|pc|g|ml|l|gal)?/i;
|
|
48
|
+
/** Date patterns */
|
|
49
|
+
const DATE_PATTERNS = [
|
|
50
|
+
/(\d{1,2})[/-](\d{1,2})[/-](\d{2,4})/, // MM/DD/YYYY or DD/MM/YYYY
|
|
51
|
+
/(\w{3})\s+(\d{1,2}),?\s*(\d{4})/i, // Jan 15, 2024
|
|
52
|
+
];
|
|
53
|
+
/** Time pattern */
|
|
54
|
+
const TIME_PATTERN = /\d{1,2}:\d{2}(?::\d{2})?\s*(?:AM|PM)?/i;
|
|
55
|
+
// Total/Subtotal patterns (used for line classification)
|
|
56
|
+
// TOTAL|GRAND TOTAL|AMOUNT DUE, SUBTOTAL, TAX
|
|
57
|
+
/** Known store patterns for detection */
|
|
58
|
+
const STORE_PATTERNS = [
|
|
59
|
+
{ pattern: /WALMART/i, store: 'walmart' },
|
|
60
|
+
{ pattern: /TARGET/i, store: 'target' },
|
|
61
|
+
{ pattern: /KROGER/i, store: 'kroger' },
|
|
62
|
+
{ pattern: /SAFEWAY/i, store: 'safeway' },
|
|
63
|
+
{ pattern: /COSTCO/i, store: 'costco' },
|
|
64
|
+
{ pattern: /TRADER\s*JOE/i, store: 'trader_joes' },
|
|
65
|
+
{ pattern: /WHOLE\s*FOODS/i, store: 'whole_foods' },
|
|
66
|
+
{ pattern: /PUBLIX/i, store: 'publix' },
|
|
67
|
+
{ pattern: /ALDI/i, store: 'aldi' },
|
|
68
|
+
];
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Hook Implementation
|
|
71
|
+
// ============================================================================
|
|
72
|
+
/**
|
|
73
|
+
* Hook for receipt scanning and parsing
|
|
74
|
+
*/
|
|
75
|
+
function useReceiptScanner(options = {}) {
|
|
76
|
+
const { serverFallback = true, serverUrl, minConfidence = 0.7, onItemDetected, onParseComplete, onError, } = options;
|
|
77
|
+
// Context
|
|
78
|
+
const { serverClient, serverAvailable, config } = (0, MLVisionProvider_1.useMLVisionContext)();
|
|
79
|
+
// State
|
|
80
|
+
const [isProcessing, setIsProcessing] = (0, react_1.useState)(false);
|
|
81
|
+
const [items, setItems] = (0, react_1.useState)([]);
|
|
82
|
+
const [metadata, setMetadata] = (0, react_1.useState)(null);
|
|
83
|
+
const [rawText, setRawText] = (0, react_1.useState)('');
|
|
84
|
+
const [confidence, setConfidence] = (0, react_1.useState)(0);
|
|
85
|
+
const [source, setSource] = (0, react_1.useState)(null);
|
|
86
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
87
|
+
// Refs
|
|
88
|
+
const mountedRef = (0, react_1.useRef)(true);
|
|
89
|
+
// Computed server URL
|
|
90
|
+
const effectiveServerUrl = serverUrl || config.serverUrl;
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Receipt Parsing Logic
|
|
93
|
+
// ============================================================================
|
|
94
|
+
/**
|
|
95
|
+
* Parse raw OCR text into receipt items
|
|
96
|
+
*/
|
|
97
|
+
const parseReceiptText = (0, react_1.useCallback)((text) => {
|
|
98
|
+
const lines = text.split('\n').filter(line => line.trim());
|
|
99
|
+
const parsedLines = lines.map((line, index) => classifyLine(line, index));
|
|
100
|
+
// Extract metadata
|
|
101
|
+
const extractedMetadata = {};
|
|
102
|
+
// Find store name
|
|
103
|
+
for (const { pattern, store } of STORE_PATTERNS) {
|
|
104
|
+
if (pattern.test(text)) {
|
|
105
|
+
extractedMetadata.storeName = store.toUpperCase().replace('_', ' ');
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Find date and time
|
|
110
|
+
for (const line of lines.slice(0, 10)) {
|
|
111
|
+
for (const datePattern of DATE_PATTERNS) {
|
|
112
|
+
const match = line.match(datePattern);
|
|
113
|
+
if (match) {
|
|
114
|
+
try {
|
|
115
|
+
extractedMetadata.date = new Date(match[0]);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Invalid date
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const timeMatch = line.match(TIME_PATTERN);
|
|
124
|
+
if (timeMatch) {
|
|
125
|
+
extractedMetadata.time = timeMatch[0];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Find totals
|
|
129
|
+
for (const parsedLine of parsedLines) {
|
|
130
|
+
if (parsedLine.type === 'total' && parsedLine.values.price) {
|
|
131
|
+
extractedMetadata.total = parsedLine.values.price;
|
|
132
|
+
}
|
|
133
|
+
else if (parsedLine.type === 'subtotal' && parsedLine.values.price) {
|
|
134
|
+
extractedMetadata.subtotal = parsedLine.values.price;
|
|
135
|
+
}
|
|
136
|
+
else if (parsedLine.type === 'tax' && parsedLine.values.price) {
|
|
137
|
+
extractedMetadata.tax = parsedLine.values.price;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Extract items
|
|
141
|
+
const extractedItems = parsedLines
|
|
142
|
+
.filter(line => line.type === 'item')
|
|
143
|
+
.map(line => {
|
|
144
|
+
const item = {
|
|
145
|
+
name: line.values.name || line.text,
|
|
146
|
+
normalizedName: normalizeItemName(line.values.name || line.text),
|
|
147
|
+
quantity: line.values.quantity,
|
|
148
|
+
totalPrice: line.values.price,
|
|
149
|
+
confidence: line.confidence,
|
|
150
|
+
rawText: line.text,
|
|
151
|
+
lineNumber: line.lineNumber,
|
|
152
|
+
needsReview: line.confidence < minConfidence,
|
|
153
|
+
};
|
|
154
|
+
// Notify per item
|
|
155
|
+
onItemDetected?.(item);
|
|
156
|
+
return item;
|
|
157
|
+
});
|
|
158
|
+
return { items: extractedItems, metadata: extractedMetadata };
|
|
159
|
+
}, [minConfidence, onItemDetected]);
|
|
160
|
+
/**
|
|
161
|
+
* Classify a single receipt line
|
|
162
|
+
*/
|
|
163
|
+
function classifyLine(text, lineNumber) {
|
|
164
|
+
const trimmed = text.trim();
|
|
165
|
+
let type = 'unknown';
|
|
166
|
+
let lineConfidence = 0.5;
|
|
167
|
+
const values = {};
|
|
168
|
+
// Check for totals first
|
|
169
|
+
if (/^TOTAL/i.test(trimmed) || /GRAND\s*TOTAL/i.test(trimmed)) {
|
|
170
|
+
type = 'total';
|
|
171
|
+
lineConfidence = 0.95;
|
|
172
|
+
}
|
|
173
|
+
else if (/^SUBTOTAL/i.test(trimmed)) {
|
|
174
|
+
type = 'subtotal';
|
|
175
|
+
lineConfidence = 0.95;
|
|
176
|
+
}
|
|
177
|
+
else if (/^TAX/i.test(trimmed) || /SALES\s*TAX/i.test(trimmed)) {
|
|
178
|
+
type = 'tax';
|
|
179
|
+
lineConfidence = 0.95;
|
|
180
|
+
}
|
|
181
|
+
else if (/^(CASH|CREDIT|DEBIT|VISA|MASTERCARD|AMEX)/i.test(trimmed)) {
|
|
182
|
+
type = 'payment';
|
|
183
|
+
lineConfidence = 0.9;
|
|
184
|
+
}
|
|
185
|
+
else if (DATE_PATTERNS.some(p => p.test(trimmed))) {
|
|
186
|
+
type = 'date_time';
|
|
187
|
+
lineConfidence = 0.85;
|
|
188
|
+
}
|
|
189
|
+
else if (STORE_PATTERNS.some(({ pattern }) => pattern.test(trimmed))) {
|
|
190
|
+
type = 'store_info';
|
|
191
|
+
lineConfidence = 0.9;
|
|
192
|
+
}
|
|
193
|
+
else if (/^\d{10,20}$/.test(trimmed.replace(/[\s-]/g, ''))) {
|
|
194
|
+
type = 'transaction';
|
|
195
|
+
lineConfidence = 0.7;
|
|
196
|
+
}
|
|
197
|
+
else if (trimmed.length > 3) {
|
|
198
|
+
// Likely an item line
|
|
199
|
+
type = 'item';
|
|
200
|
+
// Extract price
|
|
201
|
+
const prices = trimmed.match(PRICE_PATTERN);
|
|
202
|
+
if (prices && prices.length > 0) {
|
|
203
|
+
const lastPrice = prices[prices.length - 1];
|
|
204
|
+
values.price = parseFloat(lastPrice.replace('$', ''));
|
|
205
|
+
lineConfidence = 0.8;
|
|
206
|
+
}
|
|
207
|
+
// Extract quantity
|
|
208
|
+
const qtyMatch = trimmed.match(QUANTITY_PATTERN);
|
|
209
|
+
if (qtyMatch) {
|
|
210
|
+
values.quantity = parseFloat(qtyMatch[1]);
|
|
211
|
+
}
|
|
212
|
+
// Extract name (text before price)
|
|
213
|
+
if (prices && prices.length > 0) {
|
|
214
|
+
const priceIndex = trimmed.indexOf(prices[0]);
|
|
215
|
+
if (priceIndex > 0) {
|
|
216
|
+
values.name = trimmed.substring(0, priceIndex).trim();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
values.name = trimmed;
|
|
221
|
+
lineConfidence = 0.6; // Lower confidence without price
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Extract price for non-item lines too
|
|
225
|
+
if (type !== 'item' && type !== 'unknown') {
|
|
226
|
+
const prices = trimmed.match(PRICE_PATTERN);
|
|
227
|
+
if (prices && prices.length > 0) {
|
|
228
|
+
const lastPrice = prices[prices.length - 1];
|
|
229
|
+
values.price = parseFloat(lastPrice.replace('$', ''));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { text: trimmed, type, values, lineNumber, confidence: lineConfidence };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Normalize item name for matching
|
|
236
|
+
*/
|
|
237
|
+
function normalizeItemName(name) {
|
|
238
|
+
return name
|
|
239
|
+
.toLowerCase()
|
|
240
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
241
|
+
.replace(/\s+/g, ' ')
|
|
242
|
+
.trim();
|
|
243
|
+
}
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// Actions
|
|
246
|
+
// ============================================================================
|
|
247
|
+
/**
|
|
248
|
+
* Scan a receipt photo
|
|
249
|
+
*/
|
|
250
|
+
const scanReceipt = (0, react_1.useCallback)(async (uri) => {
|
|
251
|
+
setIsProcessing(true);
|
|
252
|
+
setError(null);
|
|
253
|
+
const startTime = Date.now();
|
|
254
|
+
try {
|
|
255
|
+
let ocrText = '';
|
|
256
|
+
let processSource = 'on_device';
|
|
257
|
+
// Try server OCR first if available (better accuracy for receipts)
|
|
258
|
+
if (serverFallback &&
|
|
259
|
+
effectiveServerUrl &&
|
|
260
|
+
serverClient &&
|
|
261
|
+
serverAvailable) {
|
|
262
|
+
try {
|
|
263
|
+
// Note: Server OCR endpoint would return text
|
|
264
|
+
// For now, simulate with placeholder
|
|
265
|
+
console.log('[useReceiptScanner] Server OCR not yet implemented');
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
console.log('[useReceiptScanner] Server OCR failed, using on-device');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Use on-device OCR with MLKit
|
|
272
|
+
if (!ocrText) {
|
|
273
|
+
console.log('[useReceiptScanner] Running MLKit text recognition...');
|
|
274
|
+
try {
|
|
275
|
+
const result = await text_recognition_1.default.recognize(uri);
|
|
276
|
+
ocrText = result.text;
|
|
277
|
+
console.log(`[useReceiptScanner] MLKit recognized ${result.blocks.length} blocks, ${ocrText.length} chars`);
|
|
278
|
+
processSource = 'on_device';
|
|
279
|
+
}
|
|
280
|
+
catch (ocrErr) {
|
|
281
|
+
console.error('[useReceiptScanner] MLKit OCR error:', ocrErr);
|
|
282
|
+
throw new Error(`OCR failed: ${ocrErr instanceof Error ? ocrErr.message : 'Unknown error'}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Parse the OCR text
|
|
286
|
+
const { items: parsedItems, metadata: parsedMetadata } = parseReceiptText(ocrText);
|
|
287
|
+
const processingTimeMs = Date.now() - startTime;
|
|
288
|
+
const overallConfidence = parsedItems.length > 0
|
|
289
|
+
? parsedItems.reduce((sum, item) => sum + item.confidence, 0) /
|
|
290
|
+
parsedItems.length
|
|
291
|
+
: 0;
|
|
292
|
+
// Build result
|
|
293
|
+
const result = {
|
|
294
|
+
id: `receipt_${Date.now()}_${Math.random()
|
|
295
|
+
.toString(36)
|
|
296
|
+
.substr(2, 9)}`,
|
|
297
|
+
type: 'receipt',
|
|
298
|
+
items: parsedItems,
|
|
299
|
+
metadata: parsedMetadata,
|
|
300
|
+
rawText: ocrText,
|
|
301
|
+
textLines: [], // Would be populated by MLKit
|
|
302
|
+
confidence: overallConfidence,
|
|
303
|
+
itemsNeedingReview: parsedItems.filter(i => i.needsReview).length,
|
|
304
|
+
source: processSource,
|
|
305
|
+
processingTimeMs,
|
|
306
|
+
timestamp: Date.now(),
|
|
307
|
+
};
|
|
308
|
+
// Update state
|
|
309
|
+
if (mountedRef.current) {
|
|
310
|
+
setItems(parsedItems);
|
|
311
|
+
setMetadata(parsedMetadata);
|
|
312
|
+
setRawText(ocrText);
|
|
313
|
+
setConfidence(overallConfidence);
|
|
314
|
+
setSource(processSource);
|
|
315
|
+
}
|
|
316
|
+
// Callback
|
|
317
|
+
onParseComplete?.(result);
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
const scanError = err instanceof Error ? err : new Error('Receipt scan failed');
|
|
322
|
+
console.error('[useReceiptScanner] Error:', scanError);
|
|
323
|
+
if (mountedRef.current) {
|
|
324
|
+
setError(scanError);
|
|
325
|
+
}
|
|
326
|
+
onError?.(scanError);
|
|
327
|
+
throw scanError;
|
|
328
|
+
}
|
|
329
|
+
finally {
|
|
330
|
+
if (mountedRef.current) {
|
|
331
|
+
setIsProcessing(false);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}, [
|
|
335
|
+
serverFallback,
|
|
336
|
+
effectiveServerUrl,
|
|
337
|
+
serverClient,
|
|
338
|
+
serverAvailable,
|
|
339
|
+
parseReceiptText,
|
|
340
|
+
onParseComplete,
|
|
341
|
+
onError,
|
|
342
|
+
]);
|
|
343
|
+
/**
|
|
344
|
+
* Scan current camera frame
|
|
345
|
+
*/
|
|
346
|
+
const scanFrame = (0, react_1.useCallback)(async () => {
|
|
347
|
+
// This would capture current frame and scan
|
|
348
|
+
// Requires VisionCamera integration
|
|
349
|
+
throw new Error('Frame scanning not implemented - use scanReceipt with photo URI');
|
|
350
|
+
}, []);
|
|
351
|
+
/**
|
|
352
|
+
* Confirm an item is correct
|
|
353
|
+
*/
|
|
354
|
+
const confirmItem = (0, react_1.useCallback)((item) => {
|
|
355
|
+
setItems(prev => prev.map(i => i.lineNumber === item.lineNumber ? { ...i, needsReview: false } : i));
|
|
356
|
+
}, []);
|
|
357
|
+
/**
|
|
358
|
+
* Reject/remove an item
|
|
359
|
+
*/
|
|
360
|
+
const rejectItem = (0, react_1.useCallback)((item) => {
|
|
361
|
+
setItems(prev => prev.filter(i => i.lineNumber !== item.lineNumber));
|
|
362
|
+
}, []);
|
|
363
|
+
/**
|
|
364
|
+
* Edit an item
|
|
365
|
+
*/
|
|
366
|
+
const editItem = (0, react_1.useCallback)((item, updates) => {
|
|
367
|
+
setItems(prev => prev.map(i => i.lineNumber === item.lineNumber
|
|
368
|
+
? { ...i, ...updates, needsReview: false }
|
|
369
|
+
: i));
|
|
370
|
+
}, []);
|
|
371
|
+
/**
|
|
372
|
+
* Clear all results
|
|
373
|
+
*/
|
|
374
|
+
const clearResults = (0, react_1.useCallback)(() => {
|
|
375
|
+
setItems([]);
|
|
376
|
+
setMetadata(null);
|
|
377
|
+
setRawText('');
|
|
378
|
+
setConfidence(0);
|
|
379
|
+
setSource(null);
|
|
380
|
+
setError(null);
|
|
381
|
+
}, []);
|
|
382
|
+
// ============================================================================
|
|
383
|
+
// Return
|
|
384
|
+
// ============================================================================
|
|
385
|
+
return {
|
|
386
|
+
// State
|
|
387
|
+
isProcessing,
|
|
388
|
+
items,
|
|
389
|
+
metadata,
|
|
390
|
+
rawText,
|
|
391
|
+
confidence,
|
|
392
|
+
source,
|
|
393
|
+
error,
|
|
394
|
+
// Actions
|
|
395
|
+
scanReceipt,
|
|
396
|
+
scanFrame,
|
|
397
|
+
confirmItem,
|
|
398
|
+
rejectItem,
|
|
399
|
+
editItem,
|
|
400
|
+
clearResults,
|
|
401
|
+
// Frame processor (placeholder)
|
|
402
|
+
frameProcessor: null,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
exports.default = useReceiptScanner;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useVideoScanner Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for real-time video scanning combining product detection,
|
|
5
|
+
* barcode scanning, and text recognition.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { useVideoScanner } from '@souschef/ml-vision';
|
|
10
|
+
*
|
|
11
|
+
* function LiveScanner() {
|
|
12
|
+
* const {
|
|
13
|
+
* isScanning,
|
|
14
|
+
* products,
|
|
15
|
+
* barcodes,
|
|
16
|
+
* startScanning,
|
|
17
|
+
* stopScanning,
|
|
18
|
+
* cameraRef,
|
|
19
|
+
* } = useVideoScanner({
|
|
20
|
+
* modes: ['products', 'barcodes'],
|
|
21
|
+
* onDetected: (results) => console.log('Found:', results),
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* return (
|
|
25
|
+
* <Camera ref={cameraRef} />
|
|
26
|
+
* );
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
import type { TensorflowModel } from 'react-native-fast-tflite';
|
|
31
|
+
import type { Camera, Frame } from 'react-native-vision-camera';
|
|
32
|
+
import type { ProductDetectionResult, BarcodeDetectionResult, ProductCategory } from '../types';
|
|
33
|
+
/**
|
|
34
|
+
* Scanning mode
|
|
35
|
+
*/
|
|
36
|
+
export type ScanMode = 'products' | 'barcodes' | 'text';
|
|
37
|
+
/**
|
|
38
|
+
* Combined detection result
|
|
39
|
+
*/
|
|
40
|
+
export interface VideoScanResult {
|
|
41
|
+
products: ProductDetectionResult[];
|
|
42
|
+
barcodes: BarcodeDetectionResult[];
|
|
43
|
+
timestamp: number;
|
|
44
|
+
frameNumber: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Options for useVideoScanner hook
|
|
48
|
+
*/
|
|
49
|
+
export interface UseVideoScannerOptions {
|
|
50
|
+
/** Scanning modes to enable (default: ['products', 'barcodes']) */
|
|
51
|
+
modes?: ScanMode[];
|
|
52
|
+
/** Frame rate for processing (default: 10 fps) */
|
|
53
|
+
targetFps?: number;
|
|
54
|
+
/** Minimum confidence for product detection (default: 0.5) */
|
|
55
|
+
minProductConfidence?: number;
|
|
56
|
+
/** Product categories to detect */
|
|
57
|
+
productCategories?: ProductCategory[];
|
|
58
|
+
/** Maximum products per frame (default: 10) */
|
|
59
|
+
maxProducts?: number;
|
|
60
|
+
/** Maximum barcodes per frame (default: 5) */
|
|
61
|
+
maxBarcodes?: number;
|
|
62
|
+
/** Enable temporal smoothing (default: true) */
|
|
63
|
+
temporalSmoothing?: boolean;
|
|
64
|
+
/** Number of frames for smoothing (default: 3) */
|
|
65
|
+
smoothingFrames?: number;
|
|
66
|
+
/** Callback when items are detected */
|
|
67
|
+
onDetected?: (results: VideoScanResult) => void;
|
|
68
|
+
/** Callback on error */
|
|
69
|
+
onError?: (error: Error) => void;
|
|
70
|
+
/** Enable haptic feedback (default: true) */
|
|
71
|
+
hapticFeedback?: boolean;
|
|
72
|
+
/** Loaded TFLite model for on-device frame processing */
|
|
73
|
+
tfliteModel?: TensorflowModel | null;
|
|
74
|
+
/** Use on-device TFLite frame processor instead of server (default: true if model provided) */
|
|
75
|
+
useOnDeviceFrameProcessor?: boolean;
|
|
76
|
+
/** Only detect food classes when using COCO model (default: true) */
|
|
77
|
+
foodOnly?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Return type for useVideoScanner hook
|
|
81
|
+
*/
|
|
82
|
+
export interface UseVideoScannerReturn {
|
|
83
|
+
/** Whether scanning is active */
|
|
84
|
+
isScanning: boolean;
|
|
85
|
+
/** Whether all models are loaded */
|
|
86
|
+
isReady: boolean;
|
|
87
|
+
/** Detected products (smoothed) */
|
|
88
|
+
products: ProductDetectionResult[];
|
|
89
|
+
/** Detected barcodes */
|
|
90
|
+
barcodes: BarcodeDetectionResult[];
|
|
91
|
+
/** Current frame number */
|
|
92
|
+
frameNumber: number;
|
|
93
|
+
/** Average FPS */
|
|
94
|
+
fps: number;
|
|
95
|
+
/** Current error if any */
|
|
96
|
+
error: Error | null;
|
|
97
|
+
/** Last inference time in milliseconds */
|
|
98
|
+
inferenceTimeMs: number;
|
|
99
|
+
/** Start video scanning */
|
|
100
|
+
startScanning: () => void;
|
|
101
|
+
/** Stop video scanning */
|
|
102
|
+
stopScanning: () => void;
|
|
103
|
+
/** Toggle scanning */
|
|
104
|
+
toggleScanning: () => void;
|
|
105
|
+
/** Clear all detections */
|
|
106
|
+
clearDetections: () => void;
|
|
107
|
+
/** Capture current frame as photo */
|
|
108
|
+
captureFrame: () => Promise<string | null>;
|
|
109
|
+
cameraRef: React.RefObject<Camera | null>;
|
|
110
|
+
frameProcessorConfig: unknown;
|
|
111
|
+
/** Frame processor function for use with VisionCamera (if using TFLite) */
|
|
112
|
+
frameProcessor: ((frame: Frame) => void) | undefined;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Hook for real-time video scanning
|
|
116
|
+
*/
|
|
117
|
+
export declare function useVideoScanner(options?: UseVideoScannerOptions): UseVideoScannerReturn;
|
|
118
|
+
export default useVideoScanner;
|