Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files
web_demo/static/app.js
CHANGED
|
@@ -15,7 +15,7 @@ const fingerBreakdown = document.getElementById("fingerBreakdown");
|
|
| 15 |
const defaultSampleUrl = window.DEFAULT_SAMPLE_URL || "";
|
| 16 |
const failReasonMessageMap = {
|
| 17 |
card_not_detected:
|
| 18 |
-
"Card not detected. A card of standard credit card dimensions (85.6 × 54 mm) is required as a scale reference to measure your finger
|
| 19 |
card_not_parallel:
|
| 20 |
"Card scale calibration failed. Keep your phone parallel to the card. Use a card of standard credit card dimensions (85.6 × 54 mm) as the reference.",
|
| 21 |
card_near_edge:
|
|
@@ -39,11 +39,11 @@ const failReasonMessageMap = {
|
|
| 39 |
zone_localization_failed:
|
| 40 |
"Ring zone localization failed. Keep more of the finger base visible.",
|
| 41 |
width_measurement_failed:
|
| 42 |
-
"
|
| 43 |
sobel_edge_refinement_failed:
|
| 44 |
"Edge refinement failed. Turn on flash or use stronger, even lighting.",
|
| 45 |
width_unreasonable:
|
| 46 |
-
"Measured
|
| 47 |
disagreement_with_contour:
|
| 48 |
"Edge methods disagree too much. Retake with cleaner edges and more even lighting.",
|
| 49 |
all_fingers_failed:
|
|
@@ -134,7 +134,7 @@ const buildSizeRefTable = () => {
|
|
| 134 |
<div class="size-ref-table">
|
| 135 |
<h3 class="size-ref-title">Size Reference (${modelLabel})</h3>
|
| 136 |
<table>
|
| 137 |
-
<thead><tr><th>Size</th><th>Inner
|
| 138 |
<tbody>${rows}</tbody>
|
| 139 |
</table>
|
| 140 |
</div>`;
|
|
@@ -150,7 +150,7 @@ const renderMultiResult = (result) => {
|
|
| 150 |
overallSize.innerHTML = "";
|
| 151 |
|
| 152 |
// Per-finger cards: size + range + width
|
| 153 |
-
const fingerNames = { index: "Index
|
| 154 |
const fingerColors = { index: "#00dddd", middle: "#00cc44", ring: "#dd44dd" };
|
| 155 |
let html = '<div class="finger-cards">';
|
| 156 |
for (const [fn, label] of Object.entries(fingerNames)) {
|
|
@@ -158,7 +158,9 @@ const renderMultiResult = (result) => {
|
|
| 158 |
if (!pf) continue;
|
| 159 |
const color = fingerColors[fn] || "#888";
|
| 160 |
const ok = pf.status === "ok";
|
|
|
|
| 161 |
html += `<div class="finger-card" style="border-top: 3px solid ${color};">
|
|
|
|
| 162 |
<div class="finger-name">${label}</div>`;
|
| 163 |
if (ok) {
|
| 164 |
const size = pf.best_match;
|
|
@@ -168,7 +170,7 @@ const renderMultiResult = (result) => {
|
|
| 168 |
if (range) {
|
| 169 |
html += `<div class="finger-range">Range: ${range[0]} – ${range[1]}</div>`;
|
| 170 |
}
|
| 171 |
-
html += `<div class="finger-width">
|
| 172 |
} else {
|
| 173 |
html += `<div class="finger-failed">Failed</div>
|
| 174 |
<div class="finger-fail-reason">${pf.fail_reason || "unknown"}</div>`;
|
|
|
|
| 15 |
const defaultSampleUrl = window.DEFAULT_SAMPLE_URL || "";
|
| 16 |
const failReasonMessageMap = {
|
| 17 |
card_not_detected:
|
| 18 |
+
"Card not detected. A card of standard credit card dimensions (85.6 × 54 mm) is required as a scale reference to measure your finger diameter. Place the card beside your hand on a plain white background (e.g. a sheet of paper), and turn on your phone's flash.",
|
| 19 |
card_not_parallel:
|
| 20 |
"Card scale calibration failed. Keep your phone parallel to the card. Use a card of standard credit card dimensions (85.6 × 54 mm) as the reference.",
|
| 21 |
card_near_edge:
|
|
|
|
| 39 |
zone_localization_failed:
|
| 40 |
"Ring zone localization failed. Keep more of the finger base visible.",
|
| 41 |
width_measurement_failed:
|
| 42 |
+
"Diameter measurement failed. Retake with phone parallel to the table and steady focus.",
|
| 43 |
sobel_edge_refinement_failed:
|
| 44 |
"Edge refinement failed. Turn on flash or use stronger, even lighting.",
|
| 45 |
width_unreasonable:
|
| 46 |
+
"Measured diameter is out of range. Retake with the phone parallel to the table.",
|
| 47 |
disagreement_with_contour:
|
| 48 |
"Edge methods disagree too much. Retake with cleaner edges and more even lighting.",
|
| 49 |
all_fingers_failed:
|
|
|
|
| 134 |
<div class="size-ref-table">
|
| 135 |
<h3 class="size-ref-title">Size Reference (${modelLabel})</h3>
|
| 136 |
<table>
|
| 137 |
+
<thead><tr><th>Size</th><th>Inner Diameter (mm)</th></tr></thead>
|
| 138 |
<tbody>${rows}</tbody>
|
| 139 |
</table>
|
| 140 |
</div>`;
|
|
|
|
| 150 |
overallSize.innerHTML = "";
|
| 151 |
|
| 152 |
// Per-finger cards: size + range + width
|
| 153 |
+
const fingerNames = { index: "Index Finger", middle: "Middle Finger", ring: "Ring Finger" };
|
| 154 |
const fingerColors = { index: "#00dddd", middle: "#00cc44", ring: "#dd44dd" };
|
| 155 |
let html = '<div class="finger-cards">';
|
| 156 |
for (const [fn, label] of Object.entries(fingerNames)) {
|
|
|
|
| 158 |
if (!pf) continue;
|
| 159 |
const color = fingerColors[fn] || "#888";
|
| 160 |
const ok = pf.status === "ok";
|
| 161 |
+
const badge = fn === "index" ? `<span class="finger-badge">Recommended</span>` : "";
|
| 162 |
html += `<div class="finger-card" style="border-top: 3px solid ${color};">
|
| 163 |
+
${badge}
|
| 164 |
<div class="finger-name">${label}</div>`;
|
| 165 |
if (ok) {
|
| 166 |
const size = pf.best_match;
|
|
|
|
| 170 |
if (range) {
|
| 171 |
html += `<div class="finger-range">Range: ${range[0]} – ${range[1]}</div>`;
|
| 172 |
}
|
| 173 |
+
html += `<div class="finger-width">Diameter: ${(pf.diameter_cm * 10).toFixed(1)} mm</div>`;
|
| 174 |
} else {
|
| 175 |
html += `<div class="finger-failed">Failed</div>
|
| 176 |
<div class="finger-fail-reason">${pf.fail_reason || "unknown"}</div>`;
|
web_demo/static/mobile/mobile.css
CHANGED
|
@@ -724,10 +724,16 @@ body {
|
|
| 724 |
display: flex;
|
| 725 |
flex-direction: column;
|
| 726 |
gap: 10px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
margin-bottom: 12px;
|
| 728 |
}
|
| 729 |
|
| 730 |
.finger-card {
|
|
|
|
| 731 |
background: var(--sand);
|
| 732 |
border-radius: 10px;
|
| 733 |
padding: 14px 16px 16px;
|
|
@@ -735,6 +741,22 @@ body {
|
|
| 735 |
box-shadow: 0 1px 4px var(--shadow);
|
| 736 |
}
|
| 737 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
.finger-card .finger-name {
|
| 739 |
font-weight: 600;
|
| 740 |
font-size: 0.95rem;
|
|
@@ -766,7 +788,7 @@ body {
|
|
| 766 |
}
|
| 767 |
|
| 768 |
.finger-card .finger-width {
|
| 769 |
-
font-size: 0.
|
| 770 |
color: var(--ink-soft);
|
| 771 |
margin-top: 4px;
|
| 772 |
}
|
|
@@ -792,6 +814,7 @@ body {
|
|
| 792 |
text-align: center;
|
| 793 |
font-size: 0.85rem;
|
| 794 |
color: var(--ink-soft);
|
|
|
|
| 795 |
margin-bottom: 12px;
|
| 796 |
}
|
| 797 |
|
|
@@ -809,7 +832,7 @@ body {
|
|
| 809 |
margin: 0 0 8px;
|
| 810 |
color: var(--ink);
|
| 811 |
/* Center the model-label heading so it lines up with the centered
|
| 812 |
-
SIZE / Inner
|
| 813 |
the desktop. */
|
| 814 |
/*text-align: center;*/
|
| 815 |
}
|
|
|
|
| 724 |
display: flex;
|
| 725 |
flex-direction: column;
|
| 726 |
gap: 10px;
|
| 727 |
+
/* Padding-top (not margin-top) clears the floating "Recommended"
|
| 728 |
+
badge that overhangs the first card at top: -12px. Margin-top
|
| 729 |
+
would collapse with .panel-title's margin-bottom: 14px and produce
|
| 730 |
+
no visible change; padding doesn't collapse. */
|
| 731 |
+
padding-top: 16px;
|
| 732 |
margin-bottom: 12px;
|
| 733 |
}
|
| 734 |
|
| 735 |
.finger-card {
|
| 736 |
+
position: relative;
|
| 737 |
background: var(--sand);
|
| 738 |
border-radius: 10px;
|
| 739 |
padding: 14px 16px 16px;
|
|
|
|
| 741 |
box-shadow: 0 1px 4px var(--shadow);
|
| 742 |
}
|
| 743 |
|
| 744 |
+
.finger-card .finger-badge {
|
| 745 |
+
position: absolute;
|
| 746 |
+
top: -12px;
|
| 747 |
+
right: 12px;
|
| 748 |
+
font-size: 0.78rem;
|
| 749 |
+
font-weight: 700;
|
| 750 |
+
text-transform: uppercase;
|
| 751 |
+
letter-spacing: 0.06em;
|
| 752 |
+
padding: 5px 12px;
|
| 753 |
+
border-radius: 999px;
|
| 754 |
+
background: var(--accent);
|
| 755 |
+
color: #fff;
|
| 756 |
+
line-height: 1;
|
| 757 |
+
box-shadow: 0 1px 3px var(--shadow);
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
.finger-card .finger-name {
|
| 761 |
font-weight: 600;
|
| 762 |
font-size: 0.95rem;
|
|
|
|
| 788 |
}
|
| 789 |
|
| 790 |
.finger-card .finger-width {
|
| 791 |
+
font-size: 0.9rem;
|
| 792 |
color: var(--ink-soft);
|
| 793 |
margin-top: 4px;
|
| 794 |
}
|
|
|
|
| 814 |
text-align: center;
|
| 815 |
font-size: 0.85rem;
|
| 816 |
color: var(--ink-soft);
|
| 817 |
+
margin-top: 8px;
|
| 818 |
margin-bottom: 12px;
|
| 819 |
}
|
| 820 |
|
|
|
|
| 832 |
margin: 0 0 8px;
|
| 833 |
color: var(--ink);
|
| 834 |
/* Center the model-label heading so it lines up with the centered
|
| 835 |
+
SIZE / Inner Diameter (mm) columns below — same alignment story as
|
| 836 |
the desktop. */
|
| 837 |
/*text-align: center;*/
|
| 838 |
}
|
web_demo/static/mobile/steps/capture.js
CHANGED
|
@@ -71,8 +71,8 @@ function evaluateDistance(T) {
|
|
| 71 |
// green / amber / red) to a strict 3-state read: too far / OK /
|
| 72 |
// too close. The amber soft-zone is gone — clearer single-action
|
| 73 |
// feedback at the cost of fewer intermediate hints.
|
| 74 |
-
const minRatio = T.HAND_SPAN_RATIO_MIN ?? 0.
|
| 75 |
-
const maxRatio = T.HAND_SPAN_RATIO_MAX ?? 0.
|
| 76 |
|
| 77 |
let result = null;
|
| 78 |
try {
|
|
|
|
| 71 |
// green / amber / red) to a strict 3-state read: too far / OK /
|
| 72 |
// too close. The amber soft-zone is gone — clearer single-action
|
| 73 |
// feedback at the cost of fewer intermediate hints.
|
| 74 |
+
const minRatio = T.HAND_SPAN_RATIO_MIN ?? 0.3;
|
| 75 |
+
const maxRatio = T.HAND_SPAN_RATIO_MAX ?? 0.45;
|
| 76 |
|
| 77 |
let result = null;
|
| 78 |
try {
|
web_demo/static/mobile/steps/result.js
CHANGED
|
@@ -10,9 +10,9 @@ import { session, resetForRetake } from "../session.js";
|
|
| 10 |
import { formatFailReason } from "../../shared/fail-reasons.js";
|
| 11 |
|
| 12 |
const FINGER_LABEL = {
|
| 13 |
-
index: "Index
|
| 14 |
-
middle: "Middle",
|
| 15 |
-
ring: "Ring",
|
| 16 |
};
|
| 17 |
const FINGER_COLOR = {
|
| 18 |
index: "#00dddd",
|
|
@@ -35,9 +35,11 @@ function escape(s) {
|
|
| 35 |
function fingerCardHtml(fn, pf) {
|
| 36 |
const label = FINGER_LABEL[fn] || fn;
|
| 37 |
const color = FINGER_COLOR[fn] || "#888";
|
|
|
|
| 38 |
if (pf.status !== "ok") {
|
| 39 |
return `
|
| 40 |
<div class="finger-card finger-card-failed" style="border-top: 3px solid ${color};">
|
|
|
|
| 41 |
<div class="finger-name">${escape(label)}</div>
|
| 42 |
<div class="finger-failed">Failed</div>
|
| 43 |
<div class="finger-fail-reason">${escape(pf.fail_reason || "unknown")}</div>
|
|
@@ -48,11 +50,12 @@ function fingerCardHtml(fn, pf) {
|
|
| 48 |
const range = pf.range ? `${pf.range[0]} – ${pf.range[1]}` : "";
|
| 49 |
return `
|
| 50 |
<div class="finger-card" style="border-top: 3px solid ${color};">
|
|
|
|
| 51 |
<div class="finger-name">${escape(label)}</div>
|
| 52 |
<div class="finger-size-label">Size</div>
|
| 53 |
<div class="finger-size">${escape(pf.best_match)}</div>
|
| 54 |
${range ? `<div class="finger-range">Range: ${escape(range)}</div>` : ""}
|
| 55 |
-
<div class="finger-width">
|
| 56 |
</div>
|
| 57 |
`;
|
| 58 |
}
|
|
@@ -67,7 +70,7 @@ function buildSizeRefTable(ringModel) {
|
|
| 67 |
<div class="size-ref-table">
|
| 68 |
<h3 class="size-ref-title">Size Reference (${escape(modelLabel)})</h3>
|
| 69 |
<table>
|
| 70 |
-
<thead><tr><th>Size</th><th>Inner
|
| 71 |
<tbody>${rows}</tbody>
|
| 72 |
</table>
|
| 73 |
</div>
|
|
@@ -89,12 +92,12 @@ function renderRecommendation(payload, ringModel) {
|
|
| 89 |
}
|
| 90 |
const succeeded = (payload && payload.fingers_succeeded) ?? 0;
|
| 91 |
const total = (payload && payload.fingers_measured) ?? 0;
|
| 92 |
-
// Count
|
| 93 |
-
//
|
| 94 |
-
//
|
| 95 |
return `
|
| 96 |
-
<div class="finger-count">${escape(`${succeeded}/${total} fingers measured`)}</div>
|
| 97 |
${cards ? `<div class="finger-cards">${cards}</div>` : ""}
|
|
|
|
| 98 |
${buildSizeRefTable(ringModel)}
|
| 99 |
`;
|
| 100 |
}
|
|
@@ -128,22 +131,26 @@ export default {
|
|
| 128 |
</div>
|
| 129 |
`;
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
container.innerHTML = `
|
| 132 |
<section class="step step-result">
|
| 133 |
<div class="step-body">
|
| 134 |
${statusBanner}
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
<h2 class="panel-title">Result Overlay</h2>
|
| 140 |
-
<div class="image-frame show">
|
| 141 |
-
<img src="${escape(overlayUrl)}" alt="Measurement overlay" />
|
| 142 |
-
</div>
|
| 143 |
-
</div>
|
| 144 |
-
`
|
| 145 |
-
: ""
|
| 146 |
-
}
|
| 147 |
|
| 148 |
<div class="panel">
|
| 149 |
<h2 class="panel-title">Ring Size Recommendation</h2>
|
|
|
|
| 10 |
import { formatFailReason } from "../../shared/fail-reasons.js";
|
| 11 |
|
| 12 |
const FINGER_LABEL = {
|
| 13 |
+
index: "Index Finger",
|
| 14 |
+
middle: "Middle Finger",
|
| 15 |
+
ring: "Ring Finger",
|
| 16 |
};
|
| 17 |
const FINGER_COLOR = {
|
| 18 |
index: "#00dddd",
|
|
|
|
| 35 |
function fingerCardHtml(fn, pf) {
|
| 36 |
const label = FINGER_LABEL[fn] || fn;
|
| 37 |
const color = FINGER_COLOR[fn] || "#888";
|
| 38 |
+
const badge = fn === "index" ? `<span class="finger-badge">Recommended</span>` : "";
|
| 39 |
if (pf.status !== "ok") {
|
| 40 |
return `
|
| 41 |
<div class="finger-card finger-card-failed" style="border-top: 3px solid ${color};">
|
| 42 |
+
${badge}
|
| 43 |
<div class="finger-name">${escape(label)}</div>
|
| 44 |
<div class="finger-failed">Failed</div>
|
| 45 |
<div class="finger-fail-reason">${escape(pf.fail_reason || "unknown")}</div>
|
|
|
|
| 50 |
const range = pf.range ? `${pf.range[0]} – ${pf.range[1]}` : "";
|
| 51 |
return `
|
| 52 |
<div class="finger-card" style="border-top: 3px solid ${color};">
|
| 53 |
+
${badge}
|
| 54 |
<div class="finger-name">${escape(label)}</div>
|
| 55 |
<div class="finger-size-label">Size</div>
|
| 56 |
<div class="finger-size">${escape(pf.best_match)}</div>
|
| 57 |
${range ? `<div class="finger-range">Range: ${escape(range)}</div>` : ""}
|
| 58 |
+
<div class="finger-width">Diameter: ${escape(widthMm)} mm</div>
|
| 59 |
</div>
|
| 60 |
`;
|
| 61 |
}
|
|
|
|
| 70 |
<div class="size-ref-table">
|
| 71 |
<h3 class="size-ref-title">Size Reference (${escape(modelLabel)})</h3>
|
| 72 |
<table>
|
| 73 |
+
<thead><tr><th>Size</th><th>Inner Diameter (mm)</th></tr></thead>
|
| 74 |
<tbody>${rows}</tbody>
|
| 75 |
</table>
|
| 76 |
</div>
|
|
|
|
| 92 |
}
|
| 93 |
const succeeded = (payload && payload.fingers_succeeded) ?? 0;
|
| 94 |
const total = (payload && payload.fingers_measured) ?? 0;
|
| 95 |
+
// Count below the cards so it reads as a footnote/summary on mobile;
|
| 96 |
+
// the cards themselves are large enough to communicate that this is
|
| 97 |
+
// a per-finger breakdown without a leading label.
|
| 98 |
return `
|
|
|
|
| 99 |
${cards ? `<div class="finger-cards">${cards}</div>` : ""}
|
| 100 |
+
<div class="finger-count">${escape(`${succeeded}/${total} fingers measured`)}</div>
|
| 101 |
${buildSizeRefTable(ringModel)}
|
| 102 |
`;
|
| 103 |
}
|
|
|
|
| 131 |
</div>
|
| 132 |
`;
|
| 133 |
|
| 134 |
+
// The server always sends a result_image_url, but on hard failures
|
| 135 |
+
// (e.g. hand_not_detected) no PNG is actually written and the URL
|
| 136 |
+
// 404s. Soft failures (e.g. card_not_detected — hand was detected,
|
| 137 |
+
// mask was rendered) DO produce a partial overlay PNG that's useful
|
| 138 |
+
// for the user. So don't gate on `succeeded`; always try to load
|
| 139 |
+
// the image, and rely on <img onerror> to remove a broken one. CSS
|
| 140 |
+
// gives .image-frame a min-height and soft background, so an empty
|
| 141 |
+
// frame reads as a clean placeholder.
|
| 142 |
+
const overlayInner = overlayUrl
|
| 143 |
+
? `<img src="${escape(overlayUrl)}" alt="Measurement overlay"
|
| 144 |
+
onerror="this.remove()" />`
|
| 145 |
+
: "";
|
| 146 |
container.innerHTML = `
|
| 147 |
<section class="step step-result">
|
| 148 |
<div class="step-body">
|
| 149 |
${statusBanner}
|
| 150 |
+
<div class="panel">
|
| 151 |
+
<h2 class="panel-title">Result Overlay</h2>
|
| 152 |
+
<div class="image-frame show">${overlayInner}</div>
|
| 153 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
<div class="panel">
|
| 156 |
<h2 class="panel-title">Ring Size Recommendation</h2>
|
web_demo/static/preview/thresholds.js
CHANGED
|
@@ -8,25 +8,24 @@
|
|
| 8 |
|
| 9 |
window.PreviewThresholds = (function () {
|
| 10 |
// Distance: ‖landmark[5] - landmark[17]‖ / min(videoWidth, videoHeight).
|
| 11 |
-
//
|
| 12 |
-
//
|
| 13 |
-
//
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
//
|
| 17 |
-
//
|
| 18 |
-
//
|
| 19 |
-
const
|
|
|
|
| 20 |
|
| 21 |
// Upper distance bound. MediaPipe still returns landmarks when the hand
|
| 22 |
// partially clips the frame, so without an upper threshold the gate
|
| 23 |
// reads "Distance OK" even when the camera is too close — at which
|
| 24 |
// point there is no room left for the credit card and lens distortion
|
| 25 |
-
// biases the edge measurement.
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
const HAND_SPAN_RATIO_MAX = 0.5;
|
| 29 |
-
const HAND_SPAN_RATIO_MAX_AMBER = HAND_SPAN_RATIO_MAX * 0.88;
|
| 30 |
|
| 31 |
// Level: DeviceOrientationEvent beta (front-back) and gamma (left-right) in
|
| 32 |
// degrees. Tightened from 10° to 5° on 2026-05-07 — the previous
|
|
|
|
| 8 |
|
| 9 |
window.PreviewThresholds = (function () {
|
| 10 |
// Distance: ‖landmark[5] - landmark[17]‖ / min(videoWidth, videoHeight).
|
| 11 |
+
// Tightened to the calibration-corpus distribution on 2026-05-09 after a
|
| 12 |
+
// KOL repro (Saun Jayy, n=5) showed monotonic diameter inflation when
|
| 13 |
+
// hand_span_ratio drifted above the calibration P90 — every +0.01 in
|
| 14 |
+
// ratio added ~0.04 cm to the index diameter, producing a 3-size spread
|
| 15 |
+
// across one user. New bounds bracket the calibration corpus
|
| 16 |
+
// (input/calibration_dataset/jpg, n=22): min 0.308, P10 0.318, P50 0.340,
|
| 17 |
+
// P90 0.372, max 0.389. Outside this band the calibration coefficient
|
| 18 |
+
// extrapolates and the per-user variance balloons.
|
| 19 |
+
const HAND_SPAN_RATIO_MIN = 0.3; // a touch below calibration min (0.308); generous floor
|
| 20 |
+
const HAND_SPAN_RATIO_AMBER = HAND_SPAN_RATIO_MIN * 0.95;
|
| 21 |
|
| 22 |
// Upper distance bound. MediaPipe still returns landmarks when the hand
|
| 23 |
// partially clips the frame, so without an upper threshold the gate
|
| 24 |
// reads "Distance OK" even when the camera is too close — at which
|
| 25 |
// point there is no room left for the credit card and lens distortion
|
| 26 |
+
// biases the edge measurement.
|
| 27 |
+
const HAND_SPAN_RATIO_MAX = 0.45; // ≈ calibration max (0.389) + ~0.06 headroom
|
| 28 |
+
const HAND_SPAN_RATIO_MAX_AMBER = HAND_SPAN_RATIO_MAX * 0.95;
|
|
|
|
|
|
|
| 29 |
|
| 30 |
// Level: DeviceOrientationEvent beta (front-back) and gamma (left-right) in
|
| 31 |
// degrees. Tightened from 10° to 5° on 2026-05-07 — the previous
|
web_demo/static/shared/fail-reasons.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
| 8 |
|
| 9 |
export const FAIL_REASON_MESSAGES = {
|
| 10 |
card_not_detected:
|
| 11 |
-
"Card not detected. A card of standard credit card dimensions (85.6 × 54 mm) is required as a scale reference to measure your finger
|
| 12 |
card_not_parallel:
|
| 13 |
"Card scale calibration failed. Keep your phone parallel to the card. Use a card of standard credit card dimensions (85.6 × 54 mm) as the reference.",
|
| 14 |
card_near_edge:
|
|
@@ -32,11 +32,11 @@ export const FAIL_REASON_MESSAGES = {
|
|
| 32 |
zone_localization_failed:
|
| 33 |
"Ring zone localization failed. Keep more of the finger base visible.",
|
| 34 |
width_measurement_failed:
|
| 35 |
-
"
|
| 36 |
sobel_edge_refinement_failed:
|
| 37 |
"Edge refinement failed. Turn on flash or use stronger, even lighting.",
|
| 38 |
width_unreasonable:
|
| 39 |
-
"Measured
|
| 40 |
disagreement_with_contour:
|
| 41 |
"Edge methods disagree too much. Retake with cleaner edges and more even lighting.",
|
| 42 |
all_fingers_failed:
|
|
|
|
| 8 |
|
| 9 |
export const FAIL_REASON_MESSAGES = {
|
| 10 |
card_not_detected:
|
| 11 |
+
"Card not detected. A card of standard credit card dimensions (85.6 × 54 mm) is required as a scale reference to measure your finger diameter. Place the card beside your hand on a plain white background (e.g. a sheet of paper), and turn on your phone's flash.",
|
| 12 |
card_not_parallel:
|
| 13 |
"Card scale calibration failed. Keep your phone parallel to the card. Use a card of standard credit card dimensions (85.6 × 54 mm) as the reference.",
|
| 14 |
card_near_edge:
|
|
|
|
| 32 |
zone_localization_failed:
|
| 33 |
"Ring zone localization failed. Keep more of the finger base visible.",
|
| 34 |
width_measurement_failed:
|
| 35 |
+
"Diameter measurement failed. Retake with phone parallel to the table and steady focus.",
|
| 36 |
sobel_edge_refinement_failed:
|
| 37 |
"Edge refinement failed. Turn on flash or use stronger, even lighting.",
|
| 38 |
width_unreasonable:
|
| 39 |
+
"Measured diameter is out of range. Retake with the phone parallel to the table.",
|
| 40 |
disagreement_with_contour:
|
| 41 |
"Edge methods disagree too much. Retake with cleaner edges and more even lighting.",
|
| 42 |
all_fingers_failed:
|
web_demo/static/styles.css
CHANGED
|
@@ -369,6 +369,7 @@ pre {
|
|
| 369 |
}
|
| 370 |
|
| 371 |
.finger-card {
|
|
|
|
| 372 |
background: var(--sand);
|
| 373 |
border-radius: 8px;
|
| 374 |
padding: 12px;
|
|
@@ -376,6 +377,22 @@ pre {
|
|
| 376 |
box-shadow: 0 1px 4px var(--shadow);
|
| 377 |
}
|
| 378 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
.finger-card .finger-name {
|
| 380 |
font-weight: 600;
|
| 381 |
font-size: 0.9rem;
|
|
|
|
| 369 |
}
|
| 370 |
|
| 371 |
.finger-card {
|
| 372 |
+
position: relative;
|
| 373 |
background: var(--sand);
|
| 374 |
border-radius: 8px;
|
| 375 |
padding: 12px;
|
|
|
|
| 377 |
box-shadow: 0 1px 4px var(--shadow);
|
| 378 |
}
|
| 379 |
|
| 380 |
+
.finger-card .finger-badge {
|
| 381 |
+
position: absolute;
|
| 382 |
+
top: -10px;
|
| 383 |
+
right: 10px;
|
| 384 |
+
font-size: 0.72rem;
|
| 385 |
+
font-weight: 700;
|
| 386 |
+
text-transform: uppercase;
|
| 387 |
+
letter-spacing: 0.06em;
|
| 388 |
+
padding: 4px 10px;
|
| 389 |
+
border-radius: 999px;
|
| 390 |
+
background: var(--accent);
|
| 391 |
+
color: #fff;
|
| 392 |
+
line-height: 1;
|
| 393 |
+
box-shadow: 0 1px 3px var(--shadow);
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
.finger-card .finger-name {
|
| 397 |
font-weight: 600;
|
| 398 |
font-size: 0.9rem;
|