sunbal7 commited on
Commit
8181ae4
·
verified ·
1 Parent(s): 67e1e17

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +822 -312
app.py CHANGED
@@ -1,330 +1,840 @@
1
- import streamlit as st
2
- import numpy as np
3
- from PIL import Image
4
- import io
5
- import torch
6
- import torch.nn as nn
7
- from datetime import datetime
8
- import plotly.graph_objects as go
9
- import sys
10
- import os
11
-
12
- # Add utils to path
13
- sys.path.append('./utils')
14
-
15
- # Import custom utilities
16
- from utils.model_loader import load_medical_models, analyze_chest_xray, analyze_ultrasound
17
- from utils.image_processor import process_uploaded_image, enhance_image_quality
18
-
19
- # Set page configuration for Hugging Face
20
- st.set_page_config(
21
- page_title="Rural Diagnostic Assistant",
22
- page_icon="🏥",
23
- layout="wide",
24
- initial_sidebar_state="expanded"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  )
26
 
27
- # Custom CSS optimized for Hugging Face
28
- st.markdown("""
29
- <style>
30
- .main-header {
31
- font-size: 2.5rem;
32
- color: #1f77b4;
33
- text-align: center;
34
- margin-bottom: 1rem;
35
- font-weight: 700;
36
- }
37
- .diagnosis-card {
38
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
39
- border-radius: 10px;
40
- padding: 20px;
41
- color: white;
42
- margin: 10px 0;
43
- box-shadow: 0 4px 10px rgba(0,0,0,0.1);
44
- }
45
- .high-risk { border-left: 6px solid #ff4444; }
46
- .medium-risk { border-left: 6px solid #ffaa00; }
47
- .low-risk { border-left: 6px solid #00C851; }
48
- .upload-area {
49
- border: 2px dashed #1f77b4;
50
- border-radius: 8px;
51
- padding: 20px;
52
- text-align: center;
53
- margin: 15px 0;
54
- }
55
- .stButton>button {
56
- width: 100%;
57
- border-radius: 8px;
58
- height: 3rem;
59
- font-size: 1.1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
- </style>
62
- """, unsafe_allow_html=True)
63
-
64
- # Initialize session state for Hugging Face compatibility
65
- if 'diagnosis_history' not in st.session_state:
66
- st.session_state.diagnosis_history = []
67
- if 'models_loaded' not in st.session_state:
68
- st.session_state.models_loaded = False
69
- if 'model_manager' not in st.session_state:
70
- st.session_state.model_manager = None
71
-
72
- @st.cache_resource(show_spinner=False)
73
- def initialize_models():
74
- """Initialize models with caching for Hugging Face"""
 
 
 
 
 
 
 
 
 
 
75
  try:
76
- model_manager = load_medical_models()
77
- if model_manager:
78
- st.session_state.models_loaded = True
79
- st.session_state.model_manager = model_manager
80
- return True
81
- return False
82
- except Exception as e:
83
- st.error(f"Model initialization failed: {str(e)}")
84
- return False
85
-
86
- def main():
87
- # Title and description
88
- st.markdown('<h1 class="main-header">🏥 Rural Diagnostic Assistant</h1>', unsafe_allow_html=True)
89
- st.markdown("""
90
- **AI-Powered Medical Imaging Analysis for Rural Healthcare**
91
- *Accurate detection of TB, Pneumonia, and pregnancy risks using state-of-the-art AI models*
92
- """)
93
-
94
- # Sidebar optimized for Hugging Face
95
- with st.sidebar:
96
- st.header("⚙️ Configuration")
97
 
98
- analysis_type = st.radio(
99
- "Select Analysis Type",
100
- ["Chest X-Ray (TB/Pneumonia)", "Ultrasound (Pregnancy Risk)"],
101
- help="Choose the type of medical image to analyze"
102
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- # Hugging Face doesn't support external API keys well, so we'll use local models
105
- st.info("🔒 Privacy Focused: All analysis happens locally on our servers")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
- st.header("👤 Patient Information")
108
- patient_id = st.text_input("Patient ID", value="PT-001", placeholder="Enter patient ID")
109
- patient_age = st.slider("Age", 0, 120, 35)
110
- patient_gender = st.selectbox("Gender", ["Male", "Female", "Other"])
111
 
112
- # Quick actions
113
- st.header("🚀 Quick Actions")
114
- if st.button("🔄 Clear History"):
115
- st.session_state.diagnosis_history = []
116
- st.rerun()
117
 
118
- if st.button("ℹ️ Show Help"):
119
- st.info("""
120
- **How to use:**
121
- 1. Select analysis type
122
- 2. Upload medical image
123
- 3. Click 'Analyze Image'
124
- 4. Review results
125
-
126
- **Supported formats:** JPG, PNG, DICOM
127
- **Accuracy:** >90% on clinical datasets
128
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
- # Main content area
131
- col1, col2 = st.columns([1, 1])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
- with col1:
134
- st.header("📤 Image Upload")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
- # Upload section with better UX for Hugging Face
137
- uploaded_file = st.file_uploader(
138
- "Choose medical image file",
139
- type=['png', 'jpg', 'jpeg', 'dcm'],
140
- help="Upload X-Ray, Chest Scan, or Ultrasound image"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  )
142
 
143
- # Sample images for demo
144
- st.subheader("🎯 Try Sample Images")
145
- col1a, col2a = st.columns(2)
146
- with col1a:
147
- if st.button("Sample Chest X-Ray", use_container_width=True):
148
- # Load sample image
149
- sample_image = Image.open('./assets/sample_xray.jpg')
150
- analyze_image(sample_image, "Chest X-Ray (TB/Pneumonia)", "Sample Patient")
151
- with col2a:
152
- if st.button("Sample Ultrasound", use_container_width=True):
153
- sample_image = Image.open('./assets/sample_ultrasound.jpg')
154
- analyze_image(sample_image, "Ultrasound (Pregnancy Risk)", "Sample Patient")
155
 
156
- if uploaded_file is not None:
157
- # Process uploaded image
158
- with st.spinner("Processing image..."):
159
- image = process_uploaded_image(uploaded_file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
- if image:
162
- st.success("✅ Image processed successfully")
163
- st.image(image, caption="Uploaded Image", use_column_width=True)
164
-
165
- # Analyze button
166
- if st.button("🔍 Analyze Image", type="primary", use_container_width=True):
167
- analyze_image(image, analysis_type, patient_id)
168
-
169
- with col2:
170
- st.header("📋 Analysis Results")
171
-
172
- if len(st.session_state.diagnosis_history) > 0:
173
- latest = st.session_state.diagnosis_history[-1]
174
- display_results(latest['results'], latest['analysis_type'])
175
- else:
176
- # Show placeholder with instructions
177
- st.info("👆 Upload an image or try sample images to see analysis results")
178
- st.image("https://images.unsplash.com/photo-1576091160399-112ba8d25d1f?w=400",
179
- caption="Medical Imaging Analysis", use_column_width=True)
 
 
 
 
 
 
 
180
 
181
- # Diagnosis History
182
- if len(st.session_state.diagnosis_history) > 0:
183
- st.header("📚 Recent Diagnoses")
184
- for i, entry in enumerate(reversed(st.session_state.diagnosis_history[-3:])):
185
- with st.expander(f"🩺 {entry['patient_id']} - {entry['timestamp'].split(' ')[0]}"):
186
- st.write(f"**Type:** {entry['analysis_type']}")
187
- st.write(f"**Condition:** {entry['results']['condition']}")
188
- st.write(f"**Confidence:** {entry['results']['confidence']:.1f}%")
189
- st.write(f"**Risk:** {entry['results']['risk_level']}")
190
-
191
- def analyze_image(image, analysis_type, patient_id):
192
- """Analyze medical image with AI models"""
193
- # Initialize models if not loaded
194
- if not st.session_state.models_loaded:
195
- with st.spinner("🔄 Loading AI models (first time may take a minute)..."):
196
- success = initialize_models()
197
- if not success:
198
- st.error(" Failed to load AI models. Please refresh and try again.")
199
- return
200
-
201
- # Perform analysis
202
- with st.spinner("🔬 Analyzing image with medical AI..."):
203
- try:
204
- if "Chest" in analysis_type:
205
- results = analyze_chest_xray(st.session_state.model_manager, image)
206
- else:
207
- results = analyze_ultrasound(st.session_state.model_manager, image)
208
-
209
- # Save to history
210
- diagnosis_entry = {
211
- 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
212
- 'patient_id': patient_id,
213
- 'analysis_type': analysis_type,
214
- 'results': results,
215
- 'image': image.copy()
216
- }
217
- st.session_state.diagnosis_history.append(diagnosis_entry)
218
-
219
- st.success("✅ Analysis complete!")
220
- st.rerun()
221
-
222
- except Exception as e:
223
- st.error(f"❌ Analysis failed: {str(e)}")
224
- # Fallback to simulated results for demo
225
- results = get_fallback_results(analysis_type)
226
- diagnosis_entry = {
227
- 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
228
- 'patient_id': patient_id,
229
- 'analysis_type': analysis_type,
230
- 'results': results,
231
- 'image': image.copy()
232
- }
233
- st.session_state.diagnosis_history.append(diagnosis_entry)
234
- st.rerun()
235
-
236
- def display_results(results, analysis_type):
237
- """Display analysis results with medical UI"""
238
- risk_class = results['risk_level'].lower().replace(" ", "-")
239
-
240
- st.markdown(f'<div class="diagnosis-card {risk_class}">', unsafe_allow_html=True)
241
-
242
- # Header with condition and confidence
243
- col1, col2 = st.columns([3, 1])
244
-
245
- with col1:
246
- st.subheader(results['condition'])
247
- st.write(f"**Analysis Type:** {analysis_type}")
248
-
249
- with col2:
250
- # Confidence indicator
251
- confidence_color = "#00C851" if results['confidence'] > 85 else "#ffaa00" if results['confidence'] > 70 else "#ff4444"
252
- st.markdown(f"""
253
- <div style="text-align: center;">
254
- <div style="font-size: 2rem; color: {confidence_color}; font-weight: bold;">
255
- {results['confidence']:.1f}%
256
- </div>
257
- <div style="font-size: 0.9rem;">Confidence</div>
258
- </div>
259
- """, unsafe_allow_html=True)
260
-
261
- # Risk level badge
262
- risk_color = {"High": "#ff4444", "Medium": "#ffaa00", "Low": "#00C851"}
263
- st.markdown(f"""
264
- <div style="background-color: {risk_color[results['risk_level']]}; color: white;
265
- padding: 5px 15px; border-radius: 20px; display: inline-block;
266
- font-weight: bold; margin-bottom: 15px;">
267
- {results['risk_level']} Risk
268
- </div>
269
- """, unsafe_allow_html=True)
270
-
271
- # Findings
272
- st.write("**🔍 Clinical Findings:**")
273
- st.write(results['findings'])
274
-
275
- # Recommendations
276
- st.write("**💡 Recommendations:**")
277
- for i, rec in enumerate(results['recommendations'], 1):
278
- st.write(f"{i}. {rec}")
279
-
280
- # Confidence visualization
281
- if 'detailed_scores' in results and results['detailed_scores']:
282
- st.write("**📊 Detailed Scores:**")
283
- scores = results['detailed_scores']
284
- for condition, score in scores.items():
285
- st.progress(score/100, text=f"{condition.replace('_', ' ').title()}: {score:.1f}%")
286
-
287
- st.markdown('</div>', unsafe_allow_html=True)
288
-
289
- def get_fallback_results(analysis_type):
290
- """Fallback results for demo purposes"""
291
- if "Chest" in analysis_type:
292
- return {
293
- 'condition': 'Pneumonia Detected',
294
- 'confidence': 92.5,
295
- 'risk_level': 'High',
296
- 'recommendations': [
297
- 'Consult with pulmonologist urgently',
298
- 'Start antibiotic treatment',
299
- 'Chest CT scan for confirmation',
300
- 'Monitor oxygen saturation'
301
- ],
302
- 'findings': 'Consolidation observed in right lower lobe consistent with bacterial pneumonia',
303
- 'detailed_scores': {'pneumonia': 92.5, 'tuberculosis': 15.2, 'normal': 7.5}
304
- }
305
- else:
306
- return {
307
- 'condition': 'Normal Pregnancy',
308
- 'confidence': 94.2,
309
- 'risk_level': 'Low',
310
- 'recommendations': [
311
- 'Continue routine prenatal care',
312
- 'Next ultrasound in 4 weeks',
313
- 'Standard prenatal vitamin regimen',
314
- 'Monitor fetal movements'
315
- ],
316
- 'findings': 'Normal fetal development at 20 weeks gestation, appropriate biometric measurements',
317
- 'detailed_scores': {'normal': 94.2, 'ectopic_risk': 3.1, 'placental_issue': 2.7}
318
- }
319
-
320
- # Footer for Hugging Face
321
- st.markdown("---")
322
- st.markdown("""
323
- <div style='text-align: center'>
324
- <p>Rural Diagnostic Assistant - Deployed on Hugging Face Spaces |
325
- <a href='https://huggingface.co/spaces' target='_blank'>Learn More</a></p>
326
- </div>
327
- """, unsafe_allow_html=True)
328
 
329
- if __name__ == "__main__":
330
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ # app/main.py
4
+ from fastapi import FastAPI, Depends, HTTPException, status
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
7
+ from contextlib import asynccontextmanager
8
+ import uvicorn
9
+ from app.database import engine, Base
10
+ from app.routers import auth, sync, content, analytics, grading
11
+ from app.core.config import settings
12
+ from app.ml.model_manager import ModelManager
13
+ import logging
14
+
15
+ # Configure logging
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Initialize ML models on startup
20
+ @asynccontextmanager
21
+ async def lifespan(app: FastAPI):
22
+ # Startup
23
+ logger.info("Starting AI Tutor Backend...")
24
+
25
+ # Create database tables
26
+ Base.metadata.create_all(bind=engine)
27
+
28
+ # Initialize ML models
29
+ model_manager = ModelManager()
30
+ await model_manager.load_models()
31
+
32
+ # Store model manager in app state
33
+ app.state.model_manager = model_manager
34
+
35
+ logger.info("Backend startup complete")
36
+ yield
37
+
38
+ # Shutdown
39
+ logger.info("Shutting down AI Tutor Backend...")
40
+
41
+ app = FastAPI(
42
+ title="AI Tutor Backend",
43
+ description="Adaptive Multilingual Offline-First AI Tutor API",
44
+ version="1.0.0",
45
+ lifespan=lifespan
46
  )
47
 
48
+ # CORS middleware
49
+ app.add_middleware(
50
+ CORSMiddleware,
51
+ allow_origins=settings.ALLOWED_HOSTS,
52
+ allow_credentials=True,
53
+ allow_methods=["*"],
54
+ allow_headers=["*"],
55
+ )
56
+
57
+ # Include routers
58
+ app.include_router(auth.router, prefix="/api/v1/auth", tags=["authentication"])
59
+ app.include_router(sync.router, prefix="/api/v1/sync", tags=["synchronization"])
60
+ app.include_router(content.router, prefix="/api/v1/content", tags=["content"])
61
+ app.include_router(analytics.router, prefix="/api/v1/analytics", tags=["analytics"])
62
+ app.include_router(grading.router, prefix="/api/v1/grading", tags=["grading"])
63
+
64
+ @app.get("/")
65
+ async def root():
66
+ return {"message": "AI Tutor Backend API", "version": "1.0.0"}
67
+
68
+ @app.get("/health")
69
+ async def health_check():
70
+ return {"status": "healthy", "version": "1.0.0"}
71
+
72
+ if __name__ == "__main__":
73
+ uvicorn.run(
74
+ "app.main:app",
75
+ host=settings.HOST,
76
+ port=settings.PORT,
77
+ reload=settings.DEBUG,
78
+ log_level="info"
79
+ )
80
+
81
+ # app/core/config.py
82
+ from pydantic_settings import BaseSettings
83
+ from typing import List
84
+
85
+ class Settings(BaseSettings):
86
+ # Basic settings
87
+ APP_NAME: str = "AI Tutor Backend"
88
+ DEBUG: bool = True
89
+ HOST: str = "0.0.0.0"
90
+ PORT: int = 8000
91
+
92
+ # Database
93
+ DATABASE_URL: str = "postgresql://postgres:password@localhost/ai_tutor"
94
+
95
+ # Security
96
+ SECRET_KEY: str = "your-secret-key-change-in-production"
97
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 * 24 * 60 # 30 days
98
+
99
+ # CORS
100
+ ALLOWED_HOSTS: List[str] = ["*"]
101
+
102
+ # Redis (for caching and task queue)
103
+ REDIS_URL: str = "redis://localhost:6379"
104
+
105
+ # ML Models
106
+ MODEL_PATH: str = "./models"
107
+ HUGGINGFACE_CACHE: str = "./cache"
108
+
109
+ class Config:
110
+ env_file = ".env"
111
+
112
+ settings = Settings()
113
+
114
+ # app/database.py
115
+ from sqlalchemy import create_engine
116
+ from sqlalchemy.ext.declarative import declarative_base
117
+ from sqlalchemy.orm import sessionmaker
118
+ from app.core.config import settings
119
+
120
+ engine = create_engine(
121
+ settings.DATABASE_URL,
122
+ pool_pre_ping=True,
123
+ pool_recycle=300,
124
+ )
125
+
126
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
127
+ Base = declarative_base()
128
+
129
+ def get_db():
130
+ db = SessionLocal()
131
+ try:
132
+ yield db
133
+ finally:
134
+ db.close()
135
+
136
+ # app/models/database.py
137
+ from sqlalchemy import Column, Integer, String, Float, DateTime, Text, Boolean, ForeignKey, JSON
138
+ from sqlalchemy.orm import relationship
139
+ from sqlalchemy.sql import func
140
+ from app.database import Base
141
+ import uuid
142
+
143
+ class Student(Base):
144
+ __tablename__ = "students"
145
+
146
+ id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
147
+ name = Column(String, nullable=False)
148
+ grade = Column(Integer, nullable=False)
149
+ preferred_language = Column(String, nullable=False, default="urdu")
150
+ skill_mastery = Column(JSON, default=dict)
151
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
152
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now())
153
+ device_id = Column(String)
154
+
155
+ # Relationships
156
+ learning_sessions = relationship("LearningSession", back_populates="student")
157
+ student_responses = relationship("StudentResponse", back_populates="student")
158
+
159
+ class Question(Base):
160
+ __tablename__ = "questions"
161
+
162
+ id = Column(String, primary_key=True)
163
+ subject = Column(String, nullable=False)
164
+ topic = Column(String, nullable=False)
165
+ grade = Column(Integer, nullable=False)
166
+ skill_tags = Column(JSON, default=list)
167
+ difficulty_estimate = Column(Float, nullable=False)
168
+ prompt = Column(JSON, nullable=False) # Multi-language prompts
169
+ options = Column(JSON, default=list)
170
+ solution_steps = Column(JSON, default=list)
171
+ hints = Column(JSON, default=dict)
172
+ answer_patterns = Column(JSON, nullable=False)
173
+ explanation = Column(JSON, default=dict)
174
+ content_version = Column(String, default="1.0")
175
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
176
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now())
177
+
178
+ # Relationships
179
+ student_responses = relationship("StudentResponse", back_populates="question")
180
+
181
+ class LearningSession(Base):
182
+ __tablename__ = "learning_sessions"
183
+
184
+ id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
185
+ student_id = Column(String, ForeignKey("students.id"), nullable=False)
186
+ subject = Column(String, nullable=False)
187
+ start_time = Column(DateTime(timezone=True), nullable=False)
188
+ end_time = Column(DateTime(timezone=True))
189
+ skills_updated = Column(JSON, default=dict)
190
+ synced_at = Column(DateTime(timezone=True))
191
+ device_id = Column(String)
192
+
193
+ # Relationships
194
+ student = relationship("Student", back_populates="learning_sessions")
195
+ responses = relationship("StudentResponse", back_populates="session")
196
+
197
+ class StudentResponse(Base):
198
+ __tablename__ = "student_responses"
199
+
200
+ id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
201
+ student_id = Column(String, ForeignKey("students.id"), nullable=False)
202
+ question_id = Column(String, ForeignKey("questions.id"), nullable=False)
203
+ session_id = Column(String, ForeignKey("learning_sessions.id"), nullable=False)
204
+ response = Column(Text, nullable=False)
205
+ response_type = Column(String, nullable=False) # 'voice' or 'text'
206
+ is_correct = Column(Boolean, nullable=False)
207
+ partial_credit = Column(Float, nullable=False, default=0.0)
208
+ confidence = Column(Float, nullable=False, default=0.0)
209
+ timestamp = Column(DateTime(timezone=True), nullable=False)
210
+ synced_at = Column(DateTime(timezone=True))
211
+
212
+ # Relationships
213
+ student = relationship("Student", back_populates="student_responses")
214
+ question = relationship("Question", back_populates="student_responses")
215
+ session = relationship("LearningSession", back_populates="responses")
216
+
217
+ class Teacher(Base):
218
+ __tablename__ = "teachers"
219
+
220
+ id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
221
+ email = Column(String, unique=True, nullable=False)
222
+ hashed_password = Column(String, nullable=False)
223
+ name = Column(String, nullable=False)
224
+ school = Column(String)
225
+ subjects = Column(JSON, default=list)
226
+ grades = Column(JSON, default=list)
227
+ is_active = Column(Boolean, default=True)
228
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
229
+
230
+ class ContentBundle(Base):
231
+ __tablename__ = "content_bundles"
232
+
233
+ id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
234
+ version = Column(String, nullable=False)
235
+ grade = Column(Integer, nullable=False)
236
+ subject = Column(String, nullable=False)
237
+ language = Column(String, nullable=False)
238
+ file_path = Column(String, nullable=False)
239
+ file_size = Column(Integer, nullable=False)
240
+ checksum = Column(String, nullable=False)
241
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
242
+
243
+ # app/schemas/sync.py
244
+ from pydantic import BaseModel
245
+ from typing import List, Dict, Optional, Any
246
+ from datetime import datetime
247
+
248
+ class StudentResponseSchema(BaseModel):
249
+ id: str
250
+ student_id: str
251
+ question_id: str
252
+ session_id: str
253
+ response: str
254
+ response_type: str
255
+ is_correct: bool
256
+ partial_credit: float
257
+ confidence: float
258
+ timestamp: datetime
259
+
260
+ class LearningSessionSchema(BaseModel):
261
+ id: str
262
+ student_id: str
263
+ subject: str
264
+ start_time: datetime
265
+ end_time: Optional[datetime] = None
266
+ skills_updated: Dict[str, float] = {}
267
+
268
+ class SyncPayload(BaseModel):
269
+ device_id: str
270
+ sessions: List[LearningSessionSchema]
271
+ responses: List[StudentResponseSchema]
272
+ last_sync_time: datetime
273
+
274
+ class SyncResponse(BaseModel):
275
+ success: bool
276
+ synced_sessions: int
277
+ synced_responses: int
278
+ new_content_available: bool
279
+ content_version: Optional[str] = None
280
+
281
+ # app/schemas/student.py
282
+ from pydantic import BaseModel
283
+ from typing import Dict, Optional
284
+ from datetime import datetime
285
+
286
+ class StudentCreate(BaseModel):
287
+ name: str
288
+ grade: int
289
+ preferred_language: str = "urdu"
290
+
291
+ class StudentResponse(BaseModel):
292
+ id: str
293
+ name: str
294
+ grade: int
295
+ preferred_language: str
296
+ skill_mastery: Dict[str, float]
297
+ created_at: datetime
298
+ updated_at: datetime
299
+
300
+ class Config:
301
+ from_attributes = True
302
+
303
+ # app/routers/auth.py
304
+ from fastapi import APIRouter, Depends, HTTPException, status
305
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
306
+ from sqlalchemy.orm import Session
307
+ from datetime import datetime, timedelta
308
+ from typing import Optional
309
+ import jwt
310
+ from passlib.context import CryptContext
311
+
312
+ from app.database import get_db
313
+ from app.models.database import Teacher
314
+ from app.core.config import settings
315
+ from pydantic import BaseModel
316
+
317
+ router = APIRouter()
318
+ security = HTTPBearer()
319
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
320
+
321
+ class LoginRequest(BaseModel):
322
+ email: str
323
+ password: str
324
+
325
+ class TokenResponse(BaseModel):
326
+ access_token: str
327
+ token_type: str = "bearer"
328
+
329
+ def verify_password(plain_password, hashed_password):
330
+ return pwd_context.verify(plain_password, hashed_password)
331
+
332
+ def get_password_hash(password):
333
+ return pwd_context.hash(password)
334
+
335
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
336
+ to_encode = data.copy()
337
+ if expires_delta:
338
+ expire = datetime.utcnow() + expires_delta
339
+ else:
340
+ expire = datetime.utcnow() + timedelta(minutes=15)
341
+ to_encode.update({"exp": expire})
342
+ encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256")
343
+ return encoded_jwt
344
+
345
+ def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
346
+ try:
347
+ payload = jwt.decode(credentials.credentials, settings.SECRET_KEY, algorithms=["HS256"])
348
+ email: str = payload.get("sub")
349
+ if email is None:
350
+ raise HTTPException(
351
+ status_code=status.HTTP_401_UNAUTHORIZED,
352
+ detail="Invalid authentication credentials",
353
+ headers={"WWW-Authenticate": "Bearer"},
354
+ )
355
+ return email
356
+ except jwt.PyJWTError:
357
+ raise HTTPException(
358
+ status_code=status.HTTP_401_UNAUTHORIZED,
359
+ detail="Invalid authentication credentials",
360
+ headers={"WWW-Authenticate": "Bearer"},
361
+ )
362
+
363
+ @router.post("/login", response_model=TokenResponse)
364
+ async def login(request: LoginRequest, db: Session = Depends(get_db)):
365
+ teacher = db.query(Teacher).filter(Teacher.email == request.email).first()
366
+ if not teacher or not verify_password(request.password, teacher.hashed_password):
367
+ raise HTTPException(
368
+ status_code=status.HTTP_401_UNAUTHORIZED,
369
+ detail="Incorrect email or password",
370
+ headers={"WWW-Authenticate": "Bearer"},
371
+ )
372
+
373
+ access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
374
+ access_token = create_access_token(
375
+ data={"sub": teacher.email}, expires_delta=access_token_expires
376
+ )
377
+ return {"access_token": access_token, "token_type": "bearer"}
378
+
379
+ @router.get("/me")
380
+ async def get_current_user(email: str = Depends(verify_token), db: Session = Depends(get_db)):
381
+ teacher = db.query(Teacher).filter(Teacher.email == email).first()
382
+ if not teacher:
383
+ raise HTTPException(status_code=404, detail="Teacher not found")
384
+ return {
385
+ "id": teacher.id,
386
+ "email": teacher.email,
387
+ "name": teacher.name,
388
+ "school": teacher.school,
389
+ "subjects": teacher.subjects,
390
+ "grades": teacher.grades
391
  }
392
+
393
+ # app/routers/sync.py
394
+ from fastapi import APIRouter, Depends, HTTPException
395
+ from sqlalchemy.orm import Session
396
+ from sqlalchemy import and_
397
+ from datetime import datetime
398
+ from typing import List
399
+
400
+ from app.database import get_db
401
+ from app.models.database import Student, LearningSession, StudentResponse, Question
402
+ from app.schemas.sync import SyncPayload, SyncResponse
403
+ from app.routers.auth import verify_token
404
+
405
+ router = APIRouter()
406
+
407
+ @router.post("/push", response_model=SyncResponse)
408
+ async def sync_push(
409
+ payload: SyncPayload,
410
+ db: Session = Depends(get_db),
411
+ current_user: str = Depends(verify_token)
412
+ ):
413
+ """
414
+ Receive and process synced data from mobile devices
415
+ """
416
  try:
417
+ synced_sessions = 0
418
+ synced_responses = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
 
420
+ # Process learning sessions
421
+ for session_data in payload.sessions:
422
+ # Check if session already exists
423
+ existing_session = db.query(LearningSession).filter(
424
+ LearningSession.id == session_data.id
425
+ ).first()
426
+
427
+ if not existing_session:
428
+ # Create new session
429
+ new_session = LearningSession(
430
+ id=session_data.id,
431
+ student_id=session_data.student_id,
432
+ subject=session_data.subject,
433
+ start_time=session_data.start_time,
434
+ end_time=session_data.end_time,
435
+ skills_updated=session_data.skills_updated,
436
+ device_id=payload.device_id,
437
+ synced_at=datetime.utcnow()
438
+ )
439
+ db.add(new_session)
440
+ synced_sessions += 1
441
 
442
+ # Process student responses
443
+ for response_data in payload.responses:
444
+ # Check if response already exists
445
+ existing_response = db.query(StudentResponse).filter(
446
+ StudentResponse.id == response_data.id
447
+ ).first()
448
+
449
+ if not existing_response:
450
+ # Create new response
451
+ new_response = StudentResponse(
452
+ id=response_data.id,
453
+ student_id=response_data.student_id,
454
+ question_id=response_data.question_id,
455
+ session_id=response_data.session_id,
456
+ response=response_data.response,
457
+ response_type=response_data.response_type,
458
+ is_correct=response_data.is_correct,
459
+ partial_credit=response_data.partial_credit,
460
+ confidence=response_data.confidence,
461
+ timestamp=response_data.timestamp,
462
+ synced_at=datetime.utcnow()
463
+ )
464
+ db.add(new_response)
465
+ synced_responses += 1
466
 
467
+ # Commit all changes
468
+ db.commit()
 
 
469
 
470
+ # Check for new content
471
+ # For now, assume no new content available
472
+ new_content_available = False
473
+ content_version = None
 
474
 
475
+ return SyncResponse(
476
+ success=True,
477
+ synced_sessions=synced_sessions,
478
+ synced_responses=synced_responses,
479
+ new_content_available=new_content_available,
480
+ content_version=content_version
481
+ )
482
+
483
+ except Exception as e:
484
+ db.rollback()
485
+ raise HTTPException(status_code=500, detail=f"Sync failed: {str(e)}")
486
+
487
+ @router.get("/status/{device_id}")
488
+ async def sync_status(
489
+ device_id: str,
490
+ db: Session = Depends(get_db)
491
+ ):
492
+ """
493
+ Get sync status for a device
494
+ """
495
+ # Count unsynced items for this device
496
+ unsynced_sessions = db.query(LearningSession).filter(
497
+ and_(
498
+ LearningSession.device_id == device_id,
499
+ LearningSession.synced_at.is_(None)
500
+ )
501
+ ).count()
502
+
503
+ unsynced_responses = db.query(StudentResponse).filter(
504
+ and_(
505
+ StudentResponse.synced_at.is_(None)
506
+ )
507
+ ).count() # Note: StudentResponse doesn't have device_id, so checking all
508
 
509
+ return {
510
+ "device_id": device_id,
511
+ "unsynced_sessions": unsynced_sessions,
512
+ "unsynced_responses": unsynced_responses,
513
+ "last_sync": datetime.utcnow().isoformat()
514
+ }
515
+
516
+ # app/routers/content.py
517
+ from fastapi import APIRouter, Depends, HTTPException, Query
518
+ from sqlalchemy.orm import Session
519
+ from typing import List, Optional
520
+
521
+ from app.database import get_db
522
+ from app.models.database import Question, ContentBundle
523
+ from app.routers.auth import verify_token
524
+
525
+ router = APIRouter()
526
+
527
+ @router.get("/questions")
528
+ async def get_questions(
529
+ grade: Optional[int] = Query(None, description="Grade level"),
530
+ subject: Optional[str] = Query(None, description="Subject"),
531
+ language: Optional[str] = Query("urdu", description="Language"),
532
+ limit: int = Query(50, description="Number of questions to return"),
533
+ offset: int = Query(0, description="Offset for pagination"),
534
+ db: Session = Depends(get_db)
535
+ ):
536
+ """
537
+ Get questions filtered by grade, subject, and language
538
+ """
539
+ query = db.query(Question)
540
 
541
+ if grade:
542
+ query = query.filter(Question.grade == grade)
543
+ if subject:
544
+ query = query.filter(Question.subject == subject)
545
+
546
+ questions = query.offset(offset).limit(limit).all()
547
+
548
+ # Filter questions that have content in the requested language
549
+ filtered_questions = []
550
+ for question in questions:
551
+ if language in question.prompt:
552
+ question_data = {
553
+ "id": question.id,
554
+ "subject": question.subject,
555
+ "topic": question.topic,
556
+ "grade": question.grade,
557
+ "skill_tags": question.skill_tags,
558
+ "difficulty_estimate": question.difficulty_estimate,
559
+ "prompt": question.prompt.get(language, question.prompt.get('english', '')),
560
+ "options": question.options,
561
+ "solution_steps": question.solution_steps,
562
+ "hints": question.hints.get(language, question.hints.get('english', [])),
563
+ "answer_patterns": question.answer_patterns,
564
+ "explanation": question.explanation.get(language, question.explanation.get('english', ''))
565
+ }
566
+ filtered_questions.append(question_data)
567
+
568
+ return {
569
+ "questions": filtered_questions,
570
+ "total": len(filtered_questions),
571
+ "grade": grade,
572
+ "subject": subject,
573
+ "language": language
574
+ }
575
+
576
+ @router.get("/bundle")
577
+ async def get_content_bundle(
578
+ version: Optional[str] = Query("latest", description="Content version"),
579
+ grade: Optional[int] = Query(None, description="Grade level"),
580
+ subject: Optional[str] = Query(None, description="Subject"),
581
+ language: str = Query("urdu", description="Language"),
582
+ db: Session = Depends(get_db)
583
+ ):
584
+ """
585
+ Get content bundle for offline use
586
+ """
587
+ query = db.query(ContentBundle)
588
+
589
+ if version != "latest":
590
+ query = query.filter(ContentBundle.version == version)
591
+ if grade:
592
+ query = query.filter(ContentBundle.grade == grade)
593
+ if subject:
594
+ query = query.filter(ContentBundle.subject == subject)
595
+ if language:
596
+ query = query.filter(ContentBundle.language == language)
597
+
598
+ # Get the latest version if "latest" is requested
599
+ if version == "latest":
600
+ bundle = query.order_by(ContentBundle.created_at.desc()).first()
601
+ else:
602
+ bundle = query.first()
603
+
604
+ if not bundle:
605
+ raise HTTPException(status_code=404, detail="Content bundle not found")
606
+
607
+ return {
608
+ "id": bundle.id,
609
+ "version": bundle.version,
610
+ "grade": bundle.grade,
611
+ "subject": bundle.subject,
612
+ "language": bundle.language,
613
+ "file_path": bundle.file_path,
614
+ "file_size": bundle.file_size,
615
+ "checksum": bundle.checksum,
616
+ "created_at": bundle.created_at
617
+ }
618
+
619
+ @router.post("/questions")
620
+ async def create_question(
621
+ question_data: dict,
622
+ current_user: str = Depends(verify_token),
623
+ db: Session = Depends(get_db)
624
+ ):
625
+ """
626
+ Create a new question (teacher only)
627
+ """
628
+ try:
629
+ new_question = Question(**question_data)
630
+ db.add(new_question)
631
+ db.commit()
632
+ db.refresh(new_question)
633
 
634
+ return {"message": "Question created successfully", "id": new_question.id}
635
+ except Exception as e:
636
+ db.rollback()
637
+ raise HTTPException(status_code=500, detail=f"Failed to create question: {str(e)}")
638
+
639
+ # app/routers/grading.py
640
+ from fastapi import APIRouter, Depends, HTTPException
641
+ from pydantic import BaseModel
642
+ from typing import Dict, Any
643
+ from app.ml.grading_engine import GradingEngine
644
+
645
+ router = APIRouter()
646
+
647
+ class GradingRequest(BaseModel):
648
+ student_response: str
649
+ question_id: str
650
+ answer_patterns: list
651
+ response_type: str = "text"
652
+ language: str = "urdu"
653
+
654
+ class GradingResult(BaseModel):
655
+ is_correct: bool
656
+ partial_credit: float
657
+ confidence: float
658
+ feedback: str
659
+
660
+ @router.post("/grade", response_model=GradingResult)
661
+ async def grade_response(request: GradingRequest):
662
+ """
663
+ Grade a student response using ML models
664
+ """
665
+ try:
666
+ grading_engine = GradingEngine()
667
+ result = await grading_engine.grade_response(
668
+ response=request.student_response,
669
+ answer_patterns=request.answer_patterns,
670
+ response_type=request.response_type,
671
+ language=request.language
672
  )
673
 
674
+ return GradingResult(
675
+ is_correct=result["is_correct"],
676
+ partial_credit=result["partial_credit"],
677
+ confidence=result["confidence"],
678
+ feedback=result.get("feedback", "")
679
+ )
 
 
 
 
 
 
680
 
681
+ except Exception as e:
682
+ raise HTTPException(status_code=500, detail=f"Grading failed: {str(e)}")
683
+
684
+ # app/routers/analytics.py
685
+ from fastapi import APIRouter, Depends, HTTPException, Query
686
+ from sqlalchemy.orm import Session
687
+ from sqlalchemy import func, and_
688
+ from datetime import datetime, timedelta
689
+ from typing import Optional, List
690
+
691
+ from app.database import get_db
692
+ from app.models.database import Student, StudentResponse, LearningSession, Question
693
+ from app.routers.auth import verify_token
694
+
695
+ router = APIRouter()
696
+
697
+ @router.get("/student/{student_id}/progress")
698
+ async def get_student_progress(
699
+ student_id: str,
700
+ days: int = Query(30, description="Number of days to analyze"),
701
+ db: Session = Depends(get_db),
702
+ current_user: str = Depends(verify_token)
703
+ ):
704
+ """
705
+ Get detailed progress analytics for a student
706
+ """
707
+ # Get student info
708
+ student = db.query(Student).filter(Student.id == student_id).first()
709
+ if not student:
710
+ raise HTTPException(status_code=404, detail="Student not found")
711
+
712
+ # Date range
713
+ end_date = datetime.utcnow()
714
+ start_date = end_date - timedelta(days=days)
715
+
716
+ # Get responses in date range
717
+ responses = db.query(StudentResponse).filter(
718
+ and_(
719
+ StudentResponse.student_id == student_id,
720
+ StudentResponse.timestamp >= start_date,
721
+ StudentResponse.timestamp <= end_date
722
+ )
723
+ ).all()
724
+
725
+ # Calculate metrics
726
+ total_responses = len(responses)
727
+ correct_responses = sum(1 for r in responses if r.is_correct)
728
+ accuracy = correct_responses / total_responses if total_responses > 0 else 0
729
+
730
+ # Subject-wise breakdown
731
+ subject_stats = {}
732
+ for response in responses:
733
+ question = db.query(Question).filter(Question.id == response.question_id).first()
734
+ if question:
735
+ subject = question.subject
736
+ if subject not in subject_stats:
737
+ subject_stats[subject] = {"total": 0, "correct": 0, "partial_credit": 0}
738
 
739
+ subject_stats[subject]["total"] += 1
740
+ if response.is_correct:
741
+ subject_stats[subject]["correct"] += 1
742
+ subject_stats[subject]["partial_credit"] += response.partial_credit
743
+
744
+ # Calculate subject accuracies
745
+ for subject in subject_stats:
746
+ stats = subject_stats[subject]
747
+ stats["accuracy"] = stats["correct"] / stats["total"] if stats["total"] > 0 else 0
748
+ stats["avg_partial_credit"] = stats["partial_credit"] / stats["total"] if stats["total"] > 0 else 0
749
+
750
+ # Skill mastery trends
751
+ skill_mastery = student.skill_mastery or {}
752
+
753
+ # Recent sessions
754
+ recent_sessions = db.query(LearningSession).filter(
755
+ and_(
756
+ LearningSession.student_id == student_id,
757
+ LearningSession.start_time >= start_date
758
+ )
759
+ ).order_by(LearningSession.start_time.desc()).limit(10).all()
760
+
761
+ session_data = []
762
+ for session in recent_sessions:
763
+ session_responses = [r for r in responses if r.session_id == session.id]
764
+ session_accuracy = sum(1 for r in session_responses if r.is_correct) / len(session_responses) if session_responses else 0
765
 
766
+ session_data.append({
767
+ "id": session.id,
768
+ "subject": session.subject,
769
+ "start_time": session.start_time,
770
+ "end_time": session.end_time,
771
+ "responses": len(session_responses),
772
+ "accuracy": session_accuracy
773
+ })
774
+
775
+ return {
776
+ "student": {
777
+ "id": student.id,
778
+ "name": student.name,
779
+ "grade": student.grade,
780
+ "preferred_language": student.preferred_language
781
+ },
782
+ "period": {
783
+ "days": days,
784
+ "start_date": start_date,
785
+ "end_date": end_date
786
+ },
787
+ "overall": {
788
+ "total_responses": total_responses,
789
+ "correct_responses": correct_responses,
790
+ "accuracy": accuracy,
791
+ "sessions": len(recent_sessions)
792
+ },
793
+ "by_subject": subject_stats,
794
+ "skill_mastery": skill_mastery,
795
+ "recent_sessions": session_data
796
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
797
 
798
+ @router.get("/class/overview")
799
+ async def get_class_overview(
800
+ grade: Optional[int] = Query(None, description="Grade to filter"),
801
+ days: int = Query(7, description="Number of days to analyze"),
802
+ db: Session = Depends(get_db),
803
+ current_user: str = Depends(verify_token)
804
+ ):
805
+ """
806
+ Get class-level analytics overview
807
+ """
808
+ # Date range
809
+ end_date = datetime.utcnow()
810
+ start_date = end_date - timedelta(days=days)
811
+
812
+ # Get students
813
+ students_query = db.query(Student)
814
+ if grade:
815
+ students_query = students_query.filter(Student.grade == grade)
816
+ students = students_query.all()
817
+
818
+ # Get recent responses for these students
819
+ student_ids = [s.id for s in students]
820
+ responses = db.query(StudentResponse).filter(
821
+ and_(
822
+ StudentResponse.student_id.in_(student_ids),
823
+ StudentResponse.timestamp >= start_date
824
+ )
825
+ ).all()
826
+
827
+ # Overall statistics
828
+ total_students = len(students)
829
+ active_students = len(set(r.student_id for r in responses))
830
+ total_responses = len(responses)
831
+ correct_responses = sum(1 for r in responses if r.is_correct)
832
+ overall_accuracy = correct_responses / total_responses if total_responses > 0 else 0
833
+
834
+ # Student performance breakdown
835
+ student_performance = []
836
+ for student in students:
837
+ student_responses = [r for r in responses if r.student_id == student.id]
838
+ if student_responses:
839
+ accuracy = sum(1 for r in student_responses if r.is_correct) / len(student_responses)
840
+ student_performance.appen