chenemii commited on
Commit
33b443b
·
1 Parent(s): 95ad9d6

Fix front-facing golf metrics: improve accuracy and reliability

Browse files

- Torso side-bend: standardize sign convention, use eyes+hips for de-roll
- Hip sway: add robust baseline with MAD jitter, two-stage denoising, yaw correction
- Wrist hinge: report acute angles (50-70° pro range) instead of obtuse
- Hip-shoulder separation: improve span ratio scaling, clamp to 0.7-1.0 range
- Fix format string errors with None value safety checks
- Enhanced debug logging with normalized sway and trust ratios

app/models/front_facing_metrics.py CHANGED
The diff for this file is too large to render. See raw diff
 
app/models/llm_analyzer.py CHANGED
@@ -24,9 +24,9 @@ try:
24
  get_knee_flexion_grading,
25
  get_head_drop_grading,
26
  get_shoulder_tilt_impact_grading,
27
- get_hip_turn_impact_grading,
28
  get_hip_sway_grading,
29
- get_wrist_hinge_grading
 
30
  )
31
  except ImportError:
32
  # Fallback if imports fail - define minimal grading functions
@@ -38,13 +38,14 @@ except ImportError:
38
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
39
  def get_hip_depth_grading(value, confidence):
40
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
41
- # Hip turn grading function removed per user request
42
  def get_shoulder_tilt_impact_grading(value, confidence):
43
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
44
  def get_hip_sway_grading(value, confidence, position):
45
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
46
  def get_wrist_hinge_grading(value, confidence, position):
47
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
 
 
48
  def get_head_drop_grading(value, confidence):
49
  """Grade head drop metric based on percentage of torso length
50
 
@@ -352,23 +353,25 @@ def compute_front_facing_core_metrics(pose_data, swing_phases, frames=None, worl
352
 
353
  # Process each front-facing metric with new calibration values
354
  for metric_name, metric_data in front_metrics.items():
355
- if 'hip_turn_impact' in metric_name and isinstance(metric_data, dict) and 'grade_label' in metric_data:
356
- # New hip turn data structure with club-aware grading - pass through directly
357
- core_metrics[metric_name] = metric_data
358
- else:
359
- # Handle other metrics with traditional processing
360
- value = metric_data.get('value') if isinstance(metric_data, dict) else metric_data
361
 
362
- if 'shoulder_tilt_impact' in metric_name:
363
- core_metrics[metric_name] = get_shoulder_tilt_impact_grading(value, 0.9)
364
- # Hip turn metric processing removed per user request
365
- elif 'hip_sway_top' in metric_name:
366
- core_metrics[metric_name] = get_hip_sway_grading(value, 0.8, "top")
367
- elif 'wrist_hinge_top' in metric_name:
368
- core_metrics[metric_name] = get_wrist_hinge_grading(value, 0.8, "top")
369
- else:
370
- # Default case
371
- core_metrics[metric_name] = {'value': value, 'status': 'n/a'}
 
 
 
 
 
372
 
373
  return core_metrics
374
 
 
24
  get_knee_flexion_grading,
25
  get_head_drop_grading,
26
  get_shoulder_tilt_impact_grading,
 
27
  get_hip_sway_grading,
28
+ get_wrist_hinge_grading,
29
+ get_hip_shoulder_separation_impact_grading
30
  )
31
  except ImportError:
32
  # Fallback if imports fail - define minimal grading functions
 
38
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
39
  def get_hip_depth_grading(value, confidence):
40
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
 
41
  def get_shoulder_tilt_impact_grading(value, confidence):
42
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
43
  def get_hip_sway_grading(value, confidence, position):
44
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
45
  def get_wrist_hinge_grading(value, confidence, position):
46
  return {'value': value, 'status': 'n/a', 'badge': '⚪'}
47
+ def get_hip_shoulder_separation_impact_grading(value, confidence):
48
+ return {'value': value, 'status': 'n/a', 'badge': '⚪'}
49
  def get_head_drop_grading(value, confidence):
50
  """Grade head drop metric based on percentage of torso length
51
 
 
353
 
354
  # Process each front-facing metric with new calibration values
355
  for metric_name, metric_data in front_metrics.items():
356
+ # Skip hip turn metric entirely
357
+ if 'hip_turn_impact' in metric_name:
358
+ continue
 
 
 
359
 
360
+ # Handle other metrics with traditional processing
361
+ value = metric_data.get('value') if isinstance(metric_data, dict) else metric_data
362
+
363
+ if 'shoulder_tilt_impact' in metric_name:
364
+ core_metrics[metric_name] = get_shoulder_tilt_impact_grading(value, 0.9)
365
+ elif 'hip_shoulder_separation_impact' in metric_name:
366
+ confidence = metric_data.get('confidence', 0.8) if isinstance(metric_data, dict) else 0.8
367
+ core_metrics[metric_name] = get_hip_shoulder_separation_impact_grading(value, confidence)
368
+ elif 'hip_sway_top' in metric_name:
369
+ core_metrics[metric_name] = get_hip_sway_grading(value, 0.8, "top")
370
+ elif 'wrist_hinge_top' in metric_name:
371
+ core_metrics[metric_name] = get_wrist_hinge_grading(value, 0.8, "top")
372
+ else:
373
+ # Default case
374
+ core_metrics[metric_name] = {'value': value, 'status': 'n/a'}
375
 
376
  return core_metrics
377
 
app/streamlit_app.py CHANGED
@@ -381,21 +381,24 @@ def get_head_drop_grading(value, confidence):
381
  }
382
 
383
 
384
- def get_shoulder_tilt_impact_grading(value, confidence):
385
- """Grade shoulder tilt at impact - professional = 39°, 30 handicap = 27°"""
386
  if value is None:
387
  return {'display_value': 'n/a', 'badge': '⚪', 'status': 'No data available'}
388
 
389
- # Professional = 39°, 30 handicap = 27°
390
- if 35 <= value <= 45:
 
 
 
391
  badge = '🟢'
392
- label = 'Excellent tilt'
393
- elif 25 <= value < 35 or 45 < value <= 55:
394
  badge = '🟠'
395
- label = 'Good tilt'
396
  else:
397
  badge = '🔴'
398
- label = 'Needs improvement'
399
 
400
  return {
401
  'display_value': f'{value:.1f}°',
@@ -405,18 +408,19 @@ def get_shoulder_tilt_impact_grading(value, confidence):
405
  }
406
 
407
 
408
- def get_hip_turn_impact_grading(value, confidence):
409
- """Grade hip turn at impact - professional = 36 degrees, 30 handicap = 19 degrees"""
 
410
  if value is None:
411
  return {'display_value': 'n/a', 'badge': '⚪', 'status': 'No data available'}
412
 
413
- # Professional = 36°, 30 handicap = 19°
414
- if 30 <= value <= 45:
415
  badge = '🟢'
416
- label = 'Excellent turn'
417
- elif 20 <= value < 30 or 45 < value <= 55:
418
  badge = '🟠'
419
- label = 'Good turn'
420
  else:
421
  badge = '🔴'
422
  label = 'Needs improvement'
@@ -428,7 +432,6 @@ def get_hip_turn_impact_grading(value, confidence):
428
  'confidence': confidence
429
  }
430
 
431
-
432
  def get_hip_sway_grading(value, confidence, position="top"):
433
  """Grade hip sway - professional = 3.9" towards target, 30 handicap = 2.5" towards target"""
434
  if value is None:
@@ -501,28 +504,6 @@ def get_wrist_hinge_grading(value, confidence, position="top"):
501
 
502
 
503
 
504
- def grade_hip_turn(delta_deg: float, club: str = "iron") -> dict:
505
- """Grade hip turn with club-aware bands and actionable tips"""
506
- c = club.lower()
507
- bands = {
508
- "driver": {"excellent": (38,48), "good_low": (32,38), "good_high": (48,52)},
509
- "iron": {"excellent": (32,40), "good_low": (28,32), "good_high": (40,44)},
510
- "wedge": {"excellent": (25,35), "good_low": (22,25), "good_high": (35,38)},
511
- }
512
- b = bands["driver" if "driver" in c else "wedge" if "wedge" in c else "iron"]
513
- x = delta_deg
514
-
515
- if b["excellent"][0] <= x <= b["excellent"][1]:
516
- label, color, tip = "🟢 Excellent", "green", "Great rotation with control."
517
- elif b["good_low"][0] <= x < b["good_low"][1]:
518
- label, color, tip = "🟠 Good (slightly low)", "orange", "Open a touch more through impact."
519
- elif b["good_high"][0] <= x <= b["good_high"][1]:
520
- label, color, tip = "🟠 Good (slightly high)", "orange", "Post on lead leg; avoid over-spinning hips."
521
- else:
522
- label, color, tip = "🔴 Needs work", "red", (
523
- "Too little: shift then open. Too much: trail heel down longer; post into lead glute."
524
- )
525
- return {"label": label, "color": color, "tip": tip}
526
 
527
 
528
  def display_new_grading_scheme(core_metrics):
@@ -530,9 +511,8 @@ def display_new_grading_scheme(core_metrics):
530
  # Check if we have front-facing metrics
531
  # Front-facing metrics have unique keys that DTL doesn't have
532
  has_front_facing_metrics = any(key in core_metrics for key in [
533
- 'shoulder_tilt_impact_deg', 'hip_sway_top_inches', 'wrist_hinge_top_deg'
534
  ])
535
- # Hip turn check for front-facing metrics
536
 
537
  if has_front_facing_metrics:
538
  st.subheader("Swing Analysis")
@@ -546,7 +526,6 @@ def display_new_grading_scheme(core_metrics):
546
  knee_flexion_data = core_metrics.get("knee_flexion_deg", {})
547
  head_drop_data = core_metrics.get("head_drop_top_pct", {})
548
  hip_depth_data = core_metrics.get("hip_depth_early_extension", {})
549
- # Hip turn data for front-facing metrics
550
 
551
  # Calculate confidence with QC penalties as per feedback
552
  def get_confidence(data, metric_type='general'):
@@ -701,21 +680,30 @@ def display_new_grading_scheme(core_metrics):
701
  # Front-facing metrics are now always calculated
702
  pass
703
 
704
- # Shoulder Tilt at Impact
705
- shoulder_tilt_impact_data = core_metrics.get("shoulder_tilt_impact_deg", {})
706
- if shoulder_tilt_impact_data.get('value') is not None:
707
  confidence = 0.9 # High confidence for front-facing measurements
708
- grading = get_shoulder_tilt_impact_grading(shoulder_tilt_impact_data['value'], confidence)
709
  if grading:
710
- metrics_to_display.append(("Shoulder Tilt @ Impact", grading))
 
 
 
 
 
 
 
 
711
 
712
- # Hip Turn at Impact
713
- hip_turn_impact_data = core_metrics.get("hip_turn_impact_deg", {})
714
- if hip_turn_impact_data.get('value') is not None:
715
- confidence = 0.8 # High confidence for front-facing measurements
716
- grading = get_hip_turn_impact_grading(hip_turn_impact_data['value'], confidence)
 
717
  if grading:
718
- metrics_to_display.append(("Hip Turn @ Impact", grading))
719
 
720
  # Hip Sway at Top
721
  hip_sway_top_data = core_metrics.get("hip_sway_top_inches", {})
@@ -779,8 +767,9 @@ def get_metric_definition(metric_name):
779
  "Knee Flexion": "Measures knee bend angle at address position.",
780
  "Head Movement @ Top": "Measures head movement during backswing as percentage of torso length.",
781
  "Hip Depth / Early Extension": "Tracks loss of hip flexion through impact.",
782
- "Hip Turn @ Impact": "Measures hip rotation at ball contact.",
783
- "Shoulder Tilt @ Impact": "Measures shoulder angle at ball contact.",
 
784
  "Hip Sway @ Top": "Measures lateral hip movement at the top of backswing.",
785
  "Wrist Hinge @ Top": "Measures wrist hinge angle at the top of backswing."
786
  }
@@ -833,7 +822,19 @@ def get_metric_evaluation(metric_name, grading):
833
  return f"Your hip depth of **{value}** indicates early extension issues. This movement pattern reduces power transfer and can cause inconsistent contact. Focus on maintaining spine angle and proper hip position throughout the downswing and impact."
834
 
835
  # FRONT-FACING METRICS (4 current metrics)
836
- elif metric_name == "Shoulder Tilt @ Impact":
 
 
 
 
 
 
 
 
 
 
 
 
837
  if "🟢" in badge:
838
  return f"Your shoulder tilt of **{value}** at impact shows excellent position. This proper shoulder angle indicates ideal impact dynamics and power transfer. Good shoulder tilt at impact promotes solid contact, optimal ball flight, and consistent distance control."
839
  elif "🟠" in badge:
@@ -841,13 +842,13 @@ def get_metric_evaluation(metric_name, grading):
841
  else:
842
  return f"Your shoulder tilt of **{value}** at impact needs attention. Proper shoulder angle at impact is crucial for power transfer and ball flight control. Work on impact position for better contact and consistency."
843
 
844
- elif metric_name == "Hip Turn @ Impact":
845
  if "🟢" in badge:
846
- return f"Your hip turn of **{value}** at impact shows excellent rotation. This proper hip movement indicates ideal power transfer and body sequencing. Good hip turn at impact promotes solid contact, optimal ball flight, and consistent distance control."
847
  elif "🟠" in badge:
848
- return f"Your hip turn of **{value}** at impact is acceptable with room for improvement. Hip rotation at impact affects power transfer and ball flight characteristics. Refining your hip movement can enhance consistency and distance."
849
  else:
850
- return f"Your hip turn of **{value}** at impact needs attention. Proper hip rotation at impact is crucial for power transfer and ball flight control. Work on hip movement for better contact and consistency."
851
 
852
  elif metric_name == "Hip Sway @ Top":
853
  if "🟢" in badge:
@@ -966,14 +967,13 @@ Timing Metrics:
966
  - Knee Flexion @ Setup: {format_metric_value(core_metrics.get('knee_flexion_deg', {}), '°')}
967
  - Head Movement @ Top: {format_metric_value(core_metrics.get('head_drop_top_pct', {}), '%')}
968
  - Hip Depth / Early Extension: {format_metric_value(core_metrics.get('hip_depth_early_extension', {}), '%')}
969
- # Hip Turn @ Impact metric restored for front-facing analysis
970
 
971
  === DTL-LIMITED METRICS (Approximate) ===
972
  - Shoulder Turn Quality: {format_metric_value(core_metrics.get('shoulder_turn_quality', {}))}
973
 
974
  === FRONT-FACING METRICS ===
975
- - Shoulder Tilt @ Impact: {format_metric_value(core_metrics.get('shoulder_tilt_impact_deg', {}), '°')}
976
- # Hip Turn @ Impact metric restored for front-facing analysis
977
  - Hip Sway @ Top: {format_metric_value(core_metrics.get('hip_sway_top_inches', {}), '"')}
978
  - Wrist Hinge @ Top: {format_metric_value(core_metrics.get('wrist_hinge_top_deg', {}), '°')}
979
  """
@@ -1820,19 +1820,23 @@ def render_step_4():
1820
  shoulder_swing_plane_status = core_metrics.get("shoulder_tilt_swing_plane_top_deg", {}).get('status', 'n/a')
1821
  st.caption(f"Shoulder Tilt/Swing Plane @ Top: {shoulder_swing_plane_raw}° ({shoulder_swing_plane_status})")
1822
 
1823
- # Shoulder rotation raw value
1824
- shoulder_tilt_raw = core_metrics.get("shoulder_tilt_impact_deg", {}).get('value')
1825
- shoulder_tilt_status = core_metrics.get("shoulder_tilt_impact_deg", {}).get('status', 'n/a')
1826
- st.caption(f"Shoulder Tilt @ Impact: {shoulder_tilt_raw}° ({shoulder_tilt_status})")
 
 
 
 
 
 
1827
 
1828
- # Hip turn data for front-facing metrics
1829
 
1830
  # Hip depth raw value
1831
  hip_depth_raw = core_metrics.get("hip_depth_early_extension", {}).get('value')
1832
  hip_depth_status = core_metrics.get("hip_depth_early_extension", {}).get('status', 'n/a')
1833
  st.caption(f"Hip Depth / Early Extension: {hip_depth_raw} ({hip_depth_status})")
1834
 
1835
- # Hip turn debug info for front-facing metrics
1836
  if has_front_facing_metrics:
1837
  st.caption("Front-facing metrics detected")
1838
  else:
 
381
  }
382
 
383
 
384
+ def get_torso_sidebend_impact_grading(value, confidence):
385
+ """Grade torso side-bend at impact - professional range ~10-20° (trail-side bend positive)"""
386
  if value is None:
387
  return {'display_value': 'n/a', 'badge': '⚪', 'status': 'No data available'}
388
 
389
+ # Use absolute value for grading, but keep sign for display
390
+ abs_value = abs(value)
391
+
392
+ # Professional range ~10-20°, typical amateur ~6-24°
393
+ if 10 <= abs_value <= 20:
394
  badge = '🟢'
395
+ label = 'Excellent side-bend'
396
+ elif 6 <= abs_value < 10 or 20 < abs_value <= 24:
397
  badge = '🟠'
398
+ label = 'Good side-bend'
399
  else:
400
  badge = '🔴'
401
+ label = 'Needs work'
402
 
403
  return {
404
  'display_value': f'{value:.1f}°',
 
408
  }
409
 
410
 
411
+
412
+ def get_hip_shoulder_separation_impact_grading(value, confidence):
413
+ """Grade hip-shoulder separation at impact - typical range 10-45 degrees"""
414
  if value is None:
415
  return {'display_value': 'n/a', 'badge': '⚪', 'status': 'No data available'}
416
 
417
+ # Professional golfers typically show 20-35° separation, recreational 10-25°
418
+ if 20 <= value <= 35:
419
  badge = '🟢'
420
+ label = 'Excellent separation'
421
+ elif 15 <= value < 20 or 35 < value <= 45:
422
  badge = '🟠'
423
+ label = 'Good separation'
424
  else:
425
  badge = '🔴'
426
  label = 'Needs improvement'
 
432
  'confidence': confidence
433
  }
434
 
 
435
  def get_hip_sway_grading(value, confidence, position="top"):
436
  """Grade hip sway - professional = 3.9" towards target, 30 handicap = 2.5" towards target"""
437
  if value is None:
 
504
 
505
 
506
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
 
508
 
509
  def display_new_grading_scheme(core_metrics):
 
511
  # Check if we have front-facing metrics
512
  # Front-facing metrics have unique keys that DTL doesn't have
513
  has_front_facing_metrics = any(key in core_metrics for key in [
514
+ 'torso_side_bend_deg', 'shoulder_tilt_impact_deg', 'hip_sway_top_inches', 'wrist_hinge_top_deg', 'hip_shoulder_separation_impact_deg'
515
  ])
 
516
 
517
  if has_front_facing_metrics:
518
  st.subheader("Swing Analysis")
 
526
  knee_flexion_data = core_metrics.get("knee_flexion_deg", {})
527
  head_drop_data = core_metrics.get("head_drop_top_pct", {})
528
  hip_depth_data = core_metrics.get("hip_depth_early_extension", {})
 
529
 
530
  # Calculate confidence with QC penalties as per feedback
531
  def get_confidence(data, metric_type='general'):
 
680
  # Front-facing metrics are now always calculated
681
  pass
682
 
683
+ # Torso Side-Bend at Impact (replaces shoulder tilt)
684
+ torso_sidebend_data = core_metrics.get("torso_side_bend_deg", {})
685
+ if torso_sidebend_data.get('value') is not None:
686
  confidence = 0.9 # High confidence for front-facing measurements
687
+ grading = get_torso_sidebend_impact_grading(torso_sidebend_data['value'], confidence)
688
  if grading:
689
+ metrics_to_display.append(("Torso Side-Bend @ Impact", grading))
690
+ else:
691
+ # Fallback to old metric name for compatibility
692
+ shoulder_tilt_impact_data = core_metrics.get("shoulder_tilt_impact_deg", {})
693
+ if shoulder_tilt_impact_data.get('value') is not None:
694
+ confidence = 0.9 # High confidence for front-facing measurements
695
+ grading = get_torso_sidebend_impact_grading(shoulder_tilt_impact_data['value'], confidence)
696
+ if grading:
697
+ metrics_to_display.append(("Torso Side-Bend @ Impact", grading))
698
 
699
+
700
+ # Hip-Shoulder Separation at Impact
701
+ hip_shoulder_sep_data = core_metrics.get("hip_shoulder_separation_impact_deg", {})
702
+ if hip_shoulder_sep_data.get('value') is not None:
703
+ confidence = hip_shoulder_sep_data.get('confidence', 0.8)
704
+ grading = get_hip_shoulder_separation_impact_grading(hip_shoulder_sep_data['value'], confidence)
705
  if grading:
706
+ metrics_to_display.append(("Hip-Shoulder Separation @ Impact", grading))
707
 
708
  # Hip Sway at Top
709
  hip_sway_top_data = core_metrics.get("hip_sway_top_inches", {})
 
767
  "Knee Flexion": "Measures knee bend angle at address position.",
768
  "Head Movement @ Top": "Measures head movement during backswing as percentage of torso length.",
769
  "Hip Depth / Early Extension": "Tracks loss of hip flexion through impact.",
770
+ "Torso Side-Bend @ Impact": "Measures torso side-bend angle at ball contact.",
771
+ "Shoulder Tilt @ Impact": "Measures shoulder angle at ball contact.", # Deprecated
772
+ "Hip-Shoulder Separation @ Impact": "Measures hip rotation relative to shoulders at ball contact.",
773
  "Hip Sway @ Top": "Measures lateral hip movement at the top of backswing.",
774
  "Wrist Hinge @ Top": "Measures wrist hinge angle at the top of backswing."
775
  }
 
822
  return f"Your hip depth of **{value}** indicates early extension issues. This movement pattern reduces power transfer and can cause inconsistent contact. Focus on maintaining spine angle and proper hip position throughout the downswing and impact."
823
 
824
  # FRONT-FACING METRICS (4 current metrics)
825
+ elif metric_name == "Torso Side-Bend @ Impact":
826
+ # Interpret the sign
827
+ side_description = "trail-side" if float(value.replace('°', '')) > 0 else "lead-side"
828
+ abs_value = abs(float(value.replace('°', '')))
829
+
830
+ if "🟢" in badge:
831
+ return f"Your torso side-bend of **{value}** at impact shows excellent position. You're bending **{abs_value:.1f}°** toward the **{side_description}** which indicates ideal impact dynamics and power transfer. This optimal torso angle promotes solid contact, optimal ball flight, and consistent distance control."
832
+ elif "🟠" in badge:
833
+ return f"Your torso side-bend of **{value}** at impact is acceptable with room for improvement. You're bending **{abs_value:.1f}°** toward the **{side_description}**. Torso position at impact affects power transfer and ball flight characteristics. Refining your impact position can enhance consistency and distance."
834
+ else:
835
+ return f"Your torso side-bend of **{value}** at impact needs attention. You're bending **{abs_value:.1f}°** toward the **{side_description}**. Proper torso angle at impact is crucial for power transfer and ball flight control. Work on impact position for better contact and consistency."
836
+
837
+ elif metric_name == "Shoulder Tilt @ Impact": # Legacy support
838
  if "🟢" in badge:
839
  return f"Your shoulder tilt of **{value}** at impact shows excellent position. This proper shoulder angle indicates ideal impact dynamics and power transfer. Good shoulder tilt at impact promotes solid contact, optimal ball flight, and consistent distance control."
840
  elif "🟠" in badge:
 
842
  else:
843
  return f"Your shoulder tilt of **{value}** at impact needs attention. Proper shoulder angle at impact is crucial for power transfer and ball flight control. Work on impact position for better contact and consistency."
844
 
845
+ elif metric_name == "Hip-Shoulder Separation @ Impact":
846
  if "🟢" in badge:
847
+ return f"Your hip-shoulder separation of **{value}** at impact shows excellent body sequencing. This proper rotation sequence indicates ideal power transfer with the hips leading the shoulders through impact. Good separation promotes solid contact and optimal ball flight."
848
  elif "🟠" in badge:
849
+ return f"Your hip-shoulder separation of **{value}** at impact is acceptable with room for improvement. Hip-shoulder sequencing affects power transfer and swing efficiency. Refining your rotation sequence can enhance consistency and distance."
850
  else:
851
+ return f"Your hip-shoulder separation of **{value}** at impact needs attention. Proper sequencing with hips leading shoulders is crucial for power transfer and ball flight control. Work on rotation timing for better contact and consistency."
852
 
853
  elif metric_name == "Hip Sway @ Top":
854
  if "🟢" in badge:
 
967
  - Knee Flexion @ Setup: {format_metric_value(core_metrics.get('knee_flexion_deg', {}), '°')}
968
  - Head Movement @ Top: {format_metric_value(core_metrics.get('head_drop_top_pct', {}), '%')}
969
  - Hip Depth / Early Extension: {format_metric_value(core_metrics.get('hip_depth_early_extension', {}), '%')}
 
970
 
971
  === DTL-LIMITED METRICS (Approximate) ===
972
  - Shoulder Turn Quality: {format_metric_value(core_metrics.get('shoulder_turn_quality', {}))}
973
 
974
  === FRONT-FACING METRICS ===
975
+ - Torso Side-Bend @ Impact: {format_metric_value(core_metrics.get('torso_side_bend_deg', {}), '°')}
976
+ - Hip-Shoulder Separation @ Impact: {format_metric_value(core_metrics.get('hip_shoulder_separation_impact_deg', {}), '°')}
977
  - Hip Sway @ Top: {format_metric_value(core_metrics.get('hip_sway_top_inches', {}), '"')}
978
  - Wrist Hinge @ Top: {format_metric_value(core_metrics.get('wrist_hinge_top_deg', {}), '°')}
979
  """
 
1820
  shoulder_swing_plane_status = core_metrics.get("shoulder_tilt_swing_plane_top_deg", {}).get('status', 'n/a')
1821
  st.caption(f"Shoulder Tilt/Swing Plane @ Top: {shoulder_swing_plane_raw}° ({shoulder_swing_plane_status})")
1822
 
1823
+ # Torso side-bend raw value
1824
+ torso_sidebend_raw = core_metrics.get("torso_side_bend_deg", {}).get('value')
1825
+ torso_sidebend_status = core_metrics.get("torso_side_bend_deg", {}).get('status', 'n/a')
1826
+ if torso_sidebend_raw is not None:
1827
+ st.caption(f"Torso Side-Bend @ Impact: {torso_sidebend_raw}° ({torso_sidebend_status})")
1828
+ else:
1829
+ # Fallback to old metric for compatibility
1830
+ shoulder_tilt_raw = core_metrics.get("shoulder_tilt_impact_deg", {}).get('value')
1831
+ shoulder_tilt_status = core_metrics.get("shoulder_tilt_impact_deg", {}).get('status', 'n/a')
1832
+ st.caption(f"Torso Side-Bend @ Impact: {shoulder_tilt_raw}° ({shoulder_tilt_status})")
1833
 
 
1834
 
1835
  # Hip depth raw value
1836
  hip_depth_raw = core_metrics.get("hip_depth_early_extension", {}).get('value')
1837
  hip_depth_status = core_metrics.get("hip_depth_early_extension", {}).get('status', 'n/a')
1838
  st.caption(f"Hip Depth / Early Extension: {hip_depth_raw} ({hip_depth_status})")
1839
 
 
1840
  if has_front_facing_metrics:
1841
  st.caption("Front-facing metrics detected")
1842
  else: