Spaces:
Runtime error
Runtime error
Upload 10 files
Browse files- app.py +263 -115
- athletic_performance.py +51 -116
app.py
CHANGED
|
@@ -5,7 +5,7 @@ from athletic_performance import (
|
|
| 5 |
analyze_youtube_video, analyze_video_file, get_performance_insights,
|
| 6 |
get_ai_sports_coaching_analysis, test_gemini_api_connection,
|
| 7 |
generate_annotated_video_from_youtube, generate_annotated_video_from_file,
|
| 8 |
-
|
| 9 |
)
|
| 10 |
|
| 11 |
def analyze_jump_from_youtube(youtube_url, user_height_cm, user_weight_kg, progress=gr.Progress()):
|
|
@@ -63,7 +63,7 @@ def analyze_jump_from_youtube(youtube_url, user_height_cm, user_weight_kg, progr
|
|
| 63 |
|
| 64 |
### 📈 Performance Insights
|
| 65 |
"""
|
| 66 |
-
|
| 67 |
# Add performance insights using the new function
|
| 68 |
insights = get_performance_insights(result)
|
| 69 |
for insight in insights:
|
|
@@ -100,11 +100,11 @@ def analyze_jump_from_file(video_file, user_height_cm, user_weight_kg, progress=
|
|
| 100 |
# Handle errors
|
| 101 |
if "error" in result:
|
| 102 |
return f"❌ {result['error']}", None, None
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
# Handle potential None values safely
|
| 109 |
jump_height = result.get('jump_height_cm', 0) or 0
|
| 110 |
flight_time = result.get('flight_time_s', 0) or 0
|
|
@@ -142,7 +142,7 @@ def analyze_jump_from_file(video_file, user_height_cm, user_weight_kg, progress=
|
|
| 142 |
|
| 143 |
### 📈 Performance Insights
|
| 144 |
"""
|
| 145 |
-
|
| 146 |
# Add performance insights using the new function
|
| 147 |
insights = get_performance_insights(result)
|
| 148 |
for insight in insights:
|
|
@@ -397,95 +397,173 @@ Your annotated video is ready for download!
|
|
| 397 |
return results_text, summary_df, video_path
|
| 398 |
|
| 399 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
# Create Gradio interface
|
| 401 |
def create_interface():
|
| 402 |
-
|
| 403 |
-
custom_css = """
|
| 404 |
-
.gradio-container {
|
| 405 |
-
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%) !important;
|
| 406 |
-
color: #00ff88 !important;
|
| 407 |
-
}
|
| 408 |
-
.gr-button {
|
| 409 |
-
background: linear-gradient(45deg, #00ff88, #00cc6a) !important;
|
| 410 |
-
border: none !important;
|
| 411 |
-
color: #000 !important;
|
| 412 |
-
font-weight: bold !important;
|
| 413 |
-
border-radius: 8px !important;
|
| 414 |
-
transition: all 0.3s ease !important;
|
| 415 |
-
}
|
| 416 |
-
.gr-button:hover {
|
| 417 |
-
transform: translateY(-2px) !important;
|
| 418 |
-
box-shadow: 0 6px 20px rgba(0, 255, 136, 0.3) !important;
|
| 419 |
-
}
|
| 420 |
-
.gr-textbox, .gr-file, .gr-number {
|
| 421 |
-
background: #222 !important;
|
| 422 |
-
border: 1px solid #00ff88 !important;
|
| 423 |
-
color: #fff !important;
|
| 424 |
-
border-radius: 8px !important;
|
| 425 |
-
}
|
| 426 |
-
.gr-markdown {
|
| 427 |
-
color: #e0e0e0 !important;
|
| 428 |
-
}
|
| 429 |
-
.gr-markdown h1 {
|
| 430 |
-
color: #00ff88 !important;
|
| 431 |
-
text-shadow: 0 0 10px rgba(0, 255, 136, 0.5) !important;
|
| 432 |
-
text-align: center !important;
|
| 433 |
-
}
|
| 434 |
-
.gr-markdown h2 {
|
| 435 |
-
color: #00cc6a !important;
|
| 436 |
-
}
|
| 437 |
-
.gr-radio label {
|
| 438 |
-
background: #222 !important;
|
| 439 |
-
border: 1px solid #555 !important;
|
| 440 |
-
border-radius: 8px !important;
|
| 441 |
-
color: #fff !important;
|
| 442 |
-
}
|
| 443 |
-
.gr-radio label:checked {
|
| 444 |
-
background: #00ff88 !important;
|
| 445 |
-
color: #000 !important;
|
| 446 |
-
}
|
| 447 |
-
.gr-tab {
|
| 448 |
-
background: #333 !important;
|
| 449 |
-
border: 1px solid #00ff88 !important;
|
| 450 |
-
border-radius: 8px 8px 0 0 !important;
|
| 451 |
-
color: #fff !important;
|
| 452 |
-
}
|
| 453 |
-
.gr-tab.selected {
|
| 454 |
-
background: linear-gradient(45deg, #00ff88, #00cc6a) !important;
|
| 455 |
-
color: #000 !important;
|
| 456 |
-
}
|
| 457 |
-
.gr-dataframe {
|
| 458 |
-
background: #222 !important;
|
| 459 |
-
border: 1px solid #00ff88 !important;
|
| 460 |
-
border-radius: 8px !important;
|
| 461 |
-
}
|
| 462 |
-
.gr-video {
|
| 463 |
-
border: 2px solid #00ff88 !important;
|
| 464 |
-
border-radius: 12px !important;
|
| 465 |
-
box-shadow: 0 0 20px rgba(0, 255, 136, 0.3) !important;
|
| 466 |
-
background: #111 !important;
|
| 467 |
-
}
|
| 468 |
-
.gr-form {
|
| 469 |
-
background: #1a1a1a !important;
|
| 470 |
-
border-radius: 12px !important;
|
| 471 |
-
padding: 20px !important;
|
| 472 |
-
}
|
| 473 |
-
"""
|
| 474 |
-
|
| 475 |
-
with gr.Blocks(title="🚀 Athletic Performance AI", css=custom_css, theme=gr.themes.Soft()) as app:
|
| 476 |
gr.Markdown("""
|
| 477 |
-
#
|
| 478 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
""")
|
| 480 |
|
| 481 |
with gr.Row():
|
| 482 |
with gr.Column():
|
| 483 |
user_height = gr.Number(
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
with gr.Column():
|
| 490 |
user_weight = gr.Number(
|
| 491 |
label="Your Weight (kg)",
|
|
@@ -493,27 +571,50 @@ def create_interface():
|
|
| 493 |
minimum=30,
|
| 494 |
maximum=200
|
| 495 |
)
|
|
|
|
| 496 |
|
| 497 |
with gr.Tabs():
|
| 498 |
# YouTube URL Tab
|
| 499 |
-
with gr.TabItem("🎥 YouTube"):
|
|
|
|
| 500 |
youtube_url = gr.Textbox(
|
| 501 |
label="YouTube URL",
|
| 502 |
placeholder="https://youtube.com/watch?v=..."
|
| 503 |
)
|
| 504 |
-
youtube_btn = gr.Button("🚀 Analyze", variant="primary")
|
| 505 |
|
| 506 |
# File Upload Tab
|
| 507 |
-
with gr.TabItem("📁 Upload"):
|
|
|
|
| 508 |
video_file = gr.File(
|
| 509 |
-
label="Upload Video",
|
| 510 |
file_types=[".mp4", ".avi", ".mov", ".mkv", ".webm"]
|
| 511 |
)
|
| 512 |
-
file_btn = gr.Button("🚀 Analyze", variant="primary")
|
| 513 |
|
| 514 |
# Complete Analysis Tab
|
| 515 |
-
with gr.TabItem("🎬
|
| 516 |
-
gr.Markdown("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
|
| 518 |
with gr.Row():
|
| 519 |
with gr.Column():
|
|
@@ -522,58 +623,96 @@ def create_interface():
|
|
| 522 |
label="Gender",
|
| 523 |
value="Male"
|
| 524 |
)
|
|
|
|
| 525 |
|
|
|
|
| 526 |
default_api_key = os.getenv("GEMINI_API_KEY", "")
|
| 527 |
complete_gemini_key = gr.Textbox(
|
| 528 |
-
label="Gemini API Key (Optional)",
|
| 529 |
-
placeholder="Enter API key for AI coaching" if not default_api_key else "API key loaded",
|
| 530 |
type="password",
|
| 531 |
value=default_api_key
|
| 532 |
)
|
| 533 |
|
| 534 |
-
|
|
|
|
|
|
|
| 535 |
api_test_result = gr.Textbox(
|
| 536 |
-
label="Test Result",
|
| 537 |
-
lines=
|
| 538 |
interactive=False,
|
| 539 |
visible=False
|
| 540 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
|
| 542 |
with gr.Column():
|
| 543 |
complete_youtube_url = gr.Textbox(
|
| 544 |
-
label="YouTube URL",
|
| 545 |
placeholder="https://youtube.com/watch?v=..."
|
| 546 |
)
|
| 547 |
complete_video_file = gr.File(
|
| 548 |
-
label="
|
| 549 |
file_types=[".mp4", ".avi", ".mov", ".mkv", ".webm"]
|
| 550 |
)
|
|
|
|
| 551 |
|
| 552 |
-
complete_analysis_btn = gr.Button("🚀
|
| 553 |
|
| 554 |
# Results section
|
| 555 |
-
gr.Markdown("## 📊 Results")
|
| 556 |
-
|
| 557 |
-
# Video streaming section - make it prominent
|
| 558 |
-
with gr.Row():
|
| 559 |
-
analysis_video = gr.Video(
|
| 560 |
-
label="📺 Analysis Video",
|
| 561 |
-
visible=False,
|
| 562 |
-
height=400
|
| 563 |
-
)
|
| 564 |
|
| 565 |
with gr.Row():
|
| 566 |
with gr.Column(scale=2):
|
| 567 |
-
results_text = gr.Markdown(label="
|
| 568 |
with gr.Column(scale=1):
|
| 569 |
results_table = gr.Dataframe(
|
| 570 |
-
label="Metrics",
|
| 571 |
headers=["Metric", "Value"],
|
| 572 |
datatype=["str", "str"]
|
| 573 |
)
|
| 574 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
status_message = gr.Textbox(label="Status", interactive=False)
|
| 576 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
# Event handlers
|
| 578 |
youtube_btn.click(
|
| 579 |
fn=analyze_jump_from_youtube,
|
|
@@ -618,6 +757,15 @@ def create_interface():
|
|
| 618 |
inputs=[complete_gemini_key],
|
| 619 |
outputs=[api_test_result]
|
| 620 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
|
| 622 |
return app
|
| 623 |
|
|
|
|
| 5 |
analyze_youtube_video, analyze_video_file, get_performance_insights,
|
| 6 |
get_ai_sports_coaching_analysis, test_gemini_api_connection,
|
| 7 |
generate_annotated_video_from_youtube, generate_annotated_video_from_file,
|
| 8 |
+
generate_complete_analysis_with_video
|
| 9 |
)
|
| 10 |
|
| 11 |
def analyze_jump_from_youtube(youtube_url, user_height_cm, user_weight_kg, progress=gr.Progress()):
|
|
|
|
| 63 |
|
| 64 |
### 📈 Performance Insights
|
| 65 |
"""
|
| 66 |
+
|
| 67 |
# Add performance insights using the new function
|
| 68 |
insights = get_performance_insights(result)
|
| 69 |
for insight in insights:
|
|
|
|
| 100 |
# Handle errors
|
| 101 |
if "error" in result:
|
| 102 |
return f"❌ {result['error']}", None, None
|
| 103 |
+
|
| 104 |
+
if result is None:
|
| 105 |
+
return "⚠️ Could not analyze jump. Make sure the video shows a person clearly performing a vertical jump.", None, None
|
| 106 |
+
|
| 107 |
+
# Format results (same as YouTube function)
|
| 108 |
# Handle potential None values safely
|
| 109 |
jump_height = result.get('jump_height_cm', 0) or 0
|
| 110 |
flight_time = result.get('flight_time_s', 0) or 0
|
|
|
|
| 142 |
|
| 143 |
### 📈 Performance Insights
|
| 144 |
"""
|
| 145 |
+
|
| 146 |
# Add performance insights using the new function
|
| 147 |
insights = get_performance_insights(result)
|
| 148 |
for insight in insights:
|
|
|
|
| 397 |
return results_text, summary_df, video_path
|
| 398 |
|
| 399 |
|
| 400 |
+
def complete_analysis_with_video_stream(youtube_url, video_file, user_height_cm, user_weight_kg, gender, gemini_api_key, progress=gr.Progress()):
|
| 401 |
+
"""Complete analysis with AI coaching and video streaming."""
|
| 402 |
+
|
| 403 |
+
# Validate inputs
|
| 404 |
+
if not user_height_cm or user_height_cm <= 0:
|
| 405 |
+
return "❌ Please provide a valid height", None, None, None
|
| 406 |
+
|
| 407 |
+
if not user_weight_kg or user_weight_kg <= 0:
|
| 408 |
+
return "❌ Please provide a valid weight", None, None, None
|
| 409 |
+
|
| 410 |
+
if not gender:
|
| 411 |
+
return "❌ Please select your gender", None, None, None
|
| 412 |
+
|
| 413 |
+
# Determine video source and path
|
| 414 |
+
video_source = None
|
| 415 |
+
video_path_or_url = None
|
| 416 |
+
|
| 417 |
+
if youtube_url and youtube_url.strip():
|
| 418 |
+
video_source = "youtube"
|
| 419 |
+
video_path_or_url = youtube_url.strip()
|
| 420 |
+
progress(0.05, desc="Processing YouTube video...")
|
| 421 |
+
elif video_file:
|
| 422 |
+
video_source = "file"
|
| 423 |
+
video_path_or_url = video_file.name
|
| 424 |
+
progress(0.05, desc="Processing uploaded video...")
|
| 425 |
+
else:
|
| 426 |
+
return "❌ Please provide either a YouTube URL or upload a video file", None, None, None
|
| 427 |
+
|
| 428 |
+
try:
|
| 429 |
+
# Create progress callback
|
| 430 |
+
def progress_callback(prog, desc):
|
| 431 |
+
progress(prog, desc=desc)
|
| 432 |
+
|
| 433 |
+
# Run complete analysis
|
| 434 |
+
result = generate_complete_analysis_with_video(
|
| 435 |
+
video_source=video_source,
|
| 436 |
+
video_path_or_url=video_path_or_url,
|
| 437 |
+
user_height_cm=user_height_cm,
|
| 438 |
+
user_weight_kg=user_weight_kg,
|
| 439 |
+
gender=gender,
|
| 440 |
+
api_key=gemini_api_key.strip() if gemini_api_key else None,
|
| 441 |
+
progress_callback=progress_callback
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
# Handle errors
|
| 445 |
+
if "error" in result:
|
| 446 |
+
return f"❌ Analysis failed: {result['error']}", None, None, None
|
| 447 |
+
|
| 448 |
+
if not result.get("success"):
|
| 449 |
+
return "❌ Analysis failed. Please ensure the video shows a clear vertical jump.", None, None, None
|
| 450 |
+
|
| 451 |
+
# Extract results
|
| 452 |
+
video_path = result.get("video_path")
|
| 453 |
+
jump_metrics = result.get("jump_metrics", {})
|
| 454 |
+
jump_references = result.get("jump_references", {})
|
| 455 |
+
ai_coaching = result.get("ai_coaching")
|
| 456 |
+
|
| 457 |
+
# Handle potential None values safely
|
| 458 |
+
jump_height = jump_metrics.get('jump_height_cm', 0) or 0
|
| 459 |
+
flight_time = jump_metrics.get('flight_time_s', 0) or 0
|
| 460 |
+
peak_power = jump_metrics.get('peak_power_watts', 0) or 0
|
| 461 |
+
rfd = jump_metrics.get('rate_of_force_development', 0) or 0
|
| 462 |
+
|
| 463 |
+
# Format comprehensive results
|
| 464 |
+
results_text = f"""
|
| 465 |
+
# 🎬 Complete Athletic Performance Analysis
|
| 466 |
+
|
| 467 |
+
## 📊 Performance Metrics
|
| 468 |
+
- **Jump Height**: {jump_height:.2f} cm
|
| 469 |
+
- **Relative Jump**: {(jump_height/user_height_cm*100):.1f}% of body height
|
| 470 |
+
- **Flight Time**: {flight_time:.3f} seconds
|
| 471 |
+
- **Peak Power**: {peak_power:.0f} watts
|
| 472 |
+
- **Rate of Force Development**: {rfd:.2f}
|
| 473 |
+
|
| 474 |
+
## 📏 Performance Comparison
|
| 475 |
+
- **Your Performance**: {jump_height:.1f} cm
|
| 476 |
+
- **Average for {gender}**: {jump_references.get('average', 0):.1f} cm
|
| 477 |
+
- **Professional Level**: {jump_references.get('professional', 0):.1f} cm
|
| 478 |
+
|
| 479 |
+
## 🎥 Video Analysis Features
|
| 480 |
+
- ✅ **Real-time Pose Tracking**: Skeleton overlay throughout jump
|
| 481 |
+
- ✅ **Performance Reference Lines**: Average vs Professional benchmarks
|
| 482 |
+
- ✅ **Knee Strain Detection**: Automatic form analysis with warnings
|
| 483 |
+
- ✅ **Live Metrics Display**: Frame-by-frame jump height tracking
|
| 484 |
+
|
| 485 |
+
"""
|
| 486 |
+
|
| 487 |
+
# Add AI coaching analysis if available
|
| 488 |
+
if ai_coaching and ai_coaching.get("success"):
|
| 489 |
+
results_text += f"""
|
| 490 |
+
## 🤖 AI Sports Coach Analysis
|
| 491 |
+
|
| 492 |
+
{ai_coaching.get('analysis', 'AI analysis not available')}
|
| 493 |
+
|
| 494 |
+
---
|
| 495 |
+
*Analysis powered by Google Gemini AI*
|
| 496 |
+
"""
|
| 497 |
+
elif gemini_api_key:
|
| 498 |
+
results_text += """
|
| 499 |
+
## 🤖 AI Sports Coach Analysis
|
| 500 |
+
|
| 501 |
+
❌ AI coaching analysis failed. Please check your API key and try again.
|
| 502 |
+
"""
|
| 503 |
+
else:
|
| 504 |
+
results_text += """
|
| 505 |
+
## 🤖 AI Sports Coach Analysis
|
| 506 |
+
|
| 507 |
+
💡 **Provide a Gemini API key to get personalized sports recommendations and technique improvements!**
|
| 508 |
+
|
| 509 |
+
Get your free API key at: [Google AI Studio](https://aistudio.google.com/app/apikey)
|
| 510 |
+
"""
|
| 511 |
+
|
| 512 |
+
# Create comprehensive summary dataframe
|
| 513 |
+
summary_data = [
|
| 514 |
+
["Jump Height", f"{jump_height:.2f} cm"],
|
| 515 |
+
["Flight Time", f"{flight_time:.3f} seconds"],
|
| 516 |
+
["Peak Power", f"{peak_power:.0f} watts"],
|
| 517 |
+
["Rate of Force Development", f"{rfd:.2f}"],
|
| 518 |
+
["Performance vs Average", f"{((jump_height/jump_references.get('average', 1))*100):.0f}%"],
|
| 519 |
+
["Performance vs Pro", f"{((jump_height/jump_references.get('professional', 1))*100):.0f}%"],
|
| 520 |
+
["Video Features", "Pose + References + Strain Detection"],
|
| 521 |
+
["AI Coaching", "✅ Included" if ai_coaching and ai_coaching.get("success") else "❌ Not Available"],
|
| 522 |
+
]
|
| 523 |
+
|
| 524 |
+
summary_df = pd.DataFrame(summary_data, columns=["Metric", "Value"])
|
| 525 |
+
|
| 526 |
+
# Return results with video for streaming
|
| 527 |
+
return results_text, summary_df, video_path, "✅ Complete analysis ready!"
|
| 528 |
+
|
| 529 |
+
except Exception as e:
|
| 530 |
+
return f"❌ Unexpected error: {str(e)}", None, None, None
|
| 531 |
+
|
| 532 |
+
|
| 533 |
# Create Gradio interface
|
| 534 |
def create_interface():
|
| 535 |
+
with gr.Blocks(title="🏃♂️ Athletic Ability Analysis") as app:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
gr.Markdown("""
|
| 537 |
+
# 🏃♂️ Athletic Ability Analysis & AI Sports Coach
|
| 538 |
+
|
| 539 |
+
Analyze jumping performance from videos using computer vision and get AI-powered sports coaching recommendations.
|
| 540 |
+
Upload a video or provide a YouTube URL to get detailed metrics and personalized coaching insights.
|
| 541 |
+
|
| 542 |
+
## 🚀 Features
|
| 543 |
+
- **📊 Biomechanical Analysis**: Comprehensive jump metrics (height, power, force, RFD)
|
| 544 |
+
- **🤖 AI Sports Coach**: Personalized sport recommendations and technique improvements
|
| 545 |
+
- **🎬 Annotated Videos**: Generate training videos with pose tracking and performance overlays
|
| 546 |
+
- **⚠️ Technique Analysis**: Real-time knee strain detection and form corrections
|
| 547 |
+
- **🎯 Performance Insights**: Professional-grade analysis and training suggestions
|
| 548 |
+
- **📺 Video Streaming**: Watch your annotated analysis video directly in the browser
|
| 549 |
+
|
| 550 |
+
## 📋 Instructions
|
| 551 |
+
1. Enter your height in centimeters and weight in kilograms
|
| 552 |
+
2. Choose your analysis type:
|
| 553 |
+
- **📊 Standard Analysis**: Get detailed biomechanical metrics only
|
| 554 |
+
- **🎬 Complete Analysis**: Get annotated video + AI coaching + streaming (Recommended!)
|
| 555 |
+
3. Provide a video (YouTube URL or file upload)
|
| 556 |
+
4. Get comprehensive results with video streaming and AI coaching insights
|
| 557 |
""")
|
| 558 |
|
| 559 |
with gr.Row():
|
| 560 |
with gr.Column():
|
| 561 |
user_height = gr.Number(
|
| 562 |
+
label="Your Height (cm)",
|
| 563 |
+
value=175,
|
| 564 |
+
minimum=100,
|
| 565 |
+
maximum=250
|
| 566 |
+
)
|
| 567 |
with gr.Column():
|
| 568 |
user_weight = gr.Number(
|
| 569 |
label="Your Weight (kg)",
|
|
|
|
| 571 |
minimum=30,
|
| 572 |
maximum=200
|
| 573 |
)
|
| 574 |
+
gr.Markdown("💡 *Enter your height and weight for accurate biomechanical calculations*")
|
| 575 |
|
| 576 |
with gr.Tabs():
|
| 577 |
# YouTube URL Tab
|
| 578 |
+
with gr.TabItem("🎥 YouTube Video"):
|
| 579 |
+
gr.Markdown("📺 *Paste a YouTube URL containing a video of someone jumping*")
|
| 580 |
youtube_url = gr.Textbox(
|
| 581 |
label="YouTube URL",
|
| 582 |
placeholder="https://youtube.com/watch?v=..."
|
| 583 |
)
|
| 584 |
+
youtube_btn = gr.Button("🚀 Analyze YouTube Video", variant="primary")
|
| 585 |
|
| 586 |
# File Upload Tab
|
| 587 |
+
with gr.TabItem("📁 Upload Video"):
|
| 588 |
+
gr.Markdown("📁 *Upload a video file showing someone performing a jump*")
|
| 589 |
video_file = gr.File(
|
| 590 |
+
label="Upload Video File",
|
| 591 |
file_types=[".mp4", ".avi", ".mov", ".mkv", ".webm"]
|
| 592 |
)
|
| 593 |
+
file_btn = gr.Button("🚀 Analyze Uploaded Video", variant="primary")
|
| 594 |
|
| 595 |
# Complete Analysis Tab
|
| 596 |
+
with gr.TabItem("🎬 Complete Analysis + AI Coach"):
|
| 597 |
+
gr.Markdown("""
|
| 598 |
+
## 🎬 Complete Athletic Performance Analysis
|
| 599 |
+
|
| 600 |
+
Get the ultimate training analysis combining:
|
| 601 |
+
|
| 602 |
+
### 🎥 Annotated Training Video
|
| 603 |
+
- **🦴 Real-time Pose Tracking**: Skeleton overlay throughout jump
|
| 604 |
+
- **📏 Performance Reference Lines**: Average vs Professional benchmarks
|
| 605 |
+
- **⚠️ Knee Strain Detection**: Automatic form analysis with red warnings
|
| 606 |
+
- **📊 Live Metrics Display**: Frame-by-frame jump height tracking
|
| 607 |
+
|
| 608 |
+
### 🤖 AI Sports Coach Analysis
|
| 609 |
+
- **🏆 Sport Recommendations**: Top 3 sports matching your athletic profile
|
| 610 |
+
- **🎯 Technique Improvements**: Specific jump form corrections
|
| 611 |
+
- **📈 Training Insights**: Personalized performance enhancement tips
|
| 612 |
+
|
| 613 |
+
### 📺 Video Streaming
|
| 614 |
+
- **Watch directly in browser**: No downloads required
|
| 615 |
+
- **Professional annotations**: Training-ready video output
|
| 616 |
+
- **Shareable results**: Perfect for coaches and athletes
|
| 617 |
+
""")
|
| 618 |
|
| 619 |
with gr.Row():
|
| 620 |
with gr.Column():
|
|
|
|
| 623 |
label="Gender",
|
| 624 |
value="Male"
|
| 625 |
)
|
| 626 |
+
gr.Markdown("*Used for performance references and AI analysis*")
|
| 627 |
|
| 628 |
+
# Check if API key is available in environment
|
| 629 |
default_api_key = os.getenv("GEMINI_API_KEY", "")
|
| 630 |
complete_gemini_key = gr.Textbox(
|
| 631 |
+
label="Gemini API Key (Optional for AI Coaching)",
|
| 632 |
+
placeholder="Enter your Google Gemini API key for AI coaching" if not default_api_key else "API key loaded from environment",
|
| 633 |
type="password",
|
| 634 |
value=default_api_key
|
| 635 |
)
|
| 636 |
|
| 637 |
+
with gr.Row():
|
| 638 |
+
test_api_btn = gr.Button("🧪 Test API Key", size="sm")
|
| 639 |
+
|
| 640 |
api_test_result = gr.Textbox(
|
| 641 |
+
label="API Test Result",
|
| 642 |
+
lines=3,
|
| 643 |
interactive=False,
|
| 644 |
visible=False
|
| 645 |
)
|
| 646 |
+
|
| 647 |
+
gr.Markdown("""
|
| 648 |
+
💡 **Get your free API key**: [Google AI Studio](https://aistudio.google.com/app/apikey)
|
| 649 |
+
|
| 650 |
+
⚠️ **Note**: Video generation works without API key, but AI coaching requires one
|
| 651 |
+
""")
|
| 652 |
|
| 653 |
with gr.Column():
|
| 654 |
complete_youtube_url = gr.Textbox(
|
| 655 |
+
label="YouTube URL (Option 1)",
|
| 656 |
placeholder="https://youtube.com/watch?v=..."
|
| 657 |
)
|
| 658 |
complete_video_file = gr.File(
|
| 659 |
+
label="Upload Video File (Option 2)",
|
| 660 |
file_types=[".mp4", ".avi", ".mov", ".mkv", ".webm"]
|
| 661 |
)
|
| 662 |
+
gr.Markdown("*Provide either a YouTube URL or upload a video file*")
|
| 663 |
|
| 664 |
+
complete_analysis_btn = gr.Button("🚀 Get Complete Analysis + Video", variant="primary", size="lg")
|
| 665 |
|
| 666 |
# Results section
|
| 667 |
+
gr.Markdown("## 📊 Analysis Results")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
|
| 669 |
with gr.Row():
|
| 670 |
with gr.Column(scale=2):
|
| 671 |
+
results_text = gr.Markdown(label="Results")
|
| 672 |
with gr.Column(scale=1):
|
| 673 |
results_table = gr.Dataframe(
|
| 674 |
+
label="Metrics Summary",
|
| 675 |
headers=["Metric", "Value"],
|
| 676 |
datatype=["str", "str"]
|
| 677 |
)
|
| 678 |
|
| 679 |
+
# Video streaming section
|
| 680 |
+
with gr.Row():
|
| 681 |
+
with gr.Column():
|
| 682 |
+
analysis_video = gr.Video(
|
| 683 |
+
label="📺 Annotated Training Video",
|
| 684 |
+
visible=False
|
| 685 |
+
)
|
| 686 |
+
|
| 687 |
status_message = gr.Textbox(label="Status", interactive=False)
|
| 688 |
|
| 689 |
+
# Video requirements
|
| 690 |
+
gr.Markdown("""
|
| 691 |
+
## 📝 Video Requirements
|
| 692 |
+
|
| 693 |
+
For best results, ensure your videos meet these criteria:
|
| 694 |
+
|
| 695 |
+
- **Full body visible**: The person should be completely visible in the frame
|
| 696 |
+
- **Clear movement**: Good lighting and minimal background clutter
|
| 697 |
+
- **Vertical jumps**: Works best with straight vertical jumps
|
| 698 |
+
- **Duration**: 3-30 seconds is optimal
|
| 699 |
+
- **Quality**: Higher quality videos produce better results
|
| 700 |
+
- **Public videos**: For YouTube, ensure the video is not private
|
| 701 |
+
|
| 702 |
+
## 🔬 How it Works
|
| 703 |
+
|
| 704 |
+
1. **Pose Detection**: Uses Google's MediaPipe to detect human pose landmarks
|
| 705 |
+
2. **Hip Tracking**: Tracks the midpoint between left and right hip joints
|
| 706 |
+
3. **Biomechanical Analysis**: Calculates comprehensive metrics based on hip trajectory:
|
| 707 |
+
- **Jump Height**: Relative to your body size
|
| 708 |
+
- **Flight Time**: Duration in the air
|
| 709 |
+
- **Peak Power Output**: Maximum power generated during takeoff
|
| 710 |
+
- **Rate of Force Development (RFD)**: Speed of force generation
|
| 711 |
+
- **Ground Contact Time**: Efficiency in stretch-shortening cycle
|
| 712 |
+
- **Impulse & Peak Force**: Force characteristics during takeoff
|
| 713 |
+
- **Takeoff Phase Duration**: Time from crouch to launch
|
| 714 |
+
""")
|
| 715 |
+
|
| 716 |
# Event handlers
|
| 717 |
youtube_btn.click(
|
| 718 |
fn=analyze_jump_from_youtube,
|
|
|
|
| 757 |
inputs=[complete_gemini_key],
|
| 758 |
outputs=[api_test_result]
|
| 759 |
)
|
| 760 |
+
|
| 761 |
+
# Example section
|
| 762 |
+
gr.Examples(
|
| 763 |
+
examples=[
|
| 764 |
+
["https://www.youtube.com/watch?v=dQw4w9WgXcQ", 175, 75], # This is just a placeholder
|
| 765 |
+
],
|
| 766 |
+
inputs=[youtube_url, user_height, user_weight],
|
| 767 |
+
label="📚 Example (Replace with actual jump video URLs)"
|
| 768 |
+
)
|
| 769 |
|
| 770 |
return app
|
| 771 |
|
athletic_performance.py
CHANGED
|
@@ -9,7 +9,6 @@ import yt_dlp
|
|
| 9 |
import json
|
| 10 |
import requests
|
| 11 |
import math
|
| 12 |
-
import pandas as pd
|
| 13 |
|
| 14 |
# MediaPipe pose landmarks
|
| 15 |
LHIP, RHIP = 23, 24
|
|
@@ -827,44 +826,31 @@ def generate_annotated_video_from_file(video_file_path, user_height_cm, user_wei
|
|
| 827 |
return {"error": f"Error during video generation: {str(e)}"}
|
| 828 |
|
| 829 |
|
| 830 |
-
def
|
| 831 |
-
"""
|
|
|
|
| 832 |
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
video_path_or_url = None
|
| 846 |
-
|
| 847 |
-
if youtube_url and youtube_url.strip():
|
| 848 |
-
video_source = "youtube"
|
| 849 |
-
video_path_or_url = youtube_url.strip()
|
| 850 |
-
if progress:
|
| 851 |
-
progress(0.05, desc="Processing YouTube video...")
|
| 852 |
-
elif video_file:
|
| 853 |
-
video_source = "file"
|
| 854 |
-
video_path_or_url = video_file.name
|
| 855 |
-
if progress:
|
| 856 |
-
progress(0.05, desc="Processing uploaded video...")
|
| 857 |
-
else:
|
| 858 |
-
return "❌ Please provide either a YouTube URL or upload a video file", None, None, None
|
| 859 |
|
| 860 |
try:
|
| 861 |
# Phase 1: Generate annotated video (50% of progress)
|
| 862 |
-
if
|
| 863 |
-
|
| 864 |
|
| 865 |
def video_progress(prog, desc):
|
| 866 |
-
if
|
| 867 |
-
|
| 868 |
|
| 869 |
# Generate annotated video
|
| 870 |
if video_source == "youtube":
|
|
@@ -877,16 +863,20 @@ def complete_analysis_with_video_stream(youtube_url, video_file, user_height_cm,
|
|
| 877 |
)
|
| 878 |
|
| 879 |
if "error" in video_result:
|
| 880 |
-
return
|
| 881 |
|
| 882 |
# Phase 2: AI Sports Coaching Analysis (30% of progress)
|
| 883 |
-
if
|
| 884 |
-
|
| 885 |
|
| 886 |
ai_result = None
|
| 887 |
-
if
|
| 888 |
jump_metrics = video_result.get("jump_metrics", {})
|
| 889 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 890 |
ai_result = get_ai_sports_coaching_analysis(
|
| 891 |
jump_height_cm=jump_metrics.get('jump_height_cm', 0),
|
| 892 |
user_height_cm=user_height_cm,
|
|
@@ -894,97 +884,42 @@ def complete_analysis_with_video_stream(youtube_url, video_file, user_height_cm,
|
|
| 894 |
peak_power_watts=jump_metrics.get('peak_power_watts'),
|
| 895 |
flight_time_s=jump_metrics.get('flight_time_s'),
|
| 896 |
rfd=jump_metrics.get('rate_of_force_development'),
|
| 897 |
-
api_key=
|
| 898 |
)
|
| 899 |
|
| 900 |
-
if
|
| 901 |
-
|
| 902 |
|
| 903 |
# Phase 3: Combine results (20% of progress)
|
| 904 |
-
if
|
| 905 |
-
|
| 906 |
|
| 907 |
jump_metrics = video_result.get("jump_metrics", {})
|
| 908 |
jump_references = video_result.get("jump_references", {})
|
| 909 |
|
| 910 |
-
#
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
- **Rate of Force Development**: {rfd:.2f}
|
| 926 |
-
|
| 927 |
-
## 📏 Performance Comparison
|
| 928 |
-
- **Your Performance**: {jump_height:.1f} cm
|
| 929 |
-
- **Average for {gender}**: {jump_references.get('average', 0):.1f} cm
|
| 930 |
-
- **Professional Level**: {jump_references.get('professional', 0):.1f} cm
|
| 931 |
-
|
| 932 |
-
## 🎥 Video Analysis Features
|
| 933 |
-
- ✅ **Real-time Pose Tracking**: Skeleton overlay throughout jump
|
| 934 |
-
- ✅ **Performance Reference Lines**: Average vs Professional benchmarks
|
| 935 |
-
- ✅ **Knee Strain Detection**: Automatic form analysis with warnings
|
| 936 |
-
- ✅ **Live Metrics Display**: Frame-by-frame jump height tracking
|
| 937 |
-
|
| 938 |
-
"""
|
| 939 |
-
|
| 940 |
-
# Add AI coaching analysis if available
|
| 941 |
-
if ai_result and ai_result.get("success"):
|
| 942 |
-
results_text += f"""
|
| 943 |
-
## 🤖 AI Sports Coach Analysis
|
| 944 |
-
|
| 945 |
-
{ai_result.get('analysis', 'AI analysis not available')}
|
| 946 |
-
|
| 947 |
-
---
|
| 948 |
-
*Analysis powered by Google Gemini AI*
|
| 949 |
-
"""
|
| 950 |
-
elif gemini_api_key:
|
| 951 |
-
results_text += """
|
| 952 |
-
## 🤖 AI Sports Coach Analysis
|
| 953 |
-
|
| 954 |
-
❌ AI coaching analysis failed. Please check your API key and try again.
|
| 955 |
-
"""
|
| 956 |
-
else:
|
| 957 |
-
results_text += """
|
| 958 |
-
## 🤖 AI Sports Coach Analysis
|
| 959 |
-
|
| 960 |
-
💡 **Provide a Gemini API key to get personalized sports recommendations and technique improvements!**
|
| 961 |
-
|
| 962 |
-
Get your free API key at: [Google AI Studio](https://aistudio.google.com/app/apikey)
|
| 963 |
-
"""
|
| 964 |
-
|
| 965 |
-
# Create comprehensive summary dataframe
|
| 966 |
-
summary_data = [
|
| 967 |
-
["Jump Height", f"{jump_height:.2f} cm"],
|
| 968 |
-
["Flight Time", f"{flight_time:.3f} seconds"],
|
| 969 |
-
["Peak Power", f"{peak_power:.0f} watts"],
|
| 970 |
-
["Rate of Force Development", f"{rfd:.2f}"],
|
| 971 |
-
["Performance vs Average", f"{((jump_height/jump_references.get('average', 1))*100):.0f}%"],
|
| 972 |
-
["Performance vs Pro", f"{((jump_height/jump_references.get('professional', 1))*100):.0f}%"],
|
| 973 |
-
["Video Features", "Pose + References + Strain Detection"],
|
| 974 |
-
["AI Coaching", "✅ Included" if ai_result and ai_result.get("success") else "❌ Not Available"],
|
| 975 |
-
]
|
| 976 |
-
|
| 977 |
-
summary_df = pd.DataFrame(summary_data, columns=["Metric", "Value"])
|
| 978 |
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
if progress:
|
| 982 |
-
progress(1.0, desc="Complete analysis ready!")
|
| 983 |
|
| 984 |
-
return
|
| 985 |
|
| 986 |
except Exception as e:
|
| 987 |
-
return f"
|
| 988 |
|
| 989 |
|
| 990 |
def get_performance_insights(result_dict):
|
|
|
|
| 9 |
import json
|
| 10 |
import requests
|
| 11 |
import math
|
|
|
|
| 12 |
|
| 13 |
# MediaPipe pose landmarks
|
| 14 |
LHIP, RHIP = 23, 24
|
|
|
|
| 826 |
return {"error": f"Error during video generation: {str(e)}"}
|
| 827 |
|
| 828 |
|
| 829 |
+
def generate_complete_analysis_with_video(video_source, video_path_or_url, user_height_cm, user_weight_kg, gender, api_key=None, progress_callback=None):
|
| 830 |
+
"""
|
| 831 |
+
Complete analysis combining annotated video generation + AI sports coaching.
|
| 832 |
|
| 833 |
+
Args:
|
| 834 |
+
video_source (str): "youtube" or "file"
|
| 835 |
+
video_path_or_url (str): YouTube URL or file path
|
| 836 |
+
user_height_cm (float): User's height in centimeters
|
| 837 |
+
user_weight_kg (float): User's weight in kilograms
|
| 838 |
+
gender (str): "Male" or "Female"
|
| 839 |
+
api_key (str, optional): Gemini API key for AI coaching
|
| 840 |
+
progress_callback (callable, optional): Progress tracking function
|
| 841 |
+
|
| 842 |
+
Returns:
|
| 843 |
+
dict: Complete analysis with video path, AI coaching, and metrics
|
| 844 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 845 |
|
| 846 |
try:
|
| 847 |
# Phase 1: Generate annotated video (50% of progress)
|
| 848 |
+
if progress_callback:
|
| 849 |
+
progress_callback(0.1, "Starting comprehensive analysis...")
|
| 850 |
|
| 851 |
def video_progress(prog, desc):
|
| 852 |
+
if progress_callback:
|
| 853 |
+
progress_callback(0.1 + (prog * 0.4), f"Video: {desc}")
|
| 854 |
|
| 855 |
# Generate annotated video
|
| 856 |
if video_source == "youtube":
|
|
|
|
| 863 |
)
|
| 864 |
|
| 865 |
if "error" in video_result:
|
| 866 |
+
return video_result
|
| 867 |
|
| 868 |
# Phase 2: AI Sports Coaching Analysis (30% of progress)
|
| 869 |
+
if progress_callback:
|
| 870 |
+
progress_callback(0.5, "Generating AI sports coaching analysis...")
|
| 871 |
|
| 872 |
ai_result = None
|
| 873 |
+
if api_key:
|
| 874 |
jump_metrics = video_result.get("jump_metrics", {})
|
| 875 |
|
| 876 |
+
def ai_progress(prog, desc):
|
| 877 |
+
if progress_callback:
|
| 878 |
+
progress_callback(0.5 + (prog * 0.3), f"AI: {desc}")
|
| 879 |
+
|
| 880 |
ai_result = get_ai_sports_coaching_analysis(
|
| 881 |
jump_height_cm=jump_metrics.get('jump_height_cm', 0),
|
| 882 |
user_height_cm=user_height_cm,
|
|
|
|
| 884 |
peak_power_watts=jump_metrics.get('peak_power_watts'),
|
| 885 |
flight_time_s=jump_metrics.get('flight_time_s'),
|
| 886 |
rfd=jump_metrics.get('rate_of_force_development'),
|
| 887 |
+
api_key=api_key
|
| 888 |
)
|
| 889 |
|
| 890 |
+
if progress_callback:
|
| 891 |
+
progress_callback(0.8, "AI analysis complete!")
|
| 892 |
|
| 893 |
# Phase 3: Combine results (20% of progress)
|
| 894 |
+
if progress_callback:
|
| 895 |
+
progress_callback(0.9, "Finalizing comprehensive analysis...")
|
| 896 |
|
| 897 |
jump_metrics = video_result.get("jump_metrics", {})
|
| 898 |
jump_references = video_result.get("jump_references", {})
|
| 899 |
|
| 900 |
+
# Create comprehensive result
|
| 901 |
+
result = {
|
| 902 |
+
"success": True,
|
| 903 |
+
"video_path": video_result.get("output_video_path"),
|
| 904 |
+
"jump_metrics": jump_metrics,
|
| 905 |
+
"jump_references": jump_references,
|
| 906 |
+
"ai_coaching": ai_result if ai_result and "error" not in ai_result else None,
|
| 907 |
+
"video_features": {
|
| 908 |
+
"pose_tracking": True,
|
| 909 |
+
"reference_lines": True,
|
| 910 |
+
"knee_strain_detection": True,
|
| 911 |
+
"live_metrics": True
|
| 912 |
+
},
|
| 913 |
+
"analysis_type": "complete_with_video"
|
| 914 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 915 |
|
| 916 |
+
if progress_callback:
|
| 917 |
+
progress_callback(1.0, "Complete analysis ready!")
|
|
|
|
|
|
|
| 918 |
|
| 919 |
+
return result
|
| 920 |
|
| 921 |
except Exception as e:
|
| 922 |
+
return {"error": f"Error during complete analysis: {str(e)}"}
|
| 923 |
|
| 924 |
|
| 925 |
def get_performance_insights(result_dict):
|