marcosavoia commited on
Commit
2df4d3e
·
verified ·
1 Parent(s): 3ad3367

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +190 -0
app.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+
3
+ import gradio as gr
4
+ import matplotlib
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+
8
+ from openai import AsyncOpenAI
9
+
10
+ from ai_tennis_coach import (
11
+ classify_shots,
12
+ extract_all_shots,
13
+ create_client,
14
+ analyze_all_shots,
15
+ calculate_mean_by_shot_type,
16
+ config,
17
+ )
18
+
19
+ matplotlib.use("Agg")
20
+
21
+ MODEL_PATH = "models/tennis_rnn.h5"
22
+
23
+ METRIC_LABELS = {
24
+ "preparation": "Preparation",
25
+ "contact_point": "Contact",
26
+ "swing_followthrough": "Follow-through",
27
+ "balance_stance": "Balance",
28
+ }
29
+
30
+
31
+ def make_stats_plot(mean_scores: dict) -> plt.Figure:
32
+ labels = list(METRIC_LABELS.values())
33
+ n = len(labels)
34
+ angles = np.linspace(0, 2 * np.pi, n, endpoint=False).tolist()
35
+ angles += angles[:1] # close the polygon
36
+
37
+ fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True))
38
+ colors = ["#4C72B0", "#DD8452", "#55A868"]
39
+
40
+ for i, (shot_type, metrics_dict) in enumerate(mean_scores.items()):
41
+ values = [metrics_dict.get(m, 0) for m in METRIC_LABELS]
42
+ values += values[:1]
43
+ color = colors[i % len(colors)]
44
+ ax.plot(angles, values, "o-", linewidth=2, color=color, label=shot_type.capitalize())
45
+ ax.fill(angles, values, alpha=0.2, color=color)
46
+
47
+ ax.set_xticks(angles[:-1])
48
+ ax.set_xticklabels(labels, size=10)
49
+ ax.set_ylim(0, 10)
50
+ ax.set_yticks([2, 4, 6, 8, 10])
51
+ ax.set_yticklabels(["2", "4", "6", "8", "10"], size=8)
52
+ ax.set_title("Stroke Mechanics", pad=20)
53
+ ax.legend(loc="upper right", bbox_to_anchor=(1.35, 1.15))
54
+
55
+ plt.tight_layout()
56
+ return fig
57
+
58
+
59
+ def format_summary(results: list, analyses: list, mean_scores: dict) -> str:
60
+ lines = [f"Detected {len(results)} shot(s).\n"]
61
+
62
+ for shot_list in analyses:
63
+ for s in shot_list:
64
+ lines.append(
65
+ f"{s.shot_type.capitalize()} stroke\n"
66
+ f" Preparation: {s.preparation.score:.1f}/10 — {s.preparation.notes}\n"
67
+ f" Contact point: {s.contact_point.score:.1f}/10 — {s.contact_point.notes}\n"
68
+ f" Follow-through: {s.swing_followthrough.score:.1f}/10 — {s.swing_followthrough.notes}\n"
69
+ f" Balance: {s.balance_stance.score:.1f}/10 — {s.balance_stance.notes}\n"
70
+ f" Coach notes: {s.coach_notes}\n"
71
+ )
72
+
73
+ lines.append("--- Mean scores by shot type ---")
74
+ for shot_type, metrics in mean_scores.items():
75
+ lines.append(f"{shot_type.capitalize()}:")
76
+ for metric, value in metrics.items():
77
+ lines.append(f" {metric.replace('_', ' ').capitalize()}: {value:.1f}/10")
78
+
79
+ return "\n".join(lines)
80
+
81
+
82
+ async def run_analysis(video_path: str, left_handed: bool = False):
83
+ if video_path is None:
84
+ raise gr.Error("Please upload a video first.")
85
+
86
+ loop = asyncio.get_event_loop()
87
+ results = await loop.run_in_executor(
88
+ None, lambda: classify_shots(video_path, MODEL_PATH, left_handed=left_handed)
89
+ )
90
+
91
+ if not results:
92
+ raise gr.Error("No shots detected in the video.")
93
+
94
+ extract_all_shots(video_path, results, output_dir=config.clips_dir)
95
+
96
+ client = create_client()
97
+ analyses = await analyze_all_shots(results[:5], client)
98
+
99
+ mean_scores = calculate_mean_by_shot_type(analyses)
100
+ fig = make_stats_plot(mean_scores)
101
+ summary = format_summary(results, analyses, mean_scores)
102
+
103
+ return fig, summary
104
+
105
+
106
+ async def chat_fn(message: str, history: list, analysis_summary: str):
107
+ context = analysis_summary or "No analysis has been run yet."
108
+ system = (
109
+ "You are an expert tennis coach providing personalised advice, training recommendations, "
110
+ "and improvement suggestions. Use the player's stroke analysis below to guide your responses "
111
+ "— highlight strengths, flag weaknesses, and offer broad coaching guidance.\n\n"
112
+ f"Player stroke analysis:\n{context}"
113
+ )
114
+
115
+ # Gradio 4+ passes history as list[{"role": ..., "content": ...}]
116
+ api_messages = [{"role": "system", "content": system}]
117
+ for msg in history:
118
+ api_messages.append({"role": msg["role"], "content": msg["content"]})
119
+ api_messages.append({"role": "user", "content": message})
120
+
121
+ chat_client = AsyncOpenAI(base_url=config.api_base_url, api_key=config.hf_token)
122
+ stream = await chat_client.chat.completions.create(
123
+ model=config.model_name,
124
+ messages=api_messages,
125
+ stream=True,
126
+ max_tokens=1024,
127
+ temperature=0.7,
128
+ )
129
+
130
+ # Yield the full updated history so gr.Chatbot renders correctly
131
+ updated_history = list(history) + [{"role": "user", "content": message}]
132
+ reply = ""
133
+ async for chunk in stream:
134
+ delta = chunk.choices[0].delta.content or ""
135
+ reply += delta
136
+ yield updated_history + [{"role": "assistant", "content": reply}]
137
+
138
+
139
+ with gr.Blocks(title="AI Tennis Coach") as demo:
140
+ gr.Markdown("# AI Tennis Coach — Stroke Analysis")
141
+
142
+ analysis_state = gr.State("")
143
+
144
+ with gr.Row():
145
+ with gr.Column(scale=1):
146
+ video_input = gr.Video(
147
+ label="Upload Tennis Video",
148
+ sources=["upload"],
149
+ format="mp4",
150
+ height=320,
151
+ )
152
+ left_handed = gr.Checkbox(label="Front facing", value=False)
153
+ analyse_btn = gr.Button("Analyse", variant="primary")
154
+ stats_plot = gr.Plot(label="Stroke Mechanics Summary")
155
+
156
+ with gr.Column(scale=1):
157
+ chatbot = gr.Chatbot(
158
+ label="Coach Chat",
159
+ height=500,
160
+ placeholder="Run the analysis first, then ask your coach anything.",
161
+ )
162
+ chat_input = gr.Textbox(
163
+ placeholder="Ask your coach...",
164
+ label="Your message",
165
+ show_label=False,
166
+ )
167
+ with gr.Row():
168
+ send_btn = gr.Button("Send", variant="primary")
169
+ gr.ClearButton([chat_input, chatbot], value="Clear chat")
170
+
171
+ analyse_btn.click(
172
+ fn=run_analysis,
173
+ inputs=[video_input, left_handed],
174
+ outputs=[stats_plot, analysis_state],
175
+ )
176
+
177
+ send_btn.click(
178
+ fn=chat_fn,
179
+ inputs=[chat_input, chatbot, analysis_state],
180
+ outputs=chatbot,
181
+ )
182
+ chat_input.submit(
183
+ fn=chat_fn,
184
+ inputs=[chat_input, chatbot, analysis_state],
185
+ outputs=chatbot,
186
+ )
187
+
188
+
189
+ if __name__ == "__main__":
190
+ demo.launch()