andevs commited on
Commit
20517a8
·
verified ·
1 Parent(s): 881306c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +592 -1099
app.py CHANGED
@@ -1,33 +1,26 @@
1
  """
2
- StudyFlow AI Professional Backend
3
- Complete with storage, personalization, and enhanced features
4
  """
 
 
 
 
 
 
 
5
 
6
- from fastapi import FastAPI, File, UploadFile, Form, HTTPException, BackgroundTasks, Request
 
7
  from fastapi.middleware.cors import CORSMiddleware
8
- from fastapi.responses import JSONResponse
9
- from datetime import datetime, timedelta
10
- import json
11
  import PyPDF2
12
- import io
13
- import re
14
- from typing import List, Dict, Any, Optional
15
- import os
16
- import uuid
17
- import sqlite3
18
- from contextlib import contextmanager
19
- import requests
20
  from youtube_transcript_api import YouTubeTranscriptApi
21
- from collections import defaultdict
22
- import hashlib
23
 
24
- app = FastAPI(
25
- title="StudyFlow AI Professional",
26
- description="Complete AI learning assistant with persistent storage",
27
- version="2.5.0"
28
- )
29
 
30
- # Enable CORS
31
  app.add_middleware(
32
  CORSMiddleware,
33
  allow_origins=["*"],
@@ -41,1134 +34,634 @@ DB_PATH = "/data/studyflow.db" if os.path.exists("/data") else "studyflow.db"
41
 
42
  def init_db():
43
  """Initialize SQLite database"""
44
- with get_db() as conn:
45
- cursor = conn.cursor()
46
-
47
- # Sessions table
48
- cursor.execute('''
49
- CREATE TABLE IF NOT EXISTS sessions (
50
- id TEXT PRIMARY KEY,
51
- user_id TEXT,
52
- title TEXT,
53
- filename TEXT,
54
- content_type TEXT,
55
- content_hash TEXT,
56
- difficulty TEXT,
57
- created_at TIMESTAMP,
58
- updated_at TIMESTAMP,
59
- study_time INTEGER DEFAULT 0,
60
- is_active INTEGER DEFAULT 1
61
- )
62
- ''')
63
-
64
- # Questions table
65
- cursor.execute('''
66
- CREATE TABLE IF NOT EXISTS questions (
67
- id TEXT PRIMARY KEY,
68
- session_id TEXT,
69
- question_type TEXT,
70
- question_text TEXT,
71
- options TEXT,
72
- correct_answer TEXT,
73
- explanation TEXT,
74
- difficulty TEXT,
75
- concept TEXT,
76
- tags TEXT,
77
- created_at TIMESTAMP,
78
- FOREIGN KEY (session_id) REFERENCES sessions(id)
79
- )
80
- ''')
81
-
82
- # User answers table
83
- cursor.execute('''
84
- CREATE TABLE IF NOT EXISTS user_answers (
85
- id TEXT PRIMARY KEY,
86
- session_id TEXT,
87
- question_id TEXT,
88
- user_answer TEXT,
89
- is_correct INTEGER,
90
- time_spent INTEGER,
91
- created_at TIMESTAMP,
92
- FOREIGN KEY (session_id) REFERENCES sessions(id),
93
- FOREIGN KEY (question_id) REFERENCES questions(id)
94
- )
95
- ''')
96
-
97
- # Flashcards table
98
- cursor.execute('''
99
- CREATE TABLE IF NOT EXISTS flashcards (
100
- id TEXT PRIMARY KEY,
101
- session_id TEXT,
102
- front TEXT,
103
- back TEXT,
104
- category TEXT,
105
- difficulty TEXT,
106
- ease_factor REAL DEFAULT 2.5,
107
- last_reviewed TIMESTAMP,
108
- next_review TIMESTAMP,
109
- created_at TIMESTAMP,
110
- FOREIGN KEY (session_id) REFERENCES sessions(id)
111
- )
112
- ''')
113
-
114
- # Notes table
115
- cursor.execute('''
116
- CREATE TABLE IF NOT EXISTS notes (
117
- id TEXT PRIMARY KEY,
118
- session_id TEXT,
119
- title TEXT,
120
- content TEXT,
121
- tags TEXT,
122
- created_at TIMESTAMP,
123
- updated_at TIMESTAMP,
124
- FOREIGN KEY (session_id) REFERENCES sessions(id)
125
- )
126
- ''')
127
-
128
- # User profiles table
129
- cursor.execute('''
130
- CREATE TABLE IF NOT EXISTS user_profiles (
131
- user_id TEXT PRIMARY KEY,
132
- preferences TEXT,
133
- strengths TEXT,
134
- weaknesses TEXT,
135
- learning_patterns TEXT,
136
- created_at TIMESTAMP,
137
- updated_at TIMESTAMP
138
- )
139
- ''')
140
-
141
- # Highlights table
142
- cursor.execute('''
143
- CREATE TABLE IF NOT EXISTS highlights (
144
- id TEXT PRIMARY KEY,
145
- session_id TEXT,
146
- user_id TEXT,
147
- text TEXT,
148
- context TEXT,
149
- note TEXT,
150
- created_at TIMESTAMP,
151
- FOREIGN KEY (session_id) REFERENCES sessions(id)
152
- )
153
- ''')
154
-
155
- conn.commit()
156
-
157
- @contextmanager
158
- def get_db():
159
- """Database connection context manager"""
160
  conn = sqlite3.connect(DB_PATH)
161
- conn.row_factory = sqlite3.Row
162
- try:
163
- yield conn
164
- finally:
165
- conn.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
  # Initialize database
168
  init_db()
169
 
170
- # Enhanced vocabulary with internet fallback
171
- ENHANCED_VOCABULARY = {
172
- "algorithm": {
173
- "definition": "A set of step-by-step instructions to solve a problem or accomplish a task",
174
- "example": "Search algorithms like binary search help find data efficiently in sorted arrays",
175
- "contexts": ["computer science", "mathematics", "problem solving", "data structures"],
176
- "difficulty": "intermediate",
177
- "related": ["data structure", "complexity", "optimization"]
178
- },
179
- "hypothesis": {
180
- "definition": "A proposed explanation made on limited evidence as a starting point for further investigation",
181
- "example": "Scientists test their hypothesis through controlled experiments and observation",
182
- "contexts": ["science", "research", "statistics", "experimental design"],
183
- "difficulty": "basic",
184
- "related": ["theory", "experiment", "conclusion", "research"]
185
- },
186
- # Add more comprehensive vocabulary
187
- }
188
 
189
- def get_internet_definition(word: str) -> Optional[Dict[str, Any]]:
190
- """Get definition from internet (fallback)"""
191
  try:
192
- # Try Dictionary API
193
- response = requests.get(f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}", timeout=5)
194
- if response.status_code == 200:
195
- data = response.json()
196
- if data and isinstance(data, list):
197
- first_entry = data[0]
198
- meanings = first_entry.get('meanings', [])
199
- if meanings:
200
- first_meaning = meanings[0]
201
- definitions = first_meaning.get('definitions', [])
202
- if definitions:
203
- return {
204
- "definition": definitions[0].get('definition', 'No definition found'),
205
- "example": definitions[0].get('example', ''),
206
- "source": "dictionaryapi.dev",
207
- "phonetic": first_entry.get('phonetic', '')
208
- }
209
- except:
210
- pass
211
-
212
- return None
213
-
214
- def generate_content_hash(content: str) -> str:
215
- """Generate hash for content to avoid duplicates"""
216
- return hashlib.md5(content.encode()).hexdigest()
217
-
218
- def extract_youtube_id(url: str) -> Optional[str]:
219
- """Extract YouTube video ID from URL"""
220
- patterns = [
221
- r'(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})',
222
- r'youtube\.com\/watch\?.+&v=([a-zA-Z0-9_-]{11})'
223
- ]
224
-
225
- for pattern in patterns:
226
- match = re.search(pattern, url)
227
- if match:
228
- return match.group(1)
229
- return None
230
 
231
- def get_youtube_transcript(video_id: str) -> Optional[str]:
232
- """Get transcript from YouTube video"""
233
  try:
234
- transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
235
- transcript = " ".join([entry['text'] for entry in transcript_list])
236
- return transcript
 
237
  except Exception as e:
238
- print(f"Error getting transcript: {e}")
239
- return None
240
 
241
- def extract_key_concepts(text: str, max_concepts: int = 15) -> List[Dict[str, Any]]:
242
- """Extract key concepts with context and importance"""
243
- # Find capitalized words and phrases
244
- words = re.findall(r'\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b', text)
245
- word_freq = {}
246
-
247
- for word in words:
248
- if len(word) > 3:
249
- word_freq[word] = word_freq.get(word, 0) + 1
250
-
251
- # Get context for each concept
252
- concepts = []
253
- for concept, freq in sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:max_concepts]:
254
- # Find sentences containing the concept
255
- sentences = re.findall(r'[^.!?]*\b' + re.escape(concept) + r'\b[^.!?]*[.!?]', text)
256
-
257
- # Calculate importance based on frequency and position
258
- importance = min(100, freq * 8)
259
-
260
- concepts.append({
261
- "concept": concept,
262
- "frequency": freq,
263
- "context": sentences[0] if sentences else "",
264
- "importance": importance,
265
- "sentences": sentences[:3] # First 3 sentences
266
- })
267
-
268
- return concepts
269
-
270
- def generate_enhanced_questions(text: str, difficulty: str, session_id: str, user_id: str = "default") -> List[Dict[str, Any]]:
271
- """Generate enhanced questions with personalization"""
272
- concepts = extract_key_concepts(text, 8)
273
-
274
- # Get user's weak areas from database
275
- weak_areas = get_user_weak_areas(user_id)
276
 
277
  questions = []
278
-
279
- # Multiple Choice Questions
280
- for i, concept in enumerate(concepts[:4]):
281
- # Check if this concept is a weak area
282
- is_weak_area = any(weak in concept["concept"].lower() for weak in weak_areas)
283
-
284
- question_id = str(uuid.uuid4())
285
- options = [
286
- f"It represents a core principle discussed in the text",
287
- f"It's a technical term requiring specialized knowledge",
288
- f"It's mentioned briefly but not central to the argument",
289
- f"It contradicts other concepts presented"
290
- ]
291
-
292
- # Adjust difficulty
293
- if difficulty == "easy":
294
- correct_idx = 0
295
- elif difficulty == "hard":
296
- correct_idx = 2 if is_weak_area else 1
297
  else:
298
- correct_idx = 1 if is_weak_area else 0
299
-
300
- questions.append({
301
- "id": question_id,
302
- "type": "multiple_choice",
303
- "question": f"Based on the text, what is the role of '{concept['concept']}'?",
304
- "options": options,
305
- "correct_answer": correct_idx,
306
- "explanation": f"This concept appears {concept['frequency']} times. Context: {concept['context'][:150]}",
307
- "difficulty": difficulty,
308
- "concept": concept["concept"],
309
- "tags": ["comprehension", "analysis", "concept"],
310
- "is_weak_area": is_weak_area
311
- })
312
-
313
- # Store question in database
314
- with get_db() as conn:
315
- cursor = conn.cursor()
316
- cursor.execute('''
317
- INSERT INTO questions (id, session_id, question_type, question_text, options,
318
- correct_answer, explanation, difficulty, concept, tags, created_at)
319
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
320
- ''', (
321
- question_id, session_id, "multiple_choice", questions[-1]["question"],
322
- json.dumps(options), str(correct_idx), questions[-1]["explanation"],
323
- difficulty, concept["concept"], json.dumps(questions[-1]["tags"]), datetime.now().isoformat()
324
- ))
325
- conn.commit()
326
-
327
- # True/False Questions
328
- for i in range(3):
329
- question_id = str(uuid.uuid4())
330
- is_true = i % 2 == 0
331
-
332
- questions.append({
333
- "id": question_id,
334
- "type": "true_false",
335
- "question": f"The text primarily focuses on practical applications rather than theoretical concepts.",
336
- "options": ["True", "False"],
337
- "correct_answer": 0 if is_true else 1,
338
- "explanation": "This assesses understanding of the text's orientation and focus.",
339
- "difficulty": "medium",
340
- "tags": ["comprehension", "critical_thinking"],
341
- "is_weak_area": False
342
- })
343
-
344
- with get_db() as conn:
345
- cursor = conn.cursor()
346
- cursor.execute('''
347
- INSERT INTO questions (id, session_id, question_type, question_text, options,
348
- correct_answer, explanation, difficulty, concept, tags, created_at)
349
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
350
- ''', (
351
- question_id, session_id, "true_false", questions[-1]["question"],
352
- json.dumps(["True", "False"]), str(questions[-1]["correct_answer"]),
353
- questions[-1]["explanation"], "medium", "General",
354
- json.dumps(questions[-1]["tags"]), datetime.now().isoformat()
355
- ))
356
- conn.commit()
357
-
358
- # Fill in the Blank
359
- if len(text) > 100:
360
- sentences = re.findall(r'[^.!?]{50,150}[.!?]', text)
361
- for i, sentence in enumerate(sentences[:2]):
362
- words = sentence.split()
363
- if len(words) > 5:
364
- blank_idx = random.randint(2, len(words)-3)
365
- blank_word = words[blank_idx]
366
- words[blank_idx] = "______"
367
- blank_sentence = " ".join(words)
368
-
369
- question_id = str(uuid.uuid4())
370
-
371
- questions.append({
372
- "id": question_id,
373
- "type": "fill_blank",
374
- "question": f"Complete the sentence: '{blank_sentence}'",
375
- "correct_answer": blank_word,
376
- "explanation": f"The missing word '{blank_word}' completes the meaning based on context.",
377
- "difficulty": difficulty,
378
- "tags": ["vocabulary", "context"],
379
- "is_weak_area": False
380
- })
381
-
382
- with get_db() as conn:
383
- cursor = conn.cursor()
384
- cursor.execute('''
385
- INSERT INTO questions (id, session_id, question_type, question_text, options,
386
- correct_answer, explanation, difficulty, concept, tags, created_at)
387
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
388
- ''', (
389
- question_id, session_id, "fill_blank", questions[-1]["question"],
390
- json.dumps([]), blank_word, questions[-1]["explanation"],
391
- difficulty, "Context Completion",
392
- json.dumps(questions[-1]["tags"]), datetime.now().isoformat()
393
- ))
394
- conn.commit()
395
 
396
  return questions
397
 
398
- def get_user_weak_areas(user_id: str) -> List[str]:
399
- """Get user's weak areas from database"""
400
- try:
401
- with get_db() as conn:
402
- cursor = conn.cursor()
403
- cursor.execute('''
404
- SELECT question_id, is_correct FROM user_answers
405
- WHERE EXISTS (
406
- SELECT 1 FROM questions q
407
- WHERE q.id = user_answers.question_id
408
- AND q.session_id IN (
409
- SELECT id FROM sessions WHERE user_id = ?
410
- )
411
- )
412
- ORDER BY created_at DESC LIMIT 50
413
- ''', (user_id,))
414
-
415
- results = cursor.fetchall()
416
-
417
- # Get concepts from incorrect answers
418
- weak_concepts = []
419
- for row in results:
420
- if row['is_correct'] == 0: # Incorrect answer
421
- cursor.execute('SELECT concept FROM questions WHERE id = ?', (row['question_id'],))
422
- question = cursor.fetchone()
423
- if question and question['concept']:
424
- weak_concepts.append(question['concept'].lower())
425
-
426
- return list(set(weak_concepts[:10])) # Return unique weak areas
427
- except:
428
- return []
429
-
430
- def update_user_profile(user_id: str, question_id: str, is_correct: bool, concept: str):
431
- """Update user profile based on performance"""
432
- try:
433
- with get_db() as conn:
434
- cursor = conn.cursor()
435
-
436
- # Check if profile exists
437
- cursor.execute('SELECT * FROM user_profiles WHERE user_id = ?', (user_id,))
438
- profile = cursor.fetchone()
439
-
440
- if profile:
441
- # Update existing profile
442
- preferences = json.loads(profile['preferences']) if profile['preferences'] else {}
443
- strengths = json.loads(profile['strengths']) if profile['strengths'] else []
444
- weaknesses = json.loads(profile['weaknesses']) if profile['weaknesses'] else []
445
- patterns = json.loads(profile['learning_patterns']) if profile['learning_patterns'] else {}
446
-
447
- # Update based on performance
448
- if is_correct:
449
- if concept not in strengths:
450
- strengths.append(concept)
451
- if concept in weaknesses:
452
- weaknesses.remove(concept)
453
- else:
454
- if concept not in weaknesses:
455
- weaknesses.append(concept)
456
- if concept in strengths:
457
- strengths.remove(concept)
458
-
459
- # Update patterns
460
- today = datetime.now().strftime("%Y-%m-%d")
461
- if today not in patterns:
462
- patterns[today] = {"correct": 0, "total": 0}
463
- patterns[today]["total"] += 1
464
- if is_correct:
465
- patterns[today]["correct"] += 1
466
-
467
- cursor.execute('''
468
- UPDATE user_profiles
469
- SET strengths = ?, weaknesses = ?, learning_patterns = ?, updated_at = ?
470
- WHERE user_id = ?
471
- ''', (
472
- json.dumps(strengths[:50]),
473
- json.dumps(weaknesses[:50]),
474
- json.dumps(patterns),
475
- datetime.now().isoformat(),
476
- user_id
477
- ))
478
- else:
479
- # Create new profile
480
- strengths = [concept] if is_correct else []
481
- weaknesses = [] if is_correct else [concept]
482
- patterns = {
483
- datetime.now().strftime("%Y-%m-%d"): {
484
- "correct": 1 if is_correct else 0,
485
- "total": 1
486
- }
487
- }
488
-
489
- cursor.execute('''
490
- INSERT INTO user_profiles (user_id, strengths, weaknesses, learning_patterns, created_at, updated_at)
491
- VALUES (?, ?, ?, ?, ?, ?)
492
- ''', (
493
- user_id,
494
- json.dumps(strengths),
495
- json.dumps(weaknesses),
496
- json.dumps(patterns),
497
- datetime.now().isoformat(),
498
- datetime.now().isoformat()
499
- ))
500
-
501
- conn.commit()
502
- except Exception as e:
503
- print(f"Error updating user profile: {e}")
504
 
 
505
  @app.post("/api/process-content")
506
  async def process_content(
507
- request: Request,
508
  content_type: str = Form(...),
 
 
509
  content: str = Form(None),
510
  file: UploadFile = File(None),
511
- youtube_url: str = Form(None),
512
- difficulty: str = Form("medium"),
513
- title: str = Form(None)
514
  ):
515
- """Process any type of content and create a new session"""
516
- try:
517
- # Get user ID from request (for now, use IP as user ID)
518
- user_id = request.client.host if request.client else "anonymous"
519
-
520
- extracted_text = ""
521
- metadata = {}
522
-
523
- if content_type == "text" and content:
524
- extracted_text = content
525
- metadata = {"type": "text", "length": len(content)}
526
-
527
- elif content_type == "pdf" and file:
528
- contents = await file.read()
529
- pdf_file = io.BytesIO(contents)
530
- pdf_reader = PyPDF2.PdfReader(pdf_file)
531
-
532
- text = ""
533
- for i in range(min(20, len(pdf_reader.pages))):
534
- page_text = pdf_reader.pages[i].extract_text()
535
- if page_text:
536
- text += page_text + "\n\n"
537
-
538
- extracted_text = text
539
- metadata = {
540
- "type": "pdf",
541
- "filename": file.filename,
542
- "pages": len(pdf_reader.pages),
543
- "extracted_pages": min(20, len(pdf_reader.pages))
544
- }
545
-
546
- elif content_type == "youtube" and youtube_url:
547
- video_id = extract_youtube_id(youtube_url)
548
- if not video_id:
549
- raise HTTPException(status_code=400, detail="Invalid YouTube URL")
550
-
551
- transcript = get_youtube_transcript(video_id)
552
- if not transcript:
553
- raise HTTPException(status_code=400, detail="Could not extract transcript")
554
-
555
- extracted_text = transcript
556
- metadata = {
557
- "type": "youtube",
558
- "video_id": video_id,
559
- "url": youtube_url
560
- }
561
-
562
- if not extracted_text or len(extracted_text.strip()) < 100:
563
- raise HTTPException(status_code=400, detail="Insufficient content (minimum 100 characters)")
564
-
565
- # Generate content hash
566
- content_hash = generate_content_hash(extracted_text)
567
-
568
- # Check for existing session with same content
569
- with get_db() as conn:
570
- cursor = conn.cursor()
571
- cursor.execute('''
572
- SELECT id, title FROM sessions
573
- WHERE content_hash = ? AND user_id = ? AND is_active = 1
574
- ORDER BY created_at DESC LIMIT 1
575
- ''', (content_hash, user_id))
576
-
577
- existing = cursor.fetchone()
578
- if existing:
579
- return {
580
- "success": True,
581
- "message": "Session already exists",
582
- "session_id": existing['id'],
583
- "title": existing['title'],
584
- "is_existing": True
585
- }
586
-
587
- # Create new session
588
- session_id = str(uuid.uuid4())
589
- session_title = title or metadata.get("filename", "Untitled Session")
590
-
591
- with get_db() as conn:
592
- cursor = conn.cursor()
593
- cursor.execute('''
594
- INSERT INTO sessions (id, user_id, title, filename, content_type,
595
- content_hash, difficulty, created_at, updated_at, is_active)
596
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
597
- ''', (
598
- session_id,
599
- user_id,
600
- session_title,
601
- metadata.get("filename", ""),
602
- content_type,
603
- content_hash,
604
- difficulty,
605
- datetime.now().isoformat(),
606
- datetime.now().isoformat(),
607
- 1
608
- ))
609
- conn.commit()
610
-
611
- # Generate materials in background
612
- background_tasks = BackgroundTasks()
613
- background_tasks.add_task(
614
- generate_session_materials,
615
- session_id,
616
- user_id,
617
- extracted_text,
618
- difficulty,
619
- metadata
620
  )
621
-
 
622
  return JSONResponse(
623
- content={
624
- "success": True,
625
- "session_id": session_id,
626
- "title": session_title,
627
- "message": "Session created. Materials are being generated...",
628
- "metadata": metadata,
629
- "is_existing": False
630
- },
631
- background=background_tasks
632
  )
633
-
634
- except HTTPException:
635
- raise
636
- except Exception as e:
637
- raise HTTPException(status_code=500, detail=f"Processing error: {str(e)}")
638
-
639
- async def generate_session_materials(session_id: str, user_id: str, text: str, difficulty: str, metadata: Dict):
640
- """Generate all study materials for a session"""
641
- try:
642
- # Generate questions
643
- questions = generate_enhanced_questions(text, difficulty, session_id, user_id)
644
-
645
- # Generate flashcards
646
- concepts = extract_key_concepts(text, 20)
647
- flashcards = []
648
-
649
- for concept in concepts[:15]:
650
- flashcard_id = str(uuid.uuid4())
651
- flashcards.append({
652
- "id": flashcard_id,
653
- "front": f"What is '{concept['concept']}'?",
654
- "back": f"A key concept appearing {concept['frequency']} times.\n\nContext: {concept['context'][:200]}",
655
- "category": "Key Concepts",
656
- "difficulty": "medium",
657
- "importance": concept['importance']
658
- })
659
-
660
- # Store in database
661
- with get_db() as conn:
662
- cursor = conn.cursor()
663
- cursor.execute('''
664
- INSERT INTO flashcards (id, session_id, front, back, category,
665
- difficulty, created_at)
666
- VALUES (?, ?, ?, ?, ?, ?, ?)
667
- ''', (
668
- flashcard_id,
669
- session_id,
670
- flashcards[-1]["front"],
671
- flashcards[-1]["back"],
672
- flashcards[-1]["category"],
673
- flashcards[-1]["difficulty"],
674
- datetime.now().isoformat()
675
- ))
676
- conn.commit()
677
-
678
- # Update session to mark as ready
679
- with get_db() as conn:
680
- cursor = conn.cursor()
681
- cursor.execute('''
682
- UPDATE sessions SET updated_at = ? WHERE id = ?
683
- ''', (datetime.now().isoformat(), session_id))
684
- conn.commit()
685
-
686
- except Exception as e:
687
- print(f"Error generating materials: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688
 
689
- @app.post("/api/analyze-text")
690
- async def analyze_text(
691
- text: str = Form(...),
692
- session_id: str = Form(None),
693
- user_id: str = Form("anonymous")
694
- ):
695
- """Analyze selected text and provide explanations"""
696
- try:
697
- # Clean and prepare text
698
- text = text.strip()
699
- if len(text) < 2 or len(text) > 500:
700
- raise HTTPException(status_code=400, detail="Text must be between 2 and 500 characters")
701
-
702
- # Extract potential terms
703
- words = re.findall(r'\b[a-zA-Z]{4,}\b', text)
704
-
705
- # Get definitions
706
- definitions = []
707
- for word in set(words[:5]): # Limit to 5 unique words
708
- word_lower = word.lower()
709
-
710
- # Check enhanced vocabulary first
711
- if word_lower in ENHANCED_VOCABULARY:
712
- definition = ENHANCED_VOCABULARY[word_lower]
713
- definition["word"] = word
714
- definition["source"] = "enhanced_vocabulary"
715
- definitions.append(definition)
716
- else:
717
- # Try internet
718
- internet_def = get_internet_definition(word_lower)
719
- if internet_def:
720
- internet_def["word"] = word
721
- definitions.append(internet_def)
722
- else:
723
- # Generate contextual definition
724
- definitions.append({
725
- "word": word,
726
- "definition": f"In this context, '{word}' likely refers to a concept or term mentioned in the material.",
727
- "example": f"The word '{word}' appears in your selected text.",
728
- "source": "contextual",
729
- "contexts": ["general"]
730
- })
731
-
732
- # Store highlight if session exists
733
- if session_id:
734
- with get_db() as conn:
735
- cursor = conn.cursor()
736
- highlight_id = str(uuid.uuid4())
737
- cursor.execute('''
738
- INSERT INTO highlights (id, session_id, user_id, text, context, created_at)
739
- VALUES (?, ?, ?, ?, ?, ?)
740
- ''', (
741
- highlight_id,
742
- session_id,
743
- user_id,
744
- text,
745
- json.dumps(definitions),
746
- datetime.now().isoformat()
747
- ))
748
- conn.commit()
749
-
750
- # Generate explanation
751
- explanation = generate_text_explanation(text, definitions)
752
-
753
- return {
754
- "success": True,
755
- "text": text,
756
- "definitions": definitions,
757
- "explanation": explanation,
758
- "word_count": len(words),
759
- "suggested_actions": [
760
- "Add to vocabulary list",
761
- "Create flashcard",
762
- "Search for more examples"
763
- ]
764
  }
765
-
766
- except Exception as e:
767
- raise HTTPException(status_code=500, detail=f"Text analysis error: {str(e)}")
768
 
769
- def generate_text_explanation(text: str, definitions: List[Dict]) -> str:
770
- """Generate comprehensive explanation for selected text"""
771
- if not definitions:
772
- return f"The selected text '{text[:50]}...' appears to be a passage from your material. Consider looking up individual terms for more detailed explanations."
773
-
774
- # Build explanation
775
- explanation_parts = ["**Analysis of Selected Text:**\n\n"]
776
-
777
- for def_item in definitions[:3]: # Limit to 3 definitions
778
- word = def_item.get("word", "")
779
- definition = def_item.get("definition", "")
780
- example = def_item.get("example", "")
781
- source = def_item.get("source", "unknown")
782
-
783
- explanation_parts.append(f"**{word}**: {definition}")
784
- if example:
785
- explanation_parts.append(f" *Example*: {example}")
786
- if source != "contextual":
787
- explanation_parts.append(f" *Source*: {source}")
788
- explanation_parts.append("")
789
-
790
- explanation_parts.append("\n**Learning Suggestions:**")
791
- explanation_parts.append("1. Review these terms in context")
792
- explanation_parts.append("2. Create flashcards for retention")
793
- explanation_parts.append("3. Look for related concepts in your material")
794
-
795
- return "\n".join(explanation_parts)
 
 
 
796
 
797
  @app.post("/api/submit-answer")
798
  async def submit_answer(
799
  session_id: str = Form(...),
800
  question_id: str = Form(...),
801
  user_answer: str = Form(...),
802
- time_spent: int = Form(0),
803
- user_id: str = Form("anonymous")
804
  ):
805
- """Submit and evaluate an answer"""
806
- try:
807
- with get_db() as conn:
808
- cursor = conn.cursor()
809
-
810
- # Get question details
811
- cursor.execute('SELECT * FROM questions WHERE id = ?', (question_id,))
812
- question = cursor.fetchone()
813
-
814
- if not question:
815
- raise HTTPException(status_code=404, detail="Question not found")
816
-
817
- # Evaluate answer
818
- is_correct = False
819
- if question['question_type'] == 'multiple_choice':
820
- is_correct = str(user_answer) == question['correct_answer']
821
- elif question['question_type'] == 'true_false':
822
- is_correct = str(user_answer) == question['correct_answer']
823
- elif question['question_type'] == 'fill_blank':
824
- # Simple text comparison for fill blank
825
- correct_lower = question['correct_answer'].lower().strip()
826
- user_lower = user_answer.lower().strip()
827
- is_correct = correct_lower == user_lower or correct_lower in user_lower
828
-
829
- # Store answer
830
- answer_id = str(uuid.uuid4())
831
- cursor.execute('''
832
- INSERT INTO user_answers (id, session_id, question_id, user_answer,
833
- is_correct, time_spent, created_at)
834
- VALUES (?, ?, ?, ?, ?, ?, ?)
835
- ''', (
836
- answer_id,
837
- session_id,
838
- question_id,
839
- user_answer,
840
- 1 if is_correct else 0,
841
- time_spent,
842
- datetime.now().isoformat()
843
- ))
844
-
845
- # Update user profile
846
- update_user_profile(
847
- user_id,
848
- question_id,
849
- is_correct,
850
- question['concept'] or "General"
851
- )
852
-
853
- conn.commit()
854
-
855
- # Get explanation
856
- explanation = question['explanation'] or "No explanation available."
857
-
858
- # Get personalized feedback based on performance
859
- weak_areas = get_user_weak_areas(user_id)
860
- concept = question['concept'] or "this concept"
861
-
862
- personalized_feedback = ""
863
- if is_correct:
864
- if concept.lower() in [w.lower() for w in weak_areas]:
865
- personalized_feedback = f"Great improvement on {concept}, which was previously a weak area!"
866
- else:
867
- personalized_feedback = f"Good understanding of {concept}!"
868
- else:
869
- if concept.lower() in [w.lower() for w in weak_areas]:
870
- personalized_feedback = f"{concept} continues to be challenging. Focus on reviewing related materials."
871
- else:
872
- personalized_feedback = f"{concept} may need more attention. Consider reviewing the relevant section."
873
-
874
- return {
875
- "success": True,
876
- "is_correct": is_correct,
877
- "correct_answer": question['correct_answer'],
878
- "explanation": explanation,
879
- "personalized_feedback": personalized_feedback,
880
- "concept": concept,
881
- "answer_id": answer_id
882
- }
883
-
884
- except Exception as e:
885
- raise HTTPException(status_code=500, detail=f"Answer submission error: {str(e)}")
886
 
887
- @app.get("/api/session/{session_id}")
888
- async def get_session(session_id: str):
889
- """Get session details and materials"""
890
- try:
891
- with get_db() as conn:
892
- cursor = conn.cursor()
893
-
894
- # Get session
895
- cursor.execute('SELECT * FROM sessions WHERE id = ?', (session_id,))
896
- session = cursor.fetchone()
897
-
898
- if not session:
899
- raise HTTPException(status_code=404, detail="Session not found")
900
-
901
- # Get questions
902
- cursor.execute('SELECT * FROM questions WHERE session_id = ? ORDER BY created_at', (session_id,))
903
- questions = [dict(row) for row in cursor.fetchall()]
904
-
905
- # Get flashcards
906
- cursor.execute('SELECT * FROM flashcards WHERE session_id = ? ORDER BY created_at', (session_id,))
907
- flashcards = [dict(row) for row in cursor.fetchall()]
908
-
909
- # Get notes
910
- cursor.execute('SELECT * FROM notes WHERE session_id = ? ORDER BY updated_at DESC', (session_id,))
911
- notes = [dict(row) for row in cursor.fetchall()]
912
-
913
- # Get highlights
914
- cursor.execute('SELECT * FROM highlights WHERE session_id = ? ORDER BY created_at DESC', (session_id,))
915
- highlights = [dict(row) for row in cursor.fetchall()]
916
-
917
- # Get performance statistics
918
- cursor.execute('''
919
- SELECT
920
- COUNT(*) as total_questions,
921
- SUM(CASE WHEN is_correct = 1 THEN 1 ELSE 0 END) as correct_answers,
922
- AVG(time_spent) as avg_time_spent
923
- FROM user_answers
924
- WHERE session_id = ?
925
- ''', (session_id,))
926
-
927
- stats = cursor.fetchone()
928
- performance = {
929
- "total_questions": stats['total_questions'] or 0,
930
- "correct_answers": stats['correct_answers'] or 0,
931
- "accuracy": round((stats['correct_answers'] or 0) / max(1, (stats['total_questions'] or 1)) * 100, 1),
932
- "avg_time_spent": round(stats['avg_time_spent'] or 0, 1)
933
- }
934
-
935
- return {
936
- "success": True,
937
- "session": dict(session),
938
- "materials": {
939
- "questions": questions,
940
- "flashcards": flashcards,
941
- "notes": notes,
942
- "highlights": highlights
943
- },
944
- "performance": performance,
945
- "summary": {
946
- "question_count": len(questions),
947
- "flashcard_count": len(flashcards),
948
- "note_count": len(notes),
949
- "highlight_count": len(highlights)
950
- }
951
- }
952
-
953
- except Exception as e:
954
- raise HTTPException(status_code=500, detail=f"Session retrieval error: {str(e)}")
955
 
956
- @app.get("/api/user/sessions")
957
- async def get_user_sessions(user_id: str = "anonymous"):
958
- """Get all sessions for a user"""
959
- try:
960
- with get_db() as conn:
961
- cursor = conn.cursor()
962
-
963
- cursor.execute('''
964
- SELECT id, title, content_type, difficulty, created_at, updated_at, study_time
965
- FROM sessions
966
- WHERE user_id = ? AND is_active = 1
967
- ORDER BY updated_at DESC
968
- ''', (user_id,))
969
-
970
- sessions = [dict(row) for row in cursor.fetchall()]
971
-
972
- # Get performance for each session
973
- for session in sessions:
974
- cursor.execute('''
975
- SELECT
976
- COUNT(*) as total_answered,
977
- SUM(CASE WHEN is_correct = 1 THEN 1 ELSE 0 END) as correct_answers
978
- FROM user_answers
979
- WHERE session_id = ?
980
- ''', (session['id'],))
981
-
982
- stats = cursor.fetchone()
983
- session['performance'] = {
984
- "answered": stats['total_answered'] or 0,
985
- "correct": stats['correct_answers'] or 0,
986
- "accuracy": round((stats['correct_answers'] or 0) / max(1, (stats['total_answered'] or 1)) * 100, 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
987
  }
988
-
989
- return {
990
- "success": True,
991
- "sessions": sessions,
992
- "total_sessions": len(sessions)
993
- }
994
-
995
- except Exception as e:
996
- raise HTTPException(status_code=500, detail=f"Session list error: {str(e)}")
997
-
998
- @app.post("/api/create-note")
999
- async def create_note(
1000
- session_id: str = Form(...),
1001
- title: str = Form(...),
1002
- content: str = Form(...),
1003
- tags: str = Form("[]")
1004
- ):
1005
- """Create a note for a session"""
1006
- try:
1007
- note_id = str(uuid.uuid4())
1008
-
1009
- with get_db() as conn:
1010
- cursor = conn.cursor()
1011
- cursor.execute('''
1012
- INSERT INTO notes (id, session_id, title, content, tags, created_at, updated_at)
1013
- VALUES (?, ?, ?, ?, ?, ?, ?)
1014
- ''', (
1015
- note_id,
1016
- session_id,
1017
- title,
1018
- content,
1019
- tags,
1020
- datetime.now().isoformat(),
1021
- datetime.now().isoformat()
1022
- ))
1023
- conn.commit()
1024
-
1025
- return {
1026
- "success": True,
1027
- "note_id": note_id,
1028
- "message": "Note created successfully"
1029
  }
1030
-
1031
- except Exception as e:
1032
- raise HTTPException(status_code=500, detail=f"Note creation error: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1033
 
1034
  @app.post("/api/update-study-time")
1035
- async def update_study_time(
1036
- session_id: str = Form(...),
1037
- study_time: int = Form(...)
1038
- ):
1039
  """Update study time for a session"""
1040
- try:
1041
- with get_db() as conn:
1042
- cursor = conn.cursor()
1043
- cursor.execute('''
1044
- UPDATE sessions
1045
- SET study_time = study_time + ?, updated_at = ?
1046
- WHERE id = ?
1047
- ''', (study_time, datetime.now().isoformat(), session_id))
1048
- conn.commit()
1049
-
1050
- return {"success": True}
1051
-
1052
- except Exception as e:
1053
- raise HTTPException(status_code=500, detail=f"Study time update error: {str(e)}")
 
 
 
 
 
1054
 
1055
- @app.get("/api/user/profile")
1056
- async def get_user_profile(user_id: str = "anonymous"):
1057
- """Get user profile and learning insights"""
 
1058
  try:
1059
- with get_db() as conn:
1060
- cursor = conn.cursor()
1061
-
1062
- # Get profile
1063
- cursor.execute('SELECT * FROM user_profiles WHERE user_id = ?', (user_id,))
1064
- profile = cursor.fetchone()
1065
-
1066
- if not profile:
1067
- return {
1068
- "success": True,
1069
- "profile": None,
1070
- "message": "No profile found"
1071
- }
1072
-
1073
- # Get recent performance
1074
- cursor.execute('''
1075
- SELECT
1076
- DATE(created_at) as date,
1077
- COUNT(*) as total,
1078
- SUM(CASE WHEN is_correct = 1 THEN 1 ELSE 0 END) as correct
1079
- FROM user_answers
1080
- WHERE session_id IN (
1081
- SELECT id FROM sessions WHERE user_id = ?
1082
- )
1083
- GROUP BY DATE(created_at)
1084
- ORDER BY date DESC
1085
- LIMIT 7
1086
- ''', (user_id,))
1087
-
1088
- recent_performance = [dict(row) for row in cursor.fetchall()]
1089
-
1090
- # Get weak areas
1091
- strengths = json.loads(profile['strengths']) if profile['strengths'] else []
1092
- weaknesses = json.loads(profile['weaknesses']) if profile['weaknesses'] else []
1093
-
1094
- return {
1095
- "success": True,
1096
- "profile": dict(profile),
1097
- "insights": {
1098
- "strengths": strengths[:10],
1099
- "weaknesses": weaknesses[:10],
1100
- "recent_performance": recent_performance,
1101
- "total_weak_areas": len(weaknesses),
1102
- "total_strengths": len(strengths)
1103
- }
1104
- }
1105
-
1106
- except Exception as e:
1107
- raise HTTPException(status_code=500, detail=f"Profile retrieval error: {str(e)}")
1108
 
1109
- @app.delete("/api/session/{session_id}")
1110
- async def delete_session(session_id: str):
1111
- """Soft delete a session"""
1112
  try:
1113
- with get_db() as conn:
1114
- cursor = conn.cursor()
1115
- cursor.execute('''
1116
- UPDATE sessions SET is_active = 0, updated_at = ? WHERE id = ?
1117
- ''', (datetime.now().isoformat(), session_id))
1118
- conn.commit()
1119
-
1120
- return {"success": True, "message": "Session deleted"}
1121
-
1122
- except Exception as e:
1123
- raise HTTPException(status_code=500, detail=f"Session deletion error: {str(e)}")
1124
 
 
1125
  @app.get("/health")
1126
- async def health():
1127
  """Health check endpoint"""
1128
- try:
1129
- with get_db() as conn:
1130
- cursor = conn.cursor()
1131
- cursor.execute('SELECT COUNT(*) as count FROM sessions WHERE is_active = 1')
1132
- result = cursor.fetchone()
1133
-
1134
- return {
1135
- "status": "healthy",
1136
- "timestamp": datetime.now().isoformat(),
1137
- "database": "connected",
1138
- "active_sessions": result['count'] if result else 0
1139
- }
1140
- except:
1141
- return {"status": "unhealthy", "timestamp": datetime.now().isoformat()}
1142
-
1143
- @app.get("/")
1144
- async def root():
1145
- """Root endpoint"""
1146
- return {
1147
- "service": "StudyFlow AI Professional",
1148
- "version": "2.5.0",
1149
- "status": "running",
1150
- "features": [
1151
- "Persistent session storage",
1152
- "Personalized learning profiles",
1153
- "Text selection analysis",
1154
- "YouTube transcript processing",
1155
- "Performance tracking",
1156
- "Weak area identification",
1157
- "Tab-based interface",
1158
- "Real-time feedback"
1159
- ],
1160
- "endpoints": {
1161
- "process_content": "POST /api/process-content",
1162
- "analyze_text": "POST /api/analyze-text",
1163
- "submit_answer": "POST /api/submit-answer",
1164
- "get_session": "GET /api/session/{session_id}",
1165
- "get_user_sessions": "GET /api/user/sessions",
1166
- "get_user_profile": "GET /api/user/profile"
1167
- }
1168
- }
1169
 
1170
  if __name__ == "__main__":
1171
  import uvicorn
1172
- import random
1173
- port = int(os.getenv("PORT", 7860))
1174
- uvicorn.run(app, host="0.0.0.0", port=port)
 
1
  """
2
+ StudyFlow AI Backend - Lightweight Version for Hugging Face Spaces
 
3
  """
4
+ import os
5
+ import json
6
+ import sqlite3
7
+ import hashlib
8
+ import tempfile
9
+ from datetime import datetime
10
+ from pathlib import Path
11
 
12
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
13
+ from fastapi.responses import JSONResponse, FileResponse, HTMLResponse
14
  from fastapi.middleware.cors import CORSMiddleware
15
+ from fastapi.staticfiles import StaticFiles
 
 
16
  import PyPDF2
 
 
 
 
 
 
 
 
17
  from youtube_transcript_api import YouTubeTranscriptApi
18
+ import requests
 
19
 
20
+ # Initialize FastAPI
21
+ app = FastAPI(title="StudyFlow AI", version="1.0.0")
 
 
 
22
 
23
+ # CORS middleware
24
  app.add_middleware(
25
  CORSMiddleware,
26
  allow_origins=["*"],
 
34
 
35
  def init_db():
36
  """Initialize SQLite database"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  conn = sqlite3.connect(DB_PATH)
38
+ cursor = conn.cursor()
39
+
40
+ # Sessions table
41
+ cursor.execute('''
42
+ CREATE TABLE IF NOT EXISTS sessions (
43
+ id TEXT PRIMARY KEY,
44
+ title TEXT NOT NULL,
45
+ content_type TEXT NOT NULL,
46
+ difficulty TEXT NOT NULL,
47
+ content_hash TEXT,
48
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
49
+ last_accessed TIMESTAMP DEFAULT CURRENT_TIMESTAMP
50
+ )
51
+ ''')
52
+
53
+ # Questions table
54
+ cursor.execute('''
55
+ CREATE TABLE IF NOT EXISTS questions (
56
+ id TEXT PRIMARY KEY,
57
+ session_id TEXT NOT NULL,
58
+ question_text TEXT NOT NULL,
59
+ question_type TEXT NOT NULL,
60
+ options TEXT,
61
+ correct_answer TEXT NOT NULL,
62
+ difficulty TEXT NOT NULL,
63
+ explanation TEXT,
64
+ user_answer TEXT,
65
+ is_correct INTEGER DEFAULT 0,
66
+ time_spent INTEGER DEFAULT 0,
67
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
68
+ FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
69
+ )
70
+ ''')
71
+
72
+ # Flashcards table
73
+ cursor.execute('''
74
+ CREATE TABLE IF NOT EXISTS flashcards (
75
+ id TEXT PRIMARY KEY,
76
+ session_id TEXT NOT NULL,
77
+ front TEXT NOT NULL,
78
+ back TEXT NOT NULL,
79
+ category TEXT,
80
+ difficulty TEXT,
81
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
82
+ FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
83
+ )
84
+ ''')
85
+
86
+ # Notes table
87
+ cursor.execute('''
88
+ CREATE TABLE IF NOT EXISTS notes (
89
+ id TEXT PRIMARY KEY,
90
+ session_id TEXT NOT NULL,
91
+ title TEXT NOT NULL,
92
+ content TEXT NOT NULL,
93
+ tags TEXT,
94
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
95
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
96
+ FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
97
+ )
98
+ ''')
99
+
100
+ # Highlights table
101
+ cursor.execute('''
102
+ CREATE TABLE IF NOT EXISTS highlights (
103
+ id TEXT PRIMARY KEY,
104
+ session_id TEXT NOT NULL,
105
+ text TEXT NOT NULL,
106
+ context TEXT,
107
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
108
+ FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
109
+ )
110
+ ''')
111
+
112
+ # User profile table
113
+ cursor.execute('''
114
+ CREATE TABLE IF NOT EXISTS user_profile (
115
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
116
+ total_questions INTEGER DEFAULT 0,
117
+ correct_answers INTEGER DEFAULT 0,
118
+ total_study_time INTEGER DEFAULT 0,
119
+ weak_areas TEXT,
120
+ strengths TEXT,
121
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
122
+ )
123
+ ''')
124
+
125
+ conn.commit()
126
+ conn.close()
127
 
128
  # Initialize database
129
  init_db()
130
 
131
+ # Helper functions
132
+ def generate_id(text: str = None):
133
+ """Generate a unique ID"""
134
+ import uuid
135
+ if text:
136
+ return hashlib.md5(text.encode()).hexdigest()[:12]
137
+ return str(uuid.uuid4())[:12]
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ def extract_text_from_pdf(file_path: str) -> str:
140
+ """Extract text from PDF file"""
141
  try:
142
+ with open(file_path, 'rb') as file:
143
+ pdf_reader = PyPDF2.PdfReader(file)
144
+ text = ""
145
+ for page in pdf_reader.pages:
146
+ text += page.extract_text() + "\n"
147
+ return text
148
+ except Exception as e:
149
+ return f"Error extracting PDF: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ def extract_text_from_youtube(url: str) -> str:
152
+ """Extract transcript from YouTube video"""
153
  try:
154
+ video_id = url.split("v=")[-1].split("&")[0]
155
+ transcript = YouTubeTranscriptApi.get_transcript(video_id)
156
+ text = " ".join([entry['text'] for entry in transcript])
157
+ return text
158
  except Exception as e:
159
+ return f"Error extracting YouTube transcript: {str(e)}"
 
160
 
161
+ def generate_questions(text: str, difficulty: str, count: int = 5) -> list:
162
+ """Generate questions from text (simplified for demo)"""
163
+ sentences = [s.strip() for s in text.split('.') if len(s.strip()) > 50]
164
+ sentences = sentences[:10] # Take first 10 meaningful sentences
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  questions = []
167
+ for i, sentence in enumerate(sentences[:count]):
168
+ qid = generate_id(f"q_{i}_{sentence[:20]}")
169
+
170
+ # Simple question generation logic
171
+ words = sentence.split()
172
+ if len(words) > 8:
173
+ # Create a fill-in-the-blank question
174
+ blank_word = words[len(words)//2]
175
+ question_text = sentence.replace(blank_word, "_____")
176
+ questions.append({
177
+ "id": qid,
178
+ "question_text": question_text,
179
+ "question_type": "fill_blank",
180
+ "correct_answer": blank_word,
181
+ "difficulty": difficulty,
182
+ "explanation": f"The missing word is '{blank_word}', which fits grammatically and contextually."
183
+ })
 
 
184
  else:
185
+ # Create a multiple choice question
186
+ question_text = f"What is the main idea of: '{sentence[:100]}...'?"
187
+ options = [
188
+ "It describes a process",
189
+ "It states a fact",
190
+ "It presents an argument",
191
+ "It asks a question"
192
+ ]
193
+ questions.append({
194
+ "id": qid,
195
+ "question_text": question_text,
196
+ "question_type": "multiple_choice",
197
+ "options": json.dumps(options),
198
+ "correct_answer": "0", # Default to first option
199
+ "difficulty": difficulty,
200
+ "explanation": "This sentence appears to describe a process or sequence of events."
201
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
 
203
  return questions
204
 
205
+ def generate_flashcards(text: str, count: int = 3) -> list:
206
+ """Generate flashcards from text"""
207
+ sentences = [s.strip() for s in text.split('.') if len(s.strip()) > 30]
208
+ sentences = sentences[:count * 2] # Take pairs for front/back
209
+
210
+ flashcards = []
211
+ for i in range(0, min(len(sentences), count * 2), 2):
212
+ if i + 1 < len(sentences):
213
+ fcid = generate_id(f"fc_{i}")
214
+ flashcards.append({
215
+ "id": fcid,
216
+ "front": sentences[i][:100],
217
+ "back": sentences[i + 1][:200],
218
+ "category": "General",
219
+ "difficulty": "medium"
220
+ })
221
+
222
+ return flashcards
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
+ # API Endpoints
225
  @app.post("/api/process-content")
226
  async def process_content(
 
227
  content_type: str = Form(...),
228
+ difficulty: str = Form(...),
229
+ title: str = Form(...),
230
  content: str = Form(None),
231
  file: UploadFile = File(None),
232
+ youtube_url: str = Form(None)
 
 
233
  ):
234
+ """Process uploaded content and create study materials"""
235
+
236
+ # Extract text based on content type
237
+ text_content = ""
238
+ if content_type == "text" and content:
239
+ text_content = content
240
+ elif content_type == "pdf" and file:
241
+ # Save uploaded file temporarily
242
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
243
+ content_bytes = await file.read()
244
+ temp_file.write(content_bytes)
245
+ temp_file.close()
246
+ text_content = extract_text_from_pdf(temp_file.name)
247
+ os.unlink(temp_file.name)
248
+ elif content_type == "youtube" and youtube_url:
249
+ text_content = extract_text_from_youtube(youtube_url)
250
+ else:
251
+ return JSONResponse(
252
+ status_code=400,
253
+ content={"error": "Invalid content type or missing content"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  )
255
+
256
+ if len(text_content) < 100:
257
  return JSONResponse(
258
+ status_code=400,
259
+ content={"error": "Content too short (minimum 100 characters)"}
 
 
 
 
 
 
 
260
  )
261
+
262
+ # Generate content hash
263
+ content_hash = hashlib.md5(text_content.encode()).hexdigest()
264
+
265
+ # Check if session already exists
266
+ conn = sqlite3.connect(DB_PATH)
267
+ cursor = conn.cursor()
268
+
269
+ cursor.execute(
270
+ "SELECT id FROM sessions WHERE content_hash = ?",
271
+ (content_hash,)
272
+ )
273
+ existing = cursor.fetchone()
274
+
275
+ if existing:
276
+ conn.close()
277
+ return JSONResponse({
278
+ "message": "Session already exists",
279
+ "session_id": existing[0],
280
+ "is_existing": True
281
+ })
282
+
283
+ # Create new session
284
+ session_id = generate_id(text_content[:100])
285
+
286
+ cursor.execute('''
287
+ INSERT INTO sessions (id, title, content_type, difficulty, content_hash)
288
+ VALUES (?, ?, ?, ?, ?)
289
+ ''', (session_id, title, content_type, difficulty, content_hash))
290
+
291
+ # Generate study materials
292
+ questions = generate_questions(text_content, difficulty, 5)
293
+ flashcards = generate_flashcards(text_content, 3)
294
+
295
+ # Save questions
296
+ for q in questions:
297
+ cursor.execute('''
298
+ INSERT INTO questions (id, session_id, question_text, question_type,
299
+ options, correct_answer, difficulty, explanation)
300
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
301
+ ''', (
302
+ q["id"], session_id, q["question_text"], q["question_type"],
303
+ q.get("options"), q["correct_answer"], q["difficulty"], q.get("explanation")
304
+ ))
305
+
306
+ # Save flashcards
307
+ for fc in flashcards:
308
+ cursor.execute('''
309
+ INSERT INTO flashcards (id, session_id, front, back, category, difficulty)
310
+ VALUES (?, ?, ?, ?, ?, ?)
311
+ ''', (
312
+ fc["id"], session_id, fc["front"], fc["back"],
313
+ fc["category"], fc["difficulty"]
314
+ ))
315
+
316
+ # Create initial notes
317
+ cursor.execute('''
318
+ INSERT INTO notes (id, session_id, title, content)
319
+ VALUES (?, ?, ?, ?)
320
+ ''', (
321
+ generate_id("note1"),
322
+ session_id,
323
+ "Key Concepts",
324
+ "Here are the key concepts from your study material..."
325
+ ))
326
+
327
+ conn.commit()
328
+ conn.close()
329
+
330
+ return JSONResponse({
331
+ "message": "Session created successfully",
332
+ "session_id": session_id,
333
+ "is_existing": False,
334
+ "question_count": len(questions),
335
+ "flashcard_count": len(flashcards)
336
+ })
337
 
338
+ @app.get("/api/session/{session_id}")
339
+ async def get_session(session_id: str):
340
+ """Get session with all materials"""
341
+ conn = sqlite3.connect(DB_PATH)
342
+ conn.row_factory = sqlite3.Row
343
+ cursor = conn.cursor()
344
+
345
+ # Get session info
346
+ cursor.execute("SELECT * FROM sessions WHERE id = ?", (session_id,))
347
+ session = cursor.fetchone()
348
+
349
+ if not session:
350
+ conn.close()
351
+ raise HTTPException(status_code=404, detail="Session not found")
352
+
353
+ # Get questions
354
+ cursor.execute("SELECT * FROM questions WHERE session_id = ?", (session_id,))
355
+ questions = [dict(row) for row in cursor.fetchall()]
356
+
357
+ # Get flashcards
358
+ cursor.execute("SELECT * FROM flashcards WHERE session_id = ?", (session_id,))
359
+ flashcards = [dict(row) for row in cursor.fetchall()]
360
+
361
+ # Get notes
362
+ cursor.execute("SELECT * FROM notes WHERE session_id = ?", (session_id,))
363
+ notes = [dict(row) for row in cursor.fetchall()]
364
+
365
+ # Get highlights
366
+ cursor.execute("SELECT * FROM highlights WHERE session_id = ?", (session_id,))
367
+ highlights = [dict(row) for row in cursor.fetchall()]
368
+
369
+ # Calculate performance
370
+ total_questions = len(questions)
371
+ correct_answers = sum(1 for q in questions if q.get("is_correct") == 1)
372
+ accuracy = round((correct_answers / total_questions * 100) if total_questions > 0 else 0, 1)
373
+ avg_time_spent = round(sum(q.get("time_spent", 0) for q in questions) / total_questions if total_questions > 0 else 0, 1)
374
+
375
+ conn.close()
376
+
377
+ return JSONResponse({
378
+ "session": dict(session),
379
+ "materials": {
380
+ "questions": questions,
381
+ "flashcards": flashcards,
382
+ "notes": notes,
383
+ "highlights": highlights
384
+ },
385
+ "summary": {
386
+ "question_count": total_questions,
387
+ "flashcard_count": len(flashcards),
388
+ "note_count": len(notes),
389
+ "highlight_count": len(highlights)
390
+ },
391
+ "performance": {
392
+ "total_questions": total_questions,
393
+ "correct_answers": correct_answers,
394
+ "accuracy": accuracy,
395
+ "avg_time_spent": avg_time_spent
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  }
397
+ })
 
 
398
 
399
+ @app.get("/api/user/sessions")
400
+ async def get_user_sessions():
401
+ """Get all user sessions"""
402
+ conn = sqlite3.connect(DB_PATH)
403
+ conn.row_factory = sqlite3.Row
404
+ cursor = conn.cursor()
405
+
406
+ cursor.execute("SELECT * FROM sessions ORDER BY last_accessed DESC")
407
+ sessions = [dict(row) for row in cursor.fetchall()]
408
+
409
+ # Add performance stats to each session
410
+ for session in sessions:
411
+ cursor.execute(
412
+ "SELECT COUNT(*), SUM(is_correct) FROM questions WHERE session_id = ?",
413
+ (session["id"],)
414
+ )
415
+ result = cursor.fetchone()
416
+ total = result[0] or 0
417
+ correct = result[1] or 0
418
+ accuracy = round((correct / total * 100) if total > 0 else 0, 1)
419
+
420
+ session["performance"] = {
421
+ "total": total,
422
+ "correct": correct,
423
+ "accuracy": accuracy
424
+ }
425
+
426
+ conn.close()
427
+
428
+ return JSONResponse({"sessions": sessions})
429
 
430
  @app.post("/api/submit-answer")
431
  async def submit_answer(
432
  session_id: str = Form(...),
433
  question_id: str = Form(...),
434
  user_answer: str = Form(...),
435
+ time_spent: int = Form(0)
 
436
  ):
437
+ """Submit an answer to a question"""
438
+ conn = sqlite3.connect(DB_PATH)
439
+ cursor = conn.cursor()
440
+
441
+ # Get correct answer
442
+ cursor.execute(
443
+ "SELECT correct_answer FROM questions WHERE id = ? AND session_id = ?",
444
+ (question_id, session_id)
445
+ )
446
+ result = cursor.fetchone()
447
+
448
+ if not result:
449
+ conn.close()
450
+ raise HTTPException(status_code=404, detail="Question not found")
451
+
452
+ correct_answer = result[0]
453
+ is_correct = 1 if str(user_answer).strip().lower() == str(correct_answer).strip().lower() else 0
454
+
455
+ # Update question with user answer
456
+ cursor.execute('''
457
+ UPDATE questions
458
+ SET user_answer = ?, is_correct = ?, time_spent = ?
459
+ WHERE id = ? AND session_id = ?
460
+ ''', (user_answer, is_correct, time_spent, question_id, session_id))
461
+
462
+ # Update user profile stats
463
+ cursor.execute('''
464
+ INSERT OR REPLACE INTO user_profile (id, total_questions, correct_answers, updated_at)
465
+ VALUES (
466
+ 1,
467
+ COALESCE((SELECT total_questions FROM user_profile WHERE id = 1), 0) + 1,
468
+ COALESCE((SELECT correct_answers FROM user_profile WHERE id = 1), 0) + ?,
469
+ CURRENT_TIMESTAMP
470
+ )
471
+ ''', (is_correct,))
472
+
473
+ conn.commit()
474
+ conn.close()
475
+
476
+ return JSONResponse({
477
+ "is_correct": bool(is_correct),
478
+ "correct_answer": correct_answer,
479
+ "message": "Correct!" if is_correct else "Incorrect, try again!"
480
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
 
482
+ @app.post("/api/analyze-text")
483
+ async def analyze_text(text: str = Form(...), session_id: str = Form(...)):
484
+ """Analyze selected text (simplified version)"""
485
+
486
+ # Simple text analysis
487
+ words = text.split()
488
+ definitions = []
489
+
490
+ # Simple "definition" generation for demo
491
+ for i, word in enumerate(words[:5]): # Analyze first 5 words
492
+ if len(word) > 4: # Only "define" longer words
493
+ definitions.append({
494
+ "word": word,
495
+ "definition": f"A term used in this context meaning: '{word}'",
496
+ "source": "StudyFlow AI",
497
+ "example": f"In the sentence: '{' '.join(words[max(0,i-2):min(len(words),i+3)])}'"
498
+ })
499
+
500
+ # Generate simple explanation
501
+ word_count = len(words)
502
+ explanation = f"This text contains {word_count} words. "
503
+ if word_count < 10:
504
+ explanation += "It's a short phrase or sentence fragment."
505
+ elif word_count < 25:
506
+ explanation += "It's a sentence or two expressing a complete thought."
507
+ else:
508
+ explanation += "It's a paragraph expressing multiple related ideas."
509
+
510
+ # Save as highlight
511
+ conn = sqlite3.connect(DB_PATH)
512
+ cursor = conn.cursor()
513
+
514
+ highlight_id = generate_id(text)
515
+ cursor.execute('''
516
+ INSERT OR IGNORE INTO highlights (id, session_id, text, context)
517
+ VALUES (?, ?, ?, ?)
518
+ ''', (highlight_id, session_id, text, json.dumps(definitions)))
519
+
520
+ conn.commit()
521
+ conn.close()
522
+
523
+ return JSONResponse({
524
+ "text": text,
525
+ "definitions": definitions,
526
+ "explanation": explanation,
527
+ "suggested_actions": [
528
+ "Add to flashcards",
529
+ "Create a practice question",
530
+ "Make a note about this concept"
531
+ ]
532
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
 
534
+ @app.get("/api/user/profile")
535
+ async def get_user_profile():
536
+ """Get user learning profile"""
537
+ conn = sqlite3.connect(DB_PATH)
538
+ cursor = conn.cursor()
539
+
540
+ cursor.execute("SELECT * FROM user_profile WHERE id = 1")
541
+ profile = cursor.fetchone()
542
+
543
+ if not profile:
544
+ # Create default profile
545
+ cursor.execute('''
546
+ INSERT INTO user_profile (id, total_questions, correct_answers, total_study_time)
547
+ VALUES (1, 0, 0, 0)
548
+ ''')
549
+ conn.commit()
550
+ cursor.execute("SELECT * FROM user_profile WHERE id = 1")
551
+ profile = cursor.fetchone()
552
+
553
+ # Get insights from questions
554
+ cursor.execute('''
555
+ SELECT difficulty,
556
+ COUNT(*) as total,
557
+ SUM(is_correct) as correct
558
+ FROM questions
559
+ WHERE user_answer IS NOT NULL
560
+ GROUP BY difficulty
561
+ ''')
562
+ difficulty_stats = cursor.fetchall()
563
+
564
+ # Determine weak areas and strengths
565
+ weak_areas = []
566
+ strengths = []
567
+
568
+ for diff, total, correct in difficulty_stats:
569
+ if total > 0:
570
+ accuracy = correct / total
571
+ if accuracy < 0.5:
572
+ weak_areas.append(f"{diff} difficulty questions")
573
+ elif accuracy > 0.8:
574
+ strengths.append(f"{diff} difficulty questions")
575
+
576
+ conn.close()
577
+
578
+ return JSONResponse({
579
+ "profile": {
580
+ "total_questions": profile[1],
581
+ "correct_answers": profile[2],
582
+ "total_study_time": profile[3],
583
+ "accuracy": round((profile[2] / profile[1] * 100) if profile[1] > 0 else 0, 1)
584
+ },
585
+ "insights": {
586
+ "weaknesses": weak_areas,
587
+ "strengths": strengths,
588
+ "recent_performance": [
589
+ {
590
+ "date": datetime.now().strftime("%Y-%m-%d"),
591
+ "total": profile[1],
592
+ "correct": profile[2]
593
  }
594
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  }
596
+ })
597
+
598
+ @app.delete("/api/session/{session_id}")
599
+ async def delete_session(session_id: str):
600
+ """Delete a session"""
601
+ conn = sqlite3.connect(DB_PATH)
602
+ cursor = conn.cursor()
603
+
604
+ cursor.execute("DELETE FROM sessions WHERE id = ?", (session_id,))
605
+
606
+ conn.commit()
607
+ affected = cursor.rowcount
608
+ conn.close()
609
+
610
+ if affected == 0:
611
+ raise HTTPException(status_code=404, detail="Session not found")
612
+
613
+ return JSONResponse({"message": "Session deleted successfully"})
614
 
615
  @app.post("/api/update-study-time")
616
+ async def update_study_time(session_id: str = Form(...), study_time: int = Form(...)):
 
 
 
617
  """Update study time for a session"""
618
+ conn = sqlite3.connect(DB_PATH)
619
+ cursor = conn.cursor()
620
+
621
+ cursor.execute('''
622
+ UPDATE sessions
623
+ SET last_accessed = CURRENT_TIMESTAMP
624
+ WHERE id = ?
625
+ ''', (session_id,))
626
+
627
+ cursor.execute('''
628
+ UPDATE user_profile
629
+ SET total_study_time = COALESCE(total_study_time, 0) + ?
630
+ WHERE id = 1
631
+ ''', (study_time,))
632
+
633
+ conn.commit()
634
+ conn.close()
635
+
636
+ return JSONResponse({"message": "Study time updated"})
637
 
638
+ # Serve static files
639
+ @app.get("/")
640
+ async def serve_frontend():
641
+ """Serve the main frontend page"""
642
  try:
643
+ with open("index.html", "r", encoding="utf-8") as f:
644
+ html_content = f.read()
645
+ return HTMLResponse(content=html_content)
646
+ except FileNotFoundError:
647
+ return HTMLResponse(content="<h1>StudyFlow AI Backend is running!</h1><p>Frontend files not found.</p>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
648
 
649
+ @app.get("/app.js")
650
+ async def serve_js():
651
+ """Serve the JavaScript file"""
652
  try:
653
+ with open("app.js", "r", encoding="utf-8") as f:
654
+ js_content = f.read()
655
+ return HTMLResponse(content=js_content, media_type="application/javascript")
656
+ except FileNotFoundError:
657
+ return HTMLResponse(content="console.error('app.js not found');", media_type="application/javascript")
 
 
 
 
 
 
658
 
659
+ # Health check endpoint
660
  @app.get("/health")
661
+ async def health_check():
662
  """Health check endpoint"""
663
+ return JSONResponse({"status": "healthy", "timestamp": datetime.now().isoformat()})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
664
 
665
  if __name__ == "__main__":
666
  import uvicorn
667
+ uvicorn.run(app, host="0.0.0.0", port=7860)