feng-x commited on
Commit
ce7e647
·
verified ·
1 Parent(s): 152c440

Upload folder using huggingface_hub

Browse files
web_demo/app.py CHANGED
@@ -10,10 +10,12 @@ import csv
10
  import io
11
  import json
12
  import os
 
13
  import sys
14
  import uuid
 
15
  from pathlib import Path
16
- from typing import Dict, Any
17
 
18
  import cv2
19
  import numpy as np
@@ -43,6 +45,19 @@ def _allowed_file(filename: str) -> bool:
43
  return Path(filename).suffix.lower() in ALLOWED_EXTENSIONS
44
 
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  class _NumpyEncoder(json.JSONEncoder):
47
  """Handle numpy types that aren't natively JSON serializable."""
48
  def default(self, obj):
@@ -86,7 +101,13 @@ def _save_json(path: Path, data: Dict[str, Any]) -> None:
86
 
87
  @app.route("/")
88
  def index():
89
- return render_template("index.html", default_sample_url=DEFAULT_SAMPLE_URL)
 
 
 
 
 
 
90
 
91
 
92
  @app.route("/results/<path:filename>")
@@ -117,9 +138,9 @@ def api_measure():
117
  ring_model = request.form.get("ring_model", DEFAULT_RING_MODEL)
118
  if ring_model not in VALID_RING_MODELS:
119
  ring_model = DEFAULT_RING_MODEL
120
- run_id = uuid.uuid4().hex[:12]
121
- safe_name = secure_filename(file.filename)
122
- upload_name = f"{run_id}__{safe_name}"
123
  upload_path = UPLOAD_DIR / upload_name
124
  upload_path.parent.mkdir(parents=True, exist_ok=True)
125
  file.save(upload_path)
@@ -136,6 +157,8 @@ def api_measure():
136
  kol_name=kol_name,
137
  upload_path=upload_path,
138
  upload_name=upload_name,
 
 
139
  )
140
 
141
  return _run_measurement(
@@ -146,6 +169,8 @@ def api_measure():
146
  kol_name=kol_name,
147
  upload_path=upload_path,
148
  upload_name=upload_name,
 
 
149
  )
150
 
151
 
@@ -164,12 +189,16 @@ def api_measure_default():
164
  if image is None:
165
  return jsonify({"success": False, "error": "Failed to load default sample image"}), 500
166
 
 
 
167
  if mode == "multi":
168
  return _run_multi_measurement(
169
  image=image,
170
  input_image_url=DEFAULT_SAMPLE_URL,
171
  ring_model=ring_model,
172
  kol_name=kol_name,
 
 
173
  )
174
 
175
  return _run_measurement(
@@ -178,6 +207,8 @@ def api_measure_default():
178
  input_image_url=DEFAULT_SAMPLE_URL,
179
  ring_model=ring_model,
180
  kol_name=kol_name,
 
 
181
  )
182
 
183
 
@@ -189,10 +220,13 @@ def _run_measurement(
189
  kol_name: str = "",
190
  upload_path: Path = None,
191
  upload_name: str = "",
 
 
192
  ):
193
- run_id = uuid.uuid4().hex[:12]
 
194
 
195
- result_png_name = f"{run_id}__result.png"
196
  result_png_path = RESULTS_DIR / result_png_name
197
 
198
  result = measure_finger(
@@ -217,7 +251,7 @@ def _run_measurement(
217
 
218
  result = _numpy_safe(result)
219
 
220
- result_json_name = f"{run_id}__result.json"
221
  result_json_path = RESULTS_DIR / result_json_name
222
  _save_json(result_json_path, result)
223
 
@@ -266,11 +300,14 @@ def _run_multi_measurement(
266
  kol_name: str = "",
267
  upload_path: Path = None,
268
  upload_name: str = "",
 
 
269
  ):
270
  """Run multi-finger measurement pipeline."""
271
- run_id = uuid.uuid4().hex[:12]
 
272
 
273
- result_png_name = f"{run_id}__result.png"
274
  result_png_path = RESULTS_DIR / result_png_name
275
 
276
  result = measure_multi_finger(
@@ -307,7 +344,7 @@ def _run_multi_measurement(
307
  if ai_reason:
308
  result["ai_explanation"] = ai_reason
309
 
310
- result_json_name = f"{run_id}__result.json"
311
  result_json_path = RESULTS_DIR / result_json_name
312
  _save_json(result_json_path, result)
313
 
 
10
  import io
11
  import json
12
  import os
13
+ import re
14
  import sys
15
  import uuid
16
+ from datetime import datetime
17
  from pathlib import Path
18
+ from typing import Dict, Any, Tuple
19
 
20
  import cv2
21
  import numpy as np
 
45
  return Path(filename).suffix.lower() in ALLOWED_EXTENSIONS
46
 
47
 
48
+ def _slugify(name: str) -> str:
49
+ slug = re.sub(r"[^a-zA-Z0-9]+", "-", name or "").strip("-").lower()
50
+ return slug or "anon"
51
+
52
+
53
+ def _make_base_name(kol_name: str) -> Tuple[str, str]:
54
+ """Return (base_name, run_id). base_name = '{slug}_{timestamp}_{shortid}'."""
55
+ run_id = uuid.uuid4().hex[:8]
56
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
57
+ base_name = f"{_slugify(kol_name)}_{timestamp}_{run_id}"
58
+ return base_name, run_id
59
+
60
+
61
  class _NumpyEncoder(json.JSONEncoder):
62
  """Handle numpy types that aren't natively JSON serializable."""
63
  def default(self, obj):
 
101
 
102
  @app.route("/")
103
  def index():
104
+ return render_template("index.html", default_sample_url=DEFAULT_SAMPLE_URL, dev_mode=False)
105
+
106
+
107
+ @app.route("/dev")
108
+ @app.route("/debug")
109
+ def index_dev():
110
+ return render_template("index.html", default_sample_url=DEFAULT_SAMPLE_URL, dev_mode=True)
111
 
112
 
113
  @app.route("/results/<path:filename>")
 
138
  ring_model = request.form.get("ring_model", DEFAULT_RING_MODEL)
139
  if ring_model not in VALID_RING_MODELS:
140
  ring_model = DEFAULT_RING_MODEL
141
+ base_name, run_id = _make_base_name(kol_name)
142
+ suffix = Path(secure_filename(file.filename)).suffix.lower() or ".jpg"
143
+ upload_name = f"{base_name}{suffix}"
144
  upload_path = UPLOAD_DIR / upload_name
145
  upload_path.parent.mkdir(parents=True, exist_ok=True)
146
  file.save(upload_path)
 
157
  kol_name=kol_name,
158
  upload_path=upload_path,
159
  upload_name=upload_name,
160
+ base_name=base_name,
161
+ run_id=run_id,
162
  )
163
 
164
  return _run_measurement(
 
169
  kol_name=kol_name,
170
  upload_path=upload_path,
171
  upload_name=upload_name,
172
+ base_name=base_name,
173
+ run_id=run_id,
174
  )
175
 
176
 
 
189
  if image is None:
190
  return jsonify({"success": False, "error": "Failed to load default sample image"}), 500
191
 
192
+ base_name, run_id = _make_base_name(kol_name or "sample")
193
+
194
  if mode == "multi":
195
  return _run_multi_measurement(
196
  image=image,
197
  input_image_url=DEFAULT_SAMPLE_URL,
198
  ring_model=ring_model,
199
  kol_name=kol_name,
200
+ base_name=base_name,
201
+ run_id=run_id,
202
  )
203
 
204
  return _run_measurement(
 
207
  input_image_url=DEFAULT_SAMPLE_URL,
208
  ring_model=ring_model,
209
  kol_name=kol_name,
210
+ base_name=base_name,
211
+ run_id=run_id,
212
  )
213
 
214
 
 
220
  kol_name: str = "",
221
  upload_path: Path = None,
222
  upload_name: str = "",
223
+ base_name: str = "",
224
+ run_id: str = "",
225
  ):
226
+ if not base_name:
227
+ base_name, run_id = _make_base_name(kol_name)
228
 
229
+ result_png_name = f"{base_name}_result.png"
230
  result_png_path = RESULTS_DIR / result_png_name
231
 
232
  result = measure_finger(
 
251
 
252
  result = _numpy_safe(result)
253
 
254
+ result_json_name = f"{base_name}_result.json"
255
  result_json_path = RESULTS_DIR / result_json_name
256
  _save_json(result_json_path, result)
257
 
 
300
  kol_name: str = "",
301
  upload_path: Path = None,
302
  upload_name: str = "",
303
+ base_name: str = "",
304
+ run_id: str = "",
305
  ):
306
  """Run multi-finger measurement pipeline."""
307
+ if not base_name:
308
+ base_name, run_id = _make_base_name(kol_name)
309
 
310
+ result_png_name = f"{base_name}_result.png"
311
  result_png_path = RESULTS_DIR / result_png_name
312
 
313
  result = measure_multi_finger(
 
344
  if ai_reason:
345
  result["ai_explanation"] = ai_reason
346
 
347
+ result_json_name = f"{base_name}_result.json"
348
  result_json_path = RESULTS_DIR / result_json_name
349
  _save_json(result_json_path, result)
350
 
web_demo/static/app.js CHANGED
@@ -15,13 +15,13 @@ const fingerBreakdown = document.getElementById("fingerBreakdown");
15
  const defaultSampleUrl = window.DEFAULT_SAMPLE_URL || "";
16
  const failReasonMessageMap = {
17
  card_not_detected:
18
- "Credit card not detected. Place a full card flat beside your hand.",
19
  card_not_parallel:
20
  "Card is not parallel to the camera. Keep your phone directly above and parallel to the card.",
21
  card_near_edge:
22
  "Card appears cropped. Place the entire card within the photo frame.",
23
  hand_not_detected:
24
- "Hand not detected. Include your full palm in frame and keep fingers fully visible.",
25
  finger_isolation_failed:
26
  "Could not isolate the selected finger. Keep one target finger extended and separated.",
27
  finger_not_fully_visible:
@@ -66,23 +66,24 @@ const formatFailReasonStatus = (failReason) => {
66
  }
67
 
68
  if (failReason.startsWith("quality_score_low_")) {
69
- return `Low edge quality detected. Turn on flash and retake. (${failReason})`;
70
  }
71
 
72
  if (failReason.startsWith("consistency_low_")) {
73
- return `Edge detection was inconsistent. Keep phone parallel to table and retry. (${failReason})`;
74
  }
75
 
76
  const friendlyMessage = failReasonMessageMap[failReason];
77
  if (friendlyMessage) {
78
- return `${friendlyMessage} (${failReason})`;
79
  }
80
 
81
- return `Measurement failed: ${failReason}`;
82
  };
83
 
84
- const setStatus = (text) => {
85
  statusText.textContent = text;
 
86
  };
87
 
88
  const showImage = (imgEl, frameEl, url) => {
@@ -103,16 +104,18 @@ const RING_MODEL_LABELS = { gen: "Gen1/Gen2", air: "Air" };
103
  const kolNameInput = document.getElementById("kolNameInput");
104
 
105
  const buildMeasureSettings = () => {
106
- const fingerSelect = form.querySelector('select[name="finger_index"]');
107
  const aiToggle = document.getElementById("aiExplainToggle");
108
- const mode = modeSelect ? modeSelect.value : "single";
109
  const ringModel = ringModelSelect ? ringModelSelect.value : "gen";
 
 
110
  return {
111
  finger_index: fingerSelect ? fingerSelect.value : "index",
112
  edge_method: "sobel",
113
  mode: mode,
114
  ring_model: ringModel,
115
- ai_explain: aiToggle && aiToggle.checked ? "1" : "0",
116
  kol_name: kolNameInput ? kolNameInput.value.trim() : "",
117
  };
118
  };
@@ -205,7 +208,7 @@ const renderSingleResult = (result) => {
205
  </div>`;
206
 
207
  const diamMm = result.finger_outer_diameter_cm ? (result.finger_outer_diameter_cm * 10).toFixed(1) : "—";
208
- const fingerSelect = form.querySelector('select[name="finger_index"]');
209
  const fingerName = fingerSelect ? fingerSelect.value : "finger";
210
  const capitalName = fingerName.charAt(0).toUpperCase() + fingerName.slice(1);
211
  let html = `<div class="finger-cards">
@@ -252,7 +255,7 @@ const runMeasurement = async (endpoint, formData, inputUrlFallback = "") => {
252
 
253
  if (!response.ok) {
254
  const error = await response.json();
255
- setStatus(error.error || "Measurement failed");
256
  return;
257
  }
258
 
@@ -273,10 +276,10 @@ const runMeasurement = async (endpoint, formData, inputUrlFallback = "") => {
273
  setStatus("Measurement complete. Results updated.");
274
  } else {
275
  const failReason = data?.result?.fail_reason;
276
- setStatus(formatFailReasonStatus(failReason));
277
  }
278
  } catch (error) {
279
- setStatus("Network error. Please retry.");
280
  }
281
  };
282
 
@@ -310,7 +313,7 @@ form.addEventListener("submit", async (event) => {
310
 
311
  const settings = buildMeasureSettings();
312
  if (!settings.kol_name) {
313
- setStatus("Please enter your Name / ID before measuring.");
314
  kolNameInput.focus();
315
  return;
316
  }
 
15
  const defaultSampleUrl = window.DEFAULT_SAMPLE_URL || "";
16
  const failReasonMessageMap = {
17
  card_not_detected:
18
+ "Credit card not detected. 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 is not parallel to the camera. Keep your phone directly above and parallel to the card.",
21
  card_near_edge:
22
  "Card appears cropped. Place the entire card within the photo frame.",
23
  hand_not_detected:
24
+ "Hand not detected. Place your hand flat on a plain, white background (e.g. a sheet of paper), and spread your fingers naturally.",
25
  finger_isolation_failed:
26
  "Could not isolate the selected finger. Keep one target finger extended and separated.",
27
  finger_not_fully_visible:
 
66
  }
67
 
68
  if (failReason.startsWith("quality_score_low_")) {
69
+ return "Low edge quality detected. Turn on flash and retake.";
70
  }
71
 
72
  if (failReason.startsWith("consistency_low_")) {
73
+ return "Edge detection was inconsistent. Keep phone parallel to table and retry.";
74
  }
75
 
76
  const friendlyMessage = failReasonMessageMap[failReason];
77
  if (friendlyMessage) {
78
+ return friendlyMessage;
79
  }
80
 
81
+ return "Measurement failed. Please retake the photo and try again.";
82
  };
83
 
84
+ const setStatus = (text, { error = false } = {}) => {
85
  statusText.textContent = text;
86
+ statusText.classList.toggle("error", error);
87
  };
88
 
89
  const showImage = (imgEl, frameEl, url) => {
 
104
  const kolNameInput = document.getElementById("kolNameInput");
105
 
106
  const buildMeasureSettings = () => {
107
+ const fingerSelect = form.querySelector('[name="finger_index"]');
108
  const aiToggle = document.getElementById("aiExplainToggle");
109
+ const mode = modeSelect ? modeSelect.value : "multi";
110
  const ringModel = ringModelSelect ? ringModelSelect.value : "gen";
111
+ // Hidden inputs (non-dev mode) have no `checked` property — treat as on.
112
+ const aiOn = aiToggle ? (aiToggle.type === "checkbox" ? aiToggle.checked : true) : false;
113
  return {
114
  finger_index: fingerSelect ? fingerSelect.value : "index",
115
  edge_method: "sobel",
116
  mode: mode,
117
  ring_model: ringModel,
118
+ ai_explain: aiOn ? "1" : "0",
119
  kol_name: kolNameInput ? kolNameInput.value.trim() : "",
120
  };
121
  };
 
208
  </div>`;
209
 
210
  const diamMm = result.finger_outer_diameter_cm ? (result.finger_outer_diameter_cm * 10).toFixed(1) : "—";
211
+ const fingerSelect = form.querySelector('[name="finger_index"]');
212
  const fingerName = fingerSelect ? fingerSelect.value : "finger";
213
  const capitalName = fingerName.charAt(0).toUpperCase() + fingerName.slice(1);
214
  let html = `<div class="finger-cards">
 
255
 
256
  if (!response.ok) {
257
  const error = await response.json();
258
+ setStatus(error.error || "Measurement failed", { error: true });
259
  return;
260
  }
261
 
 
276
  setStatus("Measurement complete. Results updated.");
277
  } else {
278
  const failReason = data?.result?.fail_reason;
279
+ setStatus(formatFailReasonStatus(failReason), { error: true });
280
  }
281
  } catch (error) {
282
+ setStatus("Network error. Please retry.", { error: true });
283
  }
284
  };
285
 
 
313
 
314
  const settings = buildMeasureSettings();
315
  if (!settings.kol_name) {
316
+ setStatus("Please enter your Name / ID before measuring.", { error: true });
317
  kolNameInput.focus();
318
  return;
319
  }
web_demo/static/styles.css CHANGED
@@ -133,6 +133,26 @@ body {
133
  color: var(--ink-soft);
134
  }
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  .controls {
137
  display: grid;
138
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
@@ -183,6 +203,11 @@ select,
183
  color: var(--ink-soft);
184
  }
185
 
 
 
 
 
 
186
  .content {
187
  position: relative;
188
  z-index: 1;
 
133
  color: var(--ink-soft);
134
  }
135
 
136
+ .capture-tips {
137
+ margin: 14px 0 0;
138
+ padding: 12px 16px 12px 30px;
139
+ list-style: disc;
140
+ background: rgba(191, 58, 43, 0.06);
141
+ border-left: 3px solid rgba(191, 58, 43, 0.55);
142
+ border-radius: 8px;
143
+ font-size: 0.85rem;
144
+ color: var(--ink-soft);
145
+ line-height: 1.5;
146
+ }
147
+
148
+ .capture-tips li + li {
149
+ margin-top: 4px;
150
+ }
151
+
152
+ .capture-tips strong {
153
+ color: var(--ink);
154
+ }
155
+
156
  .controls {
157
  display: grid;
158
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
 
203
  color: var(--ink-soft);
204
  }
205
 
206
+ .status.error {
207
+ color: #c0271b;
208
+ font-weight: 600;
209
+ }
210
+
211
  .content {
212
  position: relative;
213
  z-index: 1;
web_demo/templates/index.html CHANGED
@@ -26,6 +26,13 @@
26
  <span class="file-hint">JPG / PNG supported · 1080p or higher recommended</span>
27
  </label>
28
 
 
 
 
 
 
 
 
29
  <div class="controls">
30
  <label>
31
  <span>Name / ID</span>
@@ -38,6 +45,7 @@
38
  <option value="air">Air</option>
39
  </select>
40
  </label>
 
41
  <label>
42
  <span>Mode</span>
43
  <select name="mode" id="modeSelect">
@@ -62,6 +70,11 @@
62
  <span class="toggle-hint">Uses OpenAI tokens</span>
63
  </div>
64
  </label>
 
 
 
 
 
65
  </div>
66
 
67
  <button class="primary" type="submit">Start Measurement</button>
@@ -102,6 +115,7 @@
102
  </div>
103
  </div>
104
 
 
105
  <div class="panel">
106
  <div class="panel-head">
107
  <h2>JSON Output</h2>
@@ -109,6 +123,10 @@
109
  </div>
110
  <pre id="jsonOutput">{}</pre>
111
  </div>
 
 
 
 
112
  </section>
113
  </main>
114
 
 
26
  <span class="file-hint">JPG / PNG supported · 1080p or higher recommended</span>
27
  </label>
28
 
29
+ <ul class="capture-tips">
30
+ <li><strong>Turn on your phone's flash</strong> — even in daylight, it sharpens the finger edges.</li>
31
+ <li>Place a card flat next to your hand on a <strong>plain white background</strong> (a sheet of paper works great).</li>
32
+ <li>Hold the phone <strong>directly above</strong> your hand, keeping it parallel to the table.</li>
33
+ <li><strong>Spread your fingers</strong> naturally and keep your whole hand inside the frame.</li>
34
+ </ul>
35
+
36
  <div class="controls">
37
  <label>
38
  <span>Name / ID</span>
 
45
  <option value="air">Air</option>
46
  </select>
47
  </label>
48
+ {% if dev_mode %}
49
  <label>
50
  <span>Mode</span>
51
  <select name="mode" id="modeSelect">
 
70
  <span class="toggle-hint">Uses OpenAI tokens</span>
71
  </div>
72
  </label>
73
+ {% else %}
74
+ <input type="hidden" name="mode" id="modeSelect" value="multi" />
75
+ <input type="hidden" name="finger_index" value="index" />
76
+ <input type="hidden" id="aiExplainToggle" value="on" />
77
+ {% endif %}
78
  </div>
79
 
80
  <button class="primary" type="submit">Start Measurement</button>
 
115
  </div>
116
  </div>
117
 
118
+ {% if dev_mode %}
119
  <div class="panel">
120
  <div class="panel-head">
121
  <h2>JSON Output</h2>
 
123
  </div>
124
  <pre id="jsonOutput">{}</pre>
125
  </div>
126
+ {% else %}
127
+ <pre id="jsonOutput" hidden>{}</pre>
128
+ <a id="jsonLink" hidden href="#"></a>
129
+ {% endif %}
130
  </section>
131
  </main>
132