import gradio as gr import os import datetime import json import tempfile import datetime, hashlib # === Import Groq API === from groq import Groq # === Try importing Faster-Whisper === try: from faster_whisper import WhisperModel ASR_ENGINE = "faster-whisper" except ImportError: import speech_recognition as sr ASR_ENGINE = "speech-recognition" # === Try importing pyttsx3, else fallback to gTTS === try: import pyttsx3 TTS_ENGINE = "pyttsx3" except ImportError: from gtts import gTTS TTS_ENGINE = "gtts" # === Groq API Key === # GROQ_API_KEY = os.getenv("GROQ_API_KEY", "your_api_key_here") # client = Groq(api_key=GROQ_API_KEY) GROQ_API_KEY = os.environ.get("GROQ_API_KEY") # no fallback value for security if GROQ_API_KEY is None: raise ValueError("❌ GROQ_API_KEY is not set. Please add it in Hugging Face Spaces 'Settings > Secrets'.") client = Groq(api_key=GROQ_API_KEY) # === Error tracking & history storage === ERROR_LOG_FILE = "errors.json" HISTORY_FILE = "history.json" if not os.path.exists(ERROR_LOG_FILE): with open(ERROR_LOG_FILE, "w") as f: json.dump({}, f) if not os.path.exists(HISTORY_FILE): with open(HISTORY_FILE, "w") as f: json.dump([], f) # === Functions === def analyze_text(user_input): try: prompt = f""" User input: {user_input} Task: 1. First, assess the input sentence and state clearly if it is: - Correct - Partially correct (explain briefly why) - Incorrect (explain briefly why) 2. Correct grammar & spelling → show as: Corrected Sentence: Highlight Difficult/New Words (Corrected): 3. Provide a formal version → show as: Formal Version: Highlight Difficult/New Words (Formal): 4. Provide an informal version → show as: Informal Version: Highlight Difficult/New Words (Informal): Finally, output a JSON object in this format (and nothing else at the end): {{"correct": true or false}} """ response = client.chat.completions.create( model="llama-3.3-70b-versatile", messages=[{"role": "user", "content": prompt}], ) corrected_text = response.choices[0].message.content # ✅ fixed # corrected_text = response.choices[0].message.content # ✅ Extract correctness is_correct = False if "" in corrected_text and "" in corrected_text: try: stats_str = corrected_text.split("")[1].split("")[0] stats = json.loads(stats_str) is_correct = stats.get("correct", False) except Exception: is_correct = False update_daily_stats(is_correct=is_correct) # ✅ Clean output for user (remove hidden stats) cleaned_text = corrected_text.split("")[0].strip() # if "✅ Correct" in response or "No errors" in response: # update_daily_stats(is_correct=True) # else: # update_daily_stats(is_correct=False) # return corrected_text # not return response # Save history with open(HISTORY_FILE, "r+") as f: history = json.load(f) history.append({ "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "input": user_input, "response": cleaned_text }) f.seek(0) json.dump(history, f, indent=2) return cleaned_text except Exception as e: return f"❌ Error in analyze_text: {str(e)}" def speech_to_text(audio_file): """Convert speech to text""" if ASR_ENGINE == "faster-whisper": model = WhisperModel("small") segments, _ = model.transcribe(audio_file) return " ".join([seg.text for seg in segments]) else: recognizer = sr.Recognizer() with sr.AudioFile(audio_file) as source: audio = recognizer.record(source) return recognizer.recognize_google(audio) def text_to_speech(text): try: from gtts import gTTS output_file = os.path.join(tempfile.gettempdir(), "output.mp3") tts = gTTS(text) tts.save(output_file) return output_file except Exception as e: return None def get_history(): with open(HISTORY_FILE, "r") as f: history = json.load(f) return "\n".join([f"[{h['timestamp']}] {h['input']} → {h['response']}" for h in history]) # === Progress Functions === def calculate_progress(): # Load history with open(HISTORY_FILE, "r") as f: history = json.load(f) if not history: return "No progress yet! Try practicing first.", None, None, "🔥 0-day streak" # --- Error Trend (Mistakes over time) --- dates = [h["timestamp"].split(" ")[0] for h in history] mistake_counts = [len(h["response"].split()) - len(h["input"].split()) for h in history] # rough proxy plt.figure(figsize=(5,3)) plt.plot(dates, mistake_counts, marker="o") plt.xticks(rotation=45) plt.title("Mistakes Over Time") plt.tight_layout() trend_img = save_plot_to_base64() # --- Error Categories --- categories = {"Grammar":0, "Spelling":0, "Vocabulary":0} for h in history: resp = h["response"].lower() if "grammar" in resp: categories["Grammar"] += 1 if "spelling" in resp: categories["Spelling"] += 1 if "word" in resp: categories["Vocabulary"] += 1 plt.figure(figsize=(4,3)) plt.bar(categories.keys(), categories.values(), color="orange") plt.title("Error Categories") plt.tight_layout() cat_img = save_plot_to_base64() # --- Streak Counter --- practiced_days = sorted(set(dates)) streak = 1 for i in range(len(practiced_days)-1, 0, -1): d1 = datetime.datetime.strptime(practiced_days[i], "%Y-%m-%d") d2 = datetime.datetime.strptime(practiced_days[i-1], "%Y-%m-%d") if (d1 - d2).days == 1: streak += 1 else: break return "📊 Progress Overview", trend_img, cat_img, f"🔥 {streak}-day streak" def save_plot_to_base64(): buf = io.BytesIO() plt.savefig(buf, format="png") buf.seek(0) encoded = base64.b64encode(buf.read()).decode("utf-8") buf.close() return "data:image/png;base64," + encoded # keep progress data in memory (you can later persist to file/db if needed) progress_data = { "today": str(datetime.date.today()), "correct_count": 0, "incorrect_count": 0, "streak": 0, "last_submission_id": None, # for dedupe } # added part def reset_daily_progress(): """Reset progress when a new day starts.""" progress_data.update({ "today": str(datetime.date.today()), "correct_count": 0, "incorrect_count": 0, "streak": 0, "last_submission_id": None, }) # ---- 1) Make correctness explicitly boolean ---- def normalize_is_correct(raw): """ Convert output of grammar_checker(...) to a strict boolean for GRAMMAR only. Adjust these rules to match your checker. """ if isinstance(raw, bool): return raw if isinstance(raw, dict): # prefer a clear key from your checker if "grammar_ok" in raw: return bool(raw["grammar_ok"]) if "errors" in raw: return len(raw["errors"]) == 0 if isinstance(raw, (list, tuple, set)): # treat as list of grammar errors return len(raw) == 0 if isinstance(raw, str): s = raw.strip().lower() # map typical responses from LLMs/tools return s in ("true", "correct", "ok", "no errors", "no error", "grammatically correct") # last resort return bool(raw) # ---- 2) Update stats (with dedupe + badge) ---- def update_daily_stats(is_correct: bool, submission_id: str = None): today = str(datetime.date.today()) if progress_data["today"] != today: reset_daily_progress() # de-dupe: if the same submission hits the function twice, ignore the second if submission_id and progress_data.get("last_submission_id") == submission_id: return progress_data["last_submission_id"] = submission_id if is_correct: progress_data["correct_count"] += 1 progress_data["streak"] += 1 # if progress_data["streak"] >= 5 and "🏅 5-in-a-row Champion" not in progress_data["badges"]: # progress_data["badges"].append("🏅 5-in-a-row Champion") else: progress_data["incorrect_count"] += 1 progress_data["streak"] = 0 # ---- 3) Evaluate exactly once per submission ---- def evaluate_sentence(user_sentence: str): """ Call your checker ONCE, normalize to boolean, update stats ONCE. """ raw_result = grammar_checker(user_sentence) # <- your existing checker is_correct = normalize_is_correct(raw_result) # <- strict boolean # robust per-input dedupe id (same sentence => same id; adjust if you want to allow repeats) submission_id = hashlib.sha1(user_sentence.strip().encode("utf-8")).hexdigest() update_daily_stats(is_correct, submission_id=submission_id) return is_correct, raw_result def get_progress_summary(): today = progress_data["today"] return ( f"📅 Date: {today}\n" f"✅ Correct: {progress_data['correct_count']}\n" f"📝 Practice Attempts: {progress_data['correct_count'] + progress_data['incorrect_count']}\n" f"🚀 Keep going, you’re improving every day!\n" ) # # === Inside Gradio UI === # with gr.Tab("Progress 📈"): # gr.Markdown("### 📊 Your Daily Progress") # progress_box = gr.Textbox(label="Today’s Stats", interactive=False, lines=8) # # Refresh button # refresh_progress = gr.Button("🔄 Refresh Progress") # refresh_progress.click(get_progress_summary, outputs=progress_box) # === Gradio Interface === with gr.Blocks() as demo: gr.Markdown("# 🤖✍🏻 GrammerBot") gr.Markdown("Your sentences, corrected. Your writing, perfected.") with gr.Tab("Practice 📝"): with gr.Row(): text_input = gr.Textbox(label="Type in English", placeholder="Write something...",lines=1) mic_input = gr.Audio(sources=["microphone"], type="filepath") output_text = gr.Textbox(label="AI Response") tts_output = gr.Audio(label="Listen to Response") submit_btn = gr.Button("Submit") speech_btn = gr.Button("Transcribe Speech") submit_btn.click(analyze_text, inputs=text_input, outputs=output_text)\ .then(text_to_speech, inputs=output_text, outputs=tts_output) speech_btn.click(speech_to_text, inputs=mic_input, outputs=text_input) with gr.Tab("History 📜"): history_box = gr.Textbox(label="Past Attempts", interactive=False, lines=15) refresh_btn = gr.Button("⏳ Refresh History") refresh_btn.click(get_history, outputs=history_box) # with gr.Tab("Progress 📈"): # gr.Markdown("Error tracking & progress visualization coming soon!") with gr.Tab("Progress 📈"): gr.Markdown(" 📊 Daily Progress Tracker") progress_box = gr.Textbox( label="Today's Progress", interactive=False, lines=8, value="No progress yet. Try practicing!" ) refresh_btn = gr.Button("🔄 Refresh Progress") # ✅ bind refresh button here refresh_btn.click(get_progress_summary, outputs=progress_box) # === Launch === demo.launch()