Spaces:
Sleeping
Sleeping
| 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: <your text> | |
| Highlight Difficult/New Words (Corrected): <word: definition, word: definition> | |
| 3. Provide a formal version β show as: | |
| Formal Version: <your text> | |
| Highlight Difficult/New Words (Formal): <word: definition, word: definition> | |
| 4. Provide an informal version β show as: | |
| Informal Version: <your text> | |
| Highlight Difficult/New Words (Informal): <word: definition, word: definition> | |
| Finally, output a JSON object in this format (and nothing else at the end): | |
| <stats>{{"correct": true or false}}</stats> | |
| """ | |
| 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 "<stats>" in corrected_text and "</stats>" in corrected_text: | |
| try: | |
| stats_str = corrected_text.split("<stats>")[1].split("</stats>")[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("<stats>")[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() | |