| from rate_my_run.models import TrainingSummary |
|
|
| def format_pace(pace_min_per_km: float | None) -> str: |
| """5.02 -> '5:01/km'. Returns 'n/a' for missing pace.""" |
| if pace_min_per_km is None: |
| return "n/a" |
| minutes = int(pace_min_per_km) |
| seconds = round((pace_min_per_km - minutes) * 60) |
| if seconds == 60: |
| minutes, seconds = minutes + 1, 0 |
| return f"{minutes}:{seconds:02d}/km" |
|
|
| SIGNAL_DESCRIPTIONS = { |
| "mileage_spike": "Weekly mileage rose more than 10% versus last week.", |
| "potential_fatigue": "Mileage increased while pace slowed — a possible sign of fatigue or under-recovery.", |
| "returning_after_break": "The athlete is returning to running after a notable break.", |
| "increasing_consistency": "The number of runs per week is increasing.", |
| "acwr_danger_zone": "Training load (ACWR) is in the danger zone — weekly mileage is spiking too fast versus the 4-week average, raising overuse-injury risk.", |
| "acwr_undertraining": "Training load (ACWR) is low — recent volume is below the 4-week average, so fitness may be stagnating.", |
| } |
|
|
|
|
| def acwr_band(acwr: float) -> str: |
| """Plain-English band for an ACWR value (thresholds mirror analytics.ACWR_*).""" |
| if acwr < 0.80: |
| return "under-training (fitness may stagnate)" |
| if acwr <= 1.30: |
| return "the sweet spot (safe, progressive load)" |
| if acwr <= 1.50: |
| return "elevated (monitor recovery)" |
| return "danger zone (mileage spiking too fast; high injury risk)" |
|
|
| def summary_to_text(summary: TrainingSummary, goal: str | None = None, name: str|None = None) -> str: |
| """Render a TrainingSummary into neutral English for the coaching model.""" |
| lines: list[str] = [] |
| this_week = summary.weeks[-1] |
|
|
| if goal: |
| lines.append(f"Athlete's stated goal: {goal}") |
| lines.append("") |
|
|
| if name: |
| lines.append(f"Athlete's name: {name}") |
| lines.append("") |
|
|
| |
| lines.append("CURRENT WEEK") |
| lines.append(f"- Runs: {this_week.num_runs}") |
| lines.append(f"- Distance: {this_week.distance_km:.1f} km") |
| lines.append(f"- Longest run: {this_week.longest_run_km:.1f} km") |
| if this_week.avg_pace_min_per_km is not None: |
| lines.append(f"- Average pace: {format_pace(this_week.avg_pace_min_per_km)}") |
| else: |
| lines.append("- Average pace: no runs this week") |
| lines.append("") |
|
|
| |
| lines.append("TRENDS") |
| if summary.mileage_change_pct is None: |
| lines.append("- Weekly mileage: no comparison (no runs last week).") |
| else: |
| direction = "up" if summary.mileage_change_pct >= 0 else "down" |
| lines.append(f"- Weekly mileage {direction} {abs(summary.mileage_change_pct):.0f}% vs last week.") |
|
|
|
|
| if summary.pace_trend == 'insufficient_data': |
| lines.append("- Weekly pace: insufficient data for pace trend analysis.") |
| elif summary.pace_trend == 'improving': |
| lines.append("- Weekly pace: weekly pace this week has improved compared to the recent baseline.") |
| elif summary.pace_trend == 'plateauing': |
| lines.append("- Weekly pace: weekly pace is plateauing as it has not changed significantly compared to the recent baseline.") |
| elif summary.pace_trend == 'declining': |
| lines.append("- Weekly pace: weekly pace this week has declined compared to the recent baseline.") |
|
|
| if summary.acwr is not None: |
| lines.append(f"- Training load (ACWR): {summary.acwr:.2f} — {acwr_band(summary.acwr)}.") |
|
|
| lines.append(f"- Days since last run: {summary.days_since_last_run}") |
|
|
| for i, week in enumerate(summary.weeks[-5:-1][::-1]): |
| lines.append(f"- {i+1} Weeks ago -> {week.__str__()}") |
|
|
| |
| if summary.signals: |
| lines.append("") |
| lines.append("SIGNALS") |
| for s in summary.signals: |
| lines.append(f"- {SIGNAL_DESCRIPTIONS.get(s, s)}") |
|
|
| return "\n".join(lines) |