Download app.py from vishwakayireddi1234/Interactive-Math-Tutor: direct link, hf CLI and curl.
- Browser
- Download file 14.9 kB
-
https://huggingface.co/spaces/vishwakayireddi1234/Interactive-Math-Tutor/resolve/main/app.py
- Command line
-
hf download hf://spaces/vishwakayireddi1234/Interactive-Math-Tutor/app.py
-
curl -L -o app.py https://huggingface.co/spaces/vishwakayireddi1234/Interactive-Math-Tutor/resolve/main/app.py
14.9 kB
| 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() |