Spaces:
Build error
Build error
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()
|