Language_buddy / app.py
lalaru's picture
Update app.py
f6e0f10 verified
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()