File size: 11,763 Bytes
c60654d
 
 
 
 
d00b584
35c55b8
c60654d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11af2e8
 
 
 
 
 
 
c60654d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e2b177
 
 
3c3c99e
f6e0f10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e2b177
 
 
1cdf291
3e2b177
 
 
 
7ec2750
 
b9a01e1
 
 
 
 
 
 
 
 
81b4bb3
b9a01e1
 
 
 
 
 
 
 
 
 
7ec2750
3e2b177
 
 
 
 
 
b9a01e1
3e2b177
 
 
 
b9a01e1
3e2b177
 
c60654d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e2b177
 
 
c60654d
 
3e2b177
 
 
c60654d
 
 
 
 
 
 
 
35c55b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81b4bb3
 
 
 
 
 
d00b584
1da6442
3caf4b6
 
 
 
 
 
 
 
d00b584
3caf4b6
35c55b8
d00b584
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1da6442
81b4bb3
3caf4b6
f73063c
d00b584
 
 
 
81b4bb3
1da6442
81b4bb3
 
893fbb0
 
1da6442
81b4bb3
 
 
d00b584
 
f73063c
d00b584
f73063c
d00b584
 
 
 
 
 
f73063c
1da6442
81b4bb3
d00b584
81b4bb3
d00b584
d039175
59f0b9f
1da6442
81b4bb3
1da6442
4fd85c7
 
 
 
 
 
 
 
35c55b8
 
c60654d
 
0adaddb
c43a6ae
c60654d
0adaddb
c60654d
5c5a3ed
c60654d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0adaddb
c60654d
 
1da6442
 
c60654d
1d1098d
1da6442
26c5803
 
 
 
 
 
81b4bb3
26c5803
81b4bb3
 
26c5803
c60654d
 
 
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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
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()