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', '
')
back_raw = fields[1].replace('\n', '
')
# 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
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}
Explanation: (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()