File size: 14,941 Bytes
0f02a72 4c1fbe0 d52747d 7eeafa4 58a9162 e2f0f0e 7eeafa4 7dcdf66 d52747d 7eeafa4 fe59eaf 4c1fbe0 b3c62a5 fd6c129 58a9162 4c1fbe0 58a9162 7eeafa4 e2f0f0e 7eeafa4 6367dee d52747d c6be2bd d52747d 6367dee c6be2bd 87fa1e2 4c1fbe0 499ead8 4c1fbe0 618b692 4c1fbe0 6367dee 7eeafa4 87fa1e2 c6be2bd 7eeafa4 d52747d 7eeafa4 d52747d 7eeafa4 d52747d b3c62a5 d52747d c6be2bd d52747d 7eeafa4 d52747d 58a9162 d52747d c6be2bd 7eeafa4 87fa1e2 d52747d c6be2bd d52747d 4c1fbe0 d52747d 4c1fbe0 d52747d 4c1fbe0 d52747d 4c1fbe0 c6be2bd 4c1fbe0 6367dee d52747d 4c1fbe0 bfa49d6 4c1fbe0 d52747d c6be2bd 4c1fbe0 6367dee 4c1fbe0 d52747d 4c1fbe0 d52747d bfa49d6 c6be2bd d52747d bfa49d6 c6be2bd d52747d bfa49d6 c6be2bd 6367dee c6be2bd 7eeafa4 4c1fbe0 d52747d 4c1fbe0 d52747d 4c1fbe0 d52747d 4c1fbe0 d52747d 4c1fbe0 cd3d0b1 4c1fbe0 d52747d cd3d0b1 1b39435 7dcdf66 499ead8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | import gradio as gr
import os
import requests
import json
import base64
import re
from PIL import Image, ImageOps
from io import BytesIO
GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions"
SPACE_GROQ_KEY = os.environ.get("GROQ_API_KEY", "")
TEXT_MODEL = "llama-3.3-70b-versatile"
VISION_MODEL = "meta-llama/llama-4-scout-17b-16e-instruct"
SYSTEM_PROMPT = """You are a warm, patient math tutor for students from Grade 1 through High School.
Rules you ALWAYS follow:
1. Never give just the answer. Guide the student step by step.
2. Before each step, explain WHAT you are doing and WHY in simple language.
3. Match your language to the grade level given.
4. Use proper math notation (fractions, exponents, equations).
5. End with a clearly marked Final Answer and one sentence of encouragement.
6. For any calculation, write out every arithmetic operation explicitly before evaluating it. Never skip steps or compute mentally.
7. For trigonometry, always state the formula, substitute values, then compute. Show sin/cos/tan values to 4 decimal places.
8. Never use LaTeX notation. Use plain text only. Write fractions as a/b, exponents as x^2, and put each equation on its own line.
Always format your response exactly like this:
π **Problem:** [restate the problem clearly]
---
**Step 1 β [Short title]**
*Why:* [reason in one sentence]
[Show the calculation or concept]
**Step 2 β [Short title]**
*Why:* [reason in one sentence]
[Show the calculation or concept]
(continue for all steps needed)
---
β
**Final Answer:** [clear result]
π‘ [One encouraging sentence]"""
def strip_latex(text):
text = re.sub(r'\$\$(.+?)\$\$', r'\1', text, flags=re.DOTALL)
text = re.sub(r'\$(.+?)\$', r'\1', text)
text = re.sub(r'\\frac\{(.+?)\}\{(.+?)\}', r'\1/\2', text)
text = re.sub(r'\\text\{(.+?)\}', r'\1', text)
text = text.replace(r'\cos', 'cos')
text = text.replace(r'\sin', 'sin')
text = text.replace(r'\tan', 'tan')
text = text.replace(r'\times', 'Γ')
text = text.replace(r'\div', 'Γ·')
text = text.replace(r'\approx', 'β')
text = text.replace(r'\cdot', 'Β·')
text = text.replace(r'\left', '').replace(r'\right', '')
text = text.replace(r'\sqrt', 'sqrt')
text = text.replace(r'\pi', 'Ο')
text = re.sub(r'\{|\}', '', text)
return text
def image_to_base64(pil_image):
# Flip horizontally to correct webcam mirror effect
pil_image = ImageOps.mirror(pil_image)
buffered = BytesIO()
pil_image.save(buffered, format="PNG")
return base64.b64encode(buffered.getvalue()).decode("utf-8")
def solve_math(image_input, text_input, grade_level, user_key):
key = user_key.strip() if user_key and user_key.strip() else SPACE_GROQ_KEY
if not key:
yield (
"π **A free Groq API key is required.**\n\n"
"Get one in 60 seconds β no credit card needed:\n"
"1. Go to [console.groq.com/keys](https://console.groq.com/keys)\n"
"2. Sign up free β click **Create API Key** β copy it\n"
"3. Paste it in the **Groq API Key** box on the left\n\n"
"Your key is never stored β session only."
)
return
has_image = image_input is not None
has_text = text_input and text_input.strip()
if not has_image and not has_text:
yield "β οΈ Please upload a worksheet image **or** type a math problem."
return
grade_note = f"Grade level: {grade_level}."
yield "β‘ Solving step by step..."
try:
if has_image:
img_b64 = image_to_base64(image_input)
text_note = f"\nStudent's additional note: {text_input.strip()}" if has_text else ""
user_content = [
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{img_b64}"
}
},
{
"type": "text",
"text": f"{grade_note}\n\nPlease read this math problem from the image carefully, identify all numbers and labels, then teach me how to solve it step by step.{text_note}"
}
]
model = VISION_MODEL
else:
user_content = f"{grade_note}\n\nProblem: {text_input.strip()}\n\nPlease teach me how to solve this step by step."
model = TEXT_MODEL
response = requests.post(
GROQ_API_URL,
headers={
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
},
json={
"model": model,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_content},
],
"max_tokens": 1200,
"temperature": 0,
"stream": True,
},
stream=True,
timeout=60,
)
if response.status_code == 401:
yield "β **Invalid API key.** Please check your Groq key and try again."
return
if response.status_code == 429:
yield "β οΈ Rate limit hit. Please wait a few seconds and try again."
return
if response.status_code != 200:
yield f"β API error {response.status_code}: {response.text[:200]}"
return
output = ""
for raw in response.iter_lines():
if not raw:
continue
line = raw.decode("utf-8", errors="ignore")
if line.startswith("data: "):
line = line[6:].strip()
if line == "[DONE]":
break
try:
token = json.loads(line)["choices"][0]["delta"].get("content", "")
if token:
output += token
yield strip_latex(output)
except Exception:
continue
if not output:
yield "β οΈ No response received. Please try again."
except requests.exceptions.Timeout:
yield "β Request timed out. Please try again."
except Exception as e:
yield f"β Error: {str(e)}"
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&family=JetBrains+Mono:wght@400;600&display=swap');
:root {
--bg:#0d1117; --surface:#161b22; --surface2:#1f2937;
--accent:#58a6ff; --accent2:#bc8cff; --green:#3fb950;
--text:#e6edf3; --muted:#7d8590; --border:#30363d; --radius:14px;
}
body, .gradio-container { background:var(--bg) !important; font-family:'Nunito',sans-serif !important; color:var(--text) !important; }
.math-header { text-align:center; padding:32px 20px 18px; background:linear-gradient(160deg,#161b22,#0d1117); border-bottom:1px solid var(--border); border-radius:var(--radius); margin-bottom:20px; }
.math-header h1 { font-size:2.2rem; font-weight:900; background:linear-gradient(90deg,var(--accent),var(--accent2),var(--green)); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; margin:0 0 6px; }
.math-header p { color:var(--muted); font-size:.95rem; margin:0; }
.badge-row { display:flex; gap:8px; flex-wrap:wrap; justify-content:center; margin-top:12px; }
.badge { background:var(--surface2); border:1px solid var(--border); border-radius:20px; padding:3px 12px; font-size:.76rem; color:var(--muted); }
.panel-label { font-size:.72rem; font-weight:800; letter-spacing:.12em; text-transform:uppercase; color:var(--muted); margin-bottom:6px; }
.token-box textarea, .token-box input { background:var(--surface2) !important; border:1px solid var(--border) !important; border-radius:10px !important; color:var(--text) !important; font-family:'JetBrains Mono',monospace !important; font-size:.85rem !important; }
.gr-textbox textarea, .gr-textbox input { background:var(--surface2) !important; border:1px solid var(--border) !important; border-radius:10px !important; color:var(--text) !important; font-family:'Nunito',sans-serif !important; font-size:.95rem !important; }
.solve-btn { background:linear-gradient(135deg,var(--accent),var(--accent2)) !important; color:#fff !important; font-family:'Nunito',sans-serif !important; font-weight:900 !important; font-size:1.05rem !important; border:none !important; border-radius:12px !important; padding:14px 0 !important; width:100% !important; cursor:pointer !important; transition:all .2s !important; }
.solve-btn:hover { transform:translateY(-2px) !important; box-shadow:0 8px 24px rgba(88,166,255,.35) !important; }
.clear-btn { background:var(--surface2) !important; color:var(--muted) !important; border:1px solid var(--border) !important; border-radius:12px !important; font-family:'Nunito',sans-serif !important; font-weight:700 !important; transition:all .2s !important; }
.clear-btn:hover { border-color:var(--accent) !important; color:var(--text) !important; }
.output-box { background:var(--surface) !important; border:1px solid var(--border) !important; border-radius:var(--radius) !important; padding:24px !important; min-height:420px !important; font-family:'Nunito',sans-serif !important; font-size:.97rem !important; line-height:1.8 !important; color:var(--text) !important; }
.output-box code, .output-box pre { font-family:'JetBrains Mono',monospace !important; background:var(--surface2) !important; border-radius:6px !important; padding:2px 8px !important; }
.key-info { background:var(--surface2); border:1px solid var(--border); border-left:3px solid var(--accent); border-radius:10px; padding:12px 16px; margin-top:8px; font-size:.82rem; color:var(--muted); line-height:1.7; }
.key-info a { color:var(--accent); text-decoration:none; }
.key-info strong { color:var(--text); }
.info-box { background:var(--surface2); border:1px solid var(--border); border-left:3px solid var(--green); border-radius:10px; padding:12px 16px; margin-top:8px; font-size:.82rem; color:var(--muted); line-height:1.6; }
.info-box strong { color:var(--green); }
footer { display:none !important; }
"""
GRADE_CHOICES = [
"Auto-detect from problem",
"Grade 1β2 (Basic Addition & Subtraction)",
"Grade 3β4 (Multiplication, Division, Fractions Intro)",
"Grade 5β6 (Fractions, Decimals, Geometry Basics)",
"Grade 7β8 (Algebra, Ratios, Percentages)",
"Grade 9β10 (Algebra II, Geometry, Trigonometry)",
"Grade 11β12 (Pre-Calculus, Statistics, Calculus)",
]
EXAMPLES = [
[None, "What is 24 Γ· 6 + 3 Γ 2?", "Grade 3β4 (Multiplication, Division, Fractions Intro)", ""],
[None, "If I have 3/4 of a pizza and eat 1/3, how much is left?", "Grade 5β6 (Fractions, Decimals, Geometry Basics)", ""],
[None, "Solve for x: 2x + 5 = 17", "Grade 7β8 (Algebra, Ratios, Percentages)", ""],
[None, "Find the area of a circle with radius 7 cm", "Grade 9β10 (Algebra II, Geometry, Trigonometry)", ""],
[None, "Differentiate f(x) = 3xΒ³ β 2x + 7", "Grade 11β12 (Pre-Calculus, Statistics, Calculus)", ""],
]
OWNER_KEY_SET = bool(SPACE_GROQ_KEY)
with gr.Blocks(css=CSS, title="Interactive Math Tutor") as demo:
gr.HTML("""
<div class="math-header">
<h1>π Interactive Math Tutor</h1>
<p>Step-by-step guidance Β· Grade 1 to High School Β· Upload a worksheet or type any problem</p>
<div class="badge-row">
<span class="badge">βοΈ Arithmetic</span><span class="badge">Β½ Fractions</span>
<span class="badge">π Algebra</span><span class="badge">π Geometry</span>
<span class="badge">π Trigonometry</span><span class="badge">β« Calculus</span>
</div>
</div>
""")
with gr.Row(equal_height=False):
with gr.Column(scale=4, min_width=300):
if not OWNER_KEY_SET:
gr.HTML('<div class="panel-label">π Groq API Key (free, no card)</div>')
user_key = gr.Textbox(
label="", placeholder="gsk_xxxxxxxxxxxxxxxxxxxx",
type="password", lines=1, elem_classes=["token-box"],
)
gr.HTML("""
<div class="key-info">
<strong>Get your free key in 60 seconds:</strong><br>
1. Go to <a href="https://console.groq.com/keys" target="_blank">console.groq.com/keys</a><br>
2. Sign up free (no credit card) β <strong>Create API Key</strong> β Copy<br>
3. Paste above Β· Never stored, session-only
</div>
""")
else:
user_key = gr.Textbox(value="", visible=False)
gr.HTML('<div class="info-box"><strong>β
Ready to use β no setup needed!</strong></div>')
gr.HTML('<div class="panel-label" style="margin-top:16px">π Upload Worksheet</div>')
image_input = gr.Image(type="pil", label="", height=180)
gr.HTML('<div style="font-size:.78rem;color:var(--muted);margin-top:4px">πΈ Images are read directly by AI β diagrams, triangles, and graphs all work!</div>')
gr.HTML('<div class="panel-label" style="margin-top:14px">βοΈ Or Type Your Problem</div>')
text_input = gr.Textbox(label="", placeholder="e.g. Solve for x: 5x β 3 = 22", lines=3)
gr.HTML('<div class="panel-label" style="margin-top:14px">π Grade Level</div>')
grade_dropdown = gr.Dropdown(choices=GRADE_CHOICES, value="Auto-detect from problem", label="")
with gr.Row():
solve_btn = gr.Button("π Solve & Explain", elem_classes=["solve-btn"])
clear_btn = gr.Button("π Clear", elem_classes=["clear-btn"])
gr.HTML('<div class="panel-label" style="margin-top:16px">π‘ Try an example</div>')
gr.Examples(examples=EXAMPLES, inputs=[image_input, text_input, grade_dropdown, user_key], label="")
with gr.Column(scale=6, min_width=360):
gr.HTML('<div class="panel-label">π Step-by-Step Solution</div>')
output_display = gr.Markdown(
value="*Your solution will appear here. Upload a worksheet or type a problem, then click **Solve & Explain**.*",
elem_classes=["output-box"],
)
solve_btn.click(
fn=solve_math,
inputs=[image_input, text_input, grade_dropdown, user_key],
outputs=output_display,
api_name=False,
)
clear_btn.click(
fn=lambda: (None, "", "Auto-detect from problem", "", "*Your solution will appear here.*"),
inputs=[],
outputs=[image_input, text_input, grade_dropdown, user_key, output_display],
api_name=False,
)
if __name__ == "__main__":
demo.queue().launch() |