diff --git a/src/components/calibration/CalibrationCanvas.vue b/src/components/calibration/CalibrationCanvas.vue
new file mode 100644
index 0000000..f4049e8
--- /dev/null
+++ b/src/components/calibration/CalibrationCanvas.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
diff --git a/src/components/calibration/WebcamManager.vue b/src/components/calibration/WebcamManager.vue
new file mode 100644
index 0000000..db097bf
--- /dev/null
+++ b/src/components/calibration/WebcamManager.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
diff --git a/src/services/ml/FaceDetector.js b/src/services/ml/FaceDetector.js
new file mode 100644
index 0000000..f032fa5
--- /dev/null
+++ b/src/services/ml/FaceDetector.js
@@ -0,0 +1,85 @@
+class FaceDetector {
+ constructor(model) {
+ this.model = model;
+ }
+
+ /**
+ * Estimates the face landmarks from a given video element.
+ * Includes defensive checking for video readiness.
+ *
+ * @param {HTMLVideoElement} videoElement The video element to analyze
+ * @returns {Promise} The prediction array from TensorFlow
+ */
+ async detectFace(videoElement) {
+ if (!this.model) {
+ throw new Error("TensorFlow Face Landmarks Model not loaded.");
+ }
+
+ if (!videoElement || videoElement.videoWidth === 0 || videoElement.videoHeight === 0) {
+ console.warn('FaceDetector: Video not ready or has 0 dimensions.');
+ // Return null rather than hanging or looping internally, letting the caller retry
+ return null;
+ }
+
+ try {
+ const predictions = await this.model.estimateFaces({
+ input: videoElement,
+ });
+ return predictions;
+ } catch (error) {
+ console.error("FaceDetector: Error estimating faces", error);
+ return null;
+ }
+ }
+
+ /**
+ * Helper utility to safely extract exact iris and eyelid data
+ * and detect blinks.
+ *
+ * @param {Object} prediction A single face prediction object from the model
+ * @param {Number} leftEyeThreshold
+ * @param {Number} rightEyeThreshold
+ * @returns {Object|null} Formatted prediction data or null if invalid/blinking
+ */
+ processPrediction(prediction, leftEyeThreshold, rightEyeThreshold) {
+ if (!prediction || !prediction.annotations || !prediction.annotations.leftEyeIris || !prediction.annotations.rightEyeIris) {
+ return null;
+ }
+
+ const annotations = prediction.annotations;
+
+ // left eye
+ const leftIris = annotations.leftEyeIris;
+ const leftEyelid = annotations.leftEyeUpper0.concat(annotations.leftEyeLower0);
+ const leftEyelidTip = leftEyelid[3];
+ const leftEyelidBottom = leftEyelid[11];
+ const leftDistance = this._calculateDistance(leftEyelidTip, leftEyelidBottom);
+ const isLeftBlink = leftDistance < leftEyeThreshold;
+
+ // right eye
+ const rightIris = annotations.rightEyeIris;
+ const rightEyelid = annotations.rightEyeUpper0.concat(annotations.rightEyeLower0);
+ const rightEyelidTip = rightEyelid[3];
+ const rightEyelidBottom = rightEyelid[11];
+ const rightDistance = this._calculateDistance(rightEyelidTip, rightEyelidBottom);
+ const isRightBlink = rightDistance < rightEyeThreshold;
+
+ if (isLeftBlink || isRightBlink) {
+ return { isBlinking: true };
+ }
+
+ return {
+ isBlinking: false,
+ leftIris: leftIris[0],
+ rightIris: rightIris[0]
+ };
+ }
+
+ _calculateDistance(point1, point2) {
+ const xDistance = point2[0] - point1[0];
+ const yDistance = point2[1] - point1[1];
+ return Math.sqrt(xDistance * xDistance + yDistance * yDistance);
+ }
+}
+
+export default FaceDetector;
diff --git a/src/views/DoubleCalibrationRecord.vue b/src/views/DoubleCalibrationRecord.vue
index 22c7adb..542c9e8 100644
--- a/src/views/DoubleCalibrationRecord.vue
+++ b/src/views/DoubleCalibrationRecord.vue
@@ -250,28 +250,38 @@
-
-
+
+