@visns-studio/visns-components 5.10.11 → 5.11.2
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 +85 -0
- package/package.json +7 -7
- package/src/components/cms/DataGrid.jsx +8 -8
- package/src/components/cms/DropZone.jsx +1 -1
- package/src/components/cms/Form.jsx +1 -1
- package/src/components/cms/Gallery.jsx +1 -1
- package/src/components/cms/sorting/Item.jsx +1 -1
- package/src/components/crm/Autocomplete.jsx +3 -3
- package/src/components/crm/Breadcrumb.jsx +4 -4
- package/src/components/crm/BusinessCardOcr.jsx +681 -95
- package/src/components/crm/DataGrid.jsx +34 -32
- package/src/components/crm/Field.jsx +12 -12
- package/src/components/crm/Form.jsx +2 -2
- package/src/components/crm/Navigation.jsx +11 -11
- package/src/components/crm/Notification.jsx +2 -2
- package/src/components/crm/QuickAction.jsx +3 -3
- package/src/components/crm/SectionHeader.jsx +1 -1
- package/src/components/crm/SwitchAccount.jsx +4 -4
- package/src/components/crm/auth/ClientLogin.jsx +2 -2
- package/src/components/crm/auth/ClientOTPVerify.jsx +2 -2
- package/src/components/crm/auth/Login.jsx +3 -3
- package/src/components/crm/auth/Reset.jsx +2 -2
- package/src/components/crm/auth/TwoFactorAuth.jsx +2 -2
- package/src/components/crm/columns/ColumnRenderers.jsx +320 -259
- package/src/components/crm/controls/DataGridSearch.jsx +5 -5
- package/src/components/crm/generic/AlternativeActionModal.jsx +100 -0
- package/src/components/crm/generic/ContactSelectorModal.jsx +423 -0
- package/src/components/crm/generic/DatePickerDialog.jsx +302 -0
- package/src/components/crm/generic/DateRangeSelectorModal.jsx +181 -0
- package/src/components/crm/generic/GenericClientPortal.jsx +1 -1
- package/src/components/crm/generic/GenericDashboard.jsx +4 -4
- package/src/components/crm/generic/GenericDetail.jsx +6 -6
- package/src/components/crm/generic/GenericDynamic.jsx +3 -3
- package/src/components/crm/generic/GenericEditableTable.jsx +4 -4
- package/src/components/crm/generic/GenericFormBuilder.jsx +6 -6
- package/src/components/crm/generic/GenericGrid.jsx +2888 -0
- package/src/components/crm/generic/GenericIndex.jsx +5 -1255
- package/src/components/crm/generic/GenericReport.jsx +966 -646
- package/src/components/crm/generic/GenericReportForm.jsx +194 -0
- package/src/components/crm/generic/NotificationList.jsx +1 -1
- package/src/components/crm/generic/ReasonCollectorModal.jsx +194 -0
- package/src/components/crm/generic/SortableQuoteItems.jsx +3 -3
- package/src/components/crm/generic/shared/formatters.js +231 -0
- package/src/components/crm/generic/shared/gridUtils.js +194 -0
- package/src/components/crm/generic/styles/AlternativeActionModal.css +127 -0
- package/src/components/crm/generic/styles/ContactSelectorModal.css +367 -0
- package/src/components/crm/generic/styles/DatePickerDialog.css +84 -0
- package/src/components/crm/generic/styles/DateRangeSelectorModal.css +115 -0
- package/src/components/crm/generic/styles/GenericIndex.module.scss +382 -0
- package/src/components/crm/generic/styles/GenericReport.module.scss +96 -0
- package/src/components/crm/generic/styles/ReasonCollectorModal.css +217 -0
- package/src/components/crm/modals/GalleryModal.jsx +2 -2
- package/src/components/crm/sorting/Item.jsx +3 -3
- package/src/components/crm/styles/BusinessCardOcr.module.scss +197 -4
- package/src/index.js +4 -0
|
@@ -6,16 +6,19 @@ import { toast } from 'react-toastify';
|
|
|
6
6
|
import Tesseract from 'tesseract.js';
|
|
7
7
|
import {
|
|
8
8
|
Camera,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
ImagePlus,
|
|
10
|
+
X,
|
|
11
|
+
SwitchCamera,
|
|
12
|
+
CheckCircle,
|
|
13
|
+
RotateCcw,
|
|
14
|
+
Mail,
|
|
15
|
+
Globe,
|
|
16
|
+
Building2,
|
|
17
|
+
User,
|
|
18
|
+
Zap,
|
|
19
|
+
ZapOff,
|
|
20
|
+
Focus,
|
|
21
|
+
} from 'lucide-react';
|
|
19
22
|
|
|
20
23
|
import CustomFetch from './Fetch';
|
|
21
24
|
import ocrStyles from './styles/BusinessCardOcr.module.scss';
|
|
@@ -34,6 +37,9 @@ const BusinessCardOcr = ({
|
|
|
34
37
|
const [cameraFacing, setCameraFacing] = useState('environment'); // environment (back), user (front)
|
|
35
38
|
const [availableCameras, setAvailableCameras] = useState([]);
|
|
36
39
|
const [currentCameraIndex, setCurrentCameraIndex] = useState(0);
|
|
40
|
+
const [flashEnabled, setFlashEnabled] = useState(false);
|
|
41
|
+
const [isFocusing, setIsFocusing] = useState(false);
|
|
42
|
+
const [cameraReady, setCameraReady] = useState(false);
|
|
37
43
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
38
44
|
const [extractedData, setExtractedData] = useState({});
|
|
39
45
|
const [ocrProgress, setOcrProgress] = useState(0);
|
|
@@ -112,9 +118,10 @@ const BusinessCardOcr = ({
|
|
|
112
118
|
|
|
113
119
|
const initializeCamera = async () => {
|
|
114
120
|
try {
|
|
121
|
+
setCameraReady(false);
|
|
115
122
|
let constraints;
|
|
116
123
|
|
|
117
|
-
//
|
|
124
|
+
// Enhanced camera constraints with focus and quality
|
|
118
125
|
if (
|
|
119
126
|
availableCameras.length > 0 &&
|
|
120
127
|
availableCameras[currentCameraIndex]
|
|
@@ -125,17 +132,25 @@ const BusinessCardOcr = ({
|
|
|
125
132
|
exact: availableCameras[currentCameraIndex]
|
|
126
133
|
.deviceId,
|
|
127
134
|
},
|
|
128
|
-
width: {
|
|
129
|
-
height: {
|
|
135
|
+
width: { min: 1280, ideal: 1920, max: 3840 },
|
|
136
|
+
height: { min: 720, ideal: 1080, max: 2160 },
|
|
137
|
+
focusMode: ['continuous', 'auto'],
|
|
138
|
+
exposureMode: 'auto',
|
|
139
|
+
whiteBalanceMode: 'auto',
|
|
140
|
+
torch: flashEnabled
|
|
130
141
|
},
|
|
131
142
|
};
|
|
132
143
|
} else {
|
|
133
|
-
// Fallback to facing mode
|
|
144
|
+
// Fallback to facing mode with enhanced settings
|
|
134
145
|
constraints = {
|
|
135
146
|
video: {
|
|
136
147
|
facingMode: cameraFacing,
|
|
137
|
-
width: {
|
|
138
|
-
height: {
|
|
148
|
+
width: { min: 1280, ideal: 1920, max: 3840 },
|
|
149
|
+
height: { min: 720, ideal: 1080, max: 2160 },
|
|
150
|
+
focusMode: ['continuous', 'auto'],
|
|
151
|
+
exposureMode: 'auto',
|
|
152
|
+
whiteBalanceMode: 'auto',
|
|
153
|
+
torch: flashEnabled
|
|
139
154
|
},
|
|
140
155
|
};
|
|
141
156
|
}
|
|
@@ -147,6 +162,9 @@ const BusinessCardOcr = ({
|
|
|
147
162
|
|
|
148
163
|
if (videoRef.current) {
|
|
149
164
|
videoRef.current.srcObject = stream;
|
|
165
|
+
videoRef.current.onloadedmetadata = () => {
|
|
166
|
+
setCameraReady(true);
|
|
167
|
+
};
|
|
150
168
|
}
|
|
151
169
|
} catch (error) {
|
|
152
170
|
console.error('Error accessing camera:', error);
|
|
@@ -161,9 +179,80 @@ const BusinessCardOcr = ({
|
|
|
161
179
|
}
|
|
162
180
|
};
|
|
163
181
|
|
|
164
|
-
|
|
182
|
+
// Autofocus trigger before capture
|
|
183
|
+
const triggerAutofocus = async () => {
|
|
184
|
+
if (!cameraStream) return;
|
|
185
|
+
|
|
186
|
+
setIsFocusing(true);
|
|
187
|
+
try {
|
|
188
|
+
const track = cameraStream.getVideoTracks()[0];
|
|
189
|
+
const capabilities = track.getCapabilities();
|
|
190
|
+
|
|
191
|
+
if (capabilities.focusMode && capabilities.focusMode.includes('auto')) {
|
|
192
|
+
await track.applyConstraints({
|
|
193
|
+
advanced: [{ focusMode: 'auto' }]
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Wait for focus to settle
|
|
197
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.warn('Autofocus failed:', error);
|
|
201
|
+
} finally {
|
|
202
|
+
setIsFocusing(false);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Image preprocessing pipeline
|
|
207
|
+
const preprocessImage = async (imageBlob) => {
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
const canvas = document.createElement('canvas');
|
|
210
|
+
const ctx = canvas.getContext('2d');
|
|
211
|
+
const img = new Image();
|
|
212
|
+
|
|
213
|
+
img.onload = () => {
|
|
214
|
+
// Step 1: Optimal sizing (2400px width max for OCR)
|
|
215
|
+
const maxWidth = 2400;
|
|
216
|
+
const scale = Math.min(maxWidth / img.width, maxWidth / img.height, 1);
|
|
217
|
+
canvas.width = img.width * scale;
|
|
218
|
+
canvas.height = img.height * scale;
|
|
219
|
+
|
|
220
|
+
// Step 2: Draw with anti-aliasing disabled for sharp text
|
|
221
|
+
ctx.imageSmoothingEnabled = false;
|
|
222
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
223
|
+
|
|
224
|
+
// Step 3: Enhance contrast and brightness
|
|
225
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
226
|
+
const data = imageData.data;
|
|
227
|
+
|
|
228
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
229
|
+
// Increase contrast (1.4x) and brightness (+20) for better OCR
|
|
230
|
+
data[i] = Math.min(255, Math.max(0, data[i] * 1.4 + 20)); // Red
|
|
231
|
+
data[i + 1] = Math.min(255, Math.max(0, data[i + 1] * 1.4 + 20)); // Green
|
|
232
|
+
data[i + 2] = Math.min(255, Math.max(0, data[i + 2] * 1.4 + 20)); // Blue
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
ctx.putImageData(imageData, 0, 0);
|
|
236
|
+
|
|
237
|
+
// Step 4: Convert to high-quality PNG for OCR
|
|
238
|
+
canvas.toBlob(resolve, 'image/png', 1.0);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
img.onerror = () => {
|
|
242
|
+
console.warn('Image preprocessing failed, using original');
|
|
243
|
+
resolve(imageBlob);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
img.src = URL.createObjectURL(imageBlob);
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const capturePhoto = useCallback(async () => {
|
|
165
251
|
if (!videoRef.current || !canvasRef.current) return;
|
|
166
252
|
|
|
253
|
+
// Trigger autofocus before capture
|
|
254
|
+
await triggerAutofocus();
|
|
255
|
+
|
|
167
256
|
const video = videoRef.current;
|
|
168
257
|
const canvas = canvasRef.current;
|
|
169
258
|
const context = canvas.getContext('2d');
|
|
@@ -173,15 +262,17 @@ const BusinessCardOcr = ({
|
|
|
173
262
|
context.drawImage(video, 0, 0);
|
|
174
263
|
|
|
175
264
|
canvas.toBlob(
|
|
176
|
-
(blob) => {
|
|
177
|
-
|
|
265
|
+
async (blob) => {
|
|
266
|
+
// Preprocess image for better OCR
|
|
267
|
+
const processedBlob = await preprocessImage(blob);
|
|
268
|
+
setCapturedImage(processedBlob);
|
|
178
269
|
setCurrentStep('preview');
|
|
179
270
|
stopCamera();
|
|
180
271
|
},
|
|
181
272
|
'image/jpeg',
|
|
182
|
-
0.
|
|
273
|
+
0.95
|
|
183
274
|
);
|
|
184
|
-
}, []);
|
|
275
|
+
}, [cameraStream]);
|
|
185
276
|
|
|
186
277
|
const switchCamera = () => {
|
|
187
278
|
if (availableCameras.length > 1) {
|
|
@@ -196,6 +287,28 @@ const BusinessCardOcr = ({
|
|
|
196
287
|
}
|
|
197
288
|
};
|
|
198
289
|
|
|
290
|
+
// Toggle flash/torch
|
|
291
|
+
const toggleFlash = async () => {
|
|
292
|
+
if (!cameraStream) return;
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const track = cameraStream.getVideoTracks()[0];
|
|
296
|
+
const capabilities = track.getCapabilities();
|
|
297
|
+
|
|
298
|
+
if (capabilities.torch) {
|
|
299
|
+
await track.applyConstraints({
|
|
300
|
+
advanced: [{ torch: !flashEnabled }]
|
|
301
|
+
});
|
|
302
|
+
setFlashEnabled(!flashEnabled);
|
|
303
|
+
} else {
|
|
304
|
+
toast.warn('Flash not supported on this camera');
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.warn('Flash toggle failed:', error);
|
|
308
|
+
toast.warn('Unable to control flash');
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
199
312
|
const retakePhoto = () => {
|
|
200
313
|
setCapturedImage(null);
|
|
201
314
|
setCurrentStep('camera');
|
|
@@ -254,6 +367,55 @@ const BusinessCardOcr = ({
|
|
|
254
367
|
event.target.value = '';
|
|
255
368
|
};
|
|
256
369
|
|
|
370
|
+
// Advanced OCR with optimized configuration
|
|
371
|
+
const performAdvancedOCR = async (imageBlob) => {
|
|
372
|
+
const ocrConfig = {
|
|
373
|
+
logger: (m) => {
|
|
374
|
+
if (m.status === 'recognizing text') {
|
|
375
|
+
const progress = Math.round(m.progress * 100);
|
|
376
|
+
setOcrProgress(progress);
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
options: {
|
|
380
|
+
tessedit_pageseg_mode: '6', // Single uniform block
|
|
381
|
+
tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghklmnopqrstuvwxyz0123456789@.-_+()/ ',
|
|
382
|
+
tessedit_ocr_engine_mode: '1', // Neural nets LSTM engine
|
|
383
|
+
preserve_interword_spaces: '1',
|
|
384
|
+
tessjs_create_pdf: '0',
|
|
385
|
+
tessjs_create_hocr: '0'
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
// Pass 1: Standard recognition (60% of progress)
|
|
391
|
+
setOcrProgress(10);
|
|
392
|
+
const result1 = await Tesseract.recognize(imageBlob, 'eng', ocrConfig);
|
|
393
|
+
setOcrProgress(60);
|
|
394
|
+
|
|
395
|
+
// Pass 2: Alternative segmentation for stubborn text (40% of progress)
|
|
396
|
+
const altConfig = {
|
|
397
|
+
...ocrConfig,
|
|
398
|
+
options: { ...ocrConfig.options, tessedit_pageseg_mode: '8' } // Single word
|
|
399
|
+
};
|
|
400
|
+
const result2 = await Tesseract.recognize(imageBlob, 'eng', altConfig);
|
|
401
|
+
setOcrProgress(100);
|
|
402
|
+
|
|
403
|
+
// Combine results - prefer the longer, more complete text
|
|
404
|
+
const text1 = result1.data.text.trim();
|
|
405
|
+
const text2 = result2.data.text.trim();
|
|
406
|
+
const finalText = text1.length > text2.length ? text1 : text2;
|
|
407
|
+
|
|
408
|
+
console.log('OCR Result 1:', text1);
|
|
409
|
+
console.log('OCR Result 2:', text2);
|
|
410
|
+
console.log('Final Text:', finalText);
|
|
411
|
+
|
|
412
|
+
return finalText;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
console.error('Advanced OCR failed:', error);
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
257
419
|
const processBusinessCard = async () => {
|
|
258
420
|
if (!capturedImage) return;
|
|
259
421
|
|
|
@@ -262,17 +424,8 @@ const BusinessCardOcr = ({
|
|
|
262
424
|
setOcrProgress(0);
|
|
263
425
|
|
|
264
426
|
try {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
if (m.status === 'recognizing text') {
|
|
268
|
-
const progress = Math.round(m.progress * 100);
|
|
269
|
-
setOcrProgress(progress);
|
|
270
|
-
}
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const text = result.data.text;
|
|
275
|
-
const parsedData = parseExtractedText(text);
|
|
427
|
+
const text = await performAdvancedOCR(capturedImage);
|
|
428
|
+
const parsedData = intelligentFieldMapping(text);
|
|
276
429
|
setExtractedData({ ...parsedData, rawText: text });
|
|
277
430
|
setCurrentStep('results');
|
|
278
431
|
toast.success('Business card processed successfully!');
|
|
@@ -348,10 +501,10 @@ const BusinessCardOcr = ({
|
|
|
348
501
|
const spaceRatio = spaces / total;
|
|
349
502
|
|
|
350
503
|
// Skip if mostly symbols
|
|
351
|
-
if (symbolRatio > 0.
|
|
504
|
+
if (symbolRatio > 0.4) return true;
|
|
352
505
|
|
|
353
506
|
// Skip if too many consecutive spaces or weird spacing
|
|
354
|
-
if (spaceRatio > 0.
|
|
507
|
+
if (spaceRatio > 0.5) return true;
|
|
355
508
|
|
|
356
509
|
// Skip if it's all numbers or mostly numbers (unless it looks like a phone)
|
|
357
510
|
if (
|
|
@@ -361,10 +514,34 @@ const BusinessCardOcr = ({
|
|
|
361
514
|
)
|
|
362
515
|
return true;
|
|
363
516
|
|
|
364
|
-
//
|
|
365
|
-
const formatArtifacts =
|
|
366
|
-
/^[.\-_
|
|
367
|
-
|
|
517
|
+
// Enhanced formatting artifacts detection
|
|
518
|
+
const formatArtifacts = [
|
|
519
|
+
/^[.\-_|>]+$/, // Lines of just punctuation
|
|
520
|
+
/^[^a-zA-Z]*[|>]+[^a-zA-Z]*$/, // Lines with pipes/arrows mixed with symbols
|
|
521
|
+
/^\s*[.]\s*[A-Z]?\s*$/, // Dots with random letters
|
|
522
|
+
/^[A-Z]\s*[>|]\s*[|]*\s*[A-Za-z\.]*\s*[|]*$/, // Pattern like "I | I.egal"
|
|
523
|
+
/^[)>|.\s]{3,}$/, // Lines of mixed symbols
|
|
524
|
+
/^[a-zA-Z]\s*[)>|.\s]+$/, // Single letter followed by symbols
|
|
525
|
+
/^[\s\d§.\-_]+$/, // Lines of numbers and symbols only
|
|
526
|
+
/^[EF]{2,}[.\s]*$/, // Repeated E or F letters (common OCR errors)
|
|
527
|
+
// New patterns for the current example
|
|
528
|
+
/^[A-Z\s]{4,}[R\s]{2,}.*$/, // "HEIR A NR RS RR rn ORRIN"
|
|
529
|
+
/^[A-Z][A-Z\s]{3,}[A-Z].*$/, // All caps with spaces
|
|
530
|
+
/^[—\-]\s*[A-Za-z]{1,3}\s*[§].*$/, // "— Cl § ae"
|
|
531
|
+
/^[&§¢]+.*$/, // Lines starting with symbols
|
|
532
|
+
/^[,\-'\(¢¢]+.*$/, // Lines starting with punctuation
|
|
533
|
+
/^\d+\s*[A-Za-z\s]{1,10}\s*[¢]+.*$/, // "94 Level 3 16 Irwin S ¢"
|
|
534
|
+
/^[a-z]{1,4}\s*[A-Za-z]\s*[b]\s*[A-Za-z]\s*[b][ef]\s*[fs].*$/, // "dose I b E be fs"
|
|
535
|
+
/^\d+\s*[i]\s*[A-Za-z]{1,3}\s*\d+\s*[A-Za-z]{1,3}$/, // "3 i Ee 4 Le"
|
|
536
|
+
/^[x]\s*[A-Za-z]{1,3}\s*[A-Z]{1,3}\s*[&]+.*$/, // "x Ei FY &"
|
|
537
|
+
/^[A-Z]{3,}.*[R]{2,}.*$/, // Multiple caps with repeated R's
|
|
538
|
+
/.*[die]$/, // Lines ending with "die"
|
|
539
|
+
/^[ORRIN]{3,}.*$/, // Lines with repeated caps like ORRIN
|
|
540
|
+
];
|
|
541
|
+
|
|
542
|
+
for (const pattern of formatArtifacts) {
|
|
543
|
+
if (pattern.test(trimmed)) return true;
|
|
544
|
+
}
|
|
368
545
|
|
|
369
546
|
// Skip if it has random character combinations
|
|
370
547
|
const randomPattern =
|
|
@@ -394,7 +571,7 @@ const BusinessCardOcr = ({
|
|
|
394
571
|
|
|
395
572
|
// Check for excessive symbol contamination in words
|
|
396
573
|
const symbolsInWord = (word.match(/[^a-zA-Z0-9]/g) || []).length;
|
|
397
|
-
if (symbolsInWord / word.length > 0.
|
|
574
|
+
if (symbolsInWord / word.length > 0.4) {
|
|
398
575
|
gibberishWords++;
|
|
399
576
|
continue;
|
|
400
577
|
}
|
|
@@ -427,22 +604,33 @@ const BusinessCardOcr = ({
|
|
|
427
604
|
}
|
|
428
605
|
}
|
|
429
606
|
|
|
430
|
-
// If more than
|
|
607
|
+
// If more than 50% of words are gibberish or mixed case, consider it gibberish
|
|
431
608
|
const gibberishRatio = gibberishWords / Math.max(totalWords, 1);
|
|
432
609
|
const mixedCaseRatio = mixedCaseWords / Math.max(totalWords, 1);
|
|
433
610
|
|
|
434
|
-
if (gibberishRatio > 0.
|
|
435
|
-
if (mixedCaseRatio > 0.
|
|
611
|
+
if (gibberishRatio > 0.5) return true;
|
|
612
|
+
if (mixedCaseRatio > 0.4 && totalWords > 2) return true;
|
|
436
613
|
|
|
437
|
-
// Check for specific gibberish patterns
|
|
614
|
+
// Check for specific gibberish patterns from your example
|
|
438
615
|
const gibberishPatterns = [
|
|
439
|
-
// Patterns like "
|
|
616
|
+
// Patterns like "> | I.egal Practitioner Director |"
|
|
617
|
+
/^[>|]\s*[|]\s*[I]\.[a-zA-Z]+/,
|
|
618
|
+
// Patterns like "urae (08) 9220 4402" (partial words)
|
|
619
|
+
/^[a-z]{2,4}\s*\(/,
|
|
620
|
+
// Patterns like ") . 3 dh § 8" (symbols with scattered letters/numbers)
|
|
621
|
+
/^[)].+[§].+$/,
|
|
622
|
+
// Patterns like "» 3 "e |" (mixed symbols)
|
|
623
|
+
/^[»].+["e].+[|]/,
|
|
624
|
+
// Patterns like "oy Level" (partial words)
|
|
625
|
+
/^[a-z]{1,2}\s+[A-Z][a-z]+/,
|
|
626
|
+
// Patterns like "EE. Se" (repeated letters)
|
|
627
|
+
/^[A-Z]{2,}[.]\s*[A-Z][a-z]$/,
|
|
628
|
+
// Patterns like "fio) i ) L\" (mixed parentheses and letters)
|
|
629
|
+
/^[a-z]{2,3}[)].*[)].*[L\\]/,
|
|
630
|
+
// Original patterns
|
|
440
631
|
/^[:;]\s*[A-Za-z]{1,3}\s*[:;]\s*[A-Za-z]{1,4}\s+[a-z]{1,2}\s+[a-zA-Z]{1,2}$/,
|
|
441
|
-
// Patterns like "HE — SE TL Beant 2 Es Re Se"
|
|
442
632
|
/^[A-Z]{1,3}\s*[—\-]\s*[A-Z]{1,3}\s+[A-Z]{1,3}\s+[A-Za-z]{1,6}\s+\d+\s+[A-Za-z]{1,3}\s+[A-Za-z]{1,3}\s+[A-Za-z]{1,3}$/,
|
|
443
|
-
// Patterns with excessive brackets, symbols mixed with short words
|
|
444
633
|
/[>\[\]]{2,}|[A-Za-z]{1,3}\s+[>\[\]<]+\s+[A-Za-z]{1,3}/,
|
|
445
|
-
// Excessive punctuation mixed with short letter sequences
|
|
446
634
|
/^[^a-zA-Z]*[a-zA-Z]{1,2}[^a-zA-Z]+[a-zA-Z]{1,3}[^a-zA-Z]+.*$/,
|
|
447
635
|
];
|
|
448
636
|
|
|
@@ -503,6 +691,8 @@ const BusinessCardOcr = ({
|
|
|
503
691
|
switch (fieldType) {
|
|
504
692
|
case 'name':
|
|
505
693
|
// For names: only letters, spaces, apostrophes, periods, hyphens
|
|
694
|
+
// First remove pipe symbols and other common artifacts
|
|
695
|
+
cleaned = cleaned.replace(/[|]/g, ' '); // Replace pipes with spaces
|
|
506
696
|
cleaned = cleaned.replace(/[^a-zA-Z\s'.-]/g, '');
|
|
507
697
|
// Remove excessive spaces
|
|
508
698
|
cleaned = cleaned.replace(/\s+/g, ' ');
|
|
@@ -512,9 +702,11 @@ const BusinessCardOcr = ({
|
|
|
512
702
|
|
|
513
703
|
case 'company':
|
|
514
704
|
// For companies: letters, numbers, spaces, common punctuation
|
|
705
|
+
// Remove pipe characters and other artifacts first
|
|
706
|
+
cleaned = cleaned.replace(/[|]/g, ' '); // Replace pipes with spaces
|
|
515
707
|
cleaned = cleaned.replace(/[^a-zA-Z0-9\s&.,'-]/g, '');
|
|
516
|
-
// Remove
|
|
517
|
-
cleaned = cleaned.replace(/[
|
|
708
|
+
// Remove equals signs that OCR often mistakes
|
|
709
|
+
cleaned = cleaned.replace(/[="]/g, '');
|
|
518
710
|
// Remove excessive spaces
|
|
519
711
|
cleaned = cleaned.replace(/\s+/g, ' ');
|
|
520
712
|
// Remove leading/trailing punctuation except periods
|
|
@@ -526,9 +718,11 @@ const BusinessCardOcr = ({
|
|
|
526
718
|
|
|
527
719
|
case 'position':
|
|
528
720
|
// For positions: letters, spaces, common punctuation
|
|
721
|
+
// Remove pipe characters and other artifacts first
|
|
722
|
+
cleaned = cleaned.replace(/[|]/g, ' '); // Replace pipes with spaces
|
|
529
723
|
cleaned = cleaned.replace(/[^a-zA-Z\s&.,'-]/g, '');
|
|
530
|
-
// Remove
|
|
531
|
-
cleaned = cleaned.replace(/[
|
|
724
|
+
// Remove equals signs
|
|
725
|
+
cleaned = cleaned.replace(/[="]/g, '');
|
|
532
726
|
// Remove excessive spaces
|
|
533
727
|
cleaned = cleaned.replace(/\s+/g, ' ');
|
|
534
728
|
// Remove leading/trailing punctuation
|
|
@@ -555,7 +749,7 @@ const BusinessCardOcr = ({
|
|
|
555
749
|
|
|
556
750
|
default:
|
|
557
751
|
// General cleaning: remove obvious artifacts
|
|
558
|
-
cleaned = cleaned.replace(/[|="]/g, '');
|
|
752
|
+
cleaned = cleaned.replace(/[|="]/g, ' '); // Replace pipes with spaces
|
|
559
753
|
cleaned = cleaned.replace(/\s+/g, ' ');
|
|
560
754
|
}
|
|
561
755
|
|
|
@@ -568,7 +762,369 @@ const BusinessCardOcr = ({
|
|
|
568
762
|
return cleaned;
|
|
569
763
|
};
|
|
570
764
|
|
|
765
|
+
// Helper function to capitalize first letter of each word (ucfirst)
|
|
766
|
+
const ucFirstWords = (text) => {
|
|
767
|
+
if (!text || typeof text !== 'string') return '';
|
|
768
|
+
|
|
769
|
+
return text
|
|
770
|
+
.toLowerCase()
|
|
771
|
+
.split(' ')
|
|
772
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
773
|
+
.join(' ');
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
// Intelligent field mapping with confidence scoring
|
|
777
|
+
const intelligentFieldMapping = (rawText) => {
|
|
778
|
+
console.log('Raw OCR Text:', rawText);
|
|
779
|
+
|
|
780
|
+
const lines = rawText.split('\n').filter(line => line.trim().length > 0);
|
|
781
|
+
const fieldCandidates = {};
|
|
782
|
+
|
|
783
|
+
// Enhanced email detection
|
|
784
|
+
const emailRegex = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/gi;
|
|
785
|
+
const emails = rawText.match(emailRegex);
|
|
786
|
+
if (emails && emails.length > 0) {
|
|
787
|
+
const cleanEmail = cleanFieldValue(emails[0], 'email');
|
|
788
|
+
if (cleanEmail) {
|
|
789
|
+
fieldCandidates.email = cleanEmail;
|
|
790
|
+
console.log('Found email:', cleanEmail);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Enhanced phone detection with international support
|
|
795
|
+
const phoneRegex = /(\+?[\d\s\-\(\)]{8,20})/g;
|
|
796
|
+
const phoneMatches = rawText.match(phoneRegex);
|
|
797
|
+
if (phoneMatches) {
|
|
798
|
+
const validPhones = phoneMatches.filter(p => {
|
|
799
|
+
const digits = p.replace(/\D/g, '');
|
|
800
|
+
return digits.length >= 8 && digits.length <= 15;
|
|
801
|
+
});
|
|
802
|
+
if (validPhones.length > 0) {
|
|
803
|
+
const cleanPhone = cleanFieldValue(validPhones[0].trim(), 'phone');
|
|
804
|
+
if (cleanPhone) {
|
|
805
|
+
fieldCandidates.mobile = cleanPhone;
|
|
806
|
+
fieldCandidates.phone = cleanPhone; // Backward compatibility
|
|
807
|
+
console.log('Found phone:', cleanPhone);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Website detection
|
|
813
|
+
const websiteRegex = /((?:https?:\/\/)?(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(?:\.[a-zA-Z]{2,})?)/gi;
|
|
814
|
+
const websites = rawText.match(websiteRegex)?.filter(w => !w.includes('@'));
|
|
815
|
+
if (websites && websites.length > 0) {
|
|
816
|
+
const cleanWebsite = cleanFieldValue(websites[0], 'website');
|
|
817
|
+
if (cleanWebsite) {
|
|
818
|
+
fieldCandidates.website = cleanWebsite;
|
|
819
|
+
console.log('Found website:', cleanWebsite);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Advanced name detection
|
|
824
|
+
const namePatterns = detectNames(lines, fieldCandidates);
|
|
825
|
+
console.log('Name detection candidates:', namePatterns);
|
|
826
|
+
console.log('Lines being processed for names:', lines.map((line, i) => `${i}: "${line}"`));
|
|
827
|
+
if (namePatterns.length > 0) {
|
|
828
|
+
const bestName = namePatterns[0];
|
|
829
|
+
fieldCandidates.firstname = bestName.first;
|
|
830
|
+
fieldCandidates.first_name = bestName.first; // Backward compatibility
|
|
831
|
+
if (bestName.last && bestName.last.trim()) {
|
|
832
|
+
fieldCandidates.lastname = bestName.last;
|
|
833
|
+
fieldCandidates.last_name = bestName.last; // Backward compatibility
|
|
834
|
+
console.log('Selected name:', bestName.first, '|', bestName.last, 'from line:', bestName.originalLine, 'confidence:', bestName.confidence);
|
|
835
|
+
} else {
|
|
836
|
+
console.log('Selected name (first only):', bestName.first, 'from line:', bestName.originalLine, 'confidence:', bestName.confidence);
|
|
837
|
+
console.log('Last name was empty or invalid:', `"${bestName.last}"`);
|
|
838
|
+
}
|
|
839
|
+
} else {
|
|
840
|
+
console.log('No name patterns detected. Lines processed:', lines.map((line, i) => `${i}: "${line}"`));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Position detection with context awareness
|
|
844
|
+
const positionCandidates = detectPosition(lines, fieldCandidates);
|
|
845
|
+
console.log('Position detection candidates:', positionCandidates);
|
|
846
|
+
if (positionCandidates.length > 0) {
|
|
847
|
+
fieldCandidates.position = ucFirstWords(positionCandidates[0].text);
|
|
848
|
+
console.log('Selected position:', fieldCandidates.position, 'confidence:', positionCandidates[0].confidence);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Company detection with improved logic
|
|
852
|
+
const companyCandidates = detectCompany(lines, fieldCandidates);
|
|
853
|
+
console.log('Company detection candidates:', companyCandidates);
|
|
854
|
+
if (companyCandidates.length > 0) {
|
|
855
|
+
fieldCandidates.company = ucFirstWords(companyCandidates[0].text);
|
|
856
|
+
console.log('Selected company:', fieldCandidates.company, 'confidence:', companyCandidates[0].confidence);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Collect unused lines
|
|
860
|
+
const unusedLines = [];
|
|
861
|
+
lines.forEach((line, index) => {
|
|
862
|
+
const trimmedLine = line.trim();
|
|
863
|
+
if (!isLikelyOCRArtifact(trimmedLine) &&
|
|
864
|
+
!Object.values(fieldCandidates).some(value =>
|
|
865
|
+
typeof value === 'string' && value.includes(trimmedLine)
|
|
866
|
+
)) {
|
|
867
|
+
const cleanedLine = cleanFieldValue(trimmedLine, 'general');
|
|
868
|
+
if (cleanedLine && cleanedLine.length > 1) {
|
|
869
|
+
unusedLines.push({
|
|
870
|
+
text: cleanedLine,
|
|
871
|
+
index: index,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
fieldCandidates._unusedData = unusedLines;
|
|
878
|
+
console.log('Final field candidates:', fieldCandidates);
|
|
879
|
+
|
|
880
|
+
return fieldCandidates;
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
// Enhanced name detection with ML-like scoring
|
|
884
|
+
const detectNames = (lines, existingFields) => {
|
|
885
|
+
const namePatterns = [
|
|
886
|
+
/^[A-Z][a-z]+ [A-Z][a-z]+$/, // John Smith
|
|
887
|
+
/^[A-Z]\. [A-Z][a-z]+$/, // J. Smith
|
|
888
|
+
/^[A-Z][a-z]+ [A-Z]\. [A-Z][a-z]+$/, // John A. Smith
|
|
889
|
+
/^[A-Z][a-z]+$/, // John (single name)
|
|
890
|
+
/^[A-Z][a-z]+ [A-Z][a-z]+ [A-Z][a-z]+$/, // John Middle Smith
|
|
891
|
+
];
|
|
892
|
+
|
|
893
|
+
const candidates = [];
|
|
894
|
+
|
|
895
|
+
lines.forEach((line, index) => {
|
|
896
|
+
const cleanLine = line.trim();
|
|
897
|
+
|
|
898
|
+
// Skip lines with known field data
|
|
899
|
+
if (existingFields.email && cleanLine.includes(existingFields.email)) return;
|
|
900
|
+
if (existingFields.mobile && cleanLine.includes(existingFields.mobile)) return;
|
|
901
|
+
|
|
902
|
+
// Pre-clean the line to handle artifacts like pipes
|
|
903
|
+
const preCleanedLine = cleanLine.replace(/[|]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
904
|
+
|
|
905
|
+
// Skip obvious artifacts (but check both original and pre-cleaned)
|
|
906
|
+
if (isLikelyOCRArtifact(cleanLine) && isLikelyOCRArtifact(preCleanedLine)) return;
|
|
907
|
+
|
|
908
|
+
// Check against name patterns on both original and pre-cleaned text
|
|
909
|
+
const testLines = [cleanLine, preCleanedLine];
|
|
910
|
+
|
|
911
|
+
testLines.forEach(testLine => {
|
|
912
|
+
namePatterns.forEach(pattern => {
|
|
913
|
+
if (pattern.test(testLine)) {
|
|
914
|
+
const nameWords = testLine.split(' ').filter(w => w.length > 0);
|
|
915
|
+
const confidence = calculateNameConfidence(testLine, index, lines.length);
|
|
916
|
+
|
|
917
|
+
// Lower threshold for better name capture
|
|
918
|
+
if (confidence > 0.2) {
|
|
919
|
+
const rawLast = nameWords.slice(1).join(' ');
|
|
920
|
+
const cleanedFirst = cleanFieldValue(nameWords[0], 'name');
|
|
921
|
+
const cleanedLast = cleanFieldValue(rawLast, 'name');
|
|
922
|
+
|
|
923
|
+
console.log(`Processing name: words=[${nameWords.join(', ')}], rawLast="${rawLast}", cleanedFirst="${cleanedFirst}", cleanedLast="${cleanedLast}"`);
|
|
924
|
+
|
|
925
|
+
if (cleanedFirst) {
|
|
926
|
+
console.log(`Name candidate from "${testLine}": first="${cleanedFirst}", last="${cleanedLast}", confidence=${confidence}`);
|
|
927
|
+
candidates.push({
|
|
928
|
+
first: cleanedFirst,
|
|
929
|
+
last: cleanedLast || '', // Ensure last is always a string
|
|
930
|
+
confidence: confidence,
|
|
931
|
+
index: index,
|
|
932
|
+
originalLine: cleanLine
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// Special case: Check for names that might not match strict patterns
|
|
941
|
+
// but have strong name characteristics (use pre-cleaned text)
|
|
942
|
+
const nameWords = preCleanedLine.split(' ').filter(w => w.length > 0);
|
|
943
|
+
if (nameWords.length === 2 || nameWords.length === 3) {
|
|
944
|
+
const allProperCase = nameWords.every(word =>
|
|
945
|
+
word.length > 0 &&
|
|
946
|
+
word[0] === word[0].toUpperCase() &&
|
|
947
|
+
word.slice(1) === word.slice(1).toLowerCase() &&
|
|
948
|
+
/^[A-Za-z]+$/.test(word) // Only letters, no symbols
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
if (allProperCase) {
|
|
952
|
+
const confidence = calculateNameConfidence(preCleanedLine, index, lines.length);
|
|
953
|
+
if (confidence > 0.15) { // Even lower threshold for proper case names
|
|
954
|
+
const rawLast = nameWords.slice(1).join(' ');
|
|
955
|
+
const cleanedFirst = cleanFieldValue(nameWords[0], 'name');
|
|
956
|
+
const cleanedLast = cleanFieldValue(rawLast, 'name');
|
|
957
|
+
|
|
958
|
+
console.log(`Processing proper case name: words=[${nameWords.join(', ')}], rawLast="${rawLast}", cleanedFirst="${cleanedFirst}", cleanedLast="${cleanedLast}"`);
|
|
959
|
+
|
|
960
|
+
if (cleanedFirst) {
|
|
961
|
+
console.log(`Proper case name candidate from "${preCleanedLine}": first="${cleanedFirst}", last="${cleanedLast}", confidence=${confidence + 0.1}`);
|
|
962
|
+
candidates.push({
|
|
963
|
+
first: cleanedFirst,
|
|
964
|
+
last: cleanedLast || '', // Ensure last is always a string
|
|
965
|
+
confidence: confidence + 0.1, // Bonus for proper case
|
|
966
|
+
index: index,
|
|
967
|
+
originalLine: cleanLine
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
// Sort by confidence and position (names usually appear early)
|
|
976
|
+
return candidates.sort((a, b) =>
|
|
977
|
+
(b.confidence + (1 - b.index / lines.length) * 0.3) -
|
|
978
|
+
(a.confidence + (1 - a.index / lines.length) * 0.3)
|
|
979
|
+
);
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
// Calculate name confidence based on various factors
|
|
983
|
+
const calculateNameConfidence = (text, index, totalLines) => {
|
|
984
|
+
let confidence = 0.5; // Base confidence
|
|
985
|
+
|
|
986
|
+
// Bonus for being early in the text (names usually come first)
|
|
987
|
+
if (index < totalLines * 0.3) confidence += 0.3;
|
|
988
|
+
|
|
989
|
+
// Bonus for proper capitalization
|
|
990
|
+
const words = text.split(' ');
|
|
991
|
+
const properlyCapitalized = words.every(word =>
|
|
992
|
+
word.length > 0 && word[0] === word[0].toUpperCase() &&
|
|
993
|
+
word.slice(1) === word.slice(1).toLowerCase()
|
|
994
|
+
);
|
|
995
|
+
if (properlyCapitalized) confidence += 0.2;
|
|
996
|
+
|
|
997
|
+
// Penalty for being too long (unlikely to be just a name)
|
|
998
|
+
if (text.length > 30) confidence -= 0.2;
|
|
999
|
+
|
|
1000
|
+
// Bonus for common name lengths
|
|
1001
|
+
if (text.length >= 5 && text.length <= 20) confidence += 0.1;
|
|
1002
|
+
|
|
1003
|
+
return Math.min(1, Math.max(0, confidence));
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
// Position detection with business context
|
|
1007
|
+
const detectPosition = (lines, existingFields) => {
|
|
1008
|
+
const positionKeywords = [
|
|
1009
|
+
'director', 'manager', 'ceo', 'cto', 'cfo', 'president', 'vice president',
|
|
1010
|
+
'senior', 'lead', 'head', 'chief', 'supervisor', 'coordinator',
|
|
1011
|
+
'specialist', 'analyst', 'engineer', 'developer', 'designer',
|
|
1012
|
+
'consultant', 'sales', 'marketing', 'lawyer', 'attorney', 'practitioner',
|
|
1013
|
+
'partner', 'associate', 'executive', 'officer', 'founder', 'owner',
|
|
1014
|
+
'legal practitioner' // Added for the specific example
|
|
1015
|
+
];
|
|
1016
|
+
|
|
1017
|
+
const candidates = [];
|
|
1018
|
+
|
|
1019
|
+
lines.forEach((line, index) => {
|
|
1020
|
+
const cleanLine = line.trim();
|
|
1021
|
+
const lowerLine = cleanLine.toLowerCase();
|
|
1022
|
+
|
|
1023
|
+
// Skip if line contains email/phone
|
|
1024
|
+
if (existingFields.email && cleanLine.includes(existingFields.email)) return;
|
|
1025
|
+
if (existingFields.mobile && cleanLine.includes(existingFields.mobile)) return;
|
|
1026
|
+
|
|
1027
|
+
// Pre-clean the line to handle artifacts like pipes
|
|
1028
|
+
const preCleanedLine = cleanLine.replace(/[|]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
1029
|
+
const preCleanedLower = preCleanedLine.toLowerCase();
|
|
1030
|
+
|
|
1031
|
+
// Calculate keyword score on both original and pre-cleaned text
|
|
1032
|
+
let keywordScore = 0;
|
|
1033
|
+
let matchedKeywords = 0;
|
|
1034
|
+
|
|
1035
|
+
positionKeywords.forEach(keyword => {
|
|
1036
|
+
if (lowerLine.includes(keyword) || preCleanedLower.includes(keyword)) {
|
|
1037
|
+
keywordScore += keyword.length / Math.max(cleanLine.length, preCleanedLine.length);
|
|
1038
|
+
matchedKeywords++;
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// Bonus for multiple keyword matches
|
|
1043
|
+
if (matchedKeywords > 1) keywordScore *= 1.5;
|
|
1044
|
+
|
|
1045
|
+
if (keywordScore > 0.1) {
|
|
1046
|
+
const cleanedText = cleanFieldValue(preCleanedLine, 'position');
|
|
1047
|
+
if (cleanedText && cleanedText.length > 2) {
|
|
1048
|
+
candidates.push({
|
|
1049
|
+
text: cleanedText,
|
|
1050
|
+
confidence: Math.min(0.95, keywordScore * 2),
|
|
1051
|
+
index: index
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
return candidates.sort((a, b) => b.confidence - a.confidence);
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
// Company detection with improved logic
|
|
1061
|
+
const detectCompany = (lines, existingFields) => {
|
|
1062
|
+
const companyIndicators = [
|
|
1063
|
+
'inc', 'corp', 'ltd', 'llc', 'company', 'co.', 'corporation',
|
|
1064
|
+
'incorporated', 'limited', 'enterprises', 'group', 'solutions',
|
|
1065
|
+
'services', 'systems', 'technologies', 'tech', 'consulting',
|
|
1066
|
+
'law', 'legal', 'firm', 'associates', 'partners', 'curae' // Added for the specific example
|
|
1067
|
+
];
|
|
1068
|
+
|
|
1069
|
+
const candidates = [];
|
|
1070
|
+
|
|
1071
|
+
lines.forEach((line, index) => {
|
|
1072
|
+
const cleanLine = line.trim();
|
|
1073
|
+
const lowerLine = cleanLine.toLowerCase();
|
|
1074
|
+
|
|
1075
|
+
// Skip known fields
|
|
1076
|
+
if (existingFields.email && cleanLine.includes(existingFields.email)) return;
|
|
1077
|
+
if (existingFields.mobile && cleanLine.includes(existingFields.mobile)) return;
|
|
1078
|
+
if (existingFields.firstname && cleanLine.includes(existingFields.firstname)) return;
|
|
1079
|
+
|
|
1080
|
+
// Pre-clean the line to handle artifacts like pipes
|
|
1081
|
+
const preCleanedLine = cleanLine.replace(/[|]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
1082
|
+
const preCleanedLower = preCleanedLine.toLowerCase();
|
|
1083
|
+
|
|
1084
|
+
const hasCompanyIndicator = companyIndicators.some(
|
|
1085
|
+
indicator => lowerLine.includes(indicator) || preCleanedLower.includes(indicator)
|
|
1086
|
+
);
|
|
1087
|
+
const isAllCaps = cleanLine === cleanLine.toUpperCase() && cleanLine.length > 2;
|
|
1088
|
+
const cleanedWords = preCleanedLine.split(/\s+/);
|
|
1089
|
+
const letterRatio = (preCleanedLine.match(/[a-zA-Z]/g) || []).length / preCleanedLine.length;
|
|
1090
|
+
|
|
1091
|
+
let confidence = 0;
|
|
1092
|
+
|
|
1093
|
+
// High confidence for company indicators
|
|
1094
|
+
if (hasCompanyIndicator) confidence += 0.7;
|
|
1095
|
+
|
|
1096
|
+
// Moderate confidence for all caps (common for company names)
|
|
1097
|
+
if (isAllCaps) confidence += 0.4;
|
|
1098
|
+
|
|
1099
|
+
// Bonus for reasonable length and structure
|
|
1100
|
+
if (letterRatio > 0.6 && preCleanedLine.length >= 3 && preCleanedLine.length <= 40) {
|
|
1101
|
+
confidence += 0.2;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Bonus for being positioned after name/position
|
|
1105
|
+
if (index > 0 && index < lines.length * 0.7) confidence += 0.1;
|
|
1106
|
+
|
|
1107
|
+
if (confidence > 0.3) {
|
|
1108
|
+
const cleanedText = cleanFieldValue(preCleanedLine, 'company');
|
|
1109
|
+
if (cleanedText && cleanedText.length > 2) {
|
|
1110
|
+
candidates.push({
|
|
1111
|
+
text: cleanedText,
|
|
1112
|
+
confidence: Math.min(0.95, confidence),
|
|
1113
|
+
index: index
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
return candidates.sort((a, b) => b.confidence - a.confidence);
|
|
1120
|
+
};
|
|
1121
|
+
|
|
571
1122
|
const parseExtractedText = (text) => {
|
|
1123
|
+
// Use the new intelligent field mapping system
|
|
1124
|
+
return intelligentFieldMapping(text);
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
const parseExtractedTextLegacy = (text) => {
|
|
572
1128
|
const lines = text.split('\n').filter((line) => line.trim());
|
|
573
1129
|
const parsed = {};
|
|
574
1130
|
const unusedLines = [];
|
|
@@ -831,6 +1387,8 @@ const BusinessCardOcr = ({
|
|
|
831
1387
|
return parsed;
|
|
832
1388
|
};
|
|
833
1389
|
|
|
1390
|
+
// parseExtractedText now uses intelligent mapping above
|
|
1391
|
+
|
|
834
1392
|
// Helper function to get field input type
|
|
835
1393
|
const getFieldInputType = (fieldId) => {
|
|
836
1394
|
const fieldTypeMap = {
|
|
@@ -849,14 +1407,14 @@ const BusinessCardOcr = ({
|
|
|
849
1407
|
// Helper function to get field icon
|
|
850
1408
|
const getFieldIcon = (fieldId) => {
|
|
851
1409
|
const iconMap = {
|
|
852
|
-
email: <
|
|
1410
|
+
email: <Mail size={16} />,
|
|
853
1411
|
mobile: null,
|
|
854
1412
|
phone: null,
|
|
855
|
-
website: <
|
|
1413
|
+
website: <Globe size={16} />,
|
|
856
1414
|
position: null,
|
|
857
|
-
company: <
|
|
858
|
-
firstname: <
|
|
859
|
-
lastname: <
|
|
1415
|
+
company: <Building2 size={16} />,
|
|
1416
|
+
firstname: <User size={16} />,
|
|
1417
|
+
lastname: <User size={16} />,
|
|
860
1418
|
};
|
|
861
1419
|
return iconMap[fieldId];
|
|
862
1420
|
};
|
|
@@ -1086,7 +1644,7 @@ const BusinessCardOcr = ({
|
|
|
1086
1644
|
onClick={handleClose}
|
|
1087
1645
|
className={ocrStyles.closeButton}
|
|
1088
1646
|
>
|
|
1089
|
-
<
|
|
1647
|
+
<X size={24} />
|
|
1090
1648
|
</button>
|
|
1091
1649
|
</div>
|
|
1092
1650
|
|
|
@@ -1094,7 +1652,7 @@ const BusinessCardOcr = ({
|
|
|
1094
1652
|
{currentStep === 'camera' && (
|
|
1095
1653
|
<div className={ocrStyles.cameraContainer}>
|
|
1096
1654
|
<div className={ocrStyles.captureGuide}>
|
|
1097
|
-
<p>Position business card within the frame</p>
|
|
1655
|
+
<p>Position business card within the frame and tap to focus</p>
|
|
1098
1656
|
</div>
|
|
1099
1657
|
<div className={ocrStyles.videoWrapper}>
|
|
1100
1658
|
<video
|
|
@@ -1103,45 +1661,73 @@ const BusinessCardOcr = ({
|
|
|
1103
1661
|
playsInline
|
|
1104
1662
|
muted
|
|
1105
1663
|
className={ocrStyles.cameraVideo}
|
|
1664
|
+
onClick={triggerAutofocus}
|
|
1106
1665
|
/>
|
|
1107
|
-
|
|
1666
|
+
{/* Enhanced camera overlay with focus frame */}
|
|
1667
|
+
<div className={ocrStyles.cameraOverlay}>
|
|
1668
|
+
<div className={ocrStyles.focusFrame}>
|
|
1669
|
+
{isFocusing && (
|
|
1670
|
+
<div className={ocrStyles.focusIndicator}>
|
|
1671
|
+
<Focus className={ocrStyles.focusIcon} />
|
|
1672
|
+
<span>Focusing...</span>
|
|
1673
|
+
</div>
|
|
1674
|
+
)}
|
|
1675
|
+
</div>
|
|
1676
|
+
</div>
|
|
1108
1677
|
</div>
|
|
1109
1678
|
|
|
1110
1679
|
<div className={ocrStyles.cameraControls}>
|
|
1111
|
-
{
|
|
1680
|
+
<div className={ocrStyles.controlsTop}>
|
|
1681
|
+
{availableCameras.length > 1 && (
|
|
1682
|
+
<button
|
|
1683
|
+
onClick={switchCamera}
|
|
1684
|
+
className={ocrStyles.controlButton}
|
|
1685
|
+
title="Switch Camera"
|
|
1686
|
+
>
|
|
1687
|
+
<SwitchCamera size={20} />
|
|
1688
|
+
</button>
|
|
1689
|
+
)}
|
|
1690
|
+
|
|
1691
|
+
{/* Flash toggle button */}
|
|
1692
|
+
<button
|
|
1693
|
+
onClick={toggleFlash}
|
|
1694
|
+
className={`${ocrStyles.controlButton} ${flashEnabled ? ocrStyles.active : ''}`}
|
|
1695
|
+
title={flashEnabled ? 'Turn off flash' : 'Turn on flash'}
|
|
1696
|
+
>
|
|
1697
|
+
{flashEnabled ? <Zap size={20} /> : <ZapOff size={20} />}
|
|
1698
|
+
</button>
|
|
1699
|
+
</div>
|
|
1700
|
+
|
|
1701
|
+
<div className={ocrStyles.controlsBottom}>
|
|
1112
1702
|
<button
|
|
1113
|
-
onClick={
|
|
1703
|
+
onClick={() =>
|
|
1704
|
+
fileInputRef.current?.click()
|
|
1705
|
+
}
|
|
1114
1706
|
className={ocrStyles.secondaryButton}
|
|
1115
1707
|
>
|
|
1116
|
-
<
|
|
1117
|
-
|
|
1708
|
+
<ImagePlus size={20} />
|
|
1709
|
+
Upload
|
|
1118
1710
|
</button>
|
|
1119
|
-
)}
|
|
1120
1711
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1712
|
+
<button
|
|
1713
|
+
onClick={capturePhoto}
|
|
1714
|
+
className={`${ocrStyles.captureButton} ${isFocusing ? ocrStyles.focusing : ''}`}
|
|
1715
|
+
disabled={isFocusing}
|
|
1716
|
+
>
|
|
1717
|
+
<Camera size={28} />
|
|
1718
|
+
{isFocusing ? 'Focusing...' : 'Capture'}
|
|
1719
|
+
</button>
|
|
1128
1720
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
}
|
|
1140
|
-
className={ocrStyles.secondaryButton}
|
|
1141
|
-
>
|
|
1142
|
-
<ImageIcon size={24} />
|
|
1143
|
-
Upload Image
|
|
1144
|
-
</button>
|
|
1721
|
+
<input
|
|
1722
|
+
type="file"
|
|
1723
|
+
ref={fileInputRef}
|
|
1724
|
+
onChange={handleFileUpload}
|
|
1725
|
+
accept="image/jpeg,image/png,image/jpg,image/gif,image/webp"
|
|
1726
|
+
style={{ display: 'none' }}
|
|
1727
|
+
/>
|
|
1728
|
+
|
|
1729
|
+
<div className={ocrStyles.placeholder}></div>
|
|
1730
|
+
</div>
|
|
1145
1731
|
</div>
|
|
1146
1732
|
</div>
|
|
1147
1733
|
)}
|
|
@@ -1161,7 +1747,7 @@ const BusinessCardOcr = ({
|
|
|
1161
1747
|
onClick={retakePhoto}
|
|
1162
1748
|
className={ocrStyles.secondaryButton}
|
|
1163
1749
|
>
|
|
1164
|
-
<
|
|
1750
|
+
<RotateCcw size={20} />
|
|
1165
1751
|
Retake
|
|
1166
1752
|
</button>
|
|
1167
1753
|
|
|
@@ -1170,7 +1756,7 @@ const BusinessCardOcr = ({
|
|
|
1170
1756
|
disabled={isProcessing}
|
|
1171
1757
|
className={ocrStyles.primaryButton}
|
|
1172
1758
|
>
|
|
1173
|
-
<
|
|
1759
|
+
<CheckCircle size={20} />
|
|
1174
1760
|
Process Card
|
|
1175
1761
|
</button>
|
|
1176
1762
|
</div>
|
|
@@ -1231,7 +1817,7 @@ const BusinessCardOcr = ({
|
|
|
1231
1817
|
<>
|
|
1232
1818
|
<div className={ocrStyles.fieldGroup}>
|
|
1233
1819
|
<label>
|
|
1234
|
-
<
|
|
1820
|
+
<User size={16} />
|
|
1235
1821
|
First Name
|
|
1236
1822
|
</label>
|
|
1237
1823
|
<input
|
|
@@ -1252,7 +1838,7 @@ const BusinessCardOcr = ({
|
|
|
1252
1838
|
</div>
|
|
1253
1839
|
<div className={ocrStyles.fieldGroup}>
|
|
1254
1840
|
<label>
|
|
1255
|
-
<
|
|
1841
|
+
<User size={16} />
|
|
1256
1842
|
Last Name
|
|
1257
1843
|
</label>
|
|
1258
1844
|
<input
|
|
@@ -1273,7 +1859,7 @@ const BusinessCardOcr = ({
|
|
|
1273
1859
|
</div>
|
|
1274
1860
|
<div className={ocrStyles.fieldGroup}>
|
|
1275
1861
|
<label>
|
|
1276
|
-
<
|
|
1862
|
+
<Mail size={16} />
|
|
1277
1863
|
Email
|
|
1278
1864
|
</label>
|
|
1279
1865
|
<input
|
|
@@ -1310,7 +1896,7 @@ const BusinessCardOcr = ({
|
|
|
1310
1896
|
</div>
|
|
1311
1897
|
<div className={ocrStyles.fieldGroup}>
|
|
1312
1898
|
<label>
|
|
1313
|
-
<
|
|
1899
|
+
<Building2 size={16} />
|
|
1314
1900
|
Company
|
|
1315
1901
|
</label>
|
|
1316
1902
|
<input
|
|
@@ -1439,7 +2025,7 @@ const BusinessCardOcr = ({
|
|
|
1439
2025
|
onClick={retakePhoto}
|
|
1440
2026
|
className={ocrStyles.secondaryButton}
|
|
1441
2027
|
>
|
|
1442
|
-
<
|
|
2028
|
+
<ImagePlus size={20} />
|
|
1443
2029
|
Scan Another
|
|
1444
2030
|
</button>
|
|
1445
2031
|
|