tktechnico-react-face-detection 1.0.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/dist/index.js ADDED
@@ -0,0 +1,794 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ CaptureButton: () => CaptureButton,
34
+ DetectionResultDisplay: () => DetectionResultDisplay,
35
+ FaceDetector: () => FaceDetector,
36
+ FaceOverlay: () => FaceOverlay,
37
+ ImagePreviewWithOverlay: () => ImagePreviewWithOverlay,
38
+ LoadingState: () => LoadingState,
39
+ useFaceDetectionCore: () => useFaceDetectionCore
40
+ });
41
+ module.exports = __toCommonJS(index_exports);
42
+
43
+ // src/components/FaceDetector.tsx
44
+ var import_react5 = require("react");
45
+
46
+ // src/useFaceDetectionCore.ts
47
+ var import_react = require("react");
48
+ var tf = __toESM(require("@tensorflow/tfjs"));
49
+ var blazeface = __toESM(require("@tensorflow-models/blazeface"));
50
+ var DEFAULT_CONFIDENCE_THRESHOLD = 0.7;
51
+ var DEFAULT_MAX_IMAGE_SIZE = 640;
52
+ function useFaceDetectionCore(options = {}) {
53
+ const {
54
+ confidenceThreshold = DEFAULT_CONFIDENCE_THRESHOLD,
55
+ maxImageSize = DEFAULT_MAX_IMAGE_SIZE
56
+ } = options;
57
+ const [isModelLoading, setIsModelLoading] = (0, import_react.useState)(true);
58
+ const [modelError, setModelError] = (0, import_react.useState)(null);
59
+ const modelRef = (0, import_react.useRef)(null);
60
+ const loadModel = (0, import_react.useCallback)(async () => {
61
+ try {
62
+ setIsModelLoading(true);
63
+ setModelError(null);
64
+ await tf.ready();
65
+ const model = await blazeface.load();
66
+ modelRef.current = model;
67
+ console.log("BlazeFace model loaded successfully");
68
+ setIsModelLoading(false);
69
+ } catch (error) {
70
+ console.error("Failed to load face detection model:", error);
71
+ setModelError("Failed to load face detection model. Please check your connection and try again.");
72
+ setIsModelLoading(false);
73
+ }
74
+ }, []);
75
+ (0, import_react.useEffect)(() => {
76
+ loadModel();
77
+ }, [loadModel]);
78
+ const detectFace = (0, import_react.useCallback)(async (imageElement) => {
79
+ if (!modelRef.current) {
80
+ return {
81
+ success: false,
82
+ face: null,
83
+ confidence: 0,
84
+ isLowConfidence: false,
85
+ errorMessage: "Face detection model not loaded"
86
+ };
87
+ }
88
+ try {
89
+ const canvas = document.createElement("canvas");
90
+ const ctx = canvas.getContext("2d");
91
+ if (!ctx) {
92
+ throw new Error("Could not get canvas context");
93
+ }
94
+ let width = imageElement.naturalWidth;
95
+ let height = imageElement.naturalHeight;
96
+ if (width > height && width > maxImageSize) {
97
+ height = height / width * maxImageSize;
98
+ width = maxImageSize;
99
+ } else if (height > maxImageSize) {
100
+ width = width / height * maxImageSize;
101
+ height = maxImageSize;
102
+ }
103
+ canvas.width = width;
104
+ canvas.height = height;
105
+ ctx.drawImage(imageElement, 0, 0, width, height);
106
+ const predictions = await modelRef.current.estimateFaces(canvas, false);
107
+ if (predictions.length === 0) {
108
+ return {
109
+ success: false,
110
+ face: null,
111
+ confidence: 0,
112
+ isLowConfidence: false,
113
+ errorMessage: "No face detected. Please take a clear photo of your face."
114
+ };
115
+ }
116
+ const prediction = predictions[0];
117
+ const probability = Array.isArray(prediction.probability) ? prediction.probability[0] : prediction.probability;
118
+ const scaleX = imageElement.naturalWidth / width;
119
+ const scaleY = imageElement.naturalHeight / height;
120
+ const topLeft = prediction.topLeft;
121
+ const bottomRight = prediction.bottomRight;
122
+ const face = {
123
+ topLeft: [topLeft[0] * scaleX, topLeft[1] * scaleY],
124
+ bottomRight: [bottomRight[0] * scaleX, bottomRight[1] * scaleY],
125
+ probability,
126
+ landmarks: prediction.landmarks
127
+ };
128
+ const isLowConfidence = probability < confidenceThreshold;
129
+ return {
130
+ success: true,
131
+ face,
132
+ confidence: probability,
133
+ isLowConfidence,
134
+ errorMessage: isLowConfidence ? "Face detected but image quality is low. Please retake." : null
135
+ };
136
+ } catch (error) {
137
+ console.error("Face detection error:", error);
138
+ return {
139
+ success: false,
140
+ face: null,
141
+ confidence: 0,
142
+ isLowConfidence: false,
143
+ errorMessage: "An error occurred during face detection. Please try again."
144
+ };
145
+ }
146
+ }, [confidenceThreshold, maxImageSize]);
147
+ const retryModelLoad = (0, import_react.useCallback)(() => {
148
+ loadModel();
149
+ }, [loadModel]);
150
+ return {
151
+ isModelLoading,
152
+ modelError,
153
+ detectFace,
154
+ retryModelLoad
155
+ };
156
+ }
157
+
158
+ // src/components/ImagePreviewWithOverlay.tsx
159
+ var import_react3 = require("react");
160
+
161
+ // src/components/FaceOverlay.tsx
162
+ var import_react2 = require("react");
163
+ var import_jsx_runtime = require("react/jsx-runtime");
164
+ function FaceOverlay({
165
+ imageElement,
166
+ face,
167
+ containerWidth,
168
+ containerHeight,
169
+ color = "#22c55e"
170
+ }) {
171
+ const canvasRef = (0, import_react2.useRef)(null);
172
+ (0, import_react2.useEffect)(() => {
173
+ const canvas = canvasRef.current;
174
+ if (!canvas || !imageElement || !face) {
175
+ if (canvas) {
176
+ const ctx2 = canvas.getContext("2d");
177
+ if (ctx2) {
178
+ ctx2.clearRect(0, 0, canvas.width, canvas.height);
179
+ }
180
+ }
181
+ return;
182
+ }
183
+ const ctx = canvas.getContext("2d");
184
+ if (!ctx) return;
185
+ const imageAspect = imageElement.naturalWidth / imageElement.naturalHeight;
186
+ const containerAspect = containerWidth / containerHeight;
187
+ let displayWidth;
188
+ let displayHeight;
189
+ let offsetX = 0;
190
+ let offsetY = 0;
191
+ if (imageAspect > containerAspect) {
192
+ displayWidth = containerWidth;
193
+ displayHeight = containerWidth / imageAspect;
194
+ offsetY = (containerHeight - displayHeight) / 2;
195
+ } else {
196
+ displayHeight = containerHeight;
197
+ displayWidth = containerHeight * imageAspect;
198
+ offsetX = (containerWidth - displayWidth) / 2;
199
+ }
200
+ canvas.width = containerWidth;
201
+ canvas.height = containerHeight;
202
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
203
+ const scaleX = displayWidth / imageElement.naturalWidth;
204
+ const scaleY = displayHeight / imageElement.naturalHeight;
205
+ const x1 = face.topLeft[0] * scaleX + offsetX;
206
+ const y1 = face.topLeft[1] * scaleY + offsetY;
207
+ const x2 = face.bottomRight[0] * scaleX + offsetX;
208
+ const y2 = face.bottomRight[1] * scaleY + offsetY;
209
+ const boxWidth = x2 - x1;
210
+ const boxHeight = y2 - y1;
211
+ ctx.strokeStyle = color;
212
+ ctx.lineWidth = 3;
213
+ ctx.strokeRect(x1, y1, boxWidth, boxHeight);
214
+ const confidenceText = `Confidence: ${Math.round(face.probability * 100)}%`;
215
+ ctx.font = "bold 14px system-ui, sans-serif";
216
+ const textMetrics = ctx.measureText(confidenceText);
217
+ const textPadding = 6;
218
+ const labelHeight = 24;
219
+ const labelWidth = textMetrics.width + textPadding * 2;
220
+ const labelX = x1;
221
+ const labelY = Math.max(y1 - labelHeight - 4, 4);
222
+ ctx.fillStyle = color;
223
+ ctx.fillRect(labelX, labelY, labelWidth, labelHeight);
224
+ ctx.fillStyle = "#ffffff";
225
+ ctx.textBaseline = "middle";
226
+ ctx.fillText(confidenceText, labelX + textPadding, labelY + labelHeight / 2);
227
+ const cornerLength = Math.min(20, boxWidth * 0.2, boxHeight * 0.2);
228
+ ctx.lineWidth = 4;
229
+ ctx.strokeStyle = color;
230
+ ctx.beginPath();
231
+ ctx.moveTo(x1, y1 + cornerLength);
232
+ ctx.lineTo(x1, y1);
233
+ ctx.lineTo(x1 + cornerLength, y1);
234
+ ctx.stroke();
235
+ ctx.beginPath();
236
+ ctx.moveTo(x2 - cornerLength, y1);
237
+ ctx.lineTo(x2, y1);
238
+ ctx.lineTo(x2, y1 + cornerLength);
239
+ ctx.stroke();
240
+ ctx.beginPath();
241
+ ctx.moveTo(x1, y2 - cornerLength);
242
+ ctx.lineTo(x1, y2);
243
+ ctx.lineTo(x1 + cornerLength, y2);
244
+ ctx.stroke();
245
+ ctx.beginPath();
246
+ ctx.moveTo(x2 - cornerLength, y2);
247
+ ctx.lineTo(x2, y2);
248
+ ctx.lineTo(x2, y2 - cornerLength);
249
+ ctx.stroke();
250
+ }, [imageElement, face, containerWidth, containerHeight, color]);
251
+ if (!face) return null;
252
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
253
+ "canvas",
254
+ {
255
+ ref: canvasRef,
256
+ style: {
257
+ position: "absolute",
258
+ inset: 0,
259
+ pointerEvents: "none",
260
+ zIndex: 10,
261
+ width: containerWidth,
262
+ height: containerHeight
263
+ }
264
+ }
265
+ );
266
+ }
267
+
268
+ // src/components/ImagePreviewWithOverlay.tsx
269
+ var import_jsx_runtime2 = require("react/jsx-runtime");
270
+ function ImagePreviewWithOverlay({
271
+ imageSrc,
272
+ face,
273
+ onImageLoad,
274
+ showOverlay = true,
275
+ overlayColor = "#22c55e",
276
+ placeholderText = "Capture a photo to detect your face",
277
+ className = ""
278
+ }) {
279
+ const containerRef = (0, import_react3.useRef)(null);
280
+ const imageRef = (0, import_react3.useRef)(null);
281
+ const [dimensions, setDimensions] = (0, import_react3.useState)({ width: 0, height: 0 });
282
+ const updateDimensions = (0, import_react3.useCallback)(() => {
283
+ if (containerRef.current) {
284
+ setDimensions({
285
+ width: containerRef.current.offsetWidth,
286
+ height: containerRef.current.offsetHeight
287
+ });
288
+ }
289
+ }, []);
290
+ (0, import_react3.useEffect)(() => {
291
+ updateDimensions();
292
+ window.addEventListener("resize", updateDimensions);
293
+ return () => window.removeEventListener("resize", updateDimensions);
294
+ }, [updateDimensions]);
295
+ const handleImageLoad = (0, import_react3.useCallback)(() => {
296
+ if (imageRef.current) {
297
+ onImageLoad(imageRef.current);
298
+ updateDimensions();
299
+ }
300
+ }, [onImageLoad, updateDimensions]);
301
+ const containerStyles = {
302
+ width: "100%",
303
+ aspectRatio: "3/4",
304
+ maxWidth: "24rem",
305
+ margin: "0 auto",
306
+ borderRadius: "0.75rem",
307
+ display: "flex",
308
+ alignItems: "center",
309
+ justifyContent: "center",
310
+ backgroundColor: "#f3f4f6",
311
+ border: imageSrc ? "none" : "2px dashed #d1d5db",
312
+ position: "relative",
313
+ overflow: "hidden"
314
+ };
315
+ if (!imageSrc) {
316
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: containerRef, style: containerStyles, className, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { textAlign: "center", padding: "2rem" }, children: [
317
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: {
318
+ width: "4rem",
319
+ height: "4rem",
320
+ margin: "0 auto 1rem",
321
+ borderRadius: "50%",
322
+ backgroundColor: "#e5e7eb",
323
+ display: "flex",
324
+ alignItems: "center",
325
+ justifyContent: "center"
326
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
327
+ "svg",
328
+ {
329
+ style: { width: "2rem", height: "2rem", color: "#9ca3af" },
330
+ fill: "none",
331
+ stroke: "currentColor",
332
+ viewBox: "0 0 24 24",
333
+ children: [
334
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
335
+ "path",
336
+ {
337
+ strokeLinecap: "round",
338
+ strokeLinejoin: "round",
339
+ strokeWidth: 1.5,
340
+ d: "M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
341
+ }
342
+ ),
343
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
344
+ "path",
345
+ {
346
+ strokeLinecap: "round",
347
+ strokeLinejoin: "round",
348
+ strokeWidth: 1.5,
349
+ d: "M15 13a3 3 0 11-6 0 3 3 0 016 0z"
350
+ }
351
+ )
352
+ ]
353
+ }
354
+ ) }),
355
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { color: "#6b7280", fontSize: "0.875rem" }, children: placeholderText })
356
+ ] }) });
357
+ }
358
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ref: containerRef, style: { ...containerStyles, border: "none" }, className, children: [
359
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
360
+ "img",
361
+ {
362
+ ref: imageRef,
363
+ src: imageSrc,
364
+ alt: "Captured",
365
+ onLoad: handleImageLoad,
366
+ style: { width: "100%", height: "100%", objectFit: "contain" }
367
+ }
368
+ ),
369
+ showOverlay && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
370
+ FaceOverlay,
371
+ {
372
+ imageElement: imageRef.current,
373
+ face,
374
+ containerWidth: dimensions.width,
375
+ containerHeight: dimensions.height,
376
+ color: overlayColor
377
+ }
378
+ )
379
+ ] });
380
+ }
381
+
382
+ // src/components/DetectionResultDisplay.tsx
383
+ var import_jsx_runtime3 = require("react/jsx-runtime");
384
+ function DetectionResultDisplay({
385
+ result,
386
+ className = "",
387
+ successColor = "#22c55e",
388
+ warningColor = "#eab308",
389
+ errorColor = "#ef4444"
390
+ }) {
391
+ if (!result) return null;
392
+ const baseStyles = {
393
+ display: "flex",
394
+ alignItems: "center",
395
+ gap: "0.75rem",
396
+ borderRadius: "0.5rem",
397
+ padding: "1rem",
398
+ border: "1px solid"
399
+ };
400
+ if (result.success && !result.isLowConfidence) {
401
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
402
+ "div",
403
+ {
404
+ className,
405
+ style: {
406
+ ...baseStyles,
407
+ backgroundColor: `${successColor}10`,
408
+ borderColor: `${successColor}33`
409
+ },
410
+ children: [
411
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { style: { width: "1.5rem", height: "1.5rem", flexShrink: 0 }, fill: successColor, viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" }) }),
412
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column" }, children: [
413
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontWeight: 600, color: "#111827" }, children: "Face detected successfully \u2705" }),
414
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "0.875rem", color: "#6b7280" }, children: [
415
+ "Confidence: ",
416
+ Math.round(result.confidence * 100),
417
+ "%"
418
+ ] })
419
+ ] })
420
+ ]
421
+ }
422
+ );
423
+ }
424
+ if (result.success && result.isLowConfidence) {
425
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
426
+ "div",
427
+ {
428
+ className,
429
+ style: {
430
+ ...baseStyles,
431
+ backgroundColor: `${warningColor}10`,
432
+ borderColor: `${warningColor}33`
433
+ },
434
+ children: [
435
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { style: { width: "1.5rem", height: "1.5rem", flexShrink: 0 }, fill: warningColor, viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" }) }),
436
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column" }, children: [
437
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontWeight: 600, color: "#111827" }, children: "Face detected but image quality is low" }),
438
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "0.875rem", color: "#6b7280" }, children: [
439
+ "Confidence: ",
440
+ Math.round(result.confidence * 100),
441
+ "% \u2014 Please retake for better results."
442
+ ] })
443
+ ] })
444
+ ]
445
+ }
446
+ );
447
+ }
448
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
449
+ "div",
450
+ {
451
+ className,
452
+ style: {
453
+ ...baseStyles,
454
+ backgroundColor: `${errorColor}10`,
455
+ borderColor: `${errorColor}33`
456
+ },
457
+ children: [
458
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { style: { width: "1.5rem", height: "1.5rem", flexShrink: 0 }, fill: errorColor, viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" }) }),
459
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column" }, children: [
460
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontWeight: 600, color: "#111827" }, children: "No face detected" }),
461
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "0.875rem", color: "#6b7280" }, children: result.errorMessage || "Please take a clear photo of your face." })
462
+ ] })
463
+ ]
464
+ }
465
+ );
466
+ }
467
+
468
+ // src/components/LoadingState.tsx
469
+ var import_jsx_runtime4 = require("react/jsx-runtime");
470
+ function LoadingState({
471
+ isLoading,
472
+ error,
473
+ onRetry,
474
+ loadingText = "Loading Face Detection",
475
+ errorText = "Failed to Load Model",
476
+ className = ""
477
+ }) {
478
+ const containerStyles = {
479
+ display: "flex",
480
+ flexDirection: "column",
481
+ alignItems: "center",
482
+ justifyContent: "center",
483
+ gap: "1rem",
484
+ padding: "2rem"
485
+ };
486
+ if (isLoading) {
487
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: containerStyles, className, children: [
488
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: {
489
+ width: "3rem",
490
+ height: "3rem",
491
+ border: "4px solid #3b82f6",
492
+ borderTopColor: "transparent",
493
+ borderRadius: "50%",
494
+ animation: "spin 1s linear infinite"
495
+ } }),
496
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` }),
497
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { textAlign: "center" }, children: [
498
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { fontWeight: 600, color: "#111827" }, children: loadingText }),
499
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { fontSize: "0.875rem", color: "#6b7280" }, children: "Preparing AI model for offline use..." })
500
+ ] })
501
+ ] });
502
+ }
503
+ if (error) {
504
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: containerStyles, className, children: [
505
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("svg", { style: { width: "3rem", height: "3rem", color: "#ef4444" }, fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" }) }),
506
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { textAlign: "center" }, children: [
507
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { fontWeight: 600, color: "#111827" }, children: errorText }),
508
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { fontSize: "0.875rem", color: "#6b7280", marginBottom: "1rem" }, children: error }),
509
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
510
+ "button",
511
+ {
512
+ onClick: onRetry,
513
+ style: {
514
+ display: "inline-flex",
515
+ alignItems: "center",
516
+ gap: "0.5rem",
517
+ padding: "0.5rem 1rem",
518
+ backgroundColor: "#3b82f6",
519
+ color: "white",
520
+ borderRadius: "0.5rem",
521
+ border: "none",
522
+ cursor: "pointer",
523
+ fontSize: "0.875rem",
524
+ fontWeight: 500
525
+ },
526
+ children: [
527
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("svg", { style: { width: "1rem", height: "1rem" }, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) }),
528
+ "Retry"
529
+ ]
530
+ }
531
+ )
532
+ ] })
533
+ ] });
534
+ }
535
+ return null;
536
+ }
537
+
538
+ // src/components/CaptureButton.tsx
539
+ var import_react4 = require("react");
540
+ var import_camera = require("@capacitor/camera");
541
+ var import_jsx_runtime5 = require("react/jsx-runtime");
542
+ function CaptureButton({
543
+ onImageCaptured,
544
+ isProcessing = false,
545
+ showRetake = false,
546
+ onRetake,
547
+ defaultFacing = "front",
548
+ allowCameraSwitch = true,
549
+ quality = 90,
550
+ onError,
551
+ className = ""
552
+ }) {
553
+ const [isCapturing, setIsCapturing] = (0, import_react4.useState)(false);
554
+ const [cameraFacing, setCameraFacing] = (0, import_react4.useState)(defaultFacing);
555
+ const capturePhoto = (0, import_react4.useCallback)(async () => {
556
+ try {
557
+ setIsCapturing(true);
558
+ const photo = await import_camera.Camera.getPhoto({
559
+ quality,
560
+ allowEditing: false,
561
+ resultType: import_camera.CameraResultType.DataUrl,
562
+ source: import_camera.CameraSource.Camera,
563
+ correctOrientation: true,
564
+ direction: cameraFacing === "front" ? import_camera.CameraDirection.Front : import_camera.CameraDirection.Rear
565
+ });
566
+ if (photo.dataUrl) {
567
+ onImageCaptured(photo.dataUrl);
568
+ }
569
+ } catch (error) {
570
+ console.error("Camera error:", error);
571
+ const errorMessage = error instanceof Error ? error.message : String(error);
572
+ if (errorMessage.includes("denied") || errorMessage.includes("permission")) {
573
+ onError?.("Camera permission denied. Please enable camera access in your device settings.");
574
+ } else if (!errorMessage.includes("cancelled") && !errorMessage.includes("canceled")) {
575
+ onError?.("Unable to access camera. Please try again.");
576
+ }
577
+ } finally {
578
+ setIsCapturing(false);
579
+ }
580
+ }, [onImageCaptured, cameraFacing, quality, onError]);
581
+ const handleRetake = (0, import_react4.useCallback)(() => {
582
+ onRetake?.();
583
+ capturePhoto();
584
+ }, [onRetake, capturePhoto]);
585
+ const isDisabled = isCapturing || isProcessing;
586
+ const buttonBaseStyles = {
587
+ width: "100%",
588
+ maxWidth: "20rem",
589
+ display: "flex",
590
+ alignItems: "center",
591
+ justifyContent: "center",
592
+ gap: "0.5rem",
593
+ padding: "0.75rem 1.5rem",
594
+ color: "white",
595
+ borderRadius: "0.5rem",
596
+ border: "none",
597
+ cursor: isDisabled ? "not-allowed" : "pointer",
598
+ opacity: isDisabled ? 0.5 : 1,
599
+ fontSize: "1rem",
600
+ fontWeight: 500,
601
+ transition: "background-color 0.2s"
602
+ };
603
+ const toggleButtonStyles = (isActive) => ({
604
+ flex: 1,
605
+ display: "flex",
606
+ alignItems: "center",
607
+ justifyContent: "center",
608
+ gap: "0.5rem",
609
+ padding: "0.5rem 1rem",
610
+ borderRadius: "0.5rem",
611
+ border: "1px solid",
612
+ backgroundColor: isActive ? "#3b82f6" : "white",
613
+ color: isActive ? "white" : "#374151",
614
+ borderColor: isActive ? "#3b82f6" : "#d1d5db",
615
+ cursor: "pointer",
616
+ fontSize: "0.875rem",
617
+ fontWeight: 500,
618
+ transition: "all 0.2s"
619
+ });
620
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: "1rem", width: "100%" }, className, children: [
621
+ allowCameraSwitch && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", gap: "0.5rem", width: "100%", maxWidth: "20rem" }, children: [
622
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
623
+ "button",
624
+ {
625
+ onClick: () => setCameraFacing("front"),
626
+ style: toggleButtonStyles(cameraFacing === "front"),
627
+ children: [
628
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("svg", { style: { width: "1rem", height: "1rem" }, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) }),
629
+ "Selfie"
630
+ ]
631
+ }
632
+ ),
633
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
634
+ "button",
635
+ {
636
+ onClick: () => setCameraFacing("rear"),
637
+ style: toggleButtonStyles(cameraFacing === "rear"),
638
+ children: [
639
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { style: { width: "1rem", height: "1rem" }, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: [
640
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" }),
641
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 13a3 3 0 11-6 0 3 3 0 016 0z" })
642
+ ] }),
643
+ "Rear"
644
+ ]
645
+ }
646
+ )
647
+ ] }),
648
+ showRetake ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
649
+ "button",
650
+ {
651
+ onClick: handleRetake,
652
+ disabled: isDisabled,
653
+ style: { ...buttonBaseStyles, backgroundColor: "#eab308" },
654
+ children: isCapturing ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { width: "1.25rem", height: "1.25rem", border: "2px solid white", borderTopColor: "transparent", borderRadius: "50%", animation: "spin 1s linear infinite" } }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
655
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("svg", { style: { width: "1.25rem", height: "1.25rem" }, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) }),
656
+ "Retake Photo"
657
+ ] })
658
+ }
659
+ ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
660
+ "button",
661
+ {
662
+ onClick: capturePhoto,
663
+ disabled: isDisabled,
664
+ style: { ...buttonBaseStyles, backgroundColor: "#3b82f6" },
665
+ children: isCapturing ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { width: "1.25rem", height: "1.25rem", border: "2px solid white", borderTopColor: "transparent", borderRadius: "50%", animation: "spin 1s linear infinite" } }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
666
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { style: { width: "1.25rem", height: "1.25rem" }, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: [
667
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" }),
668
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 13a3 3 0 11-6 0 3 3 0 016 0z" })
669
+ ] }),
670
+ "Capture Photo"
671
+ ] })
672
+ }
673
+ ),
674
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` }),
675
+ isProcessing && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", color: "#6b7280" }, children: [
676
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { width: "1rem", height: "1rem", border: "2px solid #3b82f6", borderTopColor: "transparent", borderRadius: "50%", animation: "spin 1s linear infinite" } }),
677
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: "Analyzing face..." })
678
+ ] })
679
+ ] });
680
+ }
681
+
682
+ // src/components/FaceDetector.tsx
683
+ var import_jsx_runtime6 = require("react/jsx-runtime");
684
+ function FaceDetector({
685
+ // Detection config
686
+ confidenceThreshold = 0.7,
687
+ maxImageSize = 640,
688
+ showOverlay = true,
689
+ showResult = true,
690
+ onDetectionComplete,
691
+ onError,
692
+ // Camera config
693
+ defaultFacing = "front",
694
+ allowCameraSwitch = true,
695
+ quality = 90,
696
+ // Styling
697
+ className = ""
698
+ }) {
699
+ const [capturedImage, setCapturedImage] = (0, import_react5.useState)(null);
700
+ const [detectionResult, setDetectionResult] = (0, import_react5.useState)(null);
701
+ const [detectedFace, setDetectedFace] = (0, import_react5.useState)(null);
702
+ const [isProcessing, setIsProcessing] = (0, import_react5.useState)(false);
703
+ const { isModelLoading, modelError, detectFace, retryModelLoad } = useFaceDetectionCore({
704
+ confidenceThreshold,
705
+ maxImageSize
706
+ });
707
+ const handleImageCaptured = (0, import_react5.useCallback)((imageDataUrl) => {
708
+ setCapturedImage(imageDataUrl);
709
+ setDetectionResult(null);
710
+ setDetectedFace(null);
711
+ }, []);
712
+ const handleImageLoad = (0, import_react5.useCallback)(async (img) => {
713
+ setIsProcessing(true);
714
+ try {
715
+ const result = await detectFace(img);
716
+ setDetectionResult(result);
717
+ if (result.success && result.face) {
718
+ setDetectedFace(result.face);
719
+ } else {
720
+ setDetectedFace(null);
721
+ }
722
+ onDetectionComplete?.(result);
723
+ } catch (error) {
724
+ console.error("Detection error:", error);
725
+ onError?.("An error occurred during detection");
726
+ } finally {
727
+ setIsProcessing(false);
728
+ }
729
+ }, [detectFace, onDetectionComplete, onError]);
730
+ const handleRetake = (0, import_react5.useCallback)(() => {
731
+ setCapturedImage(null);
732
+ setDetectionResult(null);
733
+ setDetectedFace(null);
734
+ }, []);
735
+ const handleCameraError = (0, import_react5.useCallback)((error) => {
736
+ onError?.(error);
737
+ }, [onError]);
738
+ const showRetakeButton = detectionResult !== null && (!detectionResult.success || detectionResult.isLowConfidence);
739
+ const containerStyles = {
740
+ display: "flex",
741
+ flexDirection: "column",
742
+ alignItems: "center",
743
+ gap: "1.5rem",
744
+ maxWidth: "32rem",
745
+ margin: "0 auto",
746
+ padding: "1rem 0"
747
+ };
748
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: containerStyles, className, children: [
749
+ (isModelLoading || modelError) && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
750
+ LoadingState,
751
+ {
752
+ isLoading: isModelLoading,
753
+ error: modelError,
754
+ onRetry: retryModelLoad
755
+ }
756
+ ),
757
+ !isModelLoading && !modelError && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
758
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
759
+ ImagePreviewWithOverlay,
760
+ {
761
+ imageSrc: capturedImage,
762
+ face: detectedFace,
763
+ onImageLoad: handleImageLoad,
764
+ showOverlay
765
+ }
766
+ ),
767
+ showResult && detectionResult && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { width: "100%", maxWidth: "24rem" }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DetectionResultDisplay, { result: detectionResult }) }),
768
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { width: "100%", maxWidth: "24rem" }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
769
+ CaptureButton,
770
+ {
771
+ onImageCaptured: handleImageCaptured,
772
+ isProcessing,
773
+ showRetake: showRetakeButton,
774
+ onRetake: handleRetake,
775
+ defaultFacing,
776
+ allowCameraSwitch,
777
+ quality,
778
+ onError: handleCameraError
779
+ }
780
+ ) }),
781
+ !capturedImage && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { textAlign: "center", fontSize: "0.875rem", color: "#6b7280", padding: "0 1rem" }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "Take a clear photo of your face. The app will detect your face and show a confidence score." }) })
782
+ ] })
783
+ ] });
784
+ }
785
+ // Annotate the CommonJS export names for ESM import in node:
786
+ 0 && (module.exports = {
787
+ CaptureButton,
788
+ DetectionResultDisplay,
789
+ FaceDetector,
790
+ FaceOverlay,
791
+ ImagePreviewWithOverlay,
792
+ LoadingState,
793
+ useFaceDetectionCore
794
+ });