Spaces:
Running
Running
File size: 5,884 Bytes
a3054b6 d6218c1 a3054b6 d6218c1 a3054b6 d6218c1 a3054b6 d6218c1 a3054b6 d6218c1 a3054b6 d6218c1 a3054b6 d6218c1 a3054b6 d6218c1 a3054b6 d6218c1 a3054b6 5ec46b5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | """Ring size recommendation from calibrated finger width."""
from typing import Dict, List, Literal, Optional, Tuple
# Ring model definitions: model name → {size: inner_diameter_mm}
RING_MODELS: Dict[str, Dict[int, float]] = {
"gen": {
6: 16.9,
7: 17.7,
8: 18.6,
9: 19.4,
10: 20.3,
11: 21.1,
12: 21.9,
13: 22.7,
},
"air": {
6: 16.6,
7: 17.4,
8: 18.2,
9: 19.0,
10: 19.9,
11: 20.7,
12: 21.5,
13: 22.3,
},
}
VALID_RING_MODELS = list(RING_MODELS.keys())
DEFAULT_RING_MODEL = "gen"
# Backwards-compatible alias
RING_SIZE_CHART = RING_MODELS[DEFAULT_RING_MODEL]
def _get_sorted_sizes(ring_model: str) -> List[Tuple[int, float]]:
chart = RING_MODELS.get(ring_model, RING_MODELS[DEFAULT_RING_MODEL])
return sorted(chart.items(), key=lambda x: x[1])
def recommend_ring_size(diameter_cm: float, ring_model: str = DEFAULT_RING_MODEL) -> Optional[Dict]:
"""Recommend ring size from calibrated finger outer diameter.
Returns dict with:
- best_match: nearest ring size (int)
- best_match_inner_mm: inner diameter of best match
- range_min / range_max: recommended 2-size range
- diameter_mm: input converted to mm
- ring_model: which model chart was used
Returns None if diameter is out of reasonable range.
"""
diameter_mm = diameter_cm * 10.0
if diameter_mm < 14.0 or diameter_mm > 26.0:
return None
sorted_sizes = _get_sorted_sizes(ring_model)
# Find nearest size
best_size, best_inner = min(sorted_sizes, key=lambda x: abs(x[1] - diameter_mm))
# Find second nearest size
second_size, second_inner = min(
(s for s in sorted_sizes if s[0] != best_size),
key=lambda x: abs(x[1] - diameter_mm),
)
range_min = min(best_size, second_size)
range_max = max(best_size, second_size)
return {
"best_match": best_size,
"best_match_inner_mm": best_inner,
"range_min": range_min,
"range_max": range_max,
"diameter_mm": round(diameter_mm, 2),
"ring_model": ring_model,
}
def aggregate_ring_sizes(per_finger_results: Dict[str, Dict]) -> Dict:
"""Aggregate ring size recommendations from multiple fingers.
Args:
per_finger_results: Dict mapping finger name to measurement result dict.
Each value must have keys:
- "finger_outer_diameter_cm": float or None
- "confidence": float
- "ring_size": dict from recommend_ring_size() or None
- "fail_reason": str or None
Returns:
Dict with:
- overall_best_size: int (consensus size if one exists in all
fingers' ranges, otherwise confidence-weighted best size)
- overall_range_min: int (min of all per-finger range_min)
- overall_range_max: int (max of all per-finger range_max)
- fingers_measured: int (total attempted)
- fingers_succeeded: int (with valid measurement)
- per_finger: dict of per-finger details
- fail_reason: str or None (only if ALL fingers failed)
"""
fingers_measured = len(per_finger_results)
# Build per_finger summary
per_finger: Dict[str, Dict] = {}
for name, result in per_finger_results.items():
failed = result.get("fail_reason") is not None or result.get("ring_size") is None
rs = result.get("ring_size")
per_finger[name] = {
"diameter_cm": result.get("finger_outer_diameter_cm"),
"confidence": result.get("confidence", 0.0),
"best_match": rs["best_match"] if rs else None,
"range": [rs["range_min"], rs["range_max"]] if rs else None,
"status": "failed" if failed else "ok",
"fail_reason": result.get("fail_reason"),
}
# Filter to succeeded fingers
succeeded = {
name: info for name, info in per_finger.items() if info["status"] == "ok"
}
if not succeeded:
return {
"fail_reason": "all_fingers_failed",
"fingers_measured": fingers_measured,
"fingers_succeeded": 0,
"per_finger": per_finger,
}
# Confidence-weighted voting for best size
vote_tally: Dict[int, float] = {}
for info in succeeded.values():
size = info["best_match"]
vote_tally[size] = vote_tally.get(size, 0.0) + info["confidence"]
weighted_best_size = max(vote_tally, key=lambda s: vote_tally[s])
# Intersection-first override: if a size falls in every finger's range, prefer it
all_ranges = [set(range(info["range"][0], info["range"][1] + 1))
for info in succeeded.values()]
consensus_sizes = set.intersection(*all_ranges) if all_ranges else set()
if consensus_sizes:
# Pick the consensus size closest to the confidence-weighted winner
overall_best_size = min(consensus_sizes,
key=lambda s: abs(s - weighted_best_size))
else:
overall_best_size = weighted_best_size
# Aggregate range
overall_range_min = min(info["range"][0] for info in succeeded.values())
overall_range_max = max(info["range"][1] for info in succeeded.values())
# Ensure range covers best size
if overall_best_size < overall_range_min:
overall_range_min = overall_best_size
if overall_best_size > overall_range_max:
overall_range_max = overall_best_size
return {
"overall_best_size": overall_best_size,
"overall_range_min": overall_range_min,
"overall_range_max": overall_range_max,
"fingers_measured": fingers_measured,
"fingers_succeeded": len(succeeded),
"per_finger": per_finger,
"fail_reason": None,
}
|