feng-x commited on
Commit
f21037d
·
verified ·
1 Parent(s): 6cd4ed9

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 width. 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,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
- "Width 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 width 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,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 (mm)</th></tr></thead>
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 (Recommended)", middle: "Middle", ring: "Ring" };
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">Width: ${(pf.diameter_cm * 10).toFixed(1)} mm</div>`;
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.85rem;
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 (mm) columns below — same alignment story as
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.28;
75
- const maxRatio = T.HAND_SPAN_RATIO_MAX ?? 0.55;
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 (Recommended)",
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">Width: ${escape(widthMm)} mm</div>
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 (mm)</th></tr></thead>
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 above the cards (not below) so the user reads
93
- // "X/Y fingers measured" first and immediately understands the
94
- // cards are a per-finger breakdown rather than a single result.
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
- overlayUrl
137
- ? `
138
- <div class="panel">
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
- // Originally 0.239 (P10 of the success cohort) narrowed to 0.3 on
12
- // 2026-05-07 after user feedback that the previous bound accepted
13
- // too many "still a bit far" frames as green.
14
- const HAND_SPAN_RATIO_MIN = 0.3;
15
-
16
- // Soft amber threshold (desktop only mobile collapsed to red/green
17
- // on the same date). Between 80% of the hard threshold and the
18
- // threshold itself, the desktop UI shows "almost there".
19
- const HAND_SPAN_RATIO_AMBER = HAND_SPAN_RATIO_MIN * 0.8;
 
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. Tightened from 0.55 → 0.5 on
26
- // 2026-05-07 alongside the lower-bound bump; pending a P90 sweep of
27
- // the success cohort via analyze_hand_span.py.
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 width. 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,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
- "Width 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 width 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:
 
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;