ring-sizer / src /ring_size.py
feng-x's picture
Upload folder using huggingface_hub
d6218c1 verified
"""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,
}