File size: 28,031 Bytes
a422282
91badaf
 
 
a422282
 
91badaf
a422282
91badaf
e8f3c26
91badaf
 
 
36d65da
 
91badaf
36d65da
 
 
 
 
 
 
 
95ad9d6
36d65da
 
33b443b
 
36d65da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33b443b
 
7ad8b8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a5016f
 
 
 
91badaf
 
 
 
 
 
4a5016f
 
 
 
91badaf
4a5016f
91badaf
 
 
4a5016f
a422282
 
7680d90
91badaf
7680d90
91badaf
 
7680d90
91badaf
 
7680d90
91badaf
 
 
7680d90
91badaf
7680d90
 
 
91badaf
 
 
 
 
a422282
91badaf
 
 
 
 
 
 
 
 
 
 
7680d90
 
91badaf
 
 
 
 
 
 
 
7680d90
91badaf
7680d90
a422282
 
91badaf
 
a422282
91badaf
 
 
 
 
 
 
 
 
 
 
7680d90
91badaf
7680d90
 
 
36d65da
91badaf
a422282
 
 
 
4a5016f
 
 
91badaf
36d65da
a422282
 
91badaf
a422282
36d65da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7ad8b8c
36d65da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a5016f
 
 
 
 
 
 
91badaf
4a5016f
91badaf
4a5016f
36d65da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a5016f
91badaf
36d65da
 
 
 
 
 
 
 
 
 
 
91badaf
36d65da
91badaf
 
 
 
36d65da
cffadc9
91badaf
36d65da
 
91badaf
36d65da
91badaf
36d65da
91badaf
36d65da
91badaf
 
36d65da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cffadc9
36d65da
 
 
 
 
 
 
 
 
 
 
 
cffadc9
 
36d65da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33b443b
 
 
36d65da
33b443b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cffadc9
91badaf
cffadc9
 
91badaf
 
4a5016f
 
 
 
 
91badaf
 
4a5016f
 
91badaf
4a5016f
 
91badaf
4a5016f
91badaf
 
 
 
 
af7ef4f
91badaf
af7ef4f
 
ef472f7
af7ef4f
 
 
 
 
4a5016f
91badaf
af7ef4f
91badaf
af7ef4f
 
 
 
 
4a5016f
af7ef4f
 
 
4a5016f
af7ef4f
 
 
4a5016f
af7ef4f
 
 
4a5016f
af7ef4f
 
 
4a5016f
af7ef4f
 
 
 
4a5016f
91badaf
af7ef4f
4a5016f
a422282
af7ef4f
 
a422282
 
 
91badaf
95ad9d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91badaf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cffadc9
91badaf
 
cffadc9
91badaf
cffadc9
4a5016f
91badaf
 
 
 
2d71c32
91badaf
 
4a5016f
91badaf
 
4a5016f
91badaf
 
 
 
 
4a5016f
2d71c32
91badaf
 
 
 
 
 
2f3c69c
91badaf
 
 
 
4a5016f
 
91badaf
 
 
 
2d71c32
91badaf
 
2d71c32
91badaf
 
 
e8f3c26
91badaf
 
 
 
e8f3c26
 
 
91badaf
 
 
e8f3c26
36d65da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
"""
LLM-based Golf Swing Analysis

This module handles LLM analysis and prompt generation for golf swing feedback.
"""

import os
import json
import requests
import re
from typing import Dict, Any, Optional
from .metrics_calculator import (
    calculate_back_tilt_degree, calculate_knee_bend_degree, 
    calculate_shoulder_tilt_swing_plane_at_top,
    compute_dtl_three
)
from .front_facing_metrics import compute_front_facing_metrics

# Import grading functions to avoid duplication
try:
    from ..streamlit_app import (
        get_shoulder_tilt_swing_plane_grading,
        get_back_tilt_grading,
        get_knee_flexion_grading,
        get_head_drop_grading,
        get_shoulder_tilt_impact_grading,
        get_hip_sway_grading,
        get_wrist_hinge_grading,
        get_hip_shoulder_separation_impact_grading
    )
except ImportError:
    # Fallback if imports fail - define minimal grading functions
    def get_shoulder_tilt_swing_plane_grading(value, confidence):
        return {'value': value, 'status': 'n/a', 'badge': '⚪'}
    def get_back_tilt_grading(value, confidence):
        return {'value': value, 'status': 'n/a', 'badge': '⚪'}
    def get_knee_flexion_grading(value, confidence):
        return {'value': value, 'status': 'n/a', 'badge': '⚪'}
    def get_hip_depth_grading(value, confidence):
        return {'value': value, 'status': 'n/a', 'badge': '⚪'}
    def get_shoulder_tilt_impact_grading(value, confidence):
        return {'value': value, 'status': 'n/a', 'badge': '⚪'}
    def get_hip_sway_grading(value, confidence, position):
        return {'value': value, 'status': 'n/a', 'badge': '⚪'}
    def get_wrist_hinge_grading(value, confidence, position):
        return {'value': value, 'status': 'n/a', 'badge': '⚪'}
    def get_hip_shoulder_separation_impact_grading(value, confidence):
        return {'value': value, 'status': 'n/a', 'badge': '⚪'}
    def get_head_drop_grading(value, confidence):
        """Grade head drop metric based on percentage of torso length
        
        Args:
            value: percentage value (positive = head moved down)
            confidence: confidence score (not used in current implementation)
        """
        if value is None:
            return {'value': None, 'status': 'error', 'badge': '❌'}
        
        # Address → Top: ~2–6% typical; >8% = too much "sit"
        if 2 <= value <= 6:
            return {'value': f"{value:.1f}%", 'status': 'good', 'badge': '✅'}
        elif 6 < value <= 8:
            return {'value': f"{value:.1f}%", 'status': 'caution', 'badge': '⚠️'}
        else:
            return {'value': f"{value:.1f}%", 'status': 'poor', 'badge': '❌'}


def safe_fmt_deg(v):
    """Format angle value safely, returning 'n/a' for None or invalid values"""
    if v is None or (isinstance(v, str) and v.lower() in ['none', 'n/a', '']):
        return 'n/a'
    try:
        return f"{float(v):.1f}°"
    except (ValueError, TypeError):
        return 'n/a'


def safe_fmt_percent(v):
    """Format percentage value safely, returning 'n/a' for None or invalid values"""
    if v is None or (isinstance(v, str) and v.lower() in ['none', 'n/a', '']):
        return 'n/a'
    try:
        return f"{float(v):.1f}%"
    except (ValueError, TypeError):
        return 'n/a'


def check_llm_services():
    """Check availability of LLM services"""
    services = {
        'openai': {'available': bool(os.getenv('OPENAI_API_KEY'))},
        'ollama': {'available': False}  # Will check if running
    }
    
    # Check if Ollama is running
    try:
        response = requests.get('http://localhost:11434/api/version', timeout=2)
        services['ollama']['available'] = response.status_code == 200
    except:
        pass
    
    return services


def call_openai_service(prompt, config):
    """Call OpenAI API for swing analysis"""
    api_key = os.getenv('OPENAI_API_KEY')
    if not api_key:
        return None
    
    headers = {
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json'
    }
    
    payload = {
        'model': config.get('model', 'gpt-4'),
        'messages': [{'role': 'user', 'content': prompt}],
        'max_tokens': config.get('max_tokens', 2000),
        'temperature': config.get('temperature', 0.7)
    }
    
    try:
        response = requests.post(
            'https://api.openai.com/v1/chat/completions',
            headers=headers,
            json=payload,
            timeout=30
        )
        response.raise_for_status()
        return response.json()['choices'][0]['message']['content']
    except Exception as e:
        print(f"OpenAI API error: {e}")
        return None


def call_ollama_service(prompt, config):
    """Call local Ollama service for swing analysis"""
    try:
        response = requests.post(
            'http://localhost:11434/api/generate',
            json={
                'model': config.get('model', 'llama2'),
                'prompt': prompt,
                'stream': False
            },
            timeout=60
        )
        response.raise_for_status()
        return response.json().get('response')
    except Exception as e:
        print(f"Ollama service error: {e}")
        return None


def compute_core_metrics(pose_data, swing_phases, frame_timestamps_ms=None, total_ms=None, player_handedness='right', is_front_facing=False, frames=None, world_landmarks=None, club="iron"):
    """Compute core golf swing metrics
    
    Args:
        pose_data (dict): Dictionary mapping frame indices to pose keypoints
        swing_phases (dict): Dictionary mapping phase names to lists of frame indices
        frame_timestamps_ms (list, optional): List of frame timestamps in milliseconds  
        total_ms (float, optional): Total video duration in milliseconds
        player_handedness (str): 'right' or 'left' handed player
        is_front_facing (bool): True for front-facing camera view
        club (str): Club type for grading ("driver", "iron", "wedge")
        
    Returns:
        dict: Core metrics with confidence scores and status
    """
    if is_front_facing:
        # Use front-facing metrics
        return compute_front_facing_core_metrics(pose_data, swing_phases, frames, world_landmarks, club)
    else:
        # Use DTL metrics (existing implementation)
        return compute_dtl_core_metrics(pose_data, swing_phases, frame_timestamps_ms, total_ms, player_handedness, frames)


def compute_dtl_core_metrics(pose_data, swing_phases, frame_timestamps_ms=None, total_ms=None, player_handedness='right', frames=None):
    """Compute DTL (Down-the-Line) golf swing metrics using new unified function"""
    
    # Extract frame dimensions from frames if available
    frame_w, frame_h = 1920, 1080  # Default values
    if frames and len(frames) > 0:
        if hasattr(frames[0], 'shape'):
            frame_h, frame_w = frames[0].shape[:2]
    
    # Use the new unified DTL metrics function
    try:
        dtl_metrics = compute_dtl_three(pose_data, swing_phases, frame_w, frame_h, frames)
        if dtl_metrics is None:
            # Fallback to individual calculations if unified function fails
            return compute_dtl_core_metrics_fallback(pose_data, swing_phases, frame_timestamps_ms, total_ms, player_handedness, frames)
        
        # Convert to the expected format with grading
        core_metrics = {}
        
        # Map the new metric names to the expected format (hip depth and hip turn removed)
        metric_mapping = {
            'shoulder_plane_top_deg': ('shoulder_tilt_swing_plane_top_deg', get_shoulder_tilt_swing_plane_grading),
            'back_tilt_setup_deg': ('back_tilt_deg', get_back_tilt_grading),
            'knee_flexion_deg': ('knee_flexion_deg', get_knee_flexion_grading),
            'head_drop_top_pct': ('head_drop_top_pct', get_head_drop_grading),
            # Hip turn mapping removed per user request
        }
        
        for new_key, (old_key, grading_func) in metric_mapping.items():
            value = dtl_metrics.get(new_key)
            if value is not None:
                try:
                    grading = grading_func(value, 0.8)
                    if grading:
                        # Hip turn debug info removed per user request
                        core_metrics[old_key] = grading
                except Exception:
                    # Fallback format if grading fails
                    core_metrics[old_key] = {
                        'value': value,
                        'status': 'Calculated',
                        'badge': '✅'
                    }
        
        # Add validation info if available
        validation = dtl_metrics.get('_validation')
        if validation:
            core_metrics['_validation'] = validation
        
        # Add debug info if available
        debug_info = dtl_metrics.get('_debug')
        if debug_info:
            core_metrics['_debug'] = debug_info
            
        return core_metrics
        
    except Exception as e:
        print(f"Error in compute_dtl_three: {e}")
        # Fallback to individual calculations
        return compute_dtl_core_metrics_fallback(pose_data, swing_phases, frame_timestamps_ms, total_ms, player_handedness, frames)


def compute_dtl_core_metrics_fallback(pose_data, swing_phases, frame_timestamps_ms=None, total_ms=None, player_handedness='right', frames=None):
    """Fallback function using individual metric calculations"""
    # Extract frame dimensions from frames if available
    frame_w, frame_h = 1920, 1080  # Default values
    if frames and len(frames) > 0:
        if hasattr(frames[0], 'shape'):
            frame_h, frame_w = frames[0].shape[:2]
    
    # Get phase frame indices
    setup_frames = swing_phases.get("setup", [])
    backswing_frames = swing_phases.get("backswing", []) 
    downswing_frames = swing_phases.get("downswing", [])
    impact_frames = swing_phases.get("impact", [])
    
    # Get key frame indices
    address_idx = setup_frames[0] if setup_frames else 0
    top_idx = backswing_frames[-1] if backswing_frames else address_idx
    impact_idx = impact_frames[0] if impact_frames else top_idx
    
    # Initialize core metrics with new DTL metrics - don't set default status, let grading functions handle it
    core_metrics = {}
    
    # Calculate Shoulder Tilt / Swing Plane Angle at Top - professional = 36°, 30 handicap = 29°
    try:
        # Ensure we have valid indices and data
        if backswing_frames and top_idx in pose_data and pose_data[top_idx] is not None:
            shoulder_tilt_swing_plane = calculate_shoulder_tilt_swing_plane_at_top(pose_data, swing_phases, top_idx, frames)
            if shoulder_tilt_swing_plane is not None and isinstance(shoulder_tilt_swing_plane, (int, float)):
                grading = get_shoulder_tilt_swing_plane_grading(shoulder_tilt_swing_plane, 0.8)
                if grading and isinstance(grading, dict):
                    core_metrics["shoulder_tilt_swing_plane_top_deg"] = grading
    except Exception as e:
        # Silently continue if calculation fails
        pass
    
    # Calculate Back Tilt @ Setup
    try:
        # Ensure we have valid indices and data
        if setup_frames and address_idx in pose_data and pose_data[address_idx] is not None:
            back_tilt = calculate_back_tilt_degree(pose_data, swing_phases, address_idx, frames)
            if back_tilt is not None and isinstance(back_tilt, (int, float)):
                grading = get_back_tilt_grading(back_tilt, 0.8)
                if grading and isinstance(grading, dict):
                    core_metrics["back_tilt_deg"] = grading
    except Exception as e:
        # Silently continue if calculation fails
        pass
    
    # Calculate Knee Flexion @ Setup
    knee_bend_data = calculate_knee_bend_degree(pose_data, address_idx)
    if knee_bend_data is not None:
        primary_value = knee_bend_data.get('primary_value')  # Average or single value
        if primary_value is not None:
            grading = get_knee_flexion_grading(primary_value, 0.8)
            
            # Add separate lead/trail values if available
            lead_flexion = knee_bend_data.get('lead_knee_flexion')
            trail_flexion = knee_bend_data.get('trail_knee_flexion')
            if lead_flexion is not None:
                grading['lead_knee_flexion'] = round(lead_flexion, 1)
            if trail_flexion is not None:
                grading['trail_knee_flexion'] = round(trail_flexion, 1)
            
            core_metrics["knee_flexion_deg"] = grading
    
    # Calculate Hip Depth / Early Extension
    try:
        hip_depth_data = calculate_hip_depth_early_extension(pose_data, swing_phases)
        if hip_depth_data:
            # Check if it's an error message
            if 'error' in hip_depth_data:
                # Store error but still try to display it
                core_metrics["hip_depth_early_extension"] = {
                    'value': None,
                    'detailed_data': hip_depth_data,
                    'status': f'Error: {hip_depth_data["error"]}',
                    'badge': '🔴',
                    'error': True
                }
            elif isinstance(hip_depth_data, dict) and 'depth_loss_pct' in hip_depth_data:
                # Valid data structure - proceed with grading
                grading = get_hip_depth_grading(hip_depth_data, 0.8)
                if grading:
                    core_metrics["hip_depth_early_extension"] = grading
            else:
                # Unexpected data structure - create a fallback entry
                core_metrics["hip_depth_early_extension"] = {
                    'value': None,
                    'status': 'Data format error',
                    'badge': '🔴'
                }
    except Exception as e:
        # Create a fallback entry for display
        core_metrics["hip_depth_early_extension"] = {
            'value': None,
            'status': 'Calculation failed',
            'badge': '🔴'
        }
    
    # Hip turn calculation removed per user request
    
    return core_metrics


def compute_front_facing_core_metrics(pose_data, swing_phases, frames=None, world_landmarks=None, club="iron"):
    """Compute front-facing golf swing metrics"""
    # Get front-facing metrics from the new module
    front_metrics = compute_front_facing_metrics(pose_data, swing_phases, world_landmarks=world_landmarks, frames=frames, club=club, handedness="right")
    
    # Convert to the expected format with status and badges
    core_metrics = {}
    
    
    # Process each front-facing metric with new calibration values
    for metric_name, metric_data in front_metrics.items():
        # Skip hip turn metric entirely
        if 'hip_turn_impact' in metric_name:
            continue
            
        # Handle other metrics with traditional processing
        value = metric_data.get('value') if isinstance(metric_data, dict) else metric_data
        
        if 'shoulder_tilt_impact' in metric_name:
            core_metrics[metric_name] = get_shoulder_tilt_impact_grading(value, 0.9)
        elif 'hip_shoulder_separation_impact' in metric_name:
            confidence = metric_data.get('confidence', 0.8) if isinstance(metric_data, dict) else 0.8
            core_metrics[metric_name] = get_hip_shoulder_separation_impact_grading(value, confidence)
        elif 'hip_sway_top' in metric_name:
            core_metrics[metric_name] = get_hip_sway_grading(value, 0.8, "top")
        elif 'wrist_hinge_top' in metric_name:
            core_metrics[metric_name] = get_wrist_hinge_grading(value, 0.8, "top")
        else:
            # Default case
            core_metrics[metric_name] = {'value': value, 'status': 'n/a'}
    
    return core_metrics


def prepare_data_for_llm(pose_data, swing_phases, trajectory_data=None, fps=30.0, frame_shape=None, frame_timestamps_ms=None, total_ms=None, is_front_facing=False, frames=None):
    """Prepare swing data for LLM analysis
    
    Args:
        pose_data (dict): Dictionary mapping frame indices to pose keypoints
        swing_phases (dict): Dictionary mapping phase names to lists of frame indices
        trajectory_data (dict, optional): Ball trajectory data
        fps (float): Video frame rate for timing calculations
        frame_shape (tuple, optional): Frame shape (H, W)
        frame_timestamps_ms (list, optional): List of frame timestamps in milliseconds
        total_ms (float, optional): Total video duration in milliseconds
        is_front_facing (bool): True for front-facing camera view
        
    Returns:
        dict: Formatted swing data for LLM
    """
    # Compute core metrics
    core_metrics = compute_core_metrics(
        pose_data, swing_phases, frame_timestamps_ms, total_ms, 
        player_handedness='right', is_front_facing=is_front_facing, frames=frames
    )
    
    # Calculate timing data
    setup_frames = swing_phases.get("setup", [])
    backswing_frames = swing_phases.get("backswing", []) 
    downswing_frames = swing_phases.get("downswing", [])
    impact_frames = swing_phases.get("impact", [])
    follow_through_frames = swing_phases.get("follow_through", [])
    
    total_frames = len(setup_frames) + len(backswing_frames) + len(downswing_frames) + len(impact_frames) + len(follow_through_frames)
    
    if total_ms is None:
        total_ms = total_frames * (1000.0 / fps)
    
    dt = total_ms / max(total_frames, 1) / 1000.0  # seconds per frame
    
    swing_data = {
        "swing_phases": {
            "setup": {
                "frame_count": len(setup_frames),
                "duration_ms": len(setup_frames) * dt * 1000.0
            },
            "backswing": {
                "frame_count": len(backswing_frames),
                "duration_ms": len(backswing_frames) * dt * 1000.0
            },
            "downswing": {
                "frame_count": len(downswing_frames),
                "duration_ms": len(downswing_frames) * dt * 1000.0
            },
            "impact": {
                "frame_count": len(impact_frames),
                "duration_ms": len(impact_frames) * dt * 1000.0
            },
            "follow_through": {
                "frame_count": len(follow_through_frames),
                "duration_ms": len(follow_through_frames) * dt * 1000.0
            }
        },
        "timing_metrics": {
            "total_swing_frames": total_frames,
            "total_swing_time_ms": total_frames * dt * 1000.0,
            "actual_fps": round(1.0 / dt if dt > 0 else fps, 1)
        },
        "core_metrics": core_metrics
    }
    
    return swing_data


def create_llm_prompt(analysis_data):
    """Create LLM prompt from swing analysis data"""
    prompt_template = """# Golf Swing Analysis

## NEW METRICS CALIBRATION
Use these new professional/amateur benchmarks for scoring. These represent updated golf swing mechanics based on the latest analysis:

### **NEW FRONT-FACING METRICS:**

**Shoulder Tilt @ Impact:**
- Professional = 39°
- 30 Handicap = 27°

**Hip Sway @ Top:**
- Professional = 3.9" towards target
- 30 Handicap = 2.5" towards target

**Wrist Hinge @ Top:**
- To be measured and calibrated

### **NEW DTL METRICS:**

**Shoulder Tilt/Swing Plane Angle @ Top:**
- Professional = 36° (Iron avg: 34.3°, Driver avg: 30.5°)
- 30 Handicap = 29°

**Back Tilt (°):**
- Professional = 31° (Iron avg: 30.8°, Driver avg: 32.3°)

**Knee Flexion (°):**
- Professional = 22° (Iron avg: 21.6°, Driver avg: 27.2°)

**Head Drop/Rise @ Top (%):**
- Professional = 0-5% drop (Iron: 0-7.6% range, Driver: 4.6-5.5% drop)

### **ANALYSIS INSTRUCTIONS**

**GOLF SWING ANALYSIS FORMAT**
Use the benchmarks above to guide your evaluation. Follow this exact format:

**OVERALL_SUMMARY:** [1-2 sentences maximum providing a concise evaluation of the swing's overall quality and main strengths/areas for improvement]

**PERFORMANCE_CLASSIFICATION:** [XX%]
(XX = number from 10% to 100%)

**Metric Evaluations**

For each metric below, write exactly 3 sentences evaluating the metric:
1. First sentence: State if it's good, bad, or needs improvement compared to professional standards
2. Second sentence: Compare the specific value to professional/amateur ranges  
3. Third sentence: Brief explanation of impact on swing performance

**Classification Bands:**
- **90–100%**: Tour-level
- **80–89%**: Advanced amateur
- **70–79%**: Skilled
- **60–69%**: Intermediate
- **50–59%**: Developing
- **40–49%**: Beginner
- **10–39%**: Novice

**STYLE & FORMATTING RULES:**
- Use these headers: OVERALL_SUMMARY, PERFORMANCE_CLASSIFICATION, Metric Evaluations
- No emojis anywhere in the response  
- Write 1-2 sentences maximum for the overall summary
- Write exactly 3 sentences for each metric evaluation
- Use a positive, coaching tone throughout
- Focus on biomechanics and compare actual values to the professional ranges provided

"""
    
    # Format metrics for prompt
    core_metrics = analysis_data.get('core_metrics', {})
    
    metrics_text = "SWING METRICS:\n"
    for metric_name, metric_data in core_metrics.items():
        if metric_data.get('value') is not None:
            value = metric_data['value']
            confidence = metric_data.get('confidence', 0.0)
            status = metric_data.get('status', 'unknown')
            
            if isinstance(value, dict):
                # Handle complex metrics like hip depth
                if 'depth_loss_pct' in value:
                    metrics_text += f"- Hip Depth/Early Extension: {value['depth_loss_pct']}% loss (confidence: {confidence:.0%}) - {status}\n"
                elif 'displacement_pct' in value:
                    direction = value.get('displacement_direction', 'unknown')
                    metrics_text += f"- Head Displacement: {value['displacement_pct']}% {direction} (confidence: {confidence:.0%}) - {status}\n"
            else:
                metrics_text += f"- {metric_name.replace('_', ' ').title()}: {value}° (confidence: {confidence:.0%}) - {status}\n"
    
    # Combine template with data
    full_prompt = f"{prompt_template}\n\n{metrics_text}"
    
    return full_prompt


def generate_swing_analysis(pose_data, swing_phases, trajectory_data):
    """Generate swing analysis using LLM"""
    # Prepare data for LLM
    analysis_data = prepare_data_for_llm(pose_data, swing_phases, trajectory_data)
    
    # Create prompt
    prompt = create_llm_prompt(analysis_data)
    
    # Check available services
    services = check_llm_services()
    
    # Configuration
    config = {
        'model': 'gpt-4',
        'max_tokens': 2000,
        'temperature': 0.7
    }
    
    # Try OpenAI first, then Ollama
    analysis = None
    if services.get('openai', {}).get('available'):
        analysis = call_openai_service(prompt, config)
    elif services.get('ollama', {}).get('available'):
        analysis = call_ollama_service(prompt, config)
    
    if analysis:
        return analysis
    else:
        return "LLM analysis unavailable. Please check your API configuration."


def parse_and_format_analysis(raw_analysis):
    """Parse and format the raw LLM analysis"""
    if not raw_analysis:
        return None
    
    # Simple parsing - just clean up the text
    cleaned_analysis = raw_analysis.strip()
    
    # Remove any markdown formatting for cleaner display
    cleaned_analysis = re.sub(r'\*\*(.*?)\*\*', r'\1', cleaned_analysis)
    cleaned_analysis = re.sub(r'\*(.*?)\*', r'\1', cleaned_analysis)
    
    return {
        'formatted_analysis': cleaned_analysis,
        'raw_analysis': raw_analysis
    }


def display_formatted_analysis(analysis_data):
    """Display formatted analysis (for compatibility)"""
    if not analysis_data:
        return "No analysis data available"
    
    return analysis_data.get('formatted_analysis', analysis_data.get('raw_analysis', 'No analysis available'))


def test_dtl_five_metrics_fixes(pose_data, swing_phases, frames=None):
    """Test function to validate the DTL metric fixes
    
    This function tests the fixes for:
    1. Shoulder tilt no longer returning ~90° due to yaw correction
    2. Hip turn no longer returning ~90° due to width-ratio method
    3. Hip depth properly comparing posterior hip positions
    4. All 5 metrics being computed and returned
    
    Args:
        pose_data (dict): Dictionary mapping frame indices to pose keypoints
        swing_phases (dict): Dictionary mapping phase names to lists of frame indices
        frames (list, optional): Video frames for analysis
        
    Returns:
        dict: Test results showing before/after values and validation
    """
    print("\n=== Testing DTL Five Metrics Fixes ===")
    
    # Test the new aggregator
    metrics = compute_dtl_five_metrics(pose_data, swing_phases, frames)
    
    if metrics is None:
        print("❌ Failed: compute_dtl_five_metrics returned None")
        return None
    
    print(f"✅ Success: Got {len(metrics)} metrics")
    
    # Validate each metric
    expected_metrics = [
        "shoulder_plane_top_deg",
        "back_tilt_setup_deg", 
        "knee_flexion_deg",
        "hip_depth_pct",
        # "hip_turn_impact_deg" # Removed hip turn metric
    ]
    
    results = {
        "total_metrics": len(metrics),
        "expected_metrics": len(expected_metrics),
        "metric_values": {},
        "validation_results": {}
    }
    
    for metric_name in expected_metrics:
        value = metrics.get(metric_name)
        results["metric_values"][metric_name] = value
        
        if value is None:
            print(f"⚠️  {metric_name}: None (calculation failed or outside sanity bounds)")
            results["validation_results"][metric_name] = "failed_or_clamped"
        elif metric_name.endswith("_deg") and value >= 80:
            print(f"❌ {metric_name}: {value}° (unrealistic high value - check calculation)")
            results["validation_results"][metric_name] = "unrealistic_high"
        else:
            print(f"✅ {metric_name}: {value}{'%' if 'pct' in metric_name else '°'} (reasonable value)")
            results["validation_results"][metric_name] = "success"
    
    # Summary
    successful_metrics = sum(1 for v in results["validation_results"].values() if v == "success")
    print(f"\n📊 Summary: {successful_metrics}/{len(expected_metrics)} metrics returning reasonable values")
    
    if successful_metrics >= 4:
        print("🎉 Overall: GOOD - Most metrics working properly")
    elif successful_metrics >= 2:
        print("⚠️  Overall: FAIR - Some metrics still need work")
    else:
        print("❌ Overall: POOR - Major issues remain")
    
    results["overall_status"] = "good" if successful_metrics >= 4 else "fair" if successful_metrics >= 2 else "poor"
    
    return results


def get_hip_depth_grading_from_value(value, confidence):
    """Helper function to create hip depth grading from just the percentage value"""
    if value is None:
        return None
    
    # Create a mock hip_depth_data structure for the existing grading function
    mock_data = {
        'depth_loss_pct': value,
        'confidence': confidence
    }
    
    # Try to import the grading function from streamlit_app
    try:
        from ..streamlit_app import get_hip_depth_grading
        return get_hip_depth_grading(mock_data, confidence)
    except ImportError:
        # Fallback grading logic
        if value <= 5:
            status = "Excellent - minimal early extension"
            badge = "🟢"
        elif value <= 15:
            status = "Good - slight early extension"
            badge = "🟡"
        else:
            status = "Needs work - significant early extension"
            badge = "🔴"
        
        return {
            'value': value,
            'status': status,
            'badge': badge,
            'detailed_data': mock_data
        }