File size: 17,557 Bytes
c92c46b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
import os
import zipfile
import sqlite3
import tempfile
import csv
from dotenv import load_dotenv
from groq import Groq
import httpx
import time
from pathlib import Path
import re
import pandas as pd
import PyPDF2

from tag_constants import KEYWORD_MAP


load_dotenv()

# Paths to your assets
ASSETS_DIR = Path(__file__).parent.parent / "assets"
DB_PATH = Path(__file__).parent.parent / "resources.db"
FLASHCARDS_DIR = ASSETS_DIR / "flashcards"
PYQ_CSV = ASSETS_DIR / "pyq" / "questions-data-new.csv"
VIDEO_DIR = ASSETS_DIR / "video"
import sys
# Add the project root to sys.path so we can import the app module
sys.path.append(str(Path(__file__).parent.parent))
from app.groq_client import groq_chat


def setup_db():
    """Initializes the SQLite Database for micro-resources."""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS micro_resources (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            topic TEXT,
            type TEXT, 
            content TEXT,
            duration_min INTEGER,
            cognitive_load INTEGER
        )
    """)
    conn.commit()
    return conn

def classify_flashcard(content):
    content_lower = content.lower()

    for keyword, topic in KEYWORD_MAP.items():
        if keyword in content_lower:
            return topic
    
    return "General CS" # Fallback if no keywords match

def process_anki_apkg(conn, apkg_path, batch_size=20):
    
    print(f"Processing Flashcard Deck: {apkg_path.name} in batches of {batch_size}...")
    cursor = conn.cursor()
    
    existing_flashcards = {row[0].split('\n**Back:**')[0].replace('**Front:** ', '').strip() 
        for row in cursor.execute("SELECT content FROM micro_resources WHERE type = 'flashcard'")}
    
    with tempfile.TemporaryDirectory() as tmp_dir:
        # 1. Unzip the .apkg file
        with zipfile.ZipFile(apkg_path, 'r') as zip_ref:
            zip_ref.extractall(tmp_dir)
            
        # 2. Find the SQLite DB (usually collection.anki2 or collection.anki21)
        db_file = os.path.join(tmp_dir, 'collection.anki2')
        if not os.path.exists(db_file):
            db_file = os.path.join(tmp_dir, 'collection.anki21')
            
        if not os.path.exists(db_file):
            print(f"  [!] No valid collection.anki2 DB found in {apkg_path.name}")
            return
            
        # 3. Connect to the Anki DB and extract notes
        anki_conn = sqlite3.connect(db_file)
        anki_cursor = anki_conn.cursor()
        
        try:
            # flds contains front and back separated by \x1f
            anki_cursor.execute("SELECT tags, flds FROM notes")
            rows = anki_cursor.fetchall()
            
            count = 0
            for tags, flds in rows:
                fields = flds.split('\x1f')
                if len(fields) >= 2:
                    front = fields[0].replace('\n', '<br>')
                    back_raw = fields[1].replace('\n', '<br>')
                    
                    # Phase 11: Add detailed explanation and research link to flashcard
                    augment_prompt = "You are an expert tutor. Enhance this flashcard answer with a detailed explanation and a deep-dive research link (e.g. Wikipedia or documentation). Output ONLY the enhanced answer in HTML format using <br> for newlines."
                    augment_user = f"Front: {front}\nBack: {back_raw}"
                    try:
                        back = groq_chat(system_prompt=augment_prompt, user_message=augment_user, model="openai/gpt-oss-20b")
                    except Exception as e:
                        back = f"{back_raw}<br><br><b>Explanation:</b> (Auto-generation failed - {str(e)})"

                    content = f"**Front:** {front}\n**Back:** {back}"
                    
                    if front.strip() in existing_flashcards:
                        continue  # Skip already processed flashcards
                    existing_flashcards.add(front.strip())
                    
                    detected_topic = classify_flashcard(content)
                    
                    cursor.execute("""
                        INSERT INTO micro_resources (topic, type, content, duration_min, cognitive_load)
                        VALUES (?, ?, ?, ?, ?)
                    """, (detected_topic.strip(), 'flashcard', content, 2, 2))
                    
                    count += 1
                    
                    if count % batch_size == 0:
                        conn.commit()
                        print(f" [+] Saved batch of {batch_size}. Total flashcards processed: {count}")
                        
                        time.sleep(2)
                        
            conn.commit()
            print(f"  [+] Finished! Extracted & Tagged {count} NEW flashcards.")
                    
        except Exception as e:
            print(f"  [!] Error reading Anki DB: {e}")
        finally:
            anki_conn.close()
            
    conn.commit()

def generate_solution_with_groq(question_text):
    """Uses Groq API to solve PYQ offline and extract a quick tip."""
    system_prompt = "You are an expert Computer Science professor. Solve this past-year exam question concisely. Then, provide a 1-sentence 'Quick Tip'."
    user_message = f"Question: {question_text}"
    
    try:
        return groq_chat(system_prompt=system_prompt, user_message=user_message, model="openai/gpt-oss-20b")
    except Exception as e:
        print(f"  [!] Groq API Error: {e}")
        return None

def process_pyq_csv(conn, csv_path, max_rows=10):
    """
    Reads the PYQ CSV. 
    WARNING: We limit to `max_rows` by default so you don't exhaust your Groq API limits.
    """
    print(f"Processing PYQs from {csv_path.name} (Limit: {max_rows} rows)...")
    cursor = conn.cursor()
    
    # Fetch already processed questions to allow resuming
    cursor.execute("SELECT content FROM micro_resources WHERE type='pyq_solution'")
    existing_content = [row[0] for row in cursor.fetchall()]
    
    count = 0
    with open(csv_path, 'r', encoding='utf-8') as f:
        reader = csv.DictReader(f)
        for row in reader:
            if count >= max_rows:
                break
                
            topic = row.get('topic', 'General')
            question = row.get('question', '')
            
            # Check if this question was already processed (exists in any content string)
            already_processed = any(question in content for content in existing_content)
            if already_processed:
                print(f"  -> Skipping (Already processed): {question[:50]}...")
                continue

            
            print(f"  -> Generating solution for: {question[:50]}...")
            solution = generate_solution_with_groq(question)
            
            if solution:
                content = f"**Question:** {question}\n\n**Solution & Tip:**\n{solution}"
                cursor.execute("""
                    INSERT INTO micro_resources (topic, type, content, duration_min, cognitive_load)
                    VALUES (?, ?, ?, ?, ?)
                """, (topic, 'pyq_solution', content, 5, 4))
                count += 1
                time.sleep(1) # Prevent hitting rate limits
                
    conn.commit()
    print(f"  [+] Generated {count} PYQ solutions.")

def time_to_minutes(time_str):
    """Converts SRT timestamp 'HH:MM:SS,mmm' to total minutes."""
    # Example: 00:05:30,000 -> 5.5
    parts = re.split(r'[:,]', time_str)
    if len(parts) >= 3:
        h, m, s = int(parts[0]), int(parts[1]), int(parts[2])
        return (h * 60) + m + (s / 60.0)
    return 0

def process_video_srt(conn, srt_dir, chunk_duration_min=5):
    """Parses .srt files into N-minute chunks and stores them."""
    if not srt_dir.exists():
        print("  [!] Video directory not found.")
        return

    cursor = conn.cursor()
    total_chunks = 0

    for srt_file in srt_dir.glob("*.srt"):
        print(f"Processing Video Subtitles: {srt_file.name}...")
        
        # Use filename as topic temporarily
        topic = srt_file.stem.replace('_', ' ').title()
        
        with open(srt_file, 'r', encoding='utf-8') as f:
            content = f.read()

        # Simple Regex to match SRT blocks
        blocks = re.split(r'\n\n+', content.strip())
        
        current_chunk_text = []
        current_chunk_start = None
        current_chunk_end = None
        
        for block in blocks:
            lines = block.split('\n')
            if len(lines) >= 3:
                # Line 2 has timestamps: 00:00:00,000 --> 00:00:05,000
                timestamps = lines[1].split(' --> ')
                if len(timestamps) == 2:
                    start_min = time_to_minutes(timestamps[0])
                    end_min = time_to_minutes(timestamps[1])
                    text = ' '.join(lines[2:])
                    
                    if current_chunk_start is None:
                        current_chunk_start = start_min
                    
                    current_chunk_text.append(text)
                    current_chunk_end = end_min
                    
                    # If this chunk has reached our duration limit, save it!
                    if (current_chunk_end - current_chunk_start) >= chunk_duration_min:
                        chunk_content = f"**Video:** {topic} ({int(current_chunk_start)}m - {int(current_chunk_end)}m)\n\n" + " ".join(current_chunk_text)
                        
                        cursor.execute("""
                            INSERT INTO micro_resources (topic, type, content, duration_min, cognitive_load)
                            VALUES (?, ?, ?, ?, ?)
                        """, (topic, 'video_chunk', chunk_content, chunk_duration_min, 3))
                        
                        total_chunks += 1
                        # Reset for next chunk
                        current_chunk_text = []
                        current_chunk_start = None

        # Save any leftover text as the final chunk
        if current_chunk_text and current_chunk_start is not None and current_chunk_end is not None:
            actual_duration = max(1, int(current_chunk_end - current_chunk_start))
            chunk_content = f"**Video:** {topic} ({int(current_chunk_start)}m - {int(current_chunk_end)}m)\n\n" + " ".join(current_chunk_text)
            cursor.execute("""
                INSERT INTO micro_resources (topic, type, content, duration_min, cognitive_load)
                VALUES (?, ?, ?, ?, ?)
            """, (topic, 'video_chunk', chunk_content, actual_duration, 3))
            total_chunks += 1
            
    conn.commit()
    print(f"  [+] Extracted {total_chunks} video chunks.")

def process_programming_csv(conn, csv_path):
    #read CSV usign pandas or csv.DictReader
    print(f"Processing Programming CSV: {csv_path.name}...")
    cursor = conn.cursor()
    
    df = pd.read_csv(csv_path)
    count = 0
    
    #topic can be "Programming Language" or "Topic" column
    for index, row in df.iterrows():
        question = str(row.get('Question', ''))
        language = str(row.get('Programming Language', ''))
        topic = str(row.get('Topic', ''))
        solution = str(row.get('AI-Generated Solution', ''))
        explanation = str(row.get('Explanation', ''))
        
        if pd.isna(question) or question == 'nan' : continue
    
        #Combine the question, solution and explanation into a Markdown string and save it as a pyq_solution type.
        db_topic = f"{language} {topic}".strip() if language != 'nan' else topic
        if not db_topic or db_topic == 'nan':
            db_topic = "General Programming"
        
        content = f"**Question:**\n{question}\n\n**Solution:**\n{solution}\n\n**Explanation:**\n{explanation}"
        
        cursor.execute("""
            INSERT INTO micro_resources (topic, type, content, duration_min, cognitive_load)
            VALUES (?, ?, ?, ?, ?)
        """, (db_topic, 'pyq_solution', content, 5, 4))
        count += 1
        
    conn.commit()
    print(f"  [+] Processed {count} programming resources.")
    
def process_software_csv(conn, csv_path):
    print(f"Processing Software Question from {csv_path.name}...")
    cursor = conn.cursor()
    
    #Add encoding='latin1' to the file reader to bypass the unicode error.
    df = pd.read_csv(csv_path, encoding='latin1')
    count = 0
    
    #Map its question/answer columns to our micro_resources schema.
    for index, row in df.iterrows():
        question = str(row.get('Question', ''))
        answer = str(row.get('Answer', ''))
        topic = str(row.get('Category', ''))
        
        if pd.isna(question) or question == 'nan' : continue
        
        if not topic or topic == 'nan':
            topic = "General Software"
        
        content = f"**Question:**\n{question}\n\n**Answer:**\n{answer}"
        
        cursor.execute("""
            INSERT INTO micro_resources (topic, type, content, duration_min, cognitive_load)
            VALUES (?, ?, ?, ?, ?)
        """, (topic, 'flashcard', content, 2, 2))
        count += 1
        
    conn.commit()
    print(f"  [+] Processed {count} software resources.")

def process_unstructured_folder(conn, pdf_path):
    #Read the PDFs/TXTs and split the text into small 500-word chunks.
    print(f"Processing Unstructured Folder: {pdf_path.name}...")
    cursor = conn.cursor()
    count = 0
    
    text_content = ""
    try:
        with open(pdf_path, 'rb') as f:
            reader = PyPDF2.PdfReader(f)
            for page in reader.pages:
                text_content += page.extract_text() + "\n"
    except Exception as e:
        print(f"  [!] Error reading PDF {pdf_path}: {e}")
        return
    
    words = text_content.split()
    chunk_size = 500

    for i in range(0, len(words), chunk_size):
        chunk_words = words[i:i + chunk_size]
        chunk_text = ' '.join(chunk_words)
        if not chunk_text.strip(): continue
        
        print(f"    -> Generating flashcards from chunk {count+1}...")
        system_prompt = "You are an expert Computer Science professor. Create 3-5 concise flashcards from this content. Format as markdown with **Question:** and **Answer:** pairs."
            
        try:
            time.sleep(3) # To avoid hitting rate limits
            response = groq_chat(system_prompt=system_prompt, 
                                user_message=chunk_text,
                                model="openai/gpt-oss-20b")
            if response:
                cursor.execute("""
                    INSERT INTO micro_resources (topic, type, content, duration_min, cognitive_load)
                    VALUES (?, ?, ?, ?, ?)
                """, ("Data Science", "flashcard", response, 2, 2))
        except Exception as e:
            print(f"  [!] Error generating flashcards: {e}")

        print(f"    -> Generating PYQ from chunk {count+1}...")
        pyq_prompt = "You are an expert Computer Science professor. Create 1 complex, exam-style past-year question and a detailed solution based on this content. Format as markdown with exactly '**Question:**' and '**Solution & Tip:**'."
            
        try:
            time.sleep(3)
            pyq_response = groq_chat(system_prompt=pyq_prompt, 
                                user_message=chunk_text,
                                model="openai/gpt-oss-20b")
            if pyq_response:
                cursor.execute("""
                    INSERT INTO micro_resources (topic, type, content, duration_min, cognitive_load)
                    VALUES (?, ?, ?, ?, ?)
                """, ("Data Science", "pyq_solution", pyq_response, 5, 4))
        except Exception as e:
            print(f"  [!] Error generating PYQ: {e}")

        count +=1
        conn.commit()
        
    print(f"  [+] Processed {count} unstructured resources.")

    
    #Save under data science topic
if __name__ == "__main__":
    print("Starting Offline Asset Processing...")
    conn = setup_db()
    
    # 1. Process Flashcards (.apkg files)
    if FLASHCARDS_DIR.exists():
        for apkg_file in FLASHCARDS_DIR.glob("*.apkg"):
            process_anki_apkg(conn, apkg_file)
    else:
        print(" [!] Flashcards directory not found.")
        
    # 2. Process PYQ CSV
    if PYQ_CSV.exists():
        process_pyq_csv(conn, PYQ_CSV, max_rows=10) # Set limit higher when ready!
    else:
        print(" [!] PYQ CSV not found.")
        
    # 3. Process Video Subtitles (.srt files)
    process_video_srt(conn, VIDEO_DIR, chunk_duration_min=5)
    
    pyq_dir = ASSETS_DIR / "pyq"
    
    if (pyq_dir / "programming_questions_solutions.csv").exists():
        process_programming_csv(conn, pyq_dir / "programming_questions_solutions.csv")
    else:
        print("  [!] Programming CSV not found.")
        
    if (pyq_dir / "software_questions.csv").exists():
        process_software_csv(conn, pyq_dir / "software_questions.csv")
    else:
        print("  [!] Software CSV not found.")
    ds_dir = pyq_dir / "data_science"
    if (ds_dir / "questions1.pdf").exists():
        process_unstructured_folder(conn, ds_dir / "questions1.pdf")
    else:
        print("  [!] Data Science PDF 1 not found.")
        
    if (ds_dir / "questions2.pdf").exists():
        process_unstructured_folder(conn, ds_dir / "questions2.pdf")
    else:
        print("  [!] Data Science PDF 2 not found.")
        
    conn.close()