Spaces:
Sleeping
Sleeping
| import os | |
| import streamlit as st | |
| import pandas as pd | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from stravalib.client import Client | |
| import google.generativeai as genai | |
| from dotenv import load_dotenv | |
| from datetime import datetime, timedelta | |
| # Load environment variables | |
| load_dotenv() | |
| # API Keys and Secrets | |
| STRAVA_CLIENT_ID = os.getenv('STRAVA_CLIENT_ID') | |
| STRAVA_CLIENT_SECRET = os.getenv('STRAVA_CLIENT_SECRET') | |
| GEMINI_API_KEY = os.getenv('GEMINI_API_KEY') | |
| # Strava API setup | |
| client = Client() | |
| # Gemini API setup | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| model = genai.GenerativeModel('gemini-pro') | |
| # Streamlit app | |
| st.set_page_config(page_title="Strava Run Analysis", layout="wide") | |
| # Custom CSS for better UI | |
| st.markdown(""" | |
| <style> | |
| .stApp { | |
| background-color: #f0f2f6; | |
| } | |
| .stButton>button { | |
| background-color: #fc4c02; | |
| color: white; | |
| font-weight: bold; | |
| border-radius: 5px; | |
| border: none; | |
| padding: 0.5rem 1rem; | |
| } | |
| .stButton>button:hover { | |
| background-color: #e34402; | |
| } | |
| .stSelectbox { | |
| color: #333333; | |
| } | |
| .stPlotlyChart { | |
| background-color: white; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| padding: 1rem; | |
| margin-bottom: 2rem; | |
| } | |
| .stat-card { | |
| background-color: white; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| padding: 1.5rem; | |
| text-align: center; | |
| } | |
| .stat-card h3 { | |
| color: #fc4c02; | |
| font-size: 2.5rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| .stat-card p { | |
| color: #666; | |
| font-size: 1rem; | |
| margin: 0; | |
| } | |
| .section-header { | |
| color: #333; | |
| font-size: 1.8rem; | |
| font-weight: bold; | |
| margin-top: 2rem; | |
| margin-bottom: 1rem; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| st.title("πββοΈ Strava Run Analysis") | |
| # Strava manual authentication | |
| if 'access_token' not in st.session_state: | |
| st.write("To use this app, you need to authorize it with Strava. Follow these steps:") | |
| st.write("1. Click the button below to go to Strava's authorization page:") | |
| auth_url = f"https://www.strava.com/oauth/authorize?client_id={STRAVA_CLIENT_ID}&response_type=code&redirect_uri=http://localhost&approval_prompt=force&scope=read_all,profile:read_all,activity:read_all" | |
| st.markdown(f"<a href='{auth_url}' target='_blank'><button style='background-color: #fc4c02; color: white; padding: 0.5rem 1rem; border: none; border-radius: 5px; cursor: pointer;'>Authorize Strava</button></a>", unsafe_allow_html=True) | |
| st.write("2. Log in to Strava if needed and click 'Authorize'") | |
| st.write("3. After authorizing, you'll be redirected to a page that may show an error. This is expected!") | |
| st.write("4. Copy the 'code' parameter from the URL of that page.") | |
| st.write("5. Paste that code below:") | |
| auth_code = st.text_input("Paste the authorization code here:") | |
| if auth_code: | |
| try: | |
| token_response = client.exchange_code_for_token( | |
| client_id=STRAVA_CLIENT_ID, | |
| client_secret=STRAVA_CLIENT_SECRET, | |
| code=auth_code | |
| ) | |
| st.session_state.access_token = token_response['access_token'] | |
| st.success("Authorization successful! Refreshing the app...") | |
| st.rerun() | |
| except Exception as e: | |
| st.error(f"An error occurred: {str(e)}. Please try authorizing again.") | |
| if 'access_token' in st.session_state: | |
| client.access_token = st.session_state.access_token | |
| try: | |
| athlete = client.get_athlete() | |
| st.write(f"Welcome, {athlete.firstname} {athlete.lastname}! π") | |
| except Exception as e: | |
| st.error(f"Error fetching athlete data: {str(e)}. Please try reauthorizing.") | |
| if st.button("Reauthorize"): | |
| del st.session_state.access_token | |
| st.rerun() | |
| # Fetch running activities | |
| def fetch_run_activities(): | |
| activities = list(client.get_activities(limit=200)) | |
| runs = [] | |
| for activity in activities: | |
| if activity.type == 'Run': | |
| run_data = { | |
| 'name': activity.name, | |
| 'distance': float(activity.distance.num) / 1000 if activity.distance else None, # Convert to km | |
| 'moving_time': None, | |
| 'total_elevation_gain': float(activity.total_elevation_gain) if activity.total_elevation_gain else None, | |
| 'average_speed': float(activity.average_speed) * 3.6 if activity.average_speed else None, # Convert to km/h | |
| 'average_heartrate': float(activity.average_heartrate) if activity.average_heartrate else None, | |
| 'start_date': activity.start_date.replace(tzinfo=None) if activity.start_date else None | |
| } | |
| # Safely handle moving_time | |
| try: | |
| if activity.moving_time: | |
| run_data['moving_time'] = activity.moving_time.total_seconds() / 60 # Convert to minutes | |
| except AttributeError: | |
| pass # If moving_time is not accessible, leave it as None | |
| runs.append(run_data) | |
| df = pd.DataFrame(runs) | |
| # Calculate pace only if both moving_time and distance are available | |
| df['pace'] = df.apply(lambda row: row['moving_time'] / row['distance'] if row['moving_time'] and row['distance'] else None, axis=1) | |
| return df | |
| try: | |
| df = fetch_run_activities() | |
| except Exception as e: | |
| st.error(f"Error fetching activities: {str(e)}. Please try again later.") | |
| st.stop() | |
| # Basic stats | |
| st.markdown("<h2 class='section-header'>π Run Statistics</h2>", unsafe_allow_html=True) | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.markdown("<div class='stat-card'><h3>{:.0f}</h3><p>Total Runs</p></div>".format(len(df)), unsafe_allow_html=True) | |
| with col2: | |
| st.markdown("<div class='stat-card'><h3>{:.0f} km</h3><p>Total Distance</p></div>".format(df['distance'].sum()), unsafe_allow_html=True) | |
| with col3: | |
| total_time = df['moving_time'].sum() | |
| st.markdown("<div class='stat-card'><h3>{:.0f}h {:.0f}m</h3><p>Total Time</p></div>".format(total_time // 60, total_time % 60), unsafe_allow_html=True) | |
| with col4: | |
| st.markdown("<div class='stat-card'><h3>{:.0f} m</h3><p>Total Elevation Gain</p></div>".format(df['total_elevation_gain'].sum()), unsafe_allow_html=True) | |
| # Visualizations | |
| st.markdown("<h2 class='section-header'>π Run Analysis</h2>", unsafe_allow_html=True) | |
| # Weekly distance | |
| weekly_distance = df.resample('W', on='start_date')['distance'].sum().reset_index() | |
| fig_weekly = px.bar(weekly_distance, x='start_date', y='distance', | |
| title="Weekly Running Distance", | |
| labels={'distance': 'Distance (km)', 'start_date': 'Week'}) | |
| fig_weekly.update_layout(xaxis_title="Week", yaxis_title="Distance (km)") | |
| st.plotly_chart(fig_weekly, use_container_width=True) | |
| # Pace improvement | |
| df_sorted = df.sort_values('start_date') | |
| fig_pace = px.scatter(df_sorted, x='start_date', y='pace', | |
| title="Running Pace Over Time", | |
| labels={'pace': 'Pace (min/km)', 'start_date': 'Date'}) | |
| fig_pace.add_trace(go.Scatter(x=df_sorted['start_date'], y=df_sorted['pace'].rolling(window=10).mean(), | |
| mode='lines', name='10-run moving average')) | |
| fig_pace.update_layout(yaxis_title="Pace (min/km)") | |
| st.plotly_chart(fig_pace, use_container_width=True) | |
| # Heart rate improvement (if data available) | |
| if df['average_heartrate'].notna().any(): | |
| df_hr = df_sorted[df_sorted['average_heartrate'].notna()] | |
| fig_hr = px.scatter(df_hr, x='start_date', y='average_heartrate', | |
| title="Average Heart Rate Over Time", | |
| labels={'average_heartrate': 'Average Heart Rate (bpm)', 'start_date': 'Date'}) | |
| fig_hr.add_trace(go.Scatter(x=df_hr['start_date'], y=df_hr['average_heartrate'].rolling(window=10).mean(), | |
| mode='lines', name='10-run moving average')) | |
| fig_hr.update_layout(yaxis_title="Average Heart Rate (bpm)") | |
| st.plotly_chart(fig_hr, use_container_width=True) | |
| else: | |
| st.info("No heart rate data available for analysis.") | |
| # Distance vs. Elevation gain | |
| fig_elev = px.scatter(df, x='distance', y='total_elevation_gain', | |
| title="Distance vs. Elevation Gain", | |
| labels={'distance': 'Distance (km)', 'total_elevation_gain': 'Elevation Gain (m)'}) | |
| fig_elev.update_layout(xaxis_title="Distance (km)", yaxis_title="Elevation Gain (m)") | |
| st.plotly_chart(fig_elev, use_container_width=True) | |
| # Training Plan Generation | |
| st.markdown("<h2 class='section-header'>ποΈββοΈ Generate Training Plan</h2>", unsafe_allow_html=True) | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| level = st.selectbox("Select your level:", ["Beginner", "Intermediate", "Advanced"]) | |
| with col2: | |
| race_distance = st.selectbox("Select race distance:", ["5K", "10K", "Half Marathon", "Marathon"]) | |
| with col3: | |
| plan_duration = st.selectbox("Select plan duration:", ["8 weeks", "10 weeks", "12 weeks"]) | |
| if st.button("Generate Training Plan"): | |
| with st.spinner("Generating your personalized training plan..."): | |
| try: | |
| prompt = f"""Create a {plan_duration} training plan for a {level} runner preparing for a {race_distance} race. | |
| Include weekly mileage and key workouts. Format the plan week by week, with each week on a new line. | |
| Consider the runner's current weekly mileage of {weekly_distance['distance'].iloc[-1]:.1f} km.""" | |
| response = model.generate_content(prompt) | |
| st.markdown(response.text) | |
| except Exception as e: | |
| st.error(f"Error generating training plan: {str(e)}. Please try again later.") | |
| else: | |
| st.info("Please complete the authorization process to view your Strava running data and analytics.") |