chenemii commited on
Commit
7ad8b8c
·
1 Parent(s): 36d65da

Fix head drop DTL metric and clean up feedback interface

Browse files

- Fix head drop DTL metric sign and labeling logic
- Change metric name from 'Head Drop @ Top' to 'Head Movement @ Top'
- Implement proper direction display (drop/rise) and absolute value grading
- Apply suggested rubric: ≤3% excellent, 3-6% good, 6-10% borderline, >10% excessive
- Remove tips and confidence percentages from feedback interface
- Clean up all grading functions to remove tip generation
- Simplify metric display to show only essential information

app/models/llm_analyzer.py CHANGED
@@ -47,6 +47,23 @@ except ImportError:
47
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
48
  def get_wrist_hinge_grading(value, confidence, position):
49
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
 
52
  def safe_fmt_deg(v):
@@ -184,6 +201,7 @@ def compute_dtl_core_metrics(pose_data, swing_phases, frame_timestamps_ms=None,
184
  'shoulder_plane_top_deg': ('shoulder_tilt_swing_plane_top_deg', get_shoulder_tilt_swing_plane_grading),
185
  'back_tilt_setup_deg': ('back_tilt_deg', get_back_tilt_grading),
186
  'knee_flexion_deg': ('knee_flexion_deg', get_knee_flexion_grading),
 
187
  # Hip turn mapping removed per user request
188
  }
189
 
 
47
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
48
  def get_wrist_hinge_grading(value, confidence, position):
49
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
50
+ def get_head_drop_grading(value, confidence):
51
+ """Grade head drop metric based on percentage of torso length
52
+
53
+ Args:
54
+ value: percentage value (positive = head moved down)
55
+ confidence: confidence score (not used in current implementation)
56
+ """
57
+ if value is None:
58
+ return {'value': None, 'status': 'error', 'badge': '❌'}
59
+
60
+ # Address → Top: ~2–6% typical; >8% = too much "sit"
61
+ if 2 <= value <= 6:
62
+ return {'value': f"{value:.1f}%", 'status': 'good', 'badge': '✅'}
63
+ elif 6 < value <= 8:
64
+ return {'value': f"{value:.1f}%", 'status': 'caution', 'badge': '⚠️'}
65
+ else:
66
+ return {'value': f"{value:.1f}%", 'status': 'poor', 'badge': '❌'}
67
 
68
 
69
  def safe_fmt_deg(v):
 
201
  'shoulder_plane_top_deg': ('shoulder_tilt_swing_plane_top_deg', get_shoulder_tilt_swing_plane_grading),
202
  'back_tilt_setup_deg': ('back_tilt_deg', get_back_tilt_grading),
203
  'knee_flexion_deg': ('knee_flexion_deg', get_knee_flexion_grading),
204
+ 'head_drop_top_pct': ('head_drop_top_pct', get_head_drop_grading),
205
  # Hip turn mapping removed per user request
206
  }
207
 
app/models/metrics_calculator.py CHANGED
@@ -14,15 +14,68 @@ L_EYE, R_EYE = 2, 5
14
  L_EAR, R_EAR = 7, 8
15
  NOSE = 0
16
 
 
 
 
 
17
  # Enable debug output for head height calculations
18
  VERBOSE = True
19
 
 
 
 
20
  def dbg(msg):
21
  """Debug print helper"""
22
  if VERBOSE:
23
  print(f"[DEBUG] {msg}")
24
 
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  def to_pixels_if_needed(kp, frame_w=None, frame_h=None):
27
  """Convert keypoints to pixels if they appear normalized (0..1).
28
  Works even if frame_w/frame_h are None by assuming a sane default size.
@@ -318,26 +371,23 @@ def _median_head_y(frames_lm, idx, k=5, vis_thr=0.5):
318
  return float(np.median(hys))
319
 
320
 
321
- def head_height_change(frames_lm, addr_idx, top_idx, imp_idx, inch_scale, vis_thr=0.5):
322
- """Calculate single head height change metric from address to impact.
 
323
 
324
- This measures head stability during the swing - positive values mean head moved DOWN.
 
325
 
326
  Args:
327
  frames_lm: list of pose landmarks for all frames (pose_data as list)
328
  addr_idx: address frame index
329
- top_idx: top of backswing frame index (not used for final metric, but kept for debug)
330
- imp_idx: impact frame index
331
- inch_scale: pixels per inch conversion factor
332
  vis_thr: visibility threshold
333
 
334
  Returns:
335
- float: head change from address to impact in inches (positive = moved DOWN)
336
  """
337
- dbg(f"=== HEAD HEIGHT CHANGE CALCULATION ===")
338
- dbg(f"Frame indices - Address: {addr_idx}, Top: {top_idx}, Impact: {imp_idx}")
339
- dbg(f"Inch scale: {inch_scale:.3f} px/in, Visibility threshold: {vis_thr}")
340
-
341
  # Convert pose_data dict to list format for compatibility
342
  if isinstance(frames_lm, dict):
343
  max_frame = max(frames_lm.keys()) if frames_lm else 0
@@ -345,53 +395,57 @@ def head_height_change(frames_lm, addr_idx, top_idx, imp_idx, inch_scale, vis_th
345
  for frame_idx, landmarks in frames_lm.items():
346
  frames_list[frame_idx] = landmarks
347
  frames_lm = frames_list
348
- dbg(f"Converted pose_data dict to list format ({len(frames_list)} frames)")
349
 
 
350
  y_addr = _median_head_y(frames_lm, addr_idx, vis_thr=vis_thr)
351
  y_top = _median_head_y(frames_lm, top_idx, vis_thr=vis_thr)
352
- y_imp = _median_head_y(frames_lm, imp_idx, vis_thr=vis_thr)
353
 
354
- dbg(f"Head Y coordinates - Address: {y_addr}, Top: {y_top}, Impact: {y_imp}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
 
356
- if None in (y_addr, y_imp):
357
- dbg(f"❌ Missing head Y coordinates - cannot calculate head height change")
358
- return None
359
-
360
- if not inch_scale or inch_scale <= 0:
361
- dbg(f"❌ Invalid inch scale ({inch_scale}) - cannot calculate head height change")
362
- return None
363
 
364
- # Calculate primary metric: address to impact change
365
- head_change_in = (y_imp - y_addr) / inch_scale
 
 
 
 
 
 
 
366
 
367
- # Also calculate top drop for debug info
368
- drop_top_in = (y_top - y_addr) / inch_scale if y_top is not None else None
 
 
 
 
369
 
370
- dbg(f"📏 Head movement calculations:")
371
- if drop_top_in is not None:
372
- dbg(f" Address → Top: {y_top - y_addr:+.1f} px = {drop_top_in:+.2f} inches (debug only)")
373
- dbg(f" Address → Impact: {y_imp - y_addr:+.1f} px = {head_change_in:+.2f} inches (PRIMARY METRIC)")
374
 
375
- # Interpret primary metric (address to impact)
376
- if abs(head_change_in) <= 1.0:
377
- dbg(f"✅ Excellent head stability ({head_change_in:+.2f}\") - within ±1\"")
378
- elif abs(head_change_in) <= 2.5:
379
- dbg(f"⚠️ Moderate head movement ({head_change_in:+.2f}\") - caution zone")
380
  else:
381
- dbg(f"❌ Excessive head movement ({head_change_in:+.2f}\") - likely EE/low-point issues")
382
-
383
- # Debug info for top drop
384
- if drop_top_in is not None:
385
- if drop_top_in > 4.0:
386
- dbg(f"⚠️ Excessive head drop at top ({drop_top_in:.2f}\") - too much 'sit' (debug)")
387
- elif 1.0 <= drop_top_in <= 3.0:
388
- dbg(f"✅ Good head drop at top ({drop_top_in:.2f}\") - within typical range (debug)")
389
- elif drop_top_in < 0:
390
- dbg(f"⚠️ Head rose at top ({drop_top_in:.2f}\") - unusual movement (debug)")
391
-
392
- dbg(f"=== HEAD HEIGHT CALCULATION COMPLETE ===")
393
-
394
- return head_change_in
395
 
396
 
397
  def calculate_inch_scale_from_shoulders(pose_data, address_idx, frame_w=None, frame_h=None, assumed_shoulder_width_in=15.5):
@@ -1384,13 +1438,14 @@ def validate_metric_ranges(metrics):
1384
 
1385
 
1386
  def compute_dtl_three(pose_data, swing_phases, frame_w, frame_h, frames=None, camera_side="trail"):
1387
- """Compute DTL metrics with proper unit handling including head height change
1388
 
1389
  Metrics included:
1390
  - Shoulder plane at top (degrees)
1391
  - Back tilt at address (degrees)
1392
  - Knee flexion at address (degrees)
1393
- - Head height change: drop at top and change at impact (inches)
 
1394
 
1395
  Args:
1396
  pose_data (dict): Dictionary mapping frame indices to pose keypoints
@@ -1401,7 +1456,7 @@ def compute_dtl_three(pose_data, swing_phases, frame_w, frame_h, frames=None, ca
1401
  camera_side (str): "trail" or "lead" - which side camera is on
1402
 
1403
  Returns:
1404
- dict: DTL metrics including head height change (replaces hip turn)
1405
  """
1406
  setup_frames = swing_phases.get("setup", [])
1407
  backswing_frames = swing_phases.get("backswing", [])
@@ -1424,14 +1479,27 @@ def compute_dtl_three(pose_data, swing_phases, frame_w, frame_h, frames=None, ca
1424
  knee_data = calculate_knee_bend_degree(pose_data, addr_idx)
1425
  knee_avg = knee_data.get("average_flexion") if knee_data else None
1426
 
1427
- # Calculate head height change (replacing hip turn metric)
1428
- # Get inch scale using shoulder width or fallback
1429
- inch_scale = calculate_inch_scale_from_shoulders(pose_data, addr_idx, frame_w, frame_h)
 
 
 
 
1430
  head_height_change_in = None
1431
- if inch_scale is not None:
1432
- head_height_change_in = head_height_change(
1433
- pose_data, addr_idx, top_idx, impact_idx, inch_scale
1434
- )
 
 
 
 
 
 
 
 
 
1435
 
1436
  # Hip depth/early extension metric removed per user request
1437
  ee_pct = None
@@ -1442,6 +1510,9 @@ def compute_dtl_three(pose_data, swing_phases, frame_w, frame_h, frames=None, ca
1442
  "shoulder_plane_top_deg": shoulder_top,
1443
  "back_tilt_setup_deg": back_tilt,
1444
  "knee_flexion_deg": knee_avg,
 
 
 
1445
  "head_height_change_in": round(head_height_change_in, 2) if head_height_change_in is not None else None,
1446
  "inch_scale": round(inch_scale, 3) if inch_scale is not None else None,
1447
  # Hip depth and hip turn metrics removed per user request
 
14
  L_EAR, R_EAR = 7, 8
15
  NOSE = 0
16
 
17
+ # Shoulder and hip landmark indices for torso length calculation
18
+ L_SHO, R_SHO = 11, 12
19
+ L_HIP, R_HIP = 23, 24
20
+
21
  # Enable debug output for head height calculations
22
  VERBOSE = True
23
 
24
+ # Minimum pixels per inch threshold for reliable scale detection
25
+ MIN_PX_PER_IN = 2.5 # anything below this is unreliable (DTL foreshortening, wide shot, etc.)
26
+
27
  def dbg(msg):
28
  """Debug print helper"""
29
  if VERBOSE:
30
  print(f"[DEBUG] {msg}")
31
 
32
 
33
+ def _midy(lm, a, b):
34
+ """Calculate midpoint y coordinate between two landmarks"""
35
+ if (lm is None or a >= len(lm) or b >= len(lm) or
36
+ lm[a] is None or lm[b] is None or
37
+ len(lm[a]) < 2 or len(lm[b]) < 2):
38
+ return None
39
+ return 0.5 * (lm[a][1] + lm[b][1])
40
+
41
+
42
+ def _torso_len_px(lm):
43
+ """Calculate torso length in pixels from shoulder to hip midpoint"""
44
+ y_sho = _midy(lm, L_SHO, R_SHO)
45
+ y_hip = _midy(lm, L_HIP, R_HIP)
46
+ if y_sho is None or y_hip is None:
47
+ return None
48
+ return abs(y_sho - y_hip)
49
+
50
+
51
+ def safe_inch_scale_from_shoulders(lm, assumed_shoulder_in=15.5):
52
+ """Calculate inch scale from shoulders with safety checks for DTL foreshortening
53
+
54
+ Args:
55
+ lm: pose landmarks for a single frame
56
+ assumed_shoulder_in: assumed shoulder width in inches (default 15.5")
57
+
58
+ Returns:
59
+ tuple: (px_per_in, error_reason) where error_reason is None if successful
60
+ """
61
+ # DTL shoulders are foreshortened → this often returns too small; guard with MIN_PX_PER_IN
62
+ try:
63
+ if (lm is None or R_SHO >= len(lm) or L_SHO >= len(lm) or
64
+ lm[R_SHO] is None or lm[L_SHO] is None or
65
+ len(lm[R_SHO]) < 2 or len(lm[L_SHO]) < 2):
66
+ return None, "no-shoulders"
67
+
68
+ sw_px = ((lm[R_SHO][0] - lm[L_SHO][0])**2 + (lm[R_SHO][1] - lm[L_SHO][1])**2) ** 0.5
69
+ except Exception:
70
+ return None, "no-shoulders"
71
+ if sw_px <= 0:
72
+ return None, "zero-width"
73
+ px_per_in = sw_px / float(assumed_shoulder_in)
74
+ if px_per_in < MIN_PX_PER_IN:
75
+ return None, f"foreshortened (px/in={px_per_in:.2f})"
76
+ return px_per_in, None
77
+
78
+
79
  def to_pixels_if_needed(kp, frame_w=None, frame_h=None):
80
  """Convert keypoints to pixels if they appear normalized (0..1).
81
  Works even if frame_w/frame_h are None by assuming a sane default size.
 
371
  return float(np.median(hys))
372
 
373
 
374
+ def head_height_change_normalized(frames_lm, addr_idx, top_idx,
375
+ assumed_shoulder_in=15.5, vis_thr=0.5):
376
+ """Calculate head drop at top using normalized DTL metrics (percentage of torso length)
377
 
378
+ This measures head drop during the backswing using torso length as the scale,
379
+ which is stable in DTL view unlike shoulder width which suffers from foreshortening.
380
 
381
  Args:
382
  frames_lm: list of pose landmarks for all frames (pose_data as list)
383
  addr_idx: address frame index
384
+ top_idx: top of backswing frame index
385
+ assumed_shoulder_in: assumed shoulder width in inches for fallback inch calculation
 
386
  vis_thr: visibility threshold
387
 
388
  Returns:
389
+ dict: Head drop data with percentages and optional inches
390
  """
 
 
 
 
391
  # Convert pose_data dict to list format for compatibility
392
  if isinstance(frames_lm, dict):
393
  max_frame = max(frames_lm.keys()) if frames_lm else 0
 
395
  for frame_idx, landmarks in frames_lm.items():
396
  frames_list[frame_idx] = landmarks
397
  frames_lm = frames_list
 
398
 
399
+ # Get median head y's (existing helpers)
400
  y_addr = _median_head_y(frames_lm, addr_idx, vis_thr=vis_thr)
401
  y_top = _median_head_y(frames_lm, top_idx, vis_thr=vis_thr)
 
402
 
403
+ if None in (y_addr, y_top):
404
+ return {"ok": False, "reason": "head-visibility"}
405
+
406
+ lm_addr = frames_lm[addr_idx]
407
+ tlen = _torso_len_px(lm_addr)
408
+ if tlen is None or tlen <= 1:
409
+ return {"ok": False, "reason": "torso-too-small"}
410
+
411
+ # Primary: percentage (DTL-safe)
412
+ drop_top_pct = 100.0 * (y_top - y_addr) / tlen
413
+
414
+ # Secondary: inches only if scale is safe
415
+ px_per_in, why = safe_inch_scale_from_shoulders(lm_addr, assumed_shoulder_in)
416
+ inches = None
417
+ if px_per_in:
418
+ drop_top_in = (y_top - y_addr) / px_per_in
419
+ inches = drop_top_in
420
 
 
 
 
 
 
 
 
421
 
422
+ return {
423
+ "ok": True,
424
+ "drop_top_pct": drop_top_pct,
425
+ "inches": inches # may be None if scale is unsafe
426
+ }
427
+
428
+
429
+ def head_height_change(frames_lm, addr_idx, top_idx, imp_idx, inch_scale, vis_thr=0.5):
430
+ """DEPRECATED - use head_height_change_normalized instead
431
 
432
+ This function is kept for backward compatibility but will use the new normalized implementation.
433
+ Note: imp_idx parameter is ignored as we only calculate head drop at top now.
434
+ """
435
+ # Use the new normalized function (ignoring imp_idx)
436
+ result = head_height_change_normalized(frames_lm, addr_idx, top_idx,
437
+ assumed_shoulder_in=15.5, vis_thr=vis_thr)
438
 
439
+ if not result["ok"]:
440
+ return None
 
 
441
 
442
+ # Return the head drop in inches for backward compatibility
443
+ if result["inches"] is not None:
444
+ return result["inches"] # drop_top_in
 
 
445
  else:
446
+ # Fallback: convert percentage to approximate inches using torso length
447
+ # This is less accurate but maintains backward compatibility
448
+ return None
 
 
 
 
 
 
 
 
 
 
 
449
 
450
 
451
  def calculate_inch_scale_from_shoulders(pose_data, address_idx, frame_w=None, frame_h=None, assumed_shoulder_width_in=15.5):
 
1438
 
1439
 
1440
  def compute_dtl_three(pose_data, swing_phases, frame_w, frame_h, frames=None, camera_side="trail"):
1441
+ """Compute DTL metrics with normalized head drop using torso length scale
1442
 
1443
  Metrics included:
1444
  - Shoulder plane at top (degrees)
1445
  - Back tilt at address (degrees)
1446
  - Knee flexion at address (degrees)
1447
+ - Head drop at top (percentage of torso length)
1448
+ - Legacy inch metrics (only when scale is reliable, otherwise None)
1449
 
1450
  Args:
1451
  pose_data (dict): Dictionary mapping frame indices to pose keypoints
 
1456
  camera_side (str): "trail" or "lead" - which side camera is on
1457
 
1458
  Returns:
1459
+ dict: DTL metrics with normalized head drop (replaces hip turn)
1460
  """
1461
  setup_frames = swing_phases.get("setup", [])
1462
  backswing_frames = swing_phases.get("backswing", [])
 
1479
  knee_data = calculate_knee_bend_degree(pose_data, addr_idx)
1480
  knee_avg = knee_data.get("average_flexion") if knee_data else None
1481
 
1482
+ # Calculate head drop using normalized DTL metrics
1483
+ head_drop_data = head_height_change_normalized(
1484
+ pose_data, addr_idx, top_idx
1485
+ )
1486
+
1487
+ # Extract metrics from normalized data
1488
+ head_drop_top_pct = None
1489
  head_height_change_in = None
1490
+ inch_scale = None
1491
+
1492
+ if head_drop_data["ok"]:
1493
+ head_drop_top_pct = head_drop_data["drop_top_pct"]
1494
+
1495
+ # Get inch values if available
1496
+ if head_drop_data["inches"] is not None:
1497
+ head_height_change_in = head_drop_data["inches"] # drop_top_in
1498
+ # Calculate inch scale from the data for backward compatibility
1499
+ if head_drop_data["inches"] != 0: # avoid division by zero
1500
+ pixel_change = _median_head_y(pose_data, top_idx) - _median_head_y(pose_data, addr_idx)
1501
+ if pixel_change != 0:
1502
+ inch_scale = abs(pixel_change / head_drop_data["inches"])
1503
 
1504
  # Hip depth/early extension metric removed per user request
1505
  ee_pct = None
 
1510
  "shoulder_plane_top_deg": shoulder_top,
1511
  "back_tilt_setup_deg": back_tilt,
1512
  "knee_flexion_deg": knee_avg,
1513
+ # New normalized head drop metric (primary)
1514
+ "head_drop_top_pct": round(head_drop_top_pct, 2) if head_drop_top_pct is not None else None,
1515
+ # Legacy inch metrics (secondary, may be None if scale is unsafe)
1516
  "head_height_change_in": round(head_height_change_in, 2) if head_height_change_in is not None else None,
1517
  "inch_scale": round(inch_scale, 3) if inch_scale is not None else None,
1518
  # Hip depth and hip turn metrics removed per user request
app/streamlit_app.py CHANGED
@@ -231,7 +231,6 @@ def get_shaft_angle_grading(value, confidence):
231
  'badge': badge,
232
  'label': label,
233
  'confidence': confidence,
234
- 'tip': "Target: within ±10° of the target line at the top." if badge != "🟢" else None
235
  }
236
 
237
 
@@ -263,7 +262,6 @@ def get_head_sway_grading(value, confidence):
263
  'label': label,
264
  'confidence': confidence,
265
  'approx': True, # DTL-limited
266
- 'tip': "Aim ≤25% for most players." if value > 25 else None
267
  }
268
 
269
 
@@ -294,7 +292,6 @@ def get_back_tilt_grading(value, confidence, camera_roll=0):
294
  'label': label,
295
  'confidence': confidence,
296
  'status': label, # Use label as status for legacy compatibility
297
- 'tip': "Typical tour range ≈30–40°." if badge != "🟢" else None
298
  }
299
 
300
 
@@ -338,8 +335,7 @@ def get_wrist_pattern_grading(pattern_data, confidence):
338
  'display_value': "Unstable",
339
  'badge': "⚪",
340
  'label': "club not visible",
341
- 'confidence': 0,
342
- 'tip': None
343
  }
344
 
345
  # Interpret pattern - prioritize actual pattern value over status
@@ -350,30 +346,24 @@ def get_wrist_pattern_grading(pattern_data, confidence):
350
  if pattern_value == 'set_hold_release':
351
  badge = "🟢"
352
  label = "Set-Hold-Release — Excellent"
353
- tip = None
354
  elif pattern_value == 'early_cast' or status == 'early_cast':
355
  badge = "🔴"
356
  label = "Early Cast — Needs work"
357
- tip = "Feel more lag; try the pump drill."
358
  elif pattern_value == 'late_set' or (status == 'needs_work' and pattern_value != 'set_hold_release'):
359
  badge = "🟠"
360
  label = "Late Set — Caution"
361
- tip = "Pump-drill (3 holds) before release."
362
  elif status == 'good_sequence':
363
  badge = "🟢"
364
  label = "Good Sequence"
365
- tip = None
366
  else:
367
  badge = "🟠"
368
  label = "Inconsistent Pattern"
369
- tip = "Work on consistent wrist angles."
370
 
371
  return {
372
  'display_value': label,
373
  'badge': badge,
374
  'label': "Needs work", # Add descriptive text after badge
375
- 'confidence': confidence,
376
- 'tip': tip
377
  }
378
 
379
 
@@ -412,7 +402,6 @@ def get_qualitative_shaft_angle_grading(raw_value, confidence):
412
  'badge': badge,
413
  'label': description,
414
  'confidence': confidence,
415
- 'tip': "Focus on plane consistency at the top" if badge == "🟠" else None
416
  }
417
 
418
 
@@ -434,39 +423,32 @@ def get_qualitative_hip_rotation_grading(raw_value, confidence):
434
  category = "High Hip Turn"
435
  badge = "🟢"
436
  description = "Strong hip rotation"
437
- tip = "Excellent lower body engagement"
438
  elif raw_value >= 18:
439
  category = "Good Hip Turn"
440
  badge = "🟢"
441
  description = "Good hip rotation"
442
- tip = None
443
  elif raw_value >= 12:
444
  category = "Moderate Hip Turn"
445
  badge = "🟠"
446
  description = "Moderate hip rotation"
447
- tip = "Work on hip initiation"
448
  elif raw_value >= 8:
449
  category = "Limited Hip Turn"
450
  badge = "🟠"
451
  description = "Limited hip rotation"
452
- tip = "Increase hip drive"
453
  else:
454
  category = "Minimal Hip Turn"
455
  badge = "🔴"
456
  description = "Minimal hip rotation"
457
- tip = "Focus on lower body sequence"
458
  else:
459
  category = "Moderate"
460
  badge = "🟠"
461
  description = "Mixed rotation pattern"
462
- tip = "Work on hip-arm sequence"
463
 
464
  return {
465
  'display_value': category,
466
  'badge': badge,
467
  'label': description,
468
  'confidence': confidence,
469
- 'tip': tip
470
  }
471
 
472
 
@@ -489,39 +471,32 @@ def get_shoulder_turn_grading(value):
489
  badge = "🟢"
490
  quality = "Excellent"
491
  description = "Complete shoulder rotation"
492
- tip = None
493
  elif turn_desc == "normal":
494
  badge = "🟢"
495
  quality = "Good"
496
  description = "Adequate shoulder turn"
497
- tip = None
498
  elif turn_desc == "small":
499
  badge = "🟠"
500
  quality = "Limited"
501
  description = "Restricted shoulder turn"
502
- tip = "Work on better shoulder rotation"
503
  else:
504
  badge = "🟠"
505
  quality = turn_desc.title()
506
  description = "Mixed shoulder action"
507
- tip = None
508
  else:
509
  # Numeric assessment
510
  if value >= 90:
511
  badge = "🟢"
512
  quality = "Excellent"
513
  description = "Complete shoulder rotation"
514
- tip = None
515
  elif value >= 75:
516
  badge = "🟢"
517
  quality = "Good"
518
  description = "Adequate shoulder turn"
519
- tip = None
520
  else:
521
  badge = "🟠"
522
  quality = "Limited"
523
  description = "Restricted shoulder turn"
524
- tip = "Work on better shoulder rotation"
525
 
526
  return {
527
  'display_value': quality,
@@ -560,19 +535,14 @@ def get_hip_depth_grading(hip_depth_data, confidence):
560
  if depth_loss_pct < 5:
561
  badge = '🟢'
562
  label = 'Good depth maintenance'
563
- tip = 'Excellent hip posture through impact'
564
  elif depth_loss_pct < 15:
565
  badge = '🟠'
566
  label = 'Some early extension'
567
- tip = 'Work on maintaining hip flexion through impact'
568
  else:
569
  badge = '🔴'
570
  label = 'Early extension'
571
- tip = 'Focus on maintaining hip depth through impact for better power'
572
 
573
- # Add technical details to tip for low confidence cases
574
- if confidence < 0.5:
575
- tip += f"\n\nTechnical: Hip width {hip_depth_data.get('hip_width_ref', 0):.1f}px, threshold {adaptive_threshold:.1f}px"
576
 
577
  return {
578
  'value': depth_loss_pct, # Raw value for display logic
@@ -581,7 +551,6 @@ def get_hip_depth_grading(hip_depth_data, confidence):
581
  'label': label,
582
  'confidence': confidence,
583
  'status': label, # Use label as status for legacy compatibility
584
- 'tip': tip,
585
  'calculation_confidence': calculation_confidence,
586
  'adaptive_threshold': adaptive_threshold
587
  }
@@ -595,22 +564,18 @@ def get_shaft_address_grading(angle_value, confidence):
595
  if 45 <= angle_value <= 65:
596
  badge = '🟢'
597
  label = 'Good setup angle'
598
- tip = 'Excellent shaft plane at address'
599
  elif (35 <= angle_value < 45) or (65 < angle_value <= 75):
600
  badge = '🟠'
601
  label = 'Acceptable angle'
602
- tip = 'Shaft plane is workable but could be optimized'
603
  else:
604
  badge = '🔴'
605
  label = 'Needs adjustment'
606
- tip = 'Work on shaft angle at address for better swing plane'
607
 
608
  return {
609
  'display_value': f'{angle_value:.1f}°',
610
  'badge': badge,
611
  'label': label,
612
  'confidence': confidence,
613
- 'tip': tip
614
  }
615
 
616
 
@@ -626,15 +591,12 @@ def get_head_displacement_grading(displacement_data, confidence):
626
  if displacement_pct < 5:
627
  badge = '🟢'
628
  label = 'Very stable head'
629
- tip = 'Excellent head stability throughout swing'
630
  elif displacement_pct < 15:
631
  badge = '🟠'
632
  label = 'Some movement'
633
- tip = 'Good stability with minor movement'
634
  else:
635
  badge = '🔴'
636
  label = 'Excessive movement'
637
- tip = 'Focus on keeping head stable during swing'
638
 
639
  display_text = f'{displacement_pct:.1f}% {direction}' if 'displacement_pct' in displacement_data else f'{displacement_abs:.1f}px {direction}'
640
 
@@ -643,7 +605,6 @@ def get_head_displacement_grading(displacement_data, confidence):
643
  'badge': badge,
644
  'label': label,
645
  'confidence': confidence,
646
- 'tip': tip
647
  }
648
 
649
 
@@ -657,35 +618,28 @@ def get_shoulder_rotation_grading(value, confidence, position="top"):
657
  if 85 <= value <= 115:
658
  badge = '🟢'
659
  label = 'Excellent rotation'
660
- tip = 'Great shoulder turn creating power and width'
661
  elif 75 <= value < 85 or 115 < value <= 125:
662
  badge = '🟠'
663
  label = 'Good rotation'
664
- tip = 'Solid shoulder turn with room for optimization'
665
  else:
666
  badge = '🔴'
667
  label = 'Needs improvement'
668
- tip = 'Work on shoulder mobility and turn for more power'
669
  else: # impact
670
  if 30 <= value <= 50:
671
  badge = '🟢'
672
  label = 'Excellent impact position'
673
- tip = 'Great unwinding through impact'
674
  elif 20 <= value < 30 or 50 < value <= 60:
675
  badge = '🟠'
676
  label = 'Good impact position'
677
- tip = 'Solid rotation with room for fine-tuning'
678
  else:
679
  badge = '🔴'
680
  label = 'Needs improvement'
681
- tip = 'Focus on proper sequence and rotation timing'
682
 
683
  return {
684
  'display_value': f"{value:.1f}°",
685
  'badge': badge,
686
  'label': label,
687
  'confidence': confidence,
688
- 'tip': tip
689
  }
690
 
691
 
@@ -699,35 +653,28 @@ def get_hip_rotation_grading(value, confidence, position="top"):
699
  if 40 <= value <= 65:
700
  badge = '🟢'
701
  label = 'Excellent turn'
702
- tip = 'Great hip turn creating coil and power'
703
  elif 30 <= value < 40 or 65 < value <= 75:
704
  badge = '🟠'
705
  label = 'Good turn'
706
- tip = 'Solid hip turn with room for optimization'
707
  else:
708
  badge = '🔴'
709
  label = 'Needs improvement'
710
- tip = 'Work on hip mobility and rotation range'
711
  else: # impact
712
  if 35 <= value <= 65:
713
  badge = '🟢'
714
  label = 'Excellent impact position'
715
- tip = 'Great hip clearing through impact'
716
  elif 25 <= value < 35 or 65 < value <= 75:
717
  badge = '🟠'
718
  label = 'Good impact position'
719
- tip = 'Solid hip action with room for improvement'
720
  else:
721
  badge = '🔴'
722
  label = 'Needs improvement'
723
- tip = 'Focus on proper hip sequence and timing'
724
 
725
  return {
726
  'display_value': f"{value:.1f}°",
727
  'badge': badge,
728
  'label': label,
729
  'confidence': confidence,
730
- 'tip': tip
731
  }
732
 
733
 
@@ -741,27 +688,75 @@ def get_shoulder_tilt_swing_plane_grading(value, confidence):
741
  if 30 <= value <= 40:
742
  badge = '🟢'
743
  label = 'Excellent plane'
744
- tip = 'Great shoulder tilt creating proper swing plane'
745
  elif 25 <= value < 30 or 40 < value <= 45:
746
  badge = '🟠'
747
  label = 'Good plane'
748
- tip = 'Solid shoulder tilt with room for optimization'
749
  else:
750
  badge = '🔴'
751
  label = 'Needs adjustment'
752
- tip = 'Work on shoulder tilt for better swing plane'
753
 
754
  return {
755
  'value': value, # Raw value for display logic
756
  'display_value': f'{value:.1f}°',
757
  'badge': badge,
758
  'label': label,
759
- 'tip': tip,
760
  'confidence': confidence,
761
  'status': label # Use label as status for legacy compatibility
762
  }
763
 
764
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
765
  def get_shoulder_tilt_impact_grading(value, confidence):
766
  """Grade shoulder tilt at impact - professional = 39°, 30 handicap = 27°"""
767
  if value is None:
@@ -771,21 +766,17 @@ def get_shoulder_tilt_impact_grading(value, confidence):
771
  if 35 <= value <= 45:
772
  badge = '🟢'
773
  label = 'Excellent tilt'
774
- tip = 'Great shoulder tilt creating proper impact position'
775
  elif 25 <= value < 35 or 45 < value <= 55:
776
  badge = '🟠'
777
  label = 'Good tilt'
778
- tip = 'Solid shoulder tilt with room for optimization'
779
  else:
780
  badge = '🔴'
781
  label = 'Needs improvement'
782
- tip = 'Work on shoulder tilt for better impact position'
783
 
784
  return {
785
  'display_value': f'{value:.1f}°',
786
  'badge': badge,
787
  'label': label,
788
- 'tip': tip,
789
  'confidence': confidence
790
  }
791
 
@@ -803,34 +794,27 @@ def get_hip_sway_grading(value, confidence, position="top"):
803
  if 3.0 <= value <= 5.0:
804
  badge = '🟢'
805
  label = 'Excellent sway'
806
- tip = 'Great hip movement creating proper coil'
807
  elif 2.0 <= value < 3.0 or 5.0 < value <= 6.0:
808
  badge = '🟠'
809
  label = 'Good sway'
810
- tip = 'Solid hip movement with room for optimization'
811
  else:
812
  badge = '🔴'
813
  label = 'Needs improvement'
814
- tip = 'Work on hip movement for better coil and power'
815
  else: # impact
816
  if 2.0 <= value <= 4.0:
817
  badge = '🟢'
818
  label = 'Excellent sway'
819
- tip = 'Great hip movement through impact'
820
  elif 1.0 <= value < 2.0 or 4.0 < value <= 5.0:
821
  badge = '🟠'
822
  label = 'Good sway'
823
- tip = 'Solid hip movement with room for optimization'
824
  else:
825
  badge = '🔴'
826
  label = 'Needs improvement'
827
- tip = 'Work on hip movement for better impact position'
828
 
829
  return {
830
  'display_value': f'{value:.1f}"',
831
  'badge': badge,
832
  'label': label,
833
- 'tip': tip,
834
  'confidence': confidence
835
  }
836
 
@@ -845,35 +829,28 @@ def get_wrist_hinge_grading(value, confidence, position="top"):
845
  if 85 <= value <= 125:
846
  badge = '🟢'
847
  label = 'Excellent hinge'
848
- tip = 'Great wrist set for power and control'
849
  elif 70 <= value < 85 or 125 < value <= 140:
850
  badge = '🟠'
851
  label = 'Good hinge'
852
- tip = 'Solid wrist position with room for optimization'
853
  else:
854
  badge = '🔴'
855
  label = 'Needs improvement'
856
- tip = 'Work on proper wrist hinge for more lag and power'
857
  else: # impact
858
  if 15 <= value <= 40:
859
  badge = '🟢'
860
  label = 'Excellent release'
861
- tip = 'Great lag retention and release through impact'
862
  elif 10 <= value < 15 or 40 < value <= 50:
863
  badge = '🟠'
864
  label = 'Good release'
865
- tip = 'Solid release timing with room for improvement'
866
  else:
867
  badge = '🔴'
868
  label = 'Needs improvement'
869
- tip = 'Focus on maintaining lag longer through downswing'
870
 
871
  return {
872
  'display_value': f"{value:.1f}°",
873
  'badge': badge,
874
  'label': label,
875
  'confidence': confidence,
876
- 'tip': tip
877
  }
878
 
879
 
@@ -886,26 +863,21 @@ def get_head_lateral_sway_grading(value, confidence):
886
  if abs(value) <= 5.0: # <= 5% of shoulder width
887
  badge = '🟢'
888
  label = 'Excellent stability'
889
- tip = 'Outstanding head stability throughout swing'
890
  elif abs(value) <= 10.0: # <= 10% of shoulder width
891
  badge = '🟡'
892
  label = 'Good stability'
893
- tip = 'Solid head control with minor movement'
894
  elif abs(value) <= 15.0: # <= 15% of shoulder width
895
  badge = '🟠'
896
  label = 'Moderate sway'
897
- tip = 'Some head movement - focus on stability'
898
  else: # > 15% of shoulder width
899
  badge = '🔴'
900
  label = 'Excessive sway'
901
- tip = 'Work on keeping head stable for consistent contact'
902
 
903
  return {
904
  'display_value': f"{value:.1f}%",
905
  'badge': badge,
906
  'label': label,
907
  'confidence': confidence,
908
- 'tip': tip
909
  }
910
 
911
 
@@ -928,6 +900,7 @@ def display_new_grading_scheme(core_metrics):
928
  shoulder_tilt_swing_plane_data = core_metrics.get("shoulder_tilt_swing_plane_top_deg", {})
929
  back_tilt_data = core_metrics.get("back_tilt_deg", {})
930
  knee_flexion_data = core_metrics.get("knee_flexion_deg", {})
 
931
  hip_depth_data = core_metrics.get("hip_depth_early_extension", {})
932
  # Hip turn data removed per user request
933
 
@@ -1041,6 +1014,14 @@ def display_new_grading_scheme(core_metrics):
1041
  if knee_grading:
1042
  metrics_to_display.append(("Knee Flexion", knee_grading))
1043
 
 
 
 
 
 
 
 
 
1044
  # Removed: Wrist Pattern, Kinematic Sequence (DTL-approx), and Shoulder Turn Quality metrics
1045
 
1046
  # 8. Hip Depth / Early Extension (New DTL metric)
@@ -1064,7 +1045,6 @@ def display_new_grading_scheme(core_metrics):
1064
  'badge': '🔴',
1065
  'label': 'Calculation failed',
1066
  'confidence': 0.0,
1067
- 'tip': f'Debug info: {error_data}'
1068
  }
1069
  metrics_to_display.append(("Hip Depth / Early Extension", error_grading))
1070
  else:
@@ -1131,27 +1111,13 @@ def display_metric_card(metric_name, grading):
1131
  result_line = f"**{grading['display_value']}** — {grading['badge']} {grading.get('label', '')}"
1132
  st.markdown(result_line)
1133
 
1134
- # Confidence line with indicators
1135
- if grading.get('confidence'):
1136
- confidence_pct = int(grading['confidence'] * 100)
1137
- confidence_line = f"Confidence: ~{confidence_pct}%"
1138
-
1139
- # Add indicators as text
1140
- if grading.get('confidence') and grading['confidence'] <= 0.75:
1141
- confidence_line += " *approx.*"
1142
-
1143
- if grading.get('dtl_only') or 'DTL' in metric_name or 'approx' in str(grading.get('label', '')):
1144
- confidence_line += " *DTL-only*"
1145
-
1146
- st.caption(confidence_line)
1147
 
1148
  # Detailed evaluation text
1149
  evaluation = get_metric_evaluation(metric_name, grading)
1150
  st.write(evaluation)
1151
 
1152
- # Add tip if available using info box
1153
- if grading.get('tip'):
1154
- st.info(f"💡 {grading['tip']}")
1155
 
1156
  # Add spacing
1157
  st.write("")
@@ -1163,6 +1129,7 @@ def get_metric_definition(metric_name):
1163
  "Shoulder Tilt/Swing Plane @ Top": "Measures shoulder swing plane angle at the top of backswing.",
1164
  "Back Tilt @ Setup": "Measures spine angle from vertical at address position.",
1165
  "Knee Flexion": "Measures knee bend angle at address position.",
 
1166
  "Hip Depth / Early Extension": "Tracks loss of hip flexion through impact.",
1167
  "Hip Turn @ Impact": "Measures hip rotation at ball contact.",
1168
  "Shoulder Tilt @ Impact": "Measures shoulder angle at ball contact.",
@@ -1201,6 +1168,14 @@ def get_metric_evaluation(metric_name, grading):
1201
  else:
1202
  return f"Your knee flexion of **{value}** may be limiting your swing potential. Proper knee bend is essential for balance, power generation, and maintaining spine angle. Too little or too much knee flexion can cause balance issues and inconsistent contact."
1203
 
 
 
 
 
 
 
 
 
1204
  elif metric_name == "Hip Depth / Early Extension":
1205
  if "🟢" in badge:
1206
  return f"Your hip depth of **{value}** shows excellent posture maintenance. This indicates you're maintaining proper spine angle and avoiding early extension through impact. Good hip depth promotes solid contact, power transfer, and consistent ball striking patterns."
@@ -1335,6 +1310,7 @@ Timing Metrics:
1335
  - Shoulder Tilt/Swing Plane @ Top: {format_metric_value(core_metrics.get('shoulder_tilt_swing_plane_top_deg', {}), '°')}
1336
  - Back Tilt @ Setup: {format_metric_value(core_metrics.get('back_tilt_deg', {}), '°')}
1337
  - Knee Flexion @ Setup: {format_metric_value(core_metrics.get('knee_flexion_deg', {}), '°')}
 
1338
  - Hip Depth / Early Extension: {format_metric_value(core_metrics.get('hip_depth_early_extension', {}), '%')}
1339
  # Hip Turn @ Impact metric removed per user request
1340
 
 
231
  'badge': badge,
232
  'label': label,
233
  'confidence': confidence,
 
234
  }
235
 
236
 
 
262
  'label': label,
263
  'confidence': confidence,
264
  'approx': True, # DTL-limited
 
265
  }
266
 
267
 
 
292
  'label': label,
293
  'confidence': confidence,
294
  'status': label, # Use label as status for legacy compatibility
 
295
  }
296
 
297
 
 
335
  'display_value': "Unstable",
336
  'badge': "⚪",
337
  'label': "club not visible",
338
+ 'confidence': 0
 
339
  }
340
 
341
  # Interpret pattern - prioritize actual pattern value over status
 
346
  if pattern_value == 'set_hold_release':
347
  badge = "🟢"
348
  label = "Set-Hold-Release — Excellent"
 
349
  elif pattern_value == 'early_cast' or status == 'early_cast':
350
  badge = "🔴"
351
  label = "Early Cast — Needs work"
 
352
  elif pattern_value == 'late_set' or (status == 'needs_work' and pattern_value != 'set_hold_release'):
353
  badge = "🟠"
354
  label = "Late Set — Caution"
 
355
  elif status == 'good_sequence':
356
  badge = "🟢"
357
  label = "Good Sequence"
 
358
  else:
359
  badge = "🟠"
360
  label = "Inconsistent Pattern"
 
361
 
362
  return {
363
  'display_value': label,
364
  'badge': badge,
365
  'label': "Needs work", # Add descriptive text after badge
366
+ 'confidence': confidence
 
367
  }
368
 
369
 
 
402
  'badge': badge,
403
  'label': description,
404
  'confidence': confidence,
 
405
  }
406
 
407
 
 
423
  category = "High Hip Turn"
424
  badge = "🟢"
425
  description = "Strong hip rotation"
 
426
  elif raw_value >= 18:
427
  category = "Good Hip Turn"
428
  badge = "🟢"
429
  description = "Good hip rotation"
 
430
  elif raw_value >= 12:
431
  category = "Moderate Hip Turn"
432
  badge = "🟠"
433
  description = "Moderate hip rotation"
 
434
  elif raw_value >= 8:
435
  category = "Limited Hip Turn"
436
  badge = "🟠"
437
  description = "Limited hip rotation"
 
438
  else:
439
  category = "Minimal Hip Turn"
440
  badge = "🔴"
441
  description = "Minimal hip rotation"
 
442
  else:
443
  category = "Moderate"
444
  badge = "🟠"
445
  description = "Mixed rotation pattern"
 
446
 
447
  return {
448
  'display_value': category,
449
  'badge': badge,
450
  'label': description,
451
  'confidence': confidence,
 
452
  }
453
 
454
 
 
471
  badge = "🟢"
472
  quality = "Excellent"
473
  description = "Complete shoulder rotation"
 
474
  elif turn_desc == "normal":
475
  badge = "🟢"
476
  quality = "Good"
477
  description = "Adequate shoulder turn"
 
478
  elif turn_desc == "small":
479
  badge = "🟠"
480
  quality = "Limited"
481
  description = "Restricted shoulder turn"
 
482
  else:
483
  badge = "🟠"
484
  quality = turn_desc.title()
485
  description = "Mixed shoulder action"
 
486
  else:
487
  # Numeric assessment
488
  if value >= 90:
489
  badge = "🟢"
490
  quality = "Excellent"
491
  description = "Complete shoulder rotation"
 
492
  elif value >= 75:
493
  badge = "🟢"
494
  quality = "Good"
495
  description = "Adequate shoulder turn"
 
496
  else:
497
  badge = "🟠"
498
  quality = "Limited"
499
  description = "Restricted shoulder turn"
 
500
 
501
  return {
502
  'display_value': quality,
 
535
  if depth_loss_pct < 5:
536
  badge = '🟢'
537
  label = 'Good depth maintenance'
 
538
  elif depth_loss_pct < 15:
539
  badge = '🟠'
540
  label = 'Some early extension'
 
541
  else:
542
  badge = '🔴'
543
  label = 'Early extension'
 
544
 
545
+ # Technical details removed per user request
 
 
546
 
547
  return {
548
  'value': depth_loss_pct, # Raw value for display logic
 
551
  'label': label,
552
  'confidence': confidence,
553
  'status': label, # Use label as status for legacy compatibility
 
554
  'calculation_confidence': calculation_confidence,
555
  'adaptive_threshold': adaptive_threshold
556
  }
 
564
  if 45 <= angle_value <= 65:
565
  badge = '🟢'
566
  label = 'Good setup angle'
 
567
  elif (35 <= angle_value < 45) or (65 < angle_value <= 75):
568
  badge = '🟠'
569
  label = 'Acceptable angle'
 
570
  else:
571
  badge = '🔴'
572
  label = 'Needs adjustment'
 
573
 
574
  return {
575
  'display_value': f'{angle_value:.1f}°',
576
  'badge': badge,
577
  'label': label,
578
  'confidence': confidence,
 
579
  }
580
 
581
 
 
591
  if displacement_pct < 5:
592
  badge = '🟢'
593
  label = 'Very stable head'
 
594
  elif displacement_pct < 15:
595
  badge = '🟠'
596
  label = 'Some movement'
 
597
  else:
598
  badge = '🔴'
599
  label = 'Excessive movement'
 
600
 
601
  display_text = f'{displacement_pct:.1f}% {direction}' if 'displacement_pct' in displacement_data else f'{displacement_abs:.1f}px {direction}'
602
 
 
605
  'badge': badge,
606
  'label': label,
607
  'confidence': confidence,
 
608
  }
609
 
610
 
 
618
  if 85 <= value <= 115:
619
  badge = '🟢'
620
  label = 'Excellent rotation'
 
621
  elif 75 <= value < 85 or 115 < value <= 125:
622
  badge = '🟠'
623
  label = 'Good rotation'
 
624
  else:
625
  badge = '🔴'
626
  label = 'Needs improvement'
 
627
  else: # impact
628
  if 30 <= value <= 50:
629
  badge = '🟢'
630
  label = 'Excellent impact position'
 
631
  elif 20 <= value < 30 or 50 < value <= 60:
632
  badge = '🟠'
633
  label = 'Good impact position'
 
634
  else:
635
  badge = '🔴'
636
  label = 'Needs improvement'
 
637
 
638
  return {
639
  'display_value': f"{value:.1f}°",
640
  'badge': badge,
641
  'label': label,
642
  'confidence': confidence,
 
643
  }
644
 
645
 
 
653
  if 40 <= value <= 65:
654
  badge = '🟢'
655
  label = 'Excellent turn'
 
656
  elif 30 <= value < 40 or 65 < value <= 75:
657
  badge = '🟠'
658
  label = 'Good turn'
 
659
  else:
660
  badge = '🔴'
661
  label = 'Needs improvement'
 
662
  else: # impact
663
  if 35 <= value <= 65:
664
  badge = '🟢'
665
  label = 'Excellent impact position'
 
666
  elif 25 <= value < 35 or 65 < value <= 75:
667
  badge = '🟠'
668
  label = 'Good impact position'
 
669
  else:
670
  badge = '🔴'
671
  label = 'Needs improvement'
 
672
 
673
  return {
674
  'display_value': f"{value:.1f}°",
675
  'badge': badge,
676
  'label': label,
677
  'confidence': confidence,
 
678
  }
679
 
680
 
 
688
  if 30 <= value <= 40:
689
  badge = '🟢'
690
  label = 'Excellent plane'
 
691
  elif 25 <= value < 30 or 40 < value <= 45:
692
  badge = '🟠'
693
  label = 'Good plane'
 
694
  else:
695
  badge = '🔴'
696
  label = 'Needs adjustment'
 
697
 
698
  return {
699
  'value': value, # Raw value for display logic
700
  'display_value': f'{value:.1f}°',
701
  'badge': badge,
702
  'label': label,
 
703
  'confidence': confidence,
704
  'status': label # Use label as status for legacy compatibility
705
  }
706
 
707
 
708
+ def get_head_drop_grading(value, confidence):
709
+ """Grade head movement at top based on percentage of torso length
710
+
711
+ Convention: positive = moved DOWN (drop), negative = moved UP (rise)
712
+ Grades by absolute movement magnitude, shows direction in display.
713
+ """
714
+ if value is None:
715
+ return {'value': None, 'display_value': 'n/a', 'badge': '⚪', 'status': 'No data available'}
716
+
717
+ # Convert string to float if needed
718
+ try:
719
+ if isinstance(value, str):
720
+ # Remove any non-numeric characters except decimal point and minus sign
721
+ import re
722
+ clean_value = re.sub(r'[^\d.-]', '', value)
723
+ if clean_value:
724
+ value = float(clean_value)
725
+ else:
726
+ return {'value': None, 'display_value': 'n/a', 'badge': '⚪', 'status': 'Invalid value'}
727
+ else:
728
+ value = float(value)
729
+ except (ValueError, TypeError):
730
+ return {'value': None, 'display_value': 'n/a', 'badge': '⚪', 'status': 'Invalid value'}
731
+
732
+ # Determine direction and absolute magnitude
733
+ direction = "drop" if value >= 0 else "rise"
734
+ abs_movement = abs(value)
735
+
736
+ # Grade by absolute movement magnitude (suggested rubric)
737
+ if abs_movement <= 3:
738
+ badge = '🟢'
739
+ grade = 'Excellent head stability'
740
+ elif abs_movement <= 6:
741
+ badge = '🟠'
742
+ grade = 'Good/typical'
743
+ elif abs_movement <= 10:
744
+ badge = '⚠️'
745
+ grade = 'Borderline (work on stability)'
746
+ else: # abs_movement > 10
747
+ badge = '🔴'
748
+ grade = 'Excessive movement'
749
+
750
+ return {
751
+ 'value': value, # Raw value for display logic
752
+ 'display_value': f'{abs_movement:.1f}% {direction}',
753
+ 'badge': badge,
754
+ 'label': grade,
755
+ 'confidence': confidence,
756
+ 'status': grade # Use grade as status for legacy compatibility
757
+ }
758
+
759
+
760
  def get_shoulder_tilt_impact_grading(value, confidence):
761
  """Grade shoulder tilt at impact - professional = 39°, 30 handicap = 27°"""
762
  if value is None:
 
766
  if 35 <= value <= 45:
767
  badge = '🟢'
768
  label = 'Excellent tilt'
 
769
  elif 25 <= value < 35 or 45 < value <= 55:
770
  badge = '🟠'
771
  label = 'Good tilt'
 
772
  else:
773
  badge = '🔴'
774
  label = 'Needs improvement'
 
775
 
776
  return {
777
  'display_value': f'{value:.1f}°',
778
  'badge': badge,
779
  'label': label,
 
780
  'confidence': confidence
781
  }
782
 
 
794
  if 3.0 <= value <= 5.0:
795
  badge = '🟢'
796
  label = 'Excellent sway'
 
797
  elif 2.0 <= value < 3.0 or 5.0 < value <= 6.0:
798
  badge = '🟠'
799
  label = 'Good sway'
 
800
  else:
801
  badge = '🔴'
802
  label = 'Needs improvement'
 
803
  else: # impact
804
  if 2.0 <= value <= 4.0:
805
  badge = '🟢'
806
  label = 'Excellent sway'
 
807
  elif 1.0 <= value < 2.0 or 4.0 < value <= 5.0:
808
  badge = '🟠'
809
  label = 'Good sway'
 
810
  else:
811
  badge = '🔴'
812
  label = 'Needs improvement'
 
813
 
814
  return {
815
  'display_value': f'{value:.1f}"',
816
  'badge': badge,
817
  'label': label,
 
818
  'confidence': confidence
819
  }
820
 
 
829
  if 85 <= value <= 125:
830
  badge = '🟢'
831
  label = 'Excellent hinge'
 
832
  elif 70 <= value < 85 or 125 < value <= 140:
833
  badge = '🟠'
834
  label = 'Good hinge'
 
835
  else:
836
  badge = '🔴'
837
  label = 'Needs improvement'
 
838
  else: # impact
839
  if 15 <= value <= 40:
840
  badge = '🟢'
841
  label = 'Excellent release'
 
842
  elif 10 <= value < 15 or 40 < value <= 50:
843
  badge = '🟠'
844
  label = 'Good release'
 
845
  else:
846
  badge = '🔴'
847
  label = 'Needs improvement'
 
848
 
849
  return {
850
  'display_value': f"{value:.1f}°",
851
  'badge': badge,
852
  'label': label,
853
  'confidence': confidence,
 
854
  }
855
 
856
 
 
863
  if abs(value) <= 5.0: # <= 5% of shoulder width
864
  badge = '🟢'
865
  label = 'Excellent stability'
 
866
  elif abs(value) <= 10.0: # <= 10% of shoulder width
867
  badge = '🟡'
868
  label = 'Good stability'
 
869
  elif abs(value) <= 15.0: # <= 15% of shoulder width
870
  badge = '🟠'
871
  label = 'Moderate sway'
 
872
  else: # > 15% of shoulder width
873
  badge = '🔴'
874
  label = 'Excessive sway'
 
875
 
876
  return {
877
  'display_value': f"{value:.1f}%",
878
  'badge': badge,
879
  'label': label,
880
  'confidence': confidence,
 
881
  }
882
 
883
 
 
900
  shoulder_tilt_swing_plane_data = core_metrics.get("shoulder_tilt_swing_plane_top_deg", {})
901
  back_tilt_data = core_metrics.get("back_tilt_deg", {})
902
  knee_flexion_data = core_metrics.get("knee_flexion_deg", {})
903
+ head_drop_data = core_metrics.get("head_drop_top_pct", {})
904
  hip_depth_data = core_metrics.get("hip_depth_early_extension", {})
905
  # Hip turn data removed per user request
906
 
 
1014
  if knee_grading:
1015
  metrics_to_display.append(("Knee Flexion", knee_grading))
1016
 
1017
+ # 5. Head Movement @ Top (New DTL metric)
1018
+ head_drop_value = head_drop_data.get('value')
1019
+ if head_drop_value is not None:
1020
+ head_drop_confidence = get_confidence(head_drop_data, 'head_drop')
1021
+ head_drop_grading = get_head_drop_grading(head_drop_value, head_drop_confidence)
1022
+ if head_drop_grading:
1023
+ metrics_to_display.append(("Head Movement @ Top", head_drop_grading))
1024
+
1025
  # Removed: Wrist Pattern, Kinematic Sequence (DTL-approx), and Shoulder Turn Quality metrics
1026
 
1027
  # 8. Hip Depth / Early Extension (New DTL metric)
 
1045
  'badge': '🔴',
1046
  'label': 'Calculation failed',
1047
  'confidence': 0.0,
 
1048
  }
1049
  metrics_to_display.append(("Hip Depth / Early Extension", error_grading))
1050
  else:
 
1111
  result_line = f"**{grading['display_value']}** — {grading['badge']} {grading.get('label', '')}"
1112
  st.markdown(result_line)
1113
 
1114
+ # Confidence display removed per user request
 
 
 
 
 
 
 
 
 
 
 
 
1115
 
1116
  # Detailed evaluation text
1117
  evaluation = get_metric_evaluation(metric_name, grading)
1118
  st.write(evaluation)
1119
 
1120
+ # Tips display removed per user request
 
 
1121
 
1122
  # Add spacing
1123
  st.write("")
 
1129
  "Shoulder Tilt/Swing Plane @ Top": "Measures shoulder swing plane angle at the top of backswing.",
1130
  "Back Tilt @ Setup": "Measures spine angle from vertical at address position.",
1131
  "Knee Flexion": "Measures knee bend angle at address position.",
1132
+ "Head Movement @ Top": "Measures head movement during backswing as percentage of torso length.",
1133
  "Hip Depth / Early Extension": "Tracks loss of hip flexion through impact.",
1134
  "Hip Turn @ Impact": "Measures hip rotation at ball contact.",
1135
  "Shoulder Tilt @ Impact": "Measures shoulder angle at ball contact.",
 
1168
  else:
1169
  return f"Your knee flexion of **{value}** may be limiting your swing potential. Proper knee bend is essential for balance, power generation, and maintaining spine angle. Too little or too much knee flexion can cause balance issues and inconsistent contact."
1170
 
1171
+ elif metric_name == "Head Movement @ Top":
1172
+ if "🟢" in badge:
1173
+ return f"Your head drop of **{value}** shows excellent head stability during the backswing. This minimal movement indicates proper head control and balance, which promotes consistent contact and accuracy. Good head stability is fundamental for reliable ball striking."
1174
+ elif "🟠" in badge:
1175
+ return f"Your head drop of **{value}** shows moderate movement during the backswing. Some head movement can affect balance and consistency. Working on head stability and maintaining your spine angle can improve ball striking and accuracy."
1176
+ else:
1177
+ return f"Your head drop of **{value}** indicates excessive head movement during the backswing. Too much head drop disrupts balance and swing center, leading to inconsistent contact. Focus on keeping your head stable and maintaining spine angle for better results."
1178
+
1179
  elif metric_name == "Hip Depth / Early Extension":
1180
  if "🟢" in badge:
1181
  return f"Your hip depth of **{value}** shows excellent posture maintenance. This indicates you're maintaining proper spine angle and avoiding early extension through impact. Good hip depth promotes solid contact, power transfer, and consistent ball striking patterns."
 
1310
  - Shoulder Tilt/Swing Plane @ Top: {format_metric_value(core_metrics.get('shoulder_tilt_swing_plane_top_deg', {}), '°')}
1311
  - Back Tilt @ Setup: {format_metric_value(core_metrics.get('back_tilt_deg', {}), '°')}
1312
  - Knee Flexion @ Setup: {format_metric_value(core_metrics.get('knee_flexion_deg', {}), '°')}
1313
+ - Head Movement @ Top: {format_metric_value(core_metrics.get('head_drop_top_pct', {}), '%')}
1314
  - Hip Depth / Early Extension: {format_metric_value(core_metrics.get('hip_depth_early_extension', {}), '%')}
1315
  # Hip Turn @ Impact metric removed per user request
1316