Spaces:
Sleeping
Sleeping
Update model_api.py
Browse files- model_api.py +162 -22
model_api.py
CHANGED
|
@@ -1,22 +1,162 @@
|
|
| 1 |
-
import os
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, time
|
| 2 |
+
|
| 3 |
+
def query_model(prompt, max_tokens=1500):
|
| 4 |
+
"""Single Groq API call."""
|
| 5 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "").strip()
|
| 6 |
+
|
| 7 |
+
if not GROQ_API_KEY:
|
| 8 |
+
raise ValueError(
|
| 9 |
+
"GROQ_API_KEY is not set.\n\n"
|
| 10 |
+
"Steps to fix:\n"
|
| 11 |
+
"1. Go to https://console.groq.com β sign up free\n"
|
| 12 |
+
"2. Click 'API Keys' β 'Create API Key'\n"
|
| 13 |
+
"3. Copy the key (starts with gsk_...)\n"
|
| 14 |
+
"4. In your HuggingFace Space β Settings β Variables and Secrets\n"
|
| 15 |
+
" β Add secret: Name = GROQ_API_KEY, Value = gsk_xxxxxxxxxxxx\n"
|
| 16 |
+
"5. Restart the Space"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from groq import Groq
|
| 21 |
+
client = Groq(api_key=GROQ_API_KEY)
|
| 22 |
+
|
| 23 |
+
response = client.chat.completions.create(
|
| 24 |
+
model="llama-3.3-70b-versatile", # fastest + best quality on Groq
|
| 25 |
+
messages=[
|
| 26 |
+
{
|
| 27 |
+
"role": "system",
|
| 28 |
+
"content": (
|
| 29 |
+
"You are a certified professional fitness trainer. "
|
| 30 |
+
"Provide detailed, structured, day-by-day workout plans exactly as instructed."
|
| 31 |
+
)
|
| 32 |
+
},
|
| 33 |
+
{"role": "user", "content": prompt}
|
| 34 |
+
],
|
| 35 |
+
max_tokens=max_tokens,
|
| 36 |
+
temperature=0.7,
|
| 37 |
+
)
|
| 38 |
+
return response.choices[0].message.content
|
| 39 |
+
|
| 40 |
+
except Exception as e:
|
| 41 |
+
err = str(e)
|
| 42 |
+
|
| 43 |
+
if "401" in err or "invalid_api_key" in err.lower() or "authentication" in err.lower():
|
| 44 |
+
raise ValueError(
|
| 45 |
+
"β Groq API key is invalid or expired.\n\n"
|
| 46 |
+
"Fix: Go to https://console.groq.com/keys β create a new key β "
|
| 47 |
+
"update GROQ_API_KEY in your HuggingFace Space Secrets."
|
| 48 |
+
) from None
|
| 49 |
+
|
| 50 |
+
if "429" in err or "rate_limit" in err.lower() or "rate limit" in err.lower():
|
| 51 |
+
raise ValueError(
|
| 52 |
+
"β³ Groq rate limit hit. Wait 60 seconds and try again.\n\n"
|
| 53 |
+
"Free tier: 6,000 tokens/min. Upgrade at console.groq.com for higher limits."
|
| 54 |
+
) from None
|
| 55 |
+
|
| 56 |
+
if "503" in err or "504" in err or "timeout" in err.lower():
|
| 57 |
+
raise ValueError(
|
| 58 |
+
"β‘ Groq server busy. Please try again in a few seconds."
|
| 59 |
+
) from None
|
| 60 |
+
|
| 61 |
+
raise
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def query_model_chunked(name, gender, height, weight, goal, fitness_level,
|
| 65 |
+
equipment, days_per_week=5, months=1,
|
| 66 |
+
progress_callback=None):
|
| 67 |
+
"""
|
| 68 |
+
Generate a long plan by splitting into chunks of 3 days each.
|
| 69 |
+
Groq is fast enough (~2-3s per chunk) so even 30-day plans finish in ~30s.
|
| 70 |
+
progress_callback(chunk_num, total_chunks, days_done, total_days) β optional.
|
| 71 |
+
Returns: (full_plan_text, bmi, bmi_cat)
|
| 72 |
+
"""
|
| 73 |
+
from prompt_builder import calculate_bmi, bmi_category, bmi_advice
|
| 74 |
+
|
| 75 |
+
bmi = calculate_bmi(weight, height)
|
| 76 |
+
bmi_cat = bmi_category(bmi)
|
| 77 |
+
advice = bmi_advice(bmi_cat)
|
| 78 |
+
|
| 79 |
+
total_days = min(days_per_week * 4 * months, 30)
|
| 80 |
+
eq_list = ", ".join(equipment) if equipment else "Bodyweight only"
|
| 81 |
+
|
| 82 |
+
intensity_map = {
|
| 83 |
+
"Beginner": "2β3 sets, moderate weight, 90s rest. Prioritise form.",
|
| 84 |
+
"Intermediate": "3β4 sets, progressive overload, 60β75s rest.",
|
| 85 |
+
"Advanced": "4β5 sets, heavy compounds, 45β60s rest, supersets.",
|
| 86 |
+
}
|
| 87 |
+
intensity = intensity_map.get(fitness_level, "3 sets, 60s rest.")
|
| 88 |
+
|
| 89 |
+
CHUNK_SIZE = 3 # 3 days per call β well within Groq token limits
|
| 90 |
+
chunks = []
|
| 91 |
+
start_day = 1
|
| 92 |
+
total_chunks = max(1, (total_days + CHUNK_SIZE - 1) // CHUNK_SIZE)
|
| 93 |
+
|
| 94 |
+
while start_day <= total_days:
|
| 95 |
+
end_day = min(start_day + CHUNK_SIZE - 1, total_days)
|
| 96 |
+
chunk_num = len(chunks) + 1
|
| 97 |
+
|
| 98 |
+
if progress_callback:
|
| 99 |
+
progress_callback(chunk_num, total_chunks, start_day - 1, total_days)
|
| 100 |
+
|
| 101 |
+
context_note = ""
|
| 102 |
+
if chunks:
|
| 103 |
+
context_note = (
|
| 104 |
+
f"(Continue from Day {start_day}. Do NOT repeat Days 1β{start_day-1}. "
|
| 105 |
+
f"Vary muscle groups from previous days.)"
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
prompt = f"""Fitness trainer. Generate ONLY Days {start_day}-{end_day} of a {total_days}-day plan.
|
| 109 |
+
Client: {name}, {gender}, {height}cm/{weight}kg, BMI {bmi:.1f} ({bmi_cat}), Goal: {goal}, Level: {fitness_level}, Equipment: {eq_list}.
|
| 110 |
+
{context_note}
|
| 111 |
+
|
| 112 |
+
For EACH day use EXACTLY this format:
|
| 113 |
+
## Day N - [Muscle Group]
|
| 114 |
+
**Warm-Up** - Exercise: 2x10, Exercise: 2x10
|
| 115 |
+
**Main Workout**
|
| 116 |
+
- ExerciseName β 3x12 reps (rest 60s)
|
| 117 |
+
- ExerciseName β 3x12 reps (rest 60s)
|
| 118 |
+
- ExerciseName β 3x12 reps (rest 60s)
|
| 119 |
+
- ExerciseName β 3x12 reps (rest 60s)
|
| 120 |
+
- ExerciseName β 3x12 reps (rest 60s)
|
| 121 |
+
**Cool-Down** - Stretch1, Stretch2
|
| 122 |
+
|
| 123 |
+
Rules: vary muscle groups each day, {fitness_level} appropriate, use {eq_list} only, intensity: {intensity}.
|
| 124 |
+
{"End with 1 motivational sentence for "+name+"." if end_day == total_days else "No motivational text β more days follow."}
|
| 125 |
+
Output ONLY Days {start_day}-{end_day}. No preamble. No extra text.
|
| 126 |
+
"""
|
| 127 |
+
|
| 128 |
+
# Retry up to 3 times on transient errors
|
| 129 |
+
for attempt in range(3):
|
| 130 |
+
try:
|
| 131 |
+
chunk_text = query_model(prompt, max_tokens=1500)
|
| 132 |
+
break
|
| 133 |
+
except ValueError as e:
|
| 134 |
+
# Rate limit β wait and retry
|
| 135 |
+
err = str(e)
|
| 136 |
+
if "rate limit" in err.lower() and attempt < 2:
|
| 137 |
+
wait = 65 # wait just over 1 minute for rate limit window to reset
|
| 138 |
+
if progress_callback:
|
| 139 |
+
progress_callback(chunk_num, total_chunks, start_day - 1, total_days,
|
| 140 |
+
status=f"Rate limit β waiting {wait}sβ¦")
|
| 141 |
+
time.sleep(wait)
|
| 142 |
+
continue
|
| 143 |
+
raise
|
| 144 |
+
except Exception as e:
|
| 145 |
+
err = str(e)
|
| 146 |
+
if attempt < 2:
|
| 147 |
+
time.sleep(5 * (attempt + 1))
|
| 148 |
+
continue
|
| 149 |
+
raise
|
| 150 |
+
|
| 151 |
+
chunks.append(chunk_text.strip())
|
| 152 |
+
start_day = end_day + 1
|
| 153 |
+
|
| 154 |
+
# Small pause between chunks to respect rate limits on free tier
|
| 155 |
+
if start_day <= total_days:
|
| 156 |
+
time.sleep(1)
|
| 157 |
+
|
| 158 |
+
if progress_callback:
|
| 159 |
+
progress_callback(total_chunks, total_chunks, total_days, total_days)
|
| 160 |
+
|
| 161 |
+
full_plan = "\n\n".join(chunks)
|
| 162 |
+
return full_plan, bmi, bmi_cat
|