jflo commited on
Commit
ee311e5
Β·
verified Β·
1 Parent(s): 2ffe758

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +317 -0
  2. requirements.txt +7 -0
app.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Workout Session Data Collection
3
+ Gradio app for collecting real-world labeled workout data.
4
+ Submissions are written to a private Google Sheet in real time.
5
+ """
6
+
7
+ import gradio as gr
8
+ import gspread
9
+ import os
10
+ import json
11
+ from datetime import datetime
12
+ from google.oauth2.service_account import Credentials
13
+
14
+ # ─────────────────────────────────────────────────────────────
15
+ # GOOGLE SHEETS SETUP
16
+ # Credentials are loaded from the GOOGLE_CREDENTIALS env var
17
+ # (set as a HF Space Secret β€” paste the entire service account
18
+ # JSON as a single-line string)
19
+ # ─────────────────────────────────────────────────────────────
20
+
21
+ SCOPES = [
22
+ "https://www.googleapis.com/auth/spreadsheets",
23
+ "https://www.googleapis.com/auth/drive",
24
+ ]
25
+
26
+ SHEET_NAME = os.getenv("SHEET_NAME", "Workout Training Data")
27
+
28
+ # Expected header row β€” must match the sheet's first row exactly
29
+ HEADERS = [
30
+ "timestamp",
31
+ "user_text",
32
+ "mood",
33
+ "exertion",
34
+ "soreness_region",
35
+ "soreness_severity",
36
+ "completion_status",
37
+ ]
38
+
39
+
40
+ def get_sheet():
41
+ """
42
+ Authenticate with Google Sheets using the service account
43
+ credentials stored in the GOOGLE_CREDENTIALS env var.
44
+ Returns the first worksheet of the target spreadsheet.
45
+ """
46
+ creds_json = os.getenv("GOOGLE_CREDENTIALS")
47
+ if not creds_json:
48
+ raise EnvironmentError(
49
+ "GOOGLE_CREDENTIALS env var not set. "
50
+ "Add your service account JSON as a HF Space Secret."
51
+ )
52
+
53
+ creds_dict = json.loads(creds_json)
54
+ creds = Credentials.from_service_account_info(creds_dict, scopes=SCOPES)
55
+ client = gspread.authorize(creds)
56
+ sheet = client.open(SHEET_NAME).sheet1
57
+
58
+ # Write headers if the sheet is empty
59
+ if sheet.row_count == 0 or sheet.row_values(1) != HEADERS:
60
+ sheet.insert_row(HEADERS, index=1)
61
+
62
+ return sheet
63
+
64
+
65
+ # ─────────────────────────────────────────────────────────────
66
+ # LABEL OPTIONS
67
+ # ─────────────────────────────────────────────────────────────
68
+
69
+ MOOD_OPTIONS = [
70
+ "accomplished", "anxious", "distracted", "energized",
71
+ "fatigued", "frustrated", "neutral", "positive",
72
+ ]
73
+
74
+ EXERTION_OPTIONS = ["low", "moderate", "high"]
75
+
76
+ SORENESS_REGION_OPTIONS = [
77
+ "none", "back", "biceps", "chest",
78
+ "legs", "shoulder", "triceps",
79
+ ]
80
+
81
+ SORENESS_SEVERITY_OPTIONS = ["none", "mild", "moderate", "severe"]
82
+
83
+ COMPLETION_OPTIONS = ["full", "partial"]
84
+
85
+
86
+ # ─────────────────────────────────────────────────────────────
87
+ # SUBMISSION HANDLER
88
+ # ─────────────────────────────────────────────────────────────
89
+
90
+ def submit_entry(
91
+ user_text: str,
92
+ mood: str,
93
+ exertion: str,
94
+ soreness_region: str,
95
+ soreness_severity: str,
96
+ completion_status: str,
97
+ ) -> str:
98
+ """
99
+ Validates the form and appends one row to the Google Sheet.
100
+ Returns a status string displayed to the user.
101
+ """
102
+
103
+ # ── Validation ───────────────────────────────────────────
104
+ if not user_text or len(user_text.strip()) < 10:
105
+ return "⚠️ Please describe your session in at least 10 characters."
106
+
107
+ missing = []
108
+ if not mood: missing.append("Mood")
109
+ if not exertion: missing.append("Exertion")
110
+ if not soreness_region: missing.append("Soreness Region")
111
+ if not soreness_severity: missing.append("Soreness Severity")
112
+ if not completion_status: missing.append("Completion")
113
+
114
+ if missing:
115
+ return f"⚠️ Please select: {', '.join(missing)}"
116
+
117
+ # Severity/region consistency check
118
+ if soreness_region == "none" and soreness_severity != "none":
119
+ return "⚠️ Soreness severity must be 'none' when region is 'none'."
120
+ if soreness_region != "none" and soreness_severity == "none":
121
+ return "⚠️ Please select a severity level for your soreness region."
122
+
123
+ # ── Build row ────────────────────────────────────────────
124
+ row = [
125
+ datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
126
+ user_text.strip(),
127
+ mood,
128
+ exertion,
129
+ soreness_region,
130
+ soreness_severity,
131
+ completion_status,
132
+ ]
133
+
134
+ # ── Write to Google Sheets ───────────────────────────────
135
+ try:
136
+ sheet = get_sheet()
137
+ sheet.append_row(row, value_input_option="USER_ENTERED")
138
+ except EnvironmentError as e:
139
+ return f"❌ Configuration error: {str(e)}"
140
+ except Exception as e:
141
+ return f"❌ Failed to save: {str(e)}"
142
+
143
+ return (
144
+ "βœ… Submitted! Thank you β€” your session has been logged.\n\n"
145
+ "Feel free to submit another entry."
146
+ )
147
+
148
+
149
+ # ─────────────────────────────────────────────────────────────
150
+ # GRADIO UI
151
+ # ─────────────────────────────────────────────────────────────
152
+
153
+ with gr.Blocks(
154
+ title="Workout Session Logger",
155
+ theme=gr.themes.Base(
156
+ primary_hue="orange",
157
+ neutral_hue="slate",
158
+ font=gr.themes.GoogleFont("Inter"),
159
+ ),
160
+ css="""
161
+ .submit-btn { background: #FF4500 !important; border: none !important; }
162
+ .submit-btn:hover { background: #CC3700 !important; }
163
+ footer { display: none !important; }
164
+ """,
165
+ ) as demo:
166
+
167
+ # ── Header ────────────────────────────────────────────────
168
+ gr.Markdown(
169
+ """
170
+ # πŸ‹οΈ Workout Session Logger
171
+ ### Help train a smarter fitness AI
172
+ Describe your session in your own words, then label it using the
173
+ dropdowns below. Your data helps build a model that understands
174
+ how athletes really feel after training.
175
+
176
+ **All submissions are anonymous. Be as honest as possible β€”
177
+ bad sessions are just as valuable as great ones.**
178
+ """
179
+ )
180
+
181
+ gr.Markdown("---")
182
+
183
+ # ── Free text input ───────────────────────────────────────
184
+ gr.Markdown("### 1. Describe your session")
185
+ gr.Markdown(
186
+ "_Write naturally β€” exactly how you'd tell a training partner. "
187
+ "Include how you felt, what you trained, any soreness or energy levels._"
188
+ )
189
+
190
+ user_text = gr.Textbox(
191
+ label="Session description",
192
+ placeholder=(
193
+ "e.g. Hit a new PR on deadlifts today, feeling absolutely "
194
+ "wrecked but stoked. Lower back is pretty sore and I only "
195
+ "got through 4 of 5 sets before calling it..."
196
+ ),
197
+ lines=5,
198
+ max_lines=10,
199
+ )
200
+
201
+ gr.Markdown("---")
202
+
203
+ # ── Labels ────────────────────────────────────────────────
204
+ gr.Markdown("### 2. Label your session")
205
+ gr.Markdown(
206
+ "_Select the option that best matches how you felt **after** the session._"
207
+ )
208
+
209
+ with gr.Row():
210
+ mood = gr.Dropdown(
211
+ choices=MOOD_OPTIONS,
212
+ label="Mood",
213
+ info="How did you feel after finishing?",
214
+ )
215
+ exertion = gr.Dropdown(
216
+ choices=EXERTION_OPTIONS,
217
+ label="Exertion level",
218
+ info="How hard did you push overall?",
219
+ )
220
+
221
+ with gr.Row():
222
+ soreness_region = gr.Dropdown(
223
+ choices=SORENESS_REGION_OPTIONS,
224
+ label="Soreness region",
225
+ info="Which muscle group is most sore? Select 'none' if no soreness.",
226
+ )
227
+ soreness_severity = gr.Dropdown(
228
+ choices=SORENESS_SEVERITY_OPTIONS,
229
+ label="Soreness severity",
230
+ info="How intense is the soreness? Select 'none' if no soreness.",
231
+ )
232
+
233
+ completion_status = gr.Dropdown(
234
+ choices=COMPLETION_OPTIONS,
235
+ label="Completion",
236
+ info="Did you finish the full planned session?",
237
+ )
238
+
239
+ gr.Markdown("---")
240
+
241
+ # ── Submit ────────────────────────────────────────────────
242
+ submit_btn = gr.Button(
243
+ "Submit Session",
244
+ variant="primary",
245
+ elem_classes=["submit-btn"],
246
+ size="lg",
247
+ )
248
+
249
+ status_output = gr.Textbox(
250
+ label="Status",
251
+ interactive=False,
252
+ show_label=False,
253
+ )
254
+
255
+ submit_btn.click(
256
+ fn=submit_entry,
257
+ inputs=[
258
+ user_text,
259
+ mood,
260
+ exertion,
261
+ soreness_region,
262
+ soreness_severity,
263
+ completion_status,
264
+ ],
265
+ outputs=status_output,
266
+ )
267
+
268
+ # ── Label reference ───────────────────────────────────────
269
+ with gr.Accordion("πŸ“– Label reference guide", open=False):
270
+ gr.Markdown(
271
+ """
272
+ ### Mood
273
+ | Label | When to use |
274
+ |---|---|
275
+ | **accomplished** | Hit a goal, PR, or felt proud of the effort |
276
+ | **energized** | Left the gym feeling charged and strong |
277
+ | **positive** | Good session, happy with the work done |
278
+ | **neutral** | Average session, nothing special either way |
279
+ | **fatigued** | Drained, low energy, struggled through it |
280
+ | **frustrated** | Bad session, missed lifts, things went wrong |
281
+ | **anxious** | Felt uneasy, worried about injury or performance |
282
+ | **distracted** | Couldn't focus, mind elsewhere, scattered session |
283
+
284
+ ### Exertion
285
+ | Label | When to use |
286
+ |---|---|
287
+ | **low** | Light session, easy pace, well within limits |
288
+ | **moderate** | Solid effort, challenging but manageable |
289
+ | **high** | Pushed to near-limit, very demanding session |
290
+
291
+ ### Soreness Region
292
+ Select the **primary** muscle group that is sore or was most targeted.
293
+ Choose **none** if you have no notable soreness.
294
+
295
+ ### Soreness Severity
296
+ | Label | When to use |
297
+ |---|---|
298
+ | **none** | No soreness at all |
299
+ | **mild** | Slightly tight or tender, barely noticeable |
300
+ | **moderate** | Noticeably sore, aware of it during movement |
301
+ | **severe** | Very sore, impacts range of motion or daily activity |
302
+
303
+ ### Completion
304
+ | Label | When to use |
305
+ |---|---|
306
+ | **full** | Completed all planned sets, exercises, and volume |
307
+ | **partial** | Skipped exercises, cut sets short, or left early |
308
+ """
309
+ )
310
+
311
+
312
+ # ─────────────────────────────────────────────────────────────
313
+ # LAUNCH
314
+ # ─────────────────────────────────────────────────────────────
315
+
316
+ if __name__ == "__main__":
317
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.30.6
3
+ pydantic==2.8.2
4
+ torch==2.4.0
5
+ transformers==4.44.2
6
+ anthropic==0.34.2
7
+ python-multipart==0.0.9