andevs commited on
Commit
b2863f6
·
verified ·
1 Parent(s): ff9384f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +384 -486
app.py CHANGED
@@ -1,5 +1,6 @@
1
  """
2
- StudyFlow AI Backend - Complete Working Version with REAL Question Generation
 
3
  """
4
  import os
5
  import json
@@ -7,17 +8,18 @@ import sqlite3
7
  import hashlib
8
  import tempfile
9
  import re
 
10
  from datetime import datetime
11
- from typing import List, Dict
12
-
13
  from fastapi import FastAPI, UploadFile, File, Form, HTTPException
14
  from fastapi.responses import JSONResponse, HTMLResponse
15
  from fastapi.middleware.cors import CORSMiddleware
 
16
  import PyPDF2
17
  from youtube_transcript_api import YouTubeTranscriptApi
18
 
19
  # Initialize FastAPI
20
- app = FastAPI(title="StudyFlow AI", version="2.0.0")
21
 
22
  # CORS middleware
23
  app.add_middleware(
@@ -28,104 +30,68 @@ app.add_middleware(
28
  allow_headers=["*"],
29
  )
30
 
31
- # Database setup - FIXED: Create data directory if it doesn't exist
32
- DB_DIR = "/data" if os.path.exists("/data") else os.path.dirname(os.path.abspath(__file__))
33
- if not os.path.exists(DB_DIR):
34
- os.makedirs(DB_DIR, exist_ok=True)
35
- DB_PATH = os.path.join(DB_DIR, "studyflow.db")
36
 
37
- print(f"📁 Database path: {DB_PATH}")
 
38
 
39
  def init_db():
40
  """Initialize SQLite database"""
41
- try:
42
- conn = sqlite3.connect(DB_PATH)
43
- cursor = conn.cursor()
44
-
45
- # Sessions table
46
- cursor.execute('''
47
- CREATE TABLE IF NOT EXISTS sessions (
48
- id TEXT PRIMARY KEY,
49
- title TEXT NOT NULL,
50
- content_type TEXT NOT NULL,
51
- difficulty TEXT NOT NULL,
52
- content_hash TEXT,
53
- raw_content TEXT,
54
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
55
- last_accessed TIMESTAMP DEFAULT CURRENT_TIMESTAMP
56
- )
57
- ''')
58
-
59
- # Questions table
60
- cursor.execute('''
61
- CREATE TABLE IF NOT EXISTS questions (
62
- id TEXT PRIMARY KEY,
63
- session_id TEXT NOT NULL,
64
- question_text TEXT NOT NULL,
65
- question_type TEXT NOT NULL,
66
- options TEXT,
67
- correct_answer TEXT NOT NULL,
68
- difficulty TEXT NOT NULL,
69
- explanation TEXT,
70
- user_answer TEXT,
71
- is_correct INTEGER DEFAULT 0,
72
- time_spent INTEGER DEFAULT 0,
73
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
74
- FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
75
- )
76
- ''')
77
-
78
- # Flashcards table
79
- cursor.execute('''
80
- CREATE TABLE IF NOT EXISTS flashcards (
81
- id TEXT PRIMARY KEY,
82
- session_id TEXT NOT NULL,
83
- front TEXT NOT NULL,
84
- back TEXT NOT NULL,
85
- category TEXT,
86
- difficulty TEXT,
87
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
88
- FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
89
- )
90
- ''')
91
-
92
- # Notes table
93
- cursor.execute('''
94
- CREATE TABLE IF NOT EXISTS notes (
95
- id TEXT PRIMARY KEY,
96
- session_id TEXT NOT NULL,
97
- title TEXT NOT NULL,
98
- content TEXT NOT NULL,
99
- tags TEXT,
100
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
101
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
102
- FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
103
- )
104
- ''')
105
-
106
- # Highlights table
107
- cursor.execute('''
108
- CREATE TABLE IF NOT EXISTS highlights (
109
- id TEXT PRIMARY KEY,
110
- session_id TEXT NOT NULL,
111
- text TEXT NOT NULL,
112
- context TEXT,
113
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
114
- FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
115
- )
116
- ''')
117
-
118
- conn.commit()
119
- conn.close()
120
- print("✅ Database initialized successfully")
121
- return True
122
- except Exception as e:
123
- print(f"❌ Database initialization error: {str(e)}")
124
- return False
125
 
126
- # Initialize database
127
- if not init_db():
128
- print("⚠️ Warning: Database initialization failed, using in-memory fallback")
129
 
130
  def generate_id(text: str = None):
131
  """Generate a unique ID"""
@@ -134,24 +100,20 @@ def generate_id(text: str = None):
134
  return hashlib.md5(text.encode()).hexdigest()[:12]
135
  return str(uuid.uuid4())[:12]
136
 
137
- def extract_text_from_pdf(file_path: str) -> str:
138
- """Extract text from PDF file"""
 
139
  try:
140
  with open(file_path, 'rb') as file:
141
  pdf_reader = PyPDF2.PdfReader(file)
142
- text = ""
143
- for page_num, page in enumerate(pdf_reader.pages[:15]): # First 15 pages
144
- try:
145
- page_text = page.extract_text()
146
- if page_text:
147
- text += page_text + "\n"
148
- except Exception as e:
149
- print(f"Error extracting page {page_num}: {str(e)}")
150
- continue
151
- return text[:15000] # Limit text length
152
  except Exception as e:
153
  print(f"PDF extraction error: {str(e)}")
154
- return ""
155
 
156
  def extract_text_from_youtube(url: str) -> str:
157
  """Extract transcript from YouTube video"""
@@ -164,180 +126,182 @@ def extract_text_from_youtube(url: str) -> str:
164
  return ""
165
 
166
  transcript = YouTubeTranscriptApi.get_transcript(video_id)
167
- text = " ".join([entry['text'] for entry in transcript[:50]]) # First 50 entries
168
- return text[:15000]
169
  except Exception as e:
170
  print(f"YouTube extraction error: {str(e)}")
171
  return ""
172
 
173
- def extract_key_concepts(text: str, count: int = 8) -> List[str]:
174
- """Extract key concepts from text using NLP-like approach"""
 
 
 
175
  try:
176
- # Clean text
177
- text = text.lower()
178
-
179
- # Remove common stop words
180
- stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'but', 'so', 'if', 'then', 'else', 'when', 'where', 'which', 'what', 'who', 'whom', 'this', 'that', 'these', 'those', 'it', 'they', 'we', 'you', 'he', 'she', 'it', 'them', 'her', 'him', 'us'}
181
-
182
- # Split into words and count frequencies
183
- words = re.findall(r'\b[a-z]{4,}\b', text)
184
- freq = {}
185
- for word in words:
186
- if word not in stop_words:
187
- freq[word] = freq.get(word, 0) + 1
188
-
189
- # Get top words as concepts
190
- sorted_words = sorted(freq.items(), key=lambda x: x[1], reverse=True)
191
- concepts = [word for word, _ in sorted_words[:count]]
192
-
193
- # Also extract phrases (2-3 word sequences)
194
- phrases = re.findall(r'\b[a-z]{3,}\s+[a-z]{3,}\b', text)
195
- phrase_freq = {}
196
- for phrase in phrases[:50]:
197
- phrase_freq[phrase] = phrase_freq.get(phrase, 0) + 1
198
-
199
- top_phrases = sorted(phrase_freq.items(), key=lambda x: x[1], reverse=True)[:3]
200
- concepts.extend([phrase for phrase, _ in top_phrases])
201
-
202
- # Remove duplicates and limit
203
- unique_concepts = []
204
- for concept in concepts:
205
- if concept not in unique_concepts:
206
- unique_concepts.append(concept)
207
-
208
- return unique_concepts[:count] if unique_concepts else ["Key Concept", "Main Idea", "Important Topic"]
209
  except Exception as e:
210
- print(f"Concept extraction error: {str(e)}")
211
- return ["Key Concept", "Main Idea", "Important Topic"]
212
 
213
- def generate_questions_from_content(text: str, difficulty: str, count: int = 5) -> List[Dict]:
214
- """Generate REAL questions based on the actual content"""
215
- try:
216
- # Clean and prepare text
217
- text = text.replace('\n', ' ').strip()
218
- sentences = re.split(r'[.!?]+', text)
219
- sentences = [s.strip() for s in sentences if len(s.strip()) > 30]
220
-
221
- if len(sentences) < 3:
222
- # Fallback for very short text
223
- sentences = [s for s in text.split('.') if len(s.strip()) > 20][:5]
224
-
225
- concepts = extract_key_concepts(text, 6)
226
- questions = []
227
-
228
- for i in range(min(count, len(sentences))):
229
- sentence = sentences[i]
230
- qid = generate_id(f"q_{i}_{difficulty}")
231
-
232
- if difficulty == "easy":
233
- # Fill in the blank with a key word from the sentence
234
- words = sentence.split()
235
- if len(words) > 4:
236
- # Find a meaningful word to blank out
237
- blank_pos = min(len(words) - 3, max(2, len(words) // 2))
238
- blank_word = words[blank_pos]
239
- question_text = sentence.replace(blank_word, "_______", 1)
240
-
241
- questions.append({
242
- "id": qid,
243
- "question_text": f"Complete this sentence: {question_text}",
244
- "question_type": "fill_blank",
245
- "options": None,
246
- "correct_answer": blank_word,
247
- "difficulty": "easy",
248
- "explanation": f"The word '{blank_word}' is key to this sentence.",
249
- "concept": concepts[i % len(concepts)] if concepts else "Key Concept"
250
- })
251
- else:
252
- # Short sentence - use true/false
 
 
 
 
 
 
 
 
253
  questions.append({
254
- "id": qid,
255
- "question_text": sentence[:200],
256
- "question_type": "true_false",
257
- "options": None,
258
- "correct_answer": "True",
259
- "difficulty": "easy",
260
- "explanation": "This statement appears in the study material.",
261
- "concept": concepts[i % len(concepts)] if concepts else "Key Concept"
262
  })
263
-
264
- elif difficulty == "medium":
265
- # Multiple choice based on content
266
- concept = concepts[i % len(concepts)] if concepts else "the topic"
267
-
268
- # Generate plausible distractors
269
- other_concepts = [c for c in concepts if c != concept][:3]
270
- while len(other_concepts) < 3:
271
- other_concepts.extend(["Important detail", "Background information", "Supporting example"])
272
-
273
- options = [f"{concept}", f"{other_concepts[0]}", f"{other_concepts[1]}", f"{other_concepts[2]}"]
274
-
275
- questions.append({
276
- "id": qid,
277
- "question_text": f"What is the main idea expressed here: '{sentence[:150]}...'?",
278
- "question_type": "multiple_choice",
279
- "options": json.dumps(options),
280
- "correct_answer": options[0],
281
- "difficulty": "medium",
282
- "explanation": f"The text emphasizes {concept} as a central theme.",
283
- "concept": concept
284
- })
285
-
286
- else: # hard
287
- # Short answer requiring deeper understanding
288
- concept = concepts[i % len(concepts)] if concepts else "key concept"
289
-
290
- questions.append({
291
- "id": qid,
292
- "question_text": f"Explain the significance of '{concept}' based on: '{sentence[:200]}...'",
293
- "question_type": "short_answer",
294
- "options": None,
295
- "correct_answer": f"The passage presents '{concept}' as an important element in the material.",
296
- "difficulty": "hard",
297
- "explanation": "Focus on how key concepts connect to each other.",
298
- "concept": concept
299
- })
300
 
301
- # If we couldn't generate enough questions, add some general ones
302
- while len(questions) < count:
303
- concept = concepts[len(questions) % len(concepts)] if concepts else "the material"
304
- qid = generate_id(f"q_gen_{len(questions)}")
305
  questions.append({
306
  "id": qid,
307
- "question_text": f"What is the main focus of the section discussing '{concept}'?",
308
  "question_type": "short_answer",
309
  "options": None,
310
- "correct_answer": f"The section focuses on explaining '{concept}'.",
311
- "difficulty": difficulty,
312
- "explanation": "Try to identify the main ideas when studying.",
313
- "concept": concept
314
  })
315
-
316
- return questions[:count]
317
- except Exception as e:
318
- print(f"Question generation error: {str(e)}")
319
- # Return fallback questions
320
- return [
321
- {
322
- "id": generate_id("fallback_1"),
323
- "question_text": "What is the main topic of this study material?",
324
  "question_type": "short_answer",
325
  "options": None,
326
- "correct_answer": "The main topic is what the material focuses on.",
327
- "difficulty": difficulty,
328
- "explanation": "Review the material to identify the main topic.",
329
- "concept": "Main Topic"
330
- }
331
- ]
332
-
333
- # API Endpoints
334
- @app.get("/")
335
- async def root():
336
- return {"message": "StudyFlow AI API is running", "status": "healthy"}
337
-
338
- @app.get("/health")
339
- async def health_check():
340
- return {"status": "healthy", "timestamp": datetime.now().isoformat()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
 
342
  @app.post("/api/process-content")
343
  async def process_content(
@@ -346,18 +310,22 @@ async def process_content(
346
  title: str = Form(...),
347
  content: str = Form(None),
348
  file: UploadFile = File(None),
349
- youtube_url: str = Form(None)
 
 
 
350
  ):
351
- """Process uploaded content and generate REAL questions"""
352
-
353
- print(f"Processing: type={content_type}, difficulty={difficulty}, title={title}")
354
 
 
355
  text_content = ""
 
 
 
356
 
357
  try:
358
- if content_type == "text" and content:
359
- text_content = content[:15000]
360
- print(f"Text length: {len(text_content)} chars")
361
 
362
  elif content_type == "pdf" and file:
363
  with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
@@ -365,175 +333,133 @@ async def process_content(
365
  temp_file.write(content_bytes)
366
  temp_file_path = temp_file.name
367
 
368
- text_content = extract_text_from_pdf(temp_file_path)
369
  os.unlink(temp_file_path)
370
- print(f"PDF text length: {len(text_content)} chars")
 
 
 
 
 
 
 
 
 
 
 
371
 
372
  elif content_type == "youtube" and youtube_url:
373
- text_content = extract_text_from_youtube(youtube_url)
374
- print(f"YouTube transcript length: {len(text_content)} chars")
 
 
 
 
 
 
 
 
375
 
376
- if len(text_content) < 50:
377
- raise HTTPException(status_code=400, detail=f"Content too short ({len(text_content)} chars). Minimum 50 chars required.")
378
 
379
- # Generate questions based on content
380
- questions = generate_questions_from_content(text_content, difficulty, 5)
381
- concepts = extract_key_concepts(text_content, 8)
382
- print(f"Generated {len(questions)} questions, {len(concepts)} concepts")
383
 
384
- # Create session
385
- session_id = generate_id(title)
386
- content_hash = hashlib.md5(text_content.encode()).hexdigest()
 
 
 
 
 
 
387
 
388
- # Try to save to database, fallback to memory if needed
389
- try:
390
- conn = sqlite3.connect(DB_PATH)
391
- cursor = conn.cursor()
392
-
393
- # Check if session exists
394
- cursor.execute("SELECT id FROM sessions WHERE content_hash = ?", (content_hash,))
395
- existing = cursor.fetchone()
396
-
397
- if existing:
398
- session_id = existing[0]
399
- # Clear old questions
400
- cursor.execute("DELETE FROM questions WHERE session_id = ?", (session_id,))
401
-
402
- # Save session
403
  cursor.execute(
404
- """INSERT OR REPLACE INTO sessions
405
- (id, title, content_type, difficulty, content_hash, raw_content, last_accessed)
406
- VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)""",
407
- (session_id, title, content_type, difficulty, content_hash, text_content[:5000])
408
  )
409
-
410
- # Save questions
411
- for q in questions:
412
- cursor.execute(
413
- """INSERT INTO questions
414
- (id, session_id, question_text, question_type, options, correct_answer, difficulty, explanation)
415
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
416
- (q["id"], session_id, q["question_text"], q["question_type"],
417
- q.get("options"), q["correct_answer"], q["difficulty"], q.get("explanation", ""))
418
- )
419
-
420
- conn.commit()
421
- conn.close()
422
- except Exception as db_error:
423
- print(f"Database error (non-fatal): {str(db_error)}")
424
- # Continue even if database fails - we still have the questions
425
 
426
  return {
427
  "session_id": session_id,
428
- "is_existing": False,
429
- "question_count": len(questions)
 
430
  }
431
 
432
  except HTTPException:
433
  raise
434
  except Exception as e:
435
- print(f"Error in process_content: {str(e)}")
436
  raise HTTPException(status_code=500, detail=str(e))
437
 
438
  @app.get("/api/session/{session_id}")
439
  async def get_session(session_id: str):
440
  """Get session with all materials"""
441
- try:
442
- conn = sqlite3.connect(DB_PATH)
443
- conn.row_factory = sqlite3.Row
444
- cursor = conn.cursor()
445
-
446
- cursor.execute("SELECT * FROM sessions WHERE id = ?", (session_id,))
447
- session = cursor.fetchone()
448
-
449
- if not session:
450
- # Return mock session for testing
451
- return {
452
- "session": {
453
- "id": session_id,
454
- "title": "Sample Session",
455
- "content_type": "text",
456
- "difficulty": "medium"
457
- },
458
- "materials": {
459
- "questions": [
460
- {
461
- "id": "sample_q1",
462
- "question_text": "What is the main concept being studied?",
463
- "question_type": "short_answer",
464
- "correct_answer": "The main concept is what you're learning about.",
465
- "difficulty": "medium",
466
- "explanation": "Review your material for the specific answer."
467
- }
468
- ],
469
- "flashcards": []
470
- },
471
- "summary": {"question_count": 1, "flashcard_count": 0},
472
- "performance": {"total_questions": 1, "correct_answers": 0, "accuracy": 0}
473
- }
474
-
475
- cursor.execute("SELECT * FROM questions WHERE session_id = ?", (session_id,))
476
- questions = [dict(row) for row in cursor.fetchall()]
477
-
478
- cursor.execute("SELECT * FROM flashcards WHERE session_id = ?", (session_id,))
479
- flashcards = [dict(row) for row in cursor.fetchall()]
480
-
481
- total_questions = len(questions)
482
- correct_answers = sum(1 for q in questions if q.get("is_correct") == 1)
483
- accuracy = round((correct_answers / total_questions * 100) if total_questions > 0 else 0, 1)
484
-
485
  conn.close()
486
-
487
- return {
488
- "session": dict(session),
489
- "materials": {
490
- "questions": questions,
491
- "flashcards": flashcards,
492
- "notes": [],
493
- "highlights": []
494
- },
495
- "summary": {
496
- "question_count": total_questions,
497
- "flashcard_count": len(flashcards)
498
- },
499
- "performance": {
500
- "total_questions": total_questions,
501
- "correct_answers": correct_answers,
502
- "accuracy": accuracy
503
- }
 
 
 
 
504
  }
505
- except Exception as e:
506
- print(f"Error getting session: {str(e)}")
507
- raise HTTPException(status_code=500, detail=str(e))
508
 
509
  @app.get("/api/user/sessions")
510
  async def get_user_sessions():
511
  """Get all user sessions"""
512
- try:
513
- conn = sqlite3.connect(DB_PATH)
514
- conn.row_factory = sqlite3.Row
515
- cursor = conn.cursor()
516
-
517
- cursor.execute("SELECT * FROM sessions ORDER BY last_accessed DESC")
518
- sessions = [dict(row) for row in cursor.fetchall()]
519
-
520
- for session in sessions:
521
- cursor.execute(
522
- "SELECT COUNT(*), SUM(is_correct) FROM questions WHERE session_id = ?",
523
- (session["id"],)
524
- )
525
- result = cursor.fetchone()
526
- total = result[0] or 0
527
- correct = result[1] or 0
528
- accuracy = round((correct / total * 100) if total > 0 else 0, 1)
529
-
530
- session["performance"] = {"total": total, "correct": correct, "accuracy": accuracy}
531
-
532
- conn.close()
533
- return {"sessions": sessions}
534
- except Exception as e:
535
- print(f"Error getting sessions: {str(e)}")
536
- return {"sessions": []}
537
 
538
  @app.post("/api/submit-answer")
539
  async def submit_answer(
@@ -543,80 +469,52 @@ async def submit_answer(
543
  time_spent: int = Form(0)
544
  ):
545
  """Submit an answer for evaluation"""
546
- try:
547
- conn = sqlite3.connect(DB_PATH)
548
- cursor = conn.cursor()
549
-
550
- cursor.execute("SELECT correct_answer, question_type FROM questions WHERE id = ? AND session_id = ?",
551
- (question_id, session_id))
552
- result = cursor.fetchone()
553
-
554
- if not result:
555
- # Return default success for demo
556
- return {
557
- "is_correct": True,
558
- "correct_answer": "Sample answer",
559
- "feedback": "Answer recorded!"
560
- }
561
-
562
- correct_answer = result[0]
563
- question_type = result[1]
564
-
565
- # Evaluate answer
566
- is_correct = 0
567
-
568
- if question_type == "multiple_choice":
569
- is_correct = 1 if user_answer.strip() == correct_answer.strip() else 0
570
- elif question_type == "true_false":
571
- is_correct = 1 if user_answer.strip().lower() == correct_answer.strip().lower() else 0
572
- elif question_type == "fill_blank":
573
- user_clean = user_answer.strip().lower()
574
- correct_clean = correct_answer.strip().lower()
575
- is_correct = 1 if (user_clean == correct_clean or correct_clean in user_clean or user_clean in correct_clean) else 0
576
- elif question_type == "short_answer":
577
- user_clean = user_answer.strip().lower()
578
- correct_clean = correct_answer.strip().lower()
579
- key_words = re.findall(r'\b[a-z]{4,}\b', correct_clean)
580
- matched = sum(1 for word in key_words if word in user_clean)
581
- is_correct = 1 if matched >= len(key_words) * 0.4 else 0
582
-
583
- cursor.execute(
584
- "UPDATE questions SET user_answer = ?, is_correct = ?, time_spent = ? WHERE id = ? AND session_id = ?",
585
- (user_answer, is_correct, time_spent, question_id, session_id)
586
- )
587
-
588
- conn.commit()
589
  conn.close()
590
-
591
- return {
592
- "is_correct": bool(is_correct),
593
- "correct_answer": correct_answer,
594
- "feedback": "Correct! Great job!" if is_correct else f"The correct answer is: {correct_answer}"
595
- }
596
- except Exception as e:
597
- print(f"Error submitting answer: {str(e)}")
598
- return {
599
- "is_correct": True,
600
- "correct_answer": "Answer recorded",
601
- "feedback": "Your answer has been recorded!"
602
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
 
604
- @app.delete("/api/session/{session_id}")
605
- async def delete_session(session_id: str):
606
- """Delete a session"""
607
- try:
608
- conn = sqlite3.connect(DB_PATH)
609
- cursor = conn.cursor()
610
- cursor.execute("DELETE FROM sessions WHERE id = ?", (session_id,))
611
- conn.commit()
612
- conn.close()
613
- return {"message": "Session deleted"}
614
- except Exception as e:
615
- print(f"Error deleting session: {str(e)}")
616
- return {"message": "Session deleted"}
617
 
618
  if __name__ == "__main__":
619
  import uvicorn
620
- print("🚀 Starting StudyFlow AI Backend...")
621
- print(f"📁 Database path: {DB_PATH}")
622
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  """
2
+ StudyFlow AI Backend - AI-Powered Question Generation
3
+ Uses Hugging Face Inference API for intelligent question generation
4
  """
5
  import os
6
  import json
 
8
  import hashlib
9
  import tempfile
10
  import re
11
+ import requests
12
  from datetime import datetime
13
+ from typing import List, Dict, Optional
 
14
  from fastapi import FastAPI, UploadFile, File, Form, HTTPException
15
  from fastapi.responses import JSONResponse, HTMLResponse
16
  from fastapi.middleware.cors import CORSMiddleware
17
+ from fastapi.staticfiles import StaticFiles
18
  import PyPDF2
19
  from youtube_transcript_api import YouTubeTranscriptApi
20
 
21
  # Initialize FastAPI
22
+ app = FastAPI(title="StudyFlow AI", version="3.0.0")
23
 
24
  # CORS middleware
25
  app.add_middleware(
 
30
  allow_headers=["*"],
31
  )
32
 
33
+ # Hugging Face API configuration
34
+ HF_API_TOKEN = os.environ.get("HF_API_TOKEN", "")
35
+ HF_API_URL = "https://api-inference.huggingface.co/models/mistralai/Mistral-7B-Instruct-v0.2"
 
 
36
 
37
+ # Database setup
38
+ DB_PATH = "/data/studyflow.db" if os.path.exists("/data") else "studyflow.db"
39
 
40
  def init_db():
41
  """Initialize SQLite database"""
42
+ conn = sqlite3.connect(DB_PATH)
43
+ cursor = conn.cursor()
44
+
45
+ # Sessions table with page selections
46
+ cursor.execute('''
47
+ CREATE TABLE IF NOT EXISTS sessions (
48
+ id TEXT PRIMARY KEY,
49
+ title TEXT NOT NULL,
50
+ content_type TEXT NOT NULL,
51
+ difficulty TEXT NOT NULL,
52
+ content_hash TEXT,
53
+ selected_pages TEXT,
54
+ total_pages INTEGER,
55
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
56
+ last_accessed TIMESTAMP DEFAULT CURRENT_TIMESTAMP
57
+ )
58
+ ''')
59
+
60
+ # Questions table
61
+ cursor.execute('''
62
+ CREATE TABLE IF NOT EXISTS questions (
63
+ id TEXT PRIMARY KEY,
64
+ session_id TEXT NOT NULL,
65
+ question_text TEXT NOT NULL,
66
+ question_type TEXT NOT NULL,
67
+ options TEXT,
68
+ correct_answer TEXT NOT NULL,
69
+ difficulty TEXT NOT NULL,
70
+ explanation TEXT,
71
+ user_answer TEXT,
72
+ is_correct INTEGER DEFAULT 0,
73
+ time_spent INTEGER DEFAULT 0,
74
+ page_reference INTEGER,
75
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
76
+ FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
77
+ )
78
+ ''')
79
+
80
+ # Pages table for PDF page content
81
+ cursor.execute('''
82
+ CREATE TABLE IF NOT EXISTS pages (
83
+ id TEXT PRIMARY KEY,
84
+ session_id TEXT NOT NULL,
85
+ page_number INTEGER NOT NULL,
86
+ content TEXT NOT NULL,
87
+ FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
88
+ )
89
+ ''')
90
+
91
+ conn.commit()
92
+ conn.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ init_db()
 
 
95
 
96
  def generate_id(text: str = None):
97
  """Generate a unique ID"""
 
100
  return hashlib.md5(text.encode()).hexdigest()[:12]
101
  return str(uuid.uuid4())[:12]
102
 
103
+ def extract_text_from_pdf(file_path: str) -> Dict[int, str]:
104
+ """Extract text from PDF file and return pages dictionary"""
105
+ pages_text = {}
106
  try:
107
  with open(file_path, 'rb') as file:
108
  pdf_reader = PyPDF2.PdfReader(file)
109
+ for page_num, page in enumerate(pdf_reader.pages, start=1):
110
+ page_text = page.extract_text()
111
+ if page_text and len(page_text.strip()) > 50:
112
+ pages_text[page_num] = page_text.strip()
113
+ return pages_text
 
 
 
 
 
114
  except Exception as e:
115
  print(f"PDF extraction error: {str(e)}")
116
+ return {}
117
 
118
  def extract_text_from_youtube(url: str) -> str:
119
  """Extract transcript from YouTube video"""
 
126
  return ""
127
 
128
  transcript = YouTubeTranscriptApi.get_transcript(video_id)
129
+ text = " ".join([entry['text'] for entry in transcript])
130
+ return text
131
  except Exception as e:
132
  print(f"YouTube extraction error: {str(e)}")
133
  return ""
134
 
135
+ def call_hf_api(prompt: str, max_length: int = 500) -> Optional[str]:
136
+ """Call Hugging Face API for AI-powered question generation"""
137
+ if not HF_API_TOKEN:
138
+ return None
139
+
140
  try:
141
+ headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
142
+ payload = {
143
+ "inputs": prompt,
144
+ "parameters": {
145
+ "max_new_tokens": max_length,
146
+ "temperature": 0.7,
147
+ "top_p": 0.9,
148
+ "do_sample": True
149
+ }
150
+ }
151
+ response = requests.post(HF_API_URL, headers=headers, json=payload, timeout=30)
152
+ if response.status_code == 200:
153
+ result = response.json()
154
+ return result[0].get("generated_text", "")
155
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  except Exception as e:
157
+ print(f"HF API error: {str(e)}")
158
+ return None
159
 
160
+ def generate_questions_with_ai(content: str, difficulty: str, count: int, page_ref: int = None) -> List[Dict]:
161
+ """Generate intelligent questions using AI"""
162
+
163
+ # Build prompt for AI
164
+ difficulty_prompts = {
165
+ "easy": "Generate basic recall and definition questions that test understanding of key terms and simple facts.",
166
+ "medium": "Generate conceptual questions that test understanding of relationships, causes, effects, and comparisons.",
167
+ "hard": "Generate analytical questions that require critical thinking, application, evaluation, and synthesis of ideas."
168
+ }
169
+
170
+ prompt = f"""You are an expert educator creating study questions. Based on the following text, generate {count} {difficulty}-level questions.
171
+
172
+ {difficulty_prompts[difficulty]}
173
+
174
+ For each question, provide:
175
+ 1. The question text
176
+ 2. Question type (multiple_choice, true_false, or short_answer)
177
+ 3. For multiple choice: 4 options (A, B, C, D) with one correct
178
+ 4. For true/false: the correct answer
179
+ 5. For short answer: a model answer
180
+ 6. A brief explanation of why the answer is correct
181
+
182
+ Format your response as JSON array:
183
+ [
184
+ {{
185
+ "text": "question text",
186
+ "type": "multiple_choice",
187
+ "options": ["option1", "option2", "option3", "option4"],
188
+ "correct_answer": "the correct option text",
189
+ "explanation": "explanation here"
190
+ }}
191
+ ]
192
+
193
+ TEXT CONTENT:
194
+ {content[:3000]}
195
+
196
+ Generate {count} questions in JSON format:"""
197
+
198
+ ai_response = call_hf_api(prompt, 2000)
199
+
200
+ if ai_response:
201
+ try:
202
+ # Extract JSON from response
203
+ json_match = re.search(r'\[[\s\S]*\]', ai_response)
204
+ if json_match:
205
+ questions_data = json.loads(json_match.group())
206
+ questions = []
207
+ for i, q_data in enumerate(questions_data[:count]):
208
  questions.append({
209
+ "id": generate_id(f"q_{i}"),
210
+ "question_text": q_data.get("text", ""),
211
+ "question_type": q_data.get("type", "short_answer"),
212
+ "options": json.dumps(q_data.get("options", [])) if q_data.get("options") else None,
213
+ "correct_answer": q_data.get("correct_answer", ""),
214
+ "difficulty": difficulty,
215
+ "explanation": q_data.get("explanation", "Review the material for this answer."),
216
+ "page_reference": page_ref
217
  })
218
+ if questions:
219
+ return questions
220
+ except:
221
+ pass
222
+
223
+ # Fallback to intelligent template-based generation
224
+ return generate_questions_fallback(content, difficulty, count, page_ref)
225
+
226
+ def generate_questions_fallback(content: str, difficulty: str, count: int, page_ref: int = None) -> List[Dict]:
227
+ """Enhanced fallback question generation with better intelligence"""
228
+
229
+ # Extract sentences, facts, and concepts
230
+ sentences = re.split(r'[.!?]+', content)
231
+ sentences = [s.strip() for s in sentences if len(s.strip()) > 30]
232
+
233
+ # Extract numbers/dates
234
+ numbers = re.findall(r'\b\d{4}\b|\b\d+\.\d+\b|\b\d+%\b|\b\d+\s+(?:percent|million|billion|thousand)\b', content)
235
+
236
+ # Extract proper nouns (potential key terms)
237
+ proper_nouns = re.findall(r'\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b', content)
238
+
239
+ questions = []
240
+
241
+ for i in range(min(count, max(len(sentences), 5))):
242
+ sentence = sentences[i % len(sentences)] if sentences else content[:200]
243
+ qid = generate_id(f"q_{i}")
 
 
 
 
 
 
 
 
 
 
 
244
 
245
+ if difficulty == "easy" and len(numbers) > 0:
246
+ # Number-based question
247
+ num = numbers[i % len(numbers)]
 
248
  questions.append({
249
  "id": qid,
250
+ "question_text": f"What is the significance of {num} in the context of this material?",
251
  "question_type": "short_answer",
252
  "options": None,
253
+ "correct_answer": f"The number {num} represents a key figure or measurement discussed in the text.",
254
+ "difficulty": "easy",
255
+ "explanation": "Look for context around this number in the material.",
256
+ "page_reference": page_ref
257
  })
258
+ elif difficulty == "easy" and proper_nouns:
259
+ # Term definition question
260
+ term = proper_nouns[i % len(proper_nouns)]
261
+ questions.append({
262
+ "id": qid,
263
+ "question_text": f"What does the term '{term}' refer to in this material?",
 
 
 
264
  "question_type": "short_answer",
265
  "options": None,
266
+ "correct_answer": f"'{term}' is a key term discussed in the material that relates to the main topic.",
267
+ "difficulty": "easy",
268
+ "explanation": f"Look for definitions or context around '{term}' in the text.",
269
+ "page_reference": page_ref
270
+ })
271
+ elif difficulty == "medium":
272
+ # Conceptual question with multiple choice
273
+ words = sentence.split()
274
+ key_word = next((w for w in words if len(w) > 5), "the concept")
275
+ options = [
276
+ f"The main idea about {key_word} is clearly explained",
277
+ f"A minor detail mentioned in passing",
278
+ f"An unrelated example provided for context",
279
+ f"The conclusion drawn from the discussion"
280
+ ]
281
+ questions.append({
282
+ "id": qid,
283
+ "question_text": f"Based on the text: \"{sentence[:150]}...\" What is the main idea being conveyed?",
284
+ "question_type": "multiple_choice",
285
+ "options": json.dumps(options),
286
+ "correct_answer": options[0],
287
+ "difficulty": "medium",
288
+ "explanation": f"The text emphasizes {key_word} as an important concept.",
289
+ "page_reference": page_ref
290
+ })
291
+ else:
292
+ # Analysis question
293
+ questions.append({
294
+ "id": qid,
295
+ "question_text": f"Analyze the following statement and explain its implications: \"{sentence[:200]}...\"",
296
+ "question_type": "short_answer",
297
+ "options": None,
298
+ "correct_answer": f"This statement suggests that {sentence[:100]} which has important implications for understanding the broader context.",
299
+ "difficulty": "hard",
300
+ "explanation": "Critical analysis requires considering causes, effects, and connections to other concepts.",
301
+ "page_reference": page_ref
302
+ })
303
+
304
+ return questions
305
 
306
  @app.post("/api/process-content")
307
  async def process_content(
 
310
  title: str = Form(...),
311
  content: str = Form(None),
312
  file: UploadFile = File(None),
313
+ youtube_url: str = Form(None),
314
+ selected_pages: str = Form(None), # JSON string of selected page numbers
315
+ time_start: float = Form(None), # For YouTube time selection
316
+ time_end: float = Form(None)
317
  ):
318
+ """Process uploaded content with page/segment selection"""
 
 
319
 
320
+ session_id = generate_id(title)
321
  text_content = ""
322
+ pages_dict = {}
323
+ total_pages = 0
324
+ selected_pages_list = []
325
 
326
  try:
327
+ if content_type == "text":
328
+ text_content = content[:20000] if content else ""
 
329
 
330
  elif content_type == "pdf" and file:
331
  with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
 
333
  temp_file.write(content_bytes)
334
  temp_file_path = temp_file.name
335
 
336
+ pages_dict = extract_text_from_pdf(temp_file_path)
337
  os.unlink(temp_file_path)
338
+ total_pages = len(pages_dict)
339
+
340
+ # Parse selected pages
341
+ if selected_pages:
342
+ selected_pages_list = json.loads(selected_pages)
343
+ else:
344
+ selected_pages_list = list(pages_dict.keys())
345
+
346
+ # Combine selected pages into text
347
+ for page_num in selected_pages_list:
348
+ if page_num in pages_dict:
349
+ text_content += f"\n--- Page {page_num} ---\n{pages_dict[page_num]}\n"
350
 
351
  elif content_type == "youtube" and youtube_url:
352
+ full_transcript = extract_text_from_youtube(youtube_url)
353
+ # Handle time selection
354
+ if time_start is not None and time_end is not None:
355
+ # Would need timestamp-based transcript filtering
356
+ text_content = full_transcript
357
+ else:
358
+ text_content = full_transcript
359
+
360
+ if len(text_content) < 100:
361
+ raise HTTPException(status_code=400, detail="Content too short. Please provide at least 100 characters.")
362
 
363
+ # Generate questions
364
+ questions = generate_questions_with_ai(text_content, difficulty, 5, None)
365
 
366
+ # Save to database
367
+ conn = sqlite3.connect(DB_PATH)
368
+ cursor = conn.cursor()
 
369
 
370
+ # Save session
371
+ cursor.execute(
372
+ """INSERT OR REPLACE INTO sessions
373
+ (id, title, content_type, difficulty, selected_pages, total_pages, last_accessed)
374
+ VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)""",
375
+ (session_id, title, content_type, difficulty,
376
+ json.dumps(selected_pages_list) if selected_pages_list else None,
377
+ total_pages)
378
+ )
379
 
380
+ # Save pages
381
+ for page_num, page_content in pages_dict.items():
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  cursor.execute(
383
+ "INSERT OR REPLACE INTO pages (id, session_id, page_number, content) VALUES (?, ?, ?, ?)",
384
+ (generate_id(f"page_{page_num}"), session_id, page_num, page_content[:5000])
 
 
385
  )
386
+
387
+ # Save questions
388
+ for q in questions:
389
+ cursor.execute(
390
+ """INSERT INTO questions
391
+ (id, session_id, question_text, question_type, options, correct_answer, difficulty, explanation, page_reference)
392
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
393
+ (q["id"], session_id, q["question_text"], q["question_type"],
394
+ q.get("options"), q["correct_answer"], q["difficulty"], q.get("explanation", ""),
395
+ q.get("page_reference"))
396
+ )
397
+
398
+ conn.commit()
399
+ conn.close()
 
 
400
 
401
  return {
402
  "session_id": session_id,
403
+ "question_count": len(questions),
404
+ "total_pages": total_pages,
405
+ "selected_pages": selected_pages_list
406
  }
407
 
408
  except HTTPException:
409
  raise
410
  except Exception as e:
411
+ print(f"Error: {str(e)}")
412
  raise HTTPException(status_code=500, detail=str(e))
413
 
414
  @app.get("/api/session/{session_id}")
415
  async def get_session(session_id: str):
416
  """Get session with all materials"""
417
+ conn = sqlite3.connect(DB_PATH)
418
+ conn.row_factory = sqlite3.Row
419
+ cursor = conn.cursor()
420
+
421
+ cursor.execute("SELECT * FROM sessions WHERE id = ?", (session_id,))
422
+ session = cursor.fetchone()
423
+
424
+ if not session:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  conn.close()
426
+ raise HTTPException(status_code=404, detail="Session not found")
427
+
428
+ cursor.execute("SELECT * FROM questions WHERE session_id = ?", (session_id,))
429
+ questions = [dict(row) for row in cursor.fetchall()]
430
+
431
+ cursor.execute("SELECT * FROM pages WHERE session_id = ? ORDER BY page_number", (session_id,))
432
+ pages = [dict(row) for row in cursor.fetchall()]
433
+
434
+ total_questions = len(questions)
435
+ correct_answers = sum(1 for q in questions if q.get("is_correct") == 1)
436
+ accuracy = round((correct_answers / total_questions * 100) if total_questions > 0 else 0, 1)
437
+
438
+ conn.close()
439
+
440
+ return {
441
+ "session": dict(session),
442
+ "pages": pages,
443
+ "questions": questions,
444
+ "performance": {
445
+ "total_questions": total_questions,
446
+ "correct_answers": correct_answers,
447
+ "accuracy": accuracy
448
  }
449
+ }
 
 
450
 
451
  @app.get("/api/user/sessions")
452
  async def get_user_sessions():
453
  """Get all user sessions"""
454
+ conn = sqlite3.connect(DB_PATH)
455
+ conn.row_factory = sqlite3.Row
456
+ cursor = conn.cursor()
457
+
458
+ cursor.execute("SELECT * FROM sessions ORDER BY last_accessed DESC")
459
+ sessions = [dict(row) for row in cursor.fetchall()]
460
+
461
+ conn.close()
462
+ return {"sessions": sessions}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
 
464
  @app.post("/api/submit-answer")
465
  async def submit_answer(
 
469
  time_spent: int = Form(0)
470
  ):
471
  """Submit an answer for evaluation"""
472
+ conn = sqlite3.connect(DB_PATH)
473
+ cursor = conn.cursor()
474
+
475
+ cursor.execute("SELECT correct_answer, question_type FROM questions WHERE id = ? AND session_id = ?",
476
+ (question_id, session_id))
477
+ result = cursor.fetchone()
478
+
479
+ if not result:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  conn.close()
481
+ return {"is_correct": True, "feedback": "Answer recorded!"}
482
+
483
+ correct_answer = result[0]
484
+ question_type = result[1]
485
+
486
+ # Evaluate answer
487
+ is_correct = 0
488
+ if question_type == "multiple_choice":
489
+ is_correct = 1 if user_answer.strip() == correct_answer.strip() else 0
490
+ elif question_type == "true_false":
491
+ is_correct = 1 if user_answer.strip().lower() == correct_answer.strip().lower() else 0
492
+ else:
493
+ # Smart evaluation for short answers
494
+ user_lower = user_answer.strip().lower()
495
+ correct_lower = correct_answer.strip().lower()
496
+ keywords = re.findall(r'\b[a-z]{4,}\b', correct_lower)
497
+ matches = sum(1 for kw in keywords if kw in user_lower)
498
+ is_correct = 1 if matches >= len(keywords) * 0.3 or len(user_lower) > 40 else 0
499
+
500
+ cursor.execute(
501
+ "UPDATE questions SET user_answer = ?, is_correct = ?, time_spent = ? WHERE id = ?",
502
+ (user_answer, is_correct, time_spent, question_id)
503
+ )
504
+
505
+ conn.commit()
506
+ conn.close()
507
+
508
+ return {
509
+ "is_correct": bool(is_correct),
510
+ "correct_answer": correct_answer,
511
+ "feedback": "Correct!" if is_correct else f"The correct answer is: {correct_answer}"
512
+ }
513
 
514
+ @app.get("/health")
515
+ async def health_check():
516
+ return {"status": "healthy", "timestamp": datetime.now().isoformat()}
 
 
 
 
 
 
 
 
 
 
517
 
518
  if __name__ == "__main__":
519
  import uvicorn
 
 
520
  uvicorn.run(app, host="0.0.0.0", port=7860)