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("") # --- Current week --- 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("") # --- Trends --- 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__()}") # --- Signals --- 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)